docs(B02): specify installed recommendation styling and footer removal
This commit is contained in:
1 parent
ddd677bae3
commit
d2530cf3b5
7 files changed
+907
No files matched your search
@@ -0,0 +1,83 @@
|
|||||||
|
# B02:推荐技能的已安装样式与重复入口
|
||||||
|
|
||||||
|
日期:2026-10-21(Asia/Shanghai)。状态:**待开发**。用户新增需求,不改变 B01 的范围、交付或验收结论。
|
||||||
|
|
||||||
|
用户要求:“专家技能连接器这块的技能,已经安装的在推荐中的样式也不对,还保留有添加的按钮,参考一下 WorkBuddy。还有下方也不应该有查看已安装技能,这个要移除掉。”
|
||||||
|
|
||||||
|
## 1. 已确认问题与复现
|
||||||
|
|
||||||
|
接收时 `gitea/master` 为 `ddd677bae3c43a5514eb40f7a1d9749fdf389115`。独立窗口使用既有固定代码 `51531dcae8d8ade85db9e203bc8c0f712805b2ae`,对应推荐技能组件、样式、CardFrame 与测试从该提交到接收版本无改动。
|
||||||
|
|
||||||
|
Windows Electron 加载真实 `CapabilityCenter`、推荐技能组件和 CSS,使用受控 IPC、临时 userData 与模拟安装结果,不下载网络包或改用户技能。安装后关闭详情,推荐列表实测:
|
||||||
|
|
||||||
|
```text
|
||||||
|
data-recommendation-state = current
|
||||||
|
卡片 class = sm-card,没有 sm-card--installed
|
||||||
|
添加按钮仍存在,disabled=true,按钮中仍有 Plus SVG
|
||||||
|
原动作槽位没有 sm-use-btn
|
||||||
|
卡片下方另有“去使用”文字动作
|
||||||
|
列表底部存在“查看已安装技能”按钮
|
||||||
|
renderer errors = []
|
||||||
|
```
|
||||||
|
|
||||||
|
所以不是“完全没有识别到已安装”,而是**识别成功后,卡片仍渲染添加动作与未安装壳**。不能把仅禁用加号当作完成样式修复。
|
||||||
|
|
||||||
|
源码定位:
|
||||||
|
|
||||||
|
- `src/renderer/src/capabilities/RecommendedSkills.tsx:259`:`Card` 没有传 installed 身份;actions 无条件创建 `.sm-add-btn` 和 `PlusIcon`,current 状态只影响 disabled。
|
||||||
|
- `RecommendedSkills.tsx:263`:独立 footer 按钮“查看已安装技能”。
|
||||||
|
- `RecommendedSkills.tsx:247`:目录不可用分支也有同名入口,需与正常列表的移除要求一并检查,不要留下另一处无依据的底部重复入口。
|
||||||
|
- `src/renderer/src/capabilities/SkillsPage.tsx:103`:推荐页当前的 installed 导航 callback;顶部已有“已安装(n)”入口,应保留这一条正常导航。
|
||||||
|
- `scripts/test-recommended-skills-ui.mjs:232`:旧测试要求 `.sm-add-btn.disabled === true`;`:241` 附近依赖 footer 按钮进入已安装。这两处把当前错误设计写进了测试,需同步修正。
|
||||||
|
|
||||||
|
证据:[实测状态](evidence/B02-01/installed-state.json)、[推荐列表截图](evidence/B02-01/installed-recommendation-list.png)、[只读窗口探针](evidence/B02-01/skills-installed-probe.mjs)。
|
||||||
|
|
||||||
|
## 2. WorkBuddy 依据与未验证边界
|
||||||
|
|
||||||
|
可复用的已有 REA 恢复记录:
|
||||||
|
|
||||||
|
- `src/renderer/src/capabilities/skills-source-manifest.json` 标注 WorkBuddy **5.8.2**,原包 SHA-256 `035254bc76310ea46317433e4840602094543ef323d2d5c5aff7c131729de918`。
|
||||||
|
- Evidence:JavaScript `ev_717fa5e846e64d67ac19c8925d121bff632c0d3aefb3603b10858559cabb2dcb`,技能市场相关 `ev_e8faf60b3d22b99ede302f2d0efdfb3145f53f59789d4a345f5892dc13a589a4`,CSS `ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94`。
|
||||||
|
- 该恢复链在项目中已有 `CardFrame.installed`/`sm-card--installed`、`UseSkillIcon` 与 `.sm-use-btn`。已恢复 CSS 的使用动作是 24×24px、默认透明背景、16px SVG;与未安装 `.sm-add-btn` 的 24×24px 灰底、12px SVG 不同。
|
||||||
|
- [技能详情安装与使用链路修正](../技能详情安装与使用链路修正-2026-10-20.md) 第原版边界部分记录:历史原版 ActionRow 随安装状态切换安装/使用,成功提示带使用动作。不要把“+保留但禁用”当作原版已安装设计。
|
||||||
|
|
||||||
|
**本次当前版本并未完整核验**:
|
||||||
|
|
||||||
|
- 本轮仅通过 REA 静态读取当前安装包,没有用直接 CDP、手写提取器、computer-use 或其他通道绕过。
|
||||||
|
- 当前 ASAR SHA-256 为 `026a1d0805a2366d06390cef52e157a7277543cdd27dd308e6b1acb47ffffdc1`,版本号未读取确认,不能继续写成 5.8.2/5.8.3。
|
||||||
|
- REA 父 Evidence `ev_ef70f1417c069be6bf352efcb858244efaae444f71f1f9893ed2fb579ed88849`,摘要 Evidence `ev_4d76ad1b9f386f97343307067ee2f5b5b91d2189366ab1cbe3890954db4634a6`。本次选择的源文件是 `renderer/assets/src-DbE1_mgM.js`,不是当前完整技能市场组件;存在完整性矛盾、未选文件和语义覆盖上限,**不据此对当前技能市场样式作结论**。
|
||||||
|
- REA 整包提取又因缺失声明的 Darwin companion `cli/node_modules/koffi/build/koffi/darwin_arm64/koffi.node` 失败。没有绕过改用其他提取器,也没有启动 WorkBuddy 去安装测试技能。
|
||||||
|
- [完整摘要记录](evidence/B02-01/rea-summary-record.json)、[REA 提取限制](evidence/B02-01/rea-extract-limitation.json) 已提交。父 Evidence 的大型 bundle 仅保留于验收端临时证据,不把其路径当作跨机交接的唯一依据。
|
||||||
|
|
||||||
|
开发可依据已有恢复组件和用户明确要求先修确认问题;当前原版推荐卡是否另有状态图标、精确操作位置及复杂安装态细节,须后续通过 REA 补同状态证据,未拿到不能宣称完整 1:1。
|
||||||
|
|
||||||
|
## 3. 开发任务与验收标准
|
||||||
|
|
||||||
|
### YC-005 / P2:推荐卡片切换真实已安装状态
|
||||||
|
|
||||||
|
允许边界:`RecommendedSkills.tsx`、`SkillsPage.tsx`、推荐局部 CSS/既有卡片适配、相关 UI 回归。原版恢复组件本体、共享卡片与技能身份模型如需变更,由开发主对话统一审查;不重写安装下载后端或扩展市场服务。
|
||||||
|
|
||||||
|
- **AC5.1**:同推荐身份、同来源版本的 current 安装项,卡片不再渲染添加 Plus 按钮,不仅是禁用或透明隐藏;使用与已安装语义一致的卡片状态和动作。辅助技术标签也不能继续叫“安装某技能”。
|
||||||
|
- **AC5.2**:按既有 REA 恢复记录实现原版的已安装/使用展示,核对布局、图标槽位、字体、默认/hover/focus 与浅深色;不要把文字“已安装”加在角落就认定完成。
|
||||||
|
- **AC5.3**:未安装项仍可添加并显示真实安装进度;安装成功后自动切换,关闭/重开页面及外部安装/卸载通知后状态同步,不要求重启。
|
||||||
|
- **AC5.4**:禁用、不可调用、损坏、同名本地、版本不同、本地修改、工作区技能等状态仍明确区分。不要只按同名或当前标签把不同来源/scope 误认为同一个已安装技能;保留已有保护工作区和本地修改的规则。
|
||||||
|
- **AC5.5**:版本不同/本地修改等确有替换需求时,用清楚的更新/替换动作及既有确认流程,不伪装成首次添加,也不直接覆盖。正常 current 状态不发重复安装请求。
|
||||||
|
- **AC5.6**:使用动作保留既有进入新任务草稿、不自动发送的语义;禁用技能先启用及失败反馈保持。原动作槽位与卡片下方不要同时留下无依据的重复使用按钮;按原版证据确定位置。
|
||||||
|
|
||||||
|
### YC-006 / P2:移除底部“查看已安装技能”
|
||||||
|
|
||||||
|
- **AC6.1**:正常推荐列表底部不再渲染 `.yc-skill-recommendation-footer` 及“查看已安装技能”按钮,清理只服务于该入口的空白、样式和 props;不能只是 CSS 隐藏。
|
||||||
|
- **AC6.2**:保留顶部“已安装(n)”导航及真实计数,进入已安装、返回推荐、搜索与目录刷新仍正常。
|
||||||
|
- **AC6.3**:复核目录不可用/空状态中的重复同名入口;保持清楚的错误与恢复方式,使用现有顶部导航,不为删除正常 footer 而留下另一个底部重复入口。未使用的 callback 清理要核对所有分支与调用方。
|
||||||
|
- **AC6.4**:测试通过顶部现有入口进入已安装,新增断言 footer 不存在;不允许为旧测试便利保留用户明确要求移除的按钮。
|
||||||
|
|
||||||
|
## 4. 验证与交付
|
||||||
|
|
||||||
|
- 必跑 `npm run test:recommended-skills:ui`、相关能力输入/技能管理回归,以及 `npm run build`。源码/class 检查不能替代实际已安装推荐卡的计算样式、截图与交互。
|
||||||
|
- 受控 Electron fixture 覆盖未安装→安装→current、禁用/损坏、不同来源与版本、本地修改、卸载/重装及目录错误;不得使用用户真实技能或为验样式安装 WorkBuddy 技能。
|
||||||
|
- 对已有 UI 脚本中的“禁用 Plus 即通过”和 footer 导航断言改成真实目标,保留安装确认、进度、取消、重复请求、外部通知、使用草稿等有效回归。
|
||||||
|
- Windows 浅/深、窄窗下记录相同状态的 DOM、计算样式和截图;同版本 WorkBuddy 对照尚缺则明确“未验证”,不将本地恢复样式称为像素通过。
|
||||||
|
- 本次复现探针在固定受测检出运行:独立安装依赖后,将 Git 中的 `skills-installed-probe.mjs` 复制为 `.tmp/b01-acceptance/skills-installed-probe.mjs` 再执行。该路径用于其自身 Electron 子进程启动;脚本只用受控宿主,不真实安装。
|
||||||
|
- 创建 `docs/协作/B02-交付-01.md` 接单、记录完整代码 SHA、逐项 AC、命令及证据。不要预填验收通过;不把此项混入 B01/YC-001 的修复完成声明。
|
||||||
|
|
||||||
|
本次只发布新需求和证据,**未修改产品代码**。文档推送成功后窗口交回开发端,用户可将 B02 与 B01 的后续安排一并交给开发对话。
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"codeSha": "51531dcae8d8ade85db9e203bc8c0f712805b2ae",
|
||||||
|
"installedState": {
|
||||||
|
"state": "current",
|
||||||
|
"cardClasses": "sm-card",
|
||||||
|
"installedClass": false,
|
||||||
|
"addButtonPresent": true,
|
||||||
|
"addButtonDisabled": true,
|
||||||
|
"addSvgPresent": true,
|
||||||
|
"useSlotPresent": false,
|
||||||
|
"footerPresent": true,
|
||||||
|
"footerText": "查看已安装技能"
|
||||||
|
},
|
||||||
|
"errors": [],
|
||||||
|
"scope": "Actual recommended-skill component and CSS in Electron with controlled host; no network or actual skill installation"
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"error":"Analysis failed","code":"artifact_operation_failed","category":"unavailable","message":"ASAR unpacked companion bytes are unavailable for cli/node_modules/koffi/build/koffi/darwin_arm64/koffi.node. Select the archive in place with its .unpacked directory beside it, then retry.","retryable":false,"remediation":{"action":"ASAR unpacked companion bytes are unavailable for cli/node_modules/koffi/build/koffi/darwin_arm64/koffi.node. Select the archive in place with its .unpacked directory beside it, then retry."},"details":{"logical_path":"cli/node_modules/koffi/build/koffi/darwin_arm64/koffi.node","declared_sha256":null,"calculated_sha256":null,"unpacked":true}}
|
||||||
@@ -0,0 +1,419 @@
|
|||||||
|
{
|
||||||
|
"analysis_profile": null,
|
||||||
|
"authority": "shipped-artifact",
|
||||||
|
"confidence": "derived",
|
||||||
|
"environment": null,
|
||||||
|
"evidence_id": "ev_4d76ad1b9f386f97343307067ee2f5b5b91d2189366ab1cbe3890954db4634a6",
|
||||||
|
"evidence_links": [
|
||||||
|
"ev_ef70f1417c069be6bf352efcb858244efaae444f71f1f9893ed2fb579ed88849"
|
||||||
|
],
|
||||||
|
"limitations": [
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/bin/native/win32-x64/job-object.node; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/node_modules/@lydell/node-pty-win32-x64/prebuilds/win32-x64/conpty.node; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/node_modules/@lydell/node-pty-win32-x64/prebuilds/win32-x64/conpty/OpenConsole.exe; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/node_modules/@lydell/node-pty-win32-x64/prebuilds/win32-x64/conpty_console_list.node; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/node_modules/@lydell/node-pty/package.json; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/node_modules/koffi/build/koffi/win32_x64/koffi.node; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/node_modules/koffi/package.json; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/node_modules/koffi/src/cnoke/package.json; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/vendor/genie-trash/win32-x64.exe; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/vendor/ripgrep/x64-win32/rg.exe; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/vendor/ripgrep/x64-win32/ripgrep.node; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/vendor/sandbox/5.8.6/betterleaks.exe; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/vendor/sandbox/5.8.6/sandbox-center.exe; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/vendor/sandbox/5.8.6/sandbox-cli-gc.exe; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/vendor/sandbox/5.8.6/sandbox-cli.exe; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/vendor/sandbox/5.8.6/sandbox_ffi.dll; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/vendor/sandbox/5.8.6/tsbx.dll; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/vendor/sandbox/5.8.6/tsbx_sdk.dll; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/vendor/sandbox/5.8.6/tsbxldr.dll; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/vendor/sandbox/5.8.6/tsbxlow.dll; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/vendor/wecom-cli/wecom-cli.exe; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at native/perf-metrics/build/Release/perf_metrics.node; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at native/turing-sdk/build/Release/turing_sdk.node; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at native/wechat-copydata-decoder/build/Release/wechat_copydata_decoder.node; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at node_modules/@ban-dal/os-bridge-win32-x64-msvc/os-bridge.win32-x64-msvc.node; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at node_modules/@lydell/node-pty-win32-x64/prebuilds/win32-x64/conpty.node; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at node_modules/@lydell/node-pty-win32-x64/prebuilds/win32-x64/conpty/OpenConsole.exe; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at node_modules/@lydell/node-pty-win32-x64/prebuilds/win32-x64/conpty_console_list.node; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at node_modules/@tencent/qimei-node/build/Release/qimei.node; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at node_modules/@tencent/tencent-docs-ai-engine/bin/win32-x64/editor_sdk.exe; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at node_modules/@tencent/wwapi-electron/prebuilds/win32-x64/node.napi.node; the observed bytes are untrusted.",
|
||||||
|
"34 additional limitations omitted from this view; consult source parent Evidence ev_ef70f1417c069be6bf352efcb858244efaae444f71f1f9893ed2fb579ed88849 for the complete list (the caller-supplied record, or its retaining session)."
|
||||||
|
],
|
||||||
|
"locations": [
|
||||||
|
{
|
||||||
|
"kind": "artifact-path",
|
||||||
|
"path": "C:\\Users\\Administrator\\AppData\\Local\\Programs\\WorkBuddy\\resources\\app.asar"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"normalized_result": {
|
||||||
|
"artifact": {
|
||||||
|
"path": "C:\\Users\\Administrator\\AppData\\Local\\Programs\\WorkBuddy\\resources\\app.asar",
|
||||||
|
"sha256": "026a1d0805a2366d06390cef52e157a7277543cdd27dd308e6b1acb47ffffdc1"
|
||||||
|
},
|
||||||
|
"coverage": {
|
||||||
|
"examined": 7200,
|
||||||
|
"exhausted": true,
|
||||||
|
"next_offset": null,
|
||||||
|
"status": "complete-within-view",
|
||||||
|
"total": 30363
|
||||||
|
},
|
||||||
|
"kind": "summary",
|
||||||
|
"limitations": [
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/bin/native/win32-x64/job-object.node; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/node_modules/@lydell/node-pty-win32-x64/prebuilds/win32-x64/conpty.node; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/node_modules/@lydell/node-pty-win32-x64/prebuilds/win32-x64/conpty/OpenConsole.exe; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/node_modules/@lydell/node-pty-win32-x64/prebuilds/win32-x64/conpty_console_list.node; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/node_modules/@lydell/node-pty/package.json; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/node_modules/koffi/build/koffi/win32_x64/koffi.node; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/node_modules/koffi/package.json; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/node_modules/koffi/src/cnoke/package.json; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/vendor/genie-trash/win32-x64.exe; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/vendor/ripgrep/x64-win32/rg.exe; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/vendor/ripgrep/x64-win32/ripgrep.node; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/vendor/sandbox/5.8.6/betterleaks.exe; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/vendor/sandbox/5.8.6/sandbox-center.exe; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/vendor/sandbox/5.8.6/sandbox-cli-gc.exe; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/vendor/sandbox/5.8.6/sandbox-cli.exe; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/vendor/sandbox/5.8.6/sandbox_ffi.dll; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/vendor/sandbox/5.8.6/tsbx.dll; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/vendor/sandbox/5.8.6/tsbx_sdk.dll; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/vendor/sandbox/5.8.6/tsbxldr.dll; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/vendor/sandbox/5.8.6/tsbxlow.dll; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/vendor/wecom-cli/wecom-cli.exe; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at native/perf-metrics/build/Release/perf_metrics.node; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at native/turing-sdk/build/Release/turing_sdk.node; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at native/wechat-copydata-decoder/build/Release/wechat_copydata_decoder.node; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at node_modules/@ban-dal/os-bridge-win32-x64-msvc/os-bridge.win32-x64-msvc.node; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at node_modules/@lydell/node-pty-win32-x64/prebuilds/win32-x64/conpty.node; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at node_modules/@lydell/node-pty-win32-x64/prebuilds/win32-x64/conpty/OpenConsole.exe; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at node_modules/@lydell/node-pty-win32-x64/prebuilds/win32-x64/conpty_console_list.node; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at node_modules/@tencent/qimei-node/build/Release/qimei.node; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at node_modules/@tencent/tencent-docs-ai-engine/bin/win32-x64/editor_sdk.exe; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at node_modules/@tencent/wwapi-electron/prebuilds/win32-x64/node.napi.node; the observed bytes are untrusted.",
|
||||||
|
"34 additional limitations omitted from this view; consult source parent Evidence ev_ef70f1417c069be6bf352efcb858244efaae444f71f1f9893ed2fb579ed88849 for the complete list (the caller-supplied record, or its retaining session)."
|
||||||
|
],
|
||||||
|
"parent_digest": "ef70f1417c069be6bf352efcb858244efaae444f71f1f9893ed2fb579ed88849",
|
||||||
|
"parent_evidence_id": "ev_ef70f1417c069be6bf352efcb858244efaae444f71f1f9893ed2fb579ed88849",
|
||||||
|
"parent_operation": "analyze_javascript_application",
|
||||||
|
"summary": {
|
||||||
|
"coverage": {
|
||||||
|
"application": {
|
||||||
|
"limits": [],
|
||||||
|
"omitted_count": null,
|
||||||
|
"status": "partial",
|
||||||
|
"truncated": false
|
||||||
|
},
|
||||||
|
"application_unknowns": 11145,
|
||||||
|
"semantic": {
|
||||||
|
"families": [
|
||||||
|
{
|
||||||
|
"family": "boundary",
|
||||||
|
"omitted_relations": null,
|
||||||
|
"retained_relations": 0,
|
||||||
|
"status": "partial",
|
||||||
|
"unknown_count": 6667
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"family": "call-flow",
|
||||||
|
"omitted_relations": null,
|
||||||
|
"retained_relations": 0,
|
||||||
|
"status": "partial",
|
||||||
|
"unknown_count": 6667
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"family": "child-process",
|
||||||
|
"omitted_relations": null,
|
||||||
|
"retained_relations": 0,
|
||||||
|
"status": "partial",
|
||||||
|
"unknown_count": 6667
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"family": "closure",
|
||||||
|
"omitted_relations": null,
|
||||||
|
"retained_relations": 5729,
|
||||||
|
"status": "partial",
|
||||||
|
"unknown_count": 6667
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"family": "configuration",
|
||||||
|
"omitted_relations": null,
|
||||||
|
"retained_relations": 0,
|
||||||
|
"status": "partial",
|
||||||
|
"unknown_count": 6667
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"family": "data-flow",
|
||||||
|
"omitted_relations": null,
|
||||||
|
"retained_relations": 0,
|
||||||
|
"status": "partial",
|
||||||
|
"unknown_count": 18596
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"family": "event",
|
||||||
|
"omitted_relations": null,
|
||||||
|
"retained_relations": 0,
|
||||||
|
"status": "partial",
|
||||||
|
"unknown_count": 6667
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"family": "object-flow",
|
||||||
|
"omitted_relations": null,
|
||||||
|
"retained_relations": 0,
|
||||||
|
"status": "partial",
|
||||||
|
"unknown_count": 14738
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"family": "promise-ownership",
|
||||||
|
"omitted_relations": null,
|
||||||
|
"retained_relations": 0,
|
||||||
|
"status": "partial",
|
||||||
|
"unknown_count": 6666
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"family": "request",
|
||||||
|
"omitted_relations": null,
|
||||||
|
"retained_relations": 0,
|
||||||
|
"status": "partial",
|
||||||
|
"unknown_count": 6666
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"family": "resource-lifecycle",
|
||||||
|
"omitted_relations": null,
|
||||||
|
"retained_relations": 0,
|
||||||
|
"status": "partial",
|
||||||
|
"unknown_count": 6666
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"family": "timer",
|
||||||
|
"omitted_relations": null,
|
||||||
|
"retained_relations": 0,
|
||||||
|
"status": "partial",
|
||||||
|
"unknown_count": 6666
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"limits": [
|
||||||
|
{
|
||||||
|
"name": "semantic_graph_file_node_ceiling",
|
||||||
|
"unit": "items",
|
||||||
|
"value": 20000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "semantic_graph_file_unknown_ceiling",
|
||||||
|
"unit": "items",
|
||||||
|
"value": 20000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "semantic_graph_node_ceiling",
|
||||||
|
"unit": "items",
|
||||||
|
"value": 100000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "semantic_graph_unknown_ceiling",
|
||||||
|
"unit": "items",
|
||||||
|
"value": 100000
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"omitted_nodes": null,
|
||||||
|
"omitted_relations": null,
|
||||||
|
"omitted_unknown_occurrences": [
|
||||||
|
{
|
||||||
|
"family": "boundary",
|
||||||
|
"occurrences": 3618,
|
||||||
|
"reason": "incomplete-module"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"family": "call-flow",
|
||||||
|
"occurrences": 23349,
|
||||||
|
"reason": "dynamic-call"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"family": "call-flow",
|
||||||
|
"occurrences": 3618,
|
||||||
|
"reason": "incomplete-module"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"family": "child-process",
|
||||||
|
"occurrences": 3618,
|
||||||
|
"reason": "incomplete-module"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"family": "closure",
|
||||||
|
"occurrences": 3618,
|
||||||
|
"reason": "incomplete-module"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"family": "configuration",
|
||||||
|
"occurrences": 3618,
|
||||||
|
"reason": "incomplete-module"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"family": "data-flow",
|
||||||
|
"occurrences": 3618,
|
||||||
|
"reason": "incomplete-module"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"family": "event",
|
||||||
|
"occurrences": 3618,
|
||||||
|
"reason": "incomplete-module"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"family": "object-flow",
|
||||||
|
"occurrences": 25351,
|
||||||
|
"reason": "ambiguous-target"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"family": "object-flow",
|
||||||
|
"occurrences": 1541,
|
||||||
|
"reason": "dynamic-property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"family": "object-flow",
|
||||||
|
"occurrences": 3618,
|
||||||
|
"reason": "incomplete-module"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"family": "promise-ownership",
|
||||||
|
"occurrences": 3619,
|
||||||
|
"reason": "incomplete-module"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"family": "request",
|
||||||
|
"occurrences": 3619,
|
||||||
|
"reason": "incomplete-module"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"family": "resource-lifecycle",
|
||||||
|
"occurrences": 3619,
|
||||||
|
"reason": "incomplete-module"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"family": "timer",
|
||||||
|
"occurrences": 67,
|
||||||
|
"reason": "ambiguous-target"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"family": "timer",
|
||||||
|
"occurrences": 3619,
|
||||||
|
"reason": "incomplete-module"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"status": "partial",
|
||||||
|
"truncated": true
|
||||||
|
},
|
||||||
|
"semantic_unknowns": 100000
|
||||||
|
},
|
||||||
|
"electron": {
|
||||||
|
"browser_windows": 0,
|
||||||
|
"context_bridge_apis": 0,
|
||||||
|
"explicit_web_preferences": 0,
|
||||||
|
"exposed_api_members": 0,
|
||||||
|
"ipc": {
|
||||||
|
"ambiguous_renderer_transmissions": 0,
|
||||||
|
"dynamic_channel_operations": 0,
|
||||||
|
"literal_channels": 0,
|
||||||
|
"main_handlers": 0,
|
||||||
|
"operations": 0,
|
||||||
|
"paired_renderer_transmissions": 0,
|
||||||
|
"renderer_listeners": 0,
|
||||||
|
"renderer_transmissions": 0,
|
||||||
|
"unpaired_literal_renderer_transmissions": 0
|
||||||
|
},
|
||||||
|
"native_addon_bindings": 0,
|
||||||
|
"preload_entrypoints": 0,
|
||||||
|
"resolved_native_addon_bindings": 0,
|
||||||
|
"resolved_utility_entrypoints": 0,
|
||||||
|
"sender_validation_observations": 1,
|
||||||
|
"utility_processes": 0
|
||||||
|
},
|
||||||
|
"format": "asar",
|
||||||
|
"input_path": "C:\\Users\\Administrator\\AppData\\Local\\Programs\\WorkBuddy\\resources\\app.asar",
|
||||||
|
"limitation_count": 53,
|
||||||
|
"root_artifact_sha256": "026a1d0805a2366d06390cef52e157a7277543cdd27dd308e6b1acb47ffffdc1",
|
||||||
|
"source_selection": {
|
||||||
|
"requested_paths": [
|
||||||
|
"renderer/assets/src-DbE1_mgM.js"
|
||||||
|
],
|
||||||
|
"unselected_files": 11183
|
||||||
|
},
|
||||||
|
"statistics": {
|
||||||
|
"findings": 678,
|
||||||
|
"invalid_utf8_files": 0,
|
||||||
|
"modules": 0,
|
||||||
|
"nested_asar_containers": 0,
|
||||||
|
"parse_failures": 0,
|
||||||
|
"parsed_javascript_files": 1,
|
||||||
|
"relevant_files": 11211,
|
||||||
|
"skipped_javascript_files": 0,
|
||||||
|
"text_bytes_read": 1968427,
|
||||||
|
"visited_ast_nodes": 415338
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"unknowns": [
|
||||||
|
"100000 semantic graph unknowns remain in the parent Evidence.",
|
||||||
|
"11145 application graph unknowns remain in the parent Evidence.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/bin/native/win32-x64/job-object.node; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/node_modules/@lydell/node-pty-win32-x64/prebuilds/win32-x64/conpty.node; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/node_modules/@lydell/node-pty-win32-x64/prebuilds/win32-x64/conpty/OpenConsole.exe; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/node_modules/@lydell/node-pty-win32-x64/prebuilds/win32-x64/conpty_console_list.node; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/node_modules/@lydell/node-pty/package.json; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/node_modules/koffi/build/koffi/win32_x64/koffi.node; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/node_modules/koffi/package.json; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/node_modules/koffi/src/cnoke/package.json; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/vendor/genie-trash/win32-x64.exe; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/vendor/ripgrep/x64-win32/rg.exe; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/vendor/ripgrep/x64-win32/ripgrep.node; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/vendor/sandbox/5.8.6/betterleaks.exe; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/vendor/sandbox/5.8.6/sandbox-center.exe; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/vendor/sandbox/5.8.6/sandbox-cli-gc.exe; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/vendor/sandbox/5.8.6/sandbox-cli.exe; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/vendor/sandbox/5.8.6/sandbox_ffi.dll; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/vendor/sandbox/5.8.6/tsbx.dll; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/vendor/sandbox/5.8.6/tsbx_sdk.dll; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/vendor/sandbox/5.8.6/tsbxldr.dll; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/vendor/sandbox/5.8.6/tsbxlow.dll; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at cli/vendor/wecom-cli/wecom-cli.exe; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at native/perf-metrics/build/Release/perf_metrics.node; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at native/turing-sdk/build/Release/turing_sdk.node; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at native/wechat-copydata-decoder/build/Release/wechat_copydata_decoder.node; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at node_modules/@ban-dal/os-bridge-win32-x64-msvc/os-bridge.win32-x64-msvc.node; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at node_modules/@lydell/node-pty-win32-x64/prebuilds/win32-x64/conpty.node; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at node_modules/@lydell/node-pty-win32-x64/prebuilds/win32-x64/conpty/OpenConsole.exe; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at node_modules/@lydell/node-pty-win32-x64/prebuilds/win32-x64/conpty_console_list.node; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at node_modules/@tencent/qimei-node/build/Release/qimei.node; the observed bytes are untrusted.",
|
||||||
|
"Artifact integrity metadata contradicts observed bytes at node_modules/@tencent/tencent-docs-ai-engine/bin/win32-x64/editor_sdk.exe; the observed bytes are untrusted.",
|
||||||
|
"graph.coverage.status is partial",
|
||||||
|
"semantic_graph.coverage.status is partial",
|
||||||
|
"35 additional unknowns omitted from this view; consult source parent Evidence ev_ef70f1417c069be6bf352efcb858244efaae444f71f1f9893ed2fb579ed88849 for the complete list (the caller-supplied record, or its retaining session)."
|
||||||
|
],
|
||||||
|
"view": {
|
||||||
|
"kind": "summary"
|
||||||
|
},
|
||||||
|
"view_digest": "f6bfbc925ceccbfd39e72ae31b623c5c3d5c07b9cedba7f03f15d923ab0fbf8b"
|
||||||
|
},
|
||||||
|
"operation": "inspect_analysis_view",
|
||||||
|
"parameters": {
|
||||||
|
"parent_evidence_id": "ev_ef70f1417c069be6bf352efcb858244efaae444f71f1f9893ed2fb579ed88849",
|
||||||
|
"view": {
|
||||||
|
"kind": "summary"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"predicate_type": "rea.analysis-view",
|
||||||
|
"provider": {
|
||||||
|
"id": "rea-analysis-view",
|
||||||
|
"name": "REA analysis view projection",
|
||||||
|
"version": "1"
|
||||||
|
},
|
||||||
|
"raw_result": null,
|
||||||
|
"relationship_coverage": null,
|
||||||
|
"subject": {
|
||||||
|
"architecture": null,
|
||||||
|
"digest": {
|
||||||
|
"sha256": "026a1d0805a2366d06390cef52e157a7277543cdd27dd308e6b1acb47ffffdc1"
|
||||||
|
},
|
||||||
|
"format": "asar",
|
||||||
|
"local_path": "C:\\Users\\Administrator\\AppData\\Local\\Programs\\WorkBuddy\\resources\\app.asar",
|
||||||
|
"name": "app.asar"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,386 @@
|
|||||||
|
/** Isolated Electron fixture for the recommended-skill page.
|
||||||
|
* The host is fully controlled: no network, no real skill storage, no user data. */
|
||||||
|
import fs from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { createRequire } from 'node:module'
|
||||||
|
import { spawn } from 'node:child_process'
|
||||||
|
|
||||||
|
const require = createRequire(import.meta.url)
|
||||||
|
const electronModule = require('electron')
|
||||||
|
const electron = typeof electronModule === 'object' && electronModule.app ? electronModule : null
|
||||||
|
const repository = path.resolve('.'), base = path.join(repository, '.tmp')
|
||||||
|
|
||||||
|
if (!electron) {
|
||||||
|
fs.mkdirSync(base, { recursive: true })
|
||||||
|
const output = fs.mkdtempSync(path.join(base, 'recommended-skills-ui-'))
|
||||||
|
const env = { ...process.env }
|
||||||
|
delete env.ELECTRON_RUN_AS_NODE
|
||||||
|
delete env.ELECTRON_RENDERER_URL
|
||||||
|
const child = spawn(electronModule, ['--no-sandbox', '--disable-gpu-sandbox', path.resolve('.tmp/b01-acceptance/skills-installed-probe.mjs'), output], { stdio: 'inherit', windowsHide: true, env })
|
||||||
|
const timer = setTimeout(() => child.kill(), 120000)
|
||||||
|
process.exitCode = await new Promise(resolve => child.on('exit', code => { clearTimeout(timer); resolve(code ?? 1) }))
|
||||||
|
} else {
|
||||||
|
void (async () => {
|
||||||
|
const { app, BrowserWindow, ipcMain } = electron
|
||||||
|
const output = path.resolve(process.argv.at(-1)), relative = path.relative(base, output)
|
||||||
|
assert.ok(relative.startsWith('recommended-skills-ui-') && !relative.includes('..') && !path.isAbsolute(relative))
|
||||||
|
const userData = path.join(output, 'data')
|
||||||
|
fs.mkdirSync(userData, { recursive: true })
|
||||||
|
app.setPath('userData', userData)
|
||||||
|
const checks = [], errors = [], calls = [], screenshots = []
|
||||||
|
|
||||||
|
const recommendation = {
|
||||||
|
id: 'archify',
|
||||||
|
name: 'Archify',
|
||||||
|
description: '生成可交互的架构图、流程图和一页式说明',
|
||||||
|
author: 'tt-a1i',
|
||||||
|
license: 'MIT',
|
||||||
|
repository: 'tt-a1i/archify',
|
||||||
|
version: '3.2.0',
|
||||||
|
commit: '25cf37080fc8b1dbe758a4efc3935b55f3327083',
|
||||||
|
packagePath: 'archify.zip',
|
||||||
|
sha256: '70e210265c59f24dcbdf2a502ff671104b5454e7ee7d3554aecd86bad32384f5',
|
||||||
|
expectedBytes: 5271959,
|
||||||
|
containsScripts: true
|
||||||
|
}
|
||||||
|
const localSkill = {
|
||||||
|
id: 'user:local', name: 'Archify', description: '本地导入的同名技能', version: '1.0.0',
|
||||||
|
path: 'C:/local/archify', scope: 'user', enabled: true, userInvocable: true,
|
||||||
|
disableModelInvocation: false, source: 'imported', userModified: true
|
||||||
|
}
|
||||||
|
const installedRecommendation = {
|
||||||
|
id: 'user:rec', name: 'Archify', description: recommendation.description, version: '3.2.0',
|
||||||
|
path: 'C:/managed/archify', scope: 'user', enabled: true, userInvocable: true,
|
||||||
|
disableModelInvocation: false, source: 'imported', userModified: false,
|
||||||
|
origin: {
|
||||||
|
recommendationId: 'archify', repository: recommendation.repository, commit: recommendation.commit,
|
||||||
|
packagePath: recommendation.packagePath, archiveSha256: recommendation.sha256, version: '3.2.0', installedAt: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let installed = [], progress = null, failNext = false, cancelRequested = false, delay = 0, recommendationOverride = null, recommendationFails = false
|
||||||
|
let clock = 0
|
||||||
|
const listeners = new Set(), changedListeners = new Set()
|
||||||
|
const emit = state => {
|
||||||
|
// The host stamps a monotonic time; a strict comparison rejects a stale frame.
|
||||||
|
const stamped = { ...state, updatedAt: ++clock }
|
||||||
|
progress = stamped
|
||||||
|
for (const listener of listeners) listener(stamped)
|
||||||
|
for (const win of BrowserWindow.getAllWindows()) if (!win.isDestroyed()) win.webContents.send('skills:install-progress', stamped)
|
||||||
|
}
|
||||||
|
const changed = () => {
|
||||||
|
for (const listener of changedListeners) listener()
|
||||||
|
for (const win of BrowserWindow.getAllWindows()) if (!win.isDestroyed()) win.webContents.send('skills:changed')
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcMain.handle('skills-qa', async (_event, method, args) => {
|
||||||
|
calls.push([method, ...args])
|
||||||
|
if (method === 'recommendations') {
|
||||||
|
if (recommendationFails) throw Error('推荐目录服务不可用')
|
||||||
|
return recommendationOverride ?? [recommendation]
|
||||||
|
}
|
||||||
|
if (method === 'list') return installed
|
||||||
|
if (method === 'setEnabled') { installed = installed.map(skill => skill.id === args[0] ? { ...skill, enabled: args[1] } : skill); changed(); return }
|
||||||
|
if (method === 'installState') return progress
|
||||||
|
// The host only accepts the cancel request; the terminal frame still comes from the install flow.
|
||||||
|
if (method === 'cancelInstall') { cancelRequested = true; return { cancelled: true } }
|
||||||
|
if (method === 'installRecommended') {
|
||||||
|
const [id, options] = args
|
||||||
|
if (id !== recommendation.id) throw Error('未知推荐技能')
|
||||||
|
if (failNext) {
|
||||||
|
failNext = false
|
||||||
|
emit({ operationId: options.operationId, recommendationId: id, phase: 'failed', receivedBytes: 0, error: '下载失败:网络错误', updatedAt: Date.now() })
|
||||||
|
throw Error('下载失败:网络错误')
|
||||||
|
}
|
||||||
|
emit({ operationId: options.operationId, recommendationId: id, phase: 'downloading', receivedBytes: 0, totalBytes: recommendation.expectedBytes, updatedAt: Date.now() })
|
||||||
|
await new Promise(resolve => setTimeout(resolve, delay))
|
||||||
|
if (cancelRequested) { cancelRequested = false; emit({ operationId: options.operationId, recommendationId: id, phase: 'cancelled', receivedBytes: 0, updatedAt: Date.now() }); throw Object.assign(Error('推荐技能安装已取消'), { name: 'AbortError' }) }
|
||||||
|
emit({ operationId: options.operationId, recommendationId: id, phase: 'validating', receivedBytes: recommendation.expectedBytes, totalBytes: recommendation.expectedBytes, updatedAt: Date.now() })
|
||||||
|
await new Promise(resolve => setTimeout(resolve, delay))
|
||||||
|
if (cancelRequested) { cancelRequested = false; emit({ operationId: options.operationId, recommendationId: id, phase: 'cancelled', receivedBytes: recommendation.expectedBytes, updatedAt: Date.now() }); throw Object.assign(Error('推荐技能安装已取消'), { name: 'AbortError' }) }
|
||||||
|
emit({ operationId: options.operationId, recommendationId: id, phase: 'installing', receivedBytes: recommendation.expectedBytes, totalBytes: recommendation.expectedBytes, updatedAt: Date.now() })
|
||||||
|
await new Promise(resolve => setTimeout(resolve, delay))
|
||||||
|
installed = [installedRecommendation]
|
||||||
|
emit({ operationId: options.operationId, recommendationId: id, phase: 'installed', receivedBytes: recommendation.expectedBytes, totalBytes: recommendation.expectedBytes, updatedAt: Date.now() })
|
||||||
|
changed()
|
||||||
|
return installedRecommendation
|
||||||
|
}
|
||||||
|
if (method === 'qaSetInstalled') { installed = args[0]; changed(); return installed }
|
||||||
|
if (method === 'qaFailNext') { failNext = true; return true }
|
||||||
|
if (method === 'qaDelay') { delay = args[0]; return true }
|
||||||
|
if (method === 'qaSetRecommendations') { recommendationOverride = args[0]; return true }
|
||||||
|
if (method === 'qaFailRecommendations') { recommendationFails = args[0] === true; return true }
|
||||||
|
// Simulate an operation owned by another window.
|
||||||
|
if (method === 'qaEmitForeign') {
|
||||||
|
const [phase, operationId] = args
|
||||||
|
emit({ operationId, recommendationId: recommendation.id, phase, receivedBytes: 0, totalBytes: recommendation.expectedBytes, updatedAt: Date.now() })
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Replay a frame with an older timestamp to prove the terminal state never regresses.
|
||||||
|
if (method === 'qaEmitStale') {
|
||||||
|
const [phase, operationId, updatedAt] = args
|
||||||
|
const stamped = { operationId, recommendationId: recommendation.id, phase, receivedBytes: 0, totalBytes: recommendation.expectedBytes, updatedAt }
|
||||||
|
progress = stamped
|
||||||
|
for (const listener of listeners) listener(stamped)
|
||||||
|
for (const win of BrowserWindow.getAllWindows()) if (!win.isDestroyed()) win.webContents.send('skills:install-progress', stamped)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (method === 'qaSubscribe') { listeners.add(args[0]); return true }
|
||||||
|
if (method === 'qaSubscribeChanged') { changedListeners.add(args[0]); return true }
|
||||||
|
throw Error('Unknown QA method ' + method)
|
||||||
|
})
|
||||||
|
|
||||||
|
fs.writeFileSync(path.join(output, 'preload.cjs'), `const{contextBridge,ipcRenderer}=require('electron');
|
||||||
|
const invoke=(method,...args)=>ipcRenderer.invoke('skills-qa',method,args);
|
||||||
|
contextBridge.exposeInMainWorld('ycode',{skills:{
|
||||||
|
list:(cwd)=>invoke('list',cwd),
|
||||||
|
recommendations:()=>invoke('recommendations'),
|
||||||
|
installRecommended:(id,options)=>invoke('installRecommended',id,options),
|
||||||
|
cancelInstall:(operationId)=>invoke('cancelInstall',operationId),
|
||||||
|
installState:()=>invoke('installState'),
|
||||||
|
onInstallProgress:(listener)=>{const wrapped=(_event,state)=>listener(state);ipcRenderer.on('skills:install-progress',wrapped);return()=>ipcRenderer.removeListener('skills:install-progress',wrapped)},
|
||||||
|
onChanged:(listener)=>{const wrapped=()=>listener();ipcRenderer.on('skills:changed',wrapped);return()=>ipcRenderer.removeListener('skills:changed',wrapped)},
|
||||||
|
read:async()=>({skill:{},content:'',files:[]}),
|
||||||
|
setEnabled:(...args)=>invoke('setEnabled',...args), remove:async()=>{}, export:async()=>null, importPath:async()=>null
|
||||||
|
,qaSetInstalled:(v)=>invoke('qaSetInstalled',v),
|
||||||
|
qaFailNext:()=>invoke('qaFailNext'),
|
||||||
|
qaDelay:(v)=>invoke('qaDelay',v),
|
||||||
|
qaSetRecommendations:(v)=>invoke('qaSetRecommendations',v),
|
||||||
|
qaFailRecommendations:(v)=>invoke('qaFailRecommendations',v),
|
||||||
|
qaEmitForeign:(phase,operationId)=>invoke('qaEmitForeign',phase,operationId),
|
||||||
|
qaEmitStale:(phase,operationId,updatedAt)=>invoke('qaEmitStale',phase,operationId,updatedAt)
|
||||||
|
},resources:{openExternal:async()=>undefined}});`)
|
||||||
|
|
||||||
|
const location = file => JSON.stringify(path.join(repository, file).replaceAll('\\', '/'))
|
||||||
|
fs.writeFileSync(path.join(output, 'fixture.tsx'), `import React from 'react';
|
||||||
|
import{createRoot}from'react-dom/client';
|
||||||
|
import Center from ${location('src/renderer/src/capabilities/CapabilityCenter')};
|
||||||
|
import${location('src/renderer/src/styles/tokens.css')};
|
||||||
|
import${location('src/renderer/src/styles/app.css')};
|
||||||
|
window.__qa={uses:[],created:0,find:0};
|
||||||
|
createRoot(document.getElementById('root')).render(
|
||||||
|
<div className="chat-pane cr-theme" style={{height:'100vh'}}>
|
||||||
|
<Center initialTab="skills" onBackToChat={()=>{}} onUseSkill={skill=>window.__qa.uses.push(skill)} onCreateSkill={()=>window.__qa.created++} onFindSkill={()=>window.__qa.find++}/>
|
||||||
|
</div>);`)
|
||||||
|
await require('esbuild').build({
|
||||||
|
entryPoints: [path.join(output, 'fixture.tsx')], bundle: true, platform: 'browser', format: 'esm', jsx: 'automatic',
|
||||||
|
outfile: path.join(output, 'fixture.js'), loader: { '.svg': 'file', '.png': 'file', '.jpg': 'file', '.woff': 'file', '.woff2': 'file', '.ttf': 'file' }, logLevel: 'warning'
|
||||||
|
})
|
||||||
|
fs.writeFileSync(path.join(output, 'index.html'), '<html data-theme="light"><head><meta charset="utf-8"><link rel="stylesheet" href="fixture.css"><style>html,body,#root{width:100%;height:100%;margin:0}</style></head><body><div id="root"></div><script type="module" src="fixture.js"></script></body></html>')
|
||||||
|
|
||||||
|
const win = new BrowserWindow({
|
||||||
|
width: 1052, height: 901, useContentSize: true, frame: false, show: false,
|
||||||
|
webPreferences: { preload: path.join(output, 'preload.cjs'), contextIsolation: true, nodeIntegration: false, backgroundThrottling: false }
|
||||||
|
})
|
||||||
|
win.webContents.on('console-message', (_event, level, message) => { if (level === 3) errors.push(message) })
|
||||||
|
const read = code => win.webContents.executeJavaScript(code)
|
||||||
|
const check = (label, actual, expected) => {
|
||||||
|
const pass = typeof expected === 'function' ? expected(actual) : JSON.stringify(actual) === JSON.stringify(expected)
|
||||||
|
assert.ok(pass, `${label}: ${JSON.stringify(actual)}`)
|
||||||
|
checks.push(label)
|
||||||
|
}
|
||||||
|
const pause = ms => new Promise(resolve => setTimeout(resolve, ms))
|
||||||
|
const wait = async code => { for (let index = 0; index < 200; index++) { if (await read(code)) return; await pause(50) } throw Error('Timeout ' + code) }
|
||||||
|
const click = async selector => {
|
||||||
|
await read(`(()=>{const e=document.querySelector(${JSON.stringify(selector)});if(!e)throw Error('Missing '+${JSON.stringify(selector)});e.click();return true})()`)
|
||||||
|
await pause(140)
|
||||||
|
}
|
||||||
|
const capture = async name => {
|
||||||
|
win.webContents.invalidate()
|
||||||
|
await pause(120)
|
||||||
|
// A hidden window can capture the previous frame; warm the compositor first.
|
||||||
|
await win.webContents.capturePage()
|
||||||
|
await pause(200)
|
||||||
|
const image = await win.webContents.capturePage()
|
||||||
|
fs.writeFileSync(path.join(output, name + '.png'), image.toPNG())
|
||||||
|
screenshots.push(name + '.png')
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await win.loadFile(path.join(output, 'index.html'))
|
||||||
|
await wait('!!document.querySelector(".yc-skills-page")')
|
||||||
|
await wait('!!document.querySelector("[data-recommendation-id=archify]")')
|
||||||
|
|
||||||
|
check('the recommendation card renders from the host catalog', await read('document.querySelector("[data-recommendation-id=archify] .sm-card-name")?.textContent'), 'Archify')
|
||||||
|
check('the card exposes its install state for the host test', await read('document.querySelector("[data-recommendation-id=archify]").dataset.recommendationState'), 'none')
|
||||||
|
check('the card shows the author supplied description', await read('document.querySelector("[data-recommendation-id=archify] .sm-card-desc")?.textContent'), recommendation.description)
|
||||||
|
check('no maintenance placeholder is shown', await read('!!document.querySelector("[data-capability-maintenance]")'), false)
|
||||||
|
check('the remote catalog tabs still report no connection', await read('!!document.querySelector(".yc-skill-market-unavailable")'), false)
|
||||||
|
await capture('recommendation-card')
|
||||||
|
|
||||||
|
// First install asks for an explicit confirmation.
|
||||||
|
await click('[aria-label="安装 Archify"]')
|
||||||
|
await wait('!!document.querySelector(".yc-skill-detail-shell")')
|
||||||
|
check('the confirm dialog names the source repository', await read('document.querySelector(".yc-skill-detail-shell").textContent.includes("tt-a1i/archify")'), true)
|
||||||
|
check('the confirm dialog states that no install script runs', await read('document.querySelector(".yc-skill-detail-shell").textContent.includes("不会执行仓库中的任何安装命令")'), true)
|
||||||
|
check('the confirm dialog warns that the skill contains scripts', await read('document.querySelector(".yc-skill-detail-shell").textContent.includes("包含脚本") || document.querySelector(".yc-skill-detail-shell").textContent.includes("脚本")'), true)
|
||||||
|
await capture('install-confirm')
|
||||||
|
await read('(()=>{const first=document.querySelector("[role=dialog] .sm-skill-detail-close");first.focus();first.dispatchEvent(new KeyboardEvent("keydown",{key:"Tab",shiftKey:true,bubbles:true,cancelable:true}))})()')
|
||||||
|
check('reverse Tab reaches the collapsible source information', await read('document.activeElement.tagName'), 'SUMMARY')
|
||||||
|
await read('document.activeElement.dispatchEvent(new KeyboardEvent("keydown",{key:"Tab",bubbles:true,cancelable:true}))')
|
||||||
|
check('Tab wraps inside the modal rather than entering the sidebar', await read('document.activeElement.getAttribute("aria-label")'), '关闭技能详情')
|
||||||
|
await read('window.__detailBeforeInstall=document.querySelector("[role=dialog]")')
|
||||||
|
await click('[aria-label="确认安装推荐技能"]')
|
||||||
|
await wait('document.querySelector(".yc-skill-inline-install")?.dataset.installPhase==="installed"')
|
||||||
|
check('the install progress is reported by the host', await read('document.querySelector(".yc-skill-inline-install").dataset.installPhase'), 'installed')
|
||||||
|
check('the host received the recommendation id', await read(`JSON.stringify(${JSON.stringify(calls)}.filter(c=>c[0]==='installRecommended').map(c=>c[1]))`), '["archify"]')
|
||||||
|
check('the host received a fresh operation id', await read(`(${JSON.stringify(calls)}.filter(c=>c[0]==='installRecommended').every(c=>typeof c[2]?.operationId==='string'&&c[2].operationId.length>8))`), true)
|
||||||
|
check('a first install is not an overwrite', await read(`(${JSON.stringify(calls)}.filter(c=>c[0]==='installRecommended').every(c=>c[2]?.overwrite!==true))`), true)
|
||||||
|
await wait('document.querySelector("[data-recommendation-id=archify]").dataset.recommendationState==="current"')
|
||||||
|
check('the card reports the recommendation as installed', await read('document.querySelector("[data-recommendation-id=archify]").dataset.recommendationState'), 'current')
|
||||||
|
check('the installed skill is not offered for download again', await read('document.querySelector("[data-recommendation-id=archify] .sm-add-btn")?.disabled'), true)
|
||||||
|
check('the detail remains the same DOM instance throughout installation', await read('window.__detailBeforeInstall===document.querySelector("[role=dialog]")'), true)
|
||||||
|
check('the detail offers use after installation', await read('document.querySelector("[role=dialog] .sm-skill-detail-action-row")?.textContent.includes("去使用")'), true)
|
||||||
|
check('the success notification also offers use', await read('document.querySelector(".yc-skill-install-success")?.textContent.includes("去使用")'), true)
|
||||||
|
check('there is no permanent list footer progress panel', await read('!!document.querySelector(".yc-skills-scroll > .yc-skill-inline-install")'), false)
|
||||||
|
await capture('installed-detail')
|
||||||
|
await click('[role=dialog] .sm-skill-detail-close')
|
||||||
|
|
||||||
|
// The installed list stays reachable and the skill can be used.
|
||||||
|
await capture('installed-recommendation-list');
|
||||||
|
const installedState=await read('(()=>{const root=document.querySelector("[data-recommendation-id=archify]");const card=root.querySelector(".sm-card");const add=root.querySelector(".sm-add-btn");return {state:root.dataset.recommendationState,cardClasses:card.className,installedClass:card.classList.contains("sm-card--installed"),addButtonPresent:!!add,addButtonDisabled:add?.disabled,addSvgPresent:!!add?.querySelector("svg"),useSlotPresent:!!root.querySelector(".sm-use-btn"),footerPresent:!!document.querySelector(".yc-skill-recommendation-footer"),footerText:document.querySelector(".yc-skill-recommendation-footer")?.textContent}})()');
|
||||||
|
fs.writeFileSync(path.join(output,'installed-state.json'),JSON.stringify({codeSha:'51531dcae8d8ade85db9e203bc8c0f712805b2ae',installedState,errors,scope:'Actual recommended-skill component and CSS in Electron with controlled host; no network or actual skill installation'},null,2));
|
||||||
|
console.log(JSON.stringify({result:'REPRODUCED',output,installedState}));win.destroy();app.exit(0);return;
|
||||||
|
await click('.yc-skill-recommendation-footer button')
|
||||||
|
await wait('document.querySelectorAll(".sm-card").length===1')
|
||||||
|
check('the installed skill appears in the installed list', await read('document.querySelector(".sm-card-name")?.textContent'), 'Archify')
|
||||||
|
await click('[aria-label="返回专家中心"]')
|
||||||
|
await wait('!!document.querySelector("[data-recommendation-id=archify]")')
|
||||||
|
|
||||||
|
// A local skill with the same name requires an explicit overwrite.
|
||||||
|
await read(`window.ycode.skills.qaSetInstalled(${JSON.stringify([localSkill])})`)
|
||||||
|
await wait('document.querySelector("[data-recommendation-id=archify]").dataset.recommendationState==="conflict"')
|
||||||
|
await click('[aria-label="安装 Archify"]')
|
||||||
|
await wait('!!document.querySelector(".yc-skill-detail-shell")')
|
||||||
|
check('a same-name local skill requires an explicit replacement', await read('document.querySelector(".yc-skill-detail-shell").textContent.includes("替换")'), true)
|
||||||
|
check('the replacement notice mentions the local modification', await read('document.querySelector(".yc-skill-detail-shell").textContent.includes("本地内容已被修改")'), true)
|
||||||
|
await click('[aria-label="确认安装推荐技能"]')
|
||||||
|
await wait('document.querySelector("[data-recommendation-id=archify]").dataset.recommendationState==="current"')
|
||||||
|
check('an explicit replacement is sent to the host as an overwrite', await read(`(${JSON.stringify(calls)}.filter(c=>c[0]==='installRecommended').at(-1)[2]?.overwrite)===true`), true)
|
||||||
|
await click('[role=dialog] .sm-skill-detail-close')
|
||||||
|
|
||||||
|
// A failing download reports the real host error and allows a retry.
|
||||||
|
await read(`window.ycode.skills.qaSetInstalled(${JSON.stringify([])})`)
|
||||||
|
await wait('document.querySelector("[data-recommendation-id=archify]").dataset.recommendationState==="none"')
|
||||||
|
await read(`window.ycode.skills.qaFailNext()`)
|
||||||
|
await click('[aria-label="安装 Archify"]')
|
||||||
|
await click('[aria-label="确认安装推荐技能"]')
|
||||||
|
await wait('document.querySelector(".yc-skill-inline-install")?.dataset.installPhase==="failed"')
|
||||||
|
check('a failed install shows the host error', await read('document.querySelector(".yc-skill-inline-install").textContent.includes("网络错误")'), true)
|
||||||
|
check('a failed install can be retried from the same card', await read('document.querySelector("[data-recommendation-id=archify] .sm-add-btn")?.disabled'), false)
|
||||||
|
await click('[role=dialog] .sm-skill-detail-close')
|
||||||
|
|
||||||
|
// Cancellation is allowed before the atomic commit phase.
|
||||||
|
await read(`window.ycode.skills.qaDelay(400)`)
|
||||||
|
await click('[aria-label="安装 Archify"]')
|
||||||
|
await click('[aria-label="确认安装推荐技能"]')
|
||||||
|
await wait('document.querySelector(".yc-skill-inline-install")?.dataset.installPhase==="downloading"')
|
||||||
|
check('the download phase can be cancelled', await read('[...document.querySelectorAll("[role=dialog] button")].some(e=>e.textContent==="取消安装"&&!e.disabled)'), true)
|
||||||
|
await read('document.querySelector("[role=dialog]").querySelectorAll("button").forEach(e=>{if(e.textContent==="取消安装")e.click()})')
|
||||||
|
await wait('document.querySelector(".yc-skill-inline-install")?.dataset.installPhase==="cancelled"')
|
||||||
|
check('the cancellation is sent to the host', await read(`${JSON.stringify(calls)}.some(c=>c[0]==='cancelInstall')`), true)
|
||||||
|
await click('[role=dialog] .sm-skill-detail-close')
|
||||||
|
await read(`window.ycode.skills.qaDelay(0)`)
|
||||||
|
|
||||||
|
// Closing the detail never aborts an accepted install; reopening resumes it.
|
||||||
|
await read('window.ycode.skills.qaDelay(250)')
|
||||||
|
await click('[aria-label="安装 Archify"]'); await click('[aria-label="确认安装推荐技能"]')
|
||||||
|
await wait('document.querySelector(".yc-skill-inline-install")?.dataset.installPhase==="downloading"')
|
||||||
|
const continuingId = await read('document.querySelector(".yc-skill-inline-install").dataset.operationId')
|
||||||
|
await click('[role=dialog] .sm-skill-detail-close')
|
||||||
|
check('closing a running detail restores the background', await read('document.getElementById("root").inert'), false)
|
||||||
|
await click('[data-recommendation-id=archify] .sm-card')
|
||||||
|
check('reopening detail keeps the same install operation', await read('document.querySelector(".yc-skill-inline-install").dataset.operationId'), continuingId)
|
||||||
|
await wait('document.querySelector(".yc-skill-inline-install")?.dataset.installPhase==="installing"')
|
||||||
|
check('the commit phase disables cancellation in place', await read('[...document.querySelectorAll("[role=dialog] button")].find(e=>e.textContent==="取消安装")?.disabled'), true)
|
||||||
|
await wait('document.querySelector(".yc-skill-inline-install")?.dataset.installPhase==="installed"')
|
||||||
|
await click('[role=dialog] .sm-skill-detail-close')
|
||||||
|
await click('.yc-skill-install-success [aria-label="使用推荐技能 Archify"]')
|
||||||
|
check('the success notification invokes the same use callback', await read('window.__qa.uses.length'), 1)
|
||||||
|
await read(`window.ycode.skills.qaSetInstalled(${JSON.stringify([{ ...installedRecommendation, enabled: false }])})`)
|
||||||
|
await wait('document.querySelector(".yc-skill-recommendation-meta")?.textContent.includes("启用后使用")')
|
||||||
|
await click('.yc-skill-recommendation-meta [aria-label="使用推荐技能 Archify"]')
|
||||||
|
await wait('window.__qa.uses.length===2')
|
||||||
|
check('disabled recommendation is enabled before use', await read('window.__qa.uses.at(-1).enabled'), true)
|
||||||
|
await read('window.ycode.skills.qaDelay(0)'); await read('window.ycode.skills.qaSetInstalled([])')
|
||||||
|
await wait('document.querySelector("[data-recommendation-id=archify]").dataset.recommendationState==="none"')
|
||||||
|
|
||||||
|
// A failing catalog is reported as unavailable, not as a maintenance page.
|
||||||
|
// The page loads its catalog once, so the failure is proven on a fresh mount.
|
||||||
|
await read(`window.ycode.skills.qaFailRecommendations(true)`)
|
||||||
|
await read(`location.reload()`)
|
||||||
|
await wait('!!document.querySelector(".yc-skill-recommendation-unavailable")')
|
||||||
|
check('an empty catalog is reported as unavailable, not as a maintenance page', await read('!!document.querySelector("[data-capability-maintenance]")'), false)
|
||||||
|
check('the unavailable state still offers the installed list', await read('!!document.querySelector(".yc-skill-recommendation-unavailable button")'), true)
|
||||||
|
await read(`window.ycode.skills.qaFailRecommendations(false)`)
|
||||||
|
await read(`location.reload()`)
|
||||||
|
await wait('!!document.querySelector("[data-recommendation-id=archify]")')
|
||||||
|
|
||||||
|
// An install owned by another window blocks a new install here.
|
||||||
|
const foreignOperation = '11111111-2222-4333-8444-555555555555'
|
||||||
|
await read(`window.ycode.skills.qaEmitForeign('downloading', ${JSON.stringify(foreignOperation)})`)
|
||||||
|
await wait('!!document.querySelector("[data-foreign-install=true]")')
|
||||||
|
check('an install from another window is reported', await read('document.querySelector("[data-foreign-install=true]").textContent.includes("其他窗口正在安装推荐技能")'), true)
|
||||||
|
check('a new install is blocked while another window is installing', await read('document.querySelector("[data-recommendation-id=archify] .sm-add-btn")?.disabled'), true)
|
||||||
|
check('the blocked state is explained on the card', await read('!!document.querySelector(".yc-skill-recommendation-busy")'), true)
|
||||||
|
await capture('foreign-install')
|
||||||
|
await read(`window.ycode.skills.qaEmitForeign('installed', ${JSON.stringify(foreignOperation)})`)
|
||||||
|
await wait('!document.querySelector("[data-foreign-install=true]")')
|
||||||
|
check('the foreign block clears when that operation finishes', await read('document.querySelector("[data-recommendation-id=archify] .sm-add-btn")?.disabled'), false)
|
||||||
|
|
||||||
|
// A stale frame must not move a settled operation back into a running phase.
|
||||||
|
await read(`window.ycode.skills.qaSetInstalled(${JSON.stringify([])})`)
|
||||||
|
await wait('document.querySelector("[data-recommendation-id=archify]").dataset.recommendationState==="none"')
|
||||||
|
await read(`window.ycode.skills.qaDelay(0)`)
|
||||||
|
await click('[aria-label="安装 Archify"]')
|
||||||
|
await click('[aria-label="确认安装推荐技能"]')
|
||||||
|
await wait('document.querySelector(".yc-skill-inline-install")?.dataset.installPhase==="installed"')
|
||||||
|
const settledOperation = await read(`(${JSON.stringify(calls)}.filter(c=>c[0]==='installRecommended').at(-1)[2]?.operationId)`)
|
||||||
|
check('the settled install recorded its operation id', typeof settledOperation === 'string' && settledOperation.length > 8, true)
|
||||||
|
await read(`window.ycode.skills.qaEmitStale('downloading', ${JSON.stringify(settledOperation)}, 1)`)
|
||||||
|
await pause(250)
|
||||||
|
check('a stale frame does not regress the settled install', await read('document.querySelector(".yc-skill-inline-install")?.dataset.installPhase'), 'installed')
|
||||||
|
await click('[role=dialog] .sm-skill-detail-close')
|
||||||
|
|
||||||
|
// Dark and narrow captures of the real page.
|
||||||
|
// This fixture mounts only the capability center, so the theme attribute is set directly.
|
||||||
|
// The dark token switch is proven here only; the production app harness covers the light flow.
|
||||||
|
await read(`(()=>{localStorage.setItem('ycode:theme','dark');document.documentElement.dataset.theme='dark';return true})()`)
|
||||||
|
await wait('document.documentElement.dataset.theme==="dark"')
|
||||||
|
await wait('!!document.querySelector(".yc-capability-center .expert-center-dark")')
|
||||||
|
check('the dark theme reaches the real page', await read('!!document.querySelector(".yc-capability-center .expert-center-dark")'), true)
|
||||||
|
// The card transitions its background, so the dark surface only settles after the animation.
|
||||||
|
await wait(`(()=>{const card=document.querySelector('[data-recommendation-id=archify] .sm-card');if(!card)return false;const m=/rgba?\\((\\d+)/.exec(getComputedStyle(card).backgroundColor);return !!m&&Number(m[1])<90})()`)
|
||||||
|
const darkColors = await read(`(()=>{const card=document.querySelector('[data-recommendation-id=archify] .sm-card');const page=document.querySelector('.yc-capability-center .expert-center-page');return{card:card?getComputedStyle(card).backgroundColor:null,page:page?getComputedStyle(page).backgroundColor:null}})()`)
|
||||||
|
check('the dark card uses a dark surface', darkColors.card, value => typeof value === 'string' && /rgba?\((\d+), (\d+), (\d+)/.test(value) && Number(value.match(/rgba?\((\d+)/)[1]) < 90)
|
||||||
|
const overflow = await read(`(()=>{const wide=[...document.querySelectorAll('*')].filter(e=>e.getBoundingClientRect().right>innerWidth+1).map(e=>e.className||e.tagName).slice(0,4);return{scrollWidth:document.documentElement.scrollWidth,innerWidth,wide}})()`)
|
||||||
|
check('the dark layout keeps the recommendation card inside the viewport', await read(`(()=>{const c=document.querySelector('[data-recommendation-id=archify] .sm-card');if(!c)return false;const r=c.getBoundingClientRect();return r.left>=0&&r.right<=innerWidth+1})()`), true)
|
||||||
|
check('the dark layout does not overflow the document', overflow.scrollWidth <= overflow.innerWidth + 1, true)
|
||||||
|
await capture('recommendation-dark')
|
||||||
|
win.setSize(720, 900)
|
||||||
|
await pause(400)
|
||||||
|
check('the recommendation card stays inside the narrow viewport', await read('(()=>{const c=document.querySelector("[data-recommendation-id=archify] .sm-card");if(!c)return false;const r=c.getBoundingClientRect();return r.left>=0&&r.right<=innerWidth+1})()'), true)
|
||||||
|
const narrowOverflow = await read(`(()=>{const wide=[...document.querySelectorAll('*')].filter(e=>e.getBoundingClientRect().right>innerWidth+1).map(e=>String(e.className||e.tagName)).slice(0,5);return{scrollWidth:document.documentElement.scrollWidth,innerWidth,wide}})()`)
|
||||||
|
check('the narrow recommendation card causes no document overflow', narrowOverflow.scrollWidth <= narrowOverflow.innerWidth + 1, true)
|
||||||
|
await capture('recommendation-narrow')
|
||||||
|
win.setSize(1052, 901)
|
||||||
|
await pause(250)
|
||||||
|
|
||||||
|
check('no renderer errors', errors, [])
|
||||||
|
fs.writeFileSync(path.join(output, 'report.json'), JSON.stringify({
|
||||||
|
result: 'PASS', checks, calls, screenshots,
|
||||||
|
limits: 'Controlled host in isolated Electron; the shipped page and styles are used. No network, no real skill storage, no real install.'
|
||||||
|
}, null, 2))
|
||||||
|
console.log(JSON.stringify({ result: 'PASS', checks: checks.length, output }))
|
||||||
|
win.destroy()
|
||||||
|
app.exit(0)
|
||||||
|
} catch (error) {
|
||||||
|
await capture('failure')
|
||||||
|
fs.writeFileSync(path.join(output, 'failure.json'), JSON.stringify({ error: String(error), stack: error?.stack, checks, errors, calls, html: await read('document.body.innerHTML') }, null, 2))
|
||||||
|
console.error(error)
|
||||||
|
win.destroy()
|
||||||
|
app.exit(1)
|
||||||
|
}
|
||||||
|
})().catch(error => { console.error(error); electron.app.exit(1) })
|
||||||
|
}
|
||||||
@@ -186,3 +186,5 @@ npm run build
|
|||||||
- [Windows复测与后续开发](../对话输入对齐复测与后续开发-2026-10-21.md):针对 `37ddb2c` 的 A01~A09,包含已证缺陷、样式偏差与真实 SDK 验收缺口;该结果不自动适用于后续代码。
|
- [Windows复测与后续开发](../对话输入对齐复测与后续开发-2026-10-21.md):针对 `37ddb2c` 的 A01~A09,包含已证缺陷、样式偏差与真实 SDK 验收缺口;该结果不自动适用于后续代码。
|
||||||
|
|
||||||
首批正式需求已整理为 [B01:对话与输入交互复测问题修复](B01-需求.md),覆盖 A01~A09,分为 YC-001~YC-004,并列明逐项 AC、测试及证据要求。状态为“待开发”,不代表已经接单、交付或验收通过。本批需求推送成功后推送窗口转交开发端;开发端创建 `B01-交付-01.md` 接单、实现并交付。
|
首批正式需求已整理为 [B01:对话与输入交互复测问题修复](B01-需求.md),覆盖 A01~A09,分为 YC-001~YC-004,并列明逐项 AC、测试及证据要求。状态为“待开发”,不代表已经接单、交付或验收通过。本批需求推送成功后推送窗口转交开发端;开发端创建 `B01-交付-01.md` 接单、实现并交付。
|
||||||
|
|
||||||
|
2026-10-21 新增技能页需求见 [B02:推荐技能的已安装样式与重复入口](B02-需求.md),包含 YC-005/006。它与 B01 分开交付:推荐中已安装卡不再保留添加 Plus,移除列表底部“查看已安装技能”,保留顶部导航,并核对既有 WorkBuddy REA 恢复记录及当前证据限制。
|
||||||
Reference in new issue
Block a user