// Real AGC CLI -> bundled app-server -> loopback Responses/MCP fixtures. // No account credentials, installed AppData, or paid Provider are used. import assert from 'node:assert/strict'; import { spawn, spawnSync } from 'node:child_process'; import { createHash } from 'node:crypto'; import fs from 'node:fs/promises'; import http from 'node:http'; import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const repo = path.resolve( path.dirname(fileURLToPath(import.meta.url)), '../../..', ); const args = process.argv.slice(2); const option = (name) => { const i = args.indexOf(name); return i < 0 ? undefined : args[i + 1]; }; const executable = option('--agc-exe'); assert( executable && path.isAbsolute(executable), 'pass --agc-exe with the newly built debug AGC binary', ); const codex = path.resolve( option('--codex-exe') ?? path.join( repo, 'node_modules/@openai/codex-win32-x64/vendor/x86_64-pc-windows-msvc/bin/codex.exe', ), ); const cases = ( option('--cases') ?? 'completed,passes,mcp,mcp-write,native,deadline' ).split(','); const root = await fs.mkdtemp( path.join(os.tmpdir(), 'agc-execution-production-'), ); console.log(JSON.stringify({ evidenceRoot: root })); const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const hash = (value) => createHash('sha256').update(value).digest('hex'); const exists = async (file) => fs.stat(file).then( () => true, () => false, ); const quote = (text) => process.platform === 'win32' ? "'" + text.replaceAll("'", "''") + "'" : "'" + text.replaceAll("'", "'\\''") + "'"; const nodeCommand = (source) => (process.platform === 'win32' ? '& ' : '') + quote(process.execPath) + ' -e ' + quote( process.platform === 'win32' ? // Windows PowerShell 5 removes nested double quotes from native argv. "eval(Buffer.from('" + Buffer.from(source).toString('base64') + "','base64').toString())" : source, ); const call = (id, name, arguments_, namespace) => ({ type: 'function_call', id: 'item-' + id, call_id: id, name, ...(namespace ? { namespace } : {}), arguments: JSON.stringify(arguments_), }); // Codex 0.155 起原生执行入口是统一 exec:`exec_command` + `write_stdin`(旧 `shell_command` // 已不再注册)。命令在 yield_time_ms 内结束时不返回 session,保持与旧用例同样的同步语义。 const native = (id, source) => call(id, 'exec_command', { cmd: nodeCommand(source), yield_time_ms: 30_000, }); const register = (artifact) => call( 'contract', 'agc_register_delivery_contract', { scope: '仅测试客户端执行许可,修改临时已有工程的标记文件,不生成素材或运行游戏。', changeKind: 'project', requirements: [{ kind: 'artifact', id: 'marker', path: artifact }], }, 'mcp__agc_tools', ); const final = { type: 'message', id: 'fixture-final', role: 'assistant', content: [ { type: 'output_text', text: 'Fixture actions finished; use the host delivery result.', }, ], }; // 工具回执正文既可能是字符串,也可能是 input_text 分片数组。 const toolOutputText = (value) => typeof value === 'string' ? value : Array.isArray(value) ? value.map((part) => part?.text ?? '').join('') : ''; // 原生工具的输入 schema 只随第一份请求回执留档一次,用于版本升级后核对参数形状。 const NATIVE_SCHEMA_TOOLS = new Set([ 'exec_command', 'write_stdin', 'shell_command', 'view_image', 'list_mcp_resources', 'list_mcp_resource_templates', 'read_mcp_resource', ]); const describeTool = (tool, withSchema = false) => ({ type: tool.type, name: tool.name ?? tool.function?.name, ...(tool.namespace ? { namespace: tool.namespace } : {}), ...(withSchema && tool.parameters ? { parameters: tool.parameters } : {}), ...(Array.isArray(tool.tools) ? { tools: tool.tools.map((entry) => describeTool(entry, false)) } : {}), }); async function walk(directory) { const output = []; for (const item of await fs .readdir(directory, { withFileTypes: true }) .catch(() => [])) { const file = path.join(directory, item.name); if (item.isDirectory()) output.push(...(await walk(file))); else if (item.isFile()) output.push(file); } return output; } async function readLedger(host) { for (const file of await walk(path.join(host, 'direct-executions'))) { if (!file.endsWith('.json')) continue; const value = await fs .readFile(file, 'utf8') .then(JSON.parse) .catch(() => null); if (value?.schemaVersion === 'agc-direct-execution.v1') return { file, value }; } return null; } function ownFixtureTree(directory) { if (process.platform !== 'win32') return; assert(path.resolve(directory).startsWith(path.resolve(root) + path.sep)); // Elevated Windows shells otherwise create Administrators-owned objects. // Only this newly created fixture subtree is adjusted to its launching user. const result = spawnSync( 'powershell.exe', [ '-NoProfile', '-NonInteractive', '-Command', "$ErrorActionPreference='Stop'; $fixtureRoot=$env:AGC_FIXTURE_OWNER_ROOT; " + '$fixtureSid=[System.Security.Principal.WindowsIdentity]::GetCurrent().User; ' + '$fixtureItems=@(Get-Item -LiteralPath $fixtureRoot)+@(Get-ChildItem -LiteralPath $fixtureRoot -Recurse -Force); ' + 'foreach($fixtureItem in $fixtureItems){$fixtureAcl=$fixtureItem.GetAccessControl(); $fixtureAcl.SetOwner($fixtureSid); $fixtureItem.SetAccessControl($fixtureAcl)}', ], { env: { ...process.env, AGC_FIXTURE_OWNER_ROOT: directory }, windowsHide: true, encoding: 'utf8', }, ); assert.equal( result.status, 0, 'fixture ownership setup failed: ' + result.stderr, ); } async function installFixtureMcp( host, directory, readOnlyHint, delayMs = 800, doneMarker = '', ) { const source = path.join(host, 'extensions', 'sources', 'fixture'); await fs.mkdir(source, { recursive: true }); const program = path.join(source, 'server.cjs'); const dispatch = path.join(directory, 'mcp-dispatch.jsonl'); await fs.writeFile( program, [ "const rl=require('node:readline').createInterface({input:process.stdin});", "const fs=require('node:fs'); const target=process.argv[2];", "rl.on('line',async(line)=>{let x;try{x=JSON.parse(line)}catch{return}if(x.id===undefined)return;", 'let result;switch(x.method){', "case 'initialize':result={protocolVersion:x.params.protocolVersion,capabilities:{tools:{},resources:{}},serverInfo:{name:'fixture',version:'1'}};break;", "case 'tools/list':result={tools:[{name:'execute',description:'Fixture execution with explicit MCP annotations',inputSchema:{type:'object',properties:{step:{type:'integer'}},required:['step']},annotations:{readOnlyHint:" + readOnlyHint + ',idempotentHint:true}}]};break;', "case 'tools/call':fs.appendFileSync(target,JSON.stringify({id:x.id,phase:'start',method:x.method,arguments:x.params.arguments,at:Date.now()})+'\\n');await new Promise(r=>setTimeout(r," + delayMs + "));if(process.argv[3])fs.writeFileSync(process.argv[3],'done');fs.appendFileSync(target,JSON.stringify({id:x.id,phase:'end',at:Date.now()})+'\\n');result={content:[{type:'text',text:'fixture dispatched'}]};break;", "case 'resources/list':case 'resources/templates/list':case 'resources/read':fs.appendFileSync(target,JSON.stringify({id:x.id,phase:'start',method:x.method,at:Date.now()})+'\\n');await new Promise(r=>setTimeout(r,800));fs.appendFileSync(target,JSON.stringify({id:x.id,phase:'end',at:Date.now()})+'\\n');result=x.method==='resources/list'?{resources:[{uri:'fixture://state',name:'state',mimeType:'text/plain'}]}:x.method==='resources/templates/list'?{resourceTemplates:[{uriTemplate:'fixture://{name}',name:'fixture',mimeType:'text/plain'}]}:{contents:[{uri:x.params.uri,text:'fixture resource',mimeType:'text/plain'}]};break;", "case 'ping':result={};break;", "default:process.stdout.write(JSON.stringify({jsonrpc:'2.0',id:x.id,error:{code:-32601,message:'unsupported fixture method'}})+'\\n');return;", "}process.stdout.write(JSON.stringify({jsonrpc:'2.0',id:x.id,result})+'\\n');});", ].join('\n'), ); const config = { command: process.execPath, args: [program, dispatch, doneMarker], tool_timeout_sec: 10, }; const document = JSON.stringify({ mcpServers: { fixture: config } }); await fs.writeFile(path.join(source, 'mcp.json'), document); await fs.writeFile( path.join(host, 'extensions', 'index.json'), JSON.stringify({ schemaVersion: 'direct-project-client-extensions.v1', sources: [ { id: 'fixture-source', originalName: 'fixture', storagePath: 'sources/fixture', fingerprint: hash(document), }, ], items: [ { id: 'fixture-mcp', sourceId: 'fixture-source', extensionType: 'mcp', name: 'fixture', originalName: 'fixture', sourceRelativePath: 'mcp.json', enabled: true, fingerprint: hash(document), lastError: null, mcpConfig: config, }, ], }), ); return dispatch; } async function runScenario(name) { assert( [ 'completed', 'passes', 'mcp', 'mcp-write', 'native', 'native-resources', 'patch', 'deadline', 'native-session', ].includes(name), 'unknown fixture case', ); const directory = path.join(root, name); const project = path.join(directory, 'project'); const host = path.join(directory, 'host'); const home = path.join(directory, 'home'); await Promise.all( [ project, host, home, path.join(home, 'appdata'), path.join(home, 'local'), ].map((dir) => fs.mkdir(dir, { recursive: true })), ); // An existing editor project avoids turning this execution-boundary fixture // into a new-Web-game/bootstrap/visual-quality test. await fs.writeFile( path.join(project, 'project.godot'), 'config_version=5\n[application]\nconfig/name="AGC execution fixture"\n', ); const isMcp = name === 'mcp' || name === 'mcp-write'; const mcpDispatch = isMcp || name === 'native-resources' || name === 'patch' ? await installFixtureMcp( host, directory, name === 'mcp', name === 'patch' ? 4_000 : 800, name === 'patch' ? path.join(project, 'mcp-done.txt') : '', ) : null; const writes = (filename, text) => "require('node:fs').writeFileSync(" + JSON.stringify(filename) + ',' + JSON.stringify(text) + ');'; const nativeOverlapCall = (id) => native( id, "const f=require('node:fs');const file=" + JSON.stringify(id + '.jsonl') + ";const mark=phase=>f.appendFileSync(file,JSON.stringify({phase,at:Date.now()})+'\\n');mark('start');setTimeout(()=>mark('end'),1000);", ); const plan = name === 'completed' ? [ [register('marker.txt')], [native('marker', writes('marker.txt', 'verified'))], [ native( 'forbidden-after-completion', writes('forbidden.txt', 'must not run'), ), ], ] : name === 'deadline' ? [ [register('never.txt')], [ native( 'slow', writes('started.txt', 'started') + 'setTimeout(()=>{' + writes('late.txt', 'must not run') + '},8000);', ), ], ] : isMcp ? [ [register('never.txt')], [ call('mcp-a', 'execute', { step: 1 }, 'mcp__fixture'), call('mcp-b', 'execute', { step: 1 }, 'mcp__fixture'), ], [native('fail', 'process.exit(1);')], [call('forbidden-mcp', 'execute', { step: 2 }, 'mcp__fixture')], ] : name === 'native' ? [ [register('never.txt')], [nativeOverlapCall('native-a'), nativeOverlapCall('native-b')], [native('fail', 'process.exit(1);')], [ native( 'forbidden-native', writes('forbidden.txt', 'must not run'), ), ], ] : name === 'native-resources' ? [ [register('never.txt')], [ 'list_mcp_resources', 'list_mcp_resource_templates', 'read_mcp_resource', ].flatMap((tool) => [1, 2].map((i) => call(tool + '-' + i, tool, { server: 'fixture', ...(tool === 'read_mcp_resource' ? { uri: 'fixture://state' } : {}), }), ), ), [native('fail', 'process.exit(1);')], ] : name === 'native-session' ? [ [register('session.txt')], [ call('session', 'exec_command', { cmd: nodeCommand( "const f=require('node:fs');setTimeout(()=>f.writeFileSync('session.txt','session completed'),12000);", ), yield_time_ms: 10_000, }), ], (body) => { // 统一 exec 超过 yield_time_ms 会返回会话号;用 write_stdin 轮询到会话结束。 const output = (body.input ?? []) .filter((item) => item.call_id === 'session') .map((item) => toolOutputText(item.output)) .join('\n'); const match = /session ID (\d+)/.exec(output); assert( match, 'exec_command did not return a unified exec session id: ' + output.slice(0, 400), ); return [ call('poll', 'write_stdin', { session_id: Number(match[1]), chars: '', yield_time_ms: 20_000, }), ]; }, [final], ] : name === 'patch' ? [ [ call( 'contract', 'agc_register_delivery_contract', { scope: '验收并发补丁、计划与慢工具的正常完成;两个必需产物都必须出现。', changeKind: 'project', requirements: [ { kind: 'artifact', id: 'patch', path: 'patch-proof.txt', }, { kind: 'artifact', id: 'mcp', path: 'mcp-done.txt', }, ], }, 'mcp__agc_tools', ), ], [ call( 'slow-mcp', 'execute', { step: 1 }, 'mcp__fixture', ), call( 'patch', 'agc_apply_patch', { patch: '*** Begin Patch\n*** Environment ID: local\n*** Add File: patch-proof.txt\n+parallel patch proof\n*** End Patch', }, 'mcp__agc_tools', ), call( 'plan', 'agc_update_plan', { explanation: '并行计划回执', plan: [ { step: 'parallel-plan-proof', status: 'completed', }, ], }, 'mcp__agc_tools', ), ], [ native( 'forbidden-patch-expansion', writes('forbidden.txt', 'must not run'), ), ], ] : [ [register('never.txt')], [native('build', 'console.log(1+1);')], [native('test', 'console.log(2+2);')], [native('fail', 'process.exit(1);')], [ native( 'forbidden-after-budget', writes('forbidden.txt', 'must not run'), ), ], ]; const requests = []; const responses = []; const httpArrivals = []; const fixtureErrors = []; const server = http.createServer(async (request, response) => { httpArrivals.push({ at: Date.now(), method: request.method, url: request.url, expectedAuthorization: request.headers.authorization === 'Bearer agc-loopback-fixture', }); try { assert.equal(request.method, 'POST'); assert(request.url.endsWith('/responses')); assert.equal( request.headers.authorization, 'Bearer agc-loopback-fixture', ); const chunks = []; let bytes = 0; for await (const chunk of request) { bytes += chunk.length; assert(bytes <= 32 * 1024 * 1024, 'oversized fixture request'); chunks.push(chunk); } const requestBody = JSON.parse(Buffer.concat(chunks).toString('utf8')); const index = requests.length; requests.push({ index, at: Date.now(), model: requestBody.model, parallelToolCalls: requestBody.parallel_tool_calls, ...(index === 0 ? { toolCatalogue: (requestBody.tools ?? []).map((tool) => describeTool(tool, NATIVE_SCHEMA_TOOLS.has(tool.name)), ), } : {}), // 只留档工具回执的有界原文,用于核对统一 exec 的会话/退出结果契约。 inputs: (requestBody.input ?? []).map((item) => ({ type: item.type, name: item.name, call_id: item.call_id, raw: JSON.stringify(item).slice(0, 1500), })), }); assert(index < 20, 'fixture model loop exceeded bound'); if (index === plan.length - 1 && name !== 'deadline') { // Allow host receipt settlement/sealing to win before an intentionally // unwanted expansion. A closed transport is a valid pre-dispatch stop. await sleep(300); } const step = plan[index] ?? [final]; const output = typeof step === 'function' ? step(requestBody) : step; responses.push({ index, items: output.map((item) => JSON.stringify(item).slice(0, 300)), }); const body = { id: 'response-' + index, object: 'response', model: 'gpt-5.1-codex', status: 'completed', output, usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 }, }; const events = [ { type: 'response.created', response: { ...body, status: 'in_progress', output: [] }, }, ]; output.forEach((item, output_index) => { events.push({ type: 'response.output_item.added', output_index, item }); events.push({ type: 'response.output_item.done', output_index, item }); }); events.push({ type: 'response.completed', response: body }); response.writeHead(200, { 'content-type': 'text/event-stream' }); if (name === 'patch' && index === 1) { // One response, streamed incrementally: prove the long operation really // started before offering independent patch/plan calls to the scheduler. response.write( events .slice(0, 3) .map((event) => 'data: ' + JSON.stringify(event) + '\n\n') .join(''), ); const startedDeadline = Date.now() + 5_000; while (Date.now() < startedDeadline) { const log = await fs.readFile(mcpDispatch, 'utf8').catch(() => ''); if ( log .split('\n') .some( (line) => line.includes('"method":"tools/call"') && line.includes('"phase":"start"'), ) ) break; await sleep(20); } response.end( events .slice(3) .map((event) => 'data: ' + JSON.stringify(event) + '\n\n') .join(''), ); return; } response.end( events .map((event) => 'data: ' + JSON.stringify(event) + '\n\n') .join(''), ); } catch (error) { fixtureErrors.push(String(error)); if (!response.destroyed) { response.writeHead(400); response.end(String(error)); } } }); await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); const address = server.address(); await fs.writeFile( path.join(host, 'game-creator.config.json'), JSON.stringify({ schemaVersion: 'game-creator-config.v2', agentMode: 'codex_app_server', llm: { customEnabled: true, apiKey: 'agc-loopback-fixture', baseUrl: 'http://127.0.0.1:' + address.port + '/v1', model: 'gpt-5.1-codex', visibleModels: ['gpt-5.1-codex'], apiKind: 'openai_responses', stream: true, webSearchEnabled: false, reasoningEffort: 'low', maxRetries: 0, requestTimeoutMs: 60_000, }, validation: { maxRuns: 1, maxExecutionSeconds: name === 'deadline' ? 2 : 30, maxTurnSeconds: 60, }, }), ); ownFixtureTree(directory); const env = Object.fromEntries( Object.entries(process.env).filter( ([key]) => !/TOKEN|SECRET|PASSWORD|API_KEY|AUTHORIZATION|COOKIE/i.test(key), ), ); Object.assign(env, { GENARRATIVE_AGC_DEBUG_PROVIDER_E2E: '1', HOME: home, USERPROFILE: home, APPDATA: path.join(home, 'appdata'), LOCALAPPDATA: path.join(home, 'local'), PATH: path.dirname(codex) + path.delimiter + (env.PATH ?? env.Path ?? ''), }); let stdout = '', stderr = ''; let planObservedAt = null; let observingPlan = false; let planObservationTask = Promise.resolve(); const planWatch = name === 'patch' ? setInterval(() => { if (planObservedAt !== null || observingPlan) return; observingPlan = true; planObservationTask = readLedger(host) .then((entry) => { if ( JSON.stringify(entry?.value.plan ?? null).includes( 'parallel-plan-proof', ) ) planObservedAt = Date.now(); }) .catch(() => {}) .finally(() => { observingPlan = false; }); }, 20) : null; const child = spawn( executable, [ '--config-dir', host, '--direct-codex-chat', project, '测试现有临时工程的宿主执行许可;只按fixture合同操作,不生成平台素材。', ], { cwd: directory, env, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'], }, ); child.stdout.on('data', (data) => { stdout = (stdout + data.toString('utf8')).slice(-2 * 1024 * 1024); }); child.stderr.on('data', (data) => { stderr = (stderr + data.toString('utf8')).slice(-2 * 1024 * 1024); }); const timeout = setTimeout(() => child.kill(), 90_000); const exit = await new Promise((resolve, reject) => { child.once('error', reject); child.once('exit', (code, signal) => resolve({ code, signal })); }).finally(() => clearTimeout(timeout)); if (planWatch) clearInterval(planWatch); await planObservationTask; server.closeAllConnections(); await new Promise((resolve) => server.close(resolve)); if (name === 'deadline') await sleep(9_000); const ledger = await readLedger(host); const dispatchEvents = mcpDispatch ? (await fs.readFile(mcpDispatch, 'utf8').catch(() => '')) .trim() .split('\n') .filter(Boolean) .map(JSON.parse) : []; const dispatched = dispatchEvents.filter( (entry) => entry.phase === 'start' && entry.method === 'tools/call', ); const intervals = dispatched.map((entry) => ({ id: entry.id, start: entry.at, end: dispatchEvents.find( (event) => event.id === entry.id && event.phase === 'end', )?.at, })); const overlapMs = intervals.length === 2 && intervals.every((entry) => entry.end) ? Math.min(...intervals.map((entry) => entry.end)) - Math.max(...intervals.map((entry) => entry.start)) : null; const nativeIntervals = []; if (name === 'native') for (const id of ['native-a', 'native-b']) { const entries = ( await fs .readFile(path.join(project, id + '.jsonl'), 'utf8') .catch(() => '') ) .trim() .split('\n') .filter(Boolean) .map(JSON.parse); nativeIntervals.push({ id, start: entries.find((entry) => entry.phase === 'start')?.at, end: entries.find((entry) => entry.phase === 'end')?.at, }); } const nativeOverlapMs = nativeIntervals.length === 2 && nativeIntervals.every((entry) => entry.end) ? Math.min(...nativeIntervals.map((entry) => entry.end)) - Math.max(...nativeIntervals.map((entry) => entry.start)) : null; const resourceIntervals = dispatchEvents .filter( (entry) => entry.phase === 'start' && entry.method.startsWith('resources/') && entry.at >= (requests[1]?.at ?? Infinity), ) .map((entry) => ({ id: entry.id, method: entry.method, start: entry.at, end: dispatchEvents.find( (event) => event.id === entry.id && event.phase === 'end', )?.at, })); const resourceOverlaps = Object.fromEntries( ['resources/list', 'resources/templates/list', 'resources/read'].map( (method) => { const matching = resourceIntervals.filter( (entry) => entry.method === method, ); return [ method, matching.length === 2 && matching.every((entry) => entry.end) ? Math.min(...matching.map((entry) => entry.end)) - Math.max(...matching.map((entry) => entry.start)) : null, ]; }, ), ); const report = { scenario: name, directory, exit, requests, httpArrivals, fixtureErrors, ledger: ledger?.value, responses, dispatched, dispatchEvents, intervals, overlapMs, mcpReadOnlyHint: mcpDispatch ? name === 'mcp' : undefined, nativeIntervals, nativeOverlapMs, resourceIntervals, resourceOverlaps, planObservedAt, patchModifiedAt: await fs.stat(path.join(project, 'patch-proof.txt')).then( (stat) => stat.mtimeMs, () => null, ), stdout, stderr, markers: { ready: await exists(path.join(project, 'marker.txt')), forbidden: await exists(path.join(project, 'forbidden.txt')), started: await exists(path.join(project, 'started.txt')), late: await exists(path.join(project, 'late.txt')), }, }; await fs.writeFile( path.join(directory, 'result.json'), JSON.stringify(report, null, 2), ); assert( requests.length > 0, name + ': AGC did not reach the loopback fixture', ); assert( requests.every((request) => request.parallelToolCalls === true), name + ': Direct Responses requests must allow multiple tool calls without changing the selected model', ); assert(ledger, name + ': missing host-authoritative ledger'); assert.equal(exit.code, 0, name + ': CLI failed; inspect result.json'); assert.equal( report.markers.forbidden, false, name + ': effect ran after host terminal', ); assert.equal( ledger.value.usedPasses, 1, name + ': ordinary commands must share one pass', ); assert.equal( ledger.value.phase, ['completed', 'patch', 'native-session'].includes(name) ? 'completed' : 'exhausted', ); assert.equal( ledger.value.executorStopped, true, name + ': missing full executor exit proof', ); if (name === 'completed') assert.equal(report.markers.ready, true); if (isMcp) { assert.equal( dispatched.length, 2, 'untrusted readonly hints cannot authorize a post-terminal MCP call', ); assert(dispatched.every((entry) => entry.arguments.step === 1)); assert( overlapMs > 0, name + ': real Codex MCP calls ran serially; inspect start/end evidence before changing dispatch policy', ); } if (name === 'deadline') { assert.equal( report.markers.started, true, 'must prove the real child started before the cutoff', ); assert.equal( report.markers.late, false, 'turn interruption alone did not stop the child', ); } if (name === 'native') assert( nativeOverlapMs > 0, 'independent native write commands must overlap', ); if (name === 'native-resources') for (const [method, overlap] of Object.entries(resourceOverlaps)) assert( overlap > 0, method + ': real native resource reads did not overlap', ); if (name === 'patch') { const catalogue = requests[0].toolCatalogue; assert( !catalogue.some((tool) => ['apply_patch', 'update_plan', 'request_user_input'].includes( tool.name, ), ), 'serial native registrations must be absent', ); const owned = catalogue.find((tool) => tool.name === 'mcp__agc_tools')?.tools ?? []; for (const tool of ['agc_apply_patch', 'agc_update_plan']) assert(owned.some((entry) => entry.name === tool)); assert.equal( intervals.length, 1, 'exactly one slow third-party call must execute', ); assert.equal( await fs.readFile(path.join(project, 'patch-proof.txt'), 'utf8'), 'parallel patch proof\n', ); assert.equal( await fs.readFile(path.join(project, 'mcp-done.txt'), 'utf8'), 'done', ); for (const [action, at] of Object.entries({ patch: report.patchModifiedAt, plan: planObservedAt, })) assert( at > intervals[0].start && at < intervals[0].end, action + ' did not complete while the slow MCP call was running', ); } if (name === 'native-session') { const execOutput = requests .flatMap((entry) => entry.inputs ?? []) .filter((item) => item.call_id === 'session') .map((item) => { try { return toolOutputText(JSON.parse(item.raw).output); } catch { return ''; } }) .join('\n'); assert( /Process running with session ID \d+/.test(execOutput), 'exec_command must return a unified exec session id', ); assert( await exists(path.join(project, 'session.txt')), 'write_stdin polling must let the unified exec session finish', ); assert.equal( ledger?.value?.phase, 'completed', 'host acceptance must complete after the session artifact appears', ); } console.log( JSON.stringify({ scenario: name, passed: true, result: path.join(directory, 'result.json'), }), ); } const failures = []; for (const name of cases) { try { await runScenario(name); } catch (error) { failures.push({ scenario: name, error: String(error) }); console.error(JSON.stringify(failures.at(-1))); } } assert.equal( failures.length, 0, 'production fixture failures: ' + JSON.stringify(failures), );