test(ci): retain useful regression controls after master rebase

This commit is contained in:
Tianyi Cui
2026-09-09 13:18:09 +08:00
parent da1df501b8
commit 1b56cf6013
30 changed files with 108 additions and 172 deletions
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/testing/2026-09-07-subagent-teardown-test-budgets.md
2026-09-07-subagent-teardown-test-budgets.md: 0d30e329e3a9b206e3cf3fd4e155318aa614b21c
2026-09-07-subagent-teardown-test-budgets.zh.md: 2a39bad12054ed86c8cdbb1dc16df4aeee46b401
2026-09-07-subagent-teardown-test-budgets.md: 4fe83c421383aa768ffa0d33520407ed8d099d14
2026-09-07-subagent-teardown-test-budgets.zh.md: 1487351d498c9d0ef9eb6c2e83e47425ac82b9c9
@@ -16,8 +16,6 @@ The [Codex test](../../../../packages/subagent/subagent-codex/tests/real-product
The [native Windows CI decision](../process/2026-08-08-native-windows-pull-request-ci.md) continues to own lane scheduling and budgets. This change only removes conflicting local deadlines and strengthens resource-lifetime assertions; it does not establish a Windows process-kill or filesystem defect.
The TERM-trapping ACP case also waits under the lane budget and checks the actual child outcome: `SIGKILL` on POSIX, or a nonzero exit from direct Windows force termination. Readiness follows trap installation. Failure cleanup joins the captured child before removing its private marker directory; the fixtures EOF and termination grace periods remain unchanged.
## Alternatives considered
- Increase production grace periods or filesystem retries: the failures do not demonstrate incorrect product timing or exhausted removal retries.
@@ -16,8 +16,6 @@ Status: implemented
[原生 Windows CI 决策](../process/2026-08-08-native-windows-pull-request-ci.zh.md) 继续负责通道调度和预算。本次改动仅移除冲突的局部期限并加强资源生命周期断言;它并不证明 Windows 进程终止或文件系统存在缺陷。
捕获 TERM 的 ACP 用例同样在 lane 预算内等待,并检查真实子进程结果:POSIX 上为 `SIGKILL`,Windows 直接强制终止时为非零退出码。就绪标记在安装信号处理器后写入。失败清理先等待捕获的子进程结束,再删除其私有标记目录;fixture 的 EOF 与终止宽限期保持不变。
## 曾考虑的替代方案
- 增加生产环境宽限期或文件系统重试次数:这些失败不能证明产品时序错误或删除重试耗尽。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/testing/2026-09-08-ci-completion-observations.md
2026-09-08-ci-completion-observations.md: 76f73b4c31a2e61738b91daeb0e65a4b25422047
2026-09-08-ci-completion-observations.zh.md: aa260528e0989899785338e6b7fedcd6581edb52
2026-09-08-ci-completion-observations.md: 6ecf7eb477ac5446df61bb68f6d49870464d67c4
2026-09-08-ci-completion-observations.zh.md: 9cd1f2b741aa828219559e499cc46eb93b1349df
@@ -1,4 +1,4 @@
# Agent Note: CI tests observe completion instead of host speed
# Agent Note: CI fixture completion and isolation
Status: implemented
@@ -6,47 +6,38 @@ English | [中文](2026-09-08-ci-completion-observations.zh.md)
## Problem
The [reference CI run](https://github.com/deepseek-harness/deepseek-harness/actions/runs/34206953049) rejects two asynchronous operations before their enclosing test budgets expire: a webhook-created Session is absent after the tests one-second poll, and a background PowerShell command has emitted no output before its five-second read deadline. Neither API promises those latency bounds. A successful HTTP 202 response acknowledges webhook dispatch, not Session creation or model admission.
The [reference CI run](https://github.com/deepseek-harness/deepseek-harness/actions/runs/34206953049) reports a webhook-created Session absent after a one-second poll and empty PowerShell output before a five-second read deadline. HTTP acceptance, projected UI state, process startup, and durable completion are separate observations. Tests need an explicit completion condition and controls that prevent an intermediate state from satisfying it.
## Decision
The [GitHub review browser test](../../../../apps/web/tests/github-ready-review.e2e.ts) awaits the deterministic adapters first request before asserting exact Agent and request counts. A deferred Workspace-creation barrier proves that HTTP acceptance can precede both observations; the barrier delegates to the real method and is released and restored in `finally`, with independent `onTestFinished` cleanup if the request itself stalls beyond the test timeout. The original Workspace membership, prompt content, reply, and collapsed/expanded browser expectations remain authoritative.
The [GitHub review browser test](../../../../apps/web/tests/github-ready-review.e2e.ts) holds real Workspace creation after HTTP 202, verifies that neither the Agent nor the model request exists, then releases creation and awaits the matching Session's `turn/end`. Cleanup releases the barrier, restores the method, and removes the event listener even when the test times out. Workspace membership, request counts, prompt content, and browser expectations retain their original assertions.
The [PowerShell executor tests](../../../../packages/shell/pwsh-local/tests/executor.spec.ts) await `done` before reading complete stdin/environment output. Startup and consuming-read checks hold the command at a private file barrier, so the running state and unread later output do not depend on a sleep or elapsed-time threshold. Partial-output polling inherits the lane budget. Every created Context is registered for teardown before use; subprocess disposal precedes private-directory removal. A six-second delayed command reproduces the five-second failure and passes after the completion wait on native Windows.
The [PowerShell executor tests](../../../../packages/shell/pwsh-local/tests/executor.spec.ts) hold startup and consuming reads at private file barriers. The test controls when later output becomes available; final stdin/environment output is read after `done`. Polling uses the active test budget, and every constructed Context is registered before plugin initialization. Teardown captures Contexts and directories before awaiting disposal and removes directories only after that disposal completes.
The enclosing test timeout remains the watchdog. Completion assertions do not acquire a second, shorter performance requirement merely because an operation crosses a process, filesystem, or event-loop boundary. This extends the [subagent teardown budget decision](2026-09-07-subagent-teardown-test-budgets.md) without replacing its disposal ownership or native-platform verification requirements. The [browser e2e decision](2026-07-24-web-gui-browser-e2e-lane.md) continues to own the assembled browser lane and recorded expectations.
The [queued-image test](../../../../apps/web/tests/queue-image.e2e.ts) separately holds admission and attachment retrieval, then captures the admitted row's loaded thumbnail. Cleanup shares one promise, releases held requests, and drains their handlers before closing the browser.
The [Queue browser test](../../../../apps/web/tests/queue-actions.e2e.ts) observes the collapsed sidebar and completed frame animations after a narrow resize, then reads both card rectangles and the declared inset in one browser evaluation. Separate round trips can mix a pre-resize Queue coordinate with a post-resize composer coordinate even when their inset is correct in both frames. A controlled resize barrier reproduces that mismatch; the same control passes with the atomic observation.
The [Details Session-lifecycle test](../../../../apps/web/tests/details-session-lifecycle.e2e.ts) awaits the frame's captured animation promises after closed state appears, then checks the zero-width track. Cancelled transitions also reach that assertion; animation settlement cannot make a persistent nonzero track pass.
The [publint runner tests](../../../../scripts/publint-all.spec.ts) await asynchronous child closure under the lane budget instead of imposing a five-second synchronous spawn deadline. They check spawn errors and termination signals independently of exit status. Teardown captures children and fixture roots before awaiting, terminates unfinished children, and joins closure before deleting roots. A delayed startup reproduces the former null-status failure; a forced outer timeout verifies that the child is gone while its root still exists.
The [whole-queue steering test](../../../../apps/web/tests/steering.e2e.ts) waits for enabled steering actions and the composer's queue-steering hint. A model-stream barrier keeps the following question-composer takeover pending while the test observes steering. Teardown releases that barrier before browser closure.
The [Details Session-lifecycle test](../../../../apps/web/tests/details-session-lifecycle.e2e.ts) awaits the frames captured animation promises after closed state appears, then retains the zero-width assertion. Completed and cancelled transitions both reach that assertion; cancellation cannot make a persistent nonzero track pass. A paused real grid transition reproduces the close assertion failure and completes successfully only after release, while a persistent one-pixel track remains rejected.
The [workspace-management test](../../../../apps/web/tests/workspace-management.e2e.ts) waits for restored composer focus before the next directory-dialog gesture. Its archive case gives the known seed id an explicit user title through the Session controller, then uses that exact title to identify the row across reload. An unrelated restored row cannot satisfy that locator; the durable archive assertion still checks the seed id and retained log.
The [feedback-release test](../../../../apps/web/tests/feedback-release.e2e.ts) waits for the model menu to close after selection: its projected label can change while the selection response remains in flight. The [queued-image test](../../../../apps/web/tests/queue-image.e2e.ts) distinguishes the optimistic preview from the durable rows attachment; thumbnail capture follows admission and the durable attachment response. Held-response controls pin both orderings, and independent test-finished cleanup releases intercepted requests even after timeout.
The [worker budget tests](../../../../packages/code-runtime/code-runtime-worker-thread/tests/budget.spec.ts) retain real worker execution and binding transport while controlling host timers and ELU samples. They acknowledge binding entry before exercising idle, active, and wall-clock decisions, so a bootstrap timeout cannot stand in for a budget decision during a binding. The [real-worker tests](../../../../packages/code-runtime/code-runtime-worker-thread/tests/runtime.spec.ts) independently retain actual ELU, idle-binding, and hot-loop coverage.
The [wide-table test](../../../../apps/web/tests/markdown-wide-table.e2e.ts) waits for requested and rendered frame tracks to agree and for the conversation ResizeObserver to publish the current rendered width. Two identical intermediate table widths do not prove those inputs settled. Paused native transitions and delayed observer callbacks reproduce the stale breakout reading; an intentionally broken breakout still fails the unchanged geometry assertions.
The [detached-launch tests](../../../../packages/host/open-in-app/tests/launch-detached.spec.ts) control watch time and deliver late process events through the real launcher's registered callbacks. They check one settlement, one unref, and no child kill. Real-process environment and early-exit cases remain in the [resolver tests](../../../../packages/host/open-in-app/tests/resolver.spec.ts).
The [lifecycle command-menu snapshots](../../../../apps/web/tests/lifecycle-chrome.e2e.ts) wait for a catalog option before capturing English or Chinese output; a visible listbox can still contain a stable loading placeholder. The [detached-launch tests](../../../../packages/host/open-in-app/tests/launch-detached.spec.ts) drive watch time and late process events directly through the real launcher, checking that late success, failure, and error events neither unref twice nor kill the child. Real-process environment and early-exit cases remain separate. Sleeping after spawn cannot prove that a late callback ran before coverage collection.
The [LSP backpressure test](../../../../packages/lsp/lsp-stdio/tests/instance.spec.ts) corks the real stdin stream at `didOpen`, writes the full document, and observes the pending write before aborting. The test observes query rejection immediately and joins disposal instead of relying on a short startup marker deadline or a sleep to infer backpressure.
The [whole-queue steering test](../../../../apps/web/tests/steering.e2e.ts) waits for both enabled steering actions and the composers queue-steering hint before invoking the shortcut. Optimistic rows can already contain the expected text while the Host queue remains empty. A deferred model-stream barrier keeps question-composer takeover after steering; teardown releases that barrier before closing the browser.
The [workspace-management test](../../../../apps/web/tests/workspace-management.e2e.ts) waits for restored composer focus before the next directory-dialog gesture, because workspace listing can render before Session restoration changes focus. Archive actions stay scoped to the seeded row rather than a captured fallback title that cold loading can replace; durable archive assertions retain the exact seed id.
The [worker budget tests](../../../../packages/code-runtime/code-runtime-worker-thread/tests/budget.spec.ts) retain real worker execution and binding transport while controlling only host timers and event-loop-utilization samples. They prove that idle binding time can exceed the compute allowance, active time still expires with a binding pending, and the independent wall ceiling still expires. Real measured hot-loop tests remain; a small total active-time allowance cannot isolate idle accounting from worker bootstrap and host scheduling.
The [LSP backpressure test](../../../../packages/lsp/lsp-stdio/tests/instance.spec.ts) preserves the real paused-reader fixture and large native pipe write. Before accepting the abort error, it verifies that the pending write callback settled and the captured subprocess completed; `instance.dead` alone can be true as soon as disposal starts.
### Built-client import classification
The [master Windows run](https://github.com/deepseek-harness/deepseek-harness/actions/runs/34204779455/job/101996934534) also rejects the intentional CSS import exposed by `ui-dockkit`. The [Node import sweep](../../../../packages/experimental/webworker-runtime/tests/compile/transform-corpus-check.ts) admits that exact bundle only when Node reports `ERR_UNKNOWN_FILE_EXTENSION` for its `dockkit.module.css`. Other errors at the same entry still fail, and an entry that imports successfully reports a stale exemption. This preserves the import sweep without requiring a browser-only component library to load its stylesheet under bare Node.
The [Node import sweep](../../../../packages/experimental/webworker-runtime/tests/compile/transform-corpus-check.ts) admits the Dockkit bundle only when Node reports `ERR_UNKNOWN_FILE_EXTENSION` for its exact `dockkit.module.css` path. Other errors and unexpectedly successful exempt imports fail. Scoped resolve/load hooks exercise expected CSS failure, arbitrary failure, another stylesheet, another error code, and stale exemption without modifying shared build artifacts.
## Alternatives considered
- Increase production timeouts or add test retries: neither establishes which operation completed, and both change behavior unrelated to the failing assertion.
- Replace the local deadline with a larger constant: this still overrides future lane budgets and leaves correctness dependent on host speed.
- Accept HTTP 202 or process startup as success: neither proves the expected model request or command output.
- Serialize the coverage or browser suite: the failures do not establish a shared-resource collision requiring suite-wide exclusion.
- Increase production timeouts, add retries, or serialize the suite: none establishes the missing completion observation.
- Infer completion from HTTP 202, an optimistic image, or a projected label: each can precede the operation being asserted.
- Replace real worker timing coverage with controlled samples: that would omit verification of Node's actual ELU and transport behavior.
## Consequences
A controlled pause before Workspace creation reproduces the reference assertion with the original polling wait. Releasing the barrier and awaiting the request passes the same browser expectations without rewriting goldens. These controls prove the synchronization defect; they do not measure historical runner contention. Product behavior, production timing, and CI scheduling remain unchanged.
Each fixture owns its clocks, barriers, callbacks, processes, and temporary paths. Controlled observations supplement real worker, subprocess, browser, and persistence paths. Product behavior, production timing, benchmark budgets, CI scheduling, and recorded expectations remain unchanged.
@@ -1,52 +1,43 @@
# Agent Note: CI 测试观察完成状态而非主机速度
# Agent NoteCI fixture 的完成与隔离
Status: implemented
[English](2026-09-08-ci-completion-observations.md) | 中文
## Problem
## 问题
[参考 CI 运行](https://github.com/deepseek-harness/deepseek-harness/actions/runs/34206953049)在外层测试预算耗尽前拒绝了两个异步操作:webhook 创建的 Session 在测试轮询一秒后仍不存在,后台 PowerShell 命令在五秒读取期限内尚未输出内容。两个 API 都没有承诺这些延迟上限。成功的 HTTP 202 响应只确认 webhook 已分发,不代表 Session 已创建或模型请求已接纳
[参考 CI 运行](https://github.com/deepseek-harness/deepseek-harness/actions/runs/34206953049)报告:轮询一秒后 webhook 创建的 Session 仍不存在,五秒读取期限内 PowerShell 输出为空。HTTP 接受、UI 投影状态、进程启动和持久化完成是不同的观察。测试需要明确的完成条件,并用对照阻止中间状态满足该条件
## Decision
## 决策
[GitHub 评审浏览器测试](../../../../apps/web/tests/github-ready-review.e2e.ts)先等待确定性适配器收到首个请求,再断言 Agent(智能体)和请求的精确数量。Workspace 创建的延迟屏障证明 HTTP 接纳可以早于这两项观察;屏障委托真实方法,并在 `finally` 中释放和恢复;如果请求本身挂起并超过测试超时,独立的 `onTestFinished` 清理会释放和恢复屏障。原有的 Workspace 成员关系、提示词内容、回复以及折叠和展开浏览器预期仍然是判定依据
[GitHub 评审浏览器测试](../../../../apps/web/tests/github-ready-review.e2e.ts)在 HTTP 202 后阻塞真实 Workspace 创建,验证 Agent 和模型请求均不存在,再释放创建并等待对应 Session 的 `turn/end`。即使测试超时,清理会释放屏障、恢复方法并移除事件监听器。Workspace 归属、请求数量、提示词内容和浏览器预期保留原有断言
[PowerShell 执行器测试](../../../../packages/shell/pwsh-local/tests/executor.spec.ts)先等待 `done`,再读取完整的 stdin环境变量输出。启动及消费式读取检查用私有文件屏障阻止命令结束,因此运行状态和未读的后续输出不依赖 sleep 或耗时阈值。部分输出轮询继承 lane 预算每个创建的 Context 都在使用前登记清理;子进程释放先于私有目录删除。在原生 Windows 上,延迟六秒的命令复现五秒期限失败,而改用完成等待后通过
[PowerShell 执行器测试](../../../../packages/shell/pwsh-local/tests/executor.spec.ts)用私有文件屏障控制启动与消费式读取。测试决定后续输出何时可用;最终 stdin环境变量输出`done` 后读取。轮询使用当前测试预算每个创建的 Context 都在插件初始化前登记清理在等待释放前同时取得 Context 与目录,完成释放后才删除目录
外层测试超时仍负责防止挂起。完成状态断言不会仅因操作跨越进程、文件系统或事件循环边界,就额外附带一个更短的性能要求。这扩展了 [subagent 清理预算决策](2026-09-07-subagent-teardown-test-budgets.zh.md),但不替代其 dispose(资源释放)所有权和原生平台验证要求。[浏览器 e2e 决策](2026-07-24-web-gui-browser-e2e-lane.zh.md)继续负责完整组合浏览器 lane 和录制预期
[排队图片测试](../../../../apps/web/tests/queue-image.e2e.ts)分别阻塞接纳和附件读取,再捕获已接纳行中加载完成的缩略图。清理共享一个 Promise,释放保留的请求,并在关闭浏览器前等待其 handler 完成
[Queue 浏览器测试](../../../../apps/web/tests/queue-actions.e2e.ts)在缩窄视口后观察侧栏已折叠且框架动画已完成,再在一次浏览器执行中读取两张卡片的矩形和声明的内边距。分开的往返可能混合调整前的 Queue 坐标和调整后的输入框坐标,即使两个时刻的内边距都正确。受控的尺寸调整屏障复现了这种不匹配;改用原子观察后,相同对照通过。
[详情 Session 生命周期测试](../../../../apps/web/tests/details-session-lifecycle.e2e.ts)在关闭状态出现后等待框架已捕获的动画 Promise,再检查轨道宽度为零。取消的过渡同样进入该断言;动画结束不能让持续非零的轨道通过。
[publint 运行器测试](../../../../scripts/publint-all.spec.ts)在 lane 预算内等待异步子进程关闭,而不施加五秒同步 spawn 期限。测试独立检查 spawn 错误、终止信号和退出码。清理在等待前取得子进程和 fixture 根目录,终止尚未完成的子进程,并在删除根目录前等待关闭。延迟启动复现了原来的空退出码失败;强制触发外层超时则验证子进程已经结束,而其根目录仍然存在
[整队列 steering 测试](../../../../apps/web/tests/steering.e2e.ts)等待 steering 操作可用以及 composer 显示队列 steering 提示。模型流屏障在测试观察 steering 时阻止后续问题 composer 接管。清理在关闭浏览器前释放该屏障
[详情 Session 生命周期测试](../../../../apps/web/tests/details-session-lifecycle.e2e.ts)在关闭状态出现后等待框架已捕获的动画 Promise,再保留宽度为零的断言。完成和取消的过渡都会进入该断言;取消不能让持续非零的轨道通过。暂停真实网格过渡可以复现关闭断言失败,只有释放后才成功完成,而持续一像素的轨道仍被拒绝
[Workspace 管理测试](../../../../apps/web/tests/workspace-management.e2e.ts)在下一次目录对话框操作前等待恢复后的 composer 焦点。归档用例通过 Session controller 为已知 seed id 设置显式用户标题,再用该精确标题跨重载定位行。无关的恢复行无法匹配该定位器;持久化归档断言仍检查 seed id 和保留的日志
[反馈释放测试](../../../../apps/web/tests/feedback-release.e2e.ts)在选择模型后等待菜单关闭:选择响应仍在传输时,投影标签就可能变化。[排队图片测试](../../../../apps/web/tests/queue-image.e2e.ts)区分乐观预览和持久化行的附件;缩略图捕获发生在接纳和持久化附件响应之后。受控响应屏障固定这两种顺序,独立的测试完成清理即使在超时后也会释放拦截的请求
[Worker 预算测试](../../../../packages/code-runtime/code-runtime-worker-thread/tests/budget.spec.ts)保留真实 worker 执行与绑定传输,只控制 Host 定时器和 ELU 样本。测试先确认绑定已进入,再检验 idle、active 和壁钟决策,使启动超时不能冒充绑定期间的预算决策。[真实 worker 测试](../../../../packages/code-runtime/code-runtime-worker-thread/tests/runtime.spec.ts)独立保留实际 ELU、空闲绑定和热循环覆盖
[宽表格测试](../../../../apps/web/tests/markdown-wide-table.e2e.ts)等待请求的框架轨道与渲染轨道一致,并等待会话 ResizeObserver 发布当前渲染宽度。两次相同的中间表格宽度不能证明这些输入已稳定。暂停原生过渡和延迟观察回调能够复现过期的溢出读数;故意破坏溢出布局时,未修改的几何断言仍然失败
[分离启动测试](../../../../packages/host/open-in-app/tests/launch-detached.spec.ts)控制观察时间,并通过真实 launcher 登记的回调发送迟到进程事件。测试检查仅完成一次、仅 unref 一次且不终止子进程。[Resolver 测试](../../../../packages/host/open-in-app/tests/resolver.spec.ts)保留真实进程的环境变量和提前退出用例
[生命周期命令菜单快照](../../../../apps/web/tests/lifecycle-chrome.e2e.ts)在捕获英文或中文输出前等待目录选项;可见列表框仍可能包含稳定的加载占位内容。[独立启动测试](../../../../packages/host/open-in-app/tests/launch-detached.spec.ts)通过真实启动器直接控制观察时间和延迟进程事件,检查延迟成功、失败及错误事件既不会重复 unref,也不会终止子进程。真实进程的环境和提前退出用例仍独立保留。spawn 后 sleep 无法证明延迟回调在覆盖率收集前已执行
[LSP 背压测试](../../../../packages/lsp/lsp-stdio/tests/instance.spec.ts)保留真实暂停读取的 fixture 与大型原生管道写入。接受 abort 错误前,测试验证待处理写入回调已完成、捕获的子进程也已结束;`instance.dead` 在释放开始时就可能为真
[LSP 背压测试](../../../../packages/lsp/lsp-stdio/tests/instance.spec.ts)在 `didOpen` 处 cork 真实 stdin 流,写入完整文档,并在中止前观察待完成写入。测试立即观察查询拒绝并等待释放,不依赖短启动标记期限,也不通过 sleep 推断背压。
### 已构建 Client 的导入分类
[整队转向测试](../../../../apps/web/tests/steering.e2e.ts)在触发快捷键前等待两个已启用的转向操作,以及输入框的队列转向提示。Host 队列仍为空时,乐观行可能已经包含预期文本。延迟模型流屏障使问题输入框在转向后才接管;清理在关闭浏览器前释放该屏障
[Node import sweep](../../../../packages/experimental/webworker-runtime/tests/compile/transform-corpus-check.ts)只有在 Node 针对准确的 `dockkit.module.css` 路径报告 `ERR_UNKNOWN_FILE_EXTENSION` 时才接受 Dockkit bundle。其他错误以及意外成功的豁免导入都会失败。限定范围的 resolve/load hook 覆盖预期 CSS 失败、任意失败、其他 stylesheet、其他错误码和过期豁免,不修改共享构建产物
[工作区管理测试](../../../../apps/web/tests/workspace-management.e2e.ts)在下一次目录对话框操作前等待恢复后的输入框焦点,因为工作区列表可能早于 Session 恢复引发的焦点变化完成渲染。归档操作始终限定在种子行内,不依赖冷加载可能替换的已捕获备用标题;持久化归档断言保留精确种子 id。
## 考虑过的替代方案
[worker 预算测试](../../../../packages/code-runtime/code-runtime-worker-thread/tests/budget.spec.ts)保留真实 worker 执行和绑定传输,只控制宿主定时器与事件循环利用率样本。测试证明空闲绑定时间可以超过计算额度、绑定待完成时活动时间仍会触发到期,以及独立壁钟上限仍会触发到期。真实计量的热循环测试仍保留;较小的总活动时间额度无法将空闲计费与 worker 启动及宿主调度隔离
- 增加生产超时、增加重试或串行化整个套件:均不能建立缺少的完成观察
- 从 HTTP 202、乐观图片或投影标签推断完成:它们均可能早于被断言的操作。
- 用受控样本替换真实 worker 计时覆盖:会遗漏对 Node 实际 ELU 与传输行为的验证。
### 已构建客户端的导入分类
## 影响
[master Windows 运行](https://github.com/deepseek-harness/deepseek-harness/actions/runs/34204779455/job/101996934534)还拒绝了 `ui-dockkit` 有意暴露的 CSS 导入。[Node 导入检查](../../../../packages/experimental/webworker-runtime/tests/compile/transform-corpus-check.ts)仅在 Node 针对其 `dockkit.module.css` 报告 `ERR_UNKNOWN_FILE_EXTENSION` 时,才允许这个精确的 bundle。相同入口的其他错误仍然失败,成功导入则报告豁免已过期。这保留了导入检查,同时不要求仅面向浏览器的组件库在裸 Node 中加载样式表
## Alternatives considered
- 增大生产超时或添加测试重试:两者都无法确定哪个操作已经完成,而且会改变与失败断言无关的行为。
- 用更大的常量替换局部期限:这仍然覆盖未来的 lane 预算,并使正确性依赖主机速度。
- 将 HTTP 202 或进程启动视为成功:两者都不能证明预期模型请求或命令输出已经发生。
- 串行化覆盖率或浏览器套件:这些失败没有证明存在必须对整个套件互斥的共享资源冲突。
## Consequences
在 Workspace 创建前受控暂停,可以用原来的轮询等待复现参考断言。释放屏障并等待请求后,相同浏览器预期通过,无需改写 golden。这些对照证明了同步缺陷,但没有测量历史 runner 的资源争抢。产品行为、生产时序和 CI 调度保持不变。
每个 fixture 拥有自己的时钟、屏障、回调、进程和临时路径。受控观察补充真实 worker、子进程、浏览器和持久化路径。产品行为、生产时序、基准预算、CI 调度和录制预期均保持不变
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write apps/web/tests/README.md
README.md: 252ed70f81a5fb2d41e94f79a00ea989255ce0ab
README.zh.md: f59bb623c8eae2fa0a80dc4212fc44a6addd3e72
README.md: a3503e66d780e62562348b02830517381d21df92
README.zh.md: a9039bbd0444a0786cd419c1e6aba0c3504e26d3
+1 -5
View File
@@ -10,11 +10,7 @@ the deliberate composition divergences from `dsh web` — are documented in
## Completion observations
The GitHub webhook scenario awaits the deterministic model adapters first request before inspecting its created Session; HTTP 202 only acknowledges dispatch. Its controlled Workspace-creation barrier verifies that distinction without a short polling deadline. See the [completion-observation decision](../../../.agents/notes/implemented/testing/2026-09-08-ci-completion-observations.md).
Queue layout assertions wait for the responsive frame to settle and sample both related rectangles in one browser evaluation; separate round trips can observe different layout states. Details-panel close assertions await the frames running transitions before checking zero width; published closed state alone does not imply settled geometry.
Feedback selection waits for menu closure, queued-image capture waits for the durable attachment after admission, and wide-table measurements wait for frame tracks and observer-published conversation width to agree with rendered geometry.
State-sensitive cases use Workspace, admission, attachment, and model-stream barriers to separate visible intermediate states from completed operations. Details close waits for frame transitions; archive verification assigns an explicit title to the seeded Session and follows that identity across reload. See the [CI fixture synchronization decision](../../../.agents/notes/implemented/testing/2026-09-08-ci-completion-observations.md).
## These are Host-face tests
+1 -5
View File
@@ -9,11 +9,7 @@
## 完成状态观察
GitHub webhook 场景先等待确定性模型适配器收到首个请求,再检查已创建的 Session;HTTP 202 只确认分发。受控的 Workspace 创建屏障验证这一区别,而不引入短轮询期限。参见[完成状态观察决策](../../../.agents/notes/implemented/testing/2026-09-08-ci-completion-observations.zh.md)。
Queue 布局断言等待响应式框架稳定,并在一次浏览器执行中采样相关的两个矩形;分开的往返可能观察到不同布局状态。详情面板关闭断言先等待框架正在运行的过渡结束,再检查宽度为零;已发布的关闭状态本身不代表几何布局已稳定。
反馈选择等待菜单关闭,排队图片捕获等待接纳后的持久化附件,宽表格测量等待框架轨道和观察器发布的会话宽度与渲染几何一致。
依赖状态的用例使用 Workspace、接纳、附件和模型流屏障,区分可见中间状态与已完成操作。详情关闭等待框架过渡结束;归档验证为 seed Session 设置显式标题,并跨重载跟踪该身份。参见 [CI fixture 同步决策](../../../.agents/notes/implemented/testing/2026-09-08-ci-completion-observations.zh.md)。
## 这些是 Host 面的测试
+2 -28
View File
@@ -9,7 +9,7 @@ import { gunzipSync } from 'node:zlib'
import type { Browser, Page } from 'playwright'
import { chromium } from 'playwright'
import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session'
import { afterAll, beforeAll, describe, expect, it, onTestFailed, onTestFinished } from 'vitest'
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
import {
acknowledgeReloadConnectionLoss, assertFixtureInventory, captureExpandedTurnProcessAria, captureStableAria,
compareOrRefreshGolden, fixtureUserPrompts,
@@ -199,33 +199,7 @@ describe.each(MODE === 'record' ? ['deepseek-official'] : ['deepseek-official',
const events = await readPersistedEvents(scaffold, sessionId)
expect(events.at(-1)?.type).toBe('command/done')
expect(events.at(-1)!.seq).toBeGreaterThan(authorized.at(-1)!.seq)
const release = Promise.withResolvers<undefined>()
let cleanupPromise: Promise<void> | undefined
const cleanup = (): Promise<void> => cleanupPromise ??= (async () => {
release.resolve(undefined)
await page.unrouteAll({ behavior: 'wait' })
})()
onTestFinished(cleanup, 15_000)
let responseReceived = false
await page.route('**/api/session/selectModel', async (route) => {
const response = await route.fetch({ timeout: 10_000 })
responseReceived = true
await release.promise
await route.fulfill({ response })
})
const selection = selectModel('Feedback mock')
// Observe rejection immediately; the original promise is awaited after releasing the response.
const observedSelection = Promise.allSettled([selection])
try {
await expect.poll(() => responseReceived, { timeout: 10_000 }).toBe(true)
const trigger = page.getByRole('button', { name: /^Select model, current/ })
await expect.poll(() => trigger.getAttribute('aria-label'), { timeout: 10_000 }).toContain('Feedback mock')
expect(await trigger.getAttribute('aria-expanded')).toBe('true')
} finally {
await cleanup()
await observedSelection
}
await selection
await selectModel('Feedback mock')
await selectModel('DeepSeek-V4-Flash')
const warningStart = tripwire.warnings.length
await page.reload({ waitUntil: 'load' })
+1 -2
View File
@@ -36,11 +36,9 @@ const REPLY = 'Review complete: no actionable findings.'
/** Deterministic model response for the webhook-created Session. */
class ReviewAdapter extends LlmAdapter {
readonly requests: GenerateOptions[] = []
readonly firstRequest = Promise.withResolvers<undefined>()
override async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
this.requests.push(options)
this.firstRequest.resolve(undefined)
yield { type: 'block-start', index: 0, blockType: 'text' }
yield { type: 'block-end', index: 0, block: { type: 'text', text: REPLY } }
yield { type: 'finish', reason: { kind: 'stop' } }
@@ -158,6 +156,7 @@ describe.skipIf(MODE === 'record')('web e2e: GitHub ready-for-review', () => {
return await createWorkspace(...args)
})
onTestFinished(() => {
off()
release.resolve(undefined)
create.mockRestore()
})
+23 -30
View File
@@ -9,7 +9,7 @@ import { fileURLToPath } from 'node:url'
import { join } from 'node:path'
import type { Browser, Page } from 'playwright'
import { chromium } from 'playwright'
import { afterEach, describe, expect, it, onTestFailed } from 'vitest'
import { afterEach, describe, expect, it, onTestFailed, vi } from 'vitest'
import { deriveReplayScript, parseSessionLog, type ReplayEntry } from '@deepseek-ai/dsh-llm-replay'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
import {
@@ -159,35 +159,28 @@ describe('web e2e: queue row actions', () => {
).toBe(2)
await page.setViewportSize({ width: 640, height: 1000 })
const narrowFrame = page.locator('[data-sidebar-collapsed="true"]')
await narrowFrame.waitFor()
await narrowFrame.evaluate(async (element) => {
await Promise.allSettled(element.getAnimations().map(animation => animation.finished))
})
// The frame's resize observer can move both cards between browser round trips.
const { queueBox, composerBox, dockInset } = await page.evaluate(() => {
const queue = document.querySelector('[data-queue-dock]')
const composer = document.querySelector('[data-composer-card]')
const box = (element: Element | null) => {
const rect = element?.getBoundingClientRect()
return rect === undefined ? null : { x: rect.x, width: rect.width }
}
return {
queueBox: box(queue),
composerBox: box(composer),
dockInset: composer === null ? NaN : Number.parseFloat(
getComputedStyle(composer).getPropertyValue('--dsh-composer-dock-inset')),
}
})
expect(queueBox).not.toBeNull()
expect(composerBox).not.toBeNull()
expect(queueBox!.x).toBeGreaterThanOrEqual(composerBox!.x)
expect(queueBox!.x + queueBox!.width)
.toBeLessThanOrEqual(composerBox!.x + composerBox!.width)
const queueLeftInset = queueBox!.x - composerBox!.x
const queueRightInset = composerBox!.x + composerBox!.width - queueBox!.x - queueBox!.width
expect(queueLeftInset).toBeCloseTo(dockInset, 1)
expect(queueRightInset).toBeCloseTo(dockInset, 1)
await page.locator('[data-sidebar-collapsed="true"]').waitFor()
// The responsive sidebar and composer settle independently; sample both
// rectangles in one browser task so the comparison uses one layout.
await vi.waitFor(async () => {
const metrics = await page.evaluate(() => {
const queue = document.querySelector('[data-queue-dock]')
const composer = document.querySelector('[data-composer-card]')
if (queue === null || composer === null) return undefined
const queueBox = queue.getBoundingClientRect()
const composerBox = composer.getBoundingClientRect()
return {
leftInset: queueBox.left - composerBox.left,
rightInset: composerBox.right - queueBox.right,
dockInset: Number.parseFloat(getComputedStyle(composer).getPropertyValue('--dsh-composer-dock-inset')),
}
})
expect(metrics).toBeDefined()
expect(metrics!.leftInset).toBeGreaterThanOrEqual(0)
expect(metrics!.rightInset).toBeGreaterThanOrEqual(0)
expect(metrics!.leftInset).toBeCloseTo(metrics!.dockInset, 1)
expect(metrics!.rightInset).toBeCloseTo(metrics!.dockInset, 1)
}, { timeout: 10_000 })
await page.setViewportSize({ width: 1680, height: 1000 })
const editRow = page.locator('[data-queue-dock] li', { hasText: EDIT })
+12 -5
View File
@@ -121,7 +121,7 @@ describe('web e2e: workspace management (create / rename / flat view / hover aff
* Reveal and click a row action, re-hovering if a projection update replaces
* the row before its hover-only button becomes visible.
*/
async function clickHoverAction(row: Locator, name: string | RegExp): Promise<void> {
async function clickHoverAction(row: Locator, name: string): Promise<void> {
const button = row.getByRole('button', { name })
await expect.poll(async () => {
await row.hover()
@@ -593,17 +593,24 @@ describe('web e2e: workspace management (create / rename / flat view / hover aff
it('archives the seeded session from its row menu, hiding it durably across reload', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-ws-archive'))
const sessionRow = await seededSessionRow()
const initialRow = await seededSessionRow()
// Selecting the seed hides any blank stray left by Workspace deletion,
// so archiving this last visible Ungrouped Session must remove the bucket.
await sessionRow.click()
await initialRow.click()
const { title } = await scaffold.ctx.sessionController.rename({
sessionId: SessionId(SEED_ID), title: `Archive target ${SEED_ID}`,
})
// A user-owned title binds the locator to this seed across restoration.
const sessionRow = page.getByRole('treeitem').filter({
has: page.getByText(title, { exact: true }),
})
await expect.poll(() => sessionRow.count(), { timeout: 10_000 }).toBe(1)
await expect.poll(() => sessionRow.getAttribute('aria-selected'), { timeout: 10_000 }).toBe('true')
const ungroupedSection = page.getByText('Ungrouped', { exact: true }).locator('..').locator('..').locator('..')
await expect.poll(() => ungroupedSection.locator('[role="treeitem"]').count(), { timeout: 10_000 }).toBe(2)
// Row menu: hover reveals the actions button; Archive session commits
// without a confirmation dialog (non-destructive: log + accounting stay).
// Opening a cold Session can replace its fallback title after selection.
await clickHoverAction(sessionRow, /^Session actions for /)
await clickHoverAction(sessionRow, `Session actions for ${title}`)
await page.getByRole('menuitem', { name: 'Archive session' }).click()
// The row disappears on the archive-set echo; with no other visible
// stray, the whole Ungrouped bucket withdraws.
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write benchmarks/agent-continuation/README.md
README.md: 3d38f008c4ee95c794e4e7fbd3d874d1d668b1ce
README.zh.md: 189dcae8eea8716dbcb24b1fe2b08f1113caf36d
README.md: 489939499af8d98922df2cbbdd6be793bf6e3796
README.zh.md: e99760cfd1aba0ca41e78243f333fe18446474fe
+1 -1
View File
@@ -18,7 +18,7 @@ Measure long-history request processing, cold tool-heavy continuation, and repea
From the repository root, build the libraries and workers with `pnpm run build:bench`, then run `pnpm exec vitest run --config vitest.bench.config.ts benchmarks/agent-continuation/agent-continuation.bench.ts`. Do not overlap timing runs with builds or other benchmarks.
The test reports all five fresh-process samples and enforces reviewed median budgets. Catalog and tool continuation each use a 900 ms standard hosted CI expectation with 1.25× headroom (1,125 ms); request history uses a separately reviewed 297 ms hosted limit ([calibration](../../.agents/notes/implemented/simplification/2026-09-06-agent-request-freeze-provenance.md)), and SDK continuation uses reference-machine scaling. A failed worker reports its exit, signal, timeout, and stderr; temporary roots are removed even on failure. The required benchmark lane discovers this file automatically.
The test reports all five fresh-process samples, CPU models, available parallelism, platform/architecture, and Node/V8 versions, and enforces reviewed median budgets. Catalog and tool continuation each use a 900 ms standard hosted CI expectation with 1.25× headroom (1,125 ms); request history uses a separately reviewed 297 ms hosted limit ([calibration](../../.agents/notes/implemented/simplification/2026-09-06-agent-request-freeze-provenance.md)), and SDK continuation uses reference-machine scaling. A failed worker reports its exit, signal, timeout, and stderr; temporary roots are removed even on failure. The required benchmark lane discovers this file automatically.
<a id="measurements"></a>
+1 -1
View File
@@ -18,7 +18,7 @@
在仓库根目录使用 `pnpm run build:bench` 构建库和 worker,然后运行 `pnpm exec vitest run --config vitest.bench.config.ts benchmarks/agent-continuation/agent-continuation.bench.ts`。不要让计时运行与构建或其他基准重叠。
测试报告全部五个新进程样本,并约束经审查的中位数预算。目录和工具续聊用例均使用标准托管 CI 的 900 ms 期望值与 1.25× 余量(1,125 ms);请求历史使用单独审查的 297 ms 托管上限([校准依据](../../.agents/notes/implemented/simplification/2026-09-06-agent-request-freeze-provenance.zh.md)),SDK 续聊使用参考机器缩放。worker 失败时报告退出状态、信号、超时和 stderr;失败时也会删除临时根目录。必需基准通道自动发现此文件。
测试报告全部五个新进程样本、CPU 型号、可用并行度、平台/架构和 Node/V8 版本,并约束经审查的中位数预算。目录和工具续聊用例均使用标准托管 CI 的 900 ms 期望值与 1.25× 余量(1,125 ms);请求历史使用单独审查的 297 ms 托管上限([校准依据](../../.agents/notes/implemented/simplification/2026-09-06-agent-request-freeze-provenance.zh.md)),SDK 续聊使用参考机器缩放。worker 失败时报告退出状态、信号、超时和 stderr;失败时也会删除临时根目录。必需基准通道自动发现此文件。
<a id="measurements"></a>
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/code-runtime/code-runtime-worker-thread/README.md
README.md: 1350a71f3cd6e94f185dfb830365ca9ddd423e8a
README.zh.md: db74da4639293a857bca15dc53cea0b1d5e4e157
README.md: c26f349762ab2dac9956099ab637cde9d6d93771
README.zh.md: 39885e64de0b98cfefe20f8e1b5cd362d4404eea
@@ -86,8 +86,6 @@ Model code can reach `parentPort` and forge traffic, so every inbound message is
Two independent budgets exist because the peer is hostile: `computeMs` meters the worker's measured busy time (`eventLoopUtilization()` polling every 25 ms), so a hot loop expires it whether or not a decoy dispatch is in flight, while a program idling on a slow binding accrues nothing; `maxWallMs` backstops what busy time cannot see, such as a promise nobody resolves. Both funnel into `worker.terminate()`. `maxWallMs` is range-checked at load against `MAX_TIMER_DELAY_MS` because `setTimeout` clamps a longer delay to 1 ms.
[Budget tests](tests/budget.spec.ts) control host clocks and measured ELU input while keeping worker execution and binding messages real. [Runtime tests](tests/runtime.spec.ts) separately exercise hot-loop containment with actual measurements.
### Output ledger
`maxOutputBytes` accounts the JSON serialization of the outer `logs` array plus the completion value or failure-message payload; fixed `CodeRunResult` field names and envelope syntax are outside that ledger. At or below the cap the exact value returns; a lossy completion is `invalid-output`, and a combined overflow is `output-limit` rather than a substituted inspected string. The failure retains a fitting captured prefix of the logs.
@@ -86,8 +86,6 @@ kind: "package-reference"
存在两个独立预算,因为对端不可信:`computeMs` 计量 worker 的实测忙碌时间(每 25 ms 轮询一次 `eventLoopUtilization()`),因此热循环无论是否有诱饵 dispatch 在途都会到期,而等待慢绑定的程序不累计;`maxWallMs` 为忙碌时间无法观测的情况兜底,例如永远不会 resolve 的 promise。二者最终都会调用 `worker.terminate()``maxWallMs` 在加载时对照 `MAX_TIMER_DELAY_MS` 做范围校验,因为 `setTimeout` 会把更长的延迟限制为 1 ms。
[预算测试](tests/budget.spec.ts)控制宿主时钟与实测 ELU 输入,同时保留真实 worker 执行和绑定消息。[运行时测试](tests/runtime.spec.ts)独立使用真实测量验证热循环约束。
### 输出账本
`maxOutputBytes` 统计外层 `logs` 数组加完成值或失败消息载荷的 JSON 序列化;固定的 `CodeRunResult` 字段名与信封语法不计入这份账本。未超过上限时返回精确值;有损完成值属于 `invalid-output`,组合溢出属于 `output-limit`,不会用 inspected string 代替。失败会保留日志中能容纳的已捕获前缀。
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/host/open-in-app/README.md
README.md: 97cadfb0522bc5d119eaa329ab37d6f504743121
README.zh.md: 8bc0c3f5de464bb76a0c890a56fa4f1b7d5ec050
README.md: 0f0c05f48469a03bc115d84e93a174fbfeba0c42
README.zh.md: 3ac1c72c66ea0008abe621a7ac8fe82c6caead4c
-2
View File
@@ -79,8 +79,6 @@ The package splits into a data table and three roles. [`src/catalog.ts`](src/cat
[`src/index.ts`](src/index.ts) registers the three routes on `ctx.webServer`: `GET /open-in-app/apps` (the resolution map's keys), `GET /open-in-app/icon/<id>` (the extracted icon, cached in memory per process), and `POST /open-in-app/open` (launches the map's verified launcher directly — never a re-detection). Every route asks the composition's `connection` service for a rejection first; the complete trust story — the Host/Origin fence and browser authentication — has one home in the [`src/index.ts`](src/index.ts) module comment. On top of that fence the open route validates its body at the wire: an `application/json` media type, a 64 KiB ceiling, a resolved-available catalog id, and an absolute path naming an existing directory. Resolution and icon commands run through [`@deepseek-ai/dsh-native-command`](../../util/native-command/README.md) (argv, never a shell) under their respective deadlines; PATH names go through `ctx.subprocess.resolveExecutable()` in-process.
[Launch settlement tests](tests/launch-detached.spec.ts) use controlled watch time and late process events to verify one settlement without relying on child startup speed; real-process launch cases remain in [resolver tests](tests/resolver.spec.ts).
</details>
-----
-2
View File
@@ -79,8 +79,6 @@ kind: "package-reference"
[`src/index.ts`](src/index.ts) 在 `ctx.webServer` 上注册三条路由:`GET /open-in-app/apps`(解析映射的 keys)、`GET /open-in-app/icon/<id>`(提取的图标,进程内内存缓存)、`POST /open-in-app/open`(直接使用映射中已验证的启动器——绝不重新检测)。每条路由都先向组合的 `connection` 服务询问是否拒绝;完整的信任叙述——Host/Origin 栅栏与浏览器认证——唯一的出处在 [`src/index.ts`](src/index.ts) 的模块注释。在该栅栏之上,open 路由在 wire 边界校验请求体:`application/json` 媒体类型、64 KiB 上限、解析为可用的目录 id、指向现存目录的绝对路径。解析与图标命令经 [`@deepseek-ai/dsh-native-command`](../../util/native-command/README.zh.md)(argv,绝不走 shell)在各自期限内执行;PATH 名称走 `ctx.subprocess.resolveExecutable()` 进程内解析。
[启动结算测试](tests/launch-detached.spec.ts)使用受控观察时间和延迟进程事件验证仅结算一次,不依赖子进程启动速度;真实进程启动用例仍保留在[解析器测试](tests/resolver.spec.ts)中。
</details>
-----
+11 -2
View File
@@ -8,7 +8,7 @@ import { Context } from '@deepseek-ai/cordis'
import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
import { LspInstance, readHostSource } from '@deepseek-ai/dsh-lsp-stdio'
import { encodeMessage } from '@deepseek-ai/dsh-lsp-stdio'
import type { ConnectionWriter } from '@deepseek-ai/dsh-lsp-stdio/src/connection.ts'
import type { ConnectionSpawner, ConnectionWriter } from '@deepseek-ai/dsh-lsp-stdio/src/connection.ts'
import type { InstanceSpec } from '@deepseek-ai/dsh-lsp-stdio/src/instance.ts'
import type { LspProviderQuery, LspQueryResult } from '@deepseek-ai/dsh-lsp'
import { scrubbedParentEnv } from '@deepseek-ai/dsh-subprocess'
@@ -45,6 +45,7 @@ function makeInstance(
env: Record<string, string> = {},
overrides: Partial<InstanceSpec> = {},
writer?: ConnectionWriter,
spawner: ConnectionSpawner = spawnSubprocess,
): LspInstance {
const instance = new LspInstance({
command: process.execPath,
@@ -59,7 +60,7 @@ function makeInstance(
shutdownTimeoutMs: 200,
killGraceMs: 200,
...overrides,
}, spawnSubprocess, writer)
}, spawner, writer)
live.push(instance)
return instance
}
@@ -211,6 +212,7 @@ describe('LspInstance query and abort', () => {
const marker = join(root, 'initialized.log')
const didOpenStarted = Promise.withResolvers<undefined>()
let didOpenFinished = false
let processClosed = false
const instance = makeInstance({
LSP_FAKE_INITIALIZED_MARKER: marker,
LSP_FAKE_PAUSE_STDIN_AFTER_INITIALIZED: '1',
@@ -227,6 +229,10 @@ describe('LspInstance query and abort', () => {
done(error)
})
didOpenStarted.resolve(undefined)
}, (spec) => {
const handle = spawnSubprocess(spec)
void Promise.allSettled([handle.done]).then(([result]) => { processClosed = result.status === 'fulfilled' })
return handle
})
const controller = new AbortController()
const outcome = run(instance, 'goToDefinition', controller.signal)
@@ -235,9 +241,12 @@ describe('LspInstance query and abort', () => {
await didOpenStarted.promise
signal.throwIfAborted()
expect(didOpenFinished).toBe(false)
expect(processClosed).toBe(false)
controller.abort(new Error('didOpen-abort'))
const failure = await outcome
expect(() => { throw failure }).toThrow(/didOpen-abort/)
expect(didOpenFinished).toBe(true)
expect(processClosed).toBe(true)
expect(instance.dead).toBe(true)
})
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/shell/pwsh-local/README.md
README.md: 4b773c6b22b7c2dfbdc3a8c55077721fbbc065d5
README.zh.md: 869344c27ad2efba859a3907c7bc0d45d13556d3
README.md: 1d6872ec6b342f415b1632f87be5a485c4dfa270
README.zh.md: 2594ecc1623fec9ea8022f92d1c38941f643d979
-2
View File
@@ -68,8 +68,6 @@ if (result.timedOut) console.log('timed out after', result.timeoutMs)
Call `start` to run a command in the background; it returns a handle immediately and no timeout applies. `readOutput()` merges the stream deltas into one consuming read, marking stderr under a `[stderr]` section; `kill()` terminates the provider-managed range; `done` settles when the direct command closes and never rejects. Job ids, ownership, polling, and notices belong to the generic `ctx.jobs` runtime, which the tool layer registers the handle with.
The real-process tests await `done` before asserting complete output and use private file barriers for partial-output checks. Their polling inherits the execution lanes budget, and Context disposal precedes temporary-directory removal; see the [completion-observation decision](../../../.agents/notes/implemented/testing/2026-09-08-ci-completion-observations.md).
<a id="adjusting-budgets-at-runtime"></a>
### Adjusting budgets at runtime
-2
View File
@@ -68,8 +68,6 @@ if (result.timedOut) console.log('timed out after', result.timeoutMs)
调用 `start` 即可在后台运行命令;它立即返回句柄,且不应用任何超时。`readOutput()` 把流增量合并为一次消费式读取,并在 `[stderr]` 分段下标记 stderr`kill()` 终止由提供方管理的 range`done` 在 direct command 关闭时结算且绝不 reject。job id、所有权、轮询与通知属于通用 `ctx.jobs` 运行时,工具层会把句柄注册进去。
真实进程测试先等待 `done`,再断言完整输出,并使用私有文件屏障检查部分输出。轮询继承执行 lane 的预算,Context 释放先于临时目录删除;参见[完成状态观察决策](../../../.agents/notes/implemented/testing/2026-09-08-ci-completion-observations.zh.md)。
<a id="adjusting-budgets-at-runtime"></a>
### 运行时调整预算
@@ -426,7 +426,7 @@ describe.skipIf(!hasPwsh)('PwshLocalExecutor.start (background process handles)'
const { bash } = await setup()
const barrier = commandBarrier()
const proc = bash.start(bash.resolve({
command: `Write-Output ready; ${barrier.command}; Write-Output done`,
command: `Write-Output ready; [Console]::Out.Flush(); ${barrier.command}; Write-Output done`,
env: barrier.env,
}))
expect(proc.status).toBe('running')
@@ -459,7 +459,7 @@ describe.skipIf(!hasPwsh)('PwshLocalExecutor.start (background process handles)'
const { bash } = await setup()
const barrier = commandBarrier()
const proc = bash.start(bash.resolve({
command: `Write-Output first; ${barrier.command}; Write-Output second`,
command: `Write-Output first; [Console]::Out.Flush(); ${barrier.command}; Write-Output second`,
env: barrier.env,
}))
const first = await readUntil(proc, 'first\n', task.timeout)
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/subagent/subagent-acp/README.md
README.md: 7a15951bcc260b4acb3f26e953235ea84c43acee
README.zh.md: e81a4ecf3b9430c4e065739ab9bfb43b7a2b5d1c
README.md: 87fb7f3101d2d329300213bed2acb4a479c83b72
README.zh.md: 399a262b9b6a4bba650552388b210d87f69f5687
-2
View File
@@ -93,8 +93,6 @@ This section explains how the backend drives a child over ACP and where the obse
A start resolves the child's working directory (the configured `cwd` override, else the parent session's cwd), spawns the command through the subprocess seam, performs the ACP `initialize` and `newSession` handshake, and only then publishes the run. Fulfillment means a remote session is ready and ownership has transferred to the caller. Disposal is idempotent: it closes stdin and waits a configured grace for cooperative quiescence, then escalates through SIGTERM to SIGKILL and awaits whole-range exit. Cleanup failures remain observable as ordered safe facts and never claim quiescence.
The [disposal tests](tests/subagent-acp.spec.ts) observe real child exit under the lane budget and distinguish POSIX signal escalation from Windows force termination; cleanup joins child exit before deleting marker directories. See the [teardown-test decision](../../../.agents/notes/implemented/testing/2026-09-07-subagent-teardown-test-budgets.md).
### Stop-reason mapping
The run outcome maps the ACP terminal into the shared stop-reason vocabulary (`completed`, `max-tokens`, `refusal`, `aborted`, or `error`) in [`src/run.ts`](src/run.ts).
@@ -93,8 +93,6 @@ spawn、初始化或新建会话失败会在发布前拒绝,通常先证明 ma
一次启动先解析子 agent 的工作目录(配置的 `cwd` 覆盖值,否则取父会话 cwd),经子进程 seam spawn 命令,完成 ACP `initialize``newSession` 握手,然后才发布运行。兑现意味着远程会话已就绪、所有权已转移给调用方。dispose(资源释放)是幂等的:先关闭 stdin 并按配置的宽限等待协作式完全停稳,再经 SIGTERM 升级到 SIGKILL,并等待整个 managed range 退出。清理失败会作为有序的安全事实保持可观察,且绝不声称已经完全停稳。
[资源释放测试](tests/subagent-acp.spec.ts)在 lane 预算内观察真实子进程退出,区分 POSIX 信号升级与 Windows 强制终止;清理先等待子进程退出,再删除标记目录。参见[清理测试决策](../../../.agents/notes/implemented/testing/2026-09-07-subagent-teardown-test-budgets.zh.md)。
### 停止原因映射
运行结果会把 ACP 终态映射进共享的停止原因词汇(`completed``max-tokens``refusal``aborted``error`),实现见 [`src/run.ts`](src/run.ts)。