ktg-plugin-marketplace/plugins/ms-ai-architect/tests/kb-update/test-session-start-status.test.mjs

172 lines
5.4 KiB
JavaScript

// tests/kb-update/test-session-start-status.test.mjs
// Verifies that hooks/scripts/session-start-context.mjs surfaces the
// KB-update status file correctly per Status File Schema (plan.md L122-153).
//
// Same fixture statuses as test-weekly-kb-cron-flags.test.mjs so producer/
// consumer divergence is caught at test time.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } 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 HOOK = join(__dirname, '..', '..', 'hooks', 'scripts', 'session-start-context.mjs');
const PLUGIN_ROOT = join(__dirname, '..', '..');
function mkSandbox() {
return mkdtempSync(join(tmpdir(), 'sshook-test-'));
}
function cacheDirFor(home) {
if (osPlatform() === 'darwin') {
return join(home, 'Library', 'Caches', 'ms-ai-architect');
}
if (osPlatform() === 'win32') {
return join(home, 'AppData', 'Local', 'ms-ai-architect', 'Cache');
}
return join(home, '.cache', 'ms-ai-architect');
}
function writeStatus(home, status) {
const dir = cacheDirFor(home);
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, 'kb-update-status.json'), JSON.stringify(status, null, 2));
}
function runHook(home, extraEnv = {}) {
return spawnSync('node', [HOOK], {
env: {
PATH: process.env.PATH,
HOME: home,
CLAUDE_PLUGIN_ROOT: PLUGIN_ROOT,
...extraEnv,
},
encoding: 'utf8',
timeout: 10_000,
cwd: home, // not in plugin dir, so utredning/onboarding checks stay quiet
});
}
const baseStatus = {
schema_version: 1,
last_run_status: 'success',
last_run_ts: '2026-05-05T10:00:00Z',
duration_seconds: 412,
auth_mode: 'api-key',
log_file: '/tmp/kb-update.log',
files_planned: 18,
files_committed: 18,
session_id: 'sess_demo',
total_cost_usd: 1.42,
tokens_input: 54000,
tokens_output: 27000,
max_turns_hit: false,
diagnostic: null,
};
test('failure status surfaces "KB-update: failure" line', () => {
const home = mkSandbox();
try {
writeStatus(home, { ...baseStatus, last_run_status: 'failure', diagnostic: 'No commits produced' });
const result = runHook(home);
assert.equal(result.status, 0, `stderr: ${result.stderr}`);
assert.match(result.stdout, /KB-update: failure/);
assert.match(result.stdout, /2026-05-05T10:00:00Z/);
} finally {
rmSync(home, { recursive: true, force: true });
}
});
test('partial status surfaces "KB-update: partial" line', () => {
const home = mkSandbox();
try {
writeStatus(home, { ...baseStatus, last_run_status: 'partial', files_committed: 7, max_turns_hit: true });
const result = runHook(home);
assert.equal(result.status, 0);
assert.match(result.stdout, /KB-update: partial/);
} finally {
rmSync(home, { recursive: true, force: true });
}
});
test('budget_exceeded status surfaces a line', () => {
const home = mkSandbox();
try {
writeStatus(home, { ...baseStatus, last_run_status: 'budget_exceeded', files_committed: 0 });
const result = runHook(home);
assert.equal(result.status, 0);
assert.match(result.stdout, /KB-update: budget_exceeded/);
} finally {
rmSync(home, { recursive: true, force: true });
}
});
test('success status does NOT surface a KB-update line', () => {
const home = mkSandbox();
try {
writeStatus(home, { ...baseStatus, last_run_status: 'success' });
const result = runHook(home);
assert.equal(result.status, 0);
assert.doesNotMatch(result.stdout, /KB-update:/);
} finally {
rmSync(home, { recursive: true, force: true });
}
});
test('dry-run status does NOT surface a KB-update line', () => {
const home = mkSandbox();
try {
writeStatus(home, { ...baseStatus, last_run_status: 'dry-run' });
const result = runHook(home);
assert.equal(result.status, 0);
assert.doesNotMatch(result.stdout, /KB-update:/);
} finally {
rmSync(home, { recursive: true, force: true });
}
});
test('missing status file → hook still exits 0 with no KB-update line', () => {
const home = mkSandbox();
try {
// No status file written.
const result = runHook(home);
assert.equal(result.status, 0, `stderr: ${result.stderr}`);
assert.doesNotMatch(result.stdout, /KB-update:/);
} finally {
rmSync(home, { recursive: true, force: true });
}
});
test('malformed status file → hook tolerates and exits 0', () => {
const home = mkSandbox();
try {
const dir = cacheDirFor(home);
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, 'kb-update-status.json'), '{ this is: not, valid json');
const result = runHook(home);
assert.equal(result.status, 0);
assert.doesNotMatch(result.stdout, /KB-update:/);
} finally {
rmSync(home, { recursive: true, force: true });
}
});
test('hook completes in < 1 second on warm filesystem', () => {
const home = mkSandbox();
try {
writeStatus(home, { ...baseStatus, last_run_status: 'failure' });
// Warm-up.
runHook(home);
const start = Date.now();
const result = runHook(home);
const elapsed = Date.now() - start;
assert.equal(result.status, 0);
assert.ok(elapsed < 1000, `hook took ${elapsed}ms (>1s)`);
} finally {
rmSync(home, { recursive: true, force: true });
}
});