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:
119
tests/unit/html-exporter.test.ts
Normal file
119
tests/unit/html-exporter.test.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { generateExportHtml } from "../../src/server/services/html-exporter.js";
|
||||
import type { ExportRequest, ParsedMessage } from "../../src/shared/types.js";
|
||||
|
||||
function makeMessage(
|
||||
overrides: Partial<ParsedMessage> & { uuid: string }
|
||||
): ParsedMessage {
|
||||
return {
|
||||
category: "assistant_text",
|
||||
content: "Test content",
|
||||
rawIndex: 0,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function makeExportRequest(
|
||||
messages: ParsedMessage[],
|
||||
visible?: string[],
|
||||
redacted?: string[]
|
||||
): ExportRequest {
|
||||
return {
|
||||
session: {
|
||||
id: "test-session",
|
||||
project: "test-project",
|
||||
messages,
|
||||
},
|
||||
visibleMessageUuids: visible || messages.map((m) => m.uuid),
|
||||
redactedMessageUuids: redacted || [],
|
||||
};
|
||||
}
|
||||
|
||||
describe("html-exporter", () => {
|
||||
it("generates valid HTML with DOCTYPE", async () => {
|
||||
const msg = makeMessage({ uuid: "msg-1", content: "Hello" });
|
||||
const html = await generateExportHtml(makeExportRequest([msg]));
|
||||
expect(html).toMatch(/^<!DOCTYPE html>/);
|
||||
expect(html).toContain("</html>");
|
||||
});
|
||||
|
||||
it("contains no external URL references", async () => {
|
||||
const msg = makeMessage({ uuid: "msg-1", content: "Hello" });
|
||||
const html = await generateExportHtml(makeExportRequest([msg]));
|
||||
// No script src or link href pointing to external URLs
|
||||
expect(html).not.toMatch(/<script\s+src\s*=/i);
|
||||
expect(html).not.toMatch(/<link\s+.*href\s*=\s*"https?:\/\//i);
|
||||
});
|
||||
|
||||
it("includes visible messages as HTML", async () => {
|
||||
const msg = makeMessage({
|
||||
uuid: "msg-1",
|
||||
content: "Hello **world**",
|
||||
category: "assistant_text",
|
||||
});
|
||||
const html = await generateExportHtml(makeExportRequest([msg]));
|
||||
expect(html).toContain("<strong>world</strong>");
|
||||
});
|
||||
|
||||
it("excludes filtered messages", async () => {
|
||||
const msg1 = makeMessage({ uuid: "msg-1", content: "Visible" });
|
||||
const msg2 = makeMessage({ uuid: "msg-2", content: "Invisible" });
|
||||
const html = await generateExportHtml(
|
||||
makeExportRequest([msg1, msg2], ["msg-1"], [])
|
||||
);
|
||||
expect(html).toContain("Visible");
|
||||
expect(html).not.toContain("Invisible");
|
||||
});
|
||||
|
||||
it("excludes redacted messages", async () => {
|
||||
const msg1 = makeMessage({ uuid: "msg-1", content: "Kept" });
|
||||
const msg2 = makeMessage({ uuid: "msg-2", content: "Redacted" });
|
||||
const html = await generateExportHtml(
|
||||
makeExportRequest([msg1, msg2], ["msg-1", "msg-2"], ["msg-2"])
|
||||
);
|
||||
expect(html).toContain("Kept");
|
||||
expect(html).not.toContain("Redacted");
|
||||
});
|
||||
|
||||
it("includes redacted divider between non-redacted messages where redaction occurred", async () => {
|
||||
const msg1 = makeMessage({ uuid: "msg-1", content: "Before" });
|
||||
const msg2 = makeMessage({ uuid: "msg-2", content: "Middle removed" });
|
||||
const msg3 = makeMessage({ uuid: "msg-3", content: "After" });
|
||||
const html = await generateExportHtml(
|
||||
makeExportRequest(
|
||||
[msg1, msg2, msg3],
|
||||
["msg-1", "msg-2", "msg-3"],
|
||||
["msg-2"]
|
||||
)
|
||||
);
|
||||
expect(html).toContain("content redacted");
|
||||
expect(html).toContain("Before");
|
||||
expect(html).toContain("After");
|
||||
expect(html).not.toContain("Middle removed");
|
||||
});
|
||||
|
||||
it("renders markdown to HTML", async () => {
|
||||
const msg = makeMessage({
|
||||
uuid: "msg-1",
|
||||
content: "# Heading\n\n- item 1\n- item 2\n\n```js\nconst x = 1;\n```",
|
||||
});
|
||||
const html = await generateExportHtml(makeExportRequest([msg]));
|
||||
expect(html).toContain("<h1");
|
||||
expect(html).toContain("<li>");
|
||||
expect(html).toContain("<code");
|
||||
});
|
||||
|
||||
it("includes syntax highlighting CSS", async () => {
|
||||
const msg = makeMessage({ uuid: "msg-1", content: "```js\nconst x = 1;\n```" });
|
||||
const html = await generateExportHtml(makeExportRequest([msg]));
|
||||
expect(html).toContain(".hljs");
|
||||
});
|
||||
|
||||
it("includes session metadata header", async () => {
|
||||
const msg = makeMessage({ uuid: "msg-1", content: "Hello" });
|
||||
const html = await generateExportHtml(makeExportRequest([msg]));
|
||||
expect(html).toContain("test-project");
|
||||
expect(html).toContain("Session Export");
|
||||
expect(html).toContain("1 messages");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user