Files
Genarrative/apps/ai-game-creator-shell/tests/homeWebPreflight.test.tsx
T
kdletters 485ed50b26
Project CI / AI game creator shell Rust smoke (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / AI game creator shell Rust shard 2/4 (push) Has been cancelled
Project CI / AI game creator shell Rust shard 1/4 (push) Has been cancelled
Project CI / AI game creator shell Rust shard 3/4 (push) Has been cancelled
Project CI / AI game creator shell Rust shard 4/4 (push) Has been cancelled
AGC 七项交付效率优化并升级捆绑 Codex 到 0.155.1 (#439)
## 背景

净月潭案例(2026-09-19 21:53 → 09-20 01:26,3 小时 33 分)的问题不是模型慢,而是环境未就绪、验收靠模型自述、预算只约束单个工具入口、工具调用被 SDK 全局串行闸门卡住。本 PR 落地确认后的七项交付效率合同,并把捆绑 Codex 升到当前 npm latest。

## 结果

- 新建 Web 游戏在正式生成前由宿主自动预检:捆绑 Node/npm、真实 Vite 构建、受限浏览器桌面/移动截图;失败不启动生成或付费素材。
- 首个副作用前冻结交付合同,宿主保存权威证据与预算;证据齐全后先封口、排空并取得执行器完整退出证明,再产出交付报告,模型回复不再当作验收。
- 原生 shell、内置浏览器与托管命令、第三方 MCP 共用同一执行许可和累计执行时间;`validation.maxRuns` 改为执行/返修批次语义,另设 `maxTurnSeconds` 墙钟上限。
- 所有工具可并发:移除 SDK 全局串行的 `apply_patch`/`update_plan` 注册,改由宿主 `agc_apply_patch`(官方 parser + 当前回合写许可 + 受控进程树 + 短项目事务)和 `agc_update_plan` 提供等价能力;真实请求目录里已无串行注册,长 MCP 与补丁/计划实测在同一响应内重叠。
- 付费提交与本地写入绑定原回合原租约:容量与同动作锁等待可取消,封口、取消或预算耗尽后零新增提交;已越过提交边界的请求保留 operation ID 走 GET 对账,不自动重放。
- 新增请求与工具分段计时、有界并行批读和首轮上下文预取,未知耗时不补零。
- 捆绑 Codex 0.147.0 → 0.155.1:固定版本收敛到 `build_support/codex_bundle.rs` 单一声明,vendor 解析源码按 `rust-v0.155.1` 逐字节重取并更新 UPSTREAM 证据,适配 0.155 统一 exec(`exec_command`/`write_stdin`);macOS 侧车最小系统版本仍为 15.0。

## 验证

- 生产 CLI → 捆绑 Codex 0.155.1 → 本地 Responses/MCP 夹具 9/9:补丁、完成收尾、批次、只读/可写 MCP 并发、原生命令并发、原生资源并发、统一 exec 会话、期限终止(真实重叠 775 / 764 / 999 ms)。
- 真实模型目录 2/2;vendor 上游库 111 项;宿主定向与回归 106 项;真实 Windows 进程与取消 21 项;Node 侧门禁 52 项。
- 发行载荷:artifact-only 重建 NSIS,解包验证侧车清单 `codex-cli 0.155.1`、6 个组件哈希、打包后 `bin/codex.exe --version`、Node 运行时 2130 文件与双端 PNG、`--environment-check` ready。
- `check-config`、TypeScript、编码(4991 文件)、文档索引、`git diff --check`、`cargo fmt --check` 全部通过。

## 未覆盖

- 真实陶泥儿登录态 Provider 生成尚未执行(本机 `--llm-status` 返回 `authentication-required`)。
- macOS 侧车只在本机做静态 Mach-O 与清单解析,未在 macOS 上跑 `check-macos-bundle.mjs`。
- 全仓库聚合套件在本机仍因负载敏感的后台 mock-provider 用例而红,与本次改动无关:改动前的旧二进制同样失败,换单线程后相关用例 3/3 通过。

---------

Co-authored-by: kdletters <61648117+kdletters@users.noreply.github.com>
Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/439
2026-09-21 02:29:29 +08:00

107 lines
3.7 KiB
TypeScript

// @vitest-environment jsdom
import { act, cleanup, renderHook } from '@testing-library/react';
import { afterEach, expect, it, vi } from 'vitest';
import { createGameCreationAppManifest } from '../../../packages/shared/src/contracts/gameCreationApp';
import { HOME_WEB_PREFLIGHT_FAILURE } from '../src/features/app-shell/homeWebPreflight';
import { useHomeProjectCreation } from '../src/features/app-shell/useHomeProjectCreation';
afterEach(() => {
cleanup();
delete window.__TAURI__;
});
function mount(preflight: () => Promise<unknown>) {
const calls: string[] = [];
const invoke = vi.fn(async (command: string) => {
calls.push(command);
if (command === 'preflight_web_game_creation') return preflight();
if (command === 'suggest_automatic_project_name') return '预检项目';
if (command === 'create_automatic_local_game_project')
return {
projectPath: 'C:/test/preflight-game',
manifestPath: 'C:/test/preflight-game/.agent/manifest.json',
manifest: createGameCreationAppManifest('preflight-game', '预检项目'),
};
if (command === 'get_local_game_project_revision') return { revision: 1 };
if (command === 'set_design_agent_runtime_mode')
return { activeRuntime: 'design' };
return null;
});
window.__TAURI__ = { core: { invoke } } as unknown as typeof window.__TAURI__;
const hook = renderHook(() =>
useHomeProjectCreation({
setStatus: vi.fn(),
setLauncherView: vi.fn(),
setAgentChatProjectPath: vi.fn(),
rememberRecentWorkspace: vi.fn(),
}),
);
return { ...hook, calls, invoke };
}
it('awaits the real home preflight before naming or creating a game', async () => {
let ready!: (value: unknown) => void;
const pending = new Promise((resolve) => {
ready = resolve;
});
const { result, calls } = mount(() => pending);
let creation!: Promise<string>;
await act(async () => {
creation = result.current.createHomeDraftAutomatically(
{ creationType: 'game', prompt: '做一个小游戏', attachments: [] },
'direct-build',
);
});
expect(calls).toEqual(['preflight_web_game_creation']);
await act(async () => {
ready({ status: 'ready' });
await creation;
});
expect(calls.slice(0, 3)).toEqual([
'preflight_web_game_creation',
'suggest_automatic_project_name',
'create_automatic_local_game_project',
]);
expect(result.current.currentProjectContext?.creationType).toBe('game');
});
it.each([false, true])(
'blocks naming, creation, and first generation when preflight fails (throws=%s)',
async (throws) => {
const { result, calls } = mount(async () => {
if (throws) throw new Error('private host path');
return { status: 'blocked' };
});
let status = '';
await act(async () => {
status = await result.current.createHomeDraftAutomatically(
{ creationType: 'game', prompt: '做一个小游戏', attachments: [] },
'direct-build',
);
});
expect(status).toBe(HOME_WEB_PREFLIGHT_FAILURE);
expect(calls).toEqual(['preflight_web_game_creation']);
expect(result.current.currentProjectContext).toBeNull();
expect(status).not.toContain('private');
},
);
it.each(['game', 'doc'] as const)(
'does not require the Web preflight for %s planning',
async (creationType) => {
const { result, calls } = mount(async () => {
throw new Error('must not run');
});
await act(async () => {
await result.current.createHomeDraftAutomatically(
{ creationType, prompt: '策划需求', attachments: [] },
'planning',
);
});
expect(calls).not.toContain('preflight_web_game_creation');
expect(calls).not.toContain('suggest_automatic_project_name');
expect(calls).toContain('create_automatic_local_game_project');
},
);