feat(audit): add phase0 run/reaction baseline audit events

This commit is contained in:
William Valentin
2026-02-25 00:12:31 -08:00
parent c89889d9c1
commit 23b813a92f
6 changed files with 684 additions and 3 deletions
+109 -1
View File
@@ -4,6 +4,20 @@ import { join, resolve } from 'path';
import { describe, expect, it } from 'vitest';
import { AuditLogger } from './logger.js';
function waitForFlush(): Promise<void> {
return new Promise((resolvePromise) => setTimeout(resolvePromise, 25));
}
function readAuditEvents(filePath: string): Array<{ event_type: string; level: string; event: Record<string, unknown> }> {
const content = readFileSync(filePath, 'utf-8').trim();
if (!content) {
return [];
}
return content
.split('\n')
.map((line) => JSON.parse(line) as { event_type: string; level: string; event: Record<string, unknown> });
}
describe('AuditLogger', () => {
it('expands ~ in audit path before writing logs', async () => {
const previousHome = process.env.HOME;
@@ -28,7 +42,7 @@ describe('AuditLogger', () => {
logger.systemStart('test-component');
await logger.close();
await new Promise((resolvePromise) => setTimeout(resolvePromise, 25));
await waitForFlush();
const loggerPath = (logger as unknown as { config: { path: string } }).config.path;
const expectedPath = resolve(tempHome, '.local/share/flynn/audit.log');
@@ -46,4 +60,98 @@ describe('AuditLogger', () => {
rmSync(tempHome, { recursive: true, force: true });
}
});
it('writes run and reaction baseline events with expected levels', async () => {
const previousHome = process.env.HOME;
const tempHome = mkdtempSync(join(tmpdir(), 'flynn-audit-home-'));
process.env.HOME = tempHome;
try {
const logger = new AuditLogger({
enabled: true,
path: '~/.local/share/flynn/audit.log',
max_size_mb: 10,
keep_days: 30,
levels: {
tools: 'debug',
sessions: 'debug',
automation: 'debug',
},
});
logger.runState({
session_id: 'telegram:123',
channel: 'telegram',
sender: '123',
source: 'channel',
state: 'start',
request_id: 'req-1',
});
logger.runState({
session_id: 'telegram:123',
channel: 'telegram',
sender: '123',
source: 'channel',
state: 'error',
request_id: 'req-1',
error: 'model timeout',
});
logger.runCancel({
session_id: 'telegram:123',
channel: 'telegram',
sender: '123',
source: 'channel',
requested: true,
acknowledged: true,
request_id: 'req-1',
latency_ms: 120,
});
logger.reactionMatch({
session_id: 'telegram:123',
channel: 'telegram',
sender: '123',
source: 'channel',
rule_name: 'daily-briefing-hint',
candidate_count: 4,
filter_summary: 'contains:briefing',
});
logger.reactionSkip({
session_id: 'telegram:123',
channel: 'telegram',
sender: '123',
source: 'channel',
reason: 'no_match',
candidate_count: 4,
});
await logger.close();
await waitForFlush();
const expectedPath = resolve(tempHome, '.local/share/flynn/audit.log');
const events = readAuditEvents(expectedPath);
const eventTypes = events.map((event) => event.event_type);
expect(eventTypes).toContain('run.state');
expect(eventTypes).toContain('run.cancel');
expect(eventTypes).toContain('reaction.match');
expect(eventTypes).toContain('reaction.skip');
const runError = events.find((event) => (
event.event_type === 'run.state'
&& event.event.state === 'error'
));
expect(runError?.level).toBe('error');
const reactionSkip = events.find((event) => event.event_type === 'reaction.skip');
expect(reactionSkip?.level).toBe('debug');
expect(reactionSkip?.event.reason).toBe('no_match');
} finally {
if (previousHome === undefined) {
delete process.env.HOME;
} else {
process.env.HOME = previousHome;
}
rmSync(tempHome, { recursive: true, force: true });
}
});
});