导出面板的用例去掉恒真的存在性断言
- getBy* / findBy* 找不到元素就抛,外面那层 expect(...).not.toBeNull() 永远不会失败:删掉它,等待出现改用 findBy*,需要对照的用 queryBy* + toBeNull() - 能写成行为的就别写成存在:有脚本时点按钮行的「适配」看 adapt 是否被调用;读失败时同样点一下证明入口没变成摆设;切 tab 后补一条「卡片没有重新长回来」的负向断言 - artifactsPanel 三个目标改用 role=button(可点入口,不是三行说明文字),保留 querySelector 那几条(它们真的会返回 null) - team-conventions 记下这条断言口径,并给「不建立固定频率轮询」补上导出面板对 .export/ 的显式例外(那两件事实没有事件源,只能现读)
This commit is contained in:
@@ -31,20 +31,24 @@ describe('导出产物面板', () => {
|
||||
it('首屏用大卡片展示三个导出目标', () => {
|
||||
renderPanel();
|
||||
|
||||
expect(screen.getByText('陶泥儿')).not.toBeNull();
|
||||
expect(screen.getByText('小红书小工具')).not.toBeNull();
|
||||
expect(screen.getByText('TapTap H5 小游戏')).not.toBeNull();
|
||||
// 三个目标都以可点入口出现(`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 }));
|
||||
expect(screen.getByRole('tablist', { name: '导出目标' })).not.toBeNull();
|
||||
expect(screen.getByText('陶泥儿导出页尚未接入')).not.toBeNull();
|
||||
screen.getByRole('tablist', { name: '导出目标' });
|
||||
screen.getByText('陶泥儿导出页尚未接入');
|
||||
// 切到另一个目标只换 tab 页,卡片不会重新长回来。
|
||||
expect(screen.queryByRole('button', { name: /陶泥儿/u })).toBeNull();
|
||||
|
||||
fireEvent.click(screen.getByRole('tab', { name: /TapTap H5 小游戏/u }));
|
||||
expect(screen.getByText('TapTap H5 小游戏导出页尚未接入')).not.toBeNull();
|
||||
screen.getByText('TapTap H5 小游戏导出页尚未接入');
|
||||
expect(screen.queryByText('陶泥儿导出页尚未接入')).toBeNull();
|
||||
});
|
||||
|
||||
it('小红书 tab 挂载的是主聊天同一个对话盒', () => {
|
||||
|
||||
@@ -172,9 +172,7 @@ describe('面板左半', () => {
|
||||
|
||||
await waitFor(() => expect(writeText).toHaveBeenCalledWith('小工具'));
|
||||
// 成功态也写进无障碍名,用户与读屏都看得到「已复制」。
|
||||
await waitFor(() =>
|
||||
expect(screen.getByRole('button', { name: '已复制名称' })).not.toBeNull(),
|
||||
);
|
||||
await screen.findByRole('button', { name: '已复制名称' });
|
||||
});
|
||||
|
||||
it('复制失败不静默吞掉,按钮自己说一声', async () => {
|
||||
@@ -183,11 +181,7 @@ describe('面板左半', () => {
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '复制名称' }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByRole('button', { name: '复制失败名称' }),
|
||||
).not.toBeNull(),
|
||||
);
|
||||
await screen.findByRole('button', { name: '复制失败名称' });
|
||||
});
|
||||
|
||||
it('icon 与 zip 的下载都走原生保存对话框与素材保存命令', async () => {
|
||||
@@ -243,25 +237,37 @@ describe('面板左半', () => {
|
||||
renderPane(withScript);
|
||||
fireEvent.click(screen.getByRole('button', { name: '导出' }));
|
||||
expect(withScript.runExport).toHaveBeenCalledTimes(1);
|
||||
// 有脚本时那颗按钮回到按钮行,卡片不再出现。
|
||||
expect(screen.getByRole('button', { name: '适配' })).not.toBeNull();
|
||||
// 有脚本时卡片不再出现,适配入口回到按钮行;它仍然能入队,不是个摆设。
|
||||
expect(
|
||||
screen.queryByRole('button', { name: '交给 code agent 适配' }),
|
||||
).toBeNull();
|
||||
fireEvent.click(screen.getByRole('button', { name: '适配' }));
|
||||
expect(withScript.adapt).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('注册表还没读回来时不下「还没适配」的结论', () => {
|
||||
renderPane(fakeMinitool({ form: null, loading: true, hasScript: false }));
|
||||
|
||||
// 读取中给一行状态,但不下「还没适配」的结论、也不摆那张自带入口的卡片。
|
||||
screen.getByText('正在读取导出注册表…');
|
||||
expect(screen.queryByText(missingScriptTip())).toBeNull();
|
||||
expect(screen.getByText('正在读取导出注册表…')).not.toBeNull();
|
||||
expect(
|
||||
screen.queryByRole('button', { name: '交给 code agent 适配' }),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('读失败时保留按钮行里的适配入口,不摆未适配卡片', () => {
|
||||
renderPane(fakeMinitool({ form: null, loading: false, hasScript: false }));
|
||||
const minitool = fakeMinitool({
|
||||
form: null,
|
||||
loading: false,
|
||||
hasScript: false,
|
||||
});
|
||||
renderPane(minitool);
|
||||
|
||||
expect(screen.queryByText(missingScriptTip())).toBeNull();
|
||||
expect(screen.getByRole('button', { name: '适配' })).not.toBeNull();
|
||||
// 读失败不等于入口失效:那颗按钮还在,点下去照样把任务交给 agent。
|
||||
fireEvent.click(screen.getByRole('button', { name: '适配' }));
|
||||
expect(minitool.adapt).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('冲突面板逐字段二选一,按选择保存的合并结果交回上层', () => {
|
||||
@@ -295,9 +301,8 @@ describe('面板左半', () => {
|
||||
});
|
||||
renderPane(neverAdapted);
|
||||
|
||||
expect(
|
||||
screen.getByText('宿主找过:package.json、game/package.json'),
|
||||
).not.toBeNull();
|
||||
// 现场照载荷原文写出来(宿主找过哪些 package.json 由载荷决定,前端不自己编)。
|
||||
screen.getByText('宿主找过:package.json、game/package.json');
|
||||
expect(
|
||||
screen.queryByRole('button', { name: '让 code agent 修' }),
|
||||
).toBeNull();
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
- `5e2426c8a` 导出收尾判据抽成 `conclude(...)` 并补 5 例确定性用例(非零退出 / 超时 / 产物缺失 / 空产物 / 成功),同时去掉 `XHSMiniToolExportRunResult.exitCode` 这个恒为 0 的死字段;`463341c4b` 导出成功后把脚本输出尾部报给用户;`b3be3ee71` 证据口径修正;`5dd158ee1` 补默认 ignore 的真实起进程用例。
|
||||
- `1b7741ac6` 适配与修复指令补上「包根在 game/ 时落点写成 `../.export/xhs-minitool.zip`」,skill 契约同步(manifest `2026-08-26.42`);`f8ed4e898` 保存对话框的默认文件名收进下载助手并复用既有取文件名工具;`90f651ddf` 刷新时先撤掉待触发的自动保存(附回归用例;该行为已被下面的自动刷新取代——轮询不再撤掉待保存的输入,因为「磁盘为准」的刷新动作没有了)。
|
||||
- `ac666daad` 之后:未适配提示改成带入队按钮的卡片、失败卡片按变体分派适配/修/重试、提示只在注册表读回来之后出现(同一时刻只有一张卡片、一个入口)。
|
||||
- 导出面板的用例去掉恒真的存在性断言(`getBy*` 本来就抛,不再包 `expect(...).not.toBeNull()`;等待出现改用 `findBy*`,能写成行为断言的就点一下按钮看回调),并把这条口径写进 team-conventions。
|
||||
- layout 的用例重写:原来那条 `assert_eq!(artifact_relative_path(), format!(...))` 只是把实现抄了一遍,现改成 4 条只钉关系的用例(相对路径与宿主 `PathBuf` 指向同一文件、不许跑出 `.export/` 且不出现反斜杠、注册表与产物共用一个项目根锚定的 flat 目录、脚本名保住 `build:<后缀>` 形式);逐条反证过会红,`export::` 50 → 53 例。
|
||||
- `dffe4de55` 导出链路按 ADR 收口(策略拒绝与宿主故障拆成两个变体、输出尾部只回原文与省略量);`df3a6826b` 前端失败通道按 ADR 分流(宿主故障与未分类拒绝原样抛出并去重)与复制失败可见反馈。
|
||||
- 复制按钮改用 Tauri 剪贴板插件(原 `navigator.clipboard` 在 WebView 里可能根本没有,失败还会静默吞掉),失败态在按钮上可见。
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
- AGC 模板库灰度复用 `agc:template-library`:未配置关闭,已配置时遵循现有灰度启停、用户 ID/标签和比例规则;服务端返回权威结论,客户端入口和原生清单/下载/建项均执行门禁,主体切换丢弃旧异步结果。公开 OSS 不是保密边界,已创建项目不受影响。
|
||||
|
||||
- 画布卡片类型与信息角标共用 `CanvasCardCornerActions`;菜单收纳共用 `OverflowActions`,宿主决定展示数量和资源命令。AGC 选中菜单前 5 项直显,Web 默认不折叠;浮层 portal 继续接入现有画布关闭与滚轮归属判据。
|
||||
- 断言只写在能失败的地方:`getBy*` / `findBy*` 找不到就抛,不要再包一层 `expect(...).not.toBeNull()` / `toBeDefined()`(那种写法恒真,还让每条查询多一层噪声);等元素出现用 `findBy*` 或 `await waitFor(() => screen.getBy*())`,负向用 `queryBy*` + `toBeNull()`。同理不写复述实现的断言(例如把被测函数的表达式在测试里再抄一遍),改为钉关系、边界或行为,并能逐条反证会红。
|
||||
- 修改范围保持聚焦;优先扩展现有系统、页面、组件、DTO 和脚本。
|
||||
- Agent 可见内容直接描述当前任务、输入和成功条件,细节按调用需要提供。
|
||||
- AGC 外置智能体提示词只写模型必须遵守的指令与契约(工具名与参数、调用顺序、禁止项、失败处理、产物要求),不写客户端/宿主怎么实现:内部预算字段与限额、内部状态机与枚举、执行器锁与沙箱技术栈、进程/插件部署、注入与投影管线都属于宿主实现,一律改写成对应行为要求或删除。
|
||||
@@ -52,7 +53,7 @@
|
||||
- 后端遵循 `module-*`、`spacetime-module`、`spacetime-client`、`api-server`、`platform-*`、`shared-contracts` 的现役边界。
|
||||
- 前端只负责表现、交互和临时 UI 状态;正式状态来自后端投影、API 或持久化契约。
|
||||
- AGC 渲染层是离线前端:平台接口、OSS 素材直传、Provider、错误上报与账户/认证的网络 IO 全部在 Rust 的 reqwest facade,渲染层不得再出现 `fetch(` / `XMLHttpRequest` / `EventSource` / `sendBeacon(` / `new WebSocket(` 或 `@tauri-apps/plugin-http`;客户端窗口 capability 也不得授予 `http:default`。`check:native-shells --groups=contract` 的 `ai-game-creator-shell-user-dev-boundary` 用负向门禁钉住这条,改动渲染层网络边界必须同批改该门禁与 `build-release.test.mjs`。
|
||||
- AGC 工作区的正式状态变化走 Rust 事件 + 一次受控快照读取,不建立固定频率轮询(纯 UI 计时器、拖拽重复器与动画 tick 除外):渲染层先订阅再读快照,事件重复或内容未变时保持数组身份,卸载后迟到事件不写回。Direct 活动回合的唯一事实源是 Direct 线程管理器的活动回合快照(`list_direct_active_turns` 只读它;登记、进度内容变化与收口各广播一次 `game-creator-direct-active-turns-changed`),渲染层与 Rust 都不得再引第二份注册表。
|
||||
- AGC 工作区的正式状态变化走 Rust 事件 + 一次受控快照读取,不建立固定频率轮询(纯 UI 计时器、拖拽重复器与动画 tick 除外;导出面板对 `.export/` 注册表与 npm 脚本的 2 秒重读是显式例外——那两件事实由用户项目和 agent 直接改文件,没有事件源,只能现读):渲染层先订阅再读快照,事件重复或内容未变时保持数组身份,卸载后迟到事件不写回。Direct 活动回合的唯一事实源是 Direct 线程管理器的活动回合快照(`list_direct_active_turns` 只读它;登记、进度内容变化与收口各广播一次 `game-creator-direct-active-turns-changed`),渲染层与 Rust 都不得再引第二份注册表。
|
||||
- AGC 维护态(平台 `503` 且命中 `MAINTENANCE` 或「维护」)由 Rust 在平台请求的错误分支经 `platform_maintenance::watch_platform_response` 分类并广播 `genarrative-client-maintenance-detected`;渲染层只订阅并打开唯一的「系统维护中」弹窗,不自行解析 HTTP,业务面板各自的错误文案保持不变。
|
||||
- 对已明确退役且无现役调用方、公开契约、持久化数据、活跃实例或迁移要求的对象,直接清理实现、专属测试和说明,将权威文档更新为当前状态;历史由 Git 保存。公开契约、持久化数据和正式迁移按实际需求保留最小兼容及对应测试。
|
||||
- 修改 `/api/external/v1` 时,同批更新 `docs/openapi/genarrative-external-v1.openapi.json` 与契约测试。
|
||||
|
||||
Reference in New Issue
Block a user