Files
redsen-lean-harness/tests/unit.test.mjs
T
mozempkandCopilot 383129f571 feat: scaffold redsen-lean-harness v0.1.0
Recovered from crashed session (Node OOM). Repo contains full P0-P6
scaffold: plugin.json/marketplace.json, AGENTS.md, ADRs 0001-0006,
lh CLI (init/index/graph/lane/run/memory/host/report/doctor), 10
.github/agents, 12 CLI skills, instructions, context7 mcp.json, and
unit/e2e test suite.

Fixed: run.mjs read --in-tokens/--out-tokens but tests and CLI docs
use --input-tokens/--output-tokens, so telemetry totals were always 0.
Now accepts both forms.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-09-09 22:44:15 +02:00

159 lines
5.5 KiB
JavaScript

import { test, describe, after } from 'node:test';
import assert from 'node:assert/strict';
import { defaultConfig, validateConfig, modelForRole, MEMORY_SHARDS, RALPH_EXIT_CRITERIA } from '../src/lib/config.mjs';
import { globsIntersect } from '../src/lib/lane.mjs';
import { scanSecrets } from '../src/lib/memory.mjs';
import { summarize } from '../src/lib/telemetry.mjs';
import { globMatch } from '../src/lib/repomap.mjs';
import { cleanup } from './helpers.mjs';
after(cleanup);
describe('config', () => {
test('default config is valid', () => {
assert.deepEqual(validateConfig(defaultConfig()), []);
});
test('rejects a bad isolation backend', () => {
const c = defaultConfig();
c.isolation.backend = 'kubernetes';
assert.ok(validateConfig(c).some((p) => p.includes('isolation.backend')));
});
test('rejects an unknown ralph exit criterion', () => {
const c = defaultConfig();
c.ralph.exitCriteria = ['vibes'];
assert.ok(validateConfig(c).some((p) => p.includes('vibes')));
});
test('rejects a non-positive memory budget', () => {
const c = defaultConfig();
c.memory.tokenBudget = 0;
assert.ok(validateConfig(c).some((p) => p.includes('memory.tokenBudget')));
});
test('cheap roles resolve to the cheap tier model', () => {
const c = defaultConfig();
const cheap = modelForRole(c, 'scout');
assert.equal(cheap, modelForRole(c, 'verifier'));
assert.notEqual(cheap, modelForRole(c, 'conductor'));
});
test('a role override is honoured', () => {
const c = defaultConfig();
c.models.roles.scout = 'strong';
assert.equal(modelForRole(c, 'scout'), modelForRole(c, 'conductor'));
});
test('exposes the five memory shards plus seed', () => {
for (const s of ['failures', 'corrections', 'insights', 'conventions', 'quirks']) {
assert.ok(MEMORY_SHARDS.includes(s), `missing shard ${s}`);
}
assert.ok(RALPH_EXIT_CRITERIA.length >= 5);
});
});
describe('lane scope safety', () => {
// The load-bearing invariant: two write lanes must never share a file.
const overlapping = [
['src/**', 'src/a/deep/**'],
['src/a/**', 'src/a/**'],
['**', 'anything/at/all.js'],
['src/a.js', 'src/a.js'],
['src/*.js', 'src/a.js'],
['lib/**/*.ts', 'lib/deep/nested/x.ts'],
];
for (const [a, b] of overlapping) {
test(`rejects overlap: ${a} vs ${b}`, () => {
assert.equal(globsIntersect(a, b), true, `${a} should intersect ${b}`);
assert.equal(globsIntersect(b, a), true, 'intersection must be symmetric');
});
}
const disjoint = [
['src/a/**', 'src/b/**'],
['src/a.js', 'src/b.js'],
['docs/**', 'src/**'],
['lib/**/*.ts', 'lib/x.js'],
];
for (const [a, b] of disjoint) {
test(`allows disjoint: ${a} vs ${b}`, () => {
assert.equal(globsIntersect(a, b), false, `${a} should not intersect ${b}`);
assert.equal(globsIntersect(b, a), false, 'disjointness must be symmetric');
});
}
});
describe('glob matching', () => {
test('** crosses directory boundaries', () => {
assert.equal(globMatch('src/**', 'src/a/b/c.js'), true);
});
test('* does not cross directory boundaries', () => {
assert.equal(globMatch('src/*.js', 'src/a/b.js'), false);
});
test('non-matching prefix fails', () => {
assert.equal(globMatch('src/**', 'docs/a.js'), false);
});
});
describe('secret scanning', () => {
const secrets = [
['github token', 'token ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'],
['openai key', 'sk-ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrst'],
['private key', '-----BEGIN RSA PRIVATE KEY-----'],
['aws key', 'AKIAIOSFODNN7EXAMPLE'],
];
for (const [name, text] of secrets) {
test(`detects a ${name}`, () => {
const hits = scanSecrets(text);
assert.ok(hits.length > 0, `expected a hit for ${name}`);
});
}
test('does not fire on ordinary prose', () => {
assert.deepEqual(scanSecrets('The build failed because the test timed out after 30s.'), []);
});
test('never echoes the raw secret back', () => {
const raw = 'ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
for (const hit of scanSecrets(`token ${raw}`)) {
assert.ok(!JSON.stringify(hit).includes(raw), 'secret must be redacted in the finding');
}
});
});
describe('telemetry summarize', () => {
test('sums token usage across events', () => {
const s = summarize([
{ ts: '2026-01-01T00:00:00.000Z', type: 'run.start' },
{ ts: '2026-01-01T00:00:10.000Z', type: 'agent.end', 'gen_ai.usage.input_tokens': 100, 'gen_ai.usage.output_tokens': 50 },
{ ts: '2026-01-01T00:01:00.000Z', type: 'agent.end', 'gen_ai.usage.input_tokens': 200, 'gen_ai.usage.output_tokens': 25 },
]);
assert.equal(s.totals.inputTokens, 300);
assert.equal(s.totals.outputTokens, 75);
assert.equal(s.totals.totalTokens, 375);
assert.equal(s.totals.wallMs, 60_000);
});
test('tolerates empty input', () => {
const s = summarize([]);
assert.equal(s.totals.totalTokens, 0);
assert.equal(s.totals.wallMs, 0);
});
test('ignores malformed timestamps rather than producing NaN', () => {
const s = summarize([
{ ts: 'not-a-date', type: 'run.start' },
{ ts: '2026-01-01T00:00:00.000Z', type: 'agent.end', 'gen_ai.usage.input_tokens': 10 },
]);
assert.ok(Number.isFinite(s.totals.wallMs));
assert.equal(s.totals.inputTokens, 10);
});
test('ignores non-numeric token fields', () => {
const s = summarize([{ ts: '2026-01-01T00:00:00.000Z', 'gen_ai.usage.input_tokens': 'lots' }]);
assert.equal(s.totals.inputTokens, 0);
});
});