The retired --profile is one list the parser drops (with its value, never an error) instead of a valued flag in six schemas, so the removed-surfaces check exempts one parser line, not seven; seen red on the old parser. The empty OPTIONAL_STRING_KEYS machinery, a one-item loop and an unused plugin-data dir go; the removed-surfaces check reads each file once; the Phase 8 slice in executor-prose-pins asserts both anchors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
144 lines
6.4 KiB
JavaScript
144 lines
6.4 KiB
JavaScript
// tests/lib/removed-surfaces.test.mjs
|
|
// Cleanup R-b removed three surfaces: Voyage's own measurement, statistics and
|
|
// export (R1, decision 189; Claude Code's own OpenTelemetry took over), the
|
|
// STORM loop and the research engine choice (R2, decision 190), and the
|
|
// profiles, phase signals and effort dialog (R3, decision 191). No tracked
|
|
// file may name them again, so a removed surface cannot come back unseen.
|
|
//
|
|
// Scope: every tracked text file except CHANGELOG.md (history), *.local.*
|
|
// files (never tracked) and this test. A term is a plain substring, except a
|
|
// flag, which must stand as its own token (`--engine`, `--profile`).
|
|
//
|
|
// Every group always runs. When this test fails, remove the mention; add an
|
|
// EXEMPT entry only for a line that must keep the term, with the reason.
|
|
|
|
import { test } from 'node:test';
|
|
import { strict as assert } from 'node:assert';
|
|
import { execFileSync } from 'node:child_process';
|
|
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
|
|
import { join, dirname } from 'node:path';
|
|
import { tmpdir } from 'node:os';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
const ROOT = join(HERE, '..', '..');
|
|
const SELF = 'tests/lib/removed-surfaces.test.mjs';
|
|
|
|
const flag = name => new RegExp(`(?<![\\w-])${name}(?![\\w-])`);
|
|
|
|
const GROUPS = {
|
|
R1: [
|
|
'lib/stats/', 'lib/exporters/', 'otel-export', 'post-bash-stats', 'stats-append', 'event-emit',
|
|
'yardstick.mjs', 'scripts/yardstick', 'end-state-gate', 'end-state-registry', 'comparison-gate',
|
|
'brief-approved', '-stats.jsonl', 'main-merge-gate', 'main-merge-approved', 'main-merge-declined',
|
|
'VOYAGE_EXPORT_MODE', 'VOYAGE_OTEL_ENDPOINT', 'VOYAGE_TEXTFILE_DIR', 'VOYAGE_OTEL_ALLOW_PRIVATE',
|
|
'VOYAGE_TOKEN_METER', 'gold-scorer', 'synthesis-measure',
|
|
],
|
|
R2: [
|
|
'VOYAGE_STORM_ENABLED', 'TREKRESEARCH_MAX_CONV_TURNS', 'VOYAGE_DISABLE_CAP_HOOK', 'VOYAGE_CAP_SCOPE_TTL_MS',
|
|
'research-loop-cap', 'pre-agent-cap', 'storm-measure', flag('--engine'), 'effective_engine',
|
|
],
|
|
R3: [
|
|
'lib/profiles/', flag('--profile'), 'phase_signals', 'phase_signal_result', 'resolve-phase-model',
|
|
'docs/profiles.md', 'MANIFEST_PROFILE_DRIFT', 'BRIEF_V51_MISSING_SIGNALS', 'VOYAGE_PROFILE',
|
|
'VOYAGE_ALLOW_HAIKU', 'voyage-profiles/', 'resolveTrekcontinueProfile', 'profile_used',
|
|
],
|
|
};
|
|
|
|
// Named exceptions: "path:line" -> reason. Each must still hit (a stale
|
|
// exemption fails below).
|
|
const EXEMPT = new Map([
|
|
['docs/HANDOVER-CONTRACTS.md:81', 'public contract: an older brief\'s key is ignored (decisions 179/191)'],
|
|
['docs/proevesett.md:139', 'dated v1/v2 measurement record of the fixed test set, kept unedited'],
|
|
['docs/proevesett.md:143', 'dated v1/v2 measurement record of the fixed test set, kept unedited'],
|
|
['docs/proevesett.md:146', 'dated v1/v2 measurement record of the fixed test set, kept unedited'],
|
|
['lib/parsers/arg-parser.mjs:18', 'retired flag swallows its value'],
|
|
['tests/lib/arg-parser.test.mjs:285', 'retired flag swallows its value'],
|
|
['tests/lib/arg-parser.test.mjs:286', 'retired flag swallows its value'],
|
|
['tests/lib/arg-parser.test.mjs:289', 'retired flag swallows its value'],
|
|
['tests/lib/arg-parser.test.mjs:291', 'retired flag swallows its value'],
|
|
['tests/validators/brief-validator.test.mjs:200', 'older brief stays valid (decision 191)'],
|
|
['tests/validators/brief-validator.test.mjs:203', 'older brief stays valid (decision 191)'],
|
|
['tests/validators/brief-validator.test.mjs:205', 'older brief stays valid (decision 191)'],
|
|
]);
|
|
|
|
const matches = (term, line) => (typeof term === 'string' ? line.includes(term) : term.test(line));
|
|
|
|
// Each file is read and split once for all groups; a binary file has no lines.
|
|
const lineCache = new Map();
|
|
function linesOf(root, rel) {
|
|
const path = join(root, rel);
|
|
if (!lineCache.has(path)) {
|
|
const buf = readFileSync(path);
|
|
lineCache.set(path, buf.includes(0) ? [] : buf.toString('utf-8').split('\n'));
|
|
}
|
|
return lineCache.get(path);
|
|
}
|
|
|
|
// "path:line" for every line of the given files that names a term of the group.
|
|
function scan(root, files, terms) {
|
|
const hits = [];
|
|
for (const rel of files) {
|
|
linesOf(root, rel).forEach((line, i) => {
|
|
if (terms.some(t => matches(t, line))) hits.push(`${rel}:${i + 1}`);
|
|
});
|
|
}
|
|
return hits;
|
|
}
|
|
|
|
function inScope(rel) {
|
|
return rel !== 'CHANGELOG.md' && rel !== SELF && !/\.local\./.test(rel);
|
|
}
|
|
|
|
let files = null;
|
|
function tracked() {
|
|
if (!files) {
|
|
files = execFileSync('git', ['ls-files', '-z'], { cwd: ROOT, encoding: 'utf-8' })
|
|
.split('\0').filter(Boolean).filter(inScope);
|
|
}
|
|
return files;
|
|
}
|
|
|
|
test('scan finds a term, and a flag only as its own token (known-positive)', () => {
|
|
const dir = mkdtempSync(join(tmpdir(), 'removed-surfaces-'));
|
|
try {
|
|
mkdirSync(join(dir, 'docs'), { recursive: true });
|
|
writeFileSync(join(dir, 'docs', 'a.md'), [
|
|
'Set VOYAGE_STORM_ENABLED=1 first.',
|
|
'Run with --engine deep-research.',
|
|
'The engine-findings compare stays; so does --engines-list.',
|
|
'Pass `--profile economy`.',
|
|
].join('\n'));
|
|
assert.deepEqual(scan(dir, ['docs/a.md'], GROUPS.R2), ['docs/a.md:1', 'docs/a.md:2']);
|
|
assert.deepEqual(scan(dir, ['docs/a.md'], GROUPS.R3), ['docs/a.md:4']);
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('scope leaves out history, local files and this test', () => {
|
|
assert.ok(inScope('commands/trekresearch.md'));
|
|
assert.ok(inScope('lib/validators/brief-validator.mjs'));
|
|
assert.ok(!inScope('CHANGELOG.md'));
|
|
assert.ok(!inScope('docs/notes.local.md'));
|
|
assert.ok(!inScope(SELF));
|
|
});
|
|
|
|
for (const group of Object.keys(GROUPS)) {
|
|
test(`${group}: no tracked file names a removed surface`, () => {
|
|
const all = tracked();
|
|
assert.ok(all.length > 100, `git ls-files returned ${all.length} files; refusing to pass on an empty scan`);
|
|
const hits = scan(ROOT, all, GROUPS[group]).filter(h => !EXEMPT.has(h));
|
|
assert.deepEqual(hits, [], `${hits.length} line(s) still name a ${group} surface:\n${hits.join('\n')}`);
|
|
});
|
|
}
|
|
|
|
test('every EXEMPT entry still names a removed surface', () => {
|
|
const terms = Object.values(GROUPS).flat();
|
|
const all = new Set(tracked());
|
|
for (const key of EXEMPT.keys()) {
|
|
const rel = key.replace(/:\d+$/, '');
|
|
assert.ok(all.has(rel), `EXEMPT names untracked ${rel}`);
|
|
assert.ok(scan(ROOT, [rel], terms).includes(key), `EXEMPT entry "${key}" no longer hits; remove it`);
|
|
}
|
|
});
|