import { mkdirSync, mkdtempSync, rmSync, utimesSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { computeEventsCockpit } from "../src/server/api/cockpit-events.js";
const DAY = 86_400_000;
const NOW = Date.UTC(2026, 4, 1, 12, 0, 0);
function isoAt(ms: number): string {
return new Date(ms).toISOString();
}
interface MakeEventsArgs {
toolIntents?: Array<{ ts: number; callId: string; name: string; args?: string }>;
toolResults?: Array<{ ts: number; callId: string; ok: boolean }>;
toolDenies?: Array<{ ts: number; callId: string }>;
planSubmissions?: Array<{
ts: number;
id: number;
body: string;
steps: Array<{ id: string; title: string }>;
}>;
stepCompletions?: Array<{ ts: number; stepId: string }>;
}
function eventLines(args: MakeEventsArgs): string {
const lines: string[] = [];
let id = 1;
for (const i of args.toolIntents ?? []) {
lines.push(
JSON.stringify({
id: id++,
ts: isoAt(i.ts),
turn: 1,
type: "tool.intent",
callId: i.callId,
name: i.name,
args: i.args ?? "{}",
}),
);
}
for (const r of args.toolResults ?? []) {
lines.push(
JSON.stringify({
id: id++,
ts: isoAt(r.ts),
turn: 1,
type: "tool.result",
callId: r.callId,
ok: r.ok,
output: "",
durationMs: 100,
}),
);
}
for (const d of args.toolDenies ?? []) {
lines.push(
JSON.stringify({
id: id++,
ts: isoAt(d.ts),
turn: 1,
type: "tool.denied",
callId: d.callId,
reason: "permission",
}),
);
}
for (const p of args.planSubmissions ?? []) {
lines.push(
JSON.stringify({
id: p.id,
ts: isoAt(p.ts),
turn: 1,
type: "plan.submitted",
body: p.body,
steps: p.steps.map((s) => ({ id: s.id, title: s.title, action: "" })),
}),
);
}
for (const c of args.stepCompletions ?? []) {
lines.push(
JSON.stringify({
id: id++,
ts: isoAt(c.ts),
turn: 1,
type: "plan.step.completed",
stepId: c.stepId,
completion: { kind: "ok" },
}),
);
}
return `${lines.join("\n")}\n`;
}
describe("computeEventsCockpit", () => {
let dir: string;
let sessionsDir: string;
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), "rx-cockpit-events-"));
sessionsDir = join(dir, "sessions");
mkdirSync(sessionsDir, { recursive: true });
});
afterEach(() => {
rmSync(dir, { recursive: true, force: true });
});
function writeSession(name: string, body: string): void {
const path = join(sessionsDir, `${name}.events.jsonl`);
writeFileSync(path, body);
}
it("returns nulls when sessionsDir doesn't exist", () => {
const out = computeEventsCockpit(NOW, join(dir, "no-such-dir"));
expect(out.toolCalls24h).toBeNull();
expect(out.recentPlans).toBeNull();
expect(out.toolActivity).toBeNull();
});
it("returns null fields when no event files are present", () => {
const out = computeEventsCockpit(NOW, sessionsDir);
expect(out.toolCalls24h).toBeNull();
});
it("counts tool.intent events in the trailing 24h", () => {
writeSession(
"s1",
eventLines({
toolIntents: [
{ ts: NOW - 1_000, callId: "c1", name: "run_command" },
{ ts: NOW - 12 * 60 * 60 * 1000, callId: "c2", name: "edit_file" },
{ ts: NOW - 26 * 60 * 60 * 1000, callId: "c3", name: "read_file" },
],
}),
);
const out = computeEventsCockpit(NOW, sessionsDir);
expect(out.toolCalls24h?.total).toBe(2);
});
it("computes delta vs the prior 24h window", () => {
writeSession(
"s1",
eventLines({
toolIntents: [
{ ts: NOW - 1_000, callId: "c1", name: "x" },
{ ts: NOW - 12 * 3_600_000, callId: "c2", name: "x" },
{ ts: NOW - 30 * 3_600_000, callId: "c3", name: "x" },
],
}),
);
const out = computeEventsCockpit(NOW, sessionsDir);
expect(out.toolCalls24h?.total).toBe(2);
expect(out.toolCalls24h?.delta).toBe(1);
});
it("surfaces recent tool activity newest-first with ok / err / warn levels", () => {
writeSession(
"s1",
eventLines({
toolIntents: [
{
ts: NOW - 5_000,
callId: "c1",
name: "run_command",
args: '{"command":"npm run build"}',
},
{ ts: NOW - 4_000, callId: "c2", name: "edit_file", args: '{"path":"src/index.ts"}' },
{ ts: NOW - 3_000, callId: "c3", name: "shell", args: '{"command":"rm -rf"}' },
],
toolResults: [
{ ts: NOW - 4_900, callId: "c1", ok: true },
{ ts: NOW - 3_900, callId: "c2", ok: false },
],
toolDenies: [{ ts: NOW - 2_900, callId: "c3" }],
}),
);
const out = computeEventsCockpit(NOW, sessionsDir);
expect(out.toolActivity).toHaveLength(3);
expect(out.toolActivity![0]!.name).toBe("shell");
expect(out.toolActivity![0]!.level).toBe("warn");
expect(out.toolActivity![1]!.level).toBe("err");
expect(out.toolActivity![2]!.level).toBe("ok");
});
it("rolls up plans with completion ratio + done/active status", () => {
writeSession(
"s1",
eventLines({
planSubmissions: [
{
ts: NOW - 60_000,
id: 100,
body: "release 0.18.1",
steps: [
{ id: "a", title: "tag" },
{ id: "b", title: "publish" },
],
},
],
stepCompletions: [
{ ts: NOW - 50_000, stepId: "a" },
{ ts: NOW - 40_000, stepId: "b" },
],
}),
);
const out = computeEventsCockpit(NOW, sessionsDir);
expect(out.recentPlans).toHaveLength(1);
expect(out.recentPlans![0]!.title).toBe("release 0.18.1");
expect(out.recentPlans![0]!.totalSteps).toBe(2);
expect(out.recentPlans![0]!.completedSteps).toBe(2);
expect(out.recentPlans![0]!.status).toBe("done");
});
it("marks a partially-completed plan as active", () => {
writeSession(
"s1",
eventLines({
planSubmissions: [
{
ts: NOW - 60_000,
id: 100,
body: "wip",
steps: [
{ id: "a", title: "x" },
{ id: "b", title: "y" },
{ id: "c", title: "z" },
],
},
],
stepCompletions: [{ ts: NOW - 50_000, stepId: "a" }],
}),
);
const out = computeEventsCockpit(NOW, sessionsDir);
expect(out.recentPlans![0]!.status).toBe("active");
expect(out.recentPlans![0]!.completedSteps).toBe(1);
});
it("aggregates tool calls across multiple session files", () => {
writeSession(
"s1",
eventLines({
toolIntents: [{ ts: NOW - 1_000, callId: "a", name: "x" }],
}),
);
writeSession(
"s2",
eventLines({
toolIntents: [{ ts: NOW - 2_000, callId: "b", name: "y" }],
}),
);
const out = computeEventsCockpit(NOW, sessionsDir);
expect(out.toolCalls24h?.total).toBe(2);
});
it("skips event files older than 30 days based on mtime", () => {
writeSession(
"stale",
eventLines({
toolIntents: [{ ts: NOW - 1_000, callId: "x", name: "should-not-count" }],
}),
);
const stalePath = join(sessionsDir, "stale.events.jsonl");
const ancient = (NOW - 31 * DAY) / 1000;
utimesSync(stalePath, ancient, ancient);
writeSession(
"fresh",
eventLines({
toolIntents: [{ ts: NOW - 500, callId: "y", name: "should-count" }],
}),
);
const out = computeEventsCockpit(NOW, sessionsDir);
expect(out.toolCalls24h?.total).toBe(1);
});
});