From f825666c5fa67480729c57cf4f37a659c80aa77b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sun, 4 Oct 2026 10:04:28 +0200 Subject: [PATCH] test: restore vi.stubEnv stubs automatically after every test Enable Vitest unstubEnvs for every project and the mutation lane through one TEST_ISOLATION constant, so env stubs never outlive the test that made them and a new project cannot leave it out. Move stubs that relied on persisting across tests into the tests that need them, and drop vi.unstubAllEnvs() calls that only undid vi.stubEnv. Global mock resets stay off: a bare vi.fn() would reset to returning undefined. --- .../src/recording/__tests__/overlay.test.ts | 1 - .../src/recording/swift-cache.test.ts | 1 - .../__tests__/run-script-http.test.ts | 1 - .../src/__tests__/doctor.test.ts | 1 - .../runner-startup-transport.test.ts | 1 - .../runner/__tests__/runner-transport.test.ts | 1 - .../src/agent-browser-tool.test.ts | 3 -- .../install-source-network-transport.test.ts | 1 - .../client-metro-auto-companion.test.ts | 1 - src/__tests__/cloud-connect-auth.test.ts | 1 - src/__tests__/cloud-connect-profile.test.ts | 1 - .../remote-connection-platform-axis.test.ts | 1 - src/__tests__/remote-connection.test.ts | 1 - src/__tests__/update-check.test.ts | 1 - src/cli/commands/__tests__/plugins.test.ts | 1 - .../__tests__/daemon-client-lifecycle.test.ts | 2 - .../daemon-client-newer-daemon.test.ts | 1 - .../daemon-client-policy-reuse.test.ts | 1 - .../daemon-client-startup-race.test.ts | 1 - .../command-tools-operator-inputs.test.ts | 40 +++++++++---------- .../__tests__/command-tools-parity.test.ts | 1 - vitest.config.ts | 6 ++- vitest.mutation.config.ts | 3 +- 23 files changed, 25 insertions(+), 47 deletions(-) diff --git a/packages/capture-kit/src/recording/__tests__/overlay.test.ts b/packages/capture-kit/src/recording/__tests__/overlay.test.ts index 47b1b34180..ec2a56cbee 100644 --- a/packages/capture-kit/src/recording/__tests__/overlay.test.ts +++ b/packages/capture-kit/src/recording/__tests__/overlay.test.ts @@ -48,7 +48,6 @@ beforeEach(() => { }); afterEach(() => { - vi.unstubAllEnvs(); fs.rmSync(tmpDir, { recursive: true, force: true }); }); diff --git a/packages/capture-kit/src/recording/swift-cache.test.ts b/packages/capture-kit/src/recording/swift-cache.test.ts index f04aea5913..8949f5583e 100644 --- a/packages/capture-kit/src/recording/swift-cache.test.ts +++ b/packages/capture-kit/src/recording/swift-cache.test.ts @@ -31,7 +31,6 @@ beforeEach(() => { }); afterEach(() => { - vi.unstubAllEnvs(); fs.rmSync(tmpDir, { recursive: true, force: true }); }); diff --git a/packages/maestro/src/daemon-port/__tests__/run-script-http.test.ts b/packages/maestro/src/daemon-port/__tests__/run-script-http.test.ts index a736c73d5d..4f121ee7fa 100644 --- a/packages/maestro/src/daemon-port/__tests__/run-script-http.test.ts +++ b/packages/maestro/src/daemon-port/__tests__/run-script-http.test.ts @@ -28,7 +28,6 @@ beforeEach(() => { }); afterEach(() => { - vi.unstubAllEnvs(); vi.unstubAllGlobals(); }); diff --git a/packages/platform-android/src/__tests__/doctor.test.ts b/packages/platform-android/src/__tests__/doctor.test.ts index 32a372f7ed..22aea044ac 100644 --- a/packages/platform-android/src/__tests__/doctor.test.ts +++ b/packages/platform-android/src/__tests__/doctor.test.ts @@ -42,7 +42,6 @@ import type { DoctorCheck } from '@agent-device/contracts/observability'; afterEach(() => { resetAndroidTestImeActivationCacheForTests(); - vi.unstubAllEnvs(); }); function fakeAdb(currentIme: string, previousIme = 'null'): AndroidAdbExecutor { diff --git a/packages/platform-apple/src/runner/__tests__/runner-startup-transport.test.ts b/packages/platform-apple/src/runner/__tests__/runner-startup-transport.test.ts index 8e94fa5842..c5b2a27d21 100644 --- a/packages/platform-apple/src/runner/__tests__/runner-startup-transport.test.ts +++ b/packages/platform-apple/src/runner/__tests__/runner-startup-transport.test.ts @@ -57,7 +57,6 @@ beforeEach(() => { afterEach(() => { vi.unstubAllGlobals(); - vi.unstubAllEnvs(); }); test('waitForRunner propagates request cancellation without fallback', async () => { diff --git a/packages/platform-apple/src/runner/__tests__/runner-transport.test.ts b/packages/platform-apple/src/runner/__tests__/runner-transport.test.ts index b6af37284e..d15a8ef03a 100644 --- a/packages/platform-apple/src/runner/__tests__/runner-transport.test.ts +++ b/packages/platform-apple/src/runner/__tests__/runner-transport.test.ts @@ -96,7 +96,6 @@ beforeEach(() => { afterEach(() => { vi.unstubAllGlobals(); - vi.unstubAllEnvs(); }); test('sendRunnerCommandOnce does not retry or simulator fallback after request failure', async () => { diff --git a/packages/platform-web/src/agent-browser-tool.test.ts b/packages/platform-web/src/agent-browser-tool.test.ts index fbc65051ff..0a97d854f7 100644 --- a/packages/platform-web/src/agent-browser-tool.test.ts +++ b/packages/platform-web/src/agent-browser-tool.test.ts @@ -209,7 +209,6 @@ for (const scenario of [ 'agent-browser@0.27.1', ]); } finally { - vi.unstubAllEnvs(); fs.rmSync(stateDir, { recursive: true, force: true }); } }); @@ -235,7 +234,6 @@ test('managed agent-browser setup reports an install that produced no entry', as ); }); } finally { - vi.unstubAllEnvs(); fs.rmSync(stateDir, { recursive: true, force: true }); } }); @@ -261,7 +259,6 @@ test('managed agent-browser setup gives the install lock back on every path out' ); }); } finally { - vi.unstubAllEnvs(); assert.deepEqual(lockPathsUnder(stateDir), []); fs.rmSync(stateDir, { recursive: true, force: true }); } diff --git a/packages/provision-kit/src/install-source-network-transport.test.ts b/packages/provision-kit/src/install-source-network-transport.test.ts index 3b219119d8..e3616b7e11 100644 --- a/packages/provision-kit/src/install-source-network-transport.test.ts +++ b/packages/provision-kit/src/install-source-network-transport.test.ts @@ -66,6 +66,5 @@ test('direct requests connect to the approved address through the real lookup', } } finally { await new Promise((resolve) => server.close(() => resolve())); - vi.unstubAllEnvs(); } }); diff --git a/src/__tests__/client-metro-auto-companion.test.ts b/src/__tests__/client-metro-auto-companion.test.ts index 6c0613db50..5ce9ef3a69 100644 --- a/src/__tests__/client-metro-auto-companion.test.ts +++ b/src/__tests__/client-metro-auto-companion.test.ts @@ -21,7 +21,6 @@ afterEach(() => { vi.useRealTimers(); vi.clearAllMocks(); vi.restoreAllMocks(); - vi.unstubAllEnvs(); }); test('prepareMetroRuntime starts the local companion only after bridge setup needs it', async () => { diff --git a/src/__tests__/cloud-connect-auth.test.ts b/src/__tests__/cloud-connect-auth.test.ts index 8a35d800eb..08cbcc9c1f 100644 --- a/src/__tests__/cloud-connect-auth.test.ts +++ b/src/__tests__/cloud-connect-auth.test.ts @@ -9,7 +9,6 @@ import type { AgentDeviceClient } from '../agent-device-client.ts'; import { mkdtempForTestSync } from './test-utils/tmp-dir.ts'; afterEach(() => { - vi.unstubAllEnvs(); vi.unstubAllGlobals(); vi.restoreAllMocks(); }); diff --git a/src/__tests__/cloud-connect-profile.test.ts b/src/__tests__/cloud-connect-profile.test.ts index 9b27be59a9..fd25bb2551 100644 --- a/src/__tests__/cloud-connect-profile.test.ts +++ b/src/__tests__/cloud-connect-profile.test.ts @@ -35,7 +35,6 @@ vi.mock('../provider-webdriver.ts', () => ({ afterEach(() => { vi.clearAllMocks(); vi.unstubAllGlobals(); - vi.unstubAllEnvs(); }); const mockedResolveCloudAccessForConnect = vi.mocked(resolveCloudAccessForConnect); diff --git a/src/__tests__/remote-connection-platform-axis.test.ts b/src/__tests__/remote-connection-platform-axis.test.ts index 02b16927be..d81737ba0f 100644 --- a/src/__tests__/remote-connection-platform-axis.test.ts +++ b/src/__tests__/remote-connection-platform-axis.test.ts @@ -27,7 +27,6 @@ import { readRemoteConnectionState } from '../remote/remote-connection-state.ts' afterEach(() => { vi.clearAllMocks(); vi.restoreAllMocks(); - vi.unstubAllEnvs(); }); test('proxy install against an iOS-bound connection is not refused as a platform change', async () => { diff --git a/src/__tests__/remote-connection.test.ts b/src/__tests__/remote-connection.test.ts index c97a819e47..66d1e62727 100644 --- a/src/__tests__/remote-connection.test.ts +++ b/src/__tests__/remote-connection.test.ts @@ -48,7 +48,6 @@ import type { AgentDeviceClient } from '../agent-device-client.ts'; afterEach(() => { vi.clearAllMocks(); vi.restoreAllMocks(); - vi.unstubAllEnvs(); }); test('deferred Metro config ignores perf-style kind values', () => { diff --git a/src/__tests__/update-check.test.ts b/src/__tests__/update-check.test.ts index 58e2baf6b7..0c214f73b6 100644 --- a/src/__tests__/update-check.test.ts +++ b/src/__tests__/update-check.test.ts @@ -46,7 +46,6 @@ beforeEach(() => { afterEach(() => { vi.restoreAllMocks(); - vi.unstubAllEnvs(); vi.useRealTimers(); Object.defineProperty(process.stderr, 'isTTY', { configurable: true, diff --git a/src/cli/commands/__tests__/plugins.test.ts b/src/cli/commands/__tests__/plugins.test.ts index dab8e1b2fd..81825bd4ba 100644 --- a/src/cli/commands/__tests__/plugins.test.ts +++ b/src/cli/commands/__tests__/plugins.test.ts @@ -12,7 +12,6 @@ vi.mock('../../../provider-device-runtimes.ts', () => { afterEach(() => { vi.restoreAllMocks(); - vi.unstubAllEnvs(); }); test('plugins list and remove route through the CLI and emits JSON without daemon access or plugin evaluation', async () => { diff --git a/src/daemon-client/__tests__/daemon-client-lifecycle.test.ts b/src/daemon-client/__tests__/daemon-client-lifecycle.test.ts index 91f4da210a..4ad2f6e8c8 100644 --- a/src/daemon-client/__tests__/daemon-client-lifecycle.test.ts +++ b/src/daemon-client/__tests__/daemon-client-lifecycle.test.ts @@ -59,7 +59,6 @@ afterEach(() => { mockRunCmdDetached.mockReset(); mockRunCmdSync.mockClear(); mockSleep.mockClear(); - vi.unstubAllEnvs(); }); function makeTempStateDir(prefix: string): string { @@ -503,7 +502,6 @@ test('sendToDaemon does not reuse reachable daemon metadata with mismatched vers await closeLoopbackServer(staleDaemon.server); await closeLoopbackServer(freshDaemon.server); fs.rmSync(stateDir, { recursive: true, force: true }); - vi.unstubAllEnvs(); } } }); diff --git a/src/daemon-client/__tests__/daemon-client-newer-daemon.test.ts b/src/daemon-client/__tests__/daemon-client-newer-daemon.test.ts index 4229f8cc79..368d7a4755 100644 --- a/src/daemon-client/__tests__/daemon-client-newer-daemon.test.ts +++ b/src/daemon-client/__tests__/daemon-client-newer-daemon.test.ts @@ -28,7 +28,6 @@ const mockRunCmdDetached = vi.mocked(runCmdDetachedMonitored); afterEach(() => { mockRunCmdDetached.mockReset(); - vi.unstubAllEnvs(); }); test('sendToDaemon refuses to replace a reachable daemon newer than the client', async (t) => { diff --git a/src/daemon-client/__tests__/daemon-client-policy-reuse.test.ts b/src/daemon-client/__tests__/daemon-client-policy-reuse.test.ts index 62f17014e8..f6e510494f 100644 --- a/src/daemon-client/__tests__/daemon-client-policy-reuse.test.ts +++ b/src/daemon-client/__tests__/daemon-client-policy-reuse.test.ts @@ -28,7 +28,6 @@ const mockSpawnDaemon = vi.mocked(runCmdDetachedMonitored); afterEach(() => { mockSpawnDaemon.mockReset(); - vi.unstubAllEnvs(); }); test('a caller naming a policy refuses a running daemon without that policy', async (t) => { diff --git a/src/daemon-client/__tests__/daemon-client-startup-race.test.ts b/src/daemon-client/__tests__/daemon-client-startup-race.test.ts index d52ba0e6c2..0a094c07ab 100644 --- a/src/daemon-client/__tests__/daemon-client-startup-race.test.ts +++ b/src/daemon-client/__tests__/daemon-client-startup-race.test.ts @@ -62,7 +62,6 @@ afterEach(() => { mockRunCmdDetached.mockReset(); mockSleep.mockReset(); mockSleep.mockImplementation(async () => {}); - vi.unstubAllEnvs(); }); /** The code signature this client stamps on, and expects of, a daemon it may reuse. */ diff --git a/src/mcp/__tests__/command-tools-operator-inputs.test.ts b/src/mcp/__tests__/command-tools-operator-inputs.test.ts index 91b2633b0d..58d1eae43f 100644 --- a/src/mcp/__tests__/command-tools-operator-inputs.test.ts +++ b/src/mcp/__tests__/command-tools-operator-inputs.test.ts @@ -220,26 +220,22 @@ test('MCP refuses any argument the advertised schema does not list', async () => test('MCP still resolves operator env values outside the model-writable surface', async () => { vi.stubEnv('AGENT_DEVICE_DAEMON_AUTH_TOKEN', 'operator-env-token'); vi.stubEnv('AGENT_DEVICE_STATE_DIR', '/operator/state-dir'); - try { - const createdConfigs: Array> = []; - const calls: Array<{ name: string; input: Record }> = []; - const executor = createCommandToolExecutor({ - createClient: (config) => { - createdConfigs.push(config as Record); - return {} as AgentDeviceClient; - }, - runCommand: async (_client, name, input) => { - calls.push({ name, input: input as Record }); - return {}; - }, - }); - - const result = await executor.execute('wait', {}); - - assert.equal(result.isError, false); - assert.equal(calls[0]?.input.daemonAuthToken, 'operator-env-token'); - assert.equal(createdConfigs[0]?.stateDir, '/operator/state-dir'); - } finally { - vi.unstubAllEnvs(); - } + const createdConfigs: Array> = []; + const calls: Array<{ name: string; input: Record }> = []; + const executor = createCommandToolExecutor({ + createClient: (config) => { + createdConfigs.push(config as Record); + return {} as AgentDeviceClient; + }, + runCommand: async (_client, name, input) => { + calls.push({ name, input: input as Record }); + return {}; + }, + }); + + const result = await executor.execute('wait', {}); + + assert.equal(result.isError, false); + assert.equal(calls[0]?.input.daemonAuthToken, 'operator-env-token'); + assert.equal(createdConfigs[0]?.stateDir, '/operator/state-dir'); }); diff --git a/src/mcp/__tests__/command-tools-parity.test.ts b/src/mcp/__tests__/command-tools-parity.test.ts index 113ffcc363..8c9de97e49 100644 --- a/src/mcp/__tests__/command-tools-parity.test.ts +++ b/src/mcp/__tests__/command-tools-parity.test.ts @@ -12,7 +12,6 @@ import { validateAgainstSchema } from './output-schema-validator.ts'; import { mkdtempForTestSync } from '../../__tests__/test-utils/tmp-dir.ts'; afterEach(() => { - vi.unstubAllEnvs(); if (temporaryDirectory) { fs.rmSync(temporaryDirectory, { recursive: true, force: true }); temporaryDirectory = undefined; diff --git a/vitest.config.ts b/vitest.config.ts index 1b4390fc19..5e42d9810e 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -58,6 +58,10 @@ export const MUTATION_EXCLUDED_TESTS: readonly string[] = [ ...FUZZ_WORKER_TESTS, ]; +// Env stubs never outlive the test that made them. Applied to every project below and imported by +// vitest.mutation.config.ts, so no lane can leave it out. +export const TEST_ISOLATION = { unstubEnvs: true } as const; + // Imported by vitest.mutation.config.ts so the two lanes cannot drift: a guard // added here must reach the Stryker sandbox too. export const SETUP_FILES = [ @@ -245,7 +249,7 @@ export default defineConfig({ setupFiles: SETUP_FILES, }, }, - ], + ].map((project) => ({ ...project, test: { ...TEST_ISOLATION, ...project.test } })), coverage: { provider: 'v8', reporter: ['text', 'html', 'lcov', 'json-summary'], diff --git a/vitest.mutation.config.ts b/vitest.mutation.config.ts index c02d3136d4..24ab16fb98 100644 --- a/vitest.mutation.config.ts +++ b/vitest.mutation.config.ts @@ -3,7 +3,7 @@ import { fileURLToPath } from 'node:url'; import { defineConfig } from 'vitest/config'; import { readTestScope, threadHostileTestFiles } from './scripts/mutation/test-scope.ts'; import { workspaceSourceAliases } from './scripts/mutation/workspace-aliases.ts'; -import { MUTATION_EXCLUDED_TESTS, SETUP_FILES } from './vitest.config.ts'; +import { MUTATION_EXCLUDED_TESTS, SETUP_FILES, TEST_ISOLATION } from './vitest.config.ts'; const repoRoot = path.dirname(fileURLToPath(import.meta.url)); @@ -27,5 +27,6 @@ export default defineConfig({ '**/node_modules/**', ], setupFiles: [...SETUP_FILES], + ...TEST_ISOLATION, }, });