Batch 2 (installer). Root causes and fixes: 1. config-manager tests asserted stale layout (<path>/config/config.json); canonical implementation writes flat <path>/config.json matching the Docker volume-mount contract + REQUIRED_DIRS. Tests aligned to the production contract (not the other way) + create testDir in beforeEach. 2. saveConfig ENOENT when the install dir didn't exist: now mkdirs the parent before writing (implementation fix; wizard passes user-typed paths). 3. REAL BUG: saveDNSCredentials/loadDNSCredentials silently dropped the tld field on round-trip (property test caught it). Now persisted plaintext (non-secret, like server/username) and restored on load; type-normalized to string-or-null (judge polish #4). 4. installDocker/installCaddy unit tests hit the real network via DownloadManager (only child_process mocked) -> 5s timeouts. Now jest.mock('./download-manager') with fail-fast stubs. 5. dependency-checker.property.test.js ran real exec/downloads (caddy is installed on this box). Now hermetic: child_process + download-manager mocked at file scope. 6. installDocker fallback message parroted raw downloader error; now steers user to the manual instructions returned alongside (satisfies the test contract AND improves UX). Also: mkdtemp test dirs (judge polish #1), nested-mkdir regression test (polish #7). Installer suite: 119/119 green, 7/7 suites (was 17 failed / 4 suites red). Judge: Qwen lane grade A, 0 blocking, 8 polish (1,3,4,7 applied here), verdict /tmp/judge-batch2-verdict.json.
287 lines
9.7 KiB
JavaScript
287 lines
9.7 KiB
JavaScript
const DependencyChecker = require('./dependency-checker');
|
|
const { exec } = require('child_process');
|
|
|
|
// Mock child_process
|
|
jest.mock('child_process');
|
|
|
|
// Mock DownloadManager: installDocker/installCaddy construct it inline and
|
|
// would otherwise hit the real network (docker.com / GitHub releases),
|
|
// hanging past jest's 5s per-test timeout. Every network/installer operation
|
|
// is stubbed to fail fast so the code under test exercises its
|
|
// download-failed → manual-instructions fallback paths deterministically.
|
|
jest.mock('./download-manager', () => {
|
|
return jest.fn().mockImplementation(() => ({
|
|
downloadDocker: jest.fn().mockResolvedValue({
|
|
success: false,
|
|
message: 'Download unavailable in test environment'
|
|
}),
|
|
downloadCaddy: jest.fn().mockResolvedValue({
|
|
success: false,
|
|
message: 'Download unavailable in test environment'
|
|
}),
|
|
extractCaddy: jest.fn().mockResolvedValue({
|
|
success: false,
|
|
message: 'Extraction unavailable in test environment'
|
|
}),
|
|
cleanup: jest.fn().mockResolvedValue(undefined)
|
|
}));
|
|
});
|
|
|
|
describe('DependencyChecker', () => {
|
|
let checker;
|
|
|
|
beforeEach(() => {
|
|
checker = new DependencyChecker();
|
|
jest.clearAllMocks();
|
|
// Default: commands fail (prevents hanging when unmocked calls occur)
|
|
exec.mockImplementation((cmd, opts, callback) => {
|
|
callback(new Error('command not found'), { stdout: '', stderr: 'command not found' });
|
|
});
|
|
});
|
|
|
|
describe('checkDocker', () => {
|
|
test('detects Docker installed and running', async () => {
|
|
// Mock successful docker --version
|
|
exec.mockImplementationOnce((cmd, opts, callback) => {
|
|
callback(null, { stdout: 'Docker version 24.0.7, build afdd53b', stderr: '' });
|
|
});
|
|
|
|
// Mock successful docker info
|
|
exec.mockImplementationOnce((cmd, opts, callback) => {
|
|
callback(null, { stdout: 'Server Version: 24.0.7', stderr: '' });
|
|
});
|
|
|
|
const result = await checker.checkDocker();
|
|
|
|
expect(result.installed).toBe(true);
|
|
expect(result.running).toBe(true);
|
|
expect(result.version).toBe('24.0.7');
|
|
});
|
|
|
|
test('detects Docker installed but not running', async () => {
|
|
// Mock successful docker --version
|
|
exec.mockImplementationOnce((cmd, opts, callback) => {
|
|
callback(null, { stdout: 'Docker version 24.0.7, build afdd53b', stderr: '' });
|
|
});
|
|
|
|
// Mock failed docker info
|
|
exec.mockImplementationOnce((cmd, opts, callback) => {
|
|
callback(new Error('Cannot connect to Docker daemon'), { stdout: '', stderr: 'Cannot connect' });
|
|
});
|
|
|
|
const result = await checker.checkDocker();
|
|
|
|
expect(result.installed).toBe(true);
|
|
expect(result.running).toBe(false);
|
|
expect(result.version).toBe('24.0.7');
|
|
});
|
|
|
|
test('detects Docker not installed', async () => {
|
|
// Mock failed docker --version
|
|
exec.mockImplementationOnce((cmd, opts, callback) => {
|
|
callback(new Error('command not found'), { stdout: '', stderr: 'command not found' });
|
|
});
|
|
|
|
const result = await checker.checkDocker();
|
|
|
|
expect(result.installed).toBe(false);
|
|
expect(result.running).toBe(false);
|
|
expect(result.version).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('checkCaddy', () => {
|
|
test('detects Caddy installed', async () => {
|
|
// Mock successful caddy version
|
|
exec.mockImplementationOnce((cmd, opts, callback) => {
|
|
callback(null, { stdout: 'v2.7.6 h1:w0NymbG2m9PcvKWsrXO6EEkY9Ru4FJK8uQbYcev1p3A=', stderr: '' });
|
|
});
|
|
|
|
// Mock successful which/where caddy
|
|
exec.mockImplementationOnce((cmd, opts, callback) => {
|
|
callback(null, { stdout: '/usr/bin/caddy', stderr: '' });
|
|
});
|
|
|
|
const result = await checker.checkCaddy();
|
|
|
|
expect(result.installed).toBe(true);
|
|
expect(result.version).toBe('2.7.6');
|
|
expect(result.path).toBe('/usr/bin/caddy');
|
|
});
|
|
|
|
test('detects Caddy not installed', async () => {
|
|
// Mock failed caddy version
|
|
exec.mockImplementationOnce((cmd, opts, callback) => {
|
|
callback(new Error('command not found'), { stdout: '', stderr: 'command not found' });
|
|
});
|
|
|
|
const result = await checker.checkCaddy();
|
|
|
|
expect(result.installed).toBe(false);
|
|
expect(result.version).toBeNull();
|
|
expect(result.path).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('getDockerInstallInstructions', () => {
|
|
test('returns Windows instructions', () => {
|
|
const instructions = checker.getDockerInstallInstructions('windows');
|
|
|
|
expect(instructions.title).toContain('Windows');
|
|
expect(instructions.steps).toBeInstanceOf(Array);
|
|
expect(instructions.steps.length).toBeGreaterThan(0);
|
|
expect(instructions.requiresWSL2).toBe(true);
|
|
expect(instructions.url).toContain('docker.com');
|
|
});
|
|
|
|
test('returns macOS instructions', () => {
|
|
const instructions = checker.getDockerInstallInstructions('macos');
|
|
|
|
expect(instructions.title).toContain('Mac');
|
|
expect(instructions.steps).toBeInstanceOf(Array);
|
|
expect(instructions.requiresWSL2).toBe(false);
|
|
expect(instructions.url).toContain('docker.com');
|
|
});
|
|
|
|
test('returns Linux instructions', () => {
|
|
const instructions = checker.getDockerInstallInstructions('linux');
|
|
|
|
expect(instructions.title).toContain('Linux');
|
|
expect(instructions.steps).toBeInstanceOf(Array);
|
|
expect(instructions.packageManagers).toBeDefined();
|
|
expect(instructions.packageManagers.ubuntu).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('getCaddyInstallInstructions', () => {
|
|
test('returns Windows instructions', () => {
|
|
const instructions = checker.getCaddyInstallInstructions('windows');
|
|
|
|
expect(instructions.title).toContain('Windows');
|
|
expect(instructions.steps).toBeInstanceOf(Array);
|
|
expect(instructions.automated).toBe(false);
|
|
expect(instructions.url).toContain('caddyserver.com');
|
|
});
|
|
|
|
test('returns macOS instructions with automation', () => {
|
|
const instructions = checker.getCaddyInstallInstructions('macos');
|
|
|
|
expect(instructions.title).toContain('macOS');
|
|
expect(instructions.automated).toBe(true);
|
|
expect(instructions.command).toBe('brew install caddy');
|
|
});
|
|
|
|
test('returns Linux instructions with package managers', () => {
|
|
const instructions = checker.getCaddyInstallInstructions('linux');
|
|
|
|
expect(instructions.title).toContain('Linux');
|
|
expect(instructions.automated).toBe(true);
|
|
expect(instructions.packageManagers).toBeDefined();
|
|
expect(instructions.packageManagers.ubuntu).toBeDefined();
|
|
expect(instructions.packageManagers.fedora).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('installDocker', () => {
|
|
test('returns manual installation instructions', async () => {
|
|
const result = await checker.installDocker('windows');
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.automated).toBe(false);
|
|
expect(result.instructions).toBeDefined();
|
|
expect(result.message).toContain('manual');
|
|
});
|
|
});
|
|
|
|
describe('installCaddy', () => {
|
|
test('returns manual instructions for Windows', async () => {
|
|
const result = await checker.installCaddy('windows');
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.automated).toBe(false);
|
|
expect(result.instructions).toBeDefined();
|
|
});
|
|
|
|
test('attempts automated installation on macOS', async () => {
|
|
// Mock successful brew install
|
|
exec.mockImplementationOnce((cmd, opts, callback) => {
|
|
callback(null, { stdout: 'Caddy installed', stderr: '' });
|
|
});
|
|
|
|
// Mock successful caddy version check
|
|
exec.mockImplementationOnce((cmd, opts, callback) => {
|
|
callback(null, { stdout: 'v2.7.6', stderr: '' });
|
|
});
|
|
|
|
// Mock successful which caddy
|
|
exec.mockImplementationOnce((cmd, opts, callback) => {
|
|
callback(null, { stdout: '/usr/local/bin/caddy', stderr: '' });
|
|
});
|
|
|
|
const result = await checker.installCaddy('macos');
|
|
|
|
expect(result.automated).toBe(true);
|
|
// Note: success depends on mocked execution
|
|
});
|
|
});
|
|
|
|
describe('executeCommand', () => {
|
|
test('executes command successfully', async () => {
|
|
exec.mockImplementationOnce((cmd, opts, callback) => {
|
|
callback(null, { stdout: 'success', stderr: '' });
|
|
});
|
|
|
|
const result = await checker.executeCommand('test command');
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.stdout).toBe('success');
|
|
});
|
|
|
|
test('handles command failure', async () => {
|
|
const error = new Error('command failed');
|
|
error.stderr = 'error message';
|
|
|
|
exec.mockImplementationOnce((cmd, opts, callback) => {
|
|
callback(error);
|
|
});
|
|
|
|
const result = await checker.executeCommand('test command');
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.stderr).toContain('error');
|
|
});
|
|
});
|
|
|
|
describe('detectLinuxDistro', () => {
|
|
test('detects Ubuntu', async () => {
|
|
exec.mockImplementationOnce((cmd, opts, callback) => {
|
|
callback(null, { stdout: 'ID=ubuntu\nNAME="Ubuntu"', stderr: '' });
|
|
});
|
|
|
|
const distro = await checker.detectLinuxDistro();
|
|
|
|
expect(distro).toBe('ubuntu');
|
|
});
|
|
|
|
test('detects Fedora', async () => {
|
|
exec.mockImplementationOnce((cmd, opts, callback) => {
|
|
callback(null, { stdout: 'ID=fedora\nNAME="Fedora"', stderr: '' });
|
|
});
|
|
|
|
const distro = await checker.detectLinuxDistro();
|
|
|
|
expect(distro).toBe('fedora');
|
|
});
|
|
|
|
test('returns unknown for unrecognized distro', async () => {
|
|
exec.mockImplementationOnce((cmd, opts, callback) => {
|
|
callback(null, { stdout: 'ID=unknown', stderr: '' });
|
|
});
|
|
|
|
const distro = await checker.detectLinuxDistro();
|
|
|
|
expect(distro).toBe('unknown');
|
|
});
|
|
});
|
|
});
|