diff --git a/src/utils/user.test.ts b/src/utils/user.test.ts index 709f53cb39..3b040da62d 100644 --- a/src/utils/user.test.ts +++ b/src/utils/user.test.ts @@ -9,6 +9,18 @@ import * as realConfig from './config.js' import * as realEnv from './env.js' import * as realEnvUtils from './envUtils.js' +// Snapshot each real module surface into a plain object BEFORE any +// mock.module call. `import * as` gives a live namespace: mock.module +// repoints it, so restoring with the namespace itself re-installs the mock +// rather than the real module -- permanently, since mock.module lasts for the +// life of the process. That is how this suite's stderr-less `execa` stub used +// to escape into every later suite, where `result.stderr.trim()` then threw. +const realAuthSnapshot = { ...realAuth } +const realConfigSnapshot = { ...realConfig } +const realEnvSnapshot = { ...realEnv } +const realEnvUtilsSnapshot = { ...realEnvUtils } +const realExecaSnapshot = { ...realExeca } + const originalEnv = { ...process.env } const originalMacro = (globalThis as Record).MACRO @@ -18,16 +30,18 @@ async function importFreshUserModule() { async function importActualUserTestDeps() { const nonce = `${Date.now()}-${Math.random()}` - const [authModule, configModule, execaModule] = await Promise.all([ + const [authModule, configModule] = await Promise.all([ import(`./auth.js?ts=${nonce}`), import(`./config.js?ts=${nonce}`), - import('execa'), ]) + // execa comes from the pre-mock snapshot: a plain `import('execa')` here + // resolves to whatever mock is currently installed, so spreading it would + // build each new stub on top of the previous one. return { authModule, configModule, - execaModule, + execaModule: realExecaSnapshot, } } @@ -64,13 +78,13 @@ async function installCommonMocks(options?: { })) mock.module('./env.js', () => ({ - ...realEnv, - env: { platform: 'windows' }, - getHostPlatformForAnalytics: () => 'windows', + ...realEnvSnapshot, + env: { platform: 'win32' }, + getHostPlatformForAnalytics: () => 'win32', })) mock.module('./envUtils.js', () => ({ - ...realEnvUtils, + ...realEnvUtilsSnapshot, isEnvTruthy: (value: string | undefined) => !!value && value !== '0' && value.toLowerCase() !== 'false', })) @@ -80,6 +94,7 @@ async function installCommonMocks(options?: { execa: async () => ({ exitCode: options?.gitEmail ? 0 : 1, stdout: options?.gitEmail ?? '', + stderr: '', }), execaSync: () => ({ exitCode: 1, @@ -97,11 +112,11 @@ beforeEach(async () => { afterEach(() => { try { mock.restore() - mock.module('./auth.js', () => realAuth) - mock.module('./config.js', () => realConfig) - mock.module('./env.js', () => realEnv) - mock.module('./envUtils.js', () => realEnvUtils) - mock.module('execa', () => realExeca) + mock.module('./auth.js', () => realAuthSnapshot) + mock.module('./config.js', () => realConfigSnapshot) + mock.module('./env.js', () => realEnvSnapshot) + mock.module('./envUtils.js', () => realEnvUtilsSnapshot) + mock.module('execa', () => realExecaSnapshot) process.env = { ...originalEnv } if (originalMacro === undefined) { delete (globalThis as Record).MACRO diff --git a/tests/sdk/query-lifecycle.test.ts b/tests/sdk/query-lifecycle.test.ts index 27e25ab64f..1292ca0a31 100644 --- a/tests/sdk/query-lifecycle.test.ts +++ b/tests/sdk/query-lifecycle.test.ts @@ -238,36 +238,6 @@ describe('Query interrupt lifecycle', () => { expect(userCancelMsg).toBeUndefined() }, 10_000) - test('interrupt() with reason undefined yields synthetic user cancellation message', async () => { - const q = query({ - prompt: 'test undefined reason', - options: { cwd: process.cwd() }, - }) - const iterator = q[Symbol.asyncIterator]() - const firstPromise = iterator.next() - - // Interrupt during execution - q.interrupt() - - const messages: any[] = [] - try { - let result = await firstPromise - while (!result.done) { - messages.push(result.value) - result = await iterator.next() - } - } catch (err) { - if (!isExpectedDrainAbort(err)) throw err - } - - const userCancelMsg = messages.find((m: any) => - m.type === 'user' && - Array.isArray(m.message?.content) && - m.message.content[0]?.text === '[Request interrupted by user]' - ) - expect(userCancelMsg).toBeDefined() - }, 10_000) - test('Stop hook regression test - aborting with reason interrupt suppresses cancellation message', async () => { const { query: queryLoop } = await import('../../src/query.js') const { getDefaultAppState } = await import('../../src/state/AppStateStore.js')