Files
ycode/scripts/sandbox-git-complete-regression.ts
T

114 lines
12 KiB
TypeScript

import assert from 'node:assert/strict'
import { promises as fs } from 'node:fs'
import { execFileSync } from 'node:child_process'
import { join, resolve } from 'node:path'
import { configureSandbox } from '../src/main/sandbox/configuration'
import { effectiveSandboxPolicy } from '../src/main/sandbox/policy-store'
import { createGitControl } from '../src/main/sandbox/git-control'
import { captureHostGit } from '../src/main/sandbox/git-host-source'
import { gitHostAction, hostGitStatus } from '../src/main/sandbox/git-host-service'
import { executeSandboxCommand } from '../src/main/sandbox/service'
import { executeTool } from '../src/main/agent/tools'
import { workspaceAction } from '../src/main/sandbox/workspace-service'
const fixture = await fs.mkdtemp(resolve('.tmp/sandbox-git-complete-')), 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')
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}`) }
function hostGit(...args: string[]): string {
return execFileSync(git, ['-c', 'core.hooksPath=NUL', '-c', 'core.fsmonitor=false', '-c', 'commit.gpgSign=false', '-c', 'user.name=Fixture', '-c', 'user.email=fixture@example.invalid', ...args],
{ cwd: workspace, encoding: 'utf8', windowsHide: true, env: { SystemRoot: process.env.SystemRoot, PATH: `${resolve('resources/sandbox/win32-x64/git/files')};${process.env.SystemRoot}\\System32`, GIT_CONFIG_NOSYSTEM: '1', GIT_CONFIG_GLOBAL: 'NUL', GIT_CONFIG_SYSTEM: 'NUL', GIT_TERMINAL_PROMPT: '0' } })
}
let approvals = 0
const context = { workspaceRoot: workspace, sessionId: 'complete-session', runId: 'complete-run', toolCallId: 'complete-call', approve: async () => { approvals++; return true } }
const run = (command: string) => executeSandboxCommand(command, { ...context, runtimes: ['git'], workspaceMode: 'persistent', preserveGitConflicts: true })
async function gitRun(argv: string[]) {
const result = await executeTool('sandbox_git', { action: 'run', argv }, context)
await fs.writeFile(join(fixture, 'last-tool-result.json'), JSON.stringify(result, null, 2))
return result
}
try {
const policy = await effectiveSandboxPolicy(workspace), control = await createGitControl(policy)
try {
check('native parent-monitored host-only control runs the bundled Git', (await control.run(['--version'])).stdout.toString().includes('git version'))
hostGit('init', '-q', '--template=', '--initial-branch=main')
await fs.writeFile(join(workspace, 'tracked.txt'), 'initial\n')
hostGit('add', '.'); hostGit('commit', '-q', '-m', 'real-host-history')
await fs.writeFile(join(workspace, 'tracked.txt'), 'staged\n'); hostGit('add', 'tracked.txt')
await fs.writeFile(join(workspace, 'tracked.txt'), 'unstaged\n')
hostGit('config', 'credential.helper', '!echo SHOULD_NOT_RUN')
const snapshot = await captureHostGit(policy, control)
check('native metadata capture retains real HEAD, index and object history', snapshot.repository.has('HEAD') && snapshot.repository.has('index') && snapshot.files.some(file => file.path.startsWith('objects/')))
check('host executable configuration is excluded from the mirror', !snapshot.repository.get('config')!.toString().includes('SHOULD_NOT_RUN'))
} finally { await control.close() }
const imported = await gitHostAction('import', context)
check('explicit import is approval-bound and creates a session host mirror', imported.state === 'ready' && approvals === 1 && imported.pendingMetadata === false)
let result = await run('git log -1 --oneline && git diff --cached && git diff')
check('actual isolated Git sees host history and distinguishes staged and unstaged changes', result.ok && result.data.git?.mode === 'host-mirror' && result.data.stdout.includes('real-host-history') && result.data.stdout.includes('+staged') && result.data.stdout.includes('+unstaged'))
check('queries do not consume a host write approval', approvals === 1 && result.data.git?.persistence === 'unchanged')
result = await run('echo complete-local>tracked.txt && git add tracked.txt && git commit -q -m complete-local-commit && git branch feature && git tag v1')
check('private commits, branches and tags retain host ancestry without touching the host', result.ok && !hostGit('log', '-1', '--oneline').includes('complete-local-commit') && hostGit('diff').includes('+unstaged'))
const published = await gitHostAction('commit', context)
check('one approved group publishes worktree and actual host Git metadata', published.writeback === 'committed' && !!published.groupId && approvals === 2 && hostGit('log', '-1', '--oneline').includes('complete-local-commit') && hostGit('branch', '--list', 'feature').includes('feature') && hostGit('tag', '--list', 'v1').includes('v1'))
check('publication leaves the real host index and worktree coherent', hostGit('status', '--porcelain=v1').trim() === '' && (await fs.readFile(join(workspace, 'tracked.txt'), 'utf8')).includes('complete-local'))
check('host executable configuration survives publication without being imported', hostGit('config', '--get', 'credential.helper').includes('SHOULD_NOT_RUN'))
check('host status is bound to the imported session and clears metadata changes', (await hostGitStatus(workspace, context.sessionId)).pendingMetadata === false && (await hostGitStatus(workspace, 'another-session')).state === 'unimported')
const literalMessage = 'literal %PATH% & ^ "quoted"'
check('native argv executes Git directly without a command-shell expansion', (await gitRun(['commit', '--allow-empty', '-m', literalMessage])).ok && (await gitRun(['log', '-1', '--format=%s'])).content.includes(literalMessage))
check('branch switching retains the real host branch topology', (await gitRun(['switch', 'feature'])).ok)
await run('echo feature-change>feature.txt && git add feature.txt && git commit -q -m feature-change')
await gitRun(['switch', 'main']); await gitRun(['merge', '--no-edit', 'feature'])
check('a normal merge completes in the isolated draft', (await gitRun(['log', '--oneline', '-3'])).content.includes('feature-change'))
await run('echo scratch>stash.txt && git add stash.txt')
check('stash creation and restore persist across Jobs', (await gitRun(['stash', 'push', '-m', 'saved-stash'])).ok && (await gitRun(['stash', 'list'])).content.includes('saved-stash') && (await gitRun(['stash', 'pop'])).ok)
await gitRun(['reset', '--hard', 'HEAD'])
await gitRun(['switch', '-c', 'conflict-side'])
await run('echo side-value>tracked.txt && git add tracked.txt && git commit -q -m side-conflict')
await gitRun(['switch', 'main'])
await run('echo main-value>tracked.txt && git add tracked.txt && git commit -q -m main-conflict')
const merge = await gitRun(['merge', '--no-edit', 'conflict-side'])
check('a failed merge retains genuine conflict files and MERGE_HEAD for a later command', !merge.ok && (merge.data as { gitConflictState?: boolean }).gitConflictState && (await gitRun(['status', '--porcelain'])).content.includes('UU tracked.txt'))
await run('echo resolved-value>tracked.txt && git add tracked.txt')
check('conflict resolution commits with the owned noninteractive editor', (await gitRun(['commit', '--no-edit'])).ok)
await gitRun(['switch', '-c', 'cherry-source'])
await run('echo cherry>cherry.txt && git add cherry.txt && git commit -q -m cherry-source')
await gitRun(['switch', 'main'])
check('cherry-pick applies an existing real history commit', (await gitRun(['cherry-pick', 'cherry-source'])).ok && (await gitRun(['show', 'HEAD:cherry.txt'])).content.includes('cherry'))
check('revert makes a new inverse commit', (await gitRun(['revert', '--no-edit', 'HEAD'])).ok)
await gitRun(['switch', '-c', 'rebase-side'])
await run('echo rebase>rebase.txt && git add rebase.txt && git commit -q -m rebase-side')
await gitRun(['switch', 'main'])
await run('echo base>base.txt && git add base.txt && git commit -q -m rebase-base')
await gitRun(['switch', 'rebase-side'])
check('rebase rewrites a private branch onto the updated base', (await gitRun(['rebase', 'main'])).ok && (await gitRun(['log', '-3', '--oneline'])).content.includes('rebase-base'))
check('detached HEAD checkout and return survive command boundaries', (await gitRun(['switch', '--detach', 'HEAD~1'])).ok && (await gitRun(['status', '--short', '--branch'])).content.includes('HEAD') && (await gitRun(['switch', 'rebase-side'])).ok)
check('worktree reset retains coherent object history', (await gitRun(['reset', '--hard', 'HEAD~1'])).ok && (await gitRun(['fsck', '--connectivity-only'])).ok)
const beforeReject = hostGit('rev-parse', 'HEAD').trim()
const rejected = await gitHostAction('commit', { ...context, approve: async () => false })
check('rejecting the combined commit preserves host HEAD and private changes', rejected.writeback === 'rejected' && hostGit('rev-parse', 'HEAD').trim() === beforeReject && (await hostGitStatus(workspace, context.sessionId)).pendingMetadata)
const integrated = await workspaceAction('commit', context)
check('the common workspace commit routes imported Git through the same combined host transaction', integrated.writeback === 'committed' && !!integrated.git?.groupId && hostGit('branch', '--show-current').trim() === 'rebase-side')
const newProject = join(fixture, 'new-project'), newContext = { ...context, workspaceRoot: newProject, sessionId: 'new-session' }
await fs.mkdir(newProject); await fs.writeFile(join(newProject, 'new.txt'), 'new-host-file\n')
const initialized = await gitHostAction('init', newContext)
check('init creates an unborn private repository without a synthetic baseline or host .git', initialized.mode === 'new-repository' && !await fs.lstat(join(newProject, '.git')).catch(() => null))
const firstCommit = await executeTool('sandbox_git', { action: 'run', argv: ['add', '.'] }, newContext)
const firstHistory = await executeTool('sandbox_git', { action: 'run', argv: ['commit', '-m', 'first-real-commit'] }, newContext)
check('a new private repository stages existing project files and makes its first commit', firstCommit.ok && firstHistory.ok)
const firstPublish = await gitHostAction('commit', newContext)
check('first host publication creates and identity-binds .git before publishing the real first commit', firstPublish.writeback === 'committed' && !!firstPublish.groupId && (await fs.lstat(join(newProject, '.git/HEAD'))).isFile())
const linked = join(fixture, 'linked-worktree')
hostGit('worktree', 'add', '-q', '-b', 'linked-test', linked)
const linkedContext = { ...context, workspaceRoot: linked, sessionId: 'linked-session' }
check('linked worktree import binds both private and common Git directory identities', (await gitHostAction('import', linkedContext)).linkedWorktree === true)
await executeSandboxCommand('echo linked>linked.txt && git add linked.txt && git commit -q -m linked-commit', { ...linkedContext, runtimes: ['git'], workspaceMode: 'persistent' })
const linkedPublish = await gitHostAction('commit', linkedContext)
check('linked worktree publication commits its index/HEAD and shared objects/refs with separate native roots', linkedPublish.writeback === 'committed' && hostGit('log', '-1', '--oneline', 'linked-test').includes('linked-commit') && (await fs.readFile(join(linked, 'linked.txt'), 'utf8')).includes('linked'))
console.log(`SANDBOX_GIT_COMPLETE_PASS ${checks.length}/${checks.length}`)
} finally {
await fs.writeFile(join(fixture, 'report.json'), JSON.stringify({ fixture, checks }, null, 2) + '\n')
console.log(`SANDBOX_GIT_COMPLETE_REPORT ${join(fixture, 'report.json')}`)
}