From f4c9b05e54baadae94fc2a9d4d3037d6fa9e5fa3 Mon Sep 17 00:00:00 2001 From: montjeffrey Date: Thu, 8 Oct 2026 16:56:09 +0000 Subject: [PATCH] test(context): pin routing index recovery, entry validation, bounds and mode-aware advice --- .../hooks/context-prompt-suggestions.test.js | 24 ++++++ tests/lib/context-routing-index.test.js | 75 +++++++++++++++++++ tests/lib/context-selection.test.js | 15 ++++ 3 files changed, 114 insertions(+) diff --git a/tests/hooks/context-prompt-suggestions.test.js b/tests/hooks/context-prompt-suggestions.test.js index f98f080ad..15e8b225a 100644 --- a/tests/hooks/context-prompt-suggestions.test.js +++ b/tests/hooks/context-prompt-suggestions.test.js @@ -90,3 +90,27 @@ test('the session-start builder indexes the current generation once and stays si 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(({ stateRoot, 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 ${JSON.stringify(stateRoot)}`)); + }, { mode: 'suggest' }); +}); diff --git a/tests/lib/context-routing-index.test.js b/tests/lib/context-routing-index.test.js index df3191c77..5f2531556 100644 --- a/tests/lib/context-routing-index.test.js +++ b/tests/lib/context-routing-index.test.js @@ -127,3 +127,78 @@ test('stored vectors use a compact little-endian encoding that decodes exactly', const { dense: _dense, ...metadata } = decoded; assert.deepEqual(decoded.dense, sparseDense(metadata)); })); + +function republish(stateRoot, change) { + const io = require('../../scripts/lib/context-profile-store-fs'); + const { digestObject } = require('../../scripts/lib/context-profile-support'); + const directory = path.join(stateRoot, 'routing'); + const pointerFile = path.join(directory, fs.readdirSync(directory).find(name => name.endsWith('.json'))); + const { pointerDigest: _digest, ...pointer } = JSON.parse(fs.readFileSync(pointerFile, 'utf8')); + const index = JSON.parse(fs.readFileSync(path.join(directory, 'indexes', `${pointer.indexDigest}.json`), 'utf8')); + const bytes = io.jsonBytes(change(index)); + const indexDigest = io.hash(bytes); + fs.writeFileSync(path.join(directory, 'indexes', `${indexDigest}.json`), bytes); + const next = { ...change.pointer?.(pointer) || pointer, indexDigest, bytes: bytes.length }; + fs.writeFileSync(pointerFile, JSON.stringify({ ...next, pointerDigest: digestObject(next) })); +} + +test('rollback to an indexed generation retires its old pointer instead of failing', () => fixture(({ repoRoot, stateRoot }) => { + store.applyStore({ repoRoot, stateRoot, target: 'claude' }); + routing.writeRoutingIndex({ repoRoot, stateRoot }); + store.applyStore({ repoRoot, stateRoot, target: 'claude', profileId: 'full@1' }); + store.rollbackStore({ stateRoot, expectedRevision: store.getStoreStatus({ stateRoot }).revision }); + assert.equal(routing.readRoutingIndex(stateRoot), null); + assert.equal(routing.routingIndexStatus(stateRoot).status, 'missing'); + routing.writeRoutingIndex({ repoRoot, stateRoot }); + assert.equal(routing.routingIndexStatus(stateRoot).status, 'current'); +})); + +const MALFORMED = { + 'non-string id': entry => ({ ...entry, id: 42 }), + 'id outside the skill namespace': entry => ({ ...entry, id: 'agent:planner' }), + 'non-string name': entry => ({ ...entry, name: 5 }), + 'non-string description': entry => ({ ...entry, description: {} }), + 'non-array triggers': entry => ({ ...entry, triggers: 'review' }), + 'non-string trigger': entry => ({ ...entry, triggers: [1] }), + 'non-string owner module': entry => ({ ...entry, ownerModuleId: [] }), +}; +for (const [label, change] of Object.entries(MALFORMED)) { + test(`a digest-consistent index with a ${label} entry fails as an integrity error`, () => fixture(({ repoRoot, stateRoot }) => { + store.applyStore({ repoRoot, stateRoot, target: 'claude' }); + routing.writeRoutingIndex({ repoRoot, stateRoot }); + republish(stateRoot, index => ({ ...index, entries: [change(index.entries[0]), ...index.entries.slice(1)] })); + assert.throws(() => routing.readRoutingIndex(stateRoot), /integrity/); + })); +} + +test('an index larger than the read bound fails before it is read', () => fixture(({ repoRoot, stateRoot }) => { + store.applyStore({ repoRoot, stateRoot, target: 'claude' }); + routing.writeRoutingIndex({ repoRoot, stateRoot }); + const filler = Array.from({ length: 6000 }, (_, position) => ({ id: `skill:filler-${position}`, name: `filler-${position}`, + description: 'x'.repeat(900), ownerModuleId: 'm', packId: 'p', dense: { indexes: '', values: '' } })); + republish(stateRoot, index => ({ ...index, entries: [...index.entries, ...filler] })); + assert.throws(() => routing.readRoutingIndex(stateRoot), /integrity|bound/); +})); + +test('an anchored suggestion is not hidden by unanchored candidates ranked above it', () => { + const filler = position => ({ id: `skill:filler-${position}`, name: `filler-${position}`, + description: `migrate database schema safely today with care number ${position}`, ownerModuleId: 'm', packId: 'p' }); + const index = { entries: [1, 2, 3, 4].map(filler) + .concat({ id: 'skill:schema-tool', name: 'schema-tool', description: 'Generic helper', ownerModuleId: 'm', packId: 'p' }) }; + assert.deepEqual(routing.suggestContext(index, 'migrate database schema safely today').map(item => item.id), ['skill:schema-tool']); +}); + +test('a skill edited during the build never publishes an index under the stored generation', () => fixture(({ repoRoot, stateRoot }) => { + store.applyStore({ repoRoot, stateRoot, target: 'claude' }); + const selection = require('../../scripts/lib/context-selection'); + const original = selection.routingEntries; + selection.routingEntries = options => { + const file = path.join(repoRoot, 'skills/feature/SKILL.md'); + fs.writeFileSync(file, fs.readFileSync(file, 'utf8').replace(/description: .*/, 'description: Edited mid-build')); + return original(options); + }; + try { + assert.throws(() => routing.writeRoutingIndex({ repoRoot, stateRoot }), /changed|stale/); + } finally { selection.routingEntries = original; } + assert.equal(routing.readRoutingIndex(stateRoot), null); +})); diff --git a/tests/lib/context-selection.test.js b/tests/lib/context-selection.test.js index 74db84a45..c0e37d4a7 100644 --- a/tests/lib/context-selection.test.js +++ b/tests/lib/context-selection.test.js @@ -319,3 +319,18 @@ test('invalid input and oversized bodies fail closed', () => withFixture(repoRoo overrides: [{ id: 'skill:feature', requiredResources: ['skills/feature/references/details.md'] }] })); assert.throws(() => resolve(repoRoot, { explicitIds: ['skill:feature'] }, { load: true }), /budget/); })); + +test('routing entries drop only coded admission denials; a corrupt source still fails the build', () => withFixture(root => { + const { routingEntries } = require('../../scripts/lib/context-selection'); + write(root, 'skills/shared/SKILL.md', '---\nname: shared\ndescription: Shared helper\ndisable-model-invocation: true\n---\nShared'); + assert.equal(routingEntries({ repoRoot: root, target: 'claude' }).entries.some(entry => entry.id === 'skill:shared'), false); + const file = path.join(root, 'skills/feature/SKILL.md'); + fs.writeFileSync(file, Buffer.concat([fs.readFileSync(file), Buffer.from([0xff, 0x0a])])); + assert.throws(() => routingEntries({ repoRoot: root, target: 'claude' }), /UTF-8/); +})); + +test('admission denials carry stable codes', () => withFixture(root => { + write(root, 'skills/shared/SKILL.md', '---\nname: shared\ndescription: Shared helper\ndisable-model-invocation: true\n---\nShared'); + assert.throws(() => resolve(root, { query: 'shared', proposedIds: ['skill:shared'] }, { load: true }), + error => error.code === 'ECC_CONTEXT_MANUAL_ONLY'); +}));