import assert from 'node:assert/strict' import { promises as fs } from 'node:fs' import { join, resolve } from 'node:path' import { configureSandbox } from '../src/main/sandbox/configuration' import { executeSandboxCommand } from '../src/main/sandbox/service' import { commitChanges, restoreTransaction, digest } from '../src/main/sandbox/transactions' if (process.platform !== 'win32') throw new Error('Directory integration requires real Windows native execution') await fs.mkdir(resolve('.tmp'), { recursive: true }) const fixture = await fs.mkdtemp(resolve('.tmp/sandbox-directories-')), workspace = join(fixture, 'workspace'), stateRoot = join(fixture, 'state') await fs.mkdir(workspace) configureSandbox({ stateRoot, resourceDirectory: resolve('resources/sandbox/win32-x64') }) const context = { workspaceRoot: workspace, sessionId: 'directory-session', runId: 'directory-run', toolCallId: 'directory-call', approve: async () => true } const checks: Array<{ label: string; pass: boolean }> = [] function check(label: string, condition: unknown): void { checks.push({ label, pass: !!condition }); assert.ok(condition, label); console.log(`PASS ${label}`) } try { let approvals = 0 let result = await executeSandboxCommand('mkdir empty\\nested', { ...context, approve: async () => { approvals++; return true } }) check('empty directory tree commits after one batch approval', result.ok && approvals === 1 && result.data.changes.filter(change => change.operation === 'mkdir').length === 2 && (await fs.stat(join(workspace, 'empty/nested'))).isDirectory()) const manifest = JSON.parse(await fs.readFile(join(stateRoot, 'transactions', result.data.transactionId!, 'manifest.json'), 'utf8')) check('directory transaction persists a versioned recovery journal', manifest.version === 3 && manifest.directories.length === 2 && /^[a-f0-9]{16}:[a-f0-9]{32}$/.test(manifest.workspaceIdentity)) await restoreTransaction(result.data.transactionId!, workspace); await restoreTransaction(result.data.transactionId!, workspace) check('directory creation recovery removes children and parents and is idempotent', !await fs.stat(join(workspace, 'empty')).catch(() => null)) result = await executeSandboxCommand('mkdir denied', { ...context, approve: async () => false }) check('directory creation rejection leaves no host directory', !result.ok && !await fs.stat(join(workspace, 'denied')).catch(() => null)) await fs.mkdir(join(workspace, 'original/sub'), { recursive: true }); await fs.writeFile(join(workspace, 'original/sub/data.bin'), Buffer.from([0, 1, 127, 255])) result = await executeSandboxCommand('rmdir /s /q original', context) check('recursive directory deletion records files and directories in one transaction', result.ok && result.data.changes.some(change => change.operation === 'delete') && result.data.changes.filter(change => change.operation === 'rmdir').length === 2 && !await fs.stat(join(workspace, 'original')).catch(() => null)) await restoreTransaction(result.data.transactionId!, workspace) check('recursive deletion recovery recreates original directories and exact binary bytes', (await fs.readFile(join(workspace, 'original/sub/data.bin'))).equals(Buffer.from([0, 1, 127, 255]))) result = await executeSandboxCommand('move original moved', context) check('directory rename is represented by complete transactional tree changes', result.ok && !await fs.stat(join(workspace, 'original')).catch(() => null) && (await fs.stat(join(workspace, 'moved/sub'))).isDirectory()) await restoreTransaction(result.data.transactionId!, workspace) check('directory rename restores its original tree and removes the new tree', !await fs.stat(join(workspace, 'moved')).catch(() => null) && (await fs.stat(join(workspace, 'original/sub'))).isDirectory()) const conflict = await executeSandboxCommand('rmdir /s /q original', { ...context, approve: async () => { await fs.writeFile(join(workspace, 'original/external.txt'), 'external'); return true } }).then(() => null, error => error as Error) check('approval-time additions reject the full directory deletion before mutation', !!conflict && (await fs.readFile(join(workspace, 'original/sub/data.bin'))).equals(Buffer.from([0, 1, 127, 255])) && await fs.readFile(join(workspace, 'original/external.txt'), 'utf8') === 'external') await fs.unlink(join(workspace, 'original/external.txt')) await fs.writeFile(join(workspace, 'original/.env'), 'DUMMY_CREDENTIAL') await assert.rejects(executeSandboxCommand('rmdir /s /q original', context), /未授权|新增内容/) check('excluded credentials inside a deleted directory are protected by host preflight', await fs.readFile(join(workspace, 'original/.env'), 'utf8') === 'DUMMY_CREDENTIAL' && (await fs.stat(join(workspace, 'original/sub/data.bin'))).isFile()) await fs.unlink(join(workspace, 'original/.env')) result = await executeSandboxCommand('mkdir restored-conflict & echo generated>restored-conflict\\file.txt', context) await fs.writeFile(join(workspace, 'restored-conflict/external.txt'), 'external') await assert.rejects(restoreTransaction(result.data.transactionId!, workspace), /未授权|新增内容/) check('directory recovery refuses to remove later external additions', (await fs.readFile(join(workspace, 'restored-conflict/file.txt'), 'utf8')).trim() === 'generated' && await fs.readFile(join(workspace, 'restored-conflict/external.txt'), 'utf8') === 'external') await fs.unlink(join(workspace, 'restored-conflict/external.txt')); await restoreTransaction(result.data.transactionId!, workspace) check('recovery succeeds after the external conflict is explicitly resolved', !await fs.stat(join(workspace, 'restored-conflict')).catch(() => null)) await fs.writeFile(join(workspace, 'sample.txt'), 'before') let guarded = 0 const failed = await commitChanges(workspace, [{ path: 'sample.txt', beforeHash: digest(Buffer.from('before')), after: Buffer.from('after') }], context, undefined, { beforeApply: async () => undefined, check: async () => { if (++guarded === 3) throw new Error('injected policy revocation') } }, [{ path: 'compensated-empty', before: false, after: true }]).then(() => null, error => error as Error & { recoveryRequired?: boolean }) check('mid-commit failure compensates both file writes and created directories', !!failed && !failed.recoveryRequired && await fs.readFile(join(workspace, 'sample.txt'), 'utf8') === 'before' && !await fs.stat(join(workspace, 'compensated-empty')).catch(() => null)) console.log(`SANDBOX_DIRECTORY_PASS ${checks.length}/${checks.length}`) } finally { await fs.writeFile(join(fixture, 'report.json'), JSON.stringify({ checks, fixture, limitations: ['Native NTFS transactions pin root/ancestors and use relative handle operations. Rename handoff is not a single atomic swap. ACL/timestamp parity, power-loss durability and Windows recycle-bin semantics remain pending.'] }, null, 2) + '\n') console.log(`SANDBOX_DIRECTORY_REPORT ${join(fixture, 'report.json')}`) }