Add test suite: unit tests for parser, discovery, redactor, exporter, and filters
Comprehensive test coverage for all server services and shared modules:
tests/unit/session-parser.test.ts (16 tests):
- Parses every message type: user (string + array content), assistant
(text, thinking, tool_use blocks), progress/hook events (from data
field), file-history-snapshot, summary (from summary field)
- Verifies system metadata (turn_duration) and queue-operation lines
are silently skipped
- Detects <system-reminder> tags and reclassifies as system_message
- Resilience: skips malformed JSONL lines, returns empty for empty
files, preserves UUIDs from source
- Integration: parses full sample-session.jsonl fixture verifying all
9 categories are represented, handles edge-cases.jsonl with corrupt
lines
tests/unit/session-discovery.test.ts (6 tests):
- Discovers sessions from {version, entries} index format
- Handles legacy raw array format
- Gracefully returns empty for missing directories and corrupt JSON
- Aggregates across multiple project directories
- Uses fullPath from index entries when available
tests/unit/sensitive-redactor.test.ts (40+ tests):
- Tier 1 secrets: AWS access keys (AKIA/ASIA), Bedrock keys, GitHub
PATs (ghp_, github_pat_, ghu_, ghs_), GitLab (glpat-, glrt-),
OpenAI (sk-proj-, legacy sk-), Anthropic (api03, admin01),
HuggingFace, Perplexity, Stripe (sk_live/test/prod, rk_*), Slack
(bot token, webhook URL), SendGrid, GCP, Heroku, npm, PyPI, Sentry,
JWT, PEM private keys (RSA + generic), generic API key assignments
- Tier 2 PII: home directory paths (Linux/macOS/Windows), connection
strings (PostgreSQL/MongoDB/Redis), URLs with embedded credentials,
email addresses, IPv4 addresses, Bearer tokens, env var secrets
- False positive resistance: normal code, markdown, short strings,
non-home file paths, version numbers
- Allowlists: example.com/test.com emails, noreply@anthropic.com,
127.0.0.1, 0.0.0.0, RFC 5737 documentation IPs
- Edge cases: empty strings, multiple secrets, category tracking
- redactMessage: preserves uuid/category/timestamp, redacts content
and toolInput, leaves toolName unchanged, doesn't mutate original
tests/unit/html-exporter.test.ts (8 tests):
- Valid DOCTYPE HTML output with no external URL references
- Includes visible messages, excludes filtered and redacted ones
- Inserts redacted dividers at correct positions
- Renders markdown to HTML with syntax highlighting CSS
- Includes session metadata header
tests/unit/filters.test.ts (12 tests):
- Category filtering: include/exclude by enabled set
- Default state: thinking and hook_progress hidden
- All-on/all-off edge cases
- Redacted UUID exclusion
- Search match counting with empty query edge case
- Preset validation (conversation, debug)
- Category count computation with and without redactions
src/client/components/SessionList.test.tsx (7 tests):
- Two-phase navigation: project list → session list → back
- Auto-select project when selectedId matches
- Loading and empty states
- onSelect callback on session click
tests/fixtures/:
- sample-session.jsonl: representative session with all message types
- edge-cases.jsonl: corrupt lines interspersed with valid messages
- sessions-index.json: sample index file for discovery tests
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
219
tests/unit/session-parser.test.ts
Normal file
219
tests/unit/session-parser.test.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { parseSessionContent } from "../../src/server/services/session-parser.js";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
|
||||
describe("session-parser", () => {
|
||||
it("parses user messages with string content", () => {
|
||||
const line = JSON.stringify({
|
||||
type: "user",
|
||||
message: { role: "user", content: "Hello world" },
|
||||
uuid: "u-1",
|
||||
timestamp: "2025-10-15T10:00:00Z",
|
||||
});
|
||||
const msgs = parseSessionContent(line);
|
||||
expect(msgs).toHaveLength(1);
|
||||
expect(msgs[0].category).toBe("user_message");
|
||||
expect(msgs[0].content).toBe("Hello world");
|
||||
expect(msgs[0].uuid).toBe("u-1");
|
||||
expect(msgs[0].timestamp).toBe("2025-10-15T10:00:00Z");
|
||||
});
|
||||
|
||||
it("parses user messages with tool_result array content", () => {
|
||||
const line = JSON.stringify({
|
||||
type: "user",
|
||||
message: {
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "tool_result", tool_use_id: "toolu_01", content: "File contents here" },
|
||||
],
|
||||
},
|
||||
uuid: "u-2",
|
||||
});
|
||||
const msgs = parseSessionContent(line);
|
||||
expect(msgs).toHaveLength(1);
|
||||
expect(msgs[0].category).toBe("tool_result");
|
||||
expect(msgs[0].content).toBe("File contents here");
|
||||
});
|
||||
|
||||
it("parses assistant text blocks", () => {
|
||||
const line = JSON.stringify({
|
||||
type: "assistant",
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "Here is my response" }],
|
||||
},
|
||||
uuid: "a-1",
|
||||
});
|
||||
const msgs = parseSessionContent(line);
|
||||
expect(msgs).toHaveLength(1);
|
||||
expect(msgs[0].category).toBe("assistant_text");
|
||||
expect(msgs[0].content).toBe("Here is my response");
|
||||
});
|
||||
|
||||
it("parses thinking blocks", () => {
|
||||
const line = JSON.stringify({
|
||||
type: "assistant",
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: [{ type: "thinking", thinking: "Let me think about this..." }],
|
||||
},
|
||||
uuid: "a-2",
|
||||
});
|
||||
const msgs = parseSessionContent(line);
|
||||
expect(msgs).toHaveLength(1);
|
||||
expect(msgs[0].category).toBe("thinking");
|
||||
expect(msgs[0].content).toBe("Let me think about this...");
|
||||
});
|
||||
|
||||
it("parses tool_use blocks", () => {
|
||||
const line = JSON.stringify({
|
||||
type: "assistant",
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "tool_use", name: "Read", input: { file_path: "/src/index.ts" } },
|
||||
],
|
||||
},
|
||||
uuid: "a-3",
|
||||
});
|
||||
const msgs = parseSessionContent(line);
|
||||
expect(msgs).toHaveLength(1);
|
||||
expect(msgs[0].category).toBe("tool_call");
|
||||
expect(msgs[0].toolName).toBe("Read");
|
||||
expect(msgs[0].toolInput).toContain("/src/index.ts");
|
||||
});
|
||||
|
||||
it("parses progress messages from data field", () => {
|
||||
const line = JSON.stringify({
|
||||
type: "progress",
|
||||
data: { type: "hook", hookEvent: "PreToolUse", hookName: "security_check" },
|
||||
uuid: "p-1",
|
||||
timestamp: "2025-10-15T10:00:00Z",
|
||||
});
|
||||
const msgs = parseSessionContent(line);
|
||||
expect(msgs).toHaveLength(1);
|
||||
expect(msgs[0].category).toBe("hook_progress");
|
||||
expect(msgs[0].content).toContain("PreToolUse");
|
||||
expect(msgs[0].content).toContain("security_check");
|
||||
});
|
||||
|
||||
it("parses file-history-snapshot messages", () => {
|
||||
const line = JSON.stringify({
|
||||
type: "file-history-snapshot",
|
||||
messageId: "snap-1",
|
||||
snapshot: { messageId: "snap-1", trackedFileBackups: [], timestamp: "2025-10-15T10:00:00Z" },
|
||||
});
|
||||
const msgs = parseSessionContent(line);
|
||||
expect(msgs).toHaveLength(1);
|
||||
expect(msgs[0].category).toBe("file_snapshot");
|
||||
});
|
||||
|
||||
it("parses summary messages from summary field", () => {
|
||||
const line = JSON.stringify({
|
||||
type: "summary",
|
||||
summary: "Session summary here",
|
||||
leafUuid: "msg-10",
|
||||
});
|
||||
const msgs = parseSessionContent(line);
|
||||
expect(msgs).toHaveLength(1);
|
||||
expect(msgs[0].category).toBe("summary");
|
||||
expect(msgs[0].content).toBe("Session summary here");
|
||||
});
|
||||
|
||||
it("skips system metadata lines (turn_duration)", () => {
|
||||
const line = JSON.stringify({
|
||||
type: "system",
|
||||
subtype: "turn_duration",
|
||||
durationMs: 50000,
|
||||
uuid: "sys-1",
|
||||
});
|
||||
const msgs = parseSessionContent(line);
|
||||
expect(msgs).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("skips queue-operation lines", () => {
|
||||
const line = JSON.stringify({
|
||||
type: "queue-operation",
|
||||
uuid: "qo-1",
|
||||
});
|
||||
const msgs = parseSessionContent(line);
|
||||
expect(msgs).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("detects system-reminder content in user messages", () => {
|
||||
const line = JSON.stringify({
|
||||
type: "user",
|
||||
message: {
|
||||
role: "user",
|
||||
content: "<system-reminder>Some reminder</system-reminder>",
|
||||
},
|
||||
uuid: "u-sr",
|
||||
});
|
||||
const msgs = parseSessionContent(line);
|
||||
expect(msgs).toHaveLength(1);
|
||||
expect(msgs[0].category).toBe("system_message");
|
||||
});
|
||||
|
||||
it("skips malformed JSONL lines without crashing", () => {
|
||||
const content = [
|
||||
"not valid json",
|
||||
JSON.stringify({
|
||||
type: "user",
|
||||
message: { role: "user", content: "Valid message" },
|
||||
uuid: "u-valid",
|
||||
}),
|
||||
"{broken}",
|
||||
].join("\n");
|
||||
const msgs = parseSessionContent(content);
|
||||
expect(msgs).toHaveLength(1);
|
||||
expect(msgs[0].content).toBe("Valid message");
|
||||
});
|
||||
|
||||
it("returns empty array for empty files", () => {
|
||||
const msgs = parseSessionContent("");
|
||||
expect(msgs).toEqual([]);
|
||||
});
|
||||
|
||||
it("uses uuid from the JSONL line, not random", () => {
|
||||
const line = JSON.stringify({
|
||||
type: "user",
|
||||
message: { role: "user", content: "Test" },
|
||||
uuid: "my-specific-uuid-123",
|
||||
});
|
||||
const msgs = parseSessionContent(line);
|
||||
expect(msgs[0].uuid).toBe("my-specific-uuid-123");
|
||||
});
|
||||
|
||||
it("parses the full sample session fixture", async () => {
|
||||
const fixturePath = path.join(
|
||||
__dirname,
|
||||
"../fixtures/sample-session.jsonl"
|
||||
);
|
||||
const content = await fs.readFile(fixturePath, "utf-8");
|
||||
const msgs = parseSessionContent(content);
|
||||
|
||||
const categories = new Set(msgs.map((m) => m.category));
|
||||
expect(categories.has("user_message")).toBe(true);
|
||||
expect(categories.has("assistant_text")).toBe(true);
|
||||
expect(categories.has("thinking")).toBe(true);
|
||||
expect(categories.has("tool_call")).toBe(true);
|
||||
expect(categories.has("tool_result")).toBe(true);
|
||||
expect(categories.has("system_message")).toBe(true);
|
||||
expect(categories.has("hook_progress")).toBe(true);
|
||||
expect(categories.has("summary")).toBe(true);
|
||||
expect(categories.has("file_snapshot")).toBe(true);
|
||||
});
|
||||
|
||||
it("handles edge-cases fixture (corrupt lines)", async () => {
|
||||
const fixturePath = path.join(
|
||||
__dirname,
|
||||
"../fixtures/edge-cases.jsonl"
|
||||
);
|
||||
const content = await fs.readFile(fixturePath, "utf-8");
|
||||
const msgs = parseSessionContent(content);
|
||||
expect(msgs).toHaveLength(2);
|
||||
expect(msgs[0].category).toBe("user_message");
|
||||
expect(msgs[1].category).toBe("assistant_text");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user