diff --git a/src/audit/audit-event-writer.test.ts b/src/audit/audit-event-writer.test.ts index 0a9fd39c2fa8..f2968b76d640 100644 --- a/src/audit/audit-event-writer.test.ts +++ b/src/audit/audit-event-writer.test.ts @@ -78,16 +78,34 @@ afterEach(() => { const tempDirs = useAutoCleanupTempDirTracker(afterEach); describe("audit event worker", () => { - it("keeps first-use identity storage absent during maintenance without admission", async () => { + it("keeps fresh storage identity-free when recovery evidence is missing", async () => { const stateDir = tempDirs.make("openclaw-audit-writer-"); const database = { env: { OPENCLAW_STATE_DIR: stateDir } }; const errors: string[] = []; const writer = createAuditEventWriter({ stateDir, onError: (error) => errors.push(error) }); await writer.ready; + expect( + openOpenClawStateDatabase(database) + .db.prepare("SELECT name FROM sqlite_schema WHERE type = 'table' AND name = ?") + .get("execution_identity_contexts"), + ).toBeUndefined(); + expect(writer.record(input())).toBe(true); + const token = createExecutionIdentityAdmissionToken("raw-run-not-a-secret", { + contextId: "context-missing", + executionId: "execution-missing", + now: 100, + }); + const startedAt = performance.now(); + expect(writer.recordExecutionIdentity({ kind: "retry-reference", token })).toBe(true); + expect(performance.now() - startedAt).toBeLessThan(250); await writer.stop(); - expect(errors).toEqual([]); + expect(errors).toEqual(["audit execution identity recovery evidence unavailable"]); + expect(JSON.stringify(errors)).not.toContain(token.contextId); + expect(JSON.stringify(errors)).not.toContain(token.executionId); + expect(JSON.stringify(errors)).not.toContain(token.runId); + expect(listAuditEvents({ database, limit: 10 }).events).toHaveLength(1); expect( openOpenClawStateDatabase(database) .db.prepare("SELECT name FROM sqlite_schema WHERE type = 'table' AND name = ?") @@ -95,95 +113,90 @@ describe("audit event worker", () => { ).toBeUndefined(); }); - it("keeps an established current store identity-free during maintenance", async () => { + it("keeps the shared queue nonblocking under a held write lock and flushes before stop", async () => { const stateDir = tempDirs.make("openclaw-audit-writer-"); const database = { env: { OPENCLAW_STATE_DIR: stateDir } }; recordAuditEvent(input(), database); closeOpenClawStateDatabaseForTest(); const errors: string[] = []; - const writer = createAuditEventWriter({ stateDir, onError: (error) => errors.push(error) }); - + const writer = createAuditEventWriter({ + stateDir, + maxPending: 2, + onError: (error) => errors.push(error), + }); await writer.ready; - await writer.stop(); - - expect(errors).toEqual([]); - expect(listAuditEvents({ database, limit: 10 }).events).toHaveLength(1); + const { db } = openOpenClawStateDatabase(database); expect( - openOpenClawStateDatabase(database) - .db.prepare("SELECT name FROM sqlite_schema WHERE type = 'table' AND name = ?") + db + .prepare("SELECT name FROM sqlite_schema WHERE type = 'table' AND name = ?") .get("execution_identity_contexts"), ).toBeUndefined(); - }); - - it("returns immediately under SQLite contention and flushes before stop", async () => { - const stateDir = tempDirs.make("openclaw-audit-writer-"); - const database = { env: { OPENCLAW_STATE_DIR: stateDir } }; - const errors: string[] = []; - const writer = createAuditEventWriter({ stateDir, onError: (error) => errors.push(error) }); - await writer.ready; - const { db } = openOpenClawStateDatabase(database); - db.exec("BEGIN IMMEDIATE"); - const startedAt = performance.now(); - expect(writer.record(input())).toBe(true); - expect(performance.now() - startedAt).toBeLessThan(250); - db.exec("ROLLBACK"); - - await writer.stop(); - expect(errors).toEqual([]); - expect(listAuditEvents({ database, limit: 10 }).events).toHaveLength(1); - }); - - it("keeps first-use identity admission prompt under a held write lock", async () => { - const stateDir = tempDirs.make("openclaw-audit-writer-"); - const database = { env: { OPENCLAW_STATE_DIR: stateDir } }; - const { db } = openOpenClawStateDatabase(database); db.exec("DELETE FROM audit_identity_keys;"); db.exec("BEGIN IMMEDIATE"); - const errors: string[] = []; - const writer = createAuditEventWriter({ stateDir, onError: (error) => errors.push(error) }); const clearSink = configureExecutionIdentityAdmissionSink(writer.recordExecutionIdentity); const admittedAt = Date.now(); - const startedAt = performance.now(); - expect( - enqueueExecutionIdentityContextAtAdmission( - { - runId: "held-lock-run", - agentId: "main", - ingress: { - kind: "local-cli", - boundary: "agent-command.local", - state: "present", - rawSourceRef: "raw-ingress-secret", + try { + const startedAt = performance.now(); + expect(writer.record({ ...input(), sourceId: "run-2:1:started", runId: "run-2" })).toBe(true); + expect( + enqueueExecutionIdentityContextAtAdmission( + { + runId: "held-lock-run", + agentId: "main", + ingress: { + kind: "local-cli", + boundary: "agent-command.local", + state: "present", + rawSourceRef: "raw-ingress-secret", + }, + runtime: { kind: "embedded" }, + invoker: { kind: "local-account", rawPrincipalRef: "raw-principal-secret" }, }, - runtime: { kind: "embedded" }, - invoker: { kind: "local-account", rawPrincipalRef: "raw-principal-secret" }, - }, - { - enabled: true, - contextId: "held-lock-context", - executionId: "held-lock-execution", - now: admittedAt, - runtimeInstanceId: "raw-runtime-secret", - }, - ), - ).toEqual({ - candidateContextId: "held-lock-context", - candidateExecutionId: "held-lock-execution", - accepted: true, - }); - expect(performance.now() - startedAt).toBeLessThan(250); - expect( - db.prepare("SELECT name FROM sqlite_schema WHERE name = 'execution_identity_contexts'").get(), - ).toBeUndefined(); - expect(db.prepare("SELECT COUNT(*) AS count FROM audit_identity_keys").get()).toEqual({ - count: 0, - }); + { + enabled: true, + contextId: "held-lock-context", + executionId: "held-lock-execution", + now: admittedAt, + runtimeInstanceId: "raw-runtime-secret", + }, + ), + ).toEqual({ + candidateContextId: "held-lock-context", + candidateExecutionId: "held-lock-execution", + accepted: true, + }); + expect(performance.now() - startedAt).toBeLessThan(250); + expect( + writer.recordExecutionIdentity({ + kind: "retry-reference", + token: createExecutionIdentityAdmissionToken("queue-full-run", { + contextId: "queue-full-context", + executionId: "queue-full-execution", + now: admittedAt, + }), + }), + ).toBe(false); + expect(errors).toEqual(["audit event queue is full (2); dropping metadata"]); + expect( + db + .prepare("SELECT name FROM sqlite_schema WHERE name = 'execution_identity_contexts'") + .get(), + ).toBeUndefined(); + expect(db.prepare("SELECT COUNT(*) AS count FROM audit_identity_keys").get()).toEqual({ + count: 0, + }); + } finally { + try { + db.exec("ROLLBACK"); + } finally { + clearSink(); + await writer.stop(); + } + } - db.exec("ROLLBACK"); - clearSink(); - await writer.stop(); - expect(errors).toEqual([]); + expect(errors).toEqual(["audit event queue is full (2); dropping metadata"]); + expect(listAuditEvents({ database, limit: 10 }).events).toHaveLength(2); expect( inspectExecutionIdentityRun({ runId: "held-lock-run" }, { ...database, now: admittedAt }), ).toMatchObject({ @@ -212,7 +225,7 @@ describe("audit event worker", () => { } }); - it("prunes expired identity contexts at startup without a new run", async () => { + it("prunes expired identity contexts before preserving exact-envelope conflicts", async () => { const stateDir = tempDirs.make("openclaw-audit-writer-"); const database = { env: { OPENCLAW_STATE_DIR: stateDir } }; persistExecutionIdentityAdmissionEnvelope( @@ -232,54 +245,11 @@ describe("audit event worker", () => { const errors: string[] = []; const writer = createAuditEventWriter({ stateDir, onError: (error) => errors.push(error) }); await writer.ready; - expect( openOpenClawStateDatabase(database) .db.prepare("SELECT COUNT(*) AS count FROM execution_identity_contexts") .get(), ).toEqual({ count: 0 }); - await writer.stop(); - expect(errors).toEqual([]); - }); - - it("uses one pending limit across audit events and identity envelopes", async () => { - const stateDir = tempDirs.make("openclaw-audit-writer-"); - const database = { env: { OPENCLAW_STATE_DIR: stateDir } }; - const { db } = openOpenClawStateDatabase(database); - db.exec("BEGIN IMMEDIATE"); - const errors: string[] = []; - const writer = createAuditEventWriter({ - stateDir, - maxPending: 1, - onError: (error) => errors.push(error), - }); - expect(writer.record(input())).toBe(true); - expect( - writer.recordExecutionIdentity( - captureWork( - captureExecutionIdentityAdmissionEnvelope( - { - runId: "queue-full-run", - agentId: "main", - ingress: { kind: "local-cli", boundary: "agent-command.local" }, - runtime: { kind: "embedded" }, - }, - { runtimeInstanceId: "runtime-1" }, - ), - ), - ), - ).toBe(false); - expect(errors).toContain("audit event queue is full (1); dropping metadata"); - db.exec("ROLLBACK"); - await writer.stop(); - expect(listAuditEvents({ database, limit: 10 }).events).toHaveLength(1); - }); - - it("preserves exact-envelope idempotency and safely reports every canonical conflict", async () => { - const stateDir = tempDirs.make("openclaw-audit-writer-"); - const database = { env: { OPENCLAW_STATE_DIR: stateDir } }; - const errors: string[] = []; - const writer = createAuditEventWriter({ stateDir, onError: (error) => errors.push(error) }); const admittedAt = Date.now(); const original = captureExecutionIdentityAdmissionEnvelope( { @@ -363,32 +333,6 @@ describe("audit event worker", () => { } }); - it("reports a lost durable recovery reference safely without blocking the caller", async () => { - const stateDir = tempDirs.make("openclaw-audit-writer-"); - const errors: string[] = []; - const writer = createAuditEventWriter({ stateDir, onError: (error) => errors.push(error) }); - const token = createExecutionIdentityAdmissionToken("raw-run-not-a-secret", { - contextId: "context-missing", - executionId: "execution-missing", - now: 100, - }); - - const startedAt = performance.now(); - expect(writer.recordExecutionIdentity({ kind: "retry-reference", token })).toBe(true); - expect(performance.now() - startedAt).toBeLessThan(250); - await writer.stop(); - - expect(errors).toContain("audit execution identity recovery evidence unavailable"); - expect(JSON.stringify(errors)).not.toContain(token.contextId); - expect(JSON.stringify(errors)).not.toContain(token.executionId); - expect(JSON.stringify(errors)).not.toContain(token.runId); - expect( - openOpenClawStateDatabase({ env: { OPENCLAW_STATE_DIR: stateDir } }) - .db.prepare("SELECT name FROM sqlite_schema WHERE type = 'table' AND name = ?") - .get("execution_identity_contexts"), - ).toBeUndefined(); - }); - it("keeps unavailable worker, schema, and insert failures off the admission path", async () => { const envelope = captureExecutionIdentityAdmissionEnvelope( { @@ -470,12 +414,29 @@ describe("audit event worker", () => { ).toMatchObject({ state: "unknown", reasonCode: "run_not_found" }); }); - it("keeps malformed, serialization, key, and persistence failures nonblocking and redaction-safe", async () => { + it("keeps malformed, serialization, and key failures nonblocking and redaction-safe", async () => { const stateDir = tempDirs.make("openclaw-audit-writer-"); const database = { env: { OPENCLAW_STATE_DIR: stateDir } }; - const errors: string[] = []; - const writer = createAuditEventWriter({ stateDir, onError: (error) => errors.push(error) }); const rawSecret = "raw-worker-message-secret"; + persistExecutionIdentityAdmissionEnvelope( + captureExecutionIdentityAdmissionEnvelope( + { + runId: "before-key-loss", + agentId: "main", + ingress: { kind: "local-cli", boundary: "agent-command.local" }, + runtime: { kind: "embedded" }, + }, + { runtimeInstanceId: "runtime-1" }, + ), + database, + ); + openOpenClawStateDatabase(database).db.exec("DELETE FROM audit_identity_keys;"); + closeOpenClawStateDatabaseForTest(); + const errors: string[] = []; + const writer = createAuditEventWriter({ + stateDir, + onError: (error) => errors.push(error), + }); const unserializable = { ...captureExecutionIdentityAdmissionEnvelope( { @@ -494,42 +455,9 @@ describe("audit event worker", () => { }, }; expect(writer.recordExecutionIdentity(captureWork(unserializable as never))).toBe(false); - await writer.stop(); - expect(errors).toContain("audit execution identity envelope could not be queued"); - expect(JSON.stringify(errors)).not.toContain(rawSecret); - - const malformedErrors: string[] = []; - const malformedWriter = createAuditEventWriter({ - stateDir, - onError: (error) => malformedErrors.push(error), - }); - expect(malformedWriter.recordExecutionIdentity({ rawSecret } as never)).toBe(true); - await malformedWriter.stop(); - expect(malformedErrors).toContain("audit execution identity envelope rejected"); - expect(JSON.stringify(malformedErrors)).not.toContain(rawSecret); - - closeOpenClawStateDatabaseForTest(); - persistExecutionIdentityAdmissionEnvelope( - captureExecutionIdentityAdmissionEnvelope( - { - runId: "before-key-loss", - agentId: "main", - ingress: { kind: "local-cli", boundary: "agent-command.local" }, - runtime: { kind: "embedded" }, - }, - { runtimeInstanceId: "runtime-1" }, - ), - database, - ); - openOpenClawStateDatabase(database).db.exec("DELETE FROM audit_identity_keys;"); - closeOpenClawStateDatabaseForTest(); - const keyErrors: string[] = []; - const keyWriter = createAuditEventWriter({ - stateDir, - onError: (error) => keyErrors.push(error), - }); + expect(writer.recordExecutionIdentity({ rawSecret } as never)).toBe(true); expect( - keyWriter.recordExecutionIdentity( + writer.recordExecutionIdentity( captureWork( captureExecutionIdentityAdmissionEnvelope( { @@ -543,9 +471,11 @@ describe("audit event worker", () => { ), ), ).toBe(true); - await keyWriter.stop(); - expect(keyErrors).toContain("audit execution identity key unavailable"); - expect(JSON.stringify(keyErrors)).not.toContain(rawSecret); + await writer.stop(); + expect(errors).toContain("audit execution identity envelope could not be queued"); + expect(errors).toContain("audit execution identity envelope rejected"); + expect(errors).toContain("audit execution identity key unavailable"); + expect(JSON.stringify(errors)).not.toContain(rawSecret); expect( inspectExecutionIdentityRun({ runId: "after-key-loss" }, database).identity, ).toMatchObject({ state: "unknown", reasonCode: "run_not_found" });