删掉 artifactsPanel 那份只有存在性断言的外壳测试

- 四个用例里三个是「元素渲染出来了」:三个目标卡片、切 tab 后的文案、tablist 存在;关闭按钮只是把 onClose 转发一次
- 唯一有内容的那条「挂载的是主聊天同一个 .project-chat-surface」已由 chatDialogFrameLayout.test.ts 覆盖,不是唯一防线
- 面板真实行为(表单、冲突、失败卡片、自动重读、入队)由 xhsMinitoolExport.test.tsx 覆盖;前端 vitest 25 → 21 例,里程碑与实施计划的证据口径同步
This commit is contained in:
2026-10-05 21:06:34 +08:00
parent f5a3ef8241
commit ca53daaec4
4 changed files with 11 additions and 84 deletions
@@ -1,75 +0,0 @@
// @vitest-environment jsdom
/**
* 导出产物面板:卡片 → tab 的入口形态与关闭出口。
*
* 面板状态(阶段 / 当前 tab)在面板根上,本用例只钉住「大卡进入 → 卡片收成 tab → 空 tab 页」
* 这条可见链路,不覆盖右侧 chatbox 的订阅行为。
*/
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import ArtifactsPanel from '../src/view/project-development/export';
afterEach(() => {
cleanup();
});
function renderPanel() {
const onClose = vi.fn();
render(
<ArtifactsPanel
projectPath={null}
ensureConversationReadAllowed={async () => true}
ensureConversationWriteAllowed={async () => true}
onClose={onClose}
/>,
);
return { onClose };
}
describe('导出产物面板', () => {
it('首屏用大卡片展示三个导出目标', () => {
renderPanel();
// 三个目标都以可点入口出现(`getBy*` 找不到就抛,不必再包一层恒真的断言)。
screen.getByRole('button', { name: /陶泥儿/u });
screen.getByRole('button', { name: /小红书小工具/u });
screen.getByRole('button', { name: /TapTap H5 小游戏/u });
});
it('点进目标后卡片收成顶部 tab,切到空目标只换 tab 页', () => {
renderPanel();
fireEvent.click(screen.getByRole('button', { name: /陶泥儿/u }));
screen.getByRole('tablist', { name: '导出目标' });
screen.getByText('陶泥儿导出页尚未接入');
// 切到另一个目标只换 tab 页,卡片不会重新长回来。
expect(screen.queryByRole('button', { name: /陶泥儿/u })).toBeNull();
fireEvent.click(screen.getByRole('tab', { name: /TapTap H5 小游戏/u }));
screen.getByText('TapTap H5 小游戏导出页尚未接入');
expect(screen.queryByText('陶泥儿导出页尚未接入')).toBeNull();
});
it('小红书 tab 挂载的是主聊天同一个对话盒', () => {
renderPanel();
fireEvent.click(screen.getByRole('button', { name: /小红书小工具/u }));
const surface = document.querySelector(
'.game-workbench-chat .project-chat-surface.is-direct-codex',
);
expect(surface).not.toBeNull();
expect(surface?.getAttribute('aria-label')).toBe('小红书小工具导出对话');
// 对话盒里就是主聊天那套「顶栏 / 消息流 / 输入区」,不是另一份自造版式。
expect(surface?.querySelector('.project-chat-conversation')).not.toBeNull();
expect(surface?.querySelector('.project-chat-composer')).not.toBeNull();
});
it('关闭按钮把销毁交给壳层', () => {
const { onClose } = renderPanel();
fireEvent.click(screen.getByRole('button', { name: '关闭导出面板' }));
expect(onClose).toHaveBeenCalledTimes(1);
});
});
@@ -86,8 +86,8 @@ describe('交给 code agent 的指令', () => {
for (const { from, to } of XHS_MINITOOL_COPIED_SCRIPTS) {
expect(instruction).toContain(`${from} → ${to}`);
}
// pack.mjs 会删 --out-dir 里的非白名单文件,这条坑必须写进指令正文。
expect(instruction).toContain('--out-dir');
// pack.mjs 打包前会预检 --vite-built-dir:白名单外的文件让它整体失败,不会再就地删除。
expect(instruction).toContain('--vite-built-dir');
// 包根在 game/ 时落点要回退一层,否则 zip 落在 game/.export/ 下,宿主必然报产物缺失。
expect(instruction).toContain(`../${XHS_MINITOOL_ARTIFACT_RELATIVE_PATH}`);
});
@@ -14,7 +14,7 @@
- 复用而非新造:运行脚本走 `command_exec` 的 `resolve_project_command_spec_at → prepare/stage/spawn`(或 `run_project_verification_with_commit_at`),cwd 沿用 `project/export.rs` 的 `resolve_publish_build_cwd` 口径;icon 与 zip 下载复用 `save_local_project_asset_file`;不拿项目写锁、不推进 revision。
- 前端:`src/view/project-development/export/tabs/xiaohongshu/ArtifactsPane.tsx`(表单 + 产物 + 冲突/失败卡片,只有「适配 / 导出」两颗显式按钮)、新增 `state/useXhsMinitoolExport.ts`(状态、防抖自动保存、按固定间隔自动重读内容;指纹只在 hook 内部的写回基线里,不进对外状态)、新增 `state/xhsMinitoolInstruction.ts`(适配与修复指令的纯函数 + 契约常量)、新增 `state/{xhsMinitoolApi,xhsMinitoolFailure,xhsMinitoolFields,xhsMinitoolDownload,xhsMinitoolOutputTail}.ts`、新增 `src/view/project-development/export/generated/`(ts-rs 产物)。宿主不预拼用户可见文案:输出尾部的「已省略前 N 个字符」与策略拒绝那句话都在前端拼(`xhsMinitoolOutputTail.ts` / `xhsMinitoolFailure.ts`)。失败分流按 ADR 两道口子:预期拒绝留在面板,宿主侧事实故障与认不出形状的拒绝先给现场再原样抛出(按指纹去重,面板 2 秒一轮重读不会把报告池的 `count` 刷成轮询次数)。
- 快照排除:`src-tauri/src/project/filesystem.rs` 新增 `PROJECT_SNAPSHOT_SYNC_ONLY_EXCLUDED_COMPONENTS = [".export"]`;**不能**并进通用排除列表(`agent/direct_patch.rs` 直接拿它拒绝路径,`agc_apply_patch` 会拒 `.export/`,首次适配无法落地)。
- skill:`resources/agc-skills/vite-export-xhs-minitool/scripts/pack.mjs`(新增 `--zip-out <path>`,相对 cwd 解析、父目录自动创建;原 `--zip <name>` 行为不变)、`scripts/vite.config.xhs-minitool.mjs` 注释点明 `outDir` 与 `--out-dir` 必须一致、`SKILL.md` 补「被 AGC 导出面板调用时」三条硬契约;同步 `manifest.json` 指纹与 `skill_pack.rs` 的 `include_bytes!` 内容(文件数不变,仍 36 条)。
- skill:`resources/agc-skills/vite-export-xhs-minitool/scripts/pack.mjs`(新增 `--zip-out <path>`,相对 cwd 解析、父目录自动创建;原 `--zip <name>` 行为不变;`--out-dir` 更名为 `--vite-built-dir`,并把原来的「就地删除白名单外文件」改成打包前预检报错、绝不动磁盘)、`scripts/vite.config.xhs-minitool.mjs` 注释点明 `build.outDir` 与 `--vite-built-dir` 必须一致、`SKILL.md` 补「被 AGC 导出面板调用时」三条硬契约;同步 `manifest.json` 指纹与 `skill_pack.rs` 的 `include_bytes!` 内容(文件数不变,仍 36 条)。
- 文档:主规范一节、本里程碑与实施计划、`docs/README.md` 入口、`docs/project-memory/shared-memory/decision-log.md` 一条。
- 契约常量的单一来源:Rust `export/mod.rs` 的 `EXPORT_WORK_RELATIVE_DIR` 与 `draft/xhs_minitool/layout.rs` 是权威,前端 `state/xhsMinitoolInstruction.ts` 只为插值留一份副本;这份副本由 `tests/xhsMinitoolContract.test.ts`(读上面两个 `.rs`)逐条钉住,两边任一侧改名都会红。前端指令用例只钉「正文有没有把契约写进去」,不再抄一份字面量。
- 明确不动:`GAME_CREATION_APP_COMMANDS`、`shared-contracts`、`server-rs`、SpacetimeDB、OpenAPI、`api-server`,以及现有 `project/export.rs` 的发布链路。
@@ -33,15 +33,16 @@
- `cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --bin genarrative-ai-game-creator-shell export::`
- 真实起进程跑宿主链路(默认 `#[ignore]`,要本机 node/npm 与可用命令沙箱):`cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --bin genarrative-ai-game-creator-shell -- --ignored executes_the_project_script`
- `cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml export_bindings`,随后 `npm run check:generated-bindings`
- `npx vitest run apps/ai-game-creator-shell/tests/artifactsPanel.test.tsx apps/ai-game-creator-shell/tests/xhsMinitoolExport.test.tsx`
- `npx vitest run apps/ai-game-creator-shell/tests/xhsMinitoolExport.test.tsx`
- `npm run ai-game-creator-shell:typecheck`
- `npm run agc:skill-pack:sync` → `npm run agc:skill-pack:check`
- `node --test apps/ai-game-creator-shell/src-tauri/resources/agc-skills/vite-export-xhs-minitool/scripts/.pack.test.mjs`、同目录 `.validate.test.mjs`
- `npm run check:doc-index`、`npm run check:encoding`、`git diff --check`
- 真实运行时:打开一个 vite 项目 → 点一键适配 → agent 跑通后不用点刷新(2 秒内导出按钮自己亮起来)→ 点导出 → 确认 `.export/xhs-minitool.zip` 可下载、内容可交给平台;再次导出确认零 agent 调用;手改坏注册表、删掉脚本各验证一次 rich error 文案。
## 风险与回滚点
- `pack.mjs` 会就地删掉 `--out-dir` 内非白名单扩展名的文件;prompt 与 skill 都要求把 `--out-dir` 指向 vite 产物目录,绝不能指向项目源码目录。
- `pack.mjs` 在打包前预检 `--vite-built-dir`:白名单外的扩展名直接报错退出,不再就地删除;prompt 与 skill 都要求把它指向 vite 产物目录并与 `build.outDir` 一致,配错只会失败,不会误删项目源码。
- `.export/` 排除出快照同步后适配成果不跟随恢复;prompt 与面板文案要说清「换机或恢复后需重新适配」,避免用户误以为已持久化。同机 checkpoint 仍含 `.export/`,是恢复适配脚本的兜底。
- `.export/` 一旦被并进通用排除口径,`agc_apply_patch` 会以「不得修改受保护或排除路径」拒绝它,首次适配直接卡死;两侧口径差别由 `project_snapshot_sync_policy_keeps_agent_state_and_still_blocks_credentials` 钉住。
- agent 写注册表是软约束(只靠 prompt 要求);宿主严格解析必须 fail closed,解析失败一律给 rich error 并引导重新适配,不做猜测性合并。
@@ -64,7 +65,7 @@
- 复制按钮改用 Tauri 剪贴板插件(原 `navigator.clipboard` 在 WebView 里可能根本没有,失败还会静默吞掉),失败态在按钮上可见。
- 指令正文里的契约字符串全部从常量组合(`XHS_MINITOOL_EXPORT_DIR_RELATIVE_PATH` + 三个脚本落点),用例的期望值也从常量推;`9d3018a49` / `6dd0dc00c` 之后:去掉刷新按钮,改成按固定间隔自动重读内容;内容与注册表指纹拆成两条命令(自动刷新拿不到写回基线);同步细节(指纹、保存中、待保存)退出前端对外状态与界面。
已验证:Rust `export::` 53 例(`export::draft::xhs_minitool` 37 例)、`export_bindings` 116 例、`project_snapshot` 22 例、`check:generated-bindings`(118 个文件)、前端 vitest 25 例(新增 19 + 契约 2 + 既有 4)、`ai-game-creator-shell:typecheck`、`ai-game-creator-shell:check:rust:shell`(本机 4 个 shard 都跑到了底,唯一失败是 4 个 `process_session` 真 PTY 用例——与下面的已知环境噪声同一批:沙箱里的 npm 由另一个 node 版本执行、`Cannot find module '../lib/cli.js'`;本次改动涉及的 `export::` 与 `project_snapshot` 全绿)、`agc:skill-pack:check`、`check:doc-index`、`check:encoding`、`git diff --check`。
已验证:Rust `export::` 53 例(`export::draft::xhs_minitool` 37 例)、`export_bindings` 116 例、`project_snapshot` 22 例、`check:generated-bindings`(118 个文件)、前端 vitest 21 例(新增 19 + 契约 2)、`ai-game-creator-shell:typecheck`、`ai-game-creator-shell:check:rust:shell`(本机 4 个 shard 都跑到了底,唯一失败是 4 个 `process_session` 真 PTY 用例——与下面的已知环境噪声同一批:沙箱里的 npm 由另一个 node 版本执行、`Cannot find module '../lib/cli.js'`;本次改动涉及的 `export::` 与 `project_snapshot` 全绿)、`agc:skill-pack:check`、`check:doc-index`、`check:encoding`、`git diff --check`。
未验证(唯一开口项):真实 vite 项目上的首轮适配闭环——agent 跑通 `build:xhs-minitool`、产出可上传 zip、二次导出零 agent 调用;以及 `.export/` 换机后重新适配的体感。宿主侧这一段已尽力自动化:`build.rs` 的 `executes_the_project_script_and_requires_a_non_empty_artifact_each_time`(`#[ignore]`)会真起进程执行项目脚本,覆盖「第一次产出 → 删掉产物再跑一次重新产出 → 脚本成功但没产出报 artifactMissing」。本机尝试运行被容器环境挡下,读日志时注意两点、别误判成 bug:
- 子进程输出里的 `running 1 test` 是**预期现象**:命令沙箱的垫片就是当前可执行文件本身(`desktop.rs` 按 `--command-sandbox-trampoline` 分流),编译到测试态时 `sandbox_trampoline_arguments()` 会改成 `--exact ...trampoline_child_fixture --ignored`,所以垫片进程是测试二进制在跑那一个 fixture 用例。
@@ -17,7 +17,7 @@
- 注册表 `.export/xhs-minitool.json`(首次自动创建且字段全空)与 flat 工作目录 `.export/`;共享 icon 放 `.export/` 根,多目标可指同一文件。
- 前端导出面板小红书 tab:表单展示/编辑/复制、icon 与 zip 下载、显式「适配 / 导出」两颗按钮(内容与脚本判据自动重读,没有刷新按钮)、冲突时逐字段二选一。
- 前端组装首次适配指令,经会话写权限门入队到项目主 Direct 会话。
- `vite-export-xhs-minitool` skill 的脚本与示例配置按 `.export/` 布局适配(`pack.mjs` 新增 `--zip-out <path>` 指定确定落点、vite config 示例注释点明 `outDir` 与 `--out-dir` 必须一致)。
- `vite-export-xhs-minitool` skill 的脚本与示例配置按 `.export/` 布局适配(`pack.mjs` 新增 `--zip-out <path>` 指定确定落点、`--out-dir` 更名为 `--vite-built-dir` 并在打包前预检该目录,vite config 示例注释点明 `build.outDir` 与它必须一致)。
- 主规范一节、本里程碑、实施计划与决策记录同步。
## 不在范围内
@@ -52,8 +52,9 @@
- 自动化(已落):
- Rust `cargo test ... --bin genarrative-ai-game-creator-shell export::` 53 例,其中 `export::draft::xhs_minitool` 37 例:注册表自动建空表单、格式漂移静默吸收、严格解析拒绝、表单首错、icon 越界/符号链接/扩展名、`hasScript` 现算、`contentHash` 冲突、产物路径守卫,以及构建收尾判据的逐条映射(脚本缺失、非零退出带退出码与输出尾部、超时无退出码、产物缺失、空产物不算、成功回输出尾部、截断只回省略量不回句子;layout 的 4 例只钉关系不抄实现——前端字符串相对路径与宿主 `PathBuf` 必须指向同一文件、相对路径不许跑出 `.export/`(含 Windows 反斜杠)、注册表与产物共用一个项目根锚定的 flat 目录、脚本名保住 `build:<后缀>` 形式且不许退化成裸 `build`)。
- `export_bindings` 116 例 + `npm run check:generated-bindings`(118 个生成文件)通过。
- 前端 `npx vitest run tests/xhsMinitoolExport.test.tsx`(19 例)、`tests/xhsMinitoolContract.test.ts`(2 例,把前端契约常量的值逐条对到 Rust `layout.rs` / `export/mod.rs`)与既有 `tests/artifactsPanel.test.tsx`(4 例)通过;`npm run ai-game-creator-shell:typecheck` 通过。其中 19 例覆盖(期望值一律从契约常量推,不另抄字面量):指令正文三条硬契约与 game/ 落点、失败现场拼装、失败变体人话映射与三向 action 分派(适配 / 修 / 重试)、未适配卡片自带入队按钮且按钮行不重复、注册表读回来之前不下「还没适配」结论、表单复制、icon 与 zip 下载、冲突逐字段二选一、自动保存带回读到的 `baseHash`、自动重读只更新脚本判据而不动用户未保存的表单、agent 事后改注册表/加脚本不用点刷新就能带回来、导出把脚本输出尾部报给用户、失败变体的产出量与前端拼出的省略说明、认不出形状的拒绝原样抛出且不落成业务提示、宿主侧事实故障先给卡片再抛一次且同一故障只抛一次,复制失败在按钮上可见(不静默吞掉),以及「只有适配/修复才叫 agent」。
- 前端 `npx vitest run tests/xhsMinitoolExport.test.tsx`(19 例)与 `tests/xhsMinitoolContract.test.ts`(2 例,把前端契约常量的值逐条对到 Rust `layout.rs` / `export/mod.rs`)通过;面板外壳原先那份 `tests/artifactsPanel.test.tsx`(4 例)整份删除——三个目标的列表与切 tab 后的文案都是存在性断言,关闭按钮只是把 `onClose` 转发一次,而它唯一有内容的「挂载的是主聊天同一个 `.project-chat-surface`」已被 `chatDialogFrameLayout.test.ts` 覆盖;`npm run ai-game-creator-shell:typecheck` 通过。其中 19 例覆盖(期望值一律从契约常量推,不另抄字面量):指令正文三条硬契约与 game/ 落点、失败现场拼装、失败变体人话映射与三向 action 分派(适配 / 修 / 重试)、未适配卡片自带入队按钮且按钮行不重复、注册表读回来之前不下「还没适配」结论、表单复制、icon 与 zip 下载、冲突逐字段二选一、自动保存带回读到的 `baseHash`、自动重读只更新脚本判据而不动用户未保存的表单、agent 事后改注册表/加脚本不用点刷新就能带回来、导出把脚本输出尾部报给用户、失败变体的产出量与前端拼出的省略说明、认不出形状的拒绝原样抛出且不落成业务提示、宿主侧事实故障先给卡片再抛一次且同一故障只抛一次,复制失败在按钮上可见(不静默吞掉),以及「只有适配/修复才叫 agent」。
- `.export/` 快照排除有定向用例:`project_snapshot::tests::project_snapshot_sync_policy_keeps_agent_state_and_still_blocks_credentials` 同时钉住「同步排除」与「通用口径不排除(Agent 仍可写)」。
- `npm run agc:skill-pack:sync` → `agc:skill-pack:check` 通过(`manifest.json` 版本 `2026-08-26.42`,文件数不变,仍 36 条)。
- `npm run agc:skill-pack:sync` → `agc:skill-pack:check` 通过(`manifest.json` 版本 `2026-08-26.43`,文件数不变,仍 36 条)。
- `pack.mjs` 的 no-delete 预检有定向用例(`scripts/.pack.test.mjs` 5 例):`findUnsupportedFiles` 只报白名单外扩展名、存在不支持文件时整体失败且不删任何文件/不写 zip、干净目录仍能打包、CLI 拒绝旧名 `--out-dir` 不静默退回默认目录。
- 运行时(**未验证**,本里程碑唯一开口项):在真实 vite 项目上由 agent 完成首轮适配、产出可人工上传的 zip、二次导出零 agent 调用;`.export/` 换机后需重新适配的体感也需一并确认。宿主侧这一段已有默认 `#[ignore]` 的真起进程用例(`executes_the_project_script_and_requires_a_non_empty_artifact_each_time`:第一次产出 → 删掉产物重跑必须重新产出 → 脚本成功但没产出报 artifactMissing),一键复跑命令见实施计划;本次尝试被容器环境的 npm/沙箱解析挡下,不是代码问题。
- 边界(已覆盖):注册表损坏、脚本缺失、脚本非零退出、产物缺失、icon 越界与符号链接、目录遍历拒绝。