From 339e452bc0b98eb1acd3c832377707b09cb5a370 Mon Sep 17 00:00:00 2001 From: Alex Newman Date: Sat, 27 Dec 2025 20:31:46 -0500 Subject: [PATCH] test: add logger coverage test suite to enforce logging standards --- tests/logger-coverage.test.ts | 212 ++++++++++++++++++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 tests/logger-coverage.test.ts diff --git a/tests/logger-coverage.test.ts b/tests/logger-coverage.test.ts new file mode 100644 index 00000000..e8c03b7e --- /dev/null +++ b/tests/logger-coverage.test.ts @@ -0,0 +1,212 @@ +import { describe, it, expect } from "bun:test"; +import { readdir } from "fs/promises"; +import { join, relative } from "path"; +import { readFileSync } from "fs"; + +/** + * Test suite to ensure consistent logger usage across the codebase. + * + * This test enforces logging standards by: + * 1. Identifying files that should use logging + * 2. Detecting console.log/console.error usage that should be replaced with logger + * 3. Verifying logger import patterns + * 4. Reporting coverage statistics + */ + +const PROJECT_ROOT = join(import.meta.dir, ".."); +const SRC_DIR = join(PROJECT_ROOT, "src"); + +// Files/directories that don't require logging +const EXCLUDED_PATTERNS = [ + /types\//, // Type definition files + /constants\//, // Pure constants + /\.d\.ts$/, // Type declaration files + /^ui\//, // UI components (separate logging context) + /^bin\//, // CLI utilities (may use console.log for output) + /index\.ts$/, // Re-export files + /logger\.ts$/, // Logger itself + /hook-response\.ts$/, // Pure data structure + /hook-constants\.ts$/, // Pure constants + /paths\.ts$/, // Path utilities + /bun-path\.ts$/, // Path utilities + /migrations\.ts$/, // Database migrations (console.log for migration output) +]; + +// Files that should always use logger (core business logic) +// Excludes UI files, type files, and pure utilities +const HIGH_PRIORITY_PATTERNS = [ + /^services\/worker\/(?!.*types\.ts$)/, // Worker services (not type files) + /^services\/sqlite\/(?!types\.ts$|index\.ts$)/, // SQLite services + /^services\/sync\//, + /^services\/context-generator\.ts$/, + /^hooks\/(?!hook-response\.ts$)/, // All src/hooks/* except hook-response.ts (NOT ui/hooks) + /^sdk\/(?!.*types?\.ts$)/, // SDK files (not type files) + /^servers\/(?!.*types?\.ts$)/, // Server files (not type files) +]; + +// Additional check: exclude UI files from high priority +const isUIFile = (path: string) => /^ui\//.test(path); + +interface FileAnalysis { + path: string; + relativePath: string; + hasLoggerImport: boolean; + usesConsoleLog: boolean; + consoleLogLines: number[]; + loggerCallCount: number; + isHighPriority: boolean; +} + +/** + * Recursively find all TypeScript files in a directory + */ +async function findTypeScriptFiles(dir: string): Promise { + 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)) { + files.push(fullPath); + } + } + + return files; +} + +/** + * Check if a file should be excluded from logger requirements + */ +function shouldExclude(filePath: string): boolean { + const relativePath = relative(SRC_DIR, filePath); + return EXCLUDED_PATTERNS.some(pattern => pattern.test(relativePath)); +} + +/** + * Check if a file is high priority for logging + */ +function isHighPriority(filePath: string): boolean { + const relativePath = relative(SRC_DIR, filePath); + + // UI files are never high priority + if (isUIFile(relativePath)) { + return false; + } + + return HIGH_PRIORITY_PATTERNS.some(pattern => pattern.test(relativePath)); +} + +/** + * Analyze a single TypeScript file for logger usage + */ +function analyzeFile(filePath: string): FileAnalysis { + const content = readFileSync(filePath, "utf-8"); + const lines = content.split("\n"); + const relativePath = relative(PROJECT_ROOT, filePath); + + // Check for logger import (handles both .ts and .js extensions in import paths) + const hasLoggerImport = /import\s+.*logger.*from\s+['"].*logger(\.(js|ts))?['"]/.test(content); + + // Find console.log/console.error usage with line numbers + const consoleLogLines: number[] = []; + lines.forEach((line, index) => { + if (/console\.(log|error|warn|info|debug)/.test(line)) { + consoleLogLines.push(index + 1); + } + }); + + // Count logger method calls + const loggerCallMatches = content.match(/logger\.(debug|info|warn|error|success|failure|timing|dataIn|dataOut|happyPathError)\(/g); + const loggerCallCount = loggerCallMatches ? loggerCallMatches.length : 0; + + return { + path: filePath, + relativePath, + hasLoggerImport, + usesConsoleLog: consoleLogLines.length > 0, + consoleLogLines, + loggerCallCount, + isHighPriority: isHighPriority(filePath), + }; +} + +describe("Logger Coverage", () => { + let allFiles: FileAnalysis[] = []; + let relevantFiles: FileAnalysis[] = []; + + it("should scan all TypeScript files in src/", async () => { + const files = await findTypeScriptFiles(SRC_DIR); + allFiles = files.map(analyzeFile); + relevantFiles = allFiles.filter(f => !shouldExclude(f.path)); + + expect(allFiles.length).toBeGreaterThan(0); + expect(relevantFiles.length).toBeGreaterThan(0); + }); + + it("should NOT use console.log/console.error (these logs are invisible in background services)", () => { + // Only hook files can use console.log for their final output response + // Everything else (services, workers, sqlite, etc.) runs in background - console.log is USELESS there + const filesWithConsole = relevantFiles.filter(f => { + const isHookFile = /^src\/hooks\//.test(f.relativePath); + return f.usesConsoleLog && !isHookFile; + }); + + if (filesWithConsole.length > 0) { + const report = filesWithConsole + .map(f => ` ${f.relativePath}:${f.consoleLogLines.join(",")}`) + .join("\n"); + + throw new Error( + `āŒ CRITICAL: Found console.log/console.error in ${filesWithConsole.length} background service file(s):\n${report}\n\n` + + `These logs are INVISIBLE - they run in background processes where console output goes nowhere.\n` + + `Replace with logger.debug/info/warn/error calls immediately.\n\n` + + `Only hook files (src/hooks/*) should use console.log for their output response.` + ); + } + }); + + it("should have logger coverage in high-priority files", () => { + const highPriorityFiles = relevantFiles.filter(f => f.isHighPriority); + const withoutLogger = highPriorityFiles.filter(f => !f.hasLoggerImport); + + if (withoutLogger.length > 0) { + const report = withoutLogger + .map(f => ` ${f.relativePath}`) + .join("\n"); + + throw new Error( + `High-priority files missing logger import (${withoutLogger.length}):\n${report}\n\n` + + `These files should import and use logger for debugging and observability.` + ); + } + }); + + it("should report logger coverage statistics", () => { + const withLogger = relevantFiles.filter(f => f.hasLoggerImport); + const withoutLogger = relevantFiles.filter(f => !f.hasLoggerImport); + const totalCalls = relevantFiles.reduce((sum, f) => sum + f.loggerCallCount, 0); + + const coverage = ((withLogger.length / relevantFiles.length) * 100).toFixed(1); + + console.log("\nšŸ“Š Logger Coverage Report:"); + console.log(` Total files analyzed: ${relevantFiles.length}`); + console.log(` Files with logger: ${withLogger.length} (${coverage}%)`); + console.log(` Files without logger: ${withoutLogger.length}`); + console.log(` Total logger calls: ${totalCalls}`); + console.log(` Excluded files: ${allFiles.length - relevantFiles.length}`); + + if (withoutLogger.length > 0) { + console.log("\nšŸ“ Files without logger:"); + withoutLogger.forEach(f => { + const priority = f.isHighPriority ? "šŸ”“ HIGH" : " "; + console.log(` ${priority} ${f.relativePath}`); + }); + } + + // This is an informational test - we expect some files won't need logging + expect(withLogger.length).toBeGreaterThan(0); + }); +});