From aa039b747cdf1da39355475eb0b1634dbe5bff5a Mon Sep 17 00:00:00 2001 From: linnnn89 <216342082+linnnn89@users.noreply.github.com> Date: Tue, 8 Sep 2026 20:58:51 +0800 Subject: [PATCH 1/2] test: add bounded real application acceptance and explicit evidence limits --- CHANGELOG.md | 7 + CONTRIBUTING.md | 2 + README.md | 8 +- ...43\350\256\241\345\210\222\344\271\246.md" | 2 + docs/codex_worklog.md | 29 ++++ package-lock.json | 4 +- package.json | 3 +- scripts/verify-product-tasks.ts | 161 ++++++++++++++++++ skills/wincode/SKILL.md | 2 +- skills/wincode/references/ui.md | 4 +- src/Core/Config.ts | 2 +- tests/ui-code-runtime.test.ts | 5 +- .../WinCode.UIA.Host/WinCode.UIA.Host.csproj | 2 +- 13 files changed, 220 insertions(+), 11 deletions(-) create mode 100644 scripts/verify-product-tasks.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index cc48fcf..32f0e94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.12.3 + +- Add an opt-in six-task TavernDesk acceptance entry point. Discover candidate files with bounded native searches, locate unique live navigation controls, and verify command assignments and method declaration snippets against current source hashes. +- Count native and MCP calls, UTF-16 response characters, elapsed call time and repeated displayed lines. Keep failures and cleanup errors in local reports; this is neither a native-only comparison nor proof of whole-method or runtime binding correctness. +- Validate real isolated WPF repair, fixed-profile TavernDesk UI/source evidence and explicit line coverage. Real Serena remains unverified because no usable local command was found; no upstream or language-server installation was performed. +- An existing Codex connection was confirmed to retain Gateway 0.11.2 while a freshly launched Host reported 0.12.3. A new stdio probe does not establish that the user's existing connection has reloaded. + ## 0.12.2 - Support Node 24 as primary and 22 as compatibility runtime; require Node >=22. Pin .NET SDK 10.0.303 and lock native Host/fixture restores without changing declared dependencies. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 98ce26b..9e325ad 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,6 +13,8 @@ npm run delivery:verify Reports and bounded stage logs are under `test-tmp/check//`. CI uploads only the compact report, including failures; it does not upload local workspaces or screenshots. A passing core check does not establish desktop or real-upstream acceptance. +For opt-in fixed-profile TavernDesk acceptance, use `npm run test:tavern-context -- --ui-pid= --ui-hwnd=` and `npm run test:product -- `. The latter checks the fixed profile receipt and performs six navigation-to-source tasks without source filenames supplied in advance. Native candidate discovery is counted, source reads used only as the oracle are separate, and all returned bodies are checked against current file hashes. Source candidates remain distinct from verified runtime bindings. The scripts do not install Serena, activate a language server, launch the target application or use personal databases. + The delivery manifest covers Gateway JavaScript, all published Host files including dependency sidecars, four managed Skill documents, and package/SDK/Host lock configuration. It records the Git revision and toolchains. Timestamps and checkout paths do not participate in content identity. Hashes detect local mismatches; they are not signatures. Run a complete check after changing delivery inputs. Keep a complete previous checkout/artifact set for rollback; do not mix old DLLs with a new Gateway. Production startup uses `npm start` and the published Release Host. `npm run dev` explicitly enables Debug/dotnet-run fallback. `customHostPath` remains an explicit configuration override; an old helper must not be mistaken for a verified current release. Rebuilds do not replace a running client's MCP connection. diff --git a/README.md b/README.md index 86ee8b9..82b2ab7 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ WinCode is a local MCP server built for Windows and .NET engineering. It bridges - **Inspect the running app:** Enumerate visible windows, query specific controls or subtrees, and capture numbered visual overlays without activating or stealing focus from the target. - **Review with evidence:** Trace on-screen widgets back to literal XAML declaration tags, line numbers, and file hashes, with transparent reporting for ambiguity, truncation, or degraded upstreams. -Current source version: **0.12.2**. All UI tools are strictly read-only and non-destructive. See [CHANGELOG](CHANGELOG.md) for full version history. +Current source version: **0.12.3**. All UI tools are strictly read-only and non-destructive. See [CHANGELOG](CHANGELOG.md) for full version history. ### Quick start @@ -197,6 +197,8 @@ npm run benchmark:agent -- 1 # Opt-in pilot; -- 3 for three repetitions Live UI suites require an interactive Windows desktop session. In a 222-node test fixture, targeted queries reduced response text from 62 KB to ~1.6 KB while completing in ~0.78 seconds. Detailed test records are maintained in the [work log](docs/codex_worklog.md). +`npm run test:product -- ` explicitly runs six navigation-to-source tasks against an already running fixed test profile. It discovers source files, checks the live control and verifies literal command/method candidates, recording native and MCP calls, response characters and repeated source lines under `test-tmp/product-tasks`. It neither launches the application nor changes its data or source. This scripted acceptance does not establish runtime bindings, full-method coverage, native-only speedup or real Serena integration; see the acceptance matrix in the work log. + The agent benchmark covers ten scripted scenarios, including existing `dotnet-mini` C# fixtures and four levels of initial location knowledge. It validates returned files, ranges, bodies and status against current fixture contents. Tool/transport/response/cleanup failures remain in the JSON report under `test-tmp/agent-efficiency`; failed cases produce a nonzero exit code. Unchanged-evidence reuse is tested only under trusted, controlled fixture writes; edits require a new request. Reports measure MCP calls, output characters, repeated displayed lines and call time using local fallback with upstreams and GUI disabled. They do not establish real-agent completion rates, model-token savings or production cache benefits. Schema v2 results should not be compared directly with the earlier six-scenario report. --- @@ -209,7 +211,7 @@ WinCode 是面向 Windows 与 .NET 工程研发的本地 MCP 服务。它将项 - **观察实际界面:**发现系统可见窗口,按条件定向查询目标控件或子树,并在不激活、不抢占前台焦点的前提下获取数字标注截图。 - **源码双向印证:**将运行时抓取的控件关联回 XAML 源码声明的起始行号、代码片段与文件哈希,清晰报告歧义、截断与降级状态。 -当前源码版本为 **0.12.2**。所有 UI 取证工具均为纯只读与非侵入设计。版本历史见 [CHANGELOG](CHANGELOG.md)。 +当前源码版本为 **0.12.3**。所有 UI 取证工具均为纯只读与非侵入设计。版本历史见 [CHANGELOG](CHANGELOG.md)。 ### 快速上手 @@ -386,6 +388,8 @@ npm run benchmark:agent -- 1 # 显式小样本;三轮对照使用 -- 3 实机 UI 测试需要交互式 Windows 桌面会话。实测在包含 222 个节点的测试夹具中,定向查询将返回文本由 62 KB 降至约 1.6 KB,单次耗时稳定在 0.78 秒左右。详尽的测试记录参见 [工作日志](docs/codex_worklog.md)。 +`npm run test:product -- <专用测试PID> ` 显式运行六项导航到源码任务,要求固定测试 profile 已启动。它发现候选文件、核对实际控件及命令/方法文字候选,将原生和 MCP 调用、返回字符、重复源码行写入 `test-tmp/product-tasks`;不启动应用、不修改数据或源码。此脚本验收不证明运行时绑定、完整方法覆盖、相对纯原生工具提速或真实 Serena 集成;详见工作日志验收矩阵。 + Agent 基准包含 10 类脚本场景,复用现有 `dotnet-mini` C# 夹具,并按四种初始位置信息分层。返回的文件、行号、正文及状态均与当前夹具核对;工具错误、传输异常、响应损坏和清理失败会保留在 `test-tmp/agent-efficiency` 下的 JSON 报告中,失败返回非零退出码。无变化复用只在夹具写入受控、变化事件可信的条件下测试,修改后必须重新请求。测量使用本地回退,关闭上游与 GUI,记录 MCP 调用、返回字符、重复显示行和调用耗时,不代表真实 Agent 完成率、模型 Token 节省或生产缓存收益。Schema v2 场景与旧版六场景报告不同,不能直接比较两版总量。 --- diff --git "a/WinCode-\344\270\213\344\270\200\350\275\256\345\267\245\347\250\213\345\214\226\350\277\255\344\273\243\350\256\241\345\210\222\344\271\246.md" "b/WinCode-\344\270\213\344\270\200\350\275\256\345\267\245\347\250\213\345\214\226\350\277\255\344\273\243\350\256\241\345\210\222\344\271\246.md" index a8fa28d..c6930b8 100644 --- "a/WinCode-\344\270\213\344\270\200\350\275\256\345\267\245\347\250\213\345\214\226\350\277\255\344\273\243\350\256\241\345\210\222\344\271\246.md" +++ "b/WinCode-\344\270\213\344\270\200\350\275\256\345\267\245\347\250\213\345\214\226\350\277\255\344\273\243\350\256\241\345\210\222\344\271\246.md" @@ -235,3 +235,5 @@ ToolRouter 是允许知道具体实现的组装位置;Gateway 不再访问适 - 2026-09-08 更新:WP2 / 0.12.0 已按精确 head CI 合并 PR #23(main=39d2b20)。WP3 / 0.12.1 实施中,用户已确认 hello 被动、diagnose 主动;后续默认由主代理推进,独立审核与自审分别记载。 - 2026-09-08 20:16:48(北京时间):WP3 / 0.12.1 的精确 head CI 通过并合并 [PR #24](https://github.com/linnnn89/WinCode/pull/24),main=227463e。WP4 / 0.12.2 正在执行:已实现 Node 24/22、SDK/NuGet 锁定、统一 check 和桌面入口、交付清单、真实 Host 身份与维护文档;首轮核心 300 pass/1 skip、桌面 35/35。最终干净检出与 CI 尚待完成;D3/F12 继续保留未决,不将自审写成独立审查。 - WP4 干净检出经一次目录名假设修正后两次完整 check 通过,内容身份稳定;非仓库 cwd 的 stdio 与原生 Host health 通过。D3 未决,因此仅建立仓内检查和精确 head 人工核对,不声明 GitHub 已强制保护。详情与失败证据见工作日志。 +- 2026-09-08 20:44:51(北京时间):WP4 / 0.12.2 精确 head 的 Node 22/24 与 CodeQL 全通过,合并 [PR #25](https://github.com/linnnn89/WinCode/pull/25),main=ba94c00。D3 仍未决,工程交付完成不等于分支保护已生效。 +- WP5 / 0.12.3:已完成新 stdio 精准范围、固定 TavernDesk 六导航→源码候选、真实 WPF 修复闭环和实际截图核对;产品任务 30 次调用/67103 UTF-16 字符,127 显示源码行中 42 行重复,不宣称普遍提速。实际 Codex hello 仍为 0.11.2,安装 Skill 已备份同步。真实 Serena 命令缺失、用户新导入角色卡尚待定位,按本计划保持部分完成;详见工作日志验收矩阵。F12/D3 未决状态不变。 diff --git a/docs/codex_worklog.md b/docs/codex_worklog.md index 6aa0b2a..3c30aa0 100644 --- a/docs/codex_worklog.md +++ b/docs/codex_worklog.md @@ -449,3 +449,32 @@ - 干净检出修正后完整 check 两次通过(均 300 pass、1 skip、0 fail/取消),报告 2026-09-08T12-39-02-455Z-core 与 12-39-56-230Z-core。两次 Gateway buildId=88c96147c9fc9430c5d5f39f0c27cf17d9b1a2ad5323c548c46fc817a35fe9ec;交付 contentId=5795657ffac19488b24503baf9ca2d867f5d1f24fa2a5deaa3c461783066c043,时间戳变化而内容身份相同。依赖来自该检出的 npm ci 与共享 NuGet 下载缓存,没有借用原工作区 dist/bin;这不是空机器离线构建证明。 - 从系统 Temp(非仓库 cwd)直启该干净检出的发布 Host,health 成功,真实 version=0.12.2、configuration=Release、informationalVersion=0.12.2+b6901cd960da460fa91bb2eeba81e63eac716ae4、framework=.NET 10.0.11。生产 stdio 测试也在独立临时工作目录运行。 - 反证自审:只覆盖 Host 主 EXE 会漏掉依赖 DLL 混用,清单实际枚举整个发布目录并用新增/缺失/替换反例验证;同版本旧内容仍通过哈希检测。未提供签名真实性证明,也未更新父 Codex 连接。最终 PR 的 Node 22/24、CodeQL 结果仍须在提交后核对。 + +## 2026-09-08 — WP4 合并与 WP5 / 0.12.3 真实应用验收 + +- WP4 PR #25 的精确 head=8d36bc7727fa5f85ba72ff44992927d760c0614d,Node 22/24、CodeQL 三语言分析及汇总均 success,于北京时间 20:44:51 合并,main=ba94c003a1b4ed130cbd0bd73389506f3f348481。CI 34227530379、CodeQL 34227527264,日志 test-tmp/wp4-ci-passed.log。强制 Host Rebuild 后再次验证,完整交付哈希保持相同;不仅依赖增量构建跳过。 +- WP5 增加显式 test:product,复用既有 Client、stdio、EvidenceOverlap;六个固定导航 ID,输入不预先提供 XAML/C# 文件名。每项先用有界 rg 发现 XAML 与 C# 候选,再调用 ui_review、赋值精确行和方法声明窗口。按源码行与 SHA-256 核对正文;错误、耗时、实际返回字符和清理失败留在本地报告。没有新增依赖、插件层、生产缓存或源码映射推断。 +- 固定 TavernDesk profile=I:/New-tarven/work/TAVERN-TEST/profile;第一次本轮启动 PID=6512、HWND=0x140B0E,回执核对测试 DB/config/logs 路径,无 Fresh、无重复导入参数。六项通过:NavDashboard、NavChat、NavCampaigns、NavCharacters、NavWorldbooks、NavSettings。每项 2 原生+3 MCP,合计 30 次(12 原生、18 MCP)、67103 UTF-16 字符;第二次最终脚本实测调用耗时合计 5145 ms,连接/打开另计 2 次、8099 字符。显示源码 127 行、重复 42 行。结果 test-tmp/product-tasks/1788871732133-22296/report.json、1788871829665-10888/report.json;单应用、单操作者、脚本固定任务,未做盲试验或纯原生等价对照,不能推算通用提速或模型 token。 +- 同一实例上的 test:tavern-context 八种范围场景通过;精确行 2219 字符、allRequestedCovered=true,符号窗口 2453 字符定位方法但全方法覆盖未知,已知整文件请求 2387 字符仍未定位目标。范围正确与任务充分性分开。三次 UI→赋值→方法请求通过;没有把 snippet 非空升级为完整方法。报告 test-tmp/r3/acceptance-1788871741500.json。 +- 当前 Codex 的实际 hello 确认为 0.11.2,实例 9706c136-9daf-44e8-9811-2c0f3b755f6d,启动于 2026-09-08T10:33:06.633Z,buildId=186f4bfc8f4d3cbc5ac2f7fe9f96ec98fa28c6fca1e6b862916923f523d778c7。它仍调用旧 Gateway;其新启动的磁盘 Host 实际报告 0.12.3/Release。通过当前旧连接做了一次后台原图检查,printWindowDwm,捕获质量提示 unknown;实际查看可读到 V3 固定样本角色和六个导航,没有仅凭像素变化宣称成功。此证据属于旧 Gateway+新 Host 组合,不混入新 stdio 版本通过项。没有重载客户端、修改 MCP 配置或杀死该旧连接。 +- 已按用户“Skill 明确规范字段”的要求,将四份受管手册备份后同步到 C:/Users/40218/.agents/skills/wincode;随后 skill:check matched=true。备份 .wincode-backup-f76fb13c-62fb-4259-bec8-ed514885efa5。规范字段、容忍未知字段以及固定测试 profile 复用指引已进入实际安装手册;它不证明当前 MCP 已重连。 +- 本机 PATH 未发现 serena/serena-mcp-server;uv tool list 为空,受限检查 uv archive 中未发现 Serena 包;当前实际 hello 也报告 commandFound=false/handshakeOk=false。真实 Serena/C# 语义集成未运行。上游官方配置说明 C# 使用 .NET 10 与自动下载的 Roslyn Language Server(https://github.com/oraios/serena/blob/main/docs/02-usage/050_configuration.md);未因此自动下载/安装,已询问可用现有路径,保留未验证项。 +- 最终本地 check 再次通过:300 pass、1 个既有可选 TavernDesk skip、0 fail/取消,报告 test-tmp/check/2026-09-08T12-53-08-086Z-core/report.json;交互桌面 35/35,包括真实隔离 WPF 缺陷→源码修复→构建→运行复查,报告 2026-09-08T12-49-49-117Z-desktop。后续改动为文档、测试脚本计数/关闭日志,未改 UI 生产实现。真实业务源码未修改。 +- 自审反例:六次命令赋值落在同一构造函数,逐项 nextRequest 仍重复显示 42 行。Skill 因此提醒复用已核对且未变化的正文,必要时合并有界范围;测试继续报告原始重复量,不先删除重复结果美化指标。保留 runtimeSourceVerified=false、wholeMethodCoverage=unknown;不重开 R7–R9 或引入缓存层。 +- 用户随后说明导入真实角色卡;先前自有 PID 6512 已正常 CloseMainWindow 退出。复用原 profile 重启为 PID=33672、HWND=0x330A40,后台查看当前仍显示 V3 固定样本;尚未确认用户新卡所在位置,已询问名称/是否在 [TEST] 窗口导入。没有将此重启误称真实角色卡验收通过,没有扫描个人数据库。新卡状态验收待定位后补充。 +- F12 API 在 main=ba94c003a1b4ed130cbd0bd73389506f3f348481 仍 state=open;D3 分支保护也未获单独确认。真实 Serena、当前 Codex 连接重载、新角色卡定位均未完成;这些限制不以受控测试替代。遵循减少子代理偏好,本轮只有主代理自审,无新增独立审核。 + +### WP5 验收矩阵(2026-09-08,后续增订) + +| 能力 | 环境 / 数据 | 证据类型 | 结果与范围 | +| --- | --- | --- | --- | +| 构建、契约、取消/资源 | Node 24、本机隔离夹具 | 受控进程与故障注入 | 核心 300 pass、1 skip;远端最终 head 结果待 PR | +| WPF UI→源码修复闭环 | 隔离真实 WPF 窗口 | 真实运行+合成源码 | 35/35 桌面套件,含修复后运行复查 | +| 六项导航→源码 | 固定 TavernDesk / V3 测试样本 | 真实应用+字面源码候选 | 6/6;赋值与方法声明验证,运行时绑定/完整方法未知 | +| 精准范围 | TavernDesk 当前源码、独立新 stdio | 真实源码只读核对 | 8 种场景与 UI 候选闭环通过;整文件请求不保证定位 | +| 后台截图 | 当前旧 Gateway 0.11.2 + Host 0.12.3 | 实际查看原图 | 可读;captureQuality=unknown,不冒充新 Gateway GUI 验收 | +| Serena/C# 重载→引用→正文 | 未找到可用本地命令 | 未验证 | 未安装上游/语言服务器,WP5 语义集成保持未完成 | +| 用户新导入真实角色卡 | 待确定名称及测试 profile | 未验证 | 当前画面仍为固定样本,等待定位信息 | +| 当前 Codex 重连 | 存活 0.11.2 连接 | 实际 hello | 已识别旧实例,未重载 | +- 用户已确认新角色卡在书架中;Computer Use 的 list_windows 再次把测试 HWND=3344960(0x330A40)归给 AiPPTAddin.App.exe,get_window 报“window id 3344960 no longer belongs ... current owner ...”,刷新列表后唯一一次重试同样失败。未构造伪造窗口对象或绕过驱动注入输入,已请用户在该测试窗口打开新角色卡。WinCode 按 PID/HWND 的同窗读取和截图仍正常,故这项障碍归因 Computer Use,不归因 WinCode;真实卡显示验收继续等待界面切换。 +- 对最终隔离 WPF 修复测试补充反证检查:临时项目在仓库之外会脱离 global.json,因此现在明确复制已有 SDK 策略与 packages.lock.json,并采用 --locked-mode+原有离线源恢复。没有下载新依赖。test:ui-code 真实修复闭环单项复测通过,typecheck 通过,日志 test-tmp/wp5-locked-ui-code.log。 diff --git a/package-lock.json b/package-lock.json index fd8d82d..789db27 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "wincode-mcp", - "version": "0.12.2", + "version": "0.12.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "wincode-mcp", - "version": "0.12.2", + "version": "0.12.3", "license": "MIT", "dependencies": { "@modelcontextprotocol/client": "2.0.0", diff --git a/package.json b/package.json index 2fd29f8..0871380 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wincode-mcp", - "version": "0.12.2", + "version": "0.12.3", "description": "Windows-first MCP gateway: .NET project graph, evidence-bounded context, honest change-impact, long-running process hygiene", "main": "dist/index.js", "type": "module", @@ -16,6 +16,7 @@ "test:verify": "tsx tests/verify.ts", "benchmark:agent": "tsx scripts/benchmark-agent-efficiency.ts", "test:benchmark": "tsx --test tests/agent-efficiency-benchmark.test.ts", + "test:product": "tsx scripts/verify-product-tasks.ts", "test:tavern-context": "tsx scripts/verify-tavern-context.ts", "test:e2e": "tsx scripts/test-mcp-client.ts", "typecheck": "tsc -p tsconfig.test.json", diff --git a/scripts/verify-product-tasks.ts b/scripts/verify-product-tasks.ts new file mode 100644 index 0000000..dcb55d9 --- /dev/null +++ b/scripts/verify-product-tasks.ts @@ -0,0 +1,161 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { Client } from '@modelcontextprotocol/client'; +import { StdioClientTransport } from '@modelcontextprotocol/client/stdio'; +import { EvidenceOverlap } from './benchmark-agent-efficiency.js'; + +// Opt-in, read-only acceptance against an already running dedicated TavernDesk profile. +// These scripted tasks establish source candidates, not runtime binding or whole-method correctness. +const [target, pidText, hwnd] = process.argv.slice(2); +assert.ok(target && /^[1-9]\d*$/.test(pidText ?? '') && /^0x[\da-f]+$/i.test(hwnd ?? ''), + 'Usage: npm run test:product -- '); +const workspace = await fs.realpath(target); +const pid = Number(pidText); +assert.ok(Number.isSafeInteger(pid)); +const receipt = JSON.parse(await fs.readFile(path.join(workspace, 'work/TAVERN-TEST/profile/startup-result.json'), 'utf8')); +assert.equal(receipt.processId, pid, 'PID must match the fixed test profile receipt'); +assert.equal(path.resolve(receipt.testRoot), path.join(workspace, 'work/TAVERN-TEST/profile')); +assert.equal(receipt.status, 'window-shown'); +const repo = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const temporary = await fs.mkdtemp(path.join(os.tmpdir(), 'wincode-product-')); +const client = new Client({ name: 'product-task-acceptance', version: '1' }); +let transport: StdioClientTransport | undefined; +const report: any = { formatVersion: 1, startedAt: new Date().toISOString(), workspace, pid, hwnd, + connection: 'fresh production stdio', upstreams: false, codexConnectionVerified: false, + taskScope: 'unknown file → unique live navigation control → literal command assignment → method declaration candidate', + limitations: ['Six scripted navigation tasks, one operator and one fixed test application; not blind agent trials.', + 'Native rg candidate discovery is counted; oracle reads and connection setup are separate.', + 'No native-only comparison, model-token estimate, universal speedup or full-method coverage claim.', + 'No clicks, provider calls, source edits or screenshots; runtime binding/build-source identity remain unverified.'], + setup: [], tasks: [], success: false }; +const originals = new Map(); +const overlap = new EvidenceOverlap(); +const hash = (value: string) => createHash('sha256').update(value).digest('hex'); +async function source(file: string) { + const full = await fs.realpath(path.join(workspace, file)); + const relative = path.relative(workspace, full); + assert.ok(relative && !relative.startsWith('..') && !path.isAbsolute(relative)); + assert.ok((await fs.stat(full)).size <= 256 * 1024); + const text = await fs.readFile(full, 'utf8'); + if (originals.has(file)) assert.equal(hash(text), originals.get(file), 'source changed during acceptance'); + else originals.set(file, hash(text)); + return text; +} +async function call(name: string, args: Record, metrics: any[]) { + const start = performance.now(); + const metric: any = { tool: name, kind: 'mcp', characters: 0, elapsedMs: 0, success: false }; + metrics.push(metric); + try { + const result: any = await client.callTool({ name, arguments: args }, { timeout: 30000 }); + metric.characters = result.content?.reduce((n: number, block: any) => n + (block.text?.length ?? 0), 0) ?? 0; + assert.notEqual(result.isError, true, JSON.stringify(result).slice(0, 2048)); + assert.equal(result.content?.[0]?.type, 'text'); + const data = JSON.parse(result.content[0].text); + metric.success = true; + return data; + } catch (error) { metric.error = String(error).slice(0, 2048); throw error; } + finally { metric.elapsedMs = Math.round(performance.now() - start); } +} +function search(needle: string, glob: string, before: number, metrics: any[]) { + const start = performance.now(); + const result = spawnSync('rg', ['--no-heading', '--color', 'never', '-n', '-B', String(before), '-F', '--glob', glob, '--', needle, 'src'], + { cwd: workspace, encoding: 'utf8', windowsHide: true, timeout: 5000, maxBuffer: 65536 }); + metrics.push({ tool: 'rg', kind: 'native', characters: (result.stdout?.length ?? 0) + (result.stderr?.length ?? 0), + elapsedMs: Math.round(performance.now() - start), success: !result.error && result.status === 0 }); + assert.ok(!result.error && result.status === 0, result.error?.message ?? result.stderr ?? 'No source candidate'); + return result.stdout; +} +async function validateEvidence(data: any) { + assert.ok(data.evidence?.length > 0); + for (const evidence of data.evidence) { + const lines = (await source(evidence.file)).split(/\r?\n/); + assert.ok(Number.isInteger(evidence.startLine) && Number.isInteger(evidence.endLine) && evidence.startLine > 0 && evidence.endLine <= lines.length); + const expected = lines.slice(evidence.startLine - 1, evidence.endLine).join('\n'); + assert.ok(evidence.snippet === expected || (evidence.truncated === true && expected.startsWith(evidence.snippet)), 'returned body differs from source'); + } + overlap.add(data.evidence); +} +try { + const url = (file: string) => JSON.stringify(pathToFileURL(path.join(repo, 'dist', file)).href); + const bootstrap = path.join(temporary, 'probe.mjs'); + await fs.writeFile(bootstrap, ` +import { getDefaultConfig } from ${url('Core/Config.js')}; +import { ToolRouter } from ${url('Core/ToolRouter.js')}; +import { WinCodeMcpServer } from ${url('Gateway/McpServer.js')}; +const config = getDefaultConfig(${JSON.stringify(temporary)}); +config.adapters.serena.enabled = false; +config.adapters.repomix.useCli = false; +const server = new WinCodeMcpServer(new ToolRouter(config)); +process.stdin.on('end', () => { void server.stop(); }); +process.on('SIGTERM', () => { void server.stop(); }); +await server.start(); +`); + transport = new StdioClientTransport({ command: process.execPath, args: [bootstrap], cwd: temporary, stderr: 'pipe' }); + await client.connect(transport); + const hello = await call('wincode_hello_world', {}, report.setup); + assert.equal(hello.runtime.build.status, 'verified'); + report.runtime = hello.runtime; + report.schemaHash = hello.toolContract.schemaHash; + await call('workspace_open', { path: workspace }, report.setup); + for (const id of ['NavDashboard', 'NavChat', 'NavCampaigns', 'NavCharacters', 'NavWorldbooks', 'NavSettings']) { + const task: any = { id, calls: [], success: false }; + report.tasks.push(task); + try { + const xamlSearch = search(id, '*.xaml', 1, task.calls); + const xaml = [...new Set([...xamlSearch.matchAll(/^(.+\.xaml)[:-]\d+[:-]/gm)].map(match => match[1].replaceAll('\\', '/')))]; + const commands = [...new Set([...xamlSearch.matchAll(/Command="\{Binding (\w+)\}"/g)].map(match => match[1]))]; + assert.equal(xaml.length, 1, 'XAML discovery must be unambiguous'); + assert.equal(commands.length, 1, 'simple command binding must be unambiguous'); + const codeSearch = search(commands[0], '*.cs', 0, task.calls); + const code = [...new Set([...codeSearch.matchAll(/^(.+\.cs):\d+:/gm)].map(match => match[1].replaceAll('\\', '/')))]; + assert.ok(code.length > 0 && code.length <= 8, 'candidate code files exceed the existing mapper contract'); + await source(xaml[0]); + const ui = await call('wincode_ui_review', { pid, hwnd, backgroundOnly: true, capture: 'none', readStates: true, maxNodes: 100, + query: { automationId: id, controlType: 'Button' }, candidateFiles: xaml, candidateCodeFiles: code }, task.calls); + assert.equal(ui.success, true); + assert.equal(ui.queryResult.status, 'unique'); + assert.equal(ui.queryResult.searchComplete, true); + assert.equal(ui.tree.automationId, id); + assert.equal(ui.codeEvidence.runtimeSourceVerified, false); + const assignments = ui.codeEvidence.clues.filter((clue: any) => clue.identifier === commands[0]) + .flatMap((clue: any) => clue.candidates).filter((candidate: any) => candidate.kind === 'assignment' && candidate.relatedSymbol); + assert.equal(assignments.length, 1, 'command assignment must be an unambiguous literal candidate'); + const assignment = assignments[0]; + assert.equal(hash(await source(assignment.file)), assignment.fileSha256); + const evidence = await call('wincode_prepare_context', assignment.nextRequest, task.calls); + await validateEvidence(evidence); + assert.equal(evidence.coverage.allRequestedCovered, true); + const body = await call('wincode_prepare_context', { task: 'Locate the method declaration named by this command assignment.', + scopeFiles: [assignment.file], symbol: assignment.relatedSymbol }, task.calls); + await validateEvidence(body); + assert.ok(body.evidence.some((item: any) => new RegExp(`(?:private|public|protected|internal)\\s+[^\\n]*\\b${assignment.relatedSymbol}\\(`).test(item.snippet))); + task.candidate = { xaml: xaml[0], command: commands[0], file: assignment.file, line: assignment.line, method: assignment.relatedSymbol }; + task.evidence = { assignmentCovered: true, methodDeclarationLocated: true, wholeMethodCoverage: 'unknown', runtimeSourceVerified: false, + isEnabled: ui.tree.isEnabled, returnedRanges: body.evidence.map(({ file, startLine, endLine, truncated }: any) => ({ file, startLine, endLine, truncated })) }; + task.success = true; + } catch (error) { task.error = String(error).slice(0, 2048); } + } + for (const file of originals.keys()) await source(file); + report.sourceHashes = Object.fromEntries(originals); + report.overlap = { displayedSourceLines: overlap.lines, repeatedSourceLines: overlap.repeatedLines }; + report.success = report.tasks.every((task: any) => task.success); +} catch (error) { report.error = String(error).slice(0, 2048); } +finally { + try { try { await client.close(); } finally { await transport?.close(); } } + catch (error) { report.success = false; report.cleanupError = String(error).slice(0, 2048); } + try { assert.equal(path.dirname(temporary), os.tmpdir()); await fs.rm(temporary, { recursive: true, force: true }); } + catch (error) { report.success = false; report.temporaryCleanupError = String(error).slice(0, 2048); } + report.finishedAt = new Date().toISOString(); + const directory = path.join(repo, 'test-tmp/product-tasks', `${Date.now()}-${process.pid}`); + await fs.mkdir(directory, { recursive: true }); + await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify(report, null, 2) + '\n'); + console.log(JSON.stringify({ report: path.join(directory, 'report.json'), success: report.success, + tasks: report.tasks.map((task: any) => ({ id: task.id, success: task.success, calls: task.calls.length, + characters: task.calls.reduce((sum: number, call: any) => sum + call.characters, 0), error: task.error })) }, null, 2)); + if (!report.success) process.exitCode = 1; +} diff --git a/skills/wincode/SKILL.md b/skills/wincode/SKILL.md index 4b5e3f6..0415b0f 100644 --- a/skills/wincode/SKILL.md +++ b/skills/wincode/SKILL.md @@ -5,7 +5,7 @@ description: 使用 WinCode MCP 分析 Windows/.NET 工作区,或读取桌面 # WinCode -仓库手册版本:0.12.2。安装内容可用 `node scripts/sync-skill.mjs <安装目录绝对路径>` 核对;仅维护时执行,不在每个任务中例行检查。以当前连接实际 Schema 为准,手册版本不证明 MCP 已重连。 +仓库手册版本:0.12.3。安装内容可用 `node scripts/sync-skill.mjs <安装目录绝对路径>` 核对;仅维护时执行,不在每个任务中例行检查。以当前连接实际 Schema 为准,手册版本不证明 MCP 已重连。 仅按当前任务读取对应手册,不预读全部文件: - 代码、上下文、引用、影响分析:[code](references/code.md)。 diff --git a/skills/wincode/references/ui.md b/skills/wincode/references/ui.md index 4b3205d..9272cd6 100644 --- a/skills/wincode/references/ui.md +++ b/skills/wincode/references/ui.md @@ -47,4 +47,6 @@ captureQuality 在标注前检查原始像素,最多采样 1024 点;suspect- - runtimeBuildSourceIdentity=unknown、templateResolution=unsupported;不推定 DataContext 或 CanExecute,也不把构造参数相似当执行链证明。多个同名声明继续保留歧义,不能自动选择首项。 - nextRequest 先读候选所在精确行;relatedSymbol 仅为构造参数文字线索,可能是变量。核对赋值后再明确请求方法,不能因该字段自动认定为方法或完整调用链。复杂插值原始字符串/超出嵌套限制的文字扫描报告不支持。 -内置 Host 强制显示半透明 REC/WinCoding 标志并记录极简审计,不绕过。审计提示按诊断手册处理。取证不授权点击、输入或读取其他窗口。启动测试应用时使用其全新隔离数据模式,避免个人数据库。 +多个控件若指向同一文件、相邻赋值,可在确认文件未变化后复用当前会话已展示的精确行;有缺口时合并为一次有界 lineRanges 请求。不要因为每个候选都带 nextRequest 就机械重复读取。复用仅限已经核对的正文,不代表 WinCode 有生产级跨请求缓存,也不扩大运行时绑定结论。 + +内置 Host 强制显示半透明 REC/WinCoding 标志并记录极简审计,不绕过。审计提示按诊断手册处理。取证不授权点击、输入或读取其他窗口。启动测试应用时使用专用隔离数据模式;已有明确的固定测试 profile 时复用它,避免每次重复初始化,禁止使用个人数据库。 diff --git a/src/Core/Config.ts b/src/Core/Config.ts index 5ebe78a..c1a8578 100644 --- a/src/Core/Config.ts +++ b/src/Core/Config.ts @@ -1,6 +1,6 @@ import path from 'node:path'; -export const WINCODE_VERSION = '0.12.2'; +export const WINCODE_VERSION = '0.12.3'; /** * Bounded waits for every external process/RPC. None of these may be Infinity. diff --git a/tests/ui-code-runtime.test.ts b/tests/ui-code-runtime.test.ts index 4241103..baa4253 100644 --- a/tests/ui-code-runtime.test.ts +++ b/tests/ui-code-runtime.test.ts @@ -15,7 +15,7 @@ import type { UiReviewResult } from '../src/CompositeTools/UiReview.js'; const runFile = promisify(execFile); const sha256 = (data: string | Buffer) => createHash('sha256').update(data).digest('hex'); const repo = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); -const fixtureFiles = ['wpf-ui-review.csproj', 'App.xaml', 'App.xaml.cs', 'AssemblyInfo.cs', 'MainWindow.xaml', 'MainWindow.xaml.cs']; +const fixtureFiles = ['wpf-ui-review.csproj', 'packages.lock.json', 'App.xaml', 'App.xaml.cs', 'AssemblyInfo.cs', 'MainWindow.xaml', 'MainWindow.xaml.cs']; const defect = 'return false; // R6_SOURCE_DEFECT'; const repair = 'return true; // R6_SOURCE_FIXED'; @@ -88,6 +88,7 @@ it('real isolated WPF source repair closes runtime → XAML → C# → precise b }; try { await fs.mkdir(sourceRoot); + await fs.copyFile(path.join(repo, 'global.json'), path.join(root, 'global.json')); await fs.mkdir(path.join(root, 'offline-feed')); for (const file of fixtureFiles) { const content = await fs.readFile(path.join(repo, 'tests/fixtures/wpf-ui-review', file)); @@ -99,7 +100,7 @@ it('real isolated WPF source repair closes runtime → XAML → C# → precise b const nugetConfig = path.join(root, 'NuGet.Config'); await fs.writeFile(nugetConfig, ''); // All package sources are replaced by an empty local feed. Existing SDK packs/cache only. - await runFile('dotnet', ['restore', project, '--configfile', nugetConfig, '-p:NuGetAudit=false'], { + await runFile('dotnet', ['restore', project, '--locked-mode', '--configfile', nugetConfig, '-p:NuGetAudit=false'], { cwd: root, env: dotnetEnvironment, windowsHide: true, timeout: 60000, maxBuffer: 1024 * 1024, }); await compile(); diff --git a/tools/WinCode.UIA.Host/WinCode.UIA.Host.csproj b/tools/WinCode.UIA.Host/WinCode.UIA.Host.csproj index 0d42895..051fccb 100644 --- a/tools/WinCode.UIA.Host/WinCode.UIA.Host.csproj +++ b/tools/WinCode.UIA.Host/WinCode.UIA.Host.csproj @@ -1,7 +1,7 @@ - 0.12.2 + 0.12.3 true Exe net10.0-windows From c446b6b04a377ccc30394c2c9ac748914e5da96b Mon Sep 17 00:00:00 2001 From: linnnn89 <216342082+linnnn89@users.noreply.github.com> Date: Tue, 8 Sep 2026 21:08:21 +0800 Subject: [PATCH 2/2] test: preserve actual test totals and real card acceptance evidence --- ...44\273\243\350\256\241\345\210\222\344\271\246.md" | 1 + docs/codex_worklog.md | 6 ++++++ scripts/check.mjs | 11 +++++++++-- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git "a/WinCode-\344\270\213\344\270\200\350\275\256\345\267\245\347\250\213\345\214\226\350\277\255\344\273\243\350\256\241\345\210\222\344\271\246.md" "b/WinCode-\344\270\213\344\270\200\350\275\256\345\267\245\347\250\213\345\214\226\350\277\255\344\273\243\350\256\241\345\210\222\344\271\246.md" index c6930b8..34ab85b 100644 --- "a/WinCode-\344\270\213\344\270\200\350\275\256\345\267\245\347\250\213\345\214\226\350\277\255\344\273\243\350\256\241\345\210\222\344\271\246.md" +++ "b/WinCode-\344\270\213\344\270\200\350\275\256\345\267\245\347\250\213\345\214\226\350\277\255\344\273\243\350\256\241\345\210\222\344\271\246.md" @@ -237,3 +237,4 @@ ToolRouter 是允许知道具体实现的组装位置;Gateway 不再访问适 - WP4 干净检出经一次目录名假设修正后两次完整 check 通过,内容身份稳定;非仓库 cwd 的 stdio 与原生 Host health 通过。D3 未决,因此仅建立仓内检查和精确 head 人工核对,不声明 GitHub 已强制保护。详情与失败证据见工作日志。 - 2026-09-08 20:44:51(北京时间):WP4 / 0.12.2 精确 head 的 Node 22/24 与 CodeQL 全通过,合并 [PR #25](https://github.com/linnnn89/WinCode/pull/25),main=ba94c00。D3 仍未决,工程交付完成不等于分支保护已生效。 - WP5 / 0.12.3:已完成新 stdio 精准范围、固定 TavernDesk 六导航→源码候选、真实 WPF 修复闭环和实际截图核对;产品任务 30 次调用/67103 UTF-16 字符,127 显示源码行中 42 行重复,不宣称普遍提速。实际 Codex hello 仍为 0.11.2,安装 Skill 已备份同步。真实 Serena 命令缺失、用户新导入角色卡尚待定位,按本计划保持部分完成;详见工作日志验收矩阵。F12/D3 未决状态不变。 +- WP5 新卡补充:用户指明书架角色后,入库、原始 JSON 字段保持、头像文件与书架截图已核对;在新 0.12.3 Gateway/Host 中,同名两文本节点正确返回完整但 ambiguous,不猜测唯一项。含真实卡的六导航取证再度 6/6;详情动作/聊天、真实 Serena、当前 Codex 重连仍未验收。不会把 Computer Use 误归属问题记为 WinCode 故障。 diff --git a/docs/codex_worklog.md b/docs/codex_worklog.md index 3c30aa0..68d3bae 100644 --- a/docs/codex_worklog.md +++ b/docs/codex_worklog.md @@ -478,3 +478,9 @@ | 当前 Codex 重连 | 存活 0.11.2 连接 | 实际 hello | 已识别旧实例,未重载 | - 用户已确认新角色卡在书架中;Computer Use 的 list_windows 再次把测试 HWND=3344960(0x330A40)归给 AiPPTAddin.App.exe,get_window 报“window id 3344960 no longer belongs ... current owner ...”,刷新列表后唯一一次重试同样失败。未构造伪造窗口对象或绕过驱动注入输入,已请用户在该测试窗口打开新角色卡。WinCode 按 PID/HWND 的同窗读取和截图仍正常,故这项障碍归因 Computer Use,不归因 WinCode;真实卡显示验收继续等待界面切换。 - 对最终隔离 WPF 修复测试补充反证检查:临时项目在仓库之外会脱离 global.json,因此现在明确复制已有 SDK 策略与 packages.lock.json,并采用 --locked-mode+原有离线源恢复。没有下载新依赖。test:ui-code 真实修复闭环单项复测通过,typecheck 通过,日志 test-tmp/wp5-locked-ui-code.log。 +- 真实角色卡补充验收:用户指明书架中的角色后,实际截图已显示其封面及名称。仅对固定 profile/data/taverndesk.db 使用 SQLite URI mode=ro 与 PRAGMA query_only,精确不区分大小写名称匹配为 1 条;原始卡片 JSON 与导入报告 JSON 可解析,描述 2896 字符、开场白 670 字符均与原卡字段一致。头像路径按 TavernDesk AppDataPaths 规则相对 data 根解析,文件存在且位于专用数据根内;未修改数据库、未公开角色卡正文/图片/原文件路径。 +- 该角色名在界面出现为两个 TextBlock,完整搜索 84 个节点后返回 ambiguous、2 matches、不展开 tree。当前连接的截图可读;另用新载入的 0.12.3 生产 Gateway/Host 经 InMemory MCP 验证相同歧义行为,结果 test-tmp/wp5-user-card.json。它不表示重复入库或查询失败,也不证明动态角色卡已有稳定唯一 AutomationId。详情页动作/聊天生成未验收,Computer Use 窗口归属错误仍存在。 +- 真实角色卡存在的书架状态下,新 stdio 六导航任务再次 6/6 通过:30 次调用、67083 UTF-16 字符、5134 ms 调用时间合计,重复显示 42 行。报告 test-tmp/product-tasks/1788872623250-36420/report.json。此证据替代上表“新卡位置未知”的当前状态,保留此前历史记录;已确认入库/书架显示,未声称聊天或全部卡片语义兼容。 +- 临时探针曾调用不存在的 connectTransport(失败日志 wp5-user-card-probe.log),核对既有 runtime-contract 测试后改为公开 SDK InMemoryTransport 与 Gateway 内已有 server.connect,复测通过;没有为测试添加生产连接 API。头像初检把相对路径误按 WinCode cwd 解析,随后查明 AppDataPaths 的 data 根规则并正确复核;初检 false 不属于应用文件丢失。 +- 为让远端检查报告直接提供实际测试数量,check 使用 Node 内置 TAP reporter 并收集 tests/pass/fail/cancelled/skipped 摘要;不只留下“命令退出 0”。缺少完整测试摘要仍报错,不把未运行套件记为通过。 +- TAP 报告补充实测:核心 check 通过,report.tests={tests:301,pass:300,fail:0,cancelled:0,skipped:1}(2026-09-08T13-05-15-874Z-core);桌面 check 通过,report.tests={tests:35,pass:35,fail:0,cancelled:0,skipped:0}(2026-09-08T13-06-36-861Z-desktop)。更新 PR 后重新核对精确 head CI,不沿用先前 head 的成功状态。 diff --git a/scripts/check.mjs b/scripts/check.mjs index e53827f..840fa00 100644 --- a/scripts/check.mjs +++ b/scripts/check.mjs @@ -43,6 +43,13 @@ if (inventoryOnly) { return result.stdout; } const node = (name, args) => run(name, process.execPath, args); + const testTotals = output => { + const totals = Object.fromEntries([...output.matchAll(/^# (tests|pass|fail|cancelled|skipped) (\d+)\r?$/gm)] + .map(match => [match[1], Number(match[2])])); + if (!(totals.tests > 0) || ['pass', 'fail', 'cancelled', 'skipped'].some(key => totals[key] === undefined)) + throw new Error('Test process did not return a complete TAP summary.'); + return totals; + }; const tsc = path.join(root, 'node_modules/typescript/bin/tsc'); const tsx = path.join(root, 'node_modules/tsx/dist/cli.mjs'); const native = 'tools/WinCode.UIA.Host/WinCode.UIA.Host.csproj'; @@ -56,7 +63,7 @@ if (inventoryOnly) { await node('verify-delivery', ['scripts/delivery-manifest.mjs', '--verify']); await run('restore-wpf', 'dotnet', ['restore', wpf, '--locked-mode']); await run('publish-wpf', 'dotnet', ['publish', wpf, '-c', 'Release', '-r', 'win-x64', '--no-self-contained', '--no-restore', ...deterministic]); - await node('desktop-tests', [tsx, '--test', '--test-concurrency=1', ...groups['test:ui'], ...groups['test:ui-code']]); + report.tests = testTotals(await node('desktop-tests', [tsx, '--test', '--test-reporter=tap', '--test-concurrency=1', ...groups['test:ui'], ...groups['test:ui-code']])); } else { await node('typecheck', [tsc, '-p', 'tsconfig.test.json']); await node('build-gateway', ['scripts/build.mjs']); @@ -65,7 +72,7 @@ if (inventoryOnly) { await run('publish-host', 'dotnet', ['publish', native, '-c', 'Release', '-r', 'win-x64', '--no-self-contained', '--no-restore', ...deterministic]); await run('build-audit', 'dotnet', ['build', audit, '-c', 'Debug', '--no-restore', ...deterministic]); await run('build-query', 'dotnet', ['build', query, '-c', 'Release', '--no-restore', ...deterministic]); - await node('regression', [tsx, '--test', ...groups.test]); + report.tests = testTotals(await node('regression', [tsx, '--test', '--test-reporter=tap', ...groups.test])); const stdio = JSON.parse(await node('stdio', [tsx, 'scripts/test-mcp-client.ts'])); report.runtime = { build: stdio.runtime?.build, schemaHash: stdio.schemaHash, toolCount: stdio.toolCount, resourceCleanup: stdio.resourceCleanup, codexConnectionVerified: false };