172 lines
5.4 KiB
JavaScript
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 });
|
|
}
|
|
});
|