Files
wehub-resource-sync 979fb22d7c
CI / test (20, macos-latest) (push) Has been cancelled
CI / test (20, ubuntu-latest) (push) Has been cancelled
CI / test (22, macos-latest) (push) Has been cancelled
CI / test (22, ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:01:18 +08:00

146 lines
5.3 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { parseJsonlText } from "../src/replay/jsonl-parser.js";
import { projectTimeline } from "../src/replay/timeline.js";
const fx = (name: string) =>
readFileSync(join(__dirname, "fixtures/jsonl", name), "utf-8");
describe("parseJsonlText", () => {
it("parses basic user/assistant exchange", () => {
const out = parseJsonlText(fx("basic.jsonl"));
expect(out.sessionId).toBe("sess-basic");
expect(out.project).toBe("project");
expect(out.cwd).toBe("/Users/alice/project");
expect(out.observations).toHaveLength(2);
expect(out.observations[0].hookType).toBe("prompt_submit");
expect(out.observations[0].userPrompt).toBe("Fix the login bug");
expect(out.observations[1].hookType).toBe("stop");
expect(out.observations[1].assistantResponse).toBe("Looking into it now.");
});
it("parses tool_use + tool_result pairs", () => {
const out = parseJsonlText(fx("tool-use.jsonl"));
expect(out.sessionId).toBe("sess-tool");
const kinds = out.observations.map((o) => o.hookType);
expect(kinds).toEqual([
"prompt_submit",
"pre_tool_use",
"post_tool_use",
"stop",
]);
const toolCall = out.observations[1];
expect(toolCall.toolName).toBe("Bash");
expect((toolCall.toolInput as { command: string }).command).toBe("ls");
const toolResult = out.observations[2];
expect(toolResult.toolOutput).toBe("README.md\nsrc\n");
});
it("tolerates malformed lines and marks tool errors", () => {
const out = parseJsonlText(fx("errors.jsonl"));
const errObs = out.observations.find((o) => o.hookType === "post_tool_failure");
expect(errObs).toBeDefined();
expect(errObs?.toolOutput).toBe("exit 1");
});
it("falls back to generated sessionId when missing", () => {
const text = JSON.stringify({
type: "user",
timestamp: "2026-01-01T00:00:00.000Z",
message: { role: "user", content: [{ type: "text", text: "hi" }] },
});
const out = parseJsonlText(text);
expect(out.sessionId).toMatch(/^sess_/);
});
it("returns empty observations for blank input", () => {
const out = parseJsonlText("");
expect(out.observations).toHaveLength(0);
});
it("prefers the file's sessionId over the fallback", () => {
const text = [
JSON.stringify({
type: "user",
sessionId: "real-session-from-file",
timestamp: "2026-01-01T00:00:00.000Z",
message: { role: "user", content: [{ type: "text", text: "hi" }] },
}),
].join("\n");
const out = parseJsonlText(text, "fallback-should-be-ignored");
expect(out.sessionId).toBe("real-session-from-file");
for (const obs of out.observations) {
expect(obs.sessionId).toBe("real-session-from-file");
}
});
it("returns the same sessionId across repeated parses of one file", () => {
const text = JSON.stringify({
type: "user",
sessionId: "stable-id",
timestamp: "2026-01-01T00:00:00.000Z",
message: { role: "user", content: [{ type: "text", text: "hi" }] },
});
const a = parseJsonlText(text, "fb-1");
const b = parseJsonlText(text, "fb-2");
expect(a.sessionId).toBe("stable-id");
expect(b.sessionId).toBe("stable-id");
});
it("uses the fallback only when the file has no sessionId", () => {
const text = JSON.stringify({
type: "user",
timestamp: "2026-01-01T00:00:00.000Z",
message: { role: "user", content: [{ type: "text", text: "hi" }] },
});
const out = parseJsonlText(text, "fb-used");
expect(out.sessionId).toBe("fb-used");
});
});
describe("projectTimeline", () => {
it("preserves ordering and computes offsets from real timestamps", () => {
const parsed = parseJsonlText(fx("tool-use.jsonl"));
const tl = projectTimeline(parsed.observations);
expect(tl.eventCount).toBe(4);
expect(tl.events[0].kind).toBe("prompt");
expect(tl.events[1].kind).toBe("tool_call");
expect(tl.events[2].kind).toBe("tool_result");
expect(tl.events[3].kind).toBe("response");
expect(tl.events[0].offsetMs).toBe(0);
expect(tl.events[3].offsetMs).toBeGreaterThan(0);
});
it("synthesizes pacing when all timestamps identical", () => {
const parsed = parseJsonlText(fx("basic.jsonl"));
for (const obs of parsed.observations) obs.timestamp = "2026-04-17T10:00:00.000Z";
const tl = projectTimeline(parsed.observations);
expect(tl.events[0].offsetMs).toBe(0);
expect(tl.events[1].offsetMs).toBeGreaterThanOrEqual(300);
});
it("returns empty timeline for no observations", () => {
const tl = projectTimeline([]);
expect(tl.eventCount).toBe(0);
expect(tl.totalDurationMs).toBe(0);
expect(tl.events).toHaveLength(0);
});
it("marks errored tool results as tool_error kind", () => {
const parsed = parseJsonlText(fx("errors.jsonl"));
const tl = projectTimeline(parsed.observations);
expect(tl.events.some((e) => e.kind === "tool_error")).toBe(true);
});
it("uses one shared fallback timestamp when metadata missing", () => {
const text = JSON.stringify({
type: "user",
message: { role: "user", content: [{ type: "text", text: "hi" }] },
});
const out = parseJsonlText(text);
expect(out.startedAt).toBe(out.endedAt);
});
});