ktg-plugin-marketplace/plugins/graceful-handoff/tests/hooks/session-start-load-handoff.test.mjs
Kjell Tore Guttormsen 4076bf904a feat(graceful-handoff): 2.0 — SessionStart auto-load handoff on resume/compact [skip-docs]
Step 6 of v2.0 plan. SessionStart hook fires on source: resume or
source: compact, walks up to 3 levels searching for
NEXT-SESSION-*.local.md, injects content via additionalContext, and
archives the file (rename to *.archived.local.md) to prevent stale-load
in later sessions. 9 tests cover sources, multi-level search,
topic-slug variants, archive filtering, malformed payload.
2026-05-01 06:06:25 +02:00

108 lines
4.8 KiB
JavaScript

// session-start-load-handoff.test.mjs
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { mkdtempSync, mkdirSync, writeFileSync, existsSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { runHook } from './hook-helper.mjs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const HOOK = join(__dirname, '..', '..', 'hooks', 'scripts', 'session-start-load-handoff.mjs');
function makeFixture() {
return mkdtempSync(join(tmpdir(), 'sessionstart-'));
}
test('source: startup → silent (no injection)', async () => {
const dir = makeFixture();
writeFileSync(join(dir, 'NEXT-SESSION-PROMPT.local.md'), 'should not load\n');
const res = await runHook(HOOK, { source: 'startup', cwd: dir });
assert.equal(res.code, 0);
assert.equal(res.stdout.trim(), '', 'startup source should not inject');
assert.ok(existsSync(join(dir, 'NEXT-SESSION-PROMPT.local.md')), 'file should not be archived');
rmSync(dir, { recursive: true, force: true });
});
test('source: clear → silent (no injection)', async () => {
const dir = makeFixture();
writeFileSync(join(dir, 'NEXT-SESSION-PROMPT.local.md'), 'should not load\n');
const res = await runHook(HOOK, { source: 'clear', cwd: dir });
assert.equal(res.code, 0);
assert.equal(res.stdout.trim(), '');
rmSync(dir, { recursive: true, force: true });
});
test('source: resume + handoff in cwd → injected and archived', async () => {
const dir = makeFixture();
writeFileSync(join(dir, 'NEXT-SESSION-PROMPT.local.md'), '# my handoff\n\nimportant content\n');
const res = await runHook(HOOK, { source: 'resume', cwd: dir });
assert.equal(res.code, 0);
// Stdout should be JSON with additionalContext containing the file
const json = JSON.parse(res.stdout);
assert.equal(json.hookSpecificOutput.hookEventName, 'SessionStart');
assert.match(json.hookSpecificOutput.additionalContext, /important content/);
assert.match(json.hookSpecificOutput.additionalContext, /<session-handoff/);
// File should be archived
assert.ok(!existsSync(join(dir, 'NEXT-SESSION-PROMPT.local.md')), 'original should be renamed');
assert.ok(existsSync(join(dir, 'NEXT-SESSION-PROMPT.archived.local.md')), 'archive should exist');
rmSync(dir, { recursive: true, force: true });
});
test('source: compact + handoff in cwd → injected and archived', async () => {
const dir = makeFixture();
writeFileSync(join(dir, 'NEXT-SESSION-PROMPT.local.md'), '# compact handoff\n');
const res = await runHook(HOOK, { source: 'compact', cwd: dir });
assert.equal(res.code, 0);
const json = JSON.parse(res.stdout);
assert.match(json.hookSpecificOutput.additionalContext, /compact handoff/);
rmSync(dir, { recursive: true, force: true });
});
test('source: resume + handoff 2 levels above cwd → found and injected', async () => {
const root = makeFixture();
const sub = join(root, 'a', 'b');
mkdirSync(sub, { recursive: true });
writeFileSync(join(root, 'NEXT-SESSION-PROMPT.local.md'), '# parent handoff\n');
const res = await runHook(HOOK, { source: 'resume', cwd: sub });
assert.equal(res.code, 0);
const json = JSON.parse(res.stdout);
assert.match(json.hookSpecificOutput.additionalContext, /parent handoff/);
// Archived in the original parent location
assert.ok(existsSync(join(root, 'NEXT-SESSION-PROMPT.archived.local.md')));
rmSync(root, { recursive: true, force: true });
});
test('source: resume + no handoff anywhere → silent', async () => {
const dir = makeFixture();
const res = await runHook(HOOK, { source: 'resume', cwd: dir });
assert.equal(res.code, 0);
assert.equal(res.stdout.trim(), '');
rmSync(dir, { recursive: true, force: true });
});
test('source: resume + topic-slug variant NEXT-SESSION-foo.local.md → found', async () => {
const dir = makeFixture();
writeFileSync(join(dir, 'NEXT-SESSION-feature-x.local.md'), '# topic handoff\n');
const res = await runHook(HOOK, { source: 'resume', cwd: dir });
assert.equal(res.code, 0);
const json = JSON.parse(res.stdout);
assert.match(json.hookSpecificOutput.additionalContext, /topic handoff/);
rmSync(dir, { recursive: true, force: true });
});
test('archived files are not re-loaded on subsequent runs', async () => {
const dir = makeFixture();
writeFileSync(join(dir, 'NEXT-SESSION-PROMPT.archived.local.md'), 'stale - should not load\n');
const res = await runHook(HOOK, { source: 'resume', cwd: dir });
assert.equal(res.code, 0);
assert.equal(res.stdout.trim(), '', 'archived files must be ignored');
rmSync(dir, { recursive: true, force: true });
});
test('malformed JSON payload: silent exit 0', async () => {
const res = await runHook(HOOK, '{not valid');
assert.equal(res.code, 0);
assert.equal(res.stdout.trim(), '');
});