test(FN-1414): add run-audit integration tests for core and engine

- Add core run-audit integration regression tests covering task activity logging
- Add engine run-audit integration tests covering executor lifecycle and mutations
- Stabilize run-audit ordering with inline comments for deterministic behavior
- Update memory with run-audit testing learnings and patterns
- Fix test naming and use valid GitMutationType in tests
This commit is contained in:
gsxdsm
2026-04-10 09:21:38 -07:00
parent 295ae38a0d
commit 0ab37d87a6
4 changed files with 1289 additions and 1 deletions

View File

@@ -0,0 +1,560 @@
/**
* Run-Audit Core Integration Tests
*
* These tests verify end-to-end run-audit functionality across the core API:
* - Multi-domain event correlation under a single runId
* - Complete event shape verification
* - Absent run context handling (backward compatibility)
* - Partial metadata normalization
* - Deterministic duplicate-timestamp ordering
*
* Run with: pnpm --filter @fusion/core exec vitest run src/run-audit.integration.test.ts
*/
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { mkdtempSync } from "node:fs";
import { rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { Database } from "./db.js";
import { TaskStore } from "./store.js";
import type { RunAuditEventInput, RunAuditEvent } from "./types.js";
function makeTmpDir(): string {
return mkdtempSync(join(tmpdir(), "fn-run-audit-integration-test-"));
}
describe("Run Audit Integration", () => {
let rootDir: string;
let kbDir: string;
let db: Database;
let store: TaskStore;
beforeEach(async () => {
rootDir = makeTmpDir();
kbDir = join(rootDir, ".fusion");
db = new Database(kbDir);
db.init();
store = new TaskStore(rootDir);
await store.init();
});
afterEach(async () => {
try {
store.close();
} catch {
// ignore
}
try {
db.close();
} catch {
// ignore
}
await rm(rootDir, { recursive: true, force: true });
});
describe("multi-domain event correlation", () => {
it("correlates git, database, and filesystem events under a single runId", () => {
const runId = "integration-test-run-001";
const agentId = "agent-integration";
const taskId = "FN-INTEG-001";
// Record events across all three domains
store.recordRunAuditEvent({
runId,
agentId,
taskId,
domain: "git",
mutationType: "worktree:create",
target: ".worktrees/integration-task",
metadata: { branch: "fusion/integration-task" },
});
store.recordRunAuditEvent({
runId,
agentId,
taskId,
domain: "database",
mutationType: "task:update",
target: taskId,
metadata: { updatedFields: ["status"] },
});
store.recordRunAuditEvent({
runId,
agentId,
taskId,
domain: "filesystem",
mutationType: "file:write",
target: "src/integration.ts",
metadata: { size: 1234 },
});
// Query by runId
const events = store.getRunAuditEvents({ runId });
// All three domains should be present
expect(events).toHaveLength(3);
const domains = events.map((e) => e.domain);
expect(domains).toContain("git");
expect(domains).toContain("database");
expect(domains).toContain("filesystem");
});
it("returns events ordered by timestamp DESC, rowid DESC", () => {
const runId = "integration-test-run-002";
// Insert in reverse order (oldest first in IDs due to autoincrement)
store.recordRunAuditEvent({
timestamp: "2025-01-01T01:00:00.000Z",
runId,
agentId: "agent-x",
domain: "database",
mutationType: "first",
target: "t1",
});
store.recordRunAuditEvent({
timestamp: "2025-01-01T01:00:00.000Z", // Same timestamp
runId,
agentId: "agent-y",
domain: "git",
mutationType: "second",
target: "t2",
});
store.recordRunAuditEvent({
timestamp: "2025-01-01T02:00:00.000Z",
runId,
agentId: "agent-z",
domain: "filesystem",
mutationType: "third",
target: "t3",
});
const events = store.getRunAuditEvents({ runId });
// Newest first (timestamp DESC)
expect(events[0].mutationType).toBe("third");
expect(events[1].mutationType).toBe("second"); // rowid DESC tiebreaker: second inserted last
expect(events[2].mutationType).toBe("first");
});
it("filters by domain correctly", () => {
const runId = "integration-test-run-003";
store.recordRunAuditEvent({
runId,
agentId: "agent-1",
domain: "git",
mutationType: "commit:create",
target: "main",
});
store.recordRunAuditEvent({
runId,
agentId: "agent-1",
domain: "database",
mutationType: "task:update",
target: "FN-001",
});
store.recordRunAuditEvent({
runId,
agentId: "agent-1",
domain: "filesystem",
mutationType: "file:write",
target: "src/test.ts",
});
const gitEvents = store.getRunAuditEvents({ runId, domain: "git" });
expect(gitEvents).toHaveLength(1);
expect(gitEvents[0].domain).toBe("git");
});
});
describe("complete event shape verification", () => {
it("verifies all required fields are present in persisted events", () => {
const input: RunAuditEventInput = {
taskId: "FN-SHAPE-001",
agentId: "agent-shape",
runId: "run-shape-001",
domain: "database",
mutationType: "task:create",
target: "FN-SHAPE-001",
metadata: { source: "integration-test" },
};
const event = store.recordRunAuditEvent(input);
const events = store.getRunAuditEvents({ runId: input.runId });
expect(events).toHaveLength(1);
const persisted = events[0];
// Verify complete shape
expect(persisted.id).toBeDefined();
expect(typeof persisted.id).toBe("string");
expect(persisted.timestamp).toBeDefined();
expect(typeof persisted.timestamp).toBe("string");
expect(persisted.runId).toBe(input.runId);
expect(persisted.agentId).toBe(input.agentId);
expect(persisted.taskId).toBe(input.taskId);
expect(persisted.domain).toBe(input.domain);
expect(persisted.mutationType).toBe(input.mutationType);
expect(persisted.target).toBe(input.target);
expect(persisted.metadata).toEqual(input.metadata);
});
it("handles events without optional fields gracefully", () => {
const input: RunAuditEventInput = {
agentId: "agent-minimal",
runId: "run-minimal-001",
domain: "database",
mutationType: "task:log",
target: "FN-MINIMAL-001",
// No taskId, no metadata
};
const event = store.recordRunAuditEvent(input);
const events = store.getRunAuditEvents({ runId: input.runId });
expect(events).toHaveLength(1);
const persisted = events[0];
// Required fields present
expect(persisted.id).toBeDefined();
expect(persisted.timestamp).toBeDefined();
expect(persisted.runId).toBe(input.runId);
expect(persisted.agentId).toBe(input.agentId);
expect(persisted.domain).toBe(input.domain);
expect(persisted.mutationType).toBe(input.mutationType);
expect(persisted.target).toBe(input.target);
// Optional fields undefined
expect(persisted.taskId).toBeUndefined();
expect(persisted.metadata).toBeUndefined();
});
it("preserves metadata with nested objects", () => {
const complexMetadata = {
filesChanged: 5,
details: { insertions: 100, deletions: 20 },
array: ["a", "b", "c"],
nested: { deep: { value: 42 } },
};
store.recordRunAuditEvent({
runId: "run-complex-meta",
agentId: "agent-complex",
domain: "git",
mutationType: "commit:create",
target: "feature/test",
metadata: complexMetadata,
});
const events = store.getRunAuditEvents({ runId: "run-complex-meta" });
expect(events[0].metadata).toEqual(complexMetadata);
});
});
describe("absent run context regression", () => {
it("recordRunAuditEvent works with minimal required fields", () => {
// Even without explicit timestamp or full context, should not crash
const event = store.recordRunAuditEvent({
agentId: "agent-regression",
runId: "run-regression-001",
domain: "database",
mutationType: "task:log",
target: "FN-REG-001",
});
expect(event.id).toBeDefined();
expect(event.timestamp).toBeDefined();
expect(event.runId).toBe("run-regression-001");
});
it("getRunAuditEvents with empty filter returns all events", () => {
// No filters should return all events (or empty if none exist)
const events = store.getRunAuditEvents();
expect(Array.isArray(events)).toBe(true);
});
it("getRunAuditEvents with non-existent runId returns empty array", () => {
const events = store.getRunAuditEvents({ runId: "non-existent-run-id" });
expect(events).toHaveLength(0);
});
it("getRunAuditEvents with invalid domain does not crash", () => {
// Should return empty or filter correctly (no throw)
const events = store.getRunAuditEvents({ domain: "invalid-domain" as any });
expect(Array.isArray(events)).toBe(true);
// Empty because domain filter won't match any valid domains
expect(events.length).toBe(0);
});
});
describe("partial metadata normalization", () => {
it("preserves empty string metadata values", () => {
const event = store.recordRunAuditEvent({
runId: "run-normalize-001",
agentId: "agent-norm",
domain: "database",
mutationType: "task:update",
target: "FN-NORM-001",
metadata: { emptyString: "", valid: "value" },
});
const events = store.getRunAuditEvents({ runId: "run-normalize-001" });
// Empty strings are preserved as-is (no automatic normalization to undefined)
expect(events[0].metadata).toEqual({ emptyString: "", valid: "value" });
});
it("handles null metadata gracefully", () => {
const event = store.recordRunAuditEvent({
runId: "run-null-meta",
agentId: "agent-null",
domain: "database",
mutationType: "task:create",
target: "FN-NULL-001",
metadata: null as any, // Intentional: should handle gracefully
});
// Event should be persisted with null metadata
expect(event.id).toBeDefined();
expect(event.metadata).toBeNull();
// Verify event can be queried
const events = store.getRunAuditEvents({ runId: "run-null-meta" });
expect(events).toHaveLength(1);
expect(events[0].id).toBe(event.id);
});
it("records events with undefined metadata", () => {
const event = store.recordRunAuditEvent({
runId: "run-undefined-meta",
agentId: "agent-und",
domain: "git",
mutationType: "commit:create",
target: "main",
// No metadata field at all
});
const events = store.getRunAuditEvents({ runId: "run-undefined-meta" });
expect(events[0].metadata).toBeUndefined();
});
it("preserves metadata with special characters", () => {
const event = store.recordRunAuditEvent({
runId: "run-special",
agentId: "agent-special",
domain: "filesystem",
mutationType: "file:write",
target: "path/with spaces & 'special' chars.txt",
metadata: {
description: "Test with émojis 🎉 and unicode ñ",
path: "C:\\Users\\Test\\file.ts",
},
});
const events = store.getRunAuditEvents({ runId: "run-special" });
expect(events[0].metadata).toEqual({
description: "Test with émojis 🎉 and unicode ñ",
path: "C:\\Users\\Test\\file.ts",
});
});
});
describe("duplicate timestamp ordering regression", () => {
it("orders events with identical timestamps deterministically using rowid", () => {
const runId = "run-duplicate-ts";
const sameTs = "2025-06-15T12:00:00.000Z";
// Insert multiple events with identical timestamps
const ids: string[] = [];
for (let i = 0; i < 5; i++) {
const event = store.recordRunAuditEvent({
timestamp: sameTs,
runId,
agentId: `agent-${i}`,
domain: "database",
mutationType: `event-${i}`,
target: `target-${i}`,
});
ids.push(event.id);
}
// Query and verify deterministic order
const events1 = store.getRunAuditEvents({ runId });
const events2 = store.getRunAuditEvents({ runId }); // Query again
// Same order on repeated queries
expect(events1.map((e) => e.mutationType)).toEqual(events2.map((e) => e.mutationType));
// Rowid DESC means newest row first (later IDs first for autoincrement)
expect(events1[0].mutationType).toBe("event-4"); // Last inserted
expect(events1[4].mutationType).toBe("event-0"); // First inserted
});
it("handles many events with same timestamp stably", () => {
const runId = "run-many-same-ts";
const sameTs = "2025-06-15T12:00:00.000Z";
// Insert 20 events with same timestamp
for (let i = 0; i < 20; i++) {
store.recordRunAuditEvent({
timestamp: sameTs,
runId,
agentId: `agent-${i}`,
domain: "database",
mutationType: `type-${i}`,
target: `FN-${String(i).padStart(3, "0")}`,
});
}
const events = store.getRunAuditEvents({ runId });
// All 20 events present
expect(events).toHaveLength(20);
// Order is stable and deterministic
const order1 = events.map((e) => e.mutationType);
const eventsAgain = store.getRunAuditEvents({ runId });
const order2 = eventsAgain.map((e) => e.mutationType);
expect(order1).toEqual(order2);
// Each mutation type appears exactly once
const uniqueTypes = new Set(events.map((e) => e.mutationType));
expect(uniqueTypes.size).toBe(20);
});
it("maintains ordering across query limit", () => {
const runId = "run-limit-order";
const sameTs = "2025-06-15T12:00:00.000Z";
// Insert 10 events with same timestamp
for (let i = 0; i < 10; i++) {
store.recordRunAuditEvent({
timestamp: sameTs,
runId,
agentId: `agent-${i}`,
domain: "database",
mutationType: `type-${i}`,
target: `FN-${i}`,
});
}
// Query with limit - should get the newest first (rowid DESC)
const limited = store.getRunAuditEvents({ runId, limit: 5 });
expect(limited).toHaveLength(5);
expect(limited[0].mutationType).toBe("type-9"); // Newest first
expect(limited[4].mutationType).toBe("type-5");
// Query all and verify order consistency
const all = store.getRunAuditEvents({ runId });
expect(all[0].mutationType).toBe("type-9");
expect(all[9].mutationType).toBe("type-0");
});
});
describe("event metadata completeness", () => {
it("asserts non-empty mutationType in results", () => {
const eventTypes = [
"task:create",
"task:update",
"task:move",
"git:commit",
"file:write",
"worktree:create",
];
const runId = "run-complete-001";
eventTypes.forEach((type) => {
store.recordRunAuditEvent({
runId,
agentId: "agent-check",
domain: type.startsWith("git") ? "git" : type.startsWith("file") || type.startsWith("worktree") ? "filesystem" : "database",
mutationType: type,
target: "test-target",
});
});
const events = store.getRunAuditEvents({ runId });
events.forEach((event) => {
expect(event.mutationType).toBeTruthy();
expect(event.mutationType.length).toBeGreaterThan(0);
});
});
it("asserts non-empty target in results", () => {
const runId = "run-target-001";
store.recordRunAuditEvent({
runId,
agentId: "agent-target",
domain: "database",
mutationType: "task:create",
target: "FN-TARGET-001",
});
const events = store.getRunAuditEvents({ runId });
events.forEach((event) => {
expect(event.target).toBeTruthy();
expect(typeof event.target).toBe("string");
});
});
it("verifies domain is one of valid values", () => {
const validDomains = ["database", "git", "filesystem"];
const runId = "run-domain-valid";
validDomains.forEach((domain) => {
store.recordRunAuditEvent({
runId,
agentId: "agent-domain",
domain: domain as any,
mutationType: "test",
target: "test",
});
});
const events = store.getRunAuditEvents({ runId });
events.forEach((event) => {
expect(validDomains).toContain(event.domain);
});
});
});
describe("integration with TaskStore operations", () => {
it("task operations can emit correlated audit events", async () => {
const task = await store.createTask({ description: "Integration test task" });
const runId = "run-store-integration";
// Simulate engine operations with run context
await store.logEntry(task.id, "Test action", undefined, { runId, agentId: "agent-test" });
await store.addComment(task.id, "Test comment", "user", undefined, { runId, agentId: "agent-test" });
const events = store.getRunAuditEvents({ runId });
// Should have logged events from both operations
expect(events.length).toBeGreaterThanOrEqual(2);
// All events should have the runId
events.forEach((event) => {
expect(event.runId).toBe(runId);
});
// Events should have domain and mutationType
const domains = events.map((e) => e.domain);
expect(domains).toContain("database");
});
it("pauseTask emits correlated audit event", async () => {
const task = await store.createTask({ description: "Pause test task" });
const runId = "run-pause-integration";
await store.pauseTask(task.id, true, { runId, agentId: "agent-pause" });
const events = store.getRunAuditEvents({ runId });
expect(events).toHaveLength(1);
expect(events[0].domain).toBe("database");
expect(events[0].mutationType).toBe("task:pause");
expect(events[0].target).toBe(task.id);
});
});
});