mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
416c97ab39
Tier 0 (Bug Fixes):
- Fix promotion threshold inconsistency (standardized on 3)
- Add embedding backfill script and ghost pattern health check
- Add FTS5 virtual table with hybrid vector/text search
Tier 1 (Quick Wins):
- Binary reward assignment (+1/-1/0) for outcome-based learning
- At-least-one learning guarantee per session
- Temporal decay on search results (30-day half-life)
- Configurable promotion activity window
- Enable coherence gate by default
Tier 2 (Medium Investments):
- Pre-compaction flush hook to prevent knowledge loss
- OPD remediation hints ("bad because X, fix by Y")
- Per-agent MCP tool scoping for security isolation
- Daily Markdown log tier for audit trail
- Proof-of-Quality CLI command with SHA-256 attestation
- Devil's advocate scoring fixes (FTS normalization, cap, decay, freshness)
Test Fixes:
- Fix queen governance flakiness in domain-handlers tests
- Fix cross-suite singleton contamination in handler-factory and e2e tests
- Fix mcp-server domain tool timeout and invocation failures
610 test files pass, 0 failures, 18,589 tests green.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
227 lines
7.5 KiB
TypeScript
227 lines
7.5 KiB
TypeScript
/**
|
|
* Unit Tests for DailyLogger
|
|
*
|
|
* Tests the daily log tier that writes human-readable Markdown
|
|
* audit trails of learning activities.
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import * as os from 'os';
|
|
import { DailyLogger, DailyLogEntry } from '../../../src/learning/daily-log.js';
|
|
|
|
// ============================================================================
|
|
// Helpers
|
|
// ============================================================================
|
|
|
|
let tmpDir: string;
|
|
|
|
function makeEntry(overrides?: Partial<DailyLogEntry>): DailyLogEntry {
|
|
return {
|
|
timestamp: new Date('2026-03-09T14:30:00.000Z'),
|
|
type: 'pattern-learned',
|
|
summary: 'Learned vitest mocking pattern',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
// ============================================================================
|
|
// Tests
|
|
// ============================================================================
|
|
|
|
describe('DailyLogger', () => {
|
|
beforeEach(() => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aqe-daily-log-'));
|
|
});
|
|
|
|
afterEach(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
// ==========================================================================
|
|
// Buffering
|
|
// ==========================================================================
|
|
|
|
describe('buffering', () => {
|
|
it('should buffer entries without writing until flush', () => {
|
|
const logger = new DailyLogger({ logDir: tmpDir });
|
|
logger.log(makeEntry());
|
|
|
|
// No file should exist yet (only 1 entry, auto-flush at 10)
|
|
const files = fs.readdirSync(tmpDir);
|
|
expect(files).toHaveLength(0);
|
|
|
|
logger.dispose();
|
|
});
|
|
|
|
it('should auto-flush when buffer reaches 10 entries', () => {
|
|
const logger = new DailyLogger({ logDir: tmpDir });
|
|
|
|
for (let i = 0; i < 10; i++) {
|
|
logger.log(makeEntry({ summary: `Entry ${i}` }));
|
|
}
|
|
|
|
const files = fs.readdirSync(tmpDir);
|
|
expect(files.length).toBeGreaterThanOrEqual(1);
|
|
|
|
logger.dispose();
|
|
});
|
|
});
|
|
|
|
// ==========================================================================
|
|
// Markdown Output
|
|
// ==========================================================================
|
|
|
|
describe('markdown output', () => {
|
|
it('should create valid Markdown with header and table rows', () => {
|
|
const logger = new DailyLogger({ logDir: tmpDir });
|
|
logger.log(makeEntry());
|
|
logger.flush();
|
|
|
|
const files = fs.readdirSync(tmpDir);
|
|
expect(files).toHaveLength(1);
|
|
expect(files[0]).toMatch(/^\d{4}-\d{2}-\d{2}\.md$/);
|
|
|
|
const content = fs.readFileSync(path.join(tmpDir, files[0]), 'utf-8');
|
|
expect(content).toContain('# AQE Daily Log');
|
|
expect(content).toContain('| Time | Event | Summary |');
|
|
expect(content).toContain('|------|-------|---------|');
|
|
expect(content).toContain('pattern-learned');
|
|
expect(content).toContain('Learned vitest mocking pattern');
|
|
|
|
logger.dispose();
|
|
});
|
|
|
|
it('should include time from the entry timestamp', () => {
|
|
const logger = new DailyLogger({ logDir: tmpDir });
|
|
logger.log(makeEntry({ timestamp: new Date('2026-03-09T08:15:42.000Z') }));
|
|
logger.flush();
|
|
|
|
const files = fs.readdirSync(tmpDir);
|
|
const content = fs.readFileSync(path.join(tmpDir, files[0]), 'utf-8');
|
|
expect(content).toContain('08:15:42');
|
|
|
|
logger.dispose();
|
|
});
|
|
});
|
|
|
|
// ==========================================================================
|
|
// Header deduplication
|
|
// ==========================================================================
|
|
|
|
describe('header deduplication', () => {
|
|
it('should only add header for new files, not on subsequent flushes', () => {
|
|
const logger = new DailyLogger({ logDir: tmpDir });
|
|
|
|
logger.log(makeEntry({ summary: 'First entry' }));
|
|
logger.flush();
|
|
|
|
logger.log(makeEntry({ summary: 'Second entry' }));
|
|
logger.flush();
|
|
|
|
const files = fs.readdirSync(tmpDir);
|
|
const content = fs.readFileSync(path.join(tmpDir, files[0]), 'utf-8');
|
|
|
|
// Header should appear exactly once
|
|
const headerMatches = content.match(/# AQE Daily Log/g);
|
|
expect(headerMatches).toHaveLength(1);
|
|
|
|
// Both entries should be present
|
|
expect(content).toContain('First entry');
|
|
expect(content).toContain('Second entry');
|
|
|
|
logger.dispose();
|
|
});
|
|
});
|
|
|
|
// ==========================================================================
|
|
// Disabled mode
|
|
// ==========================================================================
|
|
|
|
describe('disabled mode', () => {
|
|
it('should not write anything when disabled', () => {
|
|
const logger = new DailyLogger({ logDir: tmpDir, enabled: false });
|
|
|
|
logger.log(makeEntry());
|
|
logger.log(makeEntry());
|
|
logger.flush();
|
|
|
|
const files = fs.readdirSync(tmpDir);
|
|
expect(files).toHaveLength(0);
|
|
|
|
logger.dispose();
|
|
});
|
|
});
|
|
|
|
// ==========================================================================
|
|
// dispose()
|
|
// ==========================================================================
|
|
|
|
describe('dispose', () => {
|
|
it('should flush remaining buffered entries on dispose', () => {
|
|
const logger = new DailyLogger({ logDir: tmpDir });
|
|
logger.log(makeEntry({ summary: 'Disposed entry' }));
|
|
|
|
// Not flushed yet
|
|
expect(fs.readdirSync(tmpDir)).toHaveLength(0);
|
|
|
|
logger.dispose();
|
|
|
|
// Now flushed
|
|
const files = fs.readdirSync(tmpDir);
|
|
expect(files).toHaveLength(1);
|
|
|
|
const content = fs.readFileSync(path.join(tmpDir, files[0]), 'utf-8');
|
|
expect(content).toContain('Disposed entry');
|
|
});
|
|
});
|
|
|
|
// ==========================================================================
|
|
// Special character escaping
|
|
// ==========================================================================
|
|
|
|
describe('escaping', () => {
|
|
it('should escape pipe characters in summary', () => {
|
|
const logger = new DailyLogger({ logDir: tmpDir });
|
|
logger.log(makeEntry({ summary: 'value | other | end' }));
|
|
logger.flush();
|
|
|
|
const files = fs.readdirSync(tmpDir);
|
|
const content = fs.readFileSync(path.join(tmpDir, files[0]), 'utf-8');
|
|
expect(content).toContain('value \\| other \\| end');
|
|
|
|
logger.dispose();
|
|
});
|
|
|
|
it('should replace newlines in summary with spaces', () => {
|
|
const logger = new DailyLogger({ logDir: tmpDir });
|
|
logger.log(makeEntry({ summary: 'line1\nline2\nline3' }));
|
|
logger.flush();
|
|
|
|
const files = fs.readdirSync(tmpDir);
|
|
const content = fs.readFileSync(path.join(tmpDir, files[0]), 'utf-8');
|
|
expect(content).toContain('line1 line2 line3');
|
|
expect(content).not.toMatch(/line1\nline2/);
|
|
|
|
logger.dispose();
|
|
});
|
|
});
|
|
|
|
// ==========================================================================
|
|
// getTodayLogPath
|
|
// ==========================================================================
|
|
|
|
describe('getTodayLogPath', () => {
|
|
it('should return a path ending in YYYY-MM-DD.md', () => {
|
|
const logger = new DailyLogger({ logDir: tmpDir });
|
|
const logPath = logger.getTodayLogPath();
|
|
|
|
expect(logPath).toMatch(/\d{4}-\d{2}-\d{2}\.md$/);
|
|
expect(logPath.startsWith(tmpDir)).toBe(true);
|
|
|
|
logger.dispose();
|
|
});
|
|
});
|
|
});
|