import assert from 'node:assert/strict';
import test from 'node:test';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
const TMP_SPOOL = fs.mkdtempSync(path.join(os.tmpdir(), 'jiuwen-spool-'));
process.env.AGENT_INSIGHT_JIUWEN_SPOOL_DIR = TMP_SPOOL;
import {
appendJiuwenSpans,
readJiuwenSessionIndex,
readJiuwenSpansForKeys,
pruneJiuwenSpool,
getJiuwenSpoolDir,
} from '@/lib/ingest/otel/jiuwen/spool';
import type { JiuwenSpan } from '@/lib/ingest/otel/jiuwen/aggregate';
function span(over: Partial<JiuwenSpan> & { spanId: string }): JiuwenSpan {
return {
name: 'agent.step',
traceId: 'trace',
parentSpanId: undefined,
attrs: {},
startNs: 0,
endNs: 1,
...over,
};
}
test('single-agent traces sharing the hard-coded session are NOT stitched', () => {
appendJiuwenSpans([
span({ spanId: 'a1', traceId: 'traceA', attrs: { 'agentteam.session.id': 'acp_cli_session' } }),
span({ spanId: 'b1', traceId: 'traceB', attrs: { 'agentteam.session.id': 'acp_cli_session' } }),
]);
const { multiTraceSessions, sessionToKeys, keyToSession } = readJiuwenSessionIndex();
assert.equal(multiTraceSessions.has('acp_cli_session'), false);
assert.equal(keyToSession.get('traceA'), 'acp_cli_session');
assert.deepEqual([...(sessionToKeys.get('acp_cli_session') ?? [])].sort(), ['traceA', 'traceB']);
const aSpans = readJiuwenSpansForKeys(['traceA']);
assert.deepEqual(aSpans.map((s) => s.spanId), ['a1']);
});
test('team run with a marker span stitches sibling traces by session', () => {
appendJiuwenSpans([
span({ spanId: 'c1', traceId: 'traceC', name: 'team.start', attrs: { 'agentteam.session.id': 'sess_x' } }),
span({ spanId: 'd1', traceId: 'traceD', name: 'agent.step', attrs: { 'agentteam.session.id': 'sess_x' } }),
]);
const { multiTraceSessions, sessionToKeys } = readJiuwenSessionIndex();
assert.equal(multiTraceSessions.has('sess_x'), true);
assert.deepEqual([...(sessionToKeys.get('sess_x') ?? [])].sort(), ['traceC', 'traceD']);
const group = readJiuwenSpansForKeys(['traceC', 'traceD']);
assert.deepEqual(group.map((s) => s.spanId).sort(), ['c1', 'd1']);
});
test('spanId is deduped across batches on read-back', () => {
appendJiuwenSpans([span({ spanId: 'dup', traceId: 'traceDup' })]);
appendJiuwenSpans([span({ spanId: 'dup', traceId: 'traceDup', endNs: 999 })]);
const got = readJiuwenSpansForKeys(['traceDup']);
assert.equal(got.length, 1);
assert.equal(got[0].spanId, 'dup');
});
test('spans and index are written to disk (survive restart)', () => {
appendJiuwenSpans([
span({ spanId: 'p1', traceId: 'tracePersist', name: 'team.x', attrs: { 'agentteam.session.id': 'sess_p' } }),
]);
const bucket = path.join(getJiuwenSpoolDir(), 'buckets', 'tracePersist.jsonl');
const index = path.join(getJiuwenSpoolDir(), 'session-index.jsonl');
assert.equal(fs.existsSync(bucket), true);
assert.equal(fs.existsSync(index), true);
assert.match(fs.readFileSync(index, 'utf8'), /sess_p/);
});
test('pruneJiuwenSpool removes stale buckets and prunes their index rows', () => {
appendJiuwenSpans([
span({ spanId: 'old1', traceId: 'traceOld', attrs: { 'agentteam.session.id': 'sess_old' } }),
]);
const oldBucket = path.join(getJiuwenSpoolDir(), 'buckets', 'traceOld.jsonl');
assert.equal(fs.existsSync(oldBucket), true);
const tenDaysAgo = Date.now() - 10 * 24 * 60 * 60 * 1000;
fs.utimesSync(oldBucket, new Date(tenDaysAgo), new Date(tenDaysAgo));
pruneJiuwenSpool();
assert.equal(fs.existsSync(oldBucket), false, 'stale bucket should be deleted');
const indexAfter = fs.readFileSync(path.join(getJiuwenSpoolDir(), 'session-index.jsonl'), 'utf8');
assert.equal(/traceOld/.test(indexAfter), false, 'index rows for deleted bucket should be gone');
assert.match(indexAfter, /traceC/);
});
test.after(() => {
try {
fs.rmSync(TMP_SPOOL, { recursive: true, force: true });
} catch {
}
});