116 lines
5.1 KiB
JavaScript
116 lines
5.1 KiB
JavaScript
// 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';
|
|
import { transformToOtlpJson } from '../../lib/exporters/otlp-format.mjs';
|
|
import { applyFieldAllowlist } from '../../lib/exporters/field-allowlist.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('SC#6: allowlisted token-usage record → Prometheus + OTLP metrics; PII absent (end-to-end)', () => {
|
|
// Raw record as written to token-usage-stats.jsonl (carries PII for upsert keying).
|
|
const raw = {
|
|
session_id: 'x',
|
|
transcript_path: '/p',
|
|
cwd: '/c',
|
|
ts: '2026-06-26T12:00:00.000Z',
|
|
scope: 'main-context',
|
|
model: 'claude-opus-4-8',
|
|
tokens_input: 12345,
|
|
tokens_output: 6789,
|
|
cost_usd: 0.42,
|
|
is_estimate: false,
|
|
price_table_version: '2026-06-26',
|
|
};
|
|
const rec = applyFieldAllowlist(raw, 'token-usage');
|
|
|
|
// Prometheus: numeric fields auto-promote to voyage_token_usage_* metrics.
|
|
const prom = transformToPrometheus([rec]);
|
|
assert.match(prom, /voyage_token_usage_tokens_input\b/);
|
|
assert.match(prom, /voyage_token_usage_cost_usd\b/);
|
|
// CWE-212: PII must never appear in exporter output.
|
|
assert.ok(!prom.includes('session_id'), 'session_id leaked into Prometheus output');
|
|
assert.ok(!prom.includes('transcript_path'), 'transcript_path leaked into Prometheus output');
|
|
assert.ok(!prom.includes('/p'), 'transcript_path value leaked into Prometheus output');
|
|
|
|
// OTLP parity: dot-separated, dash-preserved naming (NOT the Prometheus underscore form).
|
|
const otlp = JSON.stringify(transformToOtlpJson([rec]));
|
|
assert.ok(otlp.includes('voyage.token-usage.tokens_input'), 'OTLP token metric missing');
|
|
assert.ok(!otlp.includes('transcript_path'), 'transcript_path leaked into OTLP output');
|
|
});
|
|
|
|
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);
|
|
});
|