takt/src/__tests__/debug.test.ts

353 lines
11 KiB
TypeScript

/**
* Tests for debug logging utilities
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import {
initDebugLogger,
resetDebugLogger,
createLogger,
isDebugEnabled,
getDebugLogFile,
setVerboseConsole,
isVerboseConsole,
debugLog,
infoLog,
errorLog,
writePromptLog,
} from '../shared/utils/index.js';
import { existsSync, readFileSync, mkdirSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
function resolvePromptsLogFilePath(): string {
const debugLogFile = getDebugLogFile();
expect(debugLogFile).not.toBeNull();
if (!debugLogFile!.endsWith('.log')) {
throw new Error(`unexpected debug log file path: ${debugLogFile!}`);
}
return debugLogFile!.replace(/\.log$/, '-prompts.jsonl');
}
describe('debug logging', () => {
beforeEach(() => {
resetDebugLogger();
});
afterEach(() => {
resetDebugLogger();
});
describe('initDebugLogger', () => {
it('should not enable debug when config is undefined', () => {
initDebugLogger(undefined, '/tmp');
expect(isDebugEnabled()).toBe(false);
expect(getDebugLogFile()).toBeNull();
});
it('should not enable debug when enabled is false', () => {
initDebugLogger({ enabled: false }, '/tmp');
expect(isDebugEnabled()).toBe(false);
});
it('should enable debug when enabled is true', () => {
const projectDir = join(tmpdir(), 'takt-test-debug-enable-' + Date.now());
mkdirSync(projectDir, { recursive: true });
try {
initDebugLogger({ enabled: true }, projectDir);
expect(isDebugEnabled()).toBe(true);
expect(getDebugLogFile()).not.toBeNull();
} finally {
rmSync(projectDir, { recursive: true, force: true });
}
});
it('should write debug log to project .takt/runs/*/logs/ directory', () => {
const projectDir = join(tmpdir(), 'takt-test-debug-project-' + Date.now());
mkdirSync(projectDir, { recursive: true });
try {
initDebugLogger({ enabled: true }, projectDir);
const logFile = getDebugLogFile();
expect(logFile).not.toBeNull();
expect(logFile!).toContain(join(projectDir, '.takt', 'runs'));
expect(logFile!).toContain(`${join(projectDir, '.takt', 'runs')}/`);
expect(logFile!).toContain('/logs/');
expect(logFile!).toMatch(/debug-.*\.log$/);
expect(existsSync(logFile!)).toBe(true);
} finally {
rmSync(projectDir, { recursive: true, force: true });
}
});
it('should create prompts log file with -prompts suffix', () => {
const projectDir = join(tmpdir(), 'takt-test-debug-prompts-' + Date.now());
mkdirSync(projectDir, { recursive: true });
try {
initDebugLogger({ enabled: true }, projectDir);
const promptsLogFile = resolvePromptsLogFilePath();
expect(promptsLogFile).toContain(join(projectDir, '.takt', 'runs'));
expect(promptsLogFile).toContain('/logs/');
expect(promptsLogFile).toMatch(/debug-.*-prompts\.jsonl$/);
expect(existsSync(promptsLogFile)).toBe(true);
} finally {
rmSync(projectDir, { recursive: true, force: true });
}
});
it('should not create log file when projectDir is not provided', () => {
initDebugLogger({ enabled: true });
expect(isDebugEnabled()).toBe(true);
expect(getDebugLogFile()).toBeNull();
});
it('should use custom log file when provided', () => {
const logDir = join(tmpdir(), 'takt-test-debug-' + Date.now());
mkdirSync(logDir, { recursive: true });
const logFile = join(logDir, 'test.log');
try {
initDebugLogger({ enabled: true, logFile }, '/tmp');
expect(getDebugLogFile()).toBe(logFile);
expect(resolvePromptsLogFilePath()).toBe(join(logDir, 'test-prompts.jsonl'));
expect(existsSync(logFile)).toBe(true);
expect(existsSync(join(logDir, 'test-prompts.jsonl'))).toBe(true);
const content = readFileSync(logFile, 'utf-8');
expect(content).toContain('TAKT Debug Log');
} finally {
rmSync(logDir, { recursive: true, force: true });
}
});
it('should only initialize once', () => {
const projectDir = join(tmpdir(), 'takt-test-debug-once-' + Date.now());
mkdirSync(projectDir, { recursive: true });
try {
initDebugLogger({ enabled: true }, projectDir);
const firstFile = getDebugLogFile();
initDebugLogger({ enabled: false }, projectDir);
expect(isDebugEnabled()).toBe(true);
expect(getDebugLogFile()).toBe(firstFile);
} finally {
rmSync(projectDir, { recursive: true, force: true });
}
});
});
describe('resetDebugLogger', () => {
it('should reset all state', () => {
initDebugLogger({ enabled: true }, '/tmp');
setVerboseConsole(true);
resetDebugLogger();
expect(isDebugEnabled()).toBe(false);
expect(getDebugLogFile()).toBeNull();
expect(isVerboseConsole()).toBe(false);
});
});
describe('writePromptLog', () => {
it('should append prompt log record when debug is enabled', () => {
const projectDir = join(tmpdir(), 'takt-test-debug-write-prompts-' + Date.now());
mkdirSync(projectDir, { recursive: true });
try {
initDebugLogger({ enabled: true }, projectDir);
const promptsLogFile = resolvePromptsLogFilePath();
writePromptLog({
movement: 'plan',
phase: 1,
iteration: 2,
prompt: 'prompt text',
response: 'response text',
timestamp: '2026-02-07T00:00:00.000Z',
});
const content = readFileSync(promptsLogFile, 'utf-8').trim();
expect(content).not.toBe('');
const parsed = JSON.parse(content) as {
movement: string;
phase: number;
iteration: number;
prompt: string;
response: string;
timestamp: string;
};
expect(parsed.movement).toBe('plan');
expect(parsed.phase).toBe(1);
expect(parsed.iteration).toBe(2);
expect(parsed.prompt).toBe('prompt text');
expect(parsed.response).toBe('response text');
expect(parsed.timestamp).toBe('2026-02-07T00:00:00.000Z');
} finally {
rmSync(projectDir, { recursive: true, force: true });
}
});
it('should do nothing when debug is disabled', () => {
writePromptLog({
movement: 'plan',
phase: 1,
iteration: 1,
prompt: 'ignored prompt',
response: 'ignored response',
timestamp: '2026-02-07T00:00:00.000Z',
});
expect(getDebugLogFile()).toBeNull();
});
});
describe('setVerboseConsole / isVerboseConsole', () => {
it('should default to false', () => {
expect(isVerboseConsole()).toBe(false);
});
it('should enable verbose console', () => {
setVerboseConsole(true);
expect(isVerboseConsole()).toBe(true);
});
it('should disable verbose console', () => {
setVerboseConsole(true);
setVerboseConsole(false);
expect(isVerboseConsole()).toBe(false);
});
});
describe('verbose console output', () => {
let stderrSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
});
afterEach(() => {
stderrSpy.mockRestore();
});
it('should not output to stderr when verbose is disabled', () => {
debugLog('test', 'hello');
expect(stderrSpy).not.toHaveBeenCalled();
});
it('should output debug to stderr when verbose is enabled', () => {
setVerboseConsole(true);
debugLog('test', 'hello debug');
expect(stderrSpy).toHaveBeenCalledTimes(1);
const output = stderrSpy.mock.calls[0]?.[0] as string;
expect(output).toContain('[DEBUG]');
expect(output).toContain('[test]');
expect(output).toContain('hello debug');
});
it('should output info to stderr when verbose is enabled', () => {
setVerboseConsole(true);
infoLog('mycomp', 'info message');
expect(stderrSpy).toHaveBeenCalledTimes(1);
const output = stderrSpy.mock.calls[0]?.[0] as string;
expect(output).toContain('[INFO]');
expect(output).toContain('[mycomp]');
expect(output).toContain('info message');
});
it('should output error to stderr when verbose is enabled', () => {
setVerboseConsole(true);
errorLog('mycomp', 'error message');
expect(stderrSpy).toHaveBeenCalledTimes(1);
const output = stderrSpy.mock.calls[0]?.[0] as string;
expect(output).toContain('[ERROR]');
expect(output).toContain('[mycomp]');
expect(output).toContain('error message');
});
it('should include timestamp in console output', () => {
setVerboseConsole(true);
debugLog('test', 'with timestamp');
const output = stderrSpy.mock.calls[0]?.[0] as string;
// Timestamp format: HH:mm:ss.SSS
expect(output).toMatch(/\[\d{2}:\d{2}:\d{2}\.\d{3}\]/);
});
});
describe('createLogger', () => {
it('should create a logger with the given component name', () => {
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
setVerboseConsole(true);
const log = createLogger('my-component');
log.debug('test message');
const output = stderrSpy.mock.calls[0]?.[0] as string;
expect(output).toContain('[my-component]');
stderrSpy.mockRestore();
});
it('should provide debug, info, error, enter, exit methods', () => {
const log = createLogger('test');
expect(typeof log.debug).toBe('function');
expect(typeof log.info).toBe('function');
expect(typeof log.error).toBe('function');
expect(typeof log.enter).toBe('function');
expect(typeof log.exit).toBe('function');
});
});
describe('file logging with verbose console', () => {
it('should write to both file and stderr when both are enabled', () => {
const logDir = join(tmpdir(), 'takt-test-debug-both-' + Date.now());
mkdirSync(logDir, { recursive: true });
const logFile = join(logDir, 'test.log');
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
try {
initDebugLogger({ enabled: true, logFile }, '/tmp');
setVerboseConsole(true);
debugLog('test', 'dual output');
// Check stderr
expect(stderrSpy).toHaveBeenCalledTimes(1);
const stderrOutput = stderrSpy.mock.calls[0]?.[0] as string;
expect(stderrOutput).toContain('dual output');
// Check file
const fileContent = readFileSync(logFile, 'utf-8');
expect(fileContent).toContain('dual output');
} finally {
stderrSpy.mockRestore();
rmSync(logDir, { recursive: true, force: true });
}
});
it('should output to stderr even when file logging is disabled', () => {
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
try {
// File logging not enabled, but verbose console is
setVerboseConsole(true);
debugLog('test', 'console only');
expect(stderrSpy).toHaveBeenCalledTimes(1);
const output = stderrSpy.mock.calls[0]?.[0] as string;
expect(output).toContain('console only');
} finally {
stderrSpy.mockRestore();
}
});
});
});