mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-17 21:55:34 +02:00
139 lines
6.9 KiB
JavaScript
139 lines
6.9 KiB
JavaScript
'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).find(name => name.endsWith('.json')));
|
|
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/);
|
|
}));
|
|
|
|
test('the session-start builder indexes the current generation once and stays silent when disabled', () => {
|
|
const builder = require('../../scripts/hooks/context-routing-index');
|
|
const parent = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'ecc-routing-builder-'));
|
|
const stateRoot = path.join(parent, 'managed');
|
|
try {
|
|
assert.equal(builder.run('{}', {}, {}), '');
|
|
store.applyStore({ stateRoot, target: 'claude' });
|
|
const env = { ECC_CONTEXT_STATE_ROOT: stateRoot };
|
|
assert.match(builder.run('{}', {}, env).stderr, /built/);
|
|
assert.equal(routing.routingIndexStatus(stateRoot).status, 'current');
|
|
assert.equal(builder.run('{}', {}, env), '');
|
|
} finally { fs.rmSync(parent, { recursive: true, force: true }); }
|
|
});
|
|
|
|
test('manual mode is silent before the index is read, even when it is missing or corrupt', () => {
|
|
fixture(({ env }) => assert.deepEqual(hook.run(PROMPT, {}, env), ''), { mode: 'manual', index: false });
|
|
fixture(({ stateRoot, env }) => {
|
|
const directory = path.join(stateRoot, 'routing');
|
|
fs.writeFileSync(path.join(directory, fs.readdirSync(directory).find(name => name.endsWith('.json'))), '{');
|
|
assert.deepEqual(hook.run(PROMPT, {}, env), '');
|
|
}, { mode: 'manual' });
|
|
});
|
|
|
|
test('loading advice matches the saved mode', () => {
|
|
fixture(({ env }) => {
|
|
const text = stdout(hook.run(PROMPT, {}, env));
|
|
assert.match(text, /skill:feature/);
|
|
assert.match(text, /--load/);
|
|
});
|
|
fixture(({ env }) => {
|
|
const text = stdout(hook.run(PROMPT, {}, env));
|
|
assert.match(text, /skill:feature/);
|
|
assert.doesNotMatch(text, /--load/);
|
|
assert.match(text, /suggest mode/i);
|
|
assert.ok(text.includes(`ecc profile mode auto --state-root <store>`));
|
|
}, { mode: 'suggest' });
|
|
});
|
|
|
|
test('shell metacharacters in a store path remain data rather than executable advice', () => {
|
|
// Use a native canonical absolute root and filename characters legal on
|
|
// Windows while still exercising shell substitutions and command separators.
|
|
const root = path.join(path.resolve(os.tmpdir()), "store-$(touch SHOULD_NOT_RUN)-`echo injected`-'quote'-semi;and&");
|
|
const routingModule = require('../../scripts/lib/context-routing-index');
|
|
const originals = { readBinding: routingModule.readBinding, readRoutingIndex: routingModule.readRoutingIndex,
|
|
suggestContext: routingModule.suggestContext };
|
|
try {
|
|
routingModule.readBinding = () => ({ root, revision: 1, selectionMode: 'auto' });
|
|
routingModule.readRoutingIndex = () => ({ entries: [] });
|
|
routingModule.suggestContext = () => [{ id: 'skill:feature', description: 'Focused feature help.' }];
|
|
const text = stdout(hook.run(PROMPT, {}, { ECC_CONTEXT_STATE_ROOT: root }));
|
|
assert.ok(text.includes(`Store path (data): ${JSON.stringify(root)}`));
|
|
const commandLine = text.split('\n').find(line => line.startsWith('To load one'));
|
|
assert.ok(commandLine.includes('--state-root <store>'));
|
|
assert.ok(!commandLine.includes(root));
|
|
assert.doesNotMatch(commandLine, /touch|injected/);
|
|
} finally {
|
|
Object.assign(routingModule, originals);
|
|
}
|
|
});
|