Files
claude-mem/tests/log-level-audit.test.ts
Alex Newman 2659ec3231 fix: Claude Code 2.1.1 compatibility + log-level audit + path validation fixes (#614)
* Refactor CLAUDE.md and related files for December 2025 updates

- Updated CLAUDE.md in src/services/worker with new entries for December 2025, including changes to Search.ts, GeminiAgent.ts, SDKAgent.ts, and SessionManager.ts.
- Revised CLAUDE.md in src/shared to reflect updates and new entries for December 2025, including paths.ts and worker-utils.ts.
- Modified hook-constants.ts to clarify exit codes and their behaviors.
- Added comprehensive hooks reference documentation for Claude Code, detailing usage, events, and examples.
- Created initial CLAUDE.md files in various directories to track recent activity.

* fix: Merge user-message-hook output into context-hook hookSpecificOutput

- Add footer message to additionalContext in context-hook.ts
- Remove user-message-hook from SessionStart hooks array
- Fixes issue where stderr+exit(1) approach was silently discarded

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Update logs and documentation for recent plugin and worker service changes

- Added detailed logs for worker service activities from Dec 10, 2025 to Jan 7, 2026, including initialization patterns, cleanup confirmations, and diagnostic logging.
- Updated plugin documentation with recent activities, including plugin synchronization and configuration changes from Dec 3, 2025 to Jan 7, 2026.
- Enhanced the context hook and worker service logs to reflect improvements and fixes in the plugin architecture.
- Documented the migration and verification processes for the Claude memory system and its integration with the marketplace.

* Refactor hooks architecture and remove deprecated user-message-hook

- Updated hook configurations in CLAUDE.md and hooks.json to reflect changes in session start behavior.
- Removed user-message-hook functionality as it is no longer utilized in Claude Code 2.1.0; context is now injected silently.
- Enhanced context-hook to handle session context injection without user-visible messages.
- Cleaned up documentation across multiple files to align with the new hook structure and removed references to obsolete hooks.
- Adjusted timing and command execution for hooks to improve performance and reliability.

* fix: Address PR #610 review issues

- Replace USER_MESSAGE_ONLY test with BLOCKING_ERROR test in hook-constants.test.ts
- Standardize Claude Code 2.1.0 note wording across all three documentation files
- Exclude deprecated user-message-hook.ts from logger-usage-standards test

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: Remove hardcoded fake token counts from context injection

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Address PR #610 review issues by fixing test files, standardizing documentation notes, and verifying code quality improvements.

* fix: Add path validation to CLAUDE.md distribution to prevent invalid directory creation

- Add isValidPathForClaudeMd() function to reject invalid paths:
  - Tilde paths (~) that Node.js doesn't expand
  - URLs (http://, https://)
  - Paths with spaces (likely command text or PR references)
  - Paths with # (GitHub issue/PR references)
  - Relative paths that escape project boundary

- Integrate validation in updateFolderClaudeMdFiles loop
- Add 6 unit tests for path validation
- Update .gitignore to prevent accidental commit of malformed directories
- Clean up existing invalid directories (~/, PR #610..., git diff..., https:)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: Implement path validation in CLAUDE.md generation to prevent invalid directory creation

- Added `isValidPathForClaudeMd()` function to validate file paths in `src/utils/claude-md-utils.ts`.
- Integrated path validation in `updateFolderClaudeMdFiles` to skip invalid paths.
- Added 6 new unit tests in `tests/utils/claude-md-utils.test.ts` to cover various rejection cases.
- Updated `.gitignore` to prevent tracking of invalid directories.
- Cleaned up existing invalid directories in the repository.

* feat: Promote critical WARN logs to ERROR level across codebase

Comprehensive log-level audit promoting 38+ WARN messages to ERROR for
improved debugging and incident response:

- Parser: observation type errors, data contamination
- SDK/Agents: empty init responses (Gemini, OpenRouter)
- Worker/Queue: session recovery, auto-recovery failures
- Chroma: sync failures, search failures (now treated as critical)
- SQLite: search failures (primary data store)
- Session/Generator: failures, missing context
- Infrastructure: shutdown, process management failures
- File Operations: CLAUDE.md updates, config reads
- Branch Management: recovery checkout failures

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: Address PR #614 review issues

- Remove incorrectly tracked tilde-prefixed files from git
- Fix absolute path validation to check projectRoot boundaries
- Add test coverage for absolute path validation edge cases

Closes review issues:
- Issue 1: ~/ prefixed files removed from tracking
- Issue 3: Absolute paths now validated against projectRoot
- Issue 4: Added 3 new test cases for absolute path scenarios

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* build assets and context

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 23:34:20 -05:00

310 lines
9.5 KiB
TypeScript

/**
* Log Level Audit Test
*
* This test scans all TypeScript files in src/ to find logger calls,
* extracts the message text, and groups them by log level for review.
*
* Purpose: Help identify misclassified log messages that should be at a different level.
*
* Log Level Guidelines:
* - ERROR/failure: Critical failures that require investigation (data loss, service down)
* - WARN: Non-critical issues with fallback behavior (degraded, but functional)
* - INFO: Normal operational events (session started, request processed)
* - DEBUG: Detailed diagnostic information (variable values, flow tracing)
*/
import { describe, it, expect } from 'bun:test';
import { readdir, readFile } from 'fs/promises';
import { join, relative } from 'path';
const PROJECT_ROOT = join(import.meta.dir, '..');
const SRC_DIR = join(PROJECT_ROOT, 'src');
interface LoggerCall {
file: string;
line: number;
level: string;
component: string;
message: string;
errorParam: string | null;
fullMatch: string;
}
/**
* Recursively find all TypeScript files in a directory
*/
async function findTypeScriptFiles(dir: string): Promise<string[]> {
const files: string[] = [];
const entries = await readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...(await findTypeScriptFiles(fullPath)));
} else if (entry.isFile() && /\.ts$/.test(entry.name) && !/\.d\.ts$/.test(entry.name)) {
files.push(fullPath);
}
}
return files;
}
/**
* Extract logger calls from file content
* Handles multiline calls and captures error parameter (4th arg)
*/
function extractLoggerCalls(content: string, filePath: string): LoggerCall[] {
const calls: LoggerCall[] = [];
const lines = content.split('\n');
const seenCalls = new Set<string>();
// Build line number index for position-to-line lookup
const lineStarts: number[] = [0];
for (let i = 0; i < content.length; i++) {
if (content[i] === '\n') {
lineStarts.push(i + 1);
}
}
function getLineNumber(pos: number): number {
for (let i = lineStarts.length - 1; i >= 0; i--) {
if (lineStarts[i] <= pos) return i + 1;
}
return 1;
}
// Pattern that matches logger calls across multiple lines
// Captures: method, component, message, and everything up to closing paren
// Uses [\s\S] instead of . to match newlines
const loggerPattern = /logger\.(error|warn|info|debug|failure|success|timing|dataIn|dataOut|happyPathError)\s*\(\s*['"]([^'"]+)['"][\s\S]*?\)/g;
let match: RegExpExecArray | null;
while ((match = loggerPattern.exec(content)) !== null) {
const fullMatch = match[0];
const method = match[1];
const component = match[2];
const lineNum = getLineNumber(match.index);
// Extract message (2nd string arg) - could be single, double, or template
const messageMatch = fullMatch.match(/['"][^'"]+['"]\s*,\s*(['"`])([\s\S]*?)\1/);
const message = messageMatch ? messageMatch[2] : '(message not captured)';
// Extract error parameter (4th arg) - look for "error as Error" or similar patterns
let errorParam: string | null = null;
const errorMatch = fullMatch.match(/,\s*(error|err|e)\s+as\s+Error\s*\)/i) ||
fullMatch.match(/,\s*(error|err|e)\s*\)/i) ||
fullMatch.match(/,\s*new\s+Error\s*\([^)]*\)\s*\)/i);
if (errorMatch) {
errorParam = errorMatch[0].replace(/^\s*,\s*/, '').replace(/\s*\)\s*$/, '');
}
const key = `${filePath}:${lineNum}:${method}:${message.substring(0, 50)}`;
if (!seenCalls.has(key)) {
seenCalls.add(key);
calls.push({
file: relative(PROJECT_ROOT, filePath),
line: lineNum,
level: normalizeLevel(method),
component,
message,
errorParam,
fullMatch: fullMatch.replace(/\s+/g, ' ').trim() // Normalize whitespace for display
});
}
}
return calls;
}
/**
* Normalize log level names to standard categories
*/
function normalizeLevel(method: string): string {
switch (method) {
case 'error':
case 'failure':
return 'ERROR';
case 'warn':
case 'happyPathError':
return 'WARN';
case 'info':
case 'success':
case 'timing':
case 'dataIn':
case 'dataOut':
return 'INFO';
case 'debug':
return 'DEBUG';
default:
return method.toUpperCase();
}
}
/**
* Generate formatted audit report
*/
function generateReport(calls: LoggerCall[]): string {
const byLevel: Record<string, LoggerCall[]> = {
'ERROR': [],
'WARN': [],
'INFO': [],
'DEBUG': []
};
for (const call of calls) {
if (byLevel[call.level]) {
byLevel[call.level].push(call);
}
}
const lines: string[] = [];
lines.push('\n=== LOG LEVEL AUDIT REPORT ===\n');
lines.push(`Total logger calls found: ${calls.length}\n`);
// ERROR level
lines.push('');
lines.push('ERROR (should be critical failures only):');
lines.push('─'.repeat(60));
if (byLevel['ERROR'].length === 0) {
lines.push(' (none found)');
} else {
for (const call of byLevel['ERROR'].sort((a, b) => a.file.localeCompare(b.file))) {
lines.push(` ${call.file}:${call.line} [${call.component}]`);
lines.push(` message: "${formatMessage(call.message)}"`);
if (call.errorParam) {
lines.push(` error: ${call.errorParam}`);
}
lines.push(` full: ${call.fullMatch}`);
lines.push('');
}
}
lines.push(` Count: ${byLevel['ERROR'].length}`);
// WARN level
lines.push('');
lines.push('WARN (should be non-critical, has fallback):');
lines.push('─'.repeat(60));
if (byLevel['WARN'].length === 0) {
lines.push(' (none found)');
} else {
for (const call of byLevel['WARN'].sort((a, b) => a.file.localeCompare(b.file))) {
lines.push(` ${call.file}:${call.line} [${call.component}]`);
lines.push(` message: "${formatMessage(call.message)}"`);
if (call.errorParam) {
lines.push(` error: ${call.errorParam}`);
}
lines.push(` full: ${call.fullMatch}`);
lines.push('');
}
}
lines.push(` Count: ${byLevel['WARN'].length}`);
// INFO level
lines.push('');
lines.push('INFO (informational):');
lines.push('─'.repeat(60));
if (byLevel['INFO'].length === 0) {
lines.push(' (none found)');
} else {
for (const call of byLevel['INFO'].sort((a, b) => a.file.localeCompare(b.file))) {
lines.push(` ${call.file}:${call.line} [${call.component}]`);
lines.push(` message: "${formatMessage(call.message)}"`);
if (call.errorParam) {
lines.push(` error: ${call.errorParam}`);
}
lines.push(` full: ${call.fullMatch}`);
lines.push('');
}
}
lines.push(` Count: ${byLevel['INFO'].length}`);
// DEBUG level
lines.push('');
lines.push('DEBUG (detailed diagnostics):');
lines.push('─'.repeat(60));
if (byLevel['DEBUG'].length === 0) {
lines.push(' (none found)');
} else {
for (const call of byLevel['DEBUG'].sort((a, b) => a.file.localeCompare(b.file))) {
lines.push(` ${call.file}:${call.line} [${call.component}]`);
lines.push(` message: "${formatMessage(call.message)}"`);
if (call.errorParam) {
lines.push(` error: ${call.errorParam}`);
}
lines.push(` full: ${call.fullMatch}`);
lines.push('');
}
}
lines.push(` Count: ${byLevel['DEBUG'].length}`);
// Summary
lines.push('');
lines.push('=== SUMMARY ===');
lines.push(` ERROR: ${byLevel['ERROR'].length}`);
lines.push(` WARN: ${byLevel['WARN'].length}`);
lines.push(` INFO: ${byLevel['INFO'].length}`);
lines.push(` DEBUG: ${byLevel['DEBUG'].length}`);
lines.push(` TOTAL: ${calls.length}`);
lines.push('');
return lines.join('\n');
}
/**
* Format message for display - NO TRUNCATION
*/
function formatMessage(message: string): string {
return message;
}
describe('Log Level Audit', () => {
let allCalls: LoggerCall[] = [];
it('should scan all TypeScript files and extract logger calls', async () => {
const files = await findTypeScriptFiles(SRC_DIR);
expect(files.length).toBeGreaterThan(0);
for (const file of files) {
const content = await readFile(file, 'utf-8');
const calls = extractLoggerCalls(content, file);
allCalls.push(...calls);
}
expect(allCalls.length).toBeGreaterThan(0);
});
it('should generate audit report for log level review', () => {
const report = generateReport(allCalls);
console.log(report);
// This test always passes - it's for generating a review report
expect(true).toBe(true);
});
it('should have summary statistics', () => {
const byLevel: Record<string, number> = {
'ERROR': 0,
'WARN': 0,
'INFO': 0,
'DEBUG': 0
};
for (const call of allCalls) {
if (byLevel[call.level] !== undefined) {
byLevel[call.level]++;
}
}
console.log('\n📊 Log Level Distribution:');
console.log(` ERROR: ${byLevel['ERROR']} (${((byLevel['ERROR'] / allCalls.length) * 100).toFixed(1)}%)`);
console.log(` WARN: ${byLevel['WARN']} (${((byLevel['WARN'] / allCalls.length) * 100).toFixed(1)}%)`);
console.log(` INFO: ${byLevel['INFO']} (${((byLevel['INFO'] / allCalls.length) * 100).toFixed(1)}%)`);
console.log(` DEBUG: ${byLevel['DEBUG']} (${((byLevel['DEBUG'] / allCalls.length) * 100).toFixed(1)}%)`);
// Log distribution health check - not a hard failure, just informational
// A healthy codebase typically has: DEBUG > INFO > WARN > ERROR
expect(allCalls.length).toBeGreaterThan(0);
});
});