Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions src/__tests__/hermetic-env-setup.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { afterEach, test, vi } from 'vitest';
import assert from 'node:assert/strict';
import vitestConfig from '../../vitest.config.ts';

const HERMETIC_ENV_SETUP = 'src/__tests__/hermetic-env-setup.ts';
const AMBIENT_DAEMON_VARS = [
'AGENT_DEVICE_DAEMON_BASE_URL',
'AGENT_DEVICE_DAEMON_AUTH_TOKEN',
] as const;

type ProjectShape = { test?: { name?: string; setupFiles?: readonly string[] } };

// Wiring: the scrub only helps if every project loads it as a setup file. CI runs with the
// vars unset, so a dropped wiring is otherwise invisible — assert it structurally instead.
test('every vitest project wires the hermetic-env setup', () => {
const projects = (vitestConfig.test?.projects ?? []) as unknown as ReadonlyArray<ProjectShape>;
assert.ok(projects.length > 0, 'expected configured vitest projects');
for (const project of projects) {
const name = project.test?.name ?? '(unnamed)';
const setupFiles = project.test?.setupFiles ?? [];
assert.ok(
setupFiles.includes(HERMETIC_ENV_SETUP),
`project "${name}" must wire ${HERMETIC_ENV_SETUP} in setupFiles`,
);
}
});

// Behavior: re-import a fresh copy of the setup module with the daemon vars set, so the scrub
// is exercised on any host (CI included, where the vars are otherwise absent).
afterEach(() => {
for (const name of AMBIENT_DAEMON_VARS) delete process.env[name];
});

test('importing hermetic-env-setup scrubs the ambient daemon connection vars', async () => {
for (const name of AMBIENT_DAEMON_VARS) process.env[name] = 'leaked-from-host';
vi.resetModules();
await import('./hermetic-env-setup.ts');
for (const name of AMBIENT_DAEMON_VARS) {
assert.equal(process.env[name], undefined, `${name} must be scrubbed when the setup loads`);
}
});
23 changes: 23 additions & 0 deletions src/__tests__/hermetic-env-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Unit tests must be hermetic with respect to the host's daemon-connection
// environment. A machine actually running agent-device — including this repo's
// own remote dev containers — exports AGENT_DEVICE_DAEMON_BASE_URL and
// AGENT_DEVICE_DAEMON_AUTH_TOKEN pointing at a live daemon. Production
// flag-default resolution folds those into every command's input and connection
// config (resolveConfigBackedFlagDefaults -> readEnvFlagDefaults, and the daemon
// client's own env fallbacks), so a configured host silently diverges from CI:
// tests that assert an exact command input/config shape gain phantom
// daemonBaseUrl/daemonAuthToken keys, and daemon-client tests take the remote
// path ("Remote daemon is unavailable") instead of the local one they exercise.
//
// CI runs with these unset. Delete them here so a configured host matches CI.
// Tests that genuinely need them assign their own value or pass an explicit env
// object; that happens inside the test, after this module has loaded, so this
// scrub does not interfere.
const AMBIENT_DAEMON_ENV_VARS = [
'AGENT_DEVICE_DAEMON_BASE_URL',
'AGENT_DEVICE_DAEMON_AUTH_TOKEN',
] as const;

for (const name of AMBIENT_DAEMON_ENV_VARS) {
delete process.env[name];
}
31 changes: 24 additions & 7 deletions vitest.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { defineConfig } from 'vitest/config';
import slowTestGateReporter from './scripts/vitest-slow-test-reporter.ts';

// Tests that stub a real binary (adb/xcrun) by mutating process.env.PATH and
// Tests that stub a real binary (adb/xcrun/npx) by mutating process.env.PATH and
// then spawn it, so each case waits real subprocess/retry/poll time. Run at the
// unit suite's default ~7x file parallelism they contend for CPU and their stub
// spawns get starved past an internal budget, so production takes a generic
Expand All @@ -14,6 +14,8 @@ const SUBPROCESS_STUB_TESTS = [
'src/platforms/android/__tests__/{app-lifecycle-install,app-lifecycle-open,device-input-state,input-actions,notifications,settings}.test.ts',
'src/daemon/__tests__/runtime-hints.test.ts',
'src/platforms/apple/core/__tests__/index.test.ts',
// Stubs npx + the package managers on PATH and spawns a real Metro dev server per case.
'src/__tests__/client-metro.test.ts',
];

export default defineConfig({
Expand All @@ -40,19 +42,25 @@ export default defineConfig({
// job (scripts/maestro-conformance), like the layering guard.
],
exclude: SUBPROCESS_STUB_TESTS,
setupFiles: ['src/__tests__/process-memo-setup.ts'],
setupFiles: [
'src/__tests__/hermetic-env-setup.ts',
'src/__tests__/process-memo-setup.ts',
],
},
},
{
// The subprocess-stub tests stub adb/xcrun by mutating process.env
// The subprocess-stub tests stub adb/xcrun/npx by mutating process.env
// (PATH, AGENT_DEVICE_TEST_ARGS_FILE) and wait real subprocess/retry/poll
// time, so the group runs serialized with per-file isolation — the same
// execution contract the pre-split android index.test.ts aggregation
// provided without leaking module caches between split files.
test: {
name: 'subprocess-stub',
include: SUBPROCESS_STUB_TESTS,
setupFiles: ['src/__tests__/process-memo-setup.ts'],
setupFiles: [
'src/__tests__/hermetic-env-setup.ts',
'src/__tests__/process-memo-setup.ts',
],
fileParallelism: false,
isolate: true,
maxWorkers: 1,
Expand All @@ -62,21 +70,30 @@ export default defineConfig({
test: {
name: 'provider-integration',
include: ['test/integration/provider-scenarios/**/*.test.ts'],
setupFiles: ['src/__tests__/process-memo-setup.ts'],
setupFiles: [
'src/__tests__/hermetic-env-setup.ts',
'src/__tests__/process-memo-setup.ts',
],
},
},
{
test: {
name: 'interaction-contract',
include: ['test/integration/interaction-contract/**/*.test.ts'],
setupFiles: ['src/__tests__/process-memo-setup.ts'],
setupFiles: [
'src/__tests__/hermetic-env-setup.ts',
'src/__tests__/process-memo-setup.ts',
],
},
},
{
test: {
name: 'output-economy',
include: ['test/output-economy/**/*.test.ts'],
setupFiles: ['src/__tests__/process-memo-setup.ts'],
setupFiles: [
'src/__tests__/hermetic-env-setup.ts',
'src/__tests__/process-memo-setup.ts',
],
},
},
],
Expand Down
Loading