fix(tests): keep Windows Job fixture cleanup with its owner
This commit is contained in:
1 parent
e2107a0394
commit
1ccef293fa
4 files changed
+8
-9
No files matched your search
+2
-2
@@ -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/architecture/2026-08-28-subprocess-native-containment.md
|
||||
2026-08-28-subprocess-native-containment.md: 1b0fd162be77356001bcd9224bb2ee2889e6d6c0
|
||||
2026-08-28-subprocess-native-containment.zh.md: f33643f98a6ea6a90c0780432cbe11a35a868562
|
||||
2026-08-28-subprocess-native-containment.md: 6dba4e506e584140106cf1687db870729f925750
|
||||
2026-08-28-subprocess-native-containment.zh.md: 8e7243ad6f64e338805de2a7444949b1ce6913d9
|
||||
@@ -46,6 +46,8 @@ Linux falls back before target execution when the exact bootstrap, modern readab
|
||||
|
||||
Normal Cordis disposal starts direct-result and range observation independently, requests termination, and waits for every owned range. Consumer teardown does not inspect an ordinary PID; it retains the original operation or startup error while attempting terminate and final wait, and preserves cleanup failures in the consumer's existing error order. A confirmed empty range permanently disables later signalling against stale identities.
|
||||
|
||||
Native Windows fixtures retain the same Job-owned termination and final wait on success and failure. A descendant PID is observation-only: issuing `taskkill` after its exit can terminate an unrelated process that has reused the number, including another concurrent test worker.
|
||||
|
||||
During a JavaScript-observable host exit, `LocalSubprocessRuntime` synchronously force-terminates every still-live handle without promises or timers. Linux sends the existing direct fallback kill and the exact scope kill; Windows kills the runner so its only Job handle closes; PTY fallback scans remain best effort. Per-handle failures are contained and do not change the host's exit result. Termination modes in which JavaScript cannot run remain outside this listener's guarantee.
|
||||
|
||||
## Existing decisions and supersession
|
||||
|
||||
@@ -46,6 +46,8 @@ selector 是 per-spawn locator 或 sentinel,不是凭据或持久格式。Linu
|
||||
|
||||
正常 Cordis dispose 会独立启动 direct-result 与 range observation、请求终止,并等待每个自有 range。消费方 teardown 不检查普通 PID;它会保留原始 operation 或 startup error,同时尝试 terminate 与 final wait,并按消费方既有错误顺序保留 cleanup failure。range 一旦被确认为空,就会永久禁止后续向陈旧 identity 发送信号。
|
||||
|
||||
原生 Windows fixture 在成功和失败时都通过同一个 Job owner 请求终止并完成最终等待。后代 PID 仅用于观察:进程退出后再执行 `taskkill`,可能终止已复用该编号的无关进程,包括并发运行的其他测试 worker。
|
||||
|
||||
在 JavaScript 可观察的 host exit 期间,`LocalSubprocessRuntime` 会同步强制终止每个仍存活的句柄,不使用 Promise 或 timer。Linux 会发送既有 direct fallback kill 与准确 scope kill;Windows 会终止 runner,使其唯一 Job handle 关闭;PTY fallback 扫描仍是 best effort。每个句柄的失败相互隔离,也不改变宿主退出结果。JavaScript 无法运行的终止形态不属于该 listener 的保证。
|
||||
|
||||
## Existing decisions and supersession
|
||||
|
||||
@@ -52,10 +52,6 @@ async function waitGone(pid: number): Promise<void> {
|
||||
throw new Error(`pid ${pid} remained alive`)
|
||||
}
|
||||
|
||||
function cleanup(pid: number): void {
|
||||
spawnSync('taskkill', ['/PID', String(pid), '/T', '/F'], { stdio: 'ignore' })
|
||||
}
|
||||
|
||||
type SpawnFailure = NodeJS.ErrnoException & { path?: string }
|
||||
|
||||
function expectedSpawnFailure(error: SpawnFailure): Record<string, unknown> {
|
||||
@@ -157,7 +153,6 @@ describe.skipIf(!windowsNative)('Windows Job native containment', () => {
|
||||
stdio: { stdin: 'ignore', stdout: 'pipe', stderr: 'pipe' } as const,
|
||||
}
|
||||
const handle = bindManagedProcess(request, launchWindowsJob(request, targetEnvironment(request)))
|
||||
let descendant: number | undefined
|
||||
try {
|
||||
if (handle.stdout === undefined) throw new Error('expected piped stdout')
|
||||
if (handle.stderr === undefined) throw new Error('expected piped stderr')
|
||||
@@ -171,7 +166,7 @@ describe.skipIf(!windowsNative)('Windows Job native containment', () => {
|
||||
handle.stderr?.once('end', resolve)
|
||||
handle.stderr?.once('error', reject)
|
||||
})
|
||||
descendant = await waitForPid(pidFile)
|
||||
const descendant = await waitForPid(pidFile)
|
||||
await expect(handle.done).resolves.toEqual({ exitCode: 42, signal: null })
|
||||
await expect(Promise.race([
|
||||
Promise.all([stdoutEnded, stderrEnded]).then(() => true),
|
||||
@@ -187,9 +182,9 @@ describe.skipIf(!windowsNative)('Windows Job native containment', () => {
|
||||
await expect(handle.waitForExit()).resolves.toBe(true)
|
||||
await waitGone(descendant)
|
||||
} finally {
|
||||
// The Job owns cleanup; a descendant PID may already identify an unrelated process after exit.
|
||||
handle.terminate()
|
||||
await Promise.allSettled([handle.done, handle.waitForExit()])
|
||||
if (descendant !== undefined) cleanup(descendant)
|
||||
rmSync(targetCwd, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user