moving
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled

This commit is contained in:
Oleg Maslov
2026-09-02 10:10:29 +02:00
commit 0c3e2ead3b
3841 changed files with 970576 additions and 0 deletions

View File

@@ -0,0 +1,141 @@
import { EventEmitter } from 'node:events';
import type { spawn } from 'node:child_process';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
resolveShellEnv,
resolveShellEnvBestEffort,
resolvedShellPath,
mergePathValue,
__resetShellEnvStateForTests,
} from '../src/shell-env.js';
import { pathLookupEnv } from '../src/tool-detection.js';
/** Minimal ChildProcess double exposing only what shell-env consumes. */
function makeChild(): EventEmitter & { stdout: EventEmitter; kill: ReturnType<typeof vi.fn> } {
const child = new EventEmitter() as EventEmitter & {
stdout: EventEmitter;
kill: ReturnType<typeof vi.fn>;
};
child.stdout = new EventEmitter();
child.kill = vi.fn();
return child;
}
/** A spawn fake that emits an `env -0` payload then exits cleanly. */
function successSpawn(payload: string) {
return vi.fn(() => {
const child = makeChild();
queueMicrotask(() => {
child.stdout.emit('data', Buffer.from(payload));
child.emit('exit', 0);
});
return child;
}) as unknown as typeof spawn;
}
beforeEach(() => {
__resetShellEnvStateForTests();
});
afterEach(() => {
vi.useRealTimers();
__resetShellEnvStateForTests();
});
describe('resolveShellEnv', () => {
it('short-circuits to process env on win32 without spawning', async () => {
const spawnFn = vi.fn() as unknown as typeof spawn;
const result = await resolveShellEnv({ platform: 'win32', spawnFn });
expect(result.source).toBe('process');
expect(spawnFn).not.toHaveBeenCalled();
// process env values are all strings (undefined filtered out).
expect(Object.values(result.env).every((v) => typeof v === 'string')).toBe(true);
});
it('parses a posix env -0 payload into a login-shell result', async () => {
const spawnFn = successSpawn('PATH=/opt/homebrew/bin:/usr/bin\0FOO=bar\0');
const result = await resolveShellEnv({ platform: 'darwin', spawnFn });
expect(result.source).toBe('login-shell');
expect(result.env.PATH).toBe('/opt/homebrew/bin:/usr/bin');
expect(result.env.FOO).toBe('bar');
expect(spawnFn).toHaveBeenCalledTimes(1);
});
it('falls back to process env and arms a cooldown on timeout', async () => {
vi.useFakeTimers();
// Child never exits → both interactive and login-only spawns time out.
const spawnFn = vi.fn(() => makeChild()) as unknown as typeof spawn;
const pending = resolveShellEnv({ platform: 'darwin', spawnFn, timeoutMs: 100 });
await vi.advanceTimersByTimeAsync(100 + 2000); // interactive: SIGTERM + SIGKILL
await vi.advanceTimersByTimeAsync(100 + 2000); // login-only: SIGTERM + SIGKILL
const result = await pending;
expect(result.source).toBe('process');
expect(spawnFn).toHaveBeenCalledTimes(2);
// Within the 60s cooldown a subsequent resolve returns fallback without spawning.
(spawnFn as unknown as ReturnType<typeof vi.fn>).mockClear();
const again = await resolveShellEnv({ platform: 'darwin', spawnFn });
expect(again.source).toBe('process');
expect(spawnFn).not.toHaveBeenCalled();
});
it('is single-flight: concurrent callers share one spawn', async () => {
const spawnFn = successSpawn('PATH=/usr/bin\0');
const [a, b] = await Promise.all([
resolveShellEnv({ platform: 'darwin', spawnFn }),
resolveShellEnv({ platform: 'darwin', spawnFn }),
]);
expect(a.source).toBe('login-shell');
expect(b.source).toBe('login-shell');
expect(spawnFn).toHaveBeenCalledTimes(1);
});
it('caches a successful result for the process lifetime', async () => {
const spawnFn = successSpawn('PATH=/usr/bin\0');
await resolveShellEnv({ platform: 'darwin', spawnFn });
(spawnFn as unknown as ReturnType<typeof vi.fn>).mockClear();
const second = await resolveShellEnv({ platform: 'darwin', spawnFn });
expect(second.source).toBe('login-shell');
expect(spawnFn).not.toHaveBeenCalled();
// best-effort returns the cached login-shell result synchronously.
const be = resolveShellEnvBestEffort();
expect(be.source).toBe('login-shell');
expect(be.env.PATH).toBe('/usr/bin');
});
});
describe('mergePathValue', () => {
it('unions shell PATH ahead of base PATH and de-duplicates', () => {
expect(mergePathValue('/opt/homebrew/bin:/usr/bin', '/usr/bin:/bin')).toBe(
'/opt/homebrew/bin:/usr/bin:/bin',
);
});
it('tolerates an undefined base PATH', () => {
expect(mergePathValue('/usr/bin', undefined)).toBe('/usr/bin');
});
});
describe('detector PATH wiring', () => {
it('pathLookupEnv merges the resolved login-shell PATH on posix', async () => {
const spawnFn = successSpawn('PATH=/opt/homebrew/bin:/usr/local/bin\0');
await resolveShellEnv({ platform: 'darwin', spawnFn });
// Sanity: the resolver now exposes the login-shell PATH.
expect(resolvedShellPath()).toBe('/opt/homebrew/bin:/usr/local/bin');
const env = pathLookupEnv('darwin', { PATH: '/usr/bin' });
expect(env).toBeDefined();
expect(env?.PATH).toBe('/opt/homebrew/bin:/usr/local/bin:/usr/bin');
});
it('pathLookupEnv is a no-op on win32', () => {
expect(pathLookupEnv('win32', { PATH: '/usr/bin' })).toBeUndefined();
});
it('pathLookupEnv is a no-op when no login-shell PATH is resolved', () => {
// No resolve has run → best-effort returns process source → null shell PATH.
expect(pathLookupEnv('darwin', { PATH: '/usr/bin' })).toBeUndefined();
});
});