feat: complete WorkBuddy parity and Windows release workflow

This commit is contained in:
xiaohan.yuan committed 2026-10-18 12:45:21 +08:00
1 parent 143516b3a8
commit d5512df549
434 files changed
+54977 -1854

No files matched your search

+142
View File
@@ -0,0 +1,142 @@
import assert from 'node:assert/strict'
import { promises as fs } from 'node:fs'
import { execFileSync, spawn } from 'node:child_process'
import { randomBytes, randomUUID } from 'node:crypto'
import { fileURLToPath } from 'node:url'
import { join, resolve, relative, isAbsolute } from 'node:path'
import { configureSandbox } from '../src/main/sandbox/configuration'
import { gitHostAction, hostGitStatus } from '../src/main/sandbox/git-host-service'
import { effectiveSandboxPolicy, readSandboxPolicy, updateSandboxPolicy } from '../src/main/sandbox/policy-store'
import { hostGitRoot, loadHostGitBinding } from '../src/main/sandbox/git-host-source'
import { executeSandboxCommand } from '../src/main/sandbox/service'
import { executeTool } from '../src/main/agent/tools'
import { modeAllowsTool } from '../src/main/agent/mode-policy'
import { workspaceAction } from '../src/main/sandbox/workspace-service'
import { cleanupGitControls, createGitControl } from '../src/main/sandbox/git-control'
import { withSandboxExecution } from '../src/main/sandbox/execution'
import { PersistentWorkspace, workspaceKey } from '../src/main/sandbox/workspace'
if (process.argv[2] === '--worker-commit') {
configureSandbox({ stateRoot: process.argv[4], resourceDirectory: resolve('resources/sandbox/win32-x64') })
await gitHostAction('commit', { workspaceRoot: process.argv[3], sessionId: process.argv[5], approve: async () => true })
process.exit(0)
}
const fixture = await fs.mkdtemp(resolve('.tmp/sandbox-git-safety-')), workspace = join(fixture, 'project \u4e2d\u6587 ^'), stateRoot = join(fixture, 'state')
await fs.mkdir(workspace); await fs.mkdir(stateRoot)
configureSandbox({ stateRoot, resourceDirectory: resolve('resources/sandbox/win32-x64') })
const git = resolve('resources/sandbox/win32-x64/git/files/git.exe'), tool = resolve('resources/sandbox/win32-x64/git/files')
const env = { SystemRoot: process.env.SystemRoot, PATH: `${tool};${process.env.SystemRoot}\\System32`, GIT_CONFIG_NOSYSTEM: '1', GIT_CONFIG_GLOBAL: 'NUL', GIT_CONFIG_SYSTEM: 'NUL', GIT_TERMINAL_PROMPT: '0' }
const hostGit = (...args: string[]): string => execFileSync(git, ['-c', 'core.hooksPath=NUL', '-c', 'core.fsmonitor=false', '-c', 'user.name=Fixture', '-c', 'user.email=fixture@example.invalid', ...args], { cwd: workspace, env, encoding: 'utf8', windowsHide: true })
const context = { workspaceRoot: workspace, sessionId: 'safety-session', runId: 'safety-run', toolCallId: 'safety-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}`) }
const run = (command: string) => executeSandboxCommand(command, { ...context, runtimes: ['git'], workspaceMode: 'persistent' })
async function savedJson(path: string): Promise<any> { return fs.readFile(path, 'utf8').then(JSON.parse, error => { if (error.code === 'ENOENT') return null; throw error }) }
try {
hostGit('init', '-q', '--template=', '--initial-branch=main')
await fs.writeFile(join(workspace, 'file.txt'), 'base\n'); hostGit('add', '.'); hostGit('commit', '-q', '-m', 'base')
const rejected = await gitHostAction('import', { ...context, approve: async () => false })
check('denied history import creates no active host binding', rejected.writeback === 'rejected' && (await hostGitStatus(workspace, context.sessionId)).state === 'unimported')
let prompt = ''
await assert.rejects(gitHostAction('import', { ...context, approve: async title => { prompt = title; hostGit('update-ref', 'refs/heads/concurrent-import', 'HEAD'); return true } }), /changed while import/)
check('import approval is invalidated by an external reference change', prompt.includes('历史可能包含') && (await hostGitStatus(workspace, context.sessionId)).state === 'unimported')
await gitHostAction('import', context)
const priorRevision = (await workspaceAction('status', context)).revision!
let releaseQueue!: () => void, enteredQueue!: () => void
const queueEntered = new Promise<void>(done => { enteredQueue = done }), queueRelease = new Promise<void>(done => { releaseQueue = done })
const queuedMutation = withSandboxExecution(workspace, undefined, async () => {
enteredQueue(); await queueRelease
const policy = await effectiveSandboxPolicy(workspace), binding = await loadHostGitBinding(policy, context.sessionId)
const draft = await PersistentWorkspace.open(policy, context.sessionId, binding!.layout.projectIdentity)
try { await fs.writeFile(join(workspace, 'queued-external.txt'), 'queued-host-edit\n'); await draft.synchronize() } finally { await draft.close() }
})
await queueEntered
const staleDiscard = workspaceAction('discard', context, priorRevision)
const staleDiscardCheck = assert.rejects(staleDiscard, /版本已变化/)
await new Promise(done => setTimeout(done, 100)); releaseQueue(); await queuedMutation; await staleDiscardCheck
check('workspace actions recheck their expected revision after waiting in the shared Git queue', (await workspaceAction('status', context)).revision !== priorRevision)
const nativeTool = await executeTool('sandbox_git', { action: 'run', argv: ['status', '--short'] }, { ...context, permissionMode: 'full-access' })
check('explicit Git tool keeps native confinement in a full-access context', nativeTool.ok && (nativeTool.data as { isolated: boolean }).isolated)
check('model tool rejects forged project/session ownership', !(await executeTool('sandbox_git', { action: 'status', workspaceRoot: fixture, sessionId: 'other' }, context)).ok)
check('planning mode cannot mutate or import Git state', !modeAllowsTool('plan', 'sandbox_git') && !modeAllowsTool('ask', 'sandbox_git'))
const defaultResult = await executeSandboxCommand('git log -1 --oneline', { ...context, runtimes: ['git'] })
check('ordinary Git runtime calls select the imported session mirror after import', defaultResult.ok && defaultResult.data.git?.mode === 'host-mirror' && defaultResult.data.workspace?.mode === 'persistent')
await run('echo pending>file.txt && git add file.txt && git commit -q -m pending')
const original = hostGit('rev-parse', 'HEAD').trim()
await assert.rejects(gitHostAction('commit', { ...context, approve: async () => { hostGit('update-ref', 'refs/heads/raced', 'HEAD'); return true } }), /changed while commit approval/)
check('host metadata edits during commit approval prevent every host file write', hostGit('rev-parse', 'HEAD').trim() === original && (await fs.readFile(join(workspace, 'file.txt'), 'utf8')) === 'base\n')
const refreshed = await executeSandboxCommand('git show-ref raced && git log -1 --oneline', { ...context, runtimes: ['git'], workspaceMode: 'persistent' })
check('independent external reference updates merge without losing private commits', refreshed.ok && refreshed.data.stdout.includes('raced') && refreshed.data.stdout.includes('pending'))
await assert.rejects(gitHostAction('commit', { ...context, approve: async () => { await fs.chmod(join(workspace, '.git/index'), 0o444); return true } }), /read.only|readonly|原生|Transaction/i)
await fs.chmod(join(workspace, '.git/index'), 0o666)
check('a metadata-phase failure compensates the already committed worktree phase', (await fs.readFile(join(workspace, 'file.txt'), 'utf8')) === 'base\n' && hostGit('rev-parse', 'HEAD').trim() === original)
const bindingRoot = hostGitRoot(workspace, context.sessionId)
check('compensated failure clears only its owned Git coordination locks', !await fs.lstat(join(workspace, '.git/index.lock')).catch(() => null) && !await fs.lstat(join(bindingRoot, 'commit.json')).catch(() => null))
const partial = await gitHostAction('commit', { ...context, approve: async () => {
await fs.rename(join(bindingRoot, 'binding.json'), join(bindingRoot, 'binding-before-fault.json')); await fs.mkdir(join(bindingRoot, 'binding.json')); return true
} })
check('private metadata failure after real host writes retains a committed outcome', partial.writeback === 'committed' && partial.state === 'interrupted' && hostGit('log', '-1', '--oneline').includes('pending') && (await fs.readFile(join(workspace, 'file.txt'), 'utf8')).includes('pending'))
const committedHead = hostGit('rev-parse', 'HEAD').trim(), beforeRecoveryTransactions = (await fs.readdir(join(stateRoot, 'transactions'))).length
await fs.rmdir(join(bindingRoot, 'binding.json')); await fs.rename(join(bindingRoot, 'binding-before-fault.json'), join(bindingRoot, 'binding.json'))
const recovered = await gitHostAction('recover', context)
check('group recovery finishes bookkeeping without replaying committed host mutations', recovered.writeback === 'committed' && recovered.state === 'ready' && hostGit('rev-parse', 'HEAD').trim() === committedHead && (await fs.readdir(join(stateRoot, 'transactions'))).length === beforeRecoveryTransactions)
await run('echo root-swap>file.txt && git add file.txt && git commit -q -m root-swap')
const backup = join(workspace, '.git-before-swap')
await assert.rejects(gitHostAction('commit', { ...context, approve: async () => { await fs.rename(join(workspace, '.git'), backup); await fs.cp(backup, join(workspace, '.git'), { recursive: true }); return true } }), /changed/)
check('same-content .git directory replacement invalidates the approved root identity', hostGit('rev-parse', 'HEAD').trim() === committedHead && (await fs.readFile(join(workspace, 'file.txt'), 'utf8')).includes('pending'))
const replacement = resolve(workspace, '.git'), rel = relative(fixture, replacement)
assert.ok(rel && !rel.startsWith('..') && !isAbsolute(rel)); await fs.rm(replacement, { recursive: true }); await fs.rename(backup, replacement)
await gitHostAction('discard', context)
await run('git commit --allow-empty -q -m cleanup-acknowledgement')
const workspaceLock = join(stateRoot, 'workspaces', workspaceKey(workspace, context.sessionId), 'workspace.lock')
const alteredLockNonce = randomUUID()
const cleanupFailure = await gitHostAction('commit', { ...context, approve: async () => { await fs.writeFile(workspaceLock, JSON.stringify({ pid: process.pid, nonce: alteredLockNonce }) + '\n'); return true } })
await fs.writeFile(join(fixture, 'cleanup-failure-result.json'), JSON.stringify(cleanupFailure, null, 2))
check('a lock cleanup failure never replaces an already committed host outcome', cleanupFailure.writeback === 'committed' && cleanupFailure.state === 'interrupted' && /cleanup requires maintenance/.test(cleanupFailure.reason ?? '') && hostGit('log', '-1', '--oneline').includes('cleanup-acknowledgement'))
assert.equal((await savedJson(workspaceLock)).nonce, alteredLockNonce); await fs.unlink(workspaceLock)
for (let index = 0; index < 20; index++) await fs.writeFile(join(workspace, `large-${index}.bin`), randomBytes(512 * 1024))
await run('echo crash-draft>file.txt && git add . && git commit -q -m crash-draft && git gc --quiet')
const beforeCrashHead = hostGit('rev-parse', 'HEAD').trim(), beforeCrashBytes = await fs.readFile(join(workspace, 'file.txt'))
const worker = spawn(process.execPath, [fileURLToPath(import.meta.url), '--worker-commit', workspace, stateRoot, context.sessionId], { windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] })
let workerError = ''; worker.stderr.on('data', (data: Buffer) => { workerError = (workerError + data.toString()).slice(-4000) })
const closed = new Promise<void>((done, reject) => { worker.once('error', reject); worker.once('close', () => done()) })
let interrupted = false
for (let attempt = 0; attempt < 2500; attempt++) {
const group = await savedJson(join(bindingRoot, 'commit.json'))
if (group?.status === 'applying' && group.transactions?.worktree && !group.transactions.common) { interrupted = true; worker.kill(); break }
if (worker.exitCode !== null) break
await new Promise(done => setTimeout(done, 5))
}
if (!interrupted) { worker.kill(); await closed; throw new Error(`Did not observe the real mid-group interruption boundary: ${workerError}`) }
await closed
let crashRecovery: Awaited<ReturnType<typeof gitHostAction>> | undefined
for (let attempt = 0; attempt < 50; attempt++) {
try { crashRecovery = await gitHostAction('recover', context); break }
catch (error) { if (!/active|正在|共享|sharing|win32=32/i.test((error as Error).message) || attempt === 49) throw error; await new Promise(done => setTimeout(done, 50)) }
}
check('actual host-process death between native phases recovers a determinate outcome', !!crashRecovery && ['not-applied', 'committed'].includes(crashRecovery.writeback ?? ''))
if (crashRecovery?.writeback === 'not-applied') check('interrupted multi-root publication restores the original host files and refs', hostGit('rev-parse', 'HEAD').trim() === beforeCrashHead && (await fs.readFile(join(workspace, 'file.txt'))).equals(beforeCrashBytes))
else check('an already completed group is acknowledged without compensating its committed state', hostGit('log', '-1', '--oneline').includes('crash-draft'))
check('recovered private Git history remains available for explicit retry or inspection', (await run('git log -1 --oneline')).data.stdout.includes('crash-draft'))
const cleanup = await cleanupGitControls()
check('Git maintenance removes only abandoned controls after verifying owner/native lifetimes', cleanup.some(record => record.state === 'removed'))
const liveControl = await createGitControl(await effectiveSandboxPolicy(workspace))
try { check('maintenance preserves a live owned Git control', (await cleanupGitControls()).some(record => record.id === liveControl.root.split(/[\\/]/).at(-1) && record.state === 'active')) } finally { await liveControl.close() }
if (crashRecovery?.writeback === 'not-applied') await gitHostAction('commit', context)
check('a real pack larger than the ordinary 8 MiB file ceiling publishes through the Git metadata channel', hostGit('log', '-1', '--oneline').includes('crash-draft') && (await fs.readdir(join(workspace, '.git/objects/pack'))).some(name => name.endsWith('.pack') && name.startsWith('pack-')) && hostGit('fsck', '--connectivity-only').trim() === '')
const foreignLock = join(workspace, '.git/index.lock'); await fs.writeFile(foreignLock, 'foreign-lock')
await assert.rejects(run('git status --short'), /busy|lock/)
check('a foreign Git lock is never consumed or removed by refresh', (await fs.readFile(foreignLock, 'utf8')) === 'foreign-lock')
await fs.unlink(foreignLock)
const policy = await readSandboxPolicy(); await updateSandboxPolicy({ expectedRevision: policy.revision, rules: { deniedPaths: ['secret.txt'] } })
check('policy revocation makes the full history import stale', (await hostGitStatus(workspace, context.sessionId)).state === 'stale')
await assert.rejects(run('git log -1'), /策略|policy/)
const discarded = await workspaceAction('discard', context)
check('discard under a new policy archives the old full-history grant without silently reimporting it', discarded.writeback === 'not-applied' && (await hostGitStatus(workspace, context.sessionId)).state === 'unimported')
const currentPolicy = await effectiveSandboxPolicy(workspace)
check('the active policy remains in force after Git recovery and discard', currentPolicy.deniedPaths.includes('secret.txt'))
console.log(`SANDBOX_GIT_SAFETY_PASS ${checks.length}/${checks.length}`)
} finally {
await fs.writeFile(join(fixture, 'report.json'), JSON.stringify({ fixture, checks }, null, 2) + '\n')
console.log(`SANDBOX_GIT_SAFETY_REPORT ${join(fixture, 'report.json')}`)
}