diff --git a/README.md b/README.md index a25dc7d6..0889397b 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Model provider keys and sign-ins live in **Settings > Providers**. All keys are pnpm verify:ci ``` -Typecheck, lint, unit, contract, integration, replay-eval, and Electron e2e. +Typecheck, lint, unit, contract, integration, and Electron e2e. ## Build installers diff --git a/package.json b/package.json index 5de67b57..b8382cf2 100644 --- a/package.json +++ b/package.json @@ -17,11 +17,10 @@ "test:unit": "vitest run tests/unit", "test:contract": "vitest run tests/contract", "test:integration": "vitest run tests/integration", - "test:replay-eval": "vitest run tests/eval", - "test": "pnpm test:unit && pnpm test:contract && pnpm test:integration && pnpm test:replay-eval", + "test": "pnpm test:unit && pnpm test:contract && pnpm test:integration", "test:e2e": "pnpm build && playwright test -c tests/e2e/playwright.config.ts", "verify": "pnpm verify:ci", - "verify:ci": "pnpm typecheck && pnpm lint && pnpm test:unit && pnpm test:contract && pnpm test:integration && pnpm test:replay-eval && pnpm test:e2e", + "verify:ci": "pnpm typecheck && pnpm lint && pnpm test:unit && pnpm test:contract && pnpm test:integration && pnpm test:e2e", "package:dir": "pnpm build && electron-builder --dir --publish never", "package:win": "pnpm build && electron-builder --win --x64 --publish never", "package:mac:arm": "pnpm build && electron-builder --mac --arm64 --publish never", diff --git a/src/main/app/ipc-contracts/platform.ts b/src/main/app/ipc-contracts/platform.ts index ed876dde..4f1e263f 100644 --- a/src/main/app/ipc-contracts/platform.ts +++ b/src/main/app/ipc-contracts/platform.ts @@ -59,7 +59,6 @@ export const platformContracts = { createComplianceExport: z.strictObject({ orgId: id, subjectType: z.enum(["employee", "agent"]), subjectId: id, exportType: z.enum(COMPLIANCE_EXPORT_TYPE), requestedByEmployeeId: id, }) satisfies z.ZodType, - createAuditReplay: z.strictObject({ orgId: id, threadId: id, createdByEmployeeId: id }) satisfies z.ZodType, createLegalHold: z.strictObject({ orgId: id, entityType: z.enum(["organization", "employee", "agent", "chat_thread", "file_object"]), entityId: id, reason: z.string(), createdByEmployeeId: id, }) satisfies z.ZodType, diff --git a/src/main/app/ipc-handlers.ts b/src/main/app/ipc-handlers.ts index a6098bf9..731e788d 100644 --- a/src/main/app/ipc-handlers.ts +++ b/src/main/app/ipc-handlers.ts @@ -19,7 +19,6 @@ import { ComplianceService } from "../services/compliance-service"; import { SearchService } from "../services/search-service"; import { IdentityAccessService } from "../services/identity-access-service"; import { OperationsService } from "../services/operations-service"; -import { AuditService } from "../services/audit-service"; import { RollbackService } from "../services/rollback-service"; import { TaskExecutionService } from "../services/task-execution-service"; import { DocumentService } from "../services/document-service"; @@ -77,7 +76,6 @@ export function registerIpcHandlers(deps: IpcDeps): void { const search = new SearchService(deps.connection.db); const identityAccess = new IdentityAccessService(deps.connection.db); const operations = new OperationsService(deps.connection.db); - const audit = new AuditService(deps.connection.db); const rollback = new RollbackService(deps.connection.db); const documents = new DocumentService(deps.connection.db); const knowledge = new KnowledgeService(deps.connection.db); @@ -377,7 +375,6 @@ export function registerIpcHandlers(deps: IpcDeps): void { handle("resolveMigrationReconciliation", (input) => mutate(() => rollback.resolveReconciliationItem(input))); handle("setFeatureFlag", (input) => mutate(() => operations.setFeatureFlag(input))); handle("createComplianceExport", (input) => mutate(() => compliance.createExport(input))); - handle("createAuditReplay", (input) => mutate(() => audit.createThreadReplay(input))); handle("createLegalHold", (input) => mutate(() => compliance.createLegalHold(input))); handle("setLegalHoldState", (input) => mutate(() => compliance.setLegalHoldState(input))); handle("createRetentionFreeze", (input) => mutate(() => compliance.createRetentionFreeze(input))); diff --git a/src/main/database/migrations/098-drop-audit-replays.ts b/src/main/database/migrations/098-drop-audit-replays.ts new file mode 100644 index 00000000..e2b3f7b6 --- /dev/null +++ b/src/main/database/migrations/098-drop-audit-replays.ts @@ -0,0 +1,7 @@ +// Thread "replays" only digested audit events and could not reconstruct a run's context, tool calls or outside state. +export const dropAuditReplaysSchema = ` + drop table if exists audit_replay_runs; + + insert into schema_migrations(version, name, applied_at) + values (98, 'drop_audit_replays', datetime('now')); +`; diff --git a/src/main/database/schema.ts b/src/main/database/schema.ts index 201fa3b9..f546931c 100644 --- a/src/main/database/schema.ts +++ b/src/main/database/schema.ts @@ -96,6 +96,7 @@ import { dropConfigCountEvalsSchema } from "./migrations/094-drop-config-count-e import { dropPrBotGatesSchema } from "./migrations/095-drop-pr-bot-gates"; import { dropChaosDrillsSchema } from "./migrations/096-drop-chaos-drills"; import { dropDrillOnlySloSchema } from "./migrations/097-drop-drill-only-slo"; +import { dropAuditReplaysSchema } from "./migrations/098-drop-audit-replays"; interface Migration { version: number; @@ -201,6 +202,7 @@ const MIGRATIONS: Migration[] = [ { version: 95, name: "drop_pr_bot_gates", sql: dropPrBotGatesSchema }, { version: 96, name: "drop_chaos_drills", sql: dropChaosDrillsSchema }, { version: 97, name: "drop_drill_only_slo", sql: dropDrillOnlySloSchema }, + { version: 98, name: "drop_audit_replays", sql: dropAuditReplaysSchema }, ]; const SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1].version; diff --git a/src/main/services/audit-service.ts b/src/main/services/audit-service.ts index c081f00f..164fee51 100644 --- a/src/main/services/audit-service.ts +++ b/src/main/services/audit-service.ts @@ -1,36 +1,6 @@ -import { createHash } from "node:crypto"; import type { DatabaseSync } from "node:sqlite"; -import type { CreateAuditReplayInput } from "../../shared/commands"; import { createId } from "../../shared/ids"; -interface AuditEventRow { - id: string; - org_id: string; - entity_type: string; - entity_id: string; - actor_type: string; - actor_id: string | null; - action: string; - payload_json: string; - created_at: string; -} - -interface ChatThreadRow { - id: string; - root_message_id: string | null; -} - -interface ReplayEvent { - id: string; - entityType: string; - entityId: string; - actorType: string; - actorId: string | null; - action: string; - payload: Record; - createdAt: string; -} - export class AuditService { constructor(private readonly db: DatabaseSync) {} @@ -66,98 +36,4 @@ export class AuditService { const actor = this.db.prepare(`select id from ${table} where id = ? and org_id = ?`).get(actorId, orgId); if (!actor) throw new Error(`No ${actorType} "${actorId}" in this organization.`); } - - createThreadReplay(input: CreateAuditReplayInput): string { - const thread = this.requireThread(input.orgId, input.threadId); - this.requireEmployee(input.orgId, input.createdByEmployeeId); - const messageIds = this.threadMessageIds(input.orgId, input.threadId, thread.root_message_id); - const events = this.threadEvents(input.orgId, input.threadId, messageIds); - if (events.length === 0) throw new Error("Thread replay requires audit events."); - const replay = { - threadId: input.threadId, - rootMessageId: thread.root_message_id, - messageIds: [...messageIds].sort(), - events, - }; - const replayJson = JSON.stringify(replay); - const eventDigest = createHash("sha256").update(replayJson).digest("hex"); - const id = createId("arp"); - const now = new Date().toISOString(); - this.db.prepare(` - insert into audit_replay_runs( - id, org_id, thread_id, event_count, event_digest, replay_json, - created_by_employee_id, created_at - ) values (?, ?, ?, ?, ?, ?, ?, ?) - `).run(id, input.orgId, input.threadId, events.length, eventDigest, replayJson, input.createdByEmployeeId, now); - this.record({ - orgId: input.orgId, - entityType: "audit_replay_run", - entityId: id, - actorType: "employee", - actorId: input.createdByEmployeeId, - action: "created", - payload: { threadId: input.threadId, eventCount: events.length, eventDigest }, - }); - return id; - } - - private requireThread(orgId: string, threadId: string): ChatThreadRow { - const thread = this.db.prepare("select id, root_message_id from chat_threads where id = ? and org_id = ?") - .get(threadId, orgId) as ChatThreadRow | undefined; - if (!thread) throw new Error("Chat thread was not found."); - return thread; - } - - private requireEmployee(orgId: string, employeeId: string): void { - const employee = this.db.prepare("select id from employees where id = ? and org_id = ?").get(employeeId, orgId); - if (!employee) throw new Error("Audit replay employee was not found."); - } - - private threadMessageIds(orgId: string, threadId: string, rootMessageId: string | null): Set { - const ids = new Set(); - if (rootMessageId) ids.add(rootMessageId); - const rows = this.db.prepare("select id from chat_messages where org_id = ? and thread_id = ? order by created_at asc") - .all(orgId, threadId) as Array<{ id: string }>; - for (const row of rows) ids.add(row.id); - return ids; - } - - private threadEvents(orgId: string, threadId: string, messageIds: Set): ReplayEvent[] { - const rows = this.db.prepare("select * from audit_events where org_id = ? order by created_at asc, id asc") - .all(orgId) as unknown as AuditEventRow[]; - return rows - .filter((row) => eventMatchesThread(row, threadId, messageIds)) - .map((row) => ({ - id: row.id, - entityType: row.entity_type, - entityId: row.entity_id, - actorType: row.actor_type, - actorId: row.actor_id, - action: row.action, - payload: parsePayload(row.payload_json), - createdAt: row.created_at, - })); - } -} - -function eventMatchesThread(row: AuditEventRow, threadId: string, messageIds: Set): boolean { - if (row.entity_type === "audit_replay_run") return false; - if (row.entity_type === "chat_thread" && row.entity_id === threadId) return true; - if (row.entity_type === "chat_message" && messageIds.has(row.entity_id)) return true; - const payload = parsePayload(row.payload_json); - return stringField(payload, "threadId") === threadId - || stringField(payload, "sourceThreadId") === threadId - || messageIds.has(stringField(payload, "messageId")) - || messageIds.has(stringField(payload, "sourceMessageId")); -} - -function parsePayload(payloadJson: string): Record { - const parsed = JSON.parse(payloadJson) as unknown; - if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("Audit event payload is invalid."); - return parsed as Record; -} - -function stringField(payload: Record, key: string): string { - const value = payload[key]; - return typeof value === "string" ? value : ""; } diff --git a/src/main/services/compliance-service.ts b/src/main/services/compliance-service.ts index b2b4dbfc..7bbbfb40 100644 --- a/src/main/services/compliance-service.ts +++ b/src/main/services/compliance-service.ts @@ -200,8 +200,6 @@ export class ComplianceService { .run(orgId, subjectId); this.db.prepare("update retention_freezes set created_by_employee_id = null where org_id = ? and created_by_employee_id = ?") .run(orgId, subjectId); - this.db.prepare("update audit_replay_runs set created_by_employee_id = null where org_id = ? and created_by_employee_id = ?") - .run(orgId, subjectId); this.db.prepare("update rollback_snapshots set created_by_employee_id = null where org_id = ? and created_by_employee_id = ?") .run(orgId, subjectId); this.db.prepare("update migration_reconciliation_items set resolved_by_employee_id = null where org_id = ? and resolved_by_employee_id = ?") diff --git a/src/main/services/migration-service.ts b/src/main/services/migration-service.ts index 1689c05a..df873007 100644 --- a/src/main/services/migration-service.ts +++ b/src/main/services/migration-service.ts @@ -70,7 +70,6 @@ const LEGACY_TABLES = [ "legal_holds", "retention_freezes", "search_documents", - "audit_replay_runs", "rollback_snapshots", "migration_cutover_events", "migration_reconciliation_items", @@ -239,9 +238,6 @@ const RELATION_CHECKS: RelationCheck[] = [ { table: "legal_holds", column: "created_by_employee_id", referencedTable: "employees", referencedColumn: "id", reason: "dangling_creator_employee_id" }, { table: "retention_freezes", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" }, { table: "retention_freezes", column: "created_by_employee_id", referencedTable: "employees", referencedColumn: "id", reason: "dangling_creator_employee_id" }, - { table: "audit_replay_runs", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" }, - { table: "audit_replay_runs", column: "thread_id", referencedTable: "chat_threads", referencedColumn: "id", reason: "dangling_thread_id" }, - { table: "audit_replay_runs", column: "created_by_employee_id", referencedTable: "employees", referencedColumn: "id", reason: "dangling_creator_employee_id" }, { table: "rollback_snapshots", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" }, { table: "rollback_snapshots", column: "migration_report_id", referencedTable: "migration_reports", referencedColumn: "id", reason: "dangling_migration_report_id" }, { table: "rollback_snapshots", column: "created_by_employee_id", referencedTable: "employees", referencedColumn: "id", reason: "dangling_creator_employee_id" }, diff --git a/src/main/services/snapshot-service.ts b/src/main/services/snapshot-service.ts index 119ca979..f92042f4 100644 --- a/src/main/services/snapshot-service.ts +++ b/src/main/services/snapshot-service.ts @@ -156,7 +156,6 @@ export class SnapshotService { featureFlags, sloTargets: list(this.db, "slo_targets", "signal_key asc"), operationalEvents, - auditReplayRuns: list(this.db, "audit_replay_runs", "created_at desc"), auditEvents: recentList(this.db, "audit_events", "created_at desc", 200), documents: list(this.db, "documents", "updated_at desc"), knowledgeEntries: list(this.db, "knowledge_entries", "updated_at desc"), diff --git a/src/preload/index.ts b/src/preload/index.ts index 1bd1786b..123b95c3 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -81,7 +81,6 @@ const api: DesktopApi = { resolveMigrationReconciliation: (input) => invoke(IPC_CHANNELS.resolveMigrationReconciliation, input), setFeatureFlag: (input) => invoke(IPC_CHANNELS.setFeatureFlag, input), createComplianceExport: (input) => invoke(IPC_CHANNELS.createComplianceExport, input), - createAuditReplay: (input) => invoke(IPC_CHANNELS.createAuditReplay, input), createLegalHold: (input) => invoke(IPC_CHANNELS.createLegalHold, input), setLegalHoldState: (input) => invoke(IPC_CHANNELS.setLegalHoldState, input), createRetentionFreeze: (input) => invoke(IPC_CHANNELS.createRetentionFreeze, input), diff --git a/src/shared/audit-domain.ts b/src/shared/audit-domain.ts deleted file mode 100644 index 2b90d984..00000000 --- a/src/shared/audit-domain.ts +++ /dev/null @@ -1,10 +0,0 @@ -export interface AuditReplayRunRecord { - id: string; - orgId: string; - threadId: string; - eventCount: number; - eventDigest: string; - replayJson: string; - createdByEmployeeId: string | null; - createdAt: string; -} diff --git a/src/shared/commands/governance.ts b/src/shared/commands/governance.ts index c15009b5..4089c343 100644 --- a/src/shared/commands/governance.ts +++ b/src/shared/commands/governance.ts @@ -37,12 +37,6 @@ export interface CreateComplianceExportInput { requestedByEmployeeId: string; } -export interface CreateAuditReplayInput { - orgId: string; - threadId: string; - createdByEmployeeId: string; -} - export interface CreateLegalHoldInput { orgId: string; entityType: LegalHoldEntityType; diff --git a/src/shared/desktop-api.ts b/src/shared/desktop-api.ts index 6a507531..b76c56e0 100644 --- a/src/shared/desktop-api.ts +++ b/src/shared/desktop-api.ts @@ -38,7 +38,6 @@ import type { UpdateAutomationRuleInput, UpdateDocumentInput, UpdateTaskInput, - CreateAuditReplayInput, CreateChatSpaceInput, CreateComplianceExportInput, CreateLegalHoldInput, @@ -188,7 +187,6 @@ export interface DesktopApi { resolveMigrationReconciliation(input: ResolveMigrationReconciliationInput): Promise>; setFeatureFlag(input: SetFeatureFlagInput): Promise>; createComplianceExport(input: CreateComplianceExportInput): Promise>; - createAuditReplay(input: CreateAuditReplayInput): Promise>; createLegalHold(input: CreateLegalHoldInput): Promise>; setLegalHoldState(input: SetLegalHoldStateInput): Promise>; createRetentionFreeze(input: CreateRetentionFreezeInput): Promise>; diff --git a/src/shared/domain.ts b/src/shared/domain.ts index 7722cf8b..f6e67812 100644 --- a/src/shared/domain.ts +++ b/src/shared/domain.ts @@ -1,7 +1,6 @@ import type { ProviderDataPolicy } from "./provider-policy-domain"; export { BOARD_INTERVENTION_ACTION, BOARD_INTERVENTION_MODE, CHAT_THREAD_BOARD_STATE, type BoardInterventionAction, type BoardInterventionMode, type BoardInterventionRecord, type ChatThreadBoardState } from "./board-domain"; export type { ChatSpaceRecord, ChatMessageRecord, ChatMembershipRecord, ChatThreadRecord, ChatAttachmentRecord, ChatMentionRecord, ChatSlashInvocationRecord, ChatPresenceRecord, ChatReadStateRecord } from "./chat-domain"; -export type { AuditReplayRunRecord } from "./audit-domain"; export { COMPLIANCE_EXPORT_STATE, COMPLIANCE_EXPORT_TYPE, type ComplianceExportRecord, type ComplianceExportState, type ComplianceExportType, type ComplianceSubjectType, type LegalHoldEntityType, type LegalHoldRecord, type RetentionFreezeRecord, type RetentionFreezeScopeType } from "./compliance-domain"; export { ACCESS_DECISION, IDENTITY_SOURCE, ROLE_KEY, type AccessDecision, type AccessDecisionRecord, type EmployeeIdentityRecord, type IdentitySource, type PrincipalType, type RoleGrantRecord, type RoleKey, type ServiceIdentityRecord } from "./identity-domain"; export { ROUTE_TO_TYPE, type ApprovalAssignmentRecord, type ApprovalRoutingRuleRecord, type HiringPolicyRecord, type OrgDepartmentRecord, type RouteToType } from "./org-governance-domain"; diff --git a/src/shared/ipc.ts b/src/shared/ipc.ts index 45ab7753..24240088 100644 --- a/src/shared/ipc.ts +++ b/src/shared/ipc.ts @@ -54,7 +54,6 @@ export const IPC_CHANNELS = { resolveMigrationReconciliation: "agentcompany:resolve-migration-reconciliation", setFeatureFlag: "agentcompany:set-feature-flag", createComplianceExport: "agentcompany:create-compliance-export", - createAuditReplay: "agentcompany:create-audit-replay", createLegalHold: "agentcompany:create-legal-hold", setLegalHoldState: "agentcompany:set-legal-hold-state", createRetentionFreeze: "agentcompany:create-retention-freeze", diff --git a/src/shared/snapshot-domain.ts b/src/shared/snapshot-domain.ts index ddbc0d1a..0e2ec79e 100644 --- a/src/shared/snapshot-domain.ts +++ b/src/shared/snapshot-domain.ts @@ -1,5 +1,4 @@ import type { UsageStatsRecord } from "./usage-stats"; -import type { AuditReplayRunRecord } from "./audit-domain"; import type { IntegrationKeyStatus } from "./commands/integration-keys"; import type { BoardInterventionRecord as BoardInterventionRecordModel } from "./board-domain"; import type { @@ -136,7 +135,6 @@ export interface AppSnapshot { featureFlags: FeatureFlagRecord[]; sloTargets: SloTargetRecord[]; operationalEvents: OperationalEventRecord[]; - auditReplayRuns: AuditReplayRunRecord[]; auditEvents: AuditEventRecord[]; documents: DocumentRecord[]; knowledgeEntries: KnowledgeEntryRecord[]; diff --git a/tests/eval/replay-eval.test.ts b/tests/eval/replay-eval.test.ts deleted file mode 100644 index 6fce3f04..00000000 --- a/tests/eval/replay-eval.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { createHash, randomUUID } from "node:crypto"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { describe, expect, test } from "vitest"; -import { DatabaseConnection } from "../../src/main/database/connection"; -import { AuditService } from "../../src/main/services/audit-service"; -import { ChatService } from "../../src/main/services/chat-service"; -import { OrganizationService } from "../../src/main/services/organization-service"; -import { SnapshotService } from "../../src/main/services/snapshot-service"; - -function createConnection(): DatabaseConnection { - return new DatabaseConnection(join(tmpdir(), `agentcompany-new-replay-${randomUUID()}.db`)); -} - -function createOrganization(organization: OrganizationService): string { - return organization.createOrganization({ - organizationName: "Acme Operations", - employeeName: "Dana Lee", - mission: "Coordinate agents with auditable approvals.", - goal: "Ship a verified control plane.", - }); -} - -describe("replay eval", () => { - test("creates deterministic thread replay evidence and records the replay digest", () => { - const connection = createConnection(); - const organization = new OrganizationService(connection.db); - const chat = new ChatService(connection.db); - const audit = new AuditService(connection.db); - const snapshot = new SnapshotService(connection.db); - let orgId = ""; - let employeeId = ""; - let threadId = ""; - let replayRunId = ""; - - connection.transaction(() => { - orgId = createOrganization(organization); - employeeId = (connection.db.prepare("select id from employees where org_id = ?").get(orgId) as { id: string }).id; - const spaceId = chat.createSpace({ - orgId, - spaceType: "project", - name: "release", - topic: "Release coordination", - visibility: "workspace", - }); - const messageId = chat.sendMessage({ - orgId, - spaceId, - threadId: null, - senderType: "employee", - senderId: employeeId, - bodyMd: "/escalate replay the release approval trail", - attachmentFileIds: [], - }); - threadId = (connection.db.prepare("select thread_id from chat_messages where id = ?").get(messageId) as { thread_id: string }).thread_id; - replayRunId = audit.createThreadReplay({ orgId, threadId, createdByEmployeeId: employeeId }); - }); - - const state = snapshot.load(); - const replayRun = state.auditReplayRuns.find((run) => run.id === replayRunId); - if (!replayRun) throw new Error("Audit replay run was not created."); - const replay = JSON.parse(replayRun.replayJson) as { events: Array<{ entityType: string; action: string }> }; - expect(replayRun.threadId).toBe(threadId); - expect(replayRun.eventDigest).toBe(createHash("sha256").update(replayRun.replayJson).digest("hex")); - expect(replayRun.eventCount).toBe(replay.events.length); - expect(replay.events).toContainEqual(expect.objectContaining({ entityType: "chat_message", action: "sent" })); - expect(replay.events).toContainEqual(expect.objectContaining({ entityType: "approval", action: "requested" })); - connection.close(); - }); -}); diff --git a/tests/unit/audit-service.test.ts b/tests/unit/audit-service.test.ts index 3c18fc00..f5def054 100644 --- a/tests/unit/audit-service.test.ts +++ b/tests/unit/audit-service.test.ts @@ -3,10 +3,8 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, test } from "vitest"; import { DatabaseConnection } from "../../src/main/database/connection"; -import { AuditService } from "../../src/main/services/audit-service"; -import { ChatService } from "../../src/main/services/chat-service"; import { OrganizationService } from "../../src/main/services/organization-service"; -import { SnapshotService } from "../../src/main/services/snapshot-service"; +import { IPC_CHANNELS } from "../../src/shared/ipc"; function createConnection(): DatabaseConnection { return new DatabaseConnection(join(tmpdir(), `agentcompany-new-audit-${randomUUID()}.db`)); @@ -35,55 +33,10 @@ describe("audit service", () => { connection.close(); }); - test("creates thread scoped replay runs from immutable audit events", () => { + test("offers no thread replay that only digests events and reconstructs nothing", () => { const connection = createConnection(); - const organization = new OrganizationService(connection.db); - const chat = new ChatService(connection.db); - const audit = new AuditService(connection.db); - const snapshot = new SnapshotService(connection.db); - let orgId = ""; - let employeeId = ""; - let threadId = ""; - - connection.transaction(() => { - orgId = organization.createOrganization({ - organizationName: "Acme Operations", - employeeName: "Dana Lee", - mission: "Coordinate agents with auditable approvals.", - goal: "Ship a verified control plane.", - }); - employeeId = (connection.db.prepare("select id from employees where org_id = ?").get(orgId) as { id: string }).id; - const spaceId = chat.createSpace({ - orgId, - spaceType: "project", - name: "release", - topic: "Release coordination", - visibility: "workspace", - }); - const messageId = chat.sendMessage({ - orgId, - spaceId, - threadId: null, - senderType: "employee", - senderId: employeeId, - bodyMd: "/escalate review release plan", - attachmentFileIds: [], - }); - threadId = (connection.db.prepare("select thread_id from chat_messages where id = ?").get(messageId) as { thread_id: string }).thread_id; - audit.createThreadReplay({ orgId, threadId, createdByEmployeeId: employeeId }); - }); - - const state = snapshot.load(); - const run = state.auditReplayRuns[0]; - if (!run) throw new Error("Audit replay run was not created."); - const replay = JSON.parse(run.replayJson) as { events: Array<{ entityType: string; action: string }> }; - expect(run.threadId).toBe(threadId); - expect(run.eventDigest).toHaveLength(64); - expect(run.eventCount).toBe(replay.events.length); - expect(replay.events).toContainEqual(expect.objectContaining({ entityType: "chat_message", action: "sent" })); - expect(replay.events).toContainEqual(expect.objectContaining({ entityType: "approval", action: "requested" })); - expect(replay.events.some((event) => event.entityType === "audit_replay_run")).toBe(false); - expect(state.auditEvents.some((event) => event.entityType === "audit_replay_run" && event.action === "created")).toBe(true); + expect(connection.db.prepare("select name from sqlite_master where type = 'table' and name = 'audit_replay_runs'").all()).toEqual([]); + expect(Object.keys(IPC_CHANNELS)).not.toContain("createAuditReplay"); connection.close(); }); }); diff --git a/tests/unit/domain-services.test.ts b/tests/unit/domain-services.test.ts index ba37e8e6..821b733f 100644 --- a/tests/unit/domain-services.test.ts +++ b/tests/unit/domain-services.test.ts @@ -35,7 +35,7 @@ describe("domain services", () => { const connection = createConnection(); const rows = connection.db.prepare("select version from schema_migrations order by version asc").all() as Array<{ version: number }>; - expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97]); + expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98]); connection.close(); }); diff --git a/tests/unit/migration-service.test.ts b/tests/unit/migration-service.test.ts index a39d215b..995a6eaa 100644 --- a/tests/unit/migration-service.test.ts +++ b/tests/unit/migration-service.test.ts @@ -171,7 +171,6 @@ describe("migration service", () => { create table legal_holds(id text primary key, org_id text, created_by_employee_id text); create table retention_freezes(id text primary key, org_id text, created_by_employee_id text); create table search_documents(entity_id text); - create table audit_replay_runs(id text primary key, org_id text, thread_id text, created_by_employee_id text); create table rollback_snapshots(id text primary key, org_id text, migration_report_id text, created_by_employee_id text); create table migration_cutover_events(id text primary key, org_id text, migration_report_id text, rollback_snapshot_id text); create table migration_reconciliation_items(id text primary key, org_id text, migration_report_id text, rollback_snapshot_id text, resolved_by_employee_id text); @@ -237,7 +236,6 @@ describe("migration service", () => { insert into legal_holds(id, org_id, created_by_employee_id) values ('hold-1', 'org-1', 'emp-1'); insert into retention_freezes(id, org_id, created_by_employee_id) values ('freeze-1', 'org-1', 'emp-1'); insert into search_documents(entity_id) values ('task-1'); - insert into audit_replay_runs(id, org_id, thread_id, created_by_employee_id) values ('replay-1', 'org-1', 'thread-1', 'emp-1'); insert into rollback_snapshots(id, org_id, migration_report_id, created_by_employee_id) values ('rollback-1', 'org-1', 'migration-1', 'emp-1'); insert into migration_cutover_events(id, org_id, migration_report_id, rollback_snapshot_id) values ('cutover-1', 'org-1', 'migration-1', 'rollback-1'); insert into migration_reconciliation_items(id, org_id, migration_report_id, rollback_snapshot_id, resolved_by_employee_id) values ('reconcile-1', 'org-1', 'migration-1', 'rollback-1', 'emp-1'); @@ -281,7 +279,6 @@ describe("migration service", () => { expect(body.tableCounts.social_approval_routes).toBe(1); expect(body.tableCounts.social_audit_records).toBe(1); expect(body.tableCounts.compliance_exports).toBe(1); - expect(body.tableCounts.audit_replay_runs).toBe(1); expect(body.tableCounts.rollback_snapshots).toBe(1); expect(body.tableCounts.migration_cutover_events).toBe(1); expect(body.tableCounts.migration_reconciliation_items).toBe(1); @@ -295,7 +292,6 @@ describe("migration service", () => { expect(body.idMaps.board_interventions).toEqual(["bdi-1"]); expect(body.idMaps.file_handoff_records).toEqual(["handoff-1"]); expect(body.idMaps.retention_freezes).toEqual(["freeze-1"]); - expect(body.idMaps.audit_replay_runs).toEqual(["replay-1"]); expect(body.idMaps.rollback_snapshots).toEqual(["rollback-1"]); expect(body.idMaps.migration_cutover_events).toEqual(["cutover-1"]); expect(body.idMaps.migration_reconciliation_items).toEqual(["reconcile-1"]);