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:
552
tests/unit/sensitive-redactor.test.ts
Normal file
552
tests/unit/sensitive-redactor.test.ts
Normal file
@@ -0,0 +1,552 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
redactSensitiveContent,
|
||||
redactMessage,
|
||||
redactString,
|
||||
} from "../../src/shared/sensitive-redactor.js";
|
||||
import type { ParsedMessage } from "../../src/shared/types.js";
|
||||
|
||||
describe("sensitive-redactor", () => {
|
||||
describe("redactSensitiveContent", () => {
|
||||
// --- Tier 1: Known Secret Formats ---
|
||||
|
||||
describe("AWS credentials", () => {
|
||||
it("redacts AWS access key IDs", () => {
|
||||
const input = "Use key AKIAIOSFODNN7EXAMPLE to authenticate";
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).toContain("[AWS_KEY]");
|
||||
expect(result.sanitized).not.toContain("AKIAIOSFODNN7EXAMPLE");
|
||||
expect(result.redactionCount).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("redacts ASIA temporary credentials", () => {
|
||||
const input = "Temporary creds: ASIA2EXAMPLEXEG4567Q";
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).toContain("[AWS_KEY]");
|
||||
});
|
||||
|
||||
it("redacts AWS Bedrock API keys", () => {
|
||||
const key = "ABSK" + "A".repeat(110);
|
||||
const input = "key: " + key;
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).toContain("[AWS_BEDROCK_KEY]");
|
||||
});
|
||||
});
|
||||
|
||||
describe("GitHub tokens", () => {
|
||||
it("redacts GitHub PATs", () => {
|
||||
const token = "ghp_" + "a".repeat(36);
|
||||
const input = "token: " + token;
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).toContain("[GITHUB_TOKEN]");
|
||||
expect(result.sanitized).not.toContain("ghp_");
|
||||
});
|
||||
|
||||
it("redacts GitHub fine-grained PATs", () => {
|
||||
const token = "github_pat_" + "a".repeat(82);
|
||||
const result = redactSensitiveContent(token);
|
||||
expect(result.sanitized).toContain("[GITHUB_TOKEN]");
|
||||
});
|
||||
|
||||
it("redacts GitHub app tokens (ghu_ and ghs_)", () => {
|
||||
const input = "ghu_" + "a".repeat(36) + " and ghs_" + "b".repeat(36);
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).not.toContain("ghu_");
|
||||
expect(result.sanitized).not.toContain("ghs_");
|
||||
expect(result.redactionCount).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("GitLab tokens", () => {
|
||||
it("redacts GitLab PATs", () => {
|
||||
const input = "glpat-a1b2c3d4e5f6g7h8i9j0";
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).toContain("[GITLAB_TOKEN]");
|
||||
});
|
||||
|
||||
it("redacts GitLab runner tokens", () => {
|
||||
const input = "glrt-a1b2c3d4e5f6g7h8i9j0";
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).toContain("[GITLAB_TOKEN]");
|
||||
});
|
||||
});
|
||||
|
||||
describe("OpenAI keys", () => {
|
||||
it("redacts OpenAI project keys", () => {
|
||||
const input =
|
||||
"sk-proj-" + "a".repeat(58) + "T3BlbkFJ" + "b".repeat(58);
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).toContain("[OPENAI_KEY]");
|
||||
expect(result.sanitized).not.toContain("sk-proj-");
|
||||
});
|
||||
|
||||
it("redacts legacy OpenAI keys", () => {
|
||||
const input = "sk-" + "a".repeat(20) + "T3BlbkFJ" + "b".repeat(20);
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).toContain("[OPENAI_KEY]");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Anthropic keys", () => {
|
||||
it("redacts Anthropic API keys", () => {
|
||||
const input = "sk-ant-api03-" + "a".repeat(93) + "AA";
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).toContain("[ANTHROPIC_KEY]");
|
||||
});
|
||||
|
||||
it("redacts Anthropic admin keys", () => {
|
||||
const input = "sk-ant-admin01-" + "a".repeat(93) + "AA";
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).toContain("[ANTHROPIC_KEY]");
|
||||
});
|
||||
});
|
||||
|
||||
describe("AI service tokens", () => {
|
||||
it("redacts HuggingFace tokens", () => {
|
||||
const input = "hf_" + "a".repeat(34);
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).toContain("[HF_TOKEN]");
|
||||
});
|
||||
|
||||
it("redacts Perplexity keys", () => {
|
||||
const input = "pplx-" + "a".repeat(48);
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).toContain("[PERPLEXITY_KEY]");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Stripe keys", () => {
|
||||
it("redacts live Stripe secret keys", () => {
|
||||
const input = "sk_live_abcdefghijklmnopqrstuvwxyz1234";
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).toContain("[STRIPE_KEY]");
|
||||
expect(result.sanitized).not.toContain("sk_live_");
|
||||
});
|
||||
|
||||
it("redacts test Stripe keys", () => {
|
||||
const input = "sk_test_abcdefghijklmnop";
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).toContain("[STRIPE_KEY]");
|
||||
});
|
||||
|
||||
it("redacts restricted Stripe keys", () => {
|
||||
const input = "rk_live_abcdefghijklmnop";
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).toContain("[STRIPE_KEY]");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Slack tokens", () => {
|
||||
it("redacts Slack bot tokens", () => {
|
||||
const input = "xoxb-1234567890123-1234567890123-abcdefghij";
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).toContain("[SLACK_TOKEN]");
|
||||
});
|
||||
|
||||
it("redacts Slack webhook URLs", () => {
|
||||
const input =
|
||||
"https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX";
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).toContain("[SLACK_WEBHOOK]");
|
||||
});
|
||||
});
|
||||
|
||||
describe("SendGrid tokens", () => {
|
||||
it("redacts SendGrid API tokens", () => {
|
||||
const sgKey = "SG." + "a".repeat(22) + "." + "b".repeat(43);
|
||||
const result = redactSensitiveContent(sgKey);
|
||||
expect(result.sanitized).toContain("[SENDGRID_TOKEN]");
|
||||
});
|
||||
});
|
||||
|
||||
describe("GCP API keys", () => {
|
||||
it("redacts GCP API keys", () => {
|
||||
const input = "AIza" + "a".repeat(35);
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).toContain("[GCP_KEY]");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Heroku keys", () => {
|
||||
it("redacts Heroku API keys", () => {
|
||||
const input = "HRKU-AA" + "a".repeat(58);
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).toContain("[HEROKU_KEY]");
|
||||
});
|
||||
});
|
||||
|
||||
describe("npm tokens", () => {
|
||||
it("redacts npm access tokens", () => {
|
||||
const input = "npm_" + "a".repeat(36);
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).toContain("[NPM_TOKEN]");
|
||||
});
|
||||
});
|
||||
|
||||
describe("PyPI tokens", () => {
|
||||
it("redacts PyPI upload tokens", () => {
|
||||
const input = "pypi-AgEIcHlwaS5vcmc" + "a".repeat(50);
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).toContain("[PYPI_TOKEN]");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Sentry tokens", () => {
|
||||
it("redacts Sentry user tokens", () => {
|
||||
const input = "sntryu_" + "a".repeat(64);
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).toContain("[SENTRY_TOKEN]");
|
||||
});
|
||||
});
|
||||
|
||||
describe("JWT tokens", () => {
|
||||
it("redacts JWT tokens", () => {
|
||||
const header = Buffer.from('{"alg":"HS256"}').toString("base64url");
|
||||
const payload = Buffer.from('{"sub":"1234567890"}').toString(
|
||||
"base64url"
|
||||
);
|
||||
const sig = "abcdefghij";
|
||||
const jwt = header + "." + payload + "." + sig;
|
||||
const result = redactSensitiveContent("Bearer " + jwt);
|
||||
expect(result.sanitized).toContain("[JWT]");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Private keys (PEM)", () => {
|
||||
it("redacts RSA private keys", () => {
|
||||
const input =
|
||||
"-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA0Z3VS5JJcds3xfn" +
|
||||
"a".repeat(100) +
|
||||
"\n-----END RSA PRIVATE KEY-----";
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).toContain("[PRIVATE_KEY]");
|
||||
expect(result.sanitized).not.toContain("MIIEpAIBAAKCAQEA");
|
||||
});
|
||||
|
||||
it("redacts generic private keys", () => {
|
||||
const input =
|
||||
"-----BEGIN PRIVATE KEY-----\n" +
|
||||
"a".repeat(100) +
|
||||
"\n-----END PRIVATE KEY-----";
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).toContain("[PRIVATE_KEY]");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Generic API key pattern", () => {
|
||||
it("redacts KEY=value assignments with secret-like variable names", () => {
|
||||
const input = 'SECRET_KEY="verySecretValue1234567"';
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.redactionCount).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("redacts api_key assignments", () => {
|
||||
const input = "api_key: abcdefghijklmnopqrst";
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.redactionCount).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("redacts password assignments", () => {
|
||||
const input = 'password = "mySuperSecretPassword123"';
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.redactionCount).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
// --- Tier 2: PII/System Info ---
|
||||
|
||||
describe("Home directory paths", () => {
|
||||
it("redacts /home/username paths", () => {
|
||||
const input = "Reading file /home/taylor/.ssh/id_rsa";
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).toContain("[HOME_PATH]");
|
||||
expect(result.sanitized).not.toContain("/home/taylor");
|
||||
});
|
||||
|
||||
it("redacts /Users/username paths (macOS)", () => {
|
||||
const input = "Found at /Users/taylor/projects/app/src/index.ts";
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).toContain("[HOME_PATH]");
|
||||
expect(result.sanitized).not.toContain("/Users/taylor");
|
||||
});
|
||||
|
||||
it("redacts C:\\Users\\username paths (Windows)", () => {
|
||||
const input = "Path: C:\\Users\\Taylor\\Documents\\project";
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).toContain("[HOME_PATH]");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Connection strings", () => {
|
||||
it("redacts PostgreSQL connection strings", () => {
|
||||
const input =
|
||||
"DATABASE_URL=postgresql://admin:secret@db.example.com:5432/mydb";
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).toContain("[CONNECTION_STRING]");
|
||||
expect(result.sanitized).not.toContain("admin:secret");
|
||||
});
|
||||
|
||||
it("redacts MongoDB connection strings", () => {
|
||||
const input = "mongodb+srv://user:pass@cluster.mongodb.net/db";
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).toContain("[CONNECTION_STRING]");
|
||||
});
|
||||
|
||||
it("redacts Redis connection strings", () => {
|
||||
const input = "redis://default:password@redis.example.com:6379";
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).toContain("[CONNECTION_STRING]");
|
||||
});
|
||||
});
|
||||
|
||||
describe("URLs with credentials", () => {
|
||||
it("redacts URLs with user:pass embedded", () => {
|
||||
const input = "https://admin:s3cret@api.example.com/v1/data";
|
||||
const result = redactSensitiveContent(input);
|
||||
// Could match URL_WITH_CREDS or CONNECTION_STRING
|
||||
expect(result.redactionCount).toBeGreaterThan(0);
|
||||
expect(result.sanitized).not.toContain("admin:s3cret");
|
||||
});
|
||||
|
||||
it("does NOT redact normal URLs without credentials", () => {
|
||||
const input = "https://api.example.com/v1/data";
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).toBe(input);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Email addresses", () => {
|
||||
it("redacts email addresses", () => {
|
||||
const input = "Contact taylor@company.com for support";
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).toContain("[EMAIL]");
|
||||
expect(result.sanitized).not.toContain("taylor@company.com");
|
||||
});
|
||||
|
||||
it("does NOT redact example.com emails", () => {
|
||||
const input = "user@example.com is a placeholder";
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).toBe(input);
|
||||
});
|
||||
|
||||
it("does NOT redact test.com emails", () => {
|
||||
const input = "test@test.com for testing";
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).toBe(input);
|
||||
});
|
||||
|
||||
it("does NOT redact noreply@anthropic.com", () => {
|
||||
const input = "Co-Authored-By: Claude <noreply@anthropic.com>";
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).toBe(input);
|
||||
});
|
||||
});
|
||||
|
||||
describe("IPv4 addresses", () => {
|
||||
it("redacts non-localhost IPv4 addresses", () => {
|
||||
const input = "Server running at 10.0.1.55:8080";
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).toContain("[IP_ADDR]");
|
||||
expect(result.sanitized).not.toContain("10.0.1.55");
|
||||
});
|
||||
|
||||
it("does NOT redact 127.0.0.1", () => {
|
||||
const input = "Listening on 127.0.0.1:3000";
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).not.toContain("[IP_ADDR]");
|
||||
expect(result.sanitized).toContain("127.0.0.1");
|
||||
});
|
||||
|
||||
it("does NOT redact 0.0.0.0", () => {
|
||||
const input = "Bind to 0.0.0.0:8080";
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).not.toContain("[IP_ADDR]");
|
||||
});
|
||||
|
||||
it("does NOT redact RFC 5737 documentation IPs", () => {
|
||||
const input =
|
||||
"Example: 192.0.2.1 and 198.51.100.5 and 203.0.113.10";
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).not.toContain("[IP_ADDR]");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Bearer tokens", () => {
|
||||
it("redacts Bearer tokens in auth headers", () => {
|
||||
const input =
|
||||
"Authorization: Bearer some_long_token_value_here.with.parts";
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(
|
||||
result.sanitized.includes("[BEARER_TOKEN]") ||
|
||||
result.sanitized.includes("[JWT]")
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Environment variable secrets", () => {
|
||||
it("redacts SECRET_KEY assignments", () => {
|
||||
const input = "SECRET_KEY=abcdefghij1234567890";
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).toContain("[ENV_SECRET]");
|
||||
});
|
||||
|
||||
it("redacts DATABASE_PASSWORD assignments", () => {
|
||||
const input = 'DATABASE_PASSWORD="myDbPassw0rd"';
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.redactionCount).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
// --- False Positive Resistance ---
|
||||
|
||||
describe("false positive resistance", () => {
|
||||
it("does NOT redact normal code content", () => {
|
||||
const input =
|
||||
"function add(a: number, b: number): number { return a + b; }";
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).toBe(input);
|
||||
expect(result.redactionCount).toBe(0);
|
||||
});
|
||||
|
||||
it("does NOT redact normal markdown text", () => {
|
||||
const input =
|
||||
"# Getting Started\n\nTo install, run `npm install express`.\n\n## Features\n- Fast routing";
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).toBe(input);
|
||||
});
|
||||
|
||||
it("does NOT redact short strings that could look like keys", () => {
|
||||
const input = "The sk value is 42";
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).toBe(input);
|
||||
});
|
||||
|
||||
it("does NOT redact normal file paths outside home dirs", () => {
|
||||
const input = "Edit /etc/nginx/nginx.conf or /var/log/syslog";
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).toBe(input);
|
||||
});
|
||||
|
||||
it("does NOT redact version numbers that look like IPs", () => {
|
||||
const input = "Using version 1.2.3 of the library";
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).toBe(input);
|
||||
});
|
||||
|
||||
it("preserves surrounding text when redacting", () => {
|
||||
const token = "ghp_" + "a".repeat(36);
|
||||
const input = "Before " + token + " after";
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).toMatch(
|
||||
/^Before \[GITHUB_TOKEN\] after$/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// --- Edge Cases ---
|
||||
|
||||
describe("edge cases", () => {
|
||||
it("handles empty string", () => {
|
||||
const result = redactSensitiveContent("");
|
||||
expect(result.sanitized).toBe("");
|
||||
expect(result.redactionCount).toBe(0);
|
||||
expect(result.categories).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("handles string with no sensitive content", () => {
|
||||
const input =
|
||||
"This is a perfectly normal message with no secrets.";
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).toBe(input);
|
||||
expect(result.redactionCount).toBe(0);
|
||||
});
|
||||
|
||||
it("handles multiple different secrets in one string", () => {
|
||||
const ghToken = "ghp_" + "a".repeat(36);
|
||||
const input = "Key: " + ghToken + " and also AKIAIOSFODNN7EXAMPLE";
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.sanitized).toContain("[GITHUB_TOKEN]");
|
||||
expect(result.sanitized).toContain("[AWS_KEY]");
|
||||
expect(result.redactionCount).toBe(2);
|
||||
});
|
||||
|
||||
it("tracks matched categories", () => {
|
||||
const input = "ghp_" + "a".repeat(36);
|
||||
const result = redactSensitiveContent(input);
|
||||
expect(result.categories.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("redactString", () => {
|
||||
it("returns just the sanitized string", () => {
|
||||
const input = "ghp_" + "a".repeat(36);
|
||||
const result = redactString(input);
|
||||
expect(result).toContain("[GITHUB_TOKEN]");
|
||||
expect(typeof result).toBe("string");
|
||||
});
|
||||
});
|
||||
|
||||
describe("redactMessage", () => {
|
||||
const baseMessage: ParsedMessage = {
|
||||
uuid: "msg-1",
|
||||
category: "assistant_text",
|
||||
content: "Check ghp_" + "a".repeat(36) + " for access",
|
||||
toolName: undefined,
|
||||
toolInput: undefined,
|
||||
timestamp: "2025-10-15T10:00:00Z",
|
||||
rawIndex: 0,
|
||||
};
|
||||
|
||||
it("redacts the content field", () => {
|
||||
const redacted = redactMessage(baseMessage);
|
||||
expect(redacted.content).toContain("[GITHUB_TOKEN]");
|
||||
expect(redacted.content).not.toContain("ghp_");
|
||||
});
|
||||
|
||||
it("returns a new object without mutating the original", () => {
|
||||
const originalContent = baseMessage.content;
|
||||
const redacted = redactMessage(baseMessage);
|
||||
expect(redacted).not.toBe(baseMessage);
|
||||
expect(baseMessage.content).toBe(originalContent);
|
||||
});
|
||||
|
||||
it("preserves uuid, category, timestamp, rawIndex", () => {
|
||||
const redacted = redactMessage(baseMessage);
|
||||
expect(redacted.uuid).toBe(baseMessage.uuid);
|
||||
expect(redacted.category).toBe(baseMessage.category);
|
||||
expect(redacted.timestamp).toBe(baseMessage.timestamp);
|
||||
expect(redacted.rawIndex).toBe(baseMessage.rawIndex);
|
||||
});
|
||||
|
||||
it("redacts toolInput field", () => {
|
||||
const msg: ParsedMessage = {
|
||||
...baseMessage,
|
||||
category: "tool_call",
|
||||
toolName: "Bash",
|
||||
toolInput: JSON.stringify({
|
||||
command: "export SECRET_KEY=abcdefghijklmnopqrstuvwxyz",
|
||||
}),
|
||||
};
|
||||
const redacted = redactMessage(msg);
|
||||
expect(redacted.toolInput).not.toBe(msg.toolInput);
|
||||
});
|
||||
|
||||
it("leaves toolName unchanged for standard tool names", () => {
|
||||
const msg: ParsedMessage = {
|
||||
...baseMessage,
|
||||
category: "tool_call",
|
||||
toolName: "Read",
|
||||
toolInput: '{"file_path": "/etc/config"}',
|
||||
};
|
||||
const redacted = redactMessage(msg);
|
||||
expect(redacted.toolName).toBe("Read");
|
||||
});
|
||||
|
||||
it("handles undefined toolInput and toolName", () => {
|
||||
const redacted = redactMessage(baseMessage);
|
||||
expect(redacted.toolInput).toBeUndefined();
|
||||
expect(redacted.toolName).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user