mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
23416e1577
Closes GAP-05, GAP-08, and final verification gate. - Linux ARM64 diagnostic (GAP-05): browser-engine-installer now probes os.platform + os.arch on Linux aarch64 and checks a prioritized list of system chromium locations (/usr/bin/chromium, chromium-browser, google-chrome, google-chrome-stable, /snap/bin/chromium). When a browser is found, the install result carries a PlatformHint with an exact VIBIUM_BROWSER_PATH export the user can copy. When none exists, the hint surfaces the `apt-get install chromium` remediation. Wired into phase 09 so it prints after vibium install succeeds. 5 new tests. - GAP-08: added explicit `vibium --headless viewport 1280 720` setup steps to yaml tc006/tc007 so the eval runs are as deterministic in CI as smoke-test.sh. Updated smoke-test.sh header to note the yaml is the canonical spec and the bash script is its mirror (covers the F1 vibium-missing contract case the yaml can't express cleanly). - Trust tier promoted 2 -> 3 now that (a) the eval is runnable via CommandEvalRunner, (b) CI runs both unit tests + smoke gate, and (c) all 11 dependent browser-using skills either reference qe-browser or document their intentional Playwright fallback. - ADR-091 status: Proposed -> Implemented (2026-04-21). - v3-adrs.md index reflects 80 Implemented / 6 In Progress / 3 Proposed. Verification: 643/643 tests pass across tests/unit/validation/, tests/unit/init/, tests/unit/scripts/qe-browser-*. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
268 lines
11 KiB
TypeScript
268 lines
11 KiB
TypeScript
/**
|
|
* Browser Engine Installer (Vibium) — unit tests
|
|
*
|
|
* TDD London school: we inject a spawner so tests never touch PATH, the
|
|
* network, or global npm. The spawner is a simple function that returns
|
|
* canned `SpawnSyncReturns` objects.
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
import type { SpawnSyncReturns } from 'node:child_process';
|
|
|
|
import {
|
|
installBrowserEngine,
|
|
detectVibium,
|
|
diagnosePlatform,
|
|
DEFAULT_VIBIUM_SPEC,
|
|
type Spawner,
|
|
type PlatformProbe,
|
|
} from '../../../src/init/browser-engine-installer.js';
|
|
|
|
function makePlatformProbe(overrides: Partial<PlatformProbe>): PlatformProbe {
|
|
return {
|
|
platform: () => 'linux' as NodeJS.Platform,
|
|
arch: () => 'x64',
|
|
existsSync: () => false,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
type MockCall = { bin: string; args: string[] };
|
|
|
|
function canned(overrides: Partial<SpawnSyncReturns<string>>): SpawnSyncReturns<string> {
|
|
return {
|
|
pid: 1,
|
|
status: 0,
|
|
signal: null,
|
|
stdout: '',
|
|
stderr: '',
|
|
output: [],
|
|
...overrides,
|
|
} as SpawnSyncReturns<string>;
|
|
}
|
|
|
|
function enoent(): SpawnSyncReturns<string> {
|
|
return canned({
|
|
status: null,
|
|
error: Object.assign(new Error('ENOENT'), { code: 'ENOENT' }),
|
|
});
|
|
}
|
|
|
|
function makeSpawner(
|
|
responses: (call: MockCall, index: number) => SpawnSyncReturns<string>
|
|
): { spawner: Spawner; calls: MockCall[] } {
|
|
const calls: MockCall[] = [];
|
|
const spawner: Spawner = (bin, args) => {
|
|
const call: MockCall = { bin, args };
|
|
calls.push(call);
|
|
return responses(call, calls.length - 1);
|
|
};
|
|
return { spawner, calls };
|
|
}
|
|
|
|
describe('browser-engine-installer', () => {
|
|
describe('detectVibium', () => {
|
|
it('extracts semver from stdout v-prefixed output', () => {
|
|
const { spawner, calls } = makeSpawner(() => canned({ stdout: 'v26.3.18\n' }));
|
|
const result = detectVibium(spawner);
|
|
expect(result).toBe('26.3.18');
|
|
expect(calls).toEqual([{ bin: 'vibium', args: ['--version'] }]);
|
|
});
|
|
|
|
it('returns null when vibium is not installed', () => {
|
|
const { spawner } = makeSpawner(() => enoent());
|
|
expect(detectVibium(spawner)).toBeNull();
|
|
});
|
|
|
|
it('returns "unknown" when vibium exits 0 with empty stdout AND empty stderr', () => {
|
|
const { spawner } = makeSpawner(() => canned({ stdout: ' \n', stderr: '' }));
|
|
expect(detectVibium(spawner)).toBe('unknown');
|
|
});
|
|
|
|
// H1 regression — devil's-advocate finding:
|
|
// Many Go CLIs (including some Vibium versions) write --version output to
|
|
// stderr. detectVibium used to read only stdout and silently report 'unknown'.
|
|
describe('H1 regression: stderr fallback', () => {
|
|
it('reads version from stderr when stdout is empty', () => {
|
|
const { spawner } = makeSpawner(() => canned({ stdout: '', stderr: 'v26.3.18\n' }));
|
|
expect(detectVibium(spawner)).toBe('26.3.18');
|
|
});
|
|
|
|
it('prefers stdout over stderr when both have content', () => {
|
|
const { spawner } = makeSpawner(() =>
|
|
canned({ stdout: 'v26.3.18\n', stderr: '26.0.0\n' })
|
|
);
|
|
expect(detectVibium(spawner)).toBe('26.3.18');
|
|
});
|
|
|
|
it('extracts semver from verbose output like "vibium version 26.3.18"', () => {
|
|
const { spawner } = makeSpawner(() => canned({ stdout: 'vibium version 26.3.18 (linux/amd64)\n' }));
|
|
expect(detectVibium(spawner)).toBe('26.3.18');
|
|
});
|
|
|
|
it('handles semver with prerelease tag', () => {
|
|
const { spawner } = makeSpawner(() => canned({ stdout: 'v27.0.0-rc.1\n' }));
|
|
expect(detectVibium(spawner)).toBe('27.0.0-rc.1');
|
|
});
|
|
|
|
it('falls back to first whitespace token when no semver match', () => {
|
|
const { spawner } = makeSpawner(() => canned({ stdout: 'unknown-build foo\n' }));
|
|
expect(detectVibium(spawner)).toBe('unknown-build');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('installBrowserEngine', () => {
|
|
it('returns skipped when options.skip is true', () => {
|
|
const result = installBrowserEngine({ skip: true });
|
|
expect(result.status).toBe('skipped');
|
|
// Default spec is version-pinned so `npm install -g` won't silently
|
|
// upgrade past the Vibium release we've verified against.
|
|
expect(result.packageSpec).toBe(DEFAULT_VIBIUM_SPEC);
|
|
expect(DEFAULT_VIBIUM_SPEC).toMatch(/^vibium@/);
|
|
});
|
|
|
|
it('returns already-installed when vibium is on PATH', () => {
|
|
const { spawner, calls } = makeSpawner(() => canned({ stdout: 'v26.3.18\n' }));
|
|
const result = installBrowserEngine({ spawner });
|
|
expect(result.status).toBe('already-installed');
|
|
// H1 fix: detectVibium now extracts the bare semver, dropping the v prefix.
|
|
expect(result.version).toBe('26.3.18');
|
|
// Only the detection call, no npm install attempt.
|
|
expect(calls).toHaveLength(1);
|
|
expect(calls[0]).toEqual({ bin: 'vibium', args: ['--version'] });
|
|
});
|
|
|
|
it('returns npm-unavailable when npm is missing', () => {
|
|
const { spawner, calls } = makeSpawner((call) => {
|
|
if (call.bin === 'vibium') return enoent();
|
|
if (call.bin === 'npm') return enoent();
|
|
throw new Error(`unexpected bin: ${call.bin}`);
|
|
});
|
|
const result = installBrowserEngine({ spawner });
|
|
expect(result.status).toBe('npm-unavailable');
|
|
expect(result.message).toMatch(/npm is not available/i);
|
|
// vibium check + npm check, no install attempt
|
|
expect(calls).toHaveLength(2);
|
|
});
|
|
|
|
it('installs via npm when vibium is missing but npm works', () => {
|
|
let vibiumChecks = 0;
|
|
const { spawner, calls } = makeSpawner((call) => {
|
|
if (call.bin === 'vibium') {
|
|
vibiumChecks += 1;
|
|
// First call: not installed. Second call (post-install): installed.
|
|
return vibiumChecks === 1 ? enoent() : canned({ stdout: 'v26.3.18' });
|
|
}
|
|
if (call.bin === 'npm' && call.args[0] === '--version') {
|
|
return canned({ stdout: '10.2.0' });
|
|
}
|
|
if (call.bin === 'npm' && call.args[0] === 'install') {
|
|
return canned({ stdout: 'added 1 package' });
|
|
}
|
|
throw new Error(`unexpected call: ${call.bin} ${call.args.join(' ')}`);
|
|
});
|
|
|
|
const result = installBrowserEngine({ spawner });
|
|
expect(result.status).toBe('installed');
|
|
// H1 fix: detectVibium now extracts the bare semver.
|
|
expect(result.version).toBe('26.3.18');
|
|
// vibium check, npm check, npm install, vibium re-check
|
|
expect(calls).toHaveLength(4);
|
|
expect(calls[2]).toEqual({ bin: 'npm', args: ['install', '-g', DEFAULT_VIBIUM_SPEC] });
|
|
});
|
|
|
|
it('returns install-failed when npm install exits non-zero', () => {
|
|
const { spawner } = makeSpawner((call) => {
|
|
if (call.bin === 'vibium') return enoent();
|
|
if (call.bin === 'npm' && call.args[0] === '--version') {
|
|
return canned({ stdout: '10.2.0' });
|
|
}
|
|
return canned({ status: 1, stderr: 'EACCES: permission denied' });
|
|
});
|
|
const result = installBrowserEngine({ spawner });
|
|
expect(result.status).toBe('install-failed');
|
|
expect(result.message).toContain('EACCES');
|
|
});
|
|
|
|
it('respects a custom packageSpec', () => {
|
|
const { spawner, calls } = makeSpawner(() => canned({ stdout: 'v26.3.18' }));
|
|
const result = installBrowserEngine({ spawner, packageSpec: 'vibium@26.3.18' });
|
|
expect(result.packageSpec).toBe('vibium@26.3.18');
|
|
// Already-installed detection path should not trigger npm install.
|
|
expect(calls).toHaveLength(1);
|
|
});
|
|
|
|
describe('Linux ARM64 platform hint (GAP-05)', () => {
|
|
it('attaches no platformHint on macOS (Chrome auto-download works)', () => {
|
|
const { spawner } = makeSpawner(() => canned({ stdout: 'v26.3.18' }));
|
|
const probe = makePlatformProbe({ platform: () => 'darwin', arch: () => 'arm64' });
|
|
const result = installBrowserEngine({ spawner, platformProbe: probe });
|
|
expect(result.platformHint).toBeUndefined();
|
|
});
|
|
|
|
it('attaches no platformHint on Linux x86_64', () => {
|
|
const { spawner } = makeSpawner(() => canned({ stdout: 'v26.3.18' }));
|
|
const probe = makePlatformProbe({ platform: () => 'linux', arch: () => 'x64' });
|
|
const result = installBrowserEngine({ spawner, platformProbe: probe });
|
|
expect(result.platformHint).toBeUndefined();
|
|
});
|
|
|
|
it('points Linux ARM64 at /usr/bin/chromium when present', () => {
|
|
const { spawner } = makeSpawner(() => canned({ stdout: 'v26.3.18' }));
|
|
const probe = makePlatformProbe({
|
|
platform: () => 'linux',
|
|
arch: () => 'arm64',
|
|
existsSync: (p) => p === '/usr/bin/chromium',
|
|
});
|
|
const result = installBrowserEngine({ spawner, platformProbe: probe });
|
|
expect(result.platformHint?.code).toBe('linux-arm64-system-chromium-found');
|
|
expect(result.platformHint?.browserPath).toBe('/usr/bin/chromium');
|
|
expect(result.platformHint?.message).toMatch(/VIBIUM_BROWSER_PATH=\/usr\/bin\/chromium/);
|
|
});
|
|
|
|
it('prefers /usr/bin/chromium over /usr/bin/google-chrome when both exist', () => {
|
|
// Priority order matters — chromium is the FOSS distro default,
|
|
// google-chrome is proprietary. Users who have both usually prefer
|
|
// chromium, and either works with Vibium.
|
|
const probe = makePlatformProbe({
|
|
platform: () => 'linux',
|
|
arch: () => 'arm64',
|
|
existsSync: () => true, // all candidate paths present
|
|
});
|
|
const hint = diagnosePlatform(probe);
|
|
expect(hint?.browserPath).toBe('/usr/bin/chromium');
|
|
});
|
|
|
|
it('emits the no-chromium hint when no system browser is found', () => {
|
|
const { spawner } = makeSpawner(() => canned({ stdout: 'v26.3.18' }));
|
|
const probe = makePlatformProbe({
|
|
platform: () => 'linux',
|
|
arch: () => 'arm64',
|
|
existsSync: () => false,
|
|
});
|
|
const result = installBrowserEngine({ spawner, platformProbe: probe });
|
|
expect(result.platformHint?.code).toBe('linux-arm64-no-system-chromium');
|
|
expect(result.platformHint?.browserPath).toBeUndefined();
|
|
expect(result.platformHint?.message).toMatch(/apt-get install chromium/);
|
|
});
|
|
});
|
|
|
|
it('uses a custom npm binary when provided', () => {
|
|
let vibiumChecks = 0;
|
|
const { spawner, calls } = makeSpawner((call) => {
|
|
if (call.bin === 'vibium') {
|
|
vibiumChecks += 1;
|
|
return vibiumChecks === 1 ? enoent() : canned({ stdout: 'v26.3.18' });
|
|
}
|
|
return canned({ stdout: 'ok' });
|
|
});
|
|
|
|
const result = installBrowserEngine({ spawner, npmBin: '/usr/local/bin/npm' });
|
|
expect(result.status).toBe('installed');
|
|
const npmCalls = calls.filter((c) => c.bin === '/usr/local/bin/npm');
|
|
expect(npmCalls).toHaveLength(2);
|
|
});
|
|
});
|
|
});
|