diff --git a/apps/ai-game-creator-shell/tests/appSurface/chat-composer.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/chat-composer.suite.ts index f093acb43..373c12258 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/chat-composer.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/chat-composer.suite.ts @@ -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 | null = + null; + const { surface } = await openDirectCodexSurface( + { + chat_with_game_creator_direct_codex: ( + args: Record | 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> = [];