// tests/kb-update/test-install-cron.test.mjs // Subprocess + filesystem-snapshot tests for scripts/install-kb-cron.mjs // (Step 11). Exercises --print-only across targets and verifies idempotent // --uninstall. Never invokes launchctl/systemctl/Register-ScheduledTask // against real schedulers; --print-only short-circuits before any // side-effecting call, and --uninstall is exercised against an empty HOME // where the install file simply does not exist. import { test } from 'node:test'; import assert from 'node:assert/strict'; import { spawnSync } from 'node:child_process'; import { mkdtempSync, rmSync, readdirSync, existsSync } from 'node:fs'; import { tmpdir, platform as osPlatform } from 'node:os'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); const SCRIPT = join(__dirname, '..', '..', 'scripts', 'install-kb-cron.mjs'); function mkSandbox() { return mkdtempSync(join(tmpdir(), 'install-kb-cron-test-')); } function runInstall(args, env = {}) { return spawnSync('node', [SCRIPT, ...args], { env: { PATH: process.env.PATH, ...env }, encoding: 'utf8', timeout: 30_000, }); } function snapshotDir(dir) { const out = []; function walk(d) { if (!existsSync(d)) return; let entries; try { entries = readdirSync(d, { withFileTypes: true }); } catch { return; } for (const entry of entries) { const p = join(d, entry.name); out.push(p); if (entry.isDirectory()) walk(p); } } walk(dir); return out.sort(); } function hostTarget() { const p = osPlatform(); if (p === 'darwin') return 'macos'; if (p === 'linux') return 'linux'; if (p === 'win32') return 'windows'; return null; } test('--print-only --target macos: substituted plist with no unsubstituted placeholders', () => { const home = mkSandbox(); try { const r = runInstall(['--print-only', '--target', 'macos'], { HOME: home }); assert.equal(r.status, 0, `stderr: ${r.stderr}\nstdout: ${r.stdout}`); assert.match(r.stdout, /Label<\/key>/); assert.match(r.stdout, /StartCalendarInterval<\/key>/); assert.match(r.stdout, /3<\/integer>/, 'default day-of-week=3 (Wednesday)'); assert.match(r.stdout, /4<\/integer>/, 'default hour=4'); assert.match(r.stdout, /23<\/integer>/, 'default minute=23'); assert.doesNotMatch(r.stdout, /\{\{[A-Z_]+\}\}/, 'no unsubstituted {{...}} placeholders'); } finally { rmSync(home, { recursive: true, force: true }); } }); test('--print-only --target linux: filled service+timer with [Unit] and OnCalendar=Wed', () => { const home = mkSandbox(); try { const r = runInstall(['--print-only', '--target', 'linux'], { HOME: home }); assert.equal(r.status, 0, `stderr: ${r.stderr}`); assert.match(r.stdout, /\[Unit\]/); assert.match(r.stdout, /\[Service\]/); assert.match(r.stdout, /\[Timer\]/); assert.match(r.stdout, /OnCalendar=Wed/); assert.match(r.stdout, /ExecStart=/); assert.doesNotMatch(r.stdout, /\{\{[A-Z_]+\}\}/, 'no unsubstituted {{...}} placeholders'); } finally { rmSync(home, { recursive: true, force: true }); } }); test('--print-only --target windows: Register-ScheduledTask + InteractiveToken', () => { const home = mkSandbox(); try { const r = runInstall(['--print-only', '--target', 'windows'], { HOME: home }); assert.equal(r.status, 0, `stderr: ${r.stderr}`); assert.match(r.stdout, /Register-ScheduledTask/); assert.match(r.stdout, /InteractiveToken/); assert.match(r.stdout, /-DaysOfWeek\s+Wednesday/); assert.doesNotMatch(r.stdout, /\{\{[A-Z_]+\}\}/, 'no unsubstituted {{...}} placeholders'); } finally { rmSync(home, { recursive: true, force: true }); } }); test('--print-only writes no files (HOME snapshot before/after equal)', () => { const home = mkSandbox(); try { const before = snapshotDir(home); const r = runInstall(['--print-only', '--target', 'macos'], { HOME: home }); assert.equal(r.status, 0, `stderr: ${r.stderr}`); const after = snapshotDir(home); assert.deepEqual(after, before, 'HOME must not be touched in --print-only mode'); } finally { rmSync(home, { recursive: true, force: true }); } }); test('--print-only --target linux writes no files in HOME', () => { const home = mkSandbox(); try { const before = snapshotDir(home); const r = runInstall(['--print-only', '--target', 'linux'], { HOME: home }); assert.equal(r.status, 0, `stderr: ${r.stderr}`); const after = snapshotDir(home); assert.deepEqual(after, before, 'HOME must not be touched in --print-only mode (linux target)'); } finally { rmSync(home, { recursive: true, force: true }); } }); test('--uninstall is idempotent (exit 0 with nothing installed)', () => { const home = mkSandbox(); try { const target = hostTarget() || 'macos'; const r = runInstall(['--uninstall', '--target', target], { HOME: home }); assert.equal(r.status, 0, `stderr: ${r.stderr}\nstdout: ${r.stdout}`); } finally { rmSync(home, { recursive: true, force: true }); } }); test('--uninstall --target macos on empty HOME: idempotent (no plist, no launchctl call)', () => { const home = mkSandbox(); try { const r = runInstall(['--uninstall', '--target', 'macos'], { HOME: home }); assert.equal(r.status, 0, `stderr: ${r.stderr}`); assert.match((r.stdout || '') + (r.stderr || ''), /nothing to remove|not installed/i); } finally { rmSync(home, { recursive: true, force: true }); } }); test('--schedule with custom cron expr substitutes correctly into plist', () => { const home = mkSandbox(); try { const r = runInstall( ['--print-only', '--target', 'macos', '--schedule', '15 7 * * 5'], { HOME: home }, ); assert.equal(r.status, 0, `stderr: ${r.stderr}`); assert.match(r.stdout, /15<\/integer>/, 'minute=15'); assert.match(r.stdout, /7<\/integer>/, 'hour=7'); assert.match(r.stdout, /5<\/integer>/, 'day-of-week=5 (Friday)'); } finally { rmSync(home, { recursive: true, force: true }); } }); test('invalid --target rejects with non-zero exit', () => { const home = mkSandbox(); try { const r = runInstall(['--print-only', '--target', 'bogus'], { HOME: home }); assert.notEqual(r.status, 0); assert.match((r.stderr || '') + (r.stdout || ''), /target|invalid|unsupported/i); } finally { rmSync(home, { recursive: true, force: true }); } }); test('invalid --schedule rejects with non-zero exit', () => { const home = mkSandbox(); try { const r = runInstall( ['--print-only', '--target', 'macos', '--schedule', 'not a cron'], { HOME: home }, ); assert.notEqual(r.status, 0); assert.match((r.stderr || '') + (r.stdout || ''), /schedule|cron|invalid/i); } finally { rmSync(home, { recursive: true, force: true }); } }); test('--node-bin override appears in substituted plist', () => { const home = mkSandbox(); try { const r = runInstall( ['--print-only', '--target', 'macos', '--node-bin', '/opt/custom/bin/node'], { HOME: home }, ); assert.equal(r.status, 0, `stderr: ${r.stderr}`); assert.match(r.stdout, /\/opt\/custom\/bin\/node/); } finally { rmSync(home, { recursive: true, force: true }); } });