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:
142
tests/unit/filters.test.ts
Normal file
142
tests/unit/filters.test.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import type { ParsedMessage, MessageCategory } from "../../src/shared/types.js";
|
||||
import { ALL_CATEGORIES, DEFAULT_HIDDEN_CATEGORIES } from "../../src/shared/types.js";
|
||||
|
||||
// Replicate the filter logic to test it in isolation
|
||||
function filterMessages(
|
||||
messages: ParsedMessage[],
|
||||
enabledCategories: Set<MessageCategory>,
|
||||
redactedUuids: Set<string> = new Set()
|
||||
): ParsedMessage[] {
|
||||
return messages.filter(
|
||||
(m) => enabledCategories.has(m.category) && !redactedUuids.has(m.uuid)
|
||||
);
|
||||
}
|
||||
|
||||
function makeMsg(uuid: string, category: MessageCategory): ParsedMessage {
|
||||
return { uuid, category, content: `Content for ${uuid}`, rawIndex: 0 };
|
||||
}
|
||||
|
||||
describe("filters", () => {
|
||||
const messages: ParsedMessage[] = [
|
||||
makeMsg("1", "user_message"),
|
||||
makeMsg("2", "assistant_text"),
|
||||
makeMsg("3", "thinking"),
|
||||
makeMsg("4", "tool_call"),
|
||||
makeMsg("5", "tool_result"),
|
||||
makeMsg("6", "system_message"),
|
||||
makeMsg("7", "hook_progress"),
|
||||
makeMsg("8", "file_snapshot"),
|
||||
makeMsg("9", "summary"),
|
||||
];
|
||||
|
||||
it("correctly includes messages by enabled category", () => {
|
||||
const enabled = new Set<MessageCategory>(["user_message", "assistant_text"]);
|
||||
const filtered = filterMessages(messages, enabled);
|
||||
expect(filtered).toHaveLength(2);
|
||||
expect(filtered.map((m) => m.category)).toEqual([
|
||||
"user_message",
|
||||
"assistant_text",
|
||||
]);
|
||||
});
|
||||
|
||||
it("correctly excludes messages by disabled category", () => {
|
||||
const enabled = new Set<MessageCategory>(ALL_CATEGORIES);
|
||||
enabled.delete("thinking");
|
||||
const filtered = filterMessages(messages, enabled);
|
||||
expect(filtered).toHaveLength(8);
|
||||
expect(filtered.find((m) => m.category === "thinking")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("default filter state has thinking and hooks disabled", () => {
|
||||
const defaultEnabled = new Set(ALL_CATEGORIES);
|
||||
for (const cat of DEFAULT_HIDDEN_CATEGORIES) {
|
||||
defaultEnabled.delete(cat);
|
||||
}
|
||||
const filtered = filterMessages(messages, defaultEnabled);
|
||||
expect(filtered.find((m) => m.category === "thinking")).toBeUndefined();
|
||||
expect(filtered.find((m) => m.category === "hook_progress")).toBeUndefined();
|
||||
expect(filtered).toHaveLength(7);
|
||||
});
|
||||
|
||||
it("all-off filter returns empty array", () => {
|
||||
const filtered = filterMessages(messages, new Set());
|
||||
expect(filtered).toEqual([]);
|
||||
});
|
||||
|
||||
it("all-on filter returns all messages", () => {
|
||||
const filtered = filterMessages(messages, new Set(ALL_CATEGORIES));
|
||||
expect(filtered).toHaveLength(9);
|
||||
});
|
||||
|
||||
it("excludes redacted messages", () => {
|
||||
const enabled = new Set(ALL_CATEGORIES);
|
||||
const redacted = new Set(["1", "3"]);
|
||||
const filtered = filterMessages(messages, enabled, redacted);
|
||||
expect(filtered).toHaveLength(7);
|
||||
expect(filtered.find((m) => m.uuid === "1")).toBeUndefined();
|
||||
expect(filtered.find((m) => m.uuid === "3")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("getMatchCount returns count of messages matching search query", () => {
|
||||
const lowerQuery = "content for 1";
|
||||
const count = messages.filter((m) =>
|
||||
m.content.toLowerCase().includes(lowerQuery)
|
||||
).length;
|
||||
expect(count).toBe(1);
|
||||
});
|
||||
|
||||
it("getMatchCount returns 0 for empty query", () => {
|
||||
const lowerQuery = "";
|
||||
const count = lowerQuery
|
||||
? messages.filter((m) => m.content.toLowerCase().includes(lowerQuery)).length
|
||||
: 0;
|
||||
expect(count).toBe(0);
|
||||
});
|
||||
|
||||
it("conversation preset includes only user and assistant messages", () => {
|
||||
const preset: MessageCategory[] = ["user_message", "assistant_text"];
|
||||
const enabled = new Set(preset);
|
||||
const filtered = filterMessages(messages, enabled);
|
||||
expect(filtered).toHaveLength(2);
|
||||
expect(filtered.every((m) => m.category === "user_message" || m.category === "assistant_text")).toBe(true);
|
||||
});
|
||||
|
||||
it("debug preset includes only tool calls and results", () => {
|
||||
const preset: MessageCategory[] = ["tool_call", "tool_result"];
|
||||
const enabled = new Set(preset);
|
||||
const filtered = filterMessages(messages, enabled);
|
||||
expect(filtered).toHaveLength(2);
|
||||
expect(filtered.every((m) => m.category === "tool_call" || m.category === "tool_result")).toBe(true);
|
||||
});
|
||||
|
||||
it("category counts are computed correctly", () => {
|
||||
const counts: Record<string, number> = {};
|
||||
for (const cat of ALL_CATEGORIES) {
|
||||
counts[cat] = 0;
|
||||
}
|
||||
for (const msg of messages) {
|
||||
counts[msg.category]++;
|
||||
}
|
||||
expect(counts["user_message"]).toBe(1);
|
||||
expect(counts["assistant_text"]).toBe(1);
|
||||
expect(counts["thinking"]).toBe(1);
|
||||
expect(Object.values(counts).reduce((a, b) => a + b, 0)).toBe(9);
|
||||
});
|
||||
|
||||
it("category counts exclude redacted messages", () => {
|
||||
const redacted = new Set(["1", "2"]);
|
||||
const counts: Record<string, number> = {};
|
||||
for (const cat of ALL_CATEGORIES) {
|
||||
counts[cat] = 0;
|
||||
}
|
||||
for (const msg of messages) {
|
||||
if (!redacted.has(msg.uuid)) {
|
||||
counts[msg.category]++;
|
||||
}
|
||||
}
|
||||
expect(counts["user_message"]).toBe(0);
|
||||
expect(counts["assistant_text"]).toBe(0);
|
||||
expect(Object.values(counts).reduce((a, b) => a + b, 0)).toBe(7);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user