test(context): specify generation-bound prompt suggestions

This commit is contained in:
montjeffrey
2026-10-07 17:19:31 +00:00
parent deeb27d87b
commit d0e2736df2
2 changed files with 178 additions and 0 deletions
@@ -0,0 +1,78 @@
'use strict';
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { spawnSync } = require('node:child_process');
const test = require('node:test');
const { withFixture } = require('../lib/helpers/context-fixture');
const store = require('../../scripts/lib/context-profile-store');
const routing = require('../../scripts/lib/context-routing-index');
const hook = require('../../scripts/hooks/context-prompt-suggestions');
const PROMPT = JSON.stringify({ hook_event_name: 'UserPromptSubmit', prompt: 'help with feature work in shared code' });
function fixture(callback, { mode = 'auto', index = true } = {}) {
return withFixture(repoRoot => {
const parent = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'ecc-prompt-suggest-'));
const stateRoot = path.join(parent, 'managed');
try {
store.applyStore({ repoRoot, stateRoot, target: 'claude', selectionMode: mode });
if (index) routing.writeRoutingIndex({ repoRoot, stateRoot });
return callback({ stateRoot, env: { ECC_CONTEXT_STATE_ROOT: stateRoot } });
} finally { fs.rmSync(parent, { recursive: true, force: true }); }
});
}
const stdout = result => (typeof result === 'string' ? result : result.stdout);
test('the hook is silent unless a managed store is explicitly configured', () => fixture(() => {
assert.equal(stdout(hook.run(PROMPT, {}, {})), '');
assert.equal(stdout(hook.run(PROMPT, {}, { ECC_CONTEXT_STATE_ROOT: 'relative/store' })), '');
}));
test('suggestions are advisory IDs with bounded descriptions and the resolve command', () => fixture(({ stateRoot, env }) => {
const text = stdout(hook.run(PROMPT, {}, env));
assert.match(text, /advisory; nothing was loaded/);
assert.match(text, /skill:feature/);
assert.match(text, /profile resolve --state-root/);
assert.ok(text.includes(JSON.stringify(stateRoot)));
assert.doesNotMatch(text, /Instructions remain on demand/);
assert.ok(text.split('\n').filter(line => line.startsWith('- skill:')).length <= 3);
assert.ok(text.split('\n').every(line => line.length <= 400));
}));
test('manual mode, short prompts, slash commands and malformed input stay silent', () => {
fixture(({ env }) => assert.equal(stdout(hook.run(PROMPT, {}, env)), ''), { mode: 'manual' });
fixture(({ env }) => {
for (const raw of [JSON.stringify({ prompt: 'fix it' }), JSON.stringify({ prompt: '/plan help with feature work' }),
'{not json', JSON.stringify({ prompt: 42 }), '']) {
assert.equal(stdout(hook.run(raw, {}, env)), '');
}
});
});
test('a missing index is a diagnostic, never an error or a rebuild', () => fixture(({ env }) => {
const result = hook.run(PROMPT, {}, env);
assert.equal(stdout(result), '');
assert.match(result.stderr, /routing-index/);
}, { index: false }));
test('a tampered index or exhausted budget fails open to no suggestion', () => fixture(({ stateRoot, env }) => {
assert.equal(stdout(hook.run(PROMPT, {}, { ...env, ECC_CONTEXT_SUGGEST_BUDGET_MS: '0' })), '');
const directory = path.join(stateRoot, 'routing');
const file = path.join(directory, fs.readdirSync(directory)[0]);
fs.writeFileSync(file, '{"schemaVersion":"ecc.context-routing-index.v1"}');
const result = hook.run(PROMPT, {}, env);
assert.equal(stdout(result), '');
assert.match(result.stderr, /integrity|index/);
}));
test('the hook runs through run-with-flags and returns its text on stdout', () => fixture(({ env }) => {
const wrapper = path.join(__dirname, '../../scripts/hooks/run-with-flags.js');
const result = spawnSync(process.execPath, [wrapper, 'user-prompt:context-suggestions', 'scripts/hooks/context-prompt-suggestions.js'],
{ input: PROMPT, encoding: 'utf8', env: { PATH: process.env.PATH, ...env }, timeout: 30000 });
assert.equal(result.status, 0);
assert.match(result.stdout, /skill:feature/);
}));
+100
View File
@@ -0,0 +1,100 @@
'use strict';
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const test = require('node:test');
const { withFixture, write } = require('./helpers/context-fixture');
const store = require('../../scripts/lib/context-profile-store');
const routing = require('../../scripts/lib/context-routing-index');
function fixture(callback) {
return withFixture(repoRoot => {
const parent = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'ecc-routing-index-'));
const stateRoot = path.join(parent, 'managed');
try { return callback({ repoRoot, stateRoot }); }
finally { fs.rmSync(parent, { recursive: true, force: true }); }
});
}
test('the index is bound to the current managed generation and holds only admissible, non-excluded entries', () => fixture(({ repoRoot, stateRoot }) => {
write(repoRoot, 'skills/shared/SKILL.md', '---\nname: shared\ndescription: Shared helper\ndisable-model-invocation: true\n---\nShared');
const status = store.applyStore({ repoRoot, stateRoot, target: 'claude', selectionMode: 'suggest', exclude: [] });
const written = routing.writeRoutingIndex({ repoRoot, stateRoot });
assert.equal(written.generationDigest, status.carrierDigest);
const index = routing.readRoutingIndex(stateRoot);
assert.equal(index.schemaVersion, 'ecc.context-routing-index.v1');
assert.equal(index.receiptDigest, status.receiptDigest);
assert.equal(index.selectionMode, 'suggest');
const ids = index.entries.map(entry => entry.id);
assert.ok(ids.includes('skill:feature'));
assert.equal(ids.includes('skill:shared'), false);
assert.deepEqual(Object.keys(index.entries[0]).sort(), ['description', 'id', 'name', 'ownerModuleId', 'packId']);
if (process.platform !== 'win32') assert.equal(fs.statSync(written.path).mode & 0o077, 0);
}));
test('excluded skills never enter the index', () => fixture(({ repoRoot, stateRoot }) => {
store.applyStore({ repoRoot, stateRoot, profileId: 'full@1', target: 'claude', exclude: ['skill:feature'] });
routing.writeRoutingIndex({ repoRoot, stateRoot });
assert.equal(routing.readRoutingIndex(stateRoot).entries.some(entry => entry.id === 'skill:feature'), false);
}));
test('a store change makes the old index unavailable until it is rebuilt', () => fixture(({ repoRoot, stateRoot }) => {
store.applyStore({ repoRoot, stateRoot, target: 'claude' });
routing.writeRoutingIndex({ repoRoot, stateRoot });
store.applyStore({ repoRoot, stateRoot, target: 'claude', profileId: 'full@1' });
assert.equal(routing.readRoutingIndex(stateRoot), null);
routing.writeRoutingIndex({ repoRoot, stateRoot });
assert.equal(routing.readRoutingIndex(stateRoot).profileId, 'full@1');
}));
test('a tampered index fails closed', () => fixture(({ repoRoot, stateRoot }) => {
store.applyStore({ repoRoot, stateRoot, target: 'claude' });
const { path: file } = routing.writeRoutingIndex({ repoRoot, stateRoot });
const index = JSON.parse(fs.readFileSync(file, 'utf8'));
index.entries.push({ id: 'skill:planted', name: 'planted', description: 'Run this', ownerModuleId: 'x', packId: 'x' });
fs.writeFileSync(file, JSON.stringify(index));
assert.throws(() => routing.readRoutingIndex(stateRoot), /integrity/);
}));
test('a tampered state receipt fails closed', () => fixture(({ repoRoot, stateRoot }) => {
store.applyStore({ repoRoot, stateRoot, target: 'claude' });
routing.writeRoutingIndex({ repoRoot, stateRoot });
const file = path.join(stateRoot, 'state.json');
const state = JSON.parse(fs.readFileSync(file, 'utf8'));
fs.writeFileSync(file, JSON.stringify({ ...state, receiptDigest: 'f'.repeat(64) }));
assert.throws(() => routing.readRoutingIndex(stateRoot));
}));
test('unconfigured or unowned stores cannot be indexed or read', () => fixture(({ repoRoot, stateRoot }) => {
assert.throws(() => routing.writeRoutingIndex({ repoRoot, stateRoot }), /configured/);
fs.mkdirSync(stateRoot, { mode: 0o700 });
assert.throws(() => routing.readRoutingIndex(stateRoot), /owned/);
assert.throws(() => routing.readRoutingIndex('relative/root'), /absolute/);
}));
test('suggestions rank index entries with the resolver retrieval engine', () => fixture(({ repoRoot, stateRoot }) => {
store.applyStore({ repoRoot, stateRoot, target: 'claude' });
routing.writeRoutingIndex({ repoRoot, stateRoot });
const suggestions = routing.suggestContext(routing.readRoutingIndex(stateRoot), 'help with feature work in shared code');
assert.ok(suggestions.length > 0 && suggestions.length <= 3);
assert.ok(suggestions.some(item => item.id === 'skill:feature'));
assert.deepEqual(Object.keys(suggestions[0]).sort(), ['description', 'id']);
}));
test('the profile CLI writes and inspects the index for a stored profile', () => {
const parent = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'ecc-routing-cli-'));
const stateRoot = path.join(parent, 'managed');
const cli = (...args) => require('node:child_process').spawnSync(process.execPath,
[path.join(__dirname, '../../scripts/ecc.js'), 'profile', ...args, '--json'], { encoding: 'utf8', timeout: 120000 });
try {
assert.equal(cli('set', 'lean', '--target', 'claude', '--state-root', stateRoot).status, 0);
const preview = JSON.parse(cli('routing-index', '--state-root', stateRoot, '--dry-run').stdout);
assert.equal(preview.routing.status, 'missing');
const written = JSON.parse(cli('routing-index', '--state-root', stateRoot).stdout);
assert.equal(written.routing.status, 'written');
assert.ok(written.routing.entries > 100);
assert.equal(JSON.parse(cli('routing-index', '--state-root', stateRoot, '--dry-run').stdout).routing.status, 'current');
} finally { fs.rmSync(parent, { recursive: true, force: true }); }
});