Files
stack/packages/mosaic/__tests__/integration/unified-wizard.test.ts
jason.woltje b96cc7982a
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
fix(wizard): report gateway failures before success summary (#691)
2026-06-25 18:14:40 +00:00

188 lines
6.6 KiB
TypeScript

/**
* Unified wizard integration test — exercises the `skipGateway: false` code
* path so that wiring between `runWizard` and the two gateway stages is
* covered. The gateway stages themselves are mocked (they require a real
* daemon + network) but the dynamic imports and option plumbing are real.
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, rmSync, cpSync, existsSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { HeadlessPrompter } from '../../src/prompter/headless-prompter.js';
import { createConfigService } from '../../src/config/config-service.js';
const gatewayConfigMock = vi.fn();
const gatewayBootstrapMock = vi.fn();
vi.mock('../../src/stages/gateway-config.js', () => ({
gatewayConfigStage: (...args: unknown[]) => gatewayConfigMock(...args),
}));
vi.mock('../../src/stages/gateway-bootstrap.js', () => ({
gatewayBootstrapStage: (...args: unknown[]) => gatewayBootstrapMock(...args),
}));
// Import AFTER the mocks so runWizard picks up the mocked stage modules.
import { runWizard } from '../../src/wizard.js';
describe('Unified wizard (runWizard with default skipGateway)', () => {
let tmpDir: string;
const repoRoot = join(import.meta.dirname, '..', '..');
const originalIsTTY = process.stdin.isTTY;
const originalAssumeYes = process.env['MOSAIC_ASSUME_YES'];
beforeEach(() => {
tmpDir = mkdtempSync(join(tmpdir(), 'mosaic-unified-wizard-'));
const candidates = [join(repoRoot, 'framework', 'templates'), join(repoRoot, 'templates')];
for (const templatesDir of candidates) {
if (existsSync(templatesDir)) {
cpSync(templatesDir, join(tmpDir, 'templates'), { recursive: true });
break;
}
}
gatewayConfigMock.mockReset();
gatewayBootstrapMock.mockReset();
// Pretend we're on an interactive TTY so the wizard's headless-abort
// branch does not call `process.exit(1)` during these tests.
Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true });
delete process.env['MOSAIC_ASSUME_YES'];
});
afterEach(() => {
rmSync(tmpDir, { recursive: true, force: true });
Object.defineProperty(process.stdin, 'isTTY', {
value: originalIsTTY,
configurable: true,
});
if (originalAssumeYes === undefined) {
delete process.env['MOSAIC_ASSUME_YES'];
} else {
process.env['MOSAIC_ASSUME_YES'] = originalAssumeYes;
}
});
it('invokes the gateway config + bootstrap stages by default', async () => {
gatewayConfigMock.mockResolvedValue({ ready: true, host: 'localhost', port: 14242 });
gatewayBootstrapMock.mockResolvedValue({ completed: true });
const prompter = new HeadlessPrompter({
'Installation mode': 'quick',
'What name should agents use?': 'TestBot',
'Communication style': 'direct',
'Your name': 'Tester',
'Your pronouns': 'They/Them',
'Your timezone': 'UTC',
});
await runWizard({
mosaicHome: tmpDir,
sourceDir: tmpDir,
prompter,
configService: createConfigService(tmpDir, tmpDir),
gatewayHost: 'localhost',
gatewayPort: 14242,
skipGatewayNpmInstall: true,
});
expect(gatewayConfigMock).toHaveBeenCalledTimes(1);
expect(gatewayBootstrapMock).toHaveBeenCalledTimes(1);
const configCall = gatewayConfigMock.mock.calls[0];
expect(configCall[2]).toMatchObject({
host: 'localhost',
defaultPort: 14242,
skipInstall: true,
});
const bootstrapCall = gatewayBootstrapMock.mock.calls[0];
expect(bootstrapCall[2]).toMatchObject({ host: 'localhost', port: 14242 });
});
it('prints the success summary only after gateway health succeeds', async () => {
gatewayConfigMock.mockImplementation(async (p: HeadlessPrompter) => {
p.log('Gateway is healthy.');
return { ready: true, host: 'localhost', port: 14242 };
});
gatewayBootstrapMock.mockResolvedValue({ completed: true });
const prompter = new HeadlessPrompter({
'Installation mode': 'quick',
'What name should agents use?': 'TestBot',
'Communication style': 'direct',
'Your name': 'Tester',
'Your pronouns': 'They/Them',
'Your timezone': 'UTC',
});
await runWizard({
mosaicHome: tmpDir,
sourceDir: tmpDir,
prompter,
configService: createConfigService(tmpDir, tmpDir),
skipGatewayNpmInstall: true,
});
const logs = prompter.getLogs();
const healthIndex = logs.findIndex((line) => line.includes('Gateway is healthy.'));
const summaryIndex = logs.findIndex((line) => line.includes('Installation Summary'));
const readyIndex = logs.findIndex((line) => line.includes('Mosaic is ready.'));
expect(healthIndex).toBeGreaterThanOrEqual(0);
expect(summaryIndex).toBeGreaterThan(healthIndex);
expect(readyIndex).toBeGreaterThan(summaryIndex);
});
it('does not claim success when gateway health reports not ready', async () => {
gatewayConfigMock.mockImplementation(async (p: HeadlessPrompter) => {
p.warn('Gateway did not become healthy within 30 seconds.');
return { ready: false };
});
const prompter = new HeadlessPrompter({
'Installation mode': 'quick',
'What name should agents use?': 'TestBot',
'Communication style': 'direct',
'Your name': 'Tester',
'Your pronouns': 'They/Them',
'Your timezone': 'UTC',
});
await runWizard({
mosaicHome: tmpDir,
sourceDir: tmpDir,
prompter,
configService: createConfigService(tmpDir, tmpDir),
skipGatewayNpmInstall: true,
});
const logs = prompter.getLogs();
expect(logs.some((line) => line.includes('Gateway did not become healthy'))).toBe(true);
expect(logs.some((line) => line.includes('Installation Summary'))).toBe(false);
expect(logs.some((line) => line.includes('Mosaic is ready.'))).toBe(false);
expect(gatewayConfigMock).toHaveBeenCalledTimes(1);
expect(gatewayBootstrapMock).not.toHaveBeenCalled();
});
it('respects skipGateway: true', async () => {
const prompter = new HeadlessPrompter({
'Installation mode': 'quick',
'What name should agents use?': 'TestBot',
'Communication style': 'direct',
'Your name': 'Tester',
'Your pronouns': 'They/Them',
'Your timezone': 'UTC',
});
await runWizard({
mosaicHome: tmpDir,
sourceDir: tmpDir,
prompter,
configService: createConfigService(tmpDir, tmpDir),
skipGateway: true,
});
expect(gatewayConfigMock).not.toHaveBeenCalled();
expect(gatewayBootstrapMock).not.toHaveBeenCalled();
});
});