const assert = require('node:assert/strict') const test = require('node:test') const fs = require('node:fs') const path = require('node:path') const vm = require('node:vm') const {cleanupMemoryFixture, memoryTables} = require('./verify_resource_isolation_live.cjs') const {validateResponse, trackCreatedResource, finishVerification} = require('./verify_access_control.cjs') const {cleanupMessageFixture} = require('./verify_resource_files_live.cjs') const prefix = 'resource-isolation-1758297600000' const fixture = Object.freeze({ id: '5b4af2da-04eb-4e76-b54d-cec380559428', username: `${prefix}-alice`, email: `${prefix}-alice@example.test`, }) test('memory cleanup restricts all six tables to the newly created account and verifies zero remaining rows', () => { const statements = [] const subject = cleanupMemoryFixture(fixture, prefix, statement => { statements.push(statement) return statements.length === 1 ? '' : '0|0|0|0|0|0' }) assert.equal(subject, `web_user:${fixture.id}`) assert.equal(statements.length, 2) assert.match(statements[0], /^BEGIN;/) assert.match(statements[0], /IF NOT EXISTS \(SELECT 1 FROM users/) for (const field of ['id', 'username', 'email']) assert.ok(statements[0].includes(`${field}='${fixture[field]}'`)) assert.ok(statements[0].indexOf('RAISE EXCEPTION') < statements[0].indexOf('DELETE FROM')) assert.match(statements[0], /END; \$fixture_cleanup\$;/) assert.equal((statements[0].match(/DELETE FROM /g) || []).length, 6) for (const table of ['memory_item_embeddings', 'memory_items', 'memory_tombstones', 'memory_topic_stats', 'memory_doc_affinity', 'memory_subjects']) { const scoped = `${table} WHERE tenant_id=10059 AND subject_id='web_user:${fixture.id}'` assert.ok(statements[0].includes(`DELETE FROM ${scoped};`)) assert.ok(statements[1].includes(`SELECT count(*) FROM ${scoped}`)) } assert.match(statements[0], /COMMIT;$/) assert.equal(memoryTables.length, 6) }) test('memory cleanup rejects business accounts, other runs, malformed IDs and mismatched email before any SQL', () => { const otherPrefix = 'resource-isolation-1758297500000' const rejected = [ [{...fixture, username: 'admin'}, prefix], [{...fixture, email: 'business@example.com'}, prefix], [{...fixture, id: "x'; DELETE FROM users; --"}, prefix], [fixture, otherPrefix], [fixture, 'resource-isolation-%'], ] for (const [value, runPrefix] of rejected) { let calls = 0 assert.throws(() => cleanupMemoryFixture(value, runPrefix, () => { calls++ })) assert.equal(calls, 0) } }) test('memory cleanup surfaces owner guard errors and residual rows as failures', () => { const statements = [] assert.throws(() => cleanupMemoryFixture(fixture, prefix, statement => { statements.push(statement) throw new Error('Memory cleanup account does not match this verification run') }), /does not match/) assert.equal(statements.length, 1) let calls = 0 assert.throws(() => cleanupMemoryFixture(fixture, prefix, () => ++calls === 1 ? '' : '0|1|0|0|0|0'), /left rows/) assert.equal(calls, 2) }) test('browser shutdown and cleanup failures still allow every cleanup action and a failed report', async () => { const events = [], failures = [], cleanup = [ async () => { events.push('first') }, async () => { events.push('second'); throw new Error('delete unavailable') }, async () => { events.push('third') }, ] let report await finishVerification({ browser: {close: async () => { events.push('close'); throw new Error('browser disconnected') }}, cleanup, failures, writeReport: () => { events.push('report'); report = {passed: failures.length === 0, failures: [...failures]} }, }) assert.deepEqual(events, ['close', 'third', 'second', 'first', 'report']) assert.equal(cleanup.length, 3) assert.deepEqual(report, {passed: false, failures: ['browser close: browser disconnected', 'cleanup: delete unavailable']}) }) test('failure before browser startup still cleans fixtures and writes the report', async () => { const events = [], failures = ['login failed'] await finishVerification({ cleanup: [() => events.push('fixture')], failures, writeReport: () => events.push(failures.length ? 'failed report' : 'passed report'), }) assert.deepEqual(events, ['fixture', 'failed report']) }) test('unexpected successful creation is registered before status assertion, including duplicate and fork envelopes', async () => { const cases = [ ['/access/roles', {id: 'new-role'}, 'new-role', 400], ['/workbench/resources/content-tasks', {id: 'new-task'}, 'new-task', 403], ['/agents', {id: 'new-agent'}, 'new-agent', 403], ['/knowledge-bases', {target_id: 'new-kb'}, 'new-kb', 403], ['/knowledge-bases', {knowledge_base: {id: 'new-nested-kb'}}, 'new-nested-kb', 403], ['/knowledge', {id: 'new-document'}, 'new-document', 403], ['/workbench/resources/flows', {id: 'fork-metadata', personal_id: 'new-flow'}, 'new-flow', 410], ] for (const [collection, data, expectedID, expectedStatus] of cases) { const cleanup = [], removed = [], statements = [] const track = trackCreatedResource(collection, ['original-resource'], cleanup, route => removed.push(route), statement => statements.push(statement)) assert.throws(() => validateResponse(collection, 'POST', {data}, 201, expectedStatus, track), /HTTP 201/) assert.equal(cleanup.length, collection==='/workbench/resources/flows' ? 2 : 1) await finishVerification({cleanup, failures: [], writeReport: () => {}}) assert.deepEqual(removed, [`${collection}/${expectedID}?detach=true`]) if(collection==='/workbench/resources/flows'){ assert.equal(statements.length, 1) assert.ok(statements[0].includes("WHERE tenant_id=10059 AND kind='flows' AND id='fork-metadata' AND personal_id='new-flow'")) assert.ok(statements[0].includes("WHERE tenant_id=10059 AND kind='flows' AND resource_id='new-flow'")) }else assert.deepEqual(statements, []) } }) test('expected denial never registers an existing resource from an error response', () => { const cleanup = [] validateResponse('/agents/original/copy', 'POST', {data: {id: 'original'}}, 403, [403,404], trackCreatedResource('/agents', [], cleanup)) assert.equal(cleanup.length, 0) }) test('unexpected creation refuses to delete the original or fork metadata instead of the created resource', () => { for (const [collection, result] of [ ['/agents', {data: {id: 'original'}}], ['/knowledge-bases', {data: {target_id: 'original'}}], ['/workbench/resources/flows', {data: {id: 'fork-metadata'}}], ]) { const cleanup = [] assert.throws(() => validateResponse(collection, 'POST', result, 200, 403, trackCreatedResource(collection, ['original'], cleanup)), /new resource ID/) assert.equal(cleanup.length, 0) } }) test('resource isolation main cleans both fixture memory scopes after a failure following memory creation', {timeout: 3000}, async () => { const events = [], userIDs = [fixture.id, 'fcf8b9eb-2af9-4267-9f8c-b288849f8310'] let createdUsers = 0, createdAgents = 0, memoryCreated = false, complete const reported = new Promise(resolve => { complete = resolve }) const verificationModule = {exports: {}} const fakeFS = { readFileSync: () => JSON.stringify({email: 'fake-admin@example.test', password: 'fake-test-password'}), writeFileSync: (_file, body) => complete(JSON.parse(body)), } const fakeRequire = name => { if(name==='node:fs')return fakeFS if(name==='node:child_process')return {execFileSync: (_command, args) => { const statement = args.at(-1) events.push({type: 'sql', statement}) return statement.startsWith('SELECT ') ? '0|0|0|0|0|0' : '' }} return require(name) } fakeRequire.main = verificationModule const context = vm.createContext({ module: verificationModule, require: fakeRequire, process: {env: {}}, console: {log() {}, error() {}}, FormData, Blob, AbortSignal, fetch: async (url, request) => { const route = new URL(url).pathname.replace('/api/v1', '') const method = request.method, body = request.body instanceof FormData ? {} : JSON.parse(request.body || '{}') events.push({type: 'http', route, method}) let status = 200, data = {} if(route==='/auth/login'){ const index = body.email.includes('-alice@') ? 0 : body.email.includes('-bob@') ? 1 : -1 data = {token: 'fake-token', user: {id: index<0 ? 'fake-admin' : userIDs[index]}} }else if(route==='/access/roles'&&method==='POST')data = {data: {id: 'fake-role', version: 1}} else if(route==='/access/users'&&method==='POST')data = {data: {...body, id: userIDs[createdUsers++]}} else if(route==='/knowledge-bases'&&method==='POST')data = {data: {id: 'fake-kb', creator_id: userIDs[0]}} else if(route==='/knowledge-bases'&&method==='GET')data = {data: []} else if(route==='/agents'&&method==='POST')data = {data: {id: `fake-agent-${++createdAgents}`}} else if(route.endsWith('/knowledge/manual'))data = {data: {id: 'fake-doc'}} else if(route.includes('/suggested-questions'))data = {data: []} else if(route==='/skills/instructions/import')data = {data: {id: 'fake-skill'}} else if(route==='/memory/items'&&method==='POST'){ memoryCreated = true data = {data: {id: 'fake-memory'}} }else if(route==='/memory/items'&&method==='GET')throw new Error('injected failure after memory creation') else if(method!=='DELETE'&&!route.startsWith('/access/'))status = 403 return {status, text: async () => JSON.stringify(data)} }, }) vm.runInContext(fs.readFileSync(path.join(__dirname, 'verify_resource_isolation_live.cjs'), 'utf8'), context) const report = await reported assert.ok(memoryCreated) assert.equal(report.passed, false) assert.ok(report.failures.includes('injected failure after memory creation')) assert.deepEqual(report.fixtures.memory_subjects, userIDs.map(id => `web_user:${id}`)) for(const id of userIDs){ const cleanupIndex = events.findIndex(event => event.type==='sql' && event.statement.includes('DELETE FROM memory_items') && event.statement.includes(`subject_id='web_user:${id}'`)) const userDeleteIndex = events.findIndex(event => event.type==='http' && event.route===`/access/users/${id}` && event.method==='DELETE') assert.ok(cleanupIndex>=0&&userDeleteIndex>cleanupIndex) assert.ok(report.checks.some(check => check.name===`memory_cleanup_${id}`&&check.passed)) } assert.ok(!events.some(event => event.type==='sql'&&event.statement.includes("subject_id='web_user:fake-admin'"))) }) const filePrefix = 'file-isolation-1758297600000' const messageFixture = Object.freeze({ tenant_id: 10059, session_id: 'a75ddc61-81e2-42f1-a870-656aac0a0931', user_id: fixture.id, username: `${filePrefix}-alice`, email: `${filePrefix}-alice@example.test`, title: filePrefix, messages: [ {id: '40c638f6-eafc-4305-85a6-993d4c4250e5', content: 'Disposable attachment fixture.'}, {id: '9a38599e-4d21-485b-8bfc-8bf24fa19b3f', content: 'Unrelated message.'}, ], }) test('message cleanup guards exact tenant, account, session and both synthetic message contents', () => { const statements = [] cleanupMessageFixture(messageFixture, filePrefix, statement => { statements.push(statement) return statements.length === 1 ? '' : '0' }) assert.equal(statements.length, 2) const transaction = statements[0] assert.match(transaction, /^BEGIN;/) assert.ok(transaction.includes(`SELECT 1 FROM users WHERE id='${messageFixture.user_id}' AND tenant_id=10059 AND username='${messageFixture.username}' AND email='${messageFixture.email}'`)) const sessionScope = `id='${messageFixture.session_id}' AND tenant_id=10059 AND user_id='${messageFixture.user_id}' AND title='${filePrefix}'` assert.ok(transaction.includes(`IF NOT EXISTS (SELECT 1 FROM sessions WHERE ${sessionScope})`)) for (const message of messageFixture.messages) { const scope = `id='${message.id}' AND session_id='${messageFixture.session_id}' AND role='assistant' AND content='${message.content}'` assert.ok(transaction.includes(`IF EXISTS (SELECT 1 FROM messages WHERE id='${message.id}') AND NOT EXISTS (SELECT 1 FROM messages WHERE ${scope})`)) assert.ok(transaction.includes(`DELETE FROM messages WHERE ${scope} AND EXISTS (SELECT 1 FROM sessions WHERE ${sessionScope});`)) assert.ok(statements[1].includes(`'${message.id}'`)) } assert.ok(transaction.lastIndexOf('RAISE EXCEPTION') < transaction.indexOf('DELETE FROM')) assert.equal((transaction.match(/DELETE FROM /g) || []).length, 2) assert.match(transaction, /END; \$message_cleanup\$;/) assert.match(transaction, /COMMIT;$/) }) test('message cleanup rejects business or mismatched fixture metadata without SQL', () => { const rejected = [ [{...messageFixture, tenant_id: 10060}, filePrefix], [{...messageFixture, username: 'admin'}, filePrefix], [{...messageFixture, email: 'business@example.com'}, filePrefix], [{...messageFixture, title: 'Business conversation'}, filePrefix], [{...messageFixture, session_id: "x'; DELETE FROM sessions; --"}, filePrefix], [{...messageFixture, user_id: 'admin'}, filePrefix], [{...messageFixture, messages: [{...messageFixture.messages[0], content: 'Real customer message'}, messageFixture.messages[1]]}, filePrefix], [{...messageFixture, messages: [messageFixture.messages[0], {...messageFixture.messages[1], id: messageFixture.messages[0].id}]}, filePrefix], [{...messageFixture, messages: [messageFixture.messages[0]]}, filePrefix], [messageFixture, 'file-isolation-1758297500000'], [messageFixture, 'file-isolation-%'], ] for (const [value, runPrefix] of rejected) { let calls = 0 assert.throws(() => cleanupMessageFixture(value, runPrefix, () => { calls++ })) assert.equal(calls, 0) } }) test('message cleanup propagates database guard failures and checks residual rows', () => { let calls = 0 assert.throws(() => cleanupMessageFixture(messageFixture, filePrefix, () => { calls++ throw new Error('Message cleanup session does not match this verification run') }), /does not match/) assert.equal(calls, 1) calls = 0 assert.throws(() => cleanupMessageFixture(messageFixture, filePrefix, () => ++calls === 1 ? '' : '1'), /left rows/) assert.equal(calls, 2) }) for (const failurePoint of ['message insert', 'message file check']) test(`file isolation cleans messages and reports exact fixture IDs after ${failurePoint} failure`, {timeout: 3000}, async () => { const events = [], userIDs = [fixture.id, 'fcf8b9eb-2af9-4267-9f8c-b288849f8310'] let createdUsers = 0, createdKBs = 0, grant = false, observedPrefix, complete const reported = new Promise(resolve => { complete = resolve }) const verificationModule = {exports: {}} const resourceFiles = new Map(), physicalFiles = new Map() const fakeFS = { readFileSync: () => JSON.stringify({email: 'fake-admin@example.test', password: 'fake-test-password'}), mkdirSync() {}, rmSync: file => events.push({type: 'rm', file}), writeFileSync: (file, body) => { if (file.endsWith('.json')) complete(JSON.parse(body)) else events.push({type: 'file', file, body}) }, } const fakeRequire = name => { if (name === 'node:fs') return fakeFS if (name === 'node:child_process') return {execFileSync: (_command, _args, options) => { const statement = options.input events.push({type: 'sql', statement}) if (statement.startsWith('INSERT INTO messages') && failurePoint === 'message insert') throw new Error('injected message insert failure') if (statement.startsWith('INSERT INTO resources')) { const values = statement.match(/VALUES \('[^']+','([^']+)',10059,'local','([^']+)'/) const label = values[2].split('/').at(-1).replace('.txt', '') const file = {label, marker: `${observedPrefix} ${label}`} resourceFiles.set(`resource://${values[1]}`, file) physicalFiles.set(values[2], file) } return statement.startsWith('SELECT count(*)') ? '0' : '' }} return require(name) } fakeRequire.main = verificationModule const context = vm.createContext({ module: verificationModule, require: fakeRequire, process: {env: {}}, console: {log() {}, error() {}}, AbortSignal, Buffer, fetch: async (url, request) => { const parsed = new URL(url), route = parsed.pathname.replace('/api/v1', '') const method = request.method, body = JSON.parse(request.body || '{}') const identity = request.headers.Authorization?.replace('Bearer ', '') events.push({type: 'http', route, method}) let status = 200, data = {}, responseText if (route === '/auth/login') { const index = body.email.includes('-alice@') ? 0 : body.email.includes('-bob@') ? 1 : -1 data = {token: index < 0 ? 'admin' : index === 0 ? 'alice' : 'bob', user: {id: index < 0 ? 'fake-admin' : userIDs[index]}} } else if (route === '/access/roles' && method === 'POST') { observedPrefix = body.name data = {data: {id: 'fake-role'}} } else if (route === '/access/users' && method === 'POST') data = {data: {...body, id: userIDs[createdUsers++]}} else if (route === '/knowledge-bases' && method === 'POST') data = {data: {id: `fake-kb-${++createdKBs}`}} else if (route.endsWith('/knowledge/manual')) data = {data: {id: 'fake-doc'}} else if (route === '/access/grants') grant = body.action === 'grant' else if (route === '/sessions' && method === 'POST') data = {data: {id: messageFixture.session_id}} else if (parsed.searchParams.has('file_path')) { const ref = parsed.searchParams.get('file_path') const file = resourceFiles.get(ref) || physicalFiles.get(ref) assert.ok(file) if (file.label === 'message' && failurePoint === 'message file check') throw new Error('injected message file check failure') const allowed = identity === 'admin' || identity === 'alice' || (identity === 'bob' && grant && file.label === 'knowledge') status = allowed ? 200 : 403 responseText = allowed ? file.marker : '{}' } else if (route.startsWith('/sessions') && identity === 'bob') status = 404 else if (method === 'DELETE') { status = 204; responseText = '' } return {status, text: async () => responseText ?? JSON.stringify(data), headers: {get: () => 'private, no-store'}} }, }) vm.runInContext(fs.readFileSync(path.join(__dirname, 'verify_resource_files_live.cjs'), 'utf8'), context) const report = await reported assert.equal(report.passed, false) assert.ok(report.failures.includes(`injected ${failurePoint} failure`), JSON.stringify(report.failures)) assert.deepEqual(report.fixtures.users, userIDs) assert.deepEqual(report.fixtures.sessions, [messageFixture.session_id]) assert.equal(report.fixtures.messages.length, 2) assert.equal(report.fixtures.message_scopes.length, 1) assert.equal(report.fixtures.message_scopes[0].session_id, messageFixture.session_id) assert.equal(report.fixtures.message_scopes[0].user_id, userIDs[0]) assert.deepEqual(report.fixtures.message_scopes[0].messages.map(message => message.id), report.fixtures.messages) const cleanupIndex = events.findIndex(event => event.type === 'sql' && event.statement.includes('DELETE FROM messages')) const sessionDeleteIndex = events.findIndex(event => event.type === 'http' && event.method === 'DELETE' && event.route === `/sessions/${messageFixture.session_id}` && events.indexOf(event) > cleanupIndex) const userDeleteIndex = events.findIndex(event => event.type === 'http' && event.method === 'DELETE' && event.route === `/access/users/${userIDs[0]}`) assert.ok(cleanupIndex >= 0 && sessionDeleteIndex > cleanupIndex && userDeleteIndex > sessionDeleteIndex) const statement = events[cleanupIndex].statement for (const id of report.fixtures.messages) assert.ok(statement.includes(`id='${id}'`)) assert.ok(report.checks.some(check => check.name === `message_cleanup_${messageFixture.session_id}` && check.passed)) })