From 8e8627dee3bad93b21ccad73e6eb30467a2746be Mon Sep 17 00:00:00 2001 From: linnnn89 <216342082+linnnn89@users.noreply.github.com> Date: Fri, 11 Sep 2026 19:01:43 +0800 Subject: [PATCH] test: verify cross-Gateway UI contention and document live acceptance --- README.md | 8 +- ...43\350\256\241\345\210\222\344\271\246.md" | 7 +- ...56\346\265\201\350\257\264\346\230\216.md" | 4 +- docs/codex_worklog.md | 10 +++ .../fixtures/wpf-ui-review/MainWindow.xaml.cs | 12 ++- tests/ui-window-list.test.ts | 89 +++++++++++++++++++ 6 files changed, 117 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index c62af46..11db182 100644 --- a/README.md +++ b/README.md @@ -63,9 +63,9 @@ Each instance accepts up to **32 unfinished tool requests**, including queued re **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. +The opt-in TavernDesk scripts bind the requested repository at startup and keep cache/trash in a separate temporary directory. The eight real-project context scenarios and six UI product tasks passed locally. The dedicated application initially failed with `UnauthorizedAccessException`; a build from current, unchanged source started successfully. The original startup failure remains unexplained. -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). +Local acceptance also covered two independent Gateway processes inspecting the same and different windows: overlapping access returned `AUDIT_BUSY`, subsequent access recovered, and window identity, screenshots and helper cleanup were checked. The actual Codex connection completed Roslyn search, references, impact and refactoring guidance on an isolated TavernDesk.Core copy; after a source edit, old locations were rejected and a new search restored reference queries. This covers one project/configuration and excludes eight analyzer/generator references; it does not establish complete application coverage. Extended resource use and unresolved UI issues remain in the [test plan](WinCode-下一轮工程化迭代计划书.md); results and failures are in the [work log](docs/codex_worklog.md). **Specify a project at startup (recommended):** Add `--workspace` followed by the existing project directory's absolute path: @@ -324,9 +324,9 @@ npm run delivery:verify **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 流程已经验证。 +可选 TavernDesk 验收脚本在启动时绑定目标仓库,缓存和回收站使用独立临时目录。真实项目的 8 个上下文场景和 6 个 UI 产品任务均已在本机通过。专用应用最初报 `UnauthorizedAccessException`;用当前未修改的源码构建后启动成功,但原始启动失败的原因尚未确定。 -UI 并发检查、实际 Agent 客户端中的完整 Roslyn 操作流程,以及长期运行的资源占用仍需测试。未完成事项见[后续测试计划](WinCode-下一轮工程化迭代计划书.md),历史测试结果和失败记录见[工作日志](docs/codex_worklog.md)。 +本机还验证了两个独立 Gateway 检查同一窗口及不同窗口:重叠访问返回 `AUDIT_BUSY`,随后恢复成功,窗口身份、截图及辅助进程清理均通过检查。实际 Codex 连接使用 TavernDesk.Core 的隔离副本完成 Roslyn 搜索、引用、影响分析和重构建议;修改源码后,旧位置被拒绝,重新搜索后引用查询恢复。此次只覆盖一个项目和配置,排除了 8 个分析器/生成器引用,不代表完整应用覆盖。长期资源占用和未定位的 UI 问题见[后续测试计划](WinCode-下一轮工程化迭代计划书.md),测试结果和失败记录见[工作日志](docs/codex_worklog.md)。 **启动时指定项目(推荐):**添加 `--workspace` 和已存在的项目目录绝对路径: 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 48e6fbb..67f341e 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" @@ -2,17 +2,14 @@ 更新:2026-09-11(北京时间)。当前版本为 **0.15.0**,已验证的 main 基线为 `631f8ba`。版本和 CI 状态见 [README](README.md),实现方式见[架构说明](WinCode-架构与数据流说明.md),历史结果见[工作日志](docs/codex_worklog.md)。 -固定工作区、保持已加载的 Host、请求数量限制、独立的设计时输出目录、共享缓存回归,以及 PR #40/#41 的代码导航、UI 精简输出和测试修正已合并,Node 22/24 和 CodeQL 检查通过。TavernDesk 验收脚本已修正启动工作区,针对性回归先失败后通过,真实项目的 8 个上下文场景通过。下面保留尚未完成的测试和未定位的问题。 +固定工作区、保持已加载的 Host、请求数量限制、独立的设计时输出目录、共享缓存回归,以及 PR #40/#41 的代码导航、UI 精简输出和测试修正已合并,Node 22/24 和 CodeQL 检查通过。后续本地验收已完成 TavernDesk 的 8 个上下文场景、6 个 UI 产品任务、两个独立 Gateway 的审计争用及恢复,以及实际 Codex 连接中的单项目 Roslyn 流程。对应 PR 的交付状态见工作日志;这些测试完成项从待办移除,不扩大为所有项目或所有 UI 环境均已验证。 ## 待完成的测试与问题排查 | 事项 | 已知情况 | 下一步 | | --- | --- | --- | -| 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;服务端限流不能消除客户端发送积压 | 先在上述实际客户端连续任务中观察是否影响使用;若普通负载也出现,再检查发送背压及监听器释放。不提高监听器上限来隐藏警告,也不据此认定内存泄漏 | +| 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 d840375..f7db8d7 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;更新:2026-09-11(北京时间)。已验证的 main 基线 `631f8ba` 包含 PR #40/#41,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。本机已完成双连接 UI 及实际 Codex 连接中的单项目 Roslyn 验收,范围和结果见 [README](README.md)。** 本说明描述当前源码中已实现的结构。GitHub 分支保护的历史只读核查日期为 2026-09-08;本轮核对 PR 检查状态,不把它等同重新审计全部保护设置。历史实测结果见[工作记录](docs/codex_worklog.md)。源码版本、磁盘构建和客户端当前连接是三个不同对象,不能互相替代。 @@ -281,7 +281,7 @@ flowchart LR 本说明的架构图、数据表与关口表共同描述当前实现;新增功能应说明接入哪条数据流、使用哪个现有契约、在哪个关口拒绝或降级,以及如何留下真实验收证据。 -工作区失败恢复、trash 部分完成、有界负载和基础错误迁移已落实;当前待办见[计划](WinCode-下一轮工程化迭代计划书.md)。实际客户端 Roslyn 流程仍需单独验收,新建 stdio 连接的测试不能替代它;条件性性能研究不表示已发现泄漏。 +工作区失败恢复、trash 部分完成、有界负载和基础错误迁移已落实;当前待办见[计划](WinCode-下一轮工程化迭代计划书.md)。实际 Codex 连接已完成单项目 Roslyn 的搜索、引用、影响分析、重构建议及源码变化后的重新定位;新建 stdio 连接的自动化测试与此次客户端验收分别记录。条件性性能研究不表示已发现泄漏。 ## 2026-09-09 职责拆分 diff --git a/docs/codex_worklog.md b/docs/codex_worklog.md index 84aaa89..3c1f692 100644 --- a/docs/codex_worklog.md +++ b/docs/codex_worklog.md @@ -1356,3 +1356,13 @@ - 排查上一次完整检查中的四个 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 检查。 + +## 2026-09-11 — 完成产品任务、双 Gateway UI 与实际客户端 Roslyn 验收 + +- TavernDesk 专用 profile 的目录 ACL 未见明显异常。临时诊断构建将测试回执中的异常类型改为完整调用栈,未复现启动失败;随后按原始字节恢复 App.xaml.cs,用未修改源码重建到 WinCode 的 test-tmp,启动成功。未重置权限、使用管理员提权或接触日常数据库,原始 UnauthorizedAccessException 原因仍未确定,不归因于用户玩游戏或移动窗口。 +- 新构建的专用测试窗口 PID 3132 / HWND 0x1B0C2A 完成六个产品任务,全部通过;回执 `test-tmp/product-tasks/1789123829157-28704/report.json`,启动记录 `test-tmp/r3/tavern-current-startup-passed.json`。验收后关闭该进程,New-tavern 源码无保留改动。 +- 新增两个真实 stdio Gateway 场景,覆盖同一窗口和不同窗口。复用 WPF 夹具中的 UIA provider 握手,增加可释放的测试等待,确保首个 Helper 已进入 UIA 后再验证另一个 Gateway 的 AUDIT_BUSY;释放后检查 PID/HWND、不同标题、PNG 尺寸及 Helper 退出。仅修改测试和夹具,没有修改生产 Mutex、队列或重试行为。测试窗口使用已有后台模式,不主动抢焦点。定向 2/2 通过;随后修正 SDK stderr 静态类型不支持 resume 的类型错误,改为监听 data,类型检查通过。 +- 桌面集合最终 37/37,通过 owner-death(`test-tmp/owner-death/run-Ae2zd0/report.json`)和托盘协议(`test-tmp/tray/run-HmVbOK/report.json`)。用户重启整个 Codex 时,check:desktop 最后的托盘工作流被中断,未生成完整总报告;已完成的日志和 JUnit 在 `test-tmp/check/2026-09-11T10-53-12-759Z-desktop`。仅补跑未完成的托盘工作流,通过 `test-tmp/tray-workflow/run-8FHrV2/report.json`,survivors=[];不将这些分段结果描述为一次完整 desktop 检查通过。 +- 实际 Codex 连接实例 7509c731-baca-4031-a1fe-33a8c722b400,buildId=cbe44dd4e37479ce563c91e5ab9de3439813020de2d4664e3f4e9467e7302741,明确启用 Roslyn,目标为 TavernDesk.Core 的 53 文件隔离副本。完成声明搜索、8 处引用、影响分析和重构建议;副本源码前插入一行后,旧 location 返回 SNAPSHOT_STALE,重新搜索使声明从第 10 行/position 121 移至第 11 行/158,新 snapshot 恢复 8 处引用。报告 `test-tmp/live-roslyn-20260911/report.json`;保留 queryComplete=false、排除 8 个分析器/生成器以及单项目范围,不将 UNKNOWN 风险改称低风险。 +- 原副本已恢复,原项目与副本的 53 个源文件哈希均匹配。项目级临时 `.codex/config.toml` 已移除,CLI 核对默认 WinCode 启动参数已恢复,全局配置未改。用户明确要求后台重启后,核对临时 Gateway PID 30892 与其 Roslyn Host PID 20164 的归属并结束 Gateway,两者均退出;随后实际工具调用返回 Transport closed,说明 Codex 没有自动重建该连接。未启动无客户端连接的替代进程,也未结束其他 WinCode 实例。实际客户端验收已完成;当前连接恢复仍须客户端刷新,不能写成后台重连成功。 +- 此次新增两个测试,更新现有 README、架构说明和待办,删除已完成的验收事项。生产源码、依赖和受管 Skill 均未改变。 diff --git a/tests/fixtures/wpf-ui-review/MainWindow.xaml.cs b/tests/fixtures/wpf-ui-review/MainWindow.xaml.cs index 1d3b415..9d4def5 100644 --- a/tests/fixtures/wpf-ui-review/MainWindow.xaml.cs +++ b/tests/fixtures/wpf-ui-review/MainWindow.xaml.cs @@ -14,7 +14,8 @@ public partial class MainWindow : Window { protected override System.Windows.Automation.Peers.AutomationPeer OnCreateAutomationPeer() { - var marker = Environment.GetEnvironmentVariable("WINCODE_TEST_OWNER_UI_MARKER"); + var marker = Environment.GetEnvironmentVariable("WINCODE_TEST_OWNER_UI_MARKER") + ?? Environment.GetEnvironmentVariable("WINCODE_TEST_UI_HOLD_MARKER"); return marker == null ? base.OnCreateAutomationPeer() : new OwnerDeathPeer(this, marker); } @@ -26,7 +27,13 @@ protected override string GetNameCore() if (System.IO.File.Exists(marker + ".armed")) { System.IO.File.WriteAllText(marker, Environment.ProcessId.ToString()); - System.Threading.Thread.Sleep(60000); + if (Environment.GetEnvironmentVariable("WINCODE_TEST_UI_HOLD_MARKER") == marker) + { + var deadline = Stopwatch.StartNew(); + while (!System.IO.File.Exists(marker + ".release") && deadline.ElapsedMilliseconds < 15000) + System.Threading.Thread.Sleep(10); + } + else System.Threading.Thread.Sleep(60000); } return base.GetNameCore(); } @@ -72,6 +79,7 @@ public MainWindow() }; } if (Environment.GetCommandLineArgs().Contains("--window-list-fixture")) Title = "WinCode 窗口发现夹具"; + if (Environment.GetEnvironmentVariable("WINCODE_TEST_UI_LABEL") is string label) Title = label; if (Environment.GetCommandLineArgs().Contains("--budget-fixture")) { var panel = new StackPanel(); diff --git a/tests/ui-window-list.test.ts b/tests/ui-window-list.test.ts index 126af11..aa3719f 100644 --- a/tests/ui-window-list.test.ts +++ b/tests/ui-window-list.test.ts @@ -2,6 +2,8 @@ import { it } from 'node:test'; import assert from 'node:assert/strict'; import { spawn, ChildProcess } from 'node:child_process'; import path from 'node:path'; +import fs from 'node:fs/promises'; +import os from 'node:os'; import { Client } from '@modelcontextprotocol/client'; import { StdioClientTransport } from '@modelcontextprotocol/client/stdio'; import { FlaUiAdapter } from '../src/Adapters/FlaUiAdapter.js'; @@ -9,6 +11,93 @@ import { getDefaultConfig } from '../src/Core/Config.js'; import { killProcessTree } from '../src/Core/ResourceManager.js'; import { UiInspectResult } from '../src/Core/UiContracts.js'; +for (const sameWindow of [false, true]) { + it(`two stdio Gateways preserve ${sameWindow ? 'one window' : 'distinct windows'} across audit contention`, { timeout: 45000 }, async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), 'wincode-two-ui-')); + const marker = path.join(root, 'provider-entered'); + const fixtures: ChildProcess[] = []; + const gateways: Array<{ client: Client; transport: StdioClientTransport }> = []; + const targets: Array<{ pid: number; hwnd: string }> = []; + let first: Promise | undefined; + const call = async (index: number, name: string, args: Record = {}) => { + const result = await gateways[index].client.callTool({ name, arguments: args }, { timeout: 20000 }); + return { response: result, data: JSON.parse((result.content as Array<{ text: string }>)[0].text) }; + }; + try { + for (let i = 0; i < 2; i++) { + const child = spawn(path.resolve('tests/fixtures/wpf-ui-review/bin/Release/net10.0-windows/win-x64/publish/wpf-ui-review.exe'), + ['--background-fixture', '--auto-close=40000'], { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, + env: { ...process.env, WINCODE_TEST_UI_LABEL: `Gateway target ${i}`, ...(i === 0 ? { WINCODE_TEST_UI_HOLD_MARKER: marker } : {}) } }); + fixtures.push(child); + child.stderr!.resume(); + targets.push(await new Promise<{ pid: number; hwnd: string }>((resolve, reject) => { + const timer = setTimeout(() => reject(new Error('WPF fixture READY timeout')), 8000); + let output = ''; + child.stdout!.on('data', chunk => { + output = (output + chunk).slice(-4096); + const ready = /READY (\d+) (0x[0-9A-F]+)/i.exec(output); + if (ready) { clearTimeout(timer); resolve({ pid: Number(ready[1]), hwnd: ready[2] }); } + }); + child.once('error', error => { clearTimeout(timer); reject(error); }); + child.once('exit', code => { clearTimeout(timer); reject(new Error(`WPF fixture exited: ${code}`)); }); + })); + const workspace = path.join(root, `workspace-${i}`); + await fs.mkdir(workspace); + const transport = new StdioClientTransport({ command: process.execPath, + args: [path.resolve('dist/index.js'), '--workspace', workspace], cwd: workspace, stderr: 'pipe' }); + transport.stderr?.on('data', () => {}); + const client = new Client({ name: `two-gateways-${i}`, version: '1' }); + gateways.push({ client, transport }); + await client.connect(transport); + } + assert.notEqual(gateways[0].transport.pid, gateways[1].transport.pid); + const request = (target: number) => ({ ...targets[target], backgroundOnly: true, capture: 'original', maxDepth: 2 }); + await fs.writeFile(marker + '.armed', ''); + first = call(0, 'wincode_ui_inspect', request(0)); + // The provider marker is written only after the first helper enters real UIA access. + const deadline = Date.now() + 8000; + while (!(await fs.stat(marker).catch(() => null)) && Date.now() < deadline) + await new Promise(resolve => setTimeout(resolve, 20)); + assert.equal(await fs.readFile(marker, 'utf8'), String(targets[0].pid)); + const active = (await call(0, 'wincode_hello_world')).data.health.flaui.runtime.activePid; + assert.ok(active); + const target = sameWindow ? 0 : 1; + const blocked = await call(1, 'wincode_ui_inspect', request(target)); + assert.equal(blocked.data.errorCode, 'AUDIT_BUSY'); + assert.equal(blocked.data.success, false); + assert.equal(blocked.data.tree, undefined); + await fs.writeFile(marker + '.release', ''); + const completed = await first; + const recovered = await call(1, 'wincode_ui_inspect', request(target)); + for (const [result, expected] of [[completed, 0], [recovered, target]] as const) { + assert.equal(result.data.success, true, JSON.stringify(result.data)); + assert.equal(result.data.pid, targets[expected].pid); + assert.equal(result.data.hwnd.toLowerCase(), targets[expected].hwnd.toLowerCase()); + assert.equal(result.data.tree.name, `Gateway target ${expected}`); + const image = (result.response.content as Array<{ type: string; data?: string }>).find(block => block.type === 'image'); + assert.ok(image?.data, 'A successful original capture must contain a screenshot'); + const png = Buffer.from(image.data, 'base64'); + assert.equal(png.subarray(1, 4).toString(), 'PNG'); + assert.equal(png.readUInt32BE(16), result.data.imageWidth); + assert.equal(png.readUInt32BE(20), result.data.imageHeight); + } + assert.throws(() => process.kill(active, 0), 'Completed helper must exit'); + for (let i = 0; i < 2; i++) { + const health = (await call(i, 'wincode_hello_world')).data.health; + assert.equal(health.flaui.runtime.activePid, null); + assert.equal(health.managedChildProcesses, 0); + } + } finally { + await fs.writeFile(marker + '.release', ''); + await first?.catch(() => {}); + for (const gateway of gateways) { await gateway.client.close(); await gateway.transport.close(); } + for (const child of fixtures) { await killProcessTree(child); assert.throws(() => process.kill(child.pid!, 0)); } + assert.equal(path.dirname(root), path.resolve(os.tmpdir())); + await fs.rm(root, { recursive: true, force: true }); + } + }); +} + it('window discovery rejects invalid filters, disabled configuration and queued cancellation', async () => { const config = getDefaultConfig(process.cwd()); config.adapters.flaui!.enabled = false;