diff --git a/packages/dsh-loopx-plugin/src/cli.ts b/packages/dsh-loopx-plugin/src/cli.ts index c4079936fc..52319f68a0 100644 --- a/packages/dsh-loopx-plugin/src/cli.ts +++ b/packages/dsh-loopx-plugin/src/cli.ts @@ -377,9 +377,15 @@ export async function resolveLoopXCommand( maxOutputBytes: 16 * 1024, }, ) + if (options.signal?.aborted) { + throw new LoopXCliError('aborted', 'operation was cancelled', false) + } const version = result.exitCode === 0 ? versionText(result.stdout) : undefined if (version) return { ...candidate, version } } catch (error: unknown) { + if (options.signal?.aborted) { + throw new LoopXCliError('aborted', 'operation was cancelled', false) + } if (error instanceof LoopXCliError && error.kind === 'aborted') throw error } } diff --git a/packages/dsh-loopx-plugin/tests/cli.spec.ts b/packages/dsh-loopx-plugin/tests/cli.spec.ts index 501aa07821..cb8357a553 100644 --- a/packages/dsh-loopx-plugin/tests/cli.spec.ts +++ b/packages/dsh-loopx-plugin/tests/cli.spec.ts @@ -16,6 +16,23 @@ const command: LoopXCommand = { } describe('runJsonCommand', () => { + it.each(['valid-version', 'nonzero-exit', 'rejected-probe'])( + 'honours cancellation at the %s probe completion boundary', async outcome => { + const controller = new AbortController() + let probes = 0 + await expect(resolveLoopXCommand({ + signal: controller.signal, env: { LOOPX_BIN: 'fixture-loopx' }, + runner: async () => { + probes += 1 + controller.abort() + if (outcome === 'rejected-probe') throw new Error('probe closed during cancellation') + return { exitCode: outcome === 'valid-version' ? 0 : 1, stdout: 'loopx 1.2.4\n', stderr: '' } + }, + })).rejects.toMatchObject({ kind: 'aborted', retryable: false }) + expect(probes).toBe(1) + }, + ) + it.each(['0.5.4', '1.2.3', '1.2.4rc1', '1.2.4.dev1', 'smoke', '1.2.4unknown'])( 'rejects unavailable or pre-fix CLI version %s', async version => { await expect(resolveLoopXCommand({ diff --git a/packages/dsh-loopx-plugin/tests/driver.spec.ts b/packages/dsh-loopx-plugin/tests/driver.spec.ts index 0d817256b8..624f84ea63 100644 --- a/packages/dsh-loopx-plugin/tests/driver.spec.ts +++ b/packages/dsh-loopx-plugin/tests/driver.spec.ts @@ -467,6 +467,31 @@ function makeDriver(fixture: RunnerFixture): LoopXContinuationDriver { } describe('same-session LoopX driver', () => { + it('does not report a cancelled maintenance probe as a missing CLI', async () => { + const host = fakeAgent() + const maintenance = new AbortController() + const warnings: string[] = [] + let probes = 0 + vi.spyOn(host.agent, 'runMaintenance').mockImplementation(task => task(maintenance.signal)) + const driver = new LoopXContinuationDriver({ + isLiveAgent: () => true, + runner: async () => { + probes += 1 + maintenance.abort() + return { exitCode: 1, stdout: '', stderr: '' } + }, + warn: message => warnings.push(message), + }) + try { + observeActivated(driver, host) + await waitFor(() => probes > 0) + await new Promise(resolve => setTimeout(resolve, 0)) + expect(warnings).toEqual([]) + expect(host.nextTurn).toHaveLength(0) + expect(probes).toBe(1) + } finally { await driver.dispose() } + }) + it('retries admission after attach-time maintenance becomes free without an idle event', async () => { const fixture = runnerFixture() const host = fakeAgent()