// tests/kb-update/test-template-generation.test.mjs // Structural-regex tests for scripts/kb-update/templates/* (Step 8). // Verifies that each template file exists, contains the documented sentinel // strings, and exposes the documented placeholder set. No template execution // or real scheduling occurs in this test — that lives in Wave 6 live-test. import { test } from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync, existsSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); const TEMPLATES_DIR = join(__dirname, '..', '..', 'scripts', 'kb-update', 'templates'); const PLIST = join(TEMPLATES_DIR, 'com.fromaitochitta.ms-ai-architect.kb-update.plist'); const SERVICE = join(TEMPLATES_DIR, 'ms-ai-architect-kb-update.service'); const TIMER = join(TEMPLATES_DIR, 'ms-ai-architect-kb-update.timer'); const PS1 = join(TEMPLATES_DIR, 'ms-ai-architect-kb-update.ps1'); const README = join(TEMPLATES_DIR, 'README.md'); function readTpl(p) { assert.equal(existsSync(p), true, `template missing: ${p}`); return readFileSync(p, 'utf8'); } test('plist — exists with required keys and placeholders', () => { const content = readTpl(PLIST); assert.match(content, /Label<\/key>/); assert.match(content, /StartCalendarInterval<\/key>/); assert.match(content, /ProgramArguments<\/key>/); assert.match(content, /StandardOutPath<\/key>/); assert.match(content, /StandardErrorPath<\/key>/); assert.match(content, /EnvironmentVariables<\/key>/); assert.match(content, /RunAtLoad<\/key>\s*/); assert.match(content, /\{\{NODE_BIN\}\}/); assert.match(content, /\{\{PLUGIN_ROOT\}\}/); assert.match(content, /\{\{LOG_FILE\}\}/); assert.match(content, /\{\{SCHEDULE_HOUR\}\}/); assert.match(content, /\{\{SCHEDULE_MINUTE\}\}/); assert.match(content, /\{\{SCHEDULE_DAY_OF_WEEK\}\}/); }); test('systemd .timer — exists with OnCalendar and Persistent', () => { const content = readTpl(TIMER); assert.match(content, /\[Unit\]/); assert.match(content, /\[Timer\]/); assert.match(content, /\[Install\]/); assert.match(content, /OnCalendar=Wed/); assert.match(content, /Persistent=true/); assert.match(content, /WantedBy=timers\.target/); }); test('systemd .service — exists with [Unit], [Service] and ExecStart', () => { const content = readTpl(SERVICE); assert.match(content, /\[Unit\]/); assert.match(content, /\[Service\]/); assert.match(content, /ExecStart=/); assert.match(content, /\{\{NODE_BIN\}\}/); assert.match(content, /\{\{PLUGIN_ROOT\}\}/); }); test('PowerShell ps1 — exists with Register-ScheduledTask and InteractiveToken', () => { const content = readTpl(PS1); assert.match(content, /Register-ScheduledTask/); assert.match(content, /InteractiveToken/); assert.match(content, /New-ScheduledTaskTrigger/); assert.match(content, /-Weekly/); assert.match(content, /-DaysOfWeek\s+Wednesday/); assert.match(content, /\{\{NODE_BIN\}\}/); assert.match(content, /\{\{PLUGIN_ROOT\}\}/); }); test('README — exists and references each template by filename', () => { const content = readTpl(README); assert.match(content, /com\.fromaitochitta\.ms-ai-architect\.kb-update\.plist/); assert.match(content, /ms-ai-architect-kb-update\.service/); assert.match(content, /ms-ai-architect-kb-update\.timer/); assert.match(content, /ms-ai-architect-kb-update\.ps1/); }); test('plist + service + ps1 reference NODE_BIN and PLUGIN_ROOT', () => { // The .timer is a pure trigger — it activates the .service, which is // the only systemd unit that needs to know the binary + plugin root. // launchd and Windows put the command directly in the trigger spec, so // they need both placeholders themselves. for (const tpl of [PLIST, SERVICE, PS1]) { const content = readFileSync(tpl, 'utf8'); assert.match(content, /\{\{NODE_BIN\}\}/, `${tpl} missing NODE_BIN placeholder`); assert.match(content, /\{\{PLUGIN_ROOT\}\}/, `${tpl} missing PLUGIN_ROOT placeholder`); } }); test('.timer is placeholder-free literal (Wed 04:23 hardcoded per plan)', () => { const content = readFileSync(TIMER, 'utf8'); assert.match(content, /OnCalendar=Wed \*-\*-\* 04:23:00/); assert.doesNotMatch(content, /\{\{[A-Z_]+\}\}/); });