// tests/hooks/otel-export-textfile.test.mjs // SC #12: lib/exporters/textfile-format.mjs produces deterministic Prometheus // text-format output that matches expected.prom byte-for-byte. // // To regenerate snapshot: // node scripts/gen-expected-prom.mjs > tests/fixtures/expected.prom import { test } from 'node:test'; import { strict as assert } from 'node:assert'; import { readFileSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { transformToPrometheus, normalizeMetricName } from '../../lib/exporters/textfile-format.mjs'; const __dirname = dirname(fileURLToPath(import.meta.url)); const FIXTURES = join(__dirname, '..', 'fixtures'); function loadJsonl(name) { const text = readFileSync(join(FIXTURES, name), 'utf-8'); return text.trim().split('\n').filter(Boolean).map(l => JSON.parse(l)); } test('SC #12: stats-sample.jsonl → expected.prom snapshot byte-for-byte match', () => { const records = loadJsonl('stats-sample.jsonl'); const actual = transformToPrometheus(records); const expected = readFileSync(join(FIXTURES, 'expected.prom'), 'utf-8'); assert.equal(actual, expected, `Snapshot drift detected. Regenerate via:\n` + ` node scripts/gen-expected-prom.mjs > tests/fixtures/expected.prom`); }); test('empty-input handling: [] returns empty string (no headers)', () => { assert.equal(transformToPrometheus([]), ''); assert.equal(transformToPrometheus(null), ''); assert.equal(transformToPrometheus(undefined), ''); }); test('allowlist-redaction: caller-redacted records (without command_excerpt/session_id) emit cleanly', () => { // Simulate an allowlist-applied post-bash-stats record (command_excerpt + session_id removed) const record = { _schema_id: 'post_bash_stats', ts: '2026-05-09T08:00:00.000Z', duration_ms: 152, success: true, }; const out = transformToPrometheus([record]); // Must NOT contain command_excerpt nor session_id (caller's responsibility, but verify) assert.ok(!out.includes('command_excerpt'), 'command_excerpt leaked into output'); assert.ok(!out.includes('session_id'), 'session_id leaked into output'); // Must contain duration_ms metric assert.match(out, /voyage_post_bash_stats_duration_ms/); assert.match(out, / 152$/m); // Boolean coerced to 1 assert.match(out, /voyage_post_bash_stats_success.* 1$/m); }); test('NO client-side timestamps in output (per research/01 node_exporter#1284 mitigation)', () => { const records = loadJsonl('stats-sample.jsonl'); const out = transformToPrometheus(records); const lines = out.split('\n'); for (const line of lines) { if (line.startsWith('#') || line === '') continue; // Sample line format: metric{labels} value [timestamp] // We must NOT have a trailing numeric timestamp after the value. const parts = line.trim().split(' '); assert.ok(parts.length === 2, `Line has unexpected token count (timestamp leaked?): ${line}`); } }); test('normalizeMetricName: dots/dashes/spaces → underscore, lowercase, voyage_ prefix', () => { assert.equal(normalizeMetricName('voyage.hook.duration_ms'), 'voyage_voyage_hook_duration_ms'); assert.equal(normalizeMetricName('Plan-Critic Verdict'), 'voyage_plan_critic_verdict'); assert.equal(normalizeMetricName('METRIC NAME'), 'voyage_metric_name'); }); test('determinism: identical input produces identical output (sorted keys)', () => { const records = loadJsonl('stats-sample.jsonl'); const out1 = transformToPrometheus(records); const out2 = transformToPrometheus([...records]); assert.equal(out1, out2); });