diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3246ec4..35aa5f2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,7 +23,7 @@ Windows CI jobs have a 20-minute overall budget for cold setup and native accept `node scripts/measure-runtime-baseline.mjs` creates a small C# project, runs three fresh stdio clients, measures unused/cold/warm/exit phases, and profiles source Router startup separately. It records snapshots under `test-tmp/runtime-baseline/`, using the existing SDK. Run it without competing check jobs for a comparison; samples share OS/SDK caches and do not establish a p95, a clean-machine benchmark, or installed-client acceptance. Instrumented startup I/O counts cover the selected asynchronous Node filesystem methods and `child_process.spawn`, not all native/kernel I/O. -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 prerequisites, launch the target application or use personal databases. +For opt-in fixed-profile TavernDesk acceptance, use `npm run test:tavern-context -- --ui-pid= --ui-hwnd=` and `npm run test:product -- `. Omit the UI arguments from the first command to run only the eight context scenarios. Both scripts bind the target repository at Gateway startup, verify the root reported by hello, and keep cache/trash in a separate temporary directory. 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 prerequisites, launch the target application or use personal databases. Their fresh stdio connections do not verify the active agent-client connection or its Roslyn workflow. 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. Native Release builds run through `scripts/publish-native.mjs host|codeHost|tray` after locked restore (automatically inside `check`). The build records repository source/shared files and build settings before compilation, verifies they stayed unchanged, and seals the complete publish output. Delivery verification rejects changed source or artifacts; regenerating a delivery manifest cannot bless old DLLs. Direct `dotnet publish` is suitable for isolated fixtures; use the canonical script for delivery. This is a repository input fingerprint, not an attestation of arbitrary external MSBuild imports or the SDK installation. 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. diff --git a/README.md b/README.md index 8646c07..c62af46 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,9 @@ Each Roslyn Host writes design-time intermediate files to its own directory whil Each instance accepts up to **32 unfinished tool requests**, including queued requests. Passive hello and `tools/list` share a separate limit of **4 requests**. Raw arguments are limited to **64 KiB of UTF-8 JSON**. Queueing counts toward the request timeout; overload returns `SERVER_BUSY`. A cancelled request still counts toward the limit until its operation finishes cleanup. Shared-cache reads check the selected source content and cached attachments, and rebuild missing or corrupted entries. -**Test results on 2026-09-11:** [PR #37](https://github.com/linnnn89/WinCode/pull/37) and its cache-test correction [PR #38](https://github.com/linnnn89/WinCode/pull/38) are merged. The resulting `main` commit `d51f3e1` passed [Node 22/24 CI](https://github.com/linnnn89/WinCode/actions/runs/34571066627) and all three [CodeQL checks](https://github.com/linnnn89/WinCode/actions/runs/34571066444). The Node 22 report records 451 core tests passed, zero failed and one optional TavernDesk test skipped. Shared-cache tests passed 8/8, SDK concurrency tests 10/10 and design-time isolation tests 21/21; these three reports list no cleanup failures or leftover processes. +**Verified baseline on 2026-09-11:** Navigation and compact UI results ([PR #40](https://github.com/linnnn89/WinCode/pull/40)), followed by the worktree `.git` filter and Tray test scheduling correction ([PR #41](https://github.com/linnnn89/WinCode/pull/41)), are merged. The resulting `main` commit `631f8ba` passed [Node 22/24 CI](https://github.com/linnnn89/WinCode/actions/runs/34586761303) and all three [CodeQL checks](https://github.com/linnnn89/WinCode/actions/runs/34586761201). Node 22 recorded 453 core tests passed, zero failed and one optional TavernDesk test skipped; Roslyn Host 59/59, Gateway 22/22, shared-cache 8/8, SDK concurrency 10/10 and design-time isolation 21/21 passed. Node 22 runs additional acceptance stages, so its total job duration is not a Node 22/24 performance comparison. + +The opt-in TavernDesk scripts now bind the requested repository at startup and keep cache/trash in a separate temporary directory. The eight real-project context scenarios passed locally. The six UI product tasks remain unverified in this run: the dedicated TavernDesk application exited with `UnauthorizedAccessException` before showing its window. These scripts use fresh stdio connections; they do not verify the active Codex connection or its Roslyn workflow. Concurrent UI inspection, the full Roslyn workflow in actual agent clients, and resource use during extended runs still need testing. Pending work is listed in the [remaining test plan](WinCode-下一轮工程化迭代计划书.md); previous results and failures are in the [work log](docs/codex_worklog.md). @@ -320,7 +322,9 @@ npm run delivery:verify 每个实例最多接受 **32 个尚未完成的工具请求**,包含排队中的请求。被动 hello 和 `tools/list` 另行共享 **4 个请求名额**。原始参数上限为 **64 KiB UTF-8 JSON**。排队时间计入请求超时;超过容量时返回 `SERVER_BUSY`。取消的请求需完成清理后才不再占用名额。共享缓存会核对所选源码的内容和缓存附件,缺失或损坏时重建。 -**2026-09-11 测试结果:**[PR #37](https://github.com/linnnn89/WinCode/pull/37) 及共享缓存测试修正 [PR #38](https://github.com/linnnn89/WinCode/pull/38) 均已合并。合并后的 `main` 提交 `d51f3e1` 通过 [Node 22/24 CI](https://github.com/linnnn89/WinCode/actions/runs/34571066627) 和三项 [CodeQL 检查](https://github.com/linnnn89/WinCode/actions/runs/34571066444)。Node 22 报告中,核心测试 451 项通过、0 项失败、1 项可选 TavernDesk 测试跳过;共享缓存测试通过 8/8,SDK 并发测试 10/10,设计时隔离测试 21/21。这三组测试均未报告清理失败或遗留进程。 +**2026-09-11 已验证基线:**代码导航和 UI 精简输出([PR #40](https://github.com/linnnn89/WinCode/pull/40)),以及 worktree 的 `.git` 文件过滤与托盘测试调度修正([PR #41](https://github.com/linnnn89/WinCode/pull/41))均已合并。合并后的 `main` 提交 `631f8ba` 通过 [Node 22/24 CI](https://github.com/linnnn89/WinCode/actions/runs/34586761303) 和三项 [CodeQL 检查](https://github.com/linnnn89/WinCode/actions/runs/34586761201)。Node 22 记录核心测试 453 项通过、0 项失败、1 项可选 TavernDesk 测试跳过;Roslyn Host 59/59、Gateway 22/22、共享缓存 8/8、SDK 并发 10/10、设计时隔离 21/21 均通过。Node 22 还执行额外验收,不能用两个任务的总耗时比较 Node 22/24 的性能。 + +可选 TavernDesk 验收脚本已改为启动时绑定目标仓库,缓存和回收站使用独立临时目录。真实项目的 8 个上下文场景已在本机通过;本轮 6 个 UI 产品任务尚未验证,因为专用测试应用在显示窗口前以 `UnauthorizedAccessException` 退出。这些脚本使用新建的 stdio 连接,不代表当前 Codex 连接或其中的 Roslyn 流程已经验证。 UI 并发检查、实际 Agent 客户端中的完整 Roslyn 操作流程,以及长期运行的资源占用仍需测试。未完成事项见[后续测试计划](WinCode-下一轮工程化迭代计划书.md),历史测试结果和失败记录见[工作日志](docs/codex_worklog.md)。 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 ada75ed..48e6fbb 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" @@ -1,17 +1,18 @@ # WinCode 后续测试计划 -更新:2026-09-11(北京时间)。当前版本为 **0.15.0 / main `d51f3e1`**。版本和 CI 状态见 [README](README.md),实现方式见[架构说明](WinCode-架构与数据流说明.md),历史结果见[工作日志](docs/codex_worklog.md)。 +更新:2026-09-11(北京时间)。当前版本为 **0.15.0**,已验证的 main 基线为 `631f8ba`。版本和 CI 状态见 [README](README.md),实现方式见[架构说明](WinCode-架构与数据流说明.md),历史结果见[工作日志](docs/codex_worklog.md)。 -固定工作区、保持已加载的 Host、请求数量限制、独立的设计时输出目录和共享缓存回归测试已完成。PR #37/#38 已合并,Node 22/24 和 CodeQL 检查通过。这些内容已从待办中移除,下面只保留尚未完成的测试和未定位的问题。 +固定工作区、保持已加载的 Host、请求数量限制、独立的设计时输出目录、共享缓存回归,以及 PR #40/#41 的代码导航、UI 精简输出和测试修正已合并,Node 22/24 和 CodeQL 检查通过。TavernDesk 验收脚本已修正启动工作区,针对性回归先失败后通过,真实项目的 8 个上下文场景通过。下面保留尚未完成的测试和未定位的问题。 ## 待完成的测试与问题排查 | 事项 | 已知情况 | 下一步 | | --- | --- | --- | -| 实际客户端中的 Roslyn 操作 | 已确认本机 Codex 连接运行 0.15.0 / local-text,WinCode Skill 已安装;尚未在当前版本的实际客户端中测完 Roslyn 流程 | 使用已配置 Roslyn 的连接,依次测试符号搜索、引用查询、影响分析和重构建议;修改源码后,确认旧符号位置被拒绝,重新搜索可恢复查询。原计划中的两个实际应用、A/B 并发任务和 Codex 测试仍需完成;agy CLI 测试继续暂缓 | -| 两个 MCP 连接同时检查 UI | 现有测试覆盖一个连接对同一窗口的并发请求,尚未覆盖两个独立 Gateway | 用 WPF 测试程序分别测试两个连接检查不同窗口、同时检查同一窗口。核对 PID/HWND、控件内容、截图是否来自指定窗口,以及辅助进程是否正常退出 | -| UI 和托盘的间歇性失败 | 仍有未定位的隐藏/唤出、截图/提示窗问题,以及托盘 `--endpoint` 启动超时记录;后续测试通过还不足以说明原因已经查清 | 复现时保存错误码、原生日志和各步骤耗时,定位后重测原操作。此前结果和缺失的原始报告已在工作日志中说明 | -| SDK 突发请求警告 | 128 个请求突发发送时曾出现 11 个 drain 监听器的警告,结束后监听器数量回到 0;服务端请求限制无法消除客户端已经产生的发送积压 | 先检查常见的连续任务是否受影响。若再次出现,检查客户端发送背压及监听器的注册与释放,不仅通过提高监听器数量上限来隐藏警告 | +| TavernDesk 的 6 个 UI 产品任务 | 工作区绑定已修正;本次专用测试应用在显示窗口前以 `UnauthorizedAccessException` 退出,尚未执行 UI 任务 | 先查明应用在专用 profile 中启动失败的原因,再用匹配的启动回执、PID/HWND 执行 `test:product`;不使用日常数据库替代 | +| 两个 MCP 连接同时检查 UI | 单连接排队及审计 Mutex 拒绝已有测试;完整双 Gateway 窗口流程尚未覆盖,共用审计目录的重叠操作会返回 `AUDIT_BUSY` | 以现有 WPF 夹具验证不同窗口和同一窗口:核对目标归属、输出及进程清理,明确验证 `AUDIT_BUSY` 和操作结束后的恢复;先不改锁或加入重试队列 | +| 实际客户端中的 Roslyn 操作 | 自动化 Gateway 22 场景及 A/B/A 冷启动已通过;当前客户端完整流程仍未验证 | 核对实际连接的构建身份和 Roslyn 配置,用一个代表性项目连续执行搜索、引用、影响分析和重构建议;在隔离源码副本修改后验证旧位置被拒绝、重新搜索可恢复。只有发现具体缺口才扩大项目数量;agy CLI 继续暂缓 | +| UI 和托盘的间歇性失败 | PR #41 将真实端点启动检查移到顺序阶段,main 两个 Node 版本均通过,3000 ms 生产超时未变;隐藏/唤出、截图/提示窗及原生启动延迟的根因仍未全部定位 | 再现时保存错误码、原生日志和步骤耗时,再针对原因修复;不把调度修正及重跑通过称为已解决所有 UI/托盘问题 | +| SDK 突发请求警告 | main 的 128 请求阶段仍记录 11 个 drain 监听器警告,阶段结束后为 0;服务端限流不能消除客户端发送积压 | 先在上述实际客户端连续任务中观察是否影响使用;若普通负载也出现,再检查发送背压及监听器释放。不提高监听器上限来隐藏警告,也不据此认定内存泄漏 | ## 遇到相关需求时再测试 diff --git "a/WinCode-\346\236\266\346\236\204\344\270\216\346\225\260\346\215\256\346\265\201\350\257\264\346\230\216.md" "b/WinCode-\346\236\266\346\236\204\344\270\216\346\225\260\346\215\256\346\265\201\350\257\264\346\230\216.md" index de0a8a8..d840375 100644 --- "a/WinCode-\346\236\266\346\236\204\344\270\216\346\225\260\346\215\256\346\265\201\350\257\264\346\230\216.md" +++ "b/WinCode-\346\236\266\346\236\204\344\270\216\346\225\260\346\215\256\346\265\201\350\257\264\346\230\216.md" @@ -1,6 +1,6 @@ # WinCode 架构与数据流 -**适用版本:0.15.0,main `d51f3e1`;更新:2026-09-11(北京时间)。该版本已合并,Node 22/24 和 CodeQL 检查通过,尚未发布 GitHub Release。实际客户端 Roslyn 流程和 UI 并发测试仍待完成,当前测试结果见 [README](README.md)。** +**适用版本:0.15.0;更新:2026-09-11(北京时间)。已验证的 main 基线 `631f8ba` 包含 PR #40/#41,Node 22/24 和 CodeQL 检查通过,尚未发布 GitHub Release。实际客户端 Roslyn 流程和双连接 UI 测试仍待完成,当前测试结果见 [README](README.md)。** 本说明描述当前源码中已实现的结构。GitHub 分支保护的历史只读核查日期为 2026-09-08;本轮核对 PR 检查状态,不把它等同重新审计全部保护设置。历史实测结果见[工作记录](docs/codex_worklog.md)。源码版本、磁盘构建和客户端当前连接是三个不同对象,不能互相替代。 @@ -52,7 +52,7 @@ flowchart TB | 原生 Host | 按 PID/HWND 取证,执行有界 UIA 搜索及截图 | [Program.cs](tools/WinCode.UIA.Host/Program.cs)、[BoundedUiSearch](tools/WinCode.UIA.Host/BoundedUiSearch.cs)、[UiAudit](tools/WinCode.UIA.Host/UiAudit.cs) | | 构建交付层 | 锁定构建、回归、stdio 验证、产物身份、Skill 一致性 | [check.mjs](scripts/check.mjs)、[delivery-manifest](scripts/delivery-manifest.mjs)、[sync-skill](scripts/sync-skill.mjs) | -`ExtensionManager` 目前保留兼容接口,没有内置注册项,不承担实际插件生态或工具发现职责。Gateway 当前列出 15 个工具名称,其中包含影响分析别名;工具名称数量不等于独立业务能力数量。 +`ExtensionManager` 目前保留兼容接口,没有内置注册项,不承担实际插件生态或工具发现职责。Gateway 当前列出 17 个工具名称,其中包含影响分析别名;工具名称数量不等于独立业务能力数量。 ## 2. 一次请求怎样通过系统 @@ -179,6 +179,7 @@ flowchart TB - UI→XAML→C# 是候选证据链。动态绑定、模板、资源字典没有被完整求值;保持 `runtimeSourceVerified=false`。源码候选读取失败时保留已取得的 UI 快照。 - Host 使用 UIA/Win32 读取目标窗口;取消与超时清理自有 Helper,不终止目标应用。该设计不提供点击、输入或聊天生成能力。 - 审计保存开始/结束等简要记录;达到阈值时提醒或拒绝新 UI 访问。日志是本地可写文件,不提供防篡改保证。 +- 同一 Gateway 的 UI 请求在适配器内排队;不同 Gateway 共用审计目录时,由原生命名 Mutex 保护整个操作,发生重叠会返回 `AUDIT_BUSY`。双连接测试应检查这一返回及后续恢复,不能假定两个请求都能同时成功。 ## 5. 状态、存储与生命周期 @@ -280,7 +281,7 @@ flowchart LR 本说明的架构图、数据表与关口表共同描述当前实现;新增功能应说明接入哪条数据流、使用哪个现有契约、在哪个关口拒绝或降级,以及如何留下真实验收证据。 -工作区失败恢复、trash 部分完成、有界负载和基础错误迁移已落实;当前待办见[计划](WinCode-下一轮工程化迭代计划书.md)。实际客户端 Roslyn 验收由用户明确暂缓;条件性性能研究不表示已发现泄漏。 +工作区失败恢复、trash 部分完成、有界负载和基础错误迁移已落实;当前待办见[计划](WinCode-下一轮工程化迭代计划书.md)。实际客户端 Roslyn 流程仍需单独验收,新建 stdio 连接的测试不能替代它;条件性性能研究不表示已发现泄漏。 ## 2026-09-09 职责拆分 diff --git a/docs/codex_worklog.md b/docs/codex_worklog.md index 4a79ef2..84aaa89 100644 --- a/docs/codex_worklog.md +++ b/docs/codex_worklog.md @@ -1341,3 +1341,18 @@ - 本地第一次完整 check 失败于原有目录树断言:worktree 的 .git 是文件,现有目录过滤没有排除它。Git 官方说明此布局:https://git-scm.com/docs/git-worktree。新增一个临时文件回归先复现失败,再仅补充 .git 文件的默认排除;includeIgnored=true 仍能列出该文件。相关浏览测试 17/17 通过。 - 最终一次本地完整 check 通过:Node 24.19.0,454/454、无跳过;生产 STDIO 的 Tray 端点 251.4701 ms、17 工具与导航/EOF 检查通过;三组件正式构建和交付清单验证通过。报告 test-tmp/check/2026-09-11T09-42-37-700Z-core/report.json。先前失败报告 test-tmp/check/2026-09-11T09-37-13-728Z-core/report.json 保留。未再次运行无关 desktop 集合。 - 本次修改不增加依赖或架构层次。远端最终 PR-head 和合并后 main 检查仍待运行;当前 Codex 连接已验证 PR #40 的导航与 UI 行为,但最后这条目录过滤需要下一次正常连接加载新构建。 + +## 2026-09-11 — 修正真实项目验收的启动工作区 + +- 复核 main `631f8ba`:PR #41 已合并,[Node 22/24](https://github.com/linnnn89/WinCode/actions/runs/34586761303) 和 [CodeQL](https://github.com/linnnn89/WinCode/actions/runs/34586761201) 均通过。按用户批准的第一步,仅修正两个验收脚本、添加一个回归并更新现有文档;不修改生产源码、依赖或客户端配置。 +- `verify-tavern-context.ts` 和 `verify-product-tasks.ts` 原先把临时目录作为固定工作区,再打开目标仓库,必然返回 `WORKSPACE_MISMATCH`。现在启动时绑定目标仓库,缓存和回收站继续使用独立临时目录,并核对 hello 返回的根目录。新增回归调用真实脚本入口,先复现失败,修复后通过,同时验证源码不变、项目根下未创建测试数据。 +- 真实 New-tavern 的 8 个上下文场景通过,记录 `test-tmp/r3/acceptance-1789122972763.json`。专用 TavernDesk 测试应用 PID 10760 在显示窗口前退出,回执记录 `UnauthorizedAccessException`,因此本轮未执行 6 个 UI 产品任务。失败回执保留于 `test-tmp/r3/tavern-startup-failed-20260911.json`;确认进程已退出,未修改 New-tavern 源码或使用日常数据库,未扩展修复该应用。 +- 本地 Node 24 完整 `npm run check`:类型检查、Gateway 与原生组件构建通过;回归共 455 项,451 通过、4 失败、0 跳过。四项失败均为原有 `mcp-stdio.test.ts` 的 8000 ms 请求超时,新增回归通过。报告 `test-tmp/check/2026-09-11T10-38-39-578Z-core/report.json` 保留失败状态,后续 stdio 阶段未执行。为检查并行负载是否相关,单独运行该文件,12/12 通过,记录 `test-tmp/r3/mcp-stdio-standalone-20260911.log`;这不证明根因已确定,也不替代完整集合通过。未修改超时、生产代码或重复全量测试。 +- 交付清单核验 matched=true,contentId 保持 `290a6d2ca90407260d246b749699d515897461c770bef1ad8057483937bc656c`,本次不改变客户端运行时交付内容。README 更新已验证基线;架构说明修正为 17 个工具并说明 `AUDIT_BUSY`;待办保留双连接 UI、实际客户端 Roslyn 和未定位问题,不把 SDK 突发警告当作已证实的泄漏。 +- 本次 PR 的远端检查尚待运行;当前版本不能报告为全部验收完成或可直接合并。 + +## 2026-09-11 — 固定 stdio 回归输入并简化客户端 + +- 排查上一次完整检查中的四个 8000 ms 超时。加入有界 stderr 后运行六个相关文件,55/55 通过,子进程 stderr 共 97 字节;未复现超时,不把管道阻塞或 Node 版本差异称为已确定根因。诊断记录:`test-tmp/r3/stdio-diagnostic-parallel.log`。Node 官方文档说明测试文件默认并行执行:https://nodejs.org/docs/latest-v24.x/api/test.html。 +- 确认旧协议测试直接扫描整个开发仓库,所创建的独立 cache 配置没有传给实际 CLI。将输入改为三个固定文件组成的临时项目,源码及 trash 均在该项目内;删除无效配置。复用已安装 MCP SDK 的握手、传输与关闭,保留 8000 ms 超时、原有 12 个场景及协议错误断言,错误附最多 8192 字符 stderr。没有新增测试、依赖或生产代码。 +- 定向 12/12 通过;最终完整 `npm run check` 455/455、0 失败、0 跳过,生产 stdio、原生构建及交付清单核验通过。报告:`test-tmp/check/2026-09-11T10-46-38-024Z-core/report.json`。先前失败报告保留;当前结果证明固定输入的测试通过,不宣称已解释历史机器负载。修改后的 PR-head 仍需 Node 22/24 与 CodeQL 检查。 diff --git a/scripts/verify-product-tasks.ts b/scripts/verify-product-tasks.ts index 11f6d45..14bf486 100644 --- a/scripts/verify-product-tasks.ts +++ b/scripts/verify-product-tasks.ts @@ -87,7 +87,9 @@ try { 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)}); +const config = getDefaultConfig(${JSON.stringify(workspace)}); +config.cacheDir = ${JSON.stringify(path.join(temporary, 'cache'))}; +config.trashDir = ${JSON.stringify(path.join(temporary, 'trash'))}; config.adapters.repomix.useCli = false; const server = new WinCodeMcpServer(new ToolRouter(config)); @@ -99,6 +101,7 @@ await server.start(); await client.connect(transport); const hello = await call('wincode_hello_world', {}, report.setup); assert.equal(hello.runtime.build.status, 'verified'); + assert.equal(hello.workspace, workspace); report.runtime = hello.runtime; report.schemaHash = hello.toolContract.schemaHash; await call('workspace_open', { path: workspace }, report.setup); diff --git a/scripts/verify-tavern-context.ts b/scripts/verify-tavern-context.ts index 12b6692..92b9527 100644 --- a/scripts/verify-tavern-context.ts +++ b/scripts/verify-tavern-context.ts @@ -42,7 +42,9 @@ try { 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)}); +const config = getDefaultConfig(${JSON.stringify(workspace)}); +config.cacheDir = ${JSON.stringify(path.join(temporary, 'cache'))}; +config.trashDir = ${JSON.stringify(path.join(temporary, 'trash'))}; config.adapters.flaui.enabled = ${Boolean(uiPid)}; config.adapters.repomix.useCli = false; @@ -60,6 +62,7 @@ await server.start(); }; report.hello = (await call('wincode_hello_world', { toolName: 'wincode_prepare_context' })).data; assert.equal(report.hello.runtime.build.status, 'verified'); + assert.equal(report.hello.workspace, workspace); report.open = await call('workspace_open', { path: workspace }); assert.ok(report.open.characters <= 8000); const scenarios = [ diff --git a/tests/fixed-workspace.test.ts b/tests/fixed-workspace.test.ts index e57937a..9d5828a 100644 --- a/tests/fixed-workspace.test.ts +++ b/tests/fixed-workspace.test.ts @@ -14,6 +14,30 @@ import { WinCodeMcpServer } from '../src/Gateway/McpServer.js'; const body = (result: any) => JSON.parse(result.content[0].text); const mismatch = (error: any) => error.errorCode === 'WORKSPACE_MISMATCH'; +it('TavernDesk acceptance binds the requested project without writing test data into it', { timeout: 30000 }, async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), 'wincode-acceptance-project-')); + const sourceDirectory = path.join(root, 'src/TavernDesk.App/ViewModels'); + const file = path.join(sourceDirectory, 'MainWindowViewModel.cs'); + const source = ['class MainWindowViewModel {', ' private async Task ShowCharactersAsync()', + ' {', ' await Task.CompletedTask;', ' }', '', ' private void Other() {}', '}', + ...Array.from({ length: 240 }, () => '// fixture padding')].join('\n'); + try { + await fs.mkdir(sourceDirectory, { recursive: true }); + await fs.writeFile(file, source); + const result = spawnSync(process.execPath, ['node_modules/tsx/dist/cli.mjs', 'scripts/verify-tavern-context.ts', root], + { cwd: process.cwd(), encoding: 'utf8', windowsHide: true, timeout: 25000, maxBuffer: 1024 * 1024 }); + assert.equal(result.status, 0, result.stderr || result.error?.message); + const report = JSON.parse(result.stdout); + assert.equal(report.status, 'passed'); + assert.equal(report.measurements.length, 8); + assert.equal(await fs.readFile(file, 'utf8'), source); + assert.deepEqual(await fs.readdir(root), ['src'], 'cache and trash must remain outside the source workspace'); + } finally { + assert.equal(path.dirname(root), path.resolve(os.tmpdir())); + await fs.rm(root, { recursive: true, force: true }); + } +}); + async function fixture(run: (a: string, b: string, connect: (root: string) => Promise) => Promise) { const parent = await fs.mkdtemp(path.join(os.tmpdir(), 'wincode-fixed-')); const a = path.join(parent, '项目 A'), b = path.join(parent, '项目 B'); diff --git a/tests/mcp-stdio.test.ts b/tests/mcp-stdio.test.ts index 82a4f76..a70a467 100644 --- a/tests/mcp-stdio.test.ts +++ b/tests/mcp-stdio.test.ts @@ -1,105 +1,57 @@ import { describe, it, before, after } from 'node:test'; import assert from 'node:assert'; import fs from 'node:fs/promises'; -import { existsSync } from 'node:fs'; +import os from 'node:os'; import path from 'node:path'; -import { spawn } from 'node:child_process'; +import { Client } from '@modelcontextprotocol/client'; +import { StdioClientTransport } from '@modelcontextprotocol/client/stdio'; -import { getDefaultConfig } from '../src/Core/Config.js'; - -import { killProcessTree } from '../src/Core/ResourceManager.js'; - -// 每个功能套件拥有独立缓存;并行文件不能删除彼此正在使用的缓存。 +// Protocol checks use fixed input; the developer checkout is not a performance fixture. describe('mcp-stdio', () => { - const root = process.cwd(); - const testCacheDir = path.join(root, '.cache', `test_mcp-stdio_${process.pid}`); - const config = getDefaultConfig(root); - config.cacheDir = testCacheDir; - const FIXTURE_DOTNET = path.resolve(root, 'tests/fixtures/dotnet-mini'); + const repository = process.cwd(); + let root: string; + const FIXTURE_DOTNET = path.resolve(repository, 'tests/fixtures/dotnet-mini'); before(async () => { - await fs.mkdir(testCacheDir, { recursive: true }); + root = await fs.mkdtemp(path.join(os.tmpdir(), 'wincode-stdio-')); + await fs.mkdir(path.join(root, 'src/Core'), { recursive: true }); + await fs.mkdir(path.join(root, 'src/Gateway'), { recursive: true }); + await fs.writeFile(path.join(root, 'package.json'), JSON.stringify({ name: 'stdio-fixture', version: '1.0.0' })); + await fs.writeFile(path.join(root, 'src/Core/ToolRouter.ts'), + 'export class ToolRouter { run(): string { return "fixture"; } }\n'); + await fs.writeFile(path.join(root, 'src/Gateway/McpServer.ts'), + 'import { ToolRouter } from "../Core/ToolRouter.js";\nexport class McpServer { router = new ToolRouter(); }\n'); }); after(async () => { - await fs.rm(testCacheDir, { recursive: true, force: true }).catch(() => { }); + assert.equal(path.dirname(root), path.resolve(os.tmpdir())); + assert.ok(path.basename(root).startsWith('wincode-stdio-')); + await fs.rm(root, { recursive: true, force: true }); }); - describe('6. End-to-End MCP Stdio Protocol & All 9 Tools Execution', () => { - let proc: any; - let pendingRequests = new Map void>(); - let buffer = ''; + describe('production stdio tool workflow', () => { + const client = new Client({ name: 'stdio-regression', version: '1.0.0' }); + let transport: StdioClientTransport; + let stderr = ''; before(async () => { - const serverPath = path.resolve('dist/index.js'); - proc = spawn('node', [serverPath, '--workspace', root], { - cwd: root, - stdio: ['pipe', 'pipe', 'pipe'], + transport = new StdioClientTransport({ command: process.execPath, + args: [path.join(repository, 'dist/index.js'), '--workspace', root], cwd: root, stderr: 'pipe' }); + transport.stderr?.on('data', (chunk: Buffer) => { + stderr = (stderr + chunk.toString()).slice(-8192); }); - - proc.stdout.on('data', (chunk: Buffer) => { - buffer += chunk.toString(); - const lines = buffer.split('\n'); - buffer = lines.pop() || ''; - - for (const line of lines) { - const trimmed = line.trim(); - if (!trimmed) continue; - try { - const msg = JSON.parse(trimmed); - if (msg.id !== undefined && pendingRequests.has(msg.id)) { - const resolve = pendingRequests.get(msg.id)!; - pendingRequests.delete(msg.id); - resolve(msg); - } - } catch { } - } - }); - - // Handshake - const initPayload = JSON.stringify({ - jsonrpc: '2.0', - id: 100, - method: 'initialize', - params: { - protocolVersion: '2024-11-05', - capabilities: {}, - clientInfo: { name: 'TDD-Runner', version: '1.0.0' }, - }, - }) + '\n'; - proc.stdin.write(initPayload); - await new Promise((r) => setTimeout(r, 600)); - - const initializedPayload = JSON.stringify({ - jsonrpc: '2.0', - method: 'notifications/initialized', - }) + '\n'; - proc.stdin.write(initializedPayload); - await new Promise((r) => setTimeout(r, 200)); + await client.connect(transport, { timeout: 8000 }); }); - - after(async () => { - if (proc) { - proc.stdin.end(); - await new Promise((r) => setTimeout(r, 200)); - await killProcessTree(proc).catch(() => { }); + after(async () => { await client.close(); await transport?.close(); }); + + const callMcp = async (method: string, params?: any): Promise => { + try { + const result = method === 'tools/list' + ? await client.listTools({}, { timeout: 8000 }) + : await client.callTool(params, { timeout: 8000 }); + return { result }; + } catch (error: any) { + if (error.code === -32602) return { error: { code: error.code, message: error.message } }; + throw new Error(`MCP call ${params?.name ?? method} failed; stderr: ${stderr}`, { cause: error }); } - }); - - const callMcp = (method: string, params?: any): Promise => { - const id = Math.floor(Math.random() * 10000000); - return new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - pendingRequests.delete(id); - reject(new Error(`MCP call ${method} timed out`)); - }, 8000); - - pendingRequests.set(id, (res) => { - clearTimeout(timeout); - resolve(res); - }); - - const payload = JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n'; - proc.stdin.write(payload); - }); }; it('MCP tools/list should list all registered high-level tools', async () => {