/** * Unit tests for the isolation policy, the two seam patches, and the state * cache. Everything runs against a real temporary directory and fake seams, so * no DSH process is needed. */ import { test } from 'node:test' import assert from 'node:assert/strict' import { mkdtemp, mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { allocateSessionDir, containingRoots, isIsolated, owningRoot, setIsolated, MARKER_NAME, } from '../lib/isolate.js' import { createIsolationState } from '../lib/state.js' import { isolatedSessionsApi, patchRegistry, patchSessionCreate } from '../lib/patch.js' import { LIST_ROUTE, TOGGLE_ROUTE, registerWebRoutes } from '../lib/web.js' /** One fresh temporary project directory per test. */ async function stage() { return await mkdtemp(join(tmpdir(), 'dsh-isolation-')) } test('the marker file is the durable toggle', async () => { const root = await stage() try { assert.equal(await isIsolated(root), false) assert.equal(await setIsolated(root, true), true) assert.equal(await isIsolated(root), true) assert.match(await readFile(join(root, MARKER_NAME), 'utf8'), /per-session working directories/) assert.equal(await setIsolated(root, false), false) assert.equal(await isIsolated(root), false) } finally { await rm(root, { recursive: true, force: true }) } }) test('a missing directory reads as not isolated instead of throwing', async () => { const root = await stage() try { assert.equal(await isIsolated(join(root, 'gone')), false) } finally { await rm(root, { recursive: true, force: true }) } }) test('containingRoots picks the nearest marked ancestor', async () => { const outer = await stage() const inner = join(outer, 'packages', 'app') await mkdir(inner, { recursive: true }) try { const roots = [outer, inner] assert.equal(owningRoot(roots, join(inner, 'session-abc')), inner) assert.equal(owningRoot(roots, join(outer, 'other')), outer) assert.equal(owningRoot(roots, tmpdir()), undefined) assert.deepEqual(containingRoots(roots, join(inner, 'x')), [inner, outer]) } finally { await rm(outer, { recursive: true, force: true }) } }) test('a sibling directory with a shared prefix is not treated as contained', async () => { const parent = await stage() const root = join(parent, 'app') const sibling = join(parent, 'app-other') await mkdir(root, { recursive: true }) await mkdir(sibling, { recursive: true }) try { assert.equal(owningRoot([root], sibling), undefined) assert.equal(owningRoot([root], join(sibling, 'deep')), undefined) } finally { await rm(parent, { recursive: true, force: true }) } }) test('allocateSessionDir creates a unique session- folder', async () => { const root = await stage() try { const first = await allocateSessionDir(root, 'session-01234567-89ab-cdef-0123-456789abcdef') assert.equal(first, join(root, 'session-01234567')) assert.equal((await stat(first)).isDirectory(), true) // The same session id twice cannot reuse the folder, so the next session // with a colliding short id gets a suffixed sibling instead of sharing. const second = await allocateSessionDir(root, 'session-01234567-ffff-ffff-ffff-ffffffffffff') assert.equal(second, join(root, 'session-01234567-2')) assert.equal((await stat(second)).isDirectory(), true) } finally { await rm(root, { recursive: true, force: true }) } }) test('allocateSessionDir survives an id without a usable prefix', async () => { const root = await stage() try { const created = await allocateSessionDir(root, 'session-') assert.equal(created, join(root, 'session-unknown')) } finally { await rm(root, { recursive: true, force: true }) } }) /** * A minimal stand-in for the live Workspace registry: the same private * canonical-cwd index the real registry keeps, so the patch is exercised on the * real mechanism (index lookup) rather than a stub. */ function fakeRegistry(paths) { const records = new Map(paths.map((path, index) => [ `w${index}`, { id: `w${index}`, path, title: `w${index}`, sessionIds: [] }, ])) const sessionPaths = new Map() // The real registry builds ONE host object and hands it to every entity, and // the entities — not the registry object — are what read the index. const host = { sessionPath: id => sessionPaths.get(id), rememberSessionPath: (id, path) => { sessionPaths.set(id, path) }, } const entities = new Map([...records.values()].map(record => [ record.id, { id: record.id, path: record.path, host, record: { ...record, sessionIds: [] } }, ])) return { host, entities, /** The registry's own identity, for direct index assertions. */ index: sessionPaths, records, list: () => [...records.values()], get: id => records.get(id), } } test('the registry index maps isolated session directories onto the Workspace', async () => { const root = await stage() try { await setIsolated(root, true) const registry = fakeRegistry([root]) const iso = createIsolationState({ workspaceRegistry: registry }) await iso.scan() const undo = patchRegistry(registry, iso) const sessionDir = join(root, 'session-deadbeef') // The registry's own index is what the stock membership filter reads, so the // mapped path must be the Workspace root itself — never a descendant. registry.host.rememberSessionPath('session-x', sessionDir) assert.equal(registry.index.get('session-x'), root) assert.equal(registry.host.sessionPath('session-x'), root) // An unmapped session, and any non-isolated directory, stay untouched. registry.host.rememberSessionPath('session-y', '/elsewhere/project') assert.equal(registry.index.get('session-y'), '/elsewhere/project') undo() registry.host.rememberSessionPath('session-z', sessionDir) assert.equal(registry.index.get('session-z'), sessionDir) } finally { await rm(root, { recursive: true, force: true }) } }) test('the registry index leaves non-isolated directories alone', async () => { const root = await stage() try { const registry = fakeRegistry([root]) const iso = createIsolationState({ workspaceRegistry: registry }) await iso.scan() const undo = patchRegistry(registry, iso) registry.host.rememberSessionPath('session-abc', join(root, 'session-abc')) assert.equal(registry.host.sessionPath('session-abc'), join(root, 'session-abc')) undo() } finally { await rm(root, { recursive: true, force: true }) } }) test('the registry patch fails loud when the seam is missing', () => { assert.throws( () => patchRegistry({ entities: new Map() }, createIsolationState({ workspaceRegistry: {} })), /must be updated/, ) }) test('session creation gets a private folder in an isolating Workspace', async () => { const root = await stage() try { await setIsolated(root, true) const registry = fakeRegistry([root]) const iso = createIsolationState({ workspaceRegistry: registry }) await iso.scan() const seen = [] const controller = { async create(request) { // Mirrors the real controller: cwd comes from the named Workspace, and // a request carrying both workspaceId and cwd is rejected outright. assert.equal(request.cwd, undefined) const workspace = registry.get(request.workspaceId) seen.push({ ...request, cwd: workspace?.path }) // The real implementation stamps that cwd into the immutable // SessionHeader, so the folder has to exist by now. assert.equal((await stat(workspace.path)).isDirectory(), true) return { sessionId: request.sessionId ?? 'session-new' } }, } const undo = patchSessionCreate({ sessionController: controller, workspaceRegistry: registry }, iso) const value = await controller.create({ workspaceId: 'w0', sessionId: 'session-abcdefgh-0000' }) assert.equal(value.sessionId, 'session-abcdefgh-0000') assert.equal(seen.length, 1) assert.equal(seen[0].workspaceId, 'w0') assert.equal(seen[0].cwd, join(root, 'session-abcdefgh')) assert.equal(seen[0].sessionId, 'session-abcdefgh-0000') // The redirect lasts exactly one call: the registry reads the real path again. assert.equal(registry.get('w0').path, root) undo() } finally { await rm(root, { recursive: true, force: true }) } }) test('session creation is untouched without the marker or without a Workspace', async () => { const root = await stage() try { const registry = fakeRegistry([root]) const iso = createIsolationState({ workspaceRegistry: registry }) await iso.scan() const seen = [] const controller = { async create(request) { seen.push(request); return { sessionId: 's' } } } const undo = patchSessionCreate({ sessionController: controller, workspaceRegistry: registry }, iso) await controller.create({ workspaceId: 'w0' }) await controller.create({ cwd: '/somewhere/else' }) assert.deepEqual(seen, [{ workspaceId: 'w0' }, { cwd: '/somewhere/else' }]) undo() } finally { await rm(root, { recursive: true, force: true }) } }) test('a failed creation removes the folder it allocated', async () => { const root = await stage() try { await setIsolated(root, true) const registry = fakeRegistry([root]) const iso = createIsolationState({ workspaceRegistry: registry }) await iso.scan() let allocated const controller = { async create(request) { allocated = registry.get(request.workspaceId).path throw new Error('creation refused') }, } const undo = patchSessionCreate({ sessionController: controller, workspaceRegistry: registry }, iso) await assert.rejects(controller.create({ workspaceId: 'w0', sessionId: 'session-fedcba98-1111' }), /creation refused/) await assert.rejects(stat(allocated), { code: 'ENOENT' }) undo() } finally { await rm(root, { recursive: true, force: true }) } }) test('the service reports and toggles isolation per Workspace', async () => { const root = await stage() try { const registry = fakeRegistry([root]) const iso = createIsolationState({ workspaceRegistry: registry }) const api = isolatedSessionsApi({ workspaceRegistry: registry }, iso) await iso.scan() assert.equal(api.isIsolated('w0'), false) assert.deepEqual(await api.setIsolated('w0', true), { workspaceId: 'w0', path: root, isolates: true }) assert.equal(api.isIsolated('w0'), true) assert.deepEqual(api.list(), [root]) assert.deepEqual(await api.setIsolated('w0', false), { workspaceId: 'w0', path: root, isolates: false }) assert.deepEqual(api.list(), []) await assert.rejects(api.setIsolated('nope', true), /unknown workspace/) } finally { await rm(root, { recursive: true, force: true }) } }) test('a marker written outside the plugin is picked up by the next scan', async () => { const root = await stage() try { const registry = fakeRegistry([root]) const iso = createIsolationState({ workspaceRegistry: registry }) await iso.scan() assert.deepEqual(iso.roots(), []) await writeFile(join(root, MARKER_NAME), '') await iso.scan() assert.deepEqual(iso.roots(), [root]) } finally { await rm(root, { recursive: true, force: true }) } }) /** A stand-in for `ctx.webServer`: records routes and lets a test drive them. */ function fakeWebServer() { const routes = new Map() return { routes, register(route) { routes.set(`${route.kind}:${route.path}`, route) return () => { routes.delete(`${route.kind}:${route.path}`) } }, } } /** A stand-in for a node:http response that records what the handler wrote. */ function fakeResponse() { return { status: undefined, headers: undefined, body: '', writeHead(status, headers) { this.status = status; this.headers = headers }, end(chunk) { if (chunk !== undefined) this.body += chunk }, } } /** A request body iterable for the POST route. */ function bodyRequest(method, payload) { const text = payload === undefined ? '' : JSON.stringify(payload) return { method, async *[Symbol.asyncIterator]() { if (text !== '') yield Buffer.from(text, 'utf8') }, } } test('the list route projects every workspace with its isolation state', async () => { const marked = await stage() const plain = await stage() try { await setIsolated(marked, true) const registry = fakeRegistry([marked, plain]) const iso = createIsolationState({ workspaceRegistry: registry }) await iso.scan() const web = fakeWebServer() const undo = registerWebRoutes({ workspaceRegistry: registry }, web, iso) const route = web.routes.get(`exact:${LIST_ROUTE}`) assert.notEqual(route, undefined) const res = fakeResponse() await route.handler({ method: 'GET' }, res) const payload = JSON.parse(res.body) assert.equal(payload.isolatedCount, 1) assert.deepEqual( payload.workspaces.map(entry => [entry.path, entry.isolates]).sort(), [[marked, true], [plain, false]].sort(), ) assert.equal(typeof payload.workspaces[0].title, 'string') assert.equal(payload.workspaces[0].sessionCount, 0) undo() assert.equal(web.routes.size, 0) } finally { await rm(marked, { recursive: true, force: true }) await rm(plain, { recursive: true, force: true }) } }) test('the toggle route flips one workspace and answers the refreshed list', async () => { const root = await stage() try { const registry = fakeRegistry([root]) const iso = createIsolationState({ workspaceRegistry: registry }) await iso.scan() const service = isolatedSessionsApi({ workspaceRegistry: registry }, iso) const web = fakeWebServer() const ctx = { workspaceRegistry: registry, webServer: web, isolatedSessions: service } const undo = registerWebRoutes(ctx, web, iso) const route = web.routes.get(`exact:${TOGGLE_ROUTE}`) const res = fakeResponse() await route.handler(bodyRequest('POST', { workspaceId: 'w0', enabled: true }), res) assert.equal(res.status, 200, res.body) assert.equal(JSON.parse(res.body).workspaces[0].isolates, true) assert.equal(await isIsolated(root), true) const off = fakeResponse() await route.handler(bodyRequest('POST', { workspaceId: 'w0', enabled: false }), off) assert.equal(JSON.parse(off.body).workspaces[0].isolates, false) assert.equal(await isIsolated(root), false) undo() } finally { await rm(root, { recursive: true, force: true }) } }) test('the toggle route refuses a wrong method, a bad body, and an unknown workspace', async () => { const root = await stage() try { const registry = fakeRegistry([root]) const iso = createIsolationState({ workspaceRegistry: registry }) await iso.scan() const service = isolatedSessionsApi({ workspaceRegistry: registry }, iso) const web = fakeWebServer() const undo = registerWebRoutes({ workspaceRegistry: registry, isolatedSessions: service }, web, iso) const route = web.routes.get(`exact:${TOGGLE_ROUTE}`) const wrongMethod = fakeResponse() await route.handler({ method: 'GET' }, wrongMethod) assert.equal(wrongMethod.status, 405) const badBody = fakeResponse() await route.handler(bodyRequest('POST', { workspaceId: 7 }), badBody) assert.equal(badBody.status, 400) const unknown = fakeResponse() await route.handler(bodyRequest('POST', { workspaceId: 'nope', enabled: true }), unknown) assert.equal(unknown.status, 500) assert.match(JSON.parse(unknown.body).error, /unknown workspace/) undo() } finally { await rm(root, { recursive: true, force: true }) } })