ktg-plugin-marketplace/plugins/ms-ai-architect/tests/kb-update/test-install-cron.test.mjs
Kjell Tore Guttormsen 30d7a2024c feat(ms-ai-architect): add install-kb-cron standalone helper for cross-OS cron registration [skip-docs]
Step 11 of v1.12.0 plan (.claude/projects/2026-05-04-kb-update-fork-and-own/plan.md).

scripts/install-kb-cron.mjs lives at the scripts/ root (not inside
scripts/kb-update/) because it is a plugin-wide install tool, not part of
the KB-update pipeline itself. Reads the appropriate template from
scripts/kb-update/templates/, fills {{NODE_BIN}}, {{PLUGIN_ROOT}},
{{LOG_FILE}}, {{SCHEDULE_HOUR/MINUTE/DAY_OF_WEEK}} placeholders, writes
to the platform-specific scheduler dir, and registers the job:

  macOS  - launchctl bootstrap gui/<uid> <plist>  (load -w fallback)
  Linux  - systemctl --user daemon-reload && enable --now <timer>
  Windows - powershell -ExecutionPolicy Bypass -File <ps1>  (beta)

Flags: --print-only, --target macos|linux|windows, --uninstall, --purge,
--node-bin, --claude-bin, --schedule "M H * * D" (default: Wed 04:23).

UID resolution for launchctl is guarded by process.getuid() POSIX-only
(undefined on Windows). MCP server presence in ~/.claude.json is
warning-only per brief Spørsmål 7. WSL detected via /proc/version.
Cross-OS rendering supported via --print-only --target <other>; install
on a non-host target rejects with explicit error.

11 subprocess + filesystem-snapshot tests in
tests/kb-update/test-install-cron.test.mjs verify --print-only produces
filled templates with no unsubstituted {{...}} placeholders, --print-only
writes nothing under HOME, --uninstall is idempotent on an empty HOME,
--schedule substitutes correctly, and invalid flags reject with non-zero
exit. Tests never invoke launchctl/systemctl/Register-ScheduledTask
against real schedulers.

Tests: 110/110 pass (was 99 before this step).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-05 11:26:54 +02:00

207 lines
7.3 KiB
JavaScript

// 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, /<key>Label<\/key>/);
assert.match(r.stdout, /<key>StartCalendarInterval<\/key>/);
assert.match(r.stdout, /<integer>3<\/integer>/, 'default day-of-week=3 (Wednesday)');
assert.match(r.stdout, /<integer>4<\/integer>/, 'default hour=4');
assert.match(r.stdout, /<integer>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, /<integer>15<\/integer>/, 'minute=15');
assert.match(r.stdout, /<integer>7<\/integer>/, 'hour=7');
assert.match(r.stdout, /<integer>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 });
}
});