From df709f442c4ec05640f928ccd81ded6aaa28fc59 Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Thu, 10 Sep 2026 17:36:14 +0800 Subject: [PATCH] test(subprocess): align the teardown race with the settlement contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `disposal contains a spawn-failure rejection that races teardown` asserted one winner of the race. A bootstrap that publishes its pre-exec failure rejects with that failure, and a teardown that stops the bootstrap first settles as the requested termination — the recorded failure only outranks the stop when it was published before the stop landed. Assert the contract instead: a rejection carries the failure, a stop carries SIGTERM, which only the Linux scope records because the win32 job owner and the fallback launcher both reject the start. The two terminal cases that drive a mocked PTY exit state why they pin `internals.platform`: under the host's native scope the mocked exit races the scope bootstrap and fails as `terminal scope exited before its bootstrap consumed the launch request`. --- .../subprocess-local/tests/local.spec.ts | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/subprocess/subprocess-local/tests/local.spec.ts b/packages/subprocess/subprocess-local/tests/local.spec.ts index 135638f54a..a13f589de1 100644 --- a/packages/subprocess/subprocess-local/tests/local.spec.ts +++ b/packages/subprocess/subprocess-local/tests/local.spec.ts @@ -393,6 +393,8 @@ describe('LocalSubprocessRuntime', () => { const ctx = new Context() const fiber = await ctx.plugin(IsolatedLocalSubprocessRuntime) const service = ctx.subprocess as InstanceType + // Pins the containment choice: with the host's native scope a mocked PTY + // exit races the scope bootstrap. service.internals = { platform: 'darwin' } const handle = await ctx.subprocess.spawnTerminal({ argv: ['shell'], cwd: process.cwd(), rows: 24, cols: 80, graceMs: 1, @@ -600,6 +602,8 @@ describe('LocalSubprocessRuntime', () => { ctx.logger.error = ((error: unknown) => { disposalErrors.push(error) }) as typeof ctx.logger.error const fiber = await ctx.plugin(IsolatedLocalSubprocessRuntime) const alive = new Set([124]) + // Pins the containment choice: with the host's native scope a mocked PTY + // exit races the scope bootstrap. ;(ctx.subprocess as InstanceType).internals = { platform: 'darwin' } ;(ctx.subprocess as InstanceType).terminalInspector = { foregroundPgid: () => 123, @@ -878,10 +882,26 @@ describe('LocalSubprocessRuntime', () => { const ctx = new Context() const fiber = await ctx.plugin(LocalSubprocessRuntime) // Dispose before the rejection continuation removes the handle from the - // live set, so teardown itself must swallow the rejected done. + // live set, so teardown itself must swallow the rejected done. Two + // settlements are valid and the winner is a race: a bootstrap that + // publishes its pre-exec failure rejects with that failure, and a teardown + // that stops the bootstrap first settles as the requested termination — + // the recorded failure only outranks the stop when it was published before + // the stop landed. const handle = ctx.subprocess.spawn(spec('true', { cwd: '/nonexistent-dir-dsh-subprocess-test' })) await fiber.dispose() - await expect(handle.done).rejects.toThrow() + const settlement = await handle.done.then( + outcome => ({ kind: 'stopped' as const, outcome }), + (error: unknown) => ({ kind: 'failed' as const, error }), + ) + if (settlement.kind === 'failed') { + expect(settlement.error).toBeInstanceOf(Error) + } else { + // Only the Linux scope records a stop this way: the win32 job owner + // rejects a cancelled start and the fallback launcher rejects the ENOENT, + // so neither can produce the stopped branch. + expect(settlement.outcome.signal).toBe('SIGTERM') + } }) it('loading a second implementation throws (one processes service per context — cordis standard)', async () => {