P10's model-free part (wave 4, order A1, decision 63). The routing test now
checks every case in evals/ for from=, not only the skill-tagged ones (R1):
a case with no skill tag is not routed, only checked, and the report gives
both counts ("45 cases checked for from=, 37 routed"). The eight test-set
cases get from=proevesett-eval-suite (d311f3b) or
from=proevesett-intent-gate (3892a83), the commits that added them.
The release gate that reads the last test-set run is written into the
runbook as exempted with its reason (it waits for the fixed test set to run
again) and still says where it goes (scripts/release-check.mjs).
review-630's prototype-tag stub gets from=test: under R1 an unrouted case is
still checked for from=, and the stub tests that a prototype name is never
routed, which stands.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
82 lines
4.7 KiB
JavaScript
82 lines
4.7 KiB
JavaScript
// tests/lib/p10-model-free.test.mjs
|
|
// P10's model-free part (wave 4, order A1). Every case in evals/ names the
|
|
// event it came from (`from=`), not only the skill cases; the routing check
|
|
// covers every case and says how many it checked for `from=` and how many it
|
|
// routed; and the release gate that would read the last test-set run is
|
|
// written down as exempted with a reason, not built.
|
|
|
|
import { test } from 'node:test';
|
|
import { strict as assert } from 'node:assert';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { mkdtempSync, mkdirSync, writeFileSync, readdirSync, readFileSync, existsSync, rmSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { dirname, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { readFrontmatterFields } from '../../lib/skills/skill-frontmatter.mjs';
|
|
import { checkRouting } from '../../lib/skills/routing.mjs';
|
|
|
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
const CLI = join(ROOT, 'lib', 'skills', 'routing.mjs');
|
|
|
|
const tagsOf = (file) => {
|
|
const f = readFrontmatterFields(readFileSync(file, 'utf8')).fields.tags;
|
|
return f && Array.isArray(f.value) ? f.value.map(String) : [];
|
|
};
|
|
|
|
test('P10: every case in evals/ names the event it came from (from=)', () => {
|
|
const dirs = readdirSync(join(ROOT, 'evals')).filter((d) => existsSync(join(ROOT, 'evals', d, 'prompt.md'))).sort();
|
|
assert.ok(dirs.length >= 45, `${dirs.length} cases`);
|
|
const missing = dirs.filter((d) => !tagsOf(join(ROOT, 'evals', d, 'prompt.md')).some((t) => /^from=\S+$/.test(t)));
|
|
assert.deepEqual(missing, [], `${missing.length} of ${dirs.length} cases have no from=: ${missing.join(', ')}`);
|
|
});
|
|
|
|
/** A plugin root with one skill, its 3/2/1 cases, and the extra cases given. */
|
|
function stubRoot(extra) {
|
|
const root = mkdtempSync(join(tmpdir(), 'p10-model-free-'));
|
|
mkdirSync(join(root, 'skills', 'tidy-imports'), { recursive: true });
|
|
writeFileSync(join(root, 'skills', 'tidy-imports', 'SKILL.md'),
|
|
'---\nname: tidy-imports\ndescription: Sorts the import lines of a source file. Use when imports are out of order.\n---\n');
|
|
mkdirSync(join(root, 'commands'), { recursive: true });
|
|
writeFileSync(join(root, 'commands', 'rotate-logs.md'), '---\nname: rotate-logs\ndescription: Compresses and deletes old log files on disk.\n---\n');
|
|
const cases = {
|
|
p1: ['skill-trigger, skill=tidy-imports, from=test', 'sort the import lines'],
|
|
p2: ['skill-trigger, skill=tidy-imports, from=test', 'import lines are out of order'],
|
|
p3: ['skill-trigger, skill=tidy-imports, from=test', 'sort imports in the source file'],
|
|
n1: ['skill-no-trigger, skill=tidy-imports, owner=rotate-logs, from=test', 'delete old log files'],
|
|
n2: ['skill-no-trigger, skill=tidy-imports, owner=rotate-logs, from=test', 'compress the logs on disk'],
|
|
e1: ['skill-effect, skill=tidy-imports, from=test', 'x'],
|
|
...extra,
|
|
};
|
|
for (const [n, [tags, prompt]] of Object.entries(cases)) {
|
|
mkdirSync(join(root, 'evals', n), { recursive: true });
|
|
writeFileSync(join(root, 'evals', n, 'prompt.md'), `---\ndescription: "case ${n}"\ntags: [${tags}]\nruns: 1\n---\n\n${prompt}\n`);
|
|
}
|
|
return root;
|
|
}
|
|
|
|
test('P10 (R1): a case with no skill tag and no from= is a finding of the routing check', () => {
|
|
const root = stubRoot({ 'plain-gate': ['proevesett', '/voyage:trekplan'], 'plain-sourced': ['proevesett, from=test', '/voyage:trekplan'] });
|
|
try {
|
|
const r = checkRouting(root);
|
|
const src = r.errors.filter((e) => e.code === 'ROUTING_CASE_NO_SOURCE').map((e) => e.case);
|
|
assert.deepEqual(src, ['plain-gate'], JSON.stringify(r.errors));
|
|
assert.ok(!r.errors.some((e) => e.case === 'plain-sourced'), 'a sourced case with no skill tag is not routed');
|
|
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
});
|
|
|
|
test('P10 (R1): the report shows the denominators: cases checked for from= and cases routed', () => {
|
|
const root = stubRoot({ 'plain-sourced': ['proevesett, from=test', '/voyage:trekplan'] });
|
|
try {
|
|
const r = checkRouting(root);
|
|
assert.deepEqual([r.stats.sourced, r.stats.cases], [7, 6], JSON.stringify(r.stats));
|
|
const out = spawnSync('node', [CLI, '--root', root], { encoding: 'utf8' });
|
|
assert.match(out.stdout, /7 cases checked for from=, 6 routed/, out.stdout + out.stderr);
|
|
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
});
|
|
|
|
// Found by its words, not its heading: the section title is set at the release.
|
|
test('P10: the CHANGELOG says the eval gate is exempted, with the reason', () => {
|
|
const md = readFileSync(join(ROOT, 'CHANGELOG.md'), 'utf8');
|
|
assert.ok(/^- [^\n]*eval gate \(P10\)[^\n]*exempted[^\n]*test set runs again/m.test(md),
|
|
'a CHANGELOG line must say the eval gate (P10) is exempted until the test set runs again');
|
|
});
|