appSurface 用例:失败只经终态事件收口,界面不再停在"还在处理"
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled

- 新增「closes the turn from the host failure payload instead of leaving it running」:宿主发过 turn.started 之后以 failure 载荷收场并让命令失败,断言失败文案来自事件载荷(经同一份可见文案映射)、"陶泥儿正在处理"消失、终止钮消失、输入盒回到「发送」
- 同一条用例反向断言命令返回的错误原文不进聊天:那条通道只负责运行错误横幅
- 变异校验:让 reducer 不落失败说明条目时该用例变红(1 failed),恢复后绿
This commit is contained in:
2026-09-22 17:18:43 +08:00
parent b432556ee3
commit 80b15b24ae
@@ -503,6 +503,62 @@ export function registerChatComposerControlTests() {
});
});
it('closes the turn from the host failure payload instead of leaving it running', async () => {
let harness: ReturnType<typeof createProjectChatRuntimeHarness> | null =
null;
const { surface } = await openDirectCodexSurface(
{
chat_with_game_creator_direct_codex: (
args: Record<string, unknown> | undefined,
) => {
// 宿主先认领了这一轮(turn.started),随后失败收场:失败原因由终态事件自己带出来,
// 命令也以失败返回(真实宿主是先 append 终态、再把错误抛回前端)。
const userItemId = `direct-codex:${String(args?.clientTurnId ?? '')}:user`;
harness?.emitDirectThreadEvents(
{ type: 'turn.started', at: 5_000, userItemId },
{
type: 'turn.completed',
status: 'failed',
failure: {
kind: 'request-rejected',
message: 'codex-app-server-error:context-window-exceeded',
},
at: 6_000,
userItemId,
},
);
throw new Error('模拟宿主失败:错误只走终态事件,命令只回报失败');
},
},
(directHarness) => {
harness = directHarness;
},
);
const composer = within(surface).getByLabelText('陶泥儿对话内容');
await submitDirectTurn(surface, composer, '超限的那条');
// 聊天里的失败说明来自事件载荷(经同一份可见文案映射),不是命令返回的错误文本。
await waitFor(() => {
expect(
within(surface).getAllByText(
'陶泥儿智能创作 模型上下文已超限,请缩小任务范围后重试',
).length,
).toBeGreaterThan(0);
});
// 终态已经到了:卡片和输入区都不能再声称"还在处理"。
expect(within(surface).queryAllByText('陶泥儿正在处理')).toHaveLength(0);
expect(within(surface).queryByRole('button', { name: '终止' })).toBeNull();
expect(
within(surface).getByRole('button', { name: '发送' }),
).not.toBeNull();
// 命令返回那条通道只负责横幅:它不写第二条聊天文案。
expect(
within(surface).queryAllByText(
'模拟宿主失败:错误只走终态事件,命令只回报失败',
),
).toHaveLength(0);
});
it('keeps the next queued turn busy when the write gate refuses the running one', async () => {
const pending: Array<{ resolve: (value: string) => void }> = [];
const deferredPolicies: Array<(value: unknown) => void> = [];