真机夹具加 running 用例:进程存活期间宿主可观测到运行中回合
Project CI / AI game creator shell Rust smoke (push) Successful in 1m38s
Project CI / AI game creator shell Rust crates (push) Successful in 1m22s
Project CI / Backend tests (push) Successful in 4m21s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 8m27s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 9m13s
Project CI / Native shell tests (push) Successful in 6m23s
Project CI / Frontend tests (push) Successful in 2m12s
Project CI / AI game creator shell web tests (push) Successful in 1m46s
Project CI / Repository checks (push) Successful in 2m19s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m38s
Project CI / AI game creator shell Rust crates (push) Successful in 1m22s
Project CI / Backend tests (push) Successful in 4m21s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 8m27s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 9m13s
Project CI / Native shell tests (push) Successful in 6m23s
Project CI / Frontend tests (push) Successful in 2m12s
Project CI / AI game creator shell web tests (push) Successful in 1m46s
Project CI / Repository checks (push) Successful in 2m19s
- direct-execution-production-fixture:新增 running 场景(工具阻塞期间进程存活),轮询宿主账本抓快照,断言 phase=working、active 里恰好一条 kind=execute 在途许可、executorStopped=false;收尾后 phase=completed、active 清空、executorStopped=true,宿主侧条目顺序不变 - 复验:completed,passes,mcp,mcp-write,native,deadline,restart,running 共 8/8 passed - 记录事实:CLI/无前端宿主既无用户消息也无最终助手回复(前端写入),夹具断言只校验宿主侧条目;宿主侧"正在跑"的可观测事实两路径一致 - 文档:DirectProject 里程碑补该轮核对;pitfalls 扩写 CLI 历史只含宿主侧条目这一坑
This commit is contained in:
@@ -318,6 +318,7 @@ async function runScenario(name) {
|
||||
'deadline',
|
||||
'native-session',
|
||||
'restart',
|
||||
'running',
|
||||
].includes(name),
|
||||
'unknown fixture case',
|
||||
);
|
||||
@@ -551,18 +552,41 @@ async function runScenario(name) {
|
||||
),
|
||||
],
|
||||
]
|
||||
: [
|
||||
[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'),
|
||||
),
|
||||
],
|
||||
];
|
||||
: name === 'running'
|
||||
? [
|
||||
[register('started.txt')],
|
||||
[
|
||||
{
|
||||
type: 'message',
|
||||
id: 'fixture-running-partial',
|
||||
role: 'assistant',
|
||||
content: [
|
||||
{
|
||||
type: 'output_text',
|
||||
text: 'RUNNING-PARTIAL-TEXT',
|
||||
},
|
||||
],
|
||||
},
|
||||
native(
|
||||
'running-slow',
|
||||
writes('started.txt', 'started') +
|
||||
'setTimeout(()=>{},6000);',
|
||||
),
|
||||
],
|
||||
[final],
|
||||
]
|
||||
: [
|
||||
[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 = [];
|
||||
@@ -794,6 +818,25 @@ async function runScenario(name) {
|
||||
child.kill();
|
||||
})()
|
||||
: null;
|
||||
// running:进程存活期间轮询宿主账本,抓一份「正在跑」的快照——界面重进时判断
|
||||
// "这一轮还在跑、可以恢复/终止"读的就是这份事实。
|
||||
let runningLedgerSnapshot = null;
|
||||
const runningSnapshotWatch =
|
||||
name === 'running'
|
||||
? (async () => {
|
||||
const deadline = Date.now() + 30_000;
|
||||
while (Date.now() < deadline && !runningLedgerSnapshot) {
|
||||
const snapshot = await readLedger(host).catch(() => null);
|
||||
if (
|
||||
snapshot &&
|
||||
Object.keys(snapshot.value.active ?? {}).length > 0
|
||||
) {
|
||||
runningLedgerSnapshot = snapshot.value;
|
||||
}
|
||||
await sleep(25);
|
||||
}
|
||||
})()
|
||||
: null;
|
||||
const timeout = setTimeout(() => child.kill(), 90_000);
|
||||
const exit = await new Promise((resolve, reject) => {
|
||||
child.once('error', reject);
|
||||
@@ -801,6 +844,7 @@ async function runScenario(name) {
|
||||
}).finally(() => clearTimeout(timeout));
|
||||
if (planWatch) clearInterval(planWatch);
|
||||
if (restartKillWatch) await restartKillWatch;
|
||||
if (runningSnapshotWatch) await runningSnapshotWatch;
|
||||
await planObservationTask;
|
||||
server.closeAllConnections();
|
||||
await new Promise((resolve) => server.close(resolve));
|
||||
@@ -890,6 +934,7 @@ async function runScenario(name) {
|
||||
httpArrivals,
|
||||
fixtureErrors,
|
||||
ledger: ledger?.value,
|
||||
runningLedger: runningLedgerSnapshot,
|
||||
responses,
|
||||
dispatched,
|
||||
dispatchEvents,
|
||||
@@ -944,7 +989,7 @@ async function runScenario(name) {
|
||||
);
|
||||
assert.equal(
|
||||
ledger.value.phase,
|
||||
['completed', 'patch', 'native-session'].includes(name)
|
||||
['completed', 'patch', 'native-session', 'running'].includes(name)
|
||||
? 'completed'
|
||||
: 'exhausted',
|
||||
);
|
||||
@@ -953,6 +998,54 @@ async function runScenario(name) {
|
||||
true,
|
||||
name + ': missing full executor exit proof',
|
||||
);
|
||||
if (name === 'running') {
|
||||
assert(
|
||||
runningLedgerSnapshot,
|
||||
'running: host never published a live active lease while the process was alive',
|
||||
);
|
||||
assert.equal(
|
||||
runningLedgerSnapshot.phase,
|
||||
'working',
|
||||
'running: live turn must read as working, not terminal',
|
||||
);
|
||||
const leases = Object.values(runningLedgerSnapshot.active ?? {});
|
||||
assert.equal(
|
||||
leases.length,
|
||||
1,
|
||||
'running: exactly one live execution lease expected',
|
||||
);
|
||||
assert.equal(leases[0].kind, 'execute');
|
||||
assert.equal(
|
||||
runningLedgerSnapshot.executorStopped,
|
||||
false,
|
||||
'running: live turn must not claim its executor stopped',
|
||||
);
|
||||
const history = await fs
|
||||
.readFile(
|
||||
path.join(project, '.agent/conversations/project.jsonl'),
|
||||
'utf8',
|
||||
)
|
||||
.catch(() => '');
|
||||
// 用户消息与最终助手回复由**前端**(`append_direct_project_conversation_message`)写入,
|
||||
// CLI / 无前端宿主只落宿主侧条目:中间文本 → 工具卡片 → 工具回执。
|
||||
const rows = history.split('\n').filter(Boolean);
|
||||
const order = [
|
||||
rows.findIndex((row) => row.includes('RUNNING-PARTIAL-TEXT')),
|
||||
rows.findIndex(
|
||||
(row) => row.includes('running-slow') && row.includes('"arguments"'),
|
||||
),
|
||||
rows.findIndex(
|
||||
(row) => row.includes('running-slow') && row.includes('"output"'),
|
||||
),
|
||||
];
|
||||
assert(
|
||||
order.every((index) => index >= 0) &&
|
||||
order[0] < order[1] &&
|
||||
order[1] < order[2],
|
||||
'running: partial text → tool card → tool output must stay in order; got ' +
|
||||
JSON.stringify({ order, rows: rows.length }),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (name === 'completed') assert.equal(report.markers.ready, true);
|
||||
if (isMcp) {
|
||||
|
||||
@@ -91,3 +91,7 @@ AGC 项目开发对话的显示与恢复只依赖两项输入:**项目对话
|
||||
- **变异验证**:把"等 400ms 再杀"改成"等 20s 再杀"(让这一轮自然跑完)后,同一条用例立刻以 `restart: AGC must be killed mid-turn, got {"code":0,"signal":null}` 变红,证明这些断言确实在区分「进行中终止」和「已完成」。
|
||||
- 顺带记录一个事实:CLI / 无前端宿主**不写用户消息**(GUI 是前端先调 `append_direct_project_conversation_message`),所以用例只断言部分文本与工具卡片的顺序;用户消息的持久化属于 GUI 路径。
|
||||
- **两条运行时验收仍继续未勾选**:上面这条覆盖了「已落盘的部分文本与工具卡片按原顺序出现、且宿主不假装完成」的宿主侧证据;但判据里的「**界面不显示忙碌态**」「页面重进(含切走再切回)能恢复运行中回合并允许终止」仍要真实客户端窗口,夹具证不了 UI 投影。
|
||||
- **再补一条(2026-09-28,同一夹具加 `running` 用例)**:`--cases …,restart,running` → **8/8 passed**。`running` 场景在工具阻塞期间**进程存活**,夹具轮询宿主账本并抓快照,断言:
|
||||
- 宿主确实对外发布「这一轮正在跑」:快照 `phase=working`、`active` 里恰好一条 `kind=execute` 的在途许可、`executorStopped=false` —— 这正是界面重进时判断「有运行中回合可恢复/可终止」读的事实;
|
||||
- 收尾后账本落 `completed`、`active` 清空、`executorStopped=true`;宿主侧落的条目(中间文本 → 工具卡片 → 工具回执)顺序不变。
|
||||
- 顺带记实:CLI / 无前端宿主的历史里**既没有用户消息、也没有最终助手回复**(两者都由前端 `append_direct_project_conversation_message` 写),CLI 只有宿主侧条目与回执文本;夹具断言据此只校验宿主侧条目。
|
||||
|
||||
@@ -6123,5 +6123,5 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
|
||||
修复:①`fail_turn()` 在 `is_closed()`(宿主自己收尾)时只把原因追加进终态说明、**不改阶段**,新增 `ExecutionSession::append_terminal_note()`(`interrupt()` 同款写法但不设 `Interrupted`);用户主动终止(`host_stop_requested`)与真实通道故障仍保持原口径。
|
||||
复验(真 AGC CLI → 真 app-server → loopback Responses):`node apps/ai-game-creator-shell/scripts/direct-execution-production-fixture.mjs --agc-exe apps/ai-game-creator-shell/src-tauri/target/debug/genarrative-ai-game-creator-shell.exe --cases completed,passes,mcp,mcp-write,native,deadline` → **6/6 passed**;`completed` 用例的账本从 `phase=interrupted` 变成 `phase=completed`、`executorStopped=true`,`terminalReport` 变成宿主真实交付报告(「本轮已完成宿主验收…已通过:产物 marker.txt」),不再是「执行通道已断开」。定向单测:`cargo test … codex_app_server::execution` 13 passed、`cargo test … direct_execution` 26 passed。
|
||||
- **保留的诊断开关**:关闭原因日志留在 `GENARRATIVE_AGC_DIRECT_DEBUG=1` 下(与既有的 `agent.direct_codex.stderr bytes=` 同一开关),以后排查「谁关掉的」不用再插桩。
|
||||
- **顺带发现(2026-09-28)**:同一个夹具新增的 `restart` 用例证明——CLI / 无前端宿主**不会把用户消息写进 `.agent/conversations/project.jsonl`**(GUI 是前端先调 `append_direct_project_conversation_message`,所以总有至少一条用户条目)。写依赖"历史里一定有用户消息"的夹具断言前先确认是走 GUI 还是 CLI 路径,否则会得到假失败。
|
||||
- **顺带发现(2026-09-28)**:同一个夹具的 `restart` / `running` 用例证明——CLI / 无前端宿主**既不会把用户消息、也不会把最终助手回复写进 `.agent/conversations/project.jsonl`**(两者都由前端调 `append_direct_project_conversation_message` 写入,GUI 因此总有至少一条用户条目)。CLI 路径只落宿主侧条目(中间文本、工具卡片、工具回执),回复文本走 CLI 标准输出。写依赖"历史里一定有用户/助手消息"的断言前先确认是走 GUI 还是 CLI 路径,否则会得到假失败。宿主侧「正在跑」的可观测事实(`phase=working` + `active` 里的 `execute` 许可)两条路径一致,可以放心断言。
|
||||
- **顺带记一条环境陷阱(2026-09-28 已修)**:`apps/ai-game-creator-shell/src-tauri/resources/codex/win-x64/` 下曾有两个 codex 二进制——`bin/codex.exe` 是**真正被解析**的那份(0.155.1),而包根目录那份 `codex.exe` 是 0.147.0 的旧残留(tauri 的 Windows 资源映射只引用 `bin/` 等路径),检查都查不出来,却会让本地核对误判「应用跑的是 0.147.0」。根因是 `src-tauri/build.rs` 的 `stage_codex_target()` 只按布局拷贝、从不清理目录,旧布局的组件会永久留在随包资源目录里。现在加了 `prune_stale_codex_components()`:拷贝前删掉不在本轮布局、也不在 `manifest.json`/`NOTICE.md` 白名单里的文件并收掉空目录;实测重建后根目录 `codex.exe` 被清掉、六个声明组件与清单/声明保留。
|
||||
|
||||
Reference in New Issue
Block a user