Q33: document edits and content deletions leave attributed audit records

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-08 06:45:51 -04:00
1 parent cb464c3ee4
commit 66de4ef015
9 files changed
+147 -16

No files matched your search

+76
View File
@@ -0,0 +1,76 @@
import { 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 { OrganizationService } from "../../src/main/services/organization-service";
import { DocumentService } from "../../src/main/services/document-service";
import { KnowledgeService } from "../../src/main/services/knowledge-service";
function setup() {
const connection = new DatabaseConnection(join(tmpdir(), `ac-audit-${randomUUID()}.db`));
let orgId = "";
connection.transaction(() => {
orgId = new OrganizationService(connection.db).createOrganization({ organizationName: "A", employeeName: "Eve", mission: "m", goal: "g" });
});
const employeeId = (connection.db.prepare("select id from employees where org_id = ?").get(orgId) as { id: string }).id;
const events = (entityId: string) => connection.db.prepare(
"select actor_type, actor_id, action, payload_json from audit_events where entity_id = ? order by rowid",
).all(entityId).map((row) => ({ ...row, payload_json: JSON.parse(String(row.payload_json)) }));
return { connection, orgId, employeeId, events };
}
describe("content audit", () => {
test("document updates and deletion record who changed what", () => {
const { connection, orgId, employeeId, events } = setup();
const documents = new DocumentService(connection.db);
const id = documents.create({ orgId, title: "Spec", docType: "spec", content: "v1", authorAgentId: null, reviewerAgentId: null, projectId: null, goalId: null, taskId: null });
documents.update({ orgId, documentId: id, title: "Spec v2", content: "v2", status: "review", reason: "Added API", changedByType: "employee", changedById: employeeId });
documents.delete({ orgId, documentId: id, deletedByEmployeeId: employeeId });
expect(events(id).slice(1)).toEqual([
{ actor_type: "employee", actor_id: employeeId, action: "revised", payload_json: { version: 2, reason: "Added API", title: "Spec v2", status: "review", previousTitle: "Spec", previousStatus: "draft" } },
{ actor_type: "employee", actor_id: employeeId, action: "deleted", payload_json: { title: "Spec v2", docType: "spec", status: "review", version: 2, content: "v2" } },
]);
expect(() => documents.delete({ orgId, documentId: id, deletedByEmployeeId: employeeId })).toThrow("Document was not found.");
connection.close();
});
test("knowledge deletion records the removed entry and who removed it", () => {
const { connection, orgId, employeeId, events } = setup();
const knowledge = new KnowledgeService(connection.db);
const id = knowledge.create({
orgId, topic: "Refunds", category: "policy", content: "14 days", importance: "high",
sourceType: "employee", sourceId: employeeId, scopeType: "organization", scopeId: null, validUntil: null, supersedesId: null,
});
knowledge.delete({ orgId, entryId: id, deletedByEmployeeId: employeeId });
expect(events(id).at(-1)).toEqual({
actor_type: "employee", actor_id: employeeId, action: "deleted",
payload_json: { topic: "Refunds", category: "policy", content: "14 days", importance: "high", scopeType: "organization", scopeId: null, revision: 1 },
});
connection.close();
});
test("changes by someone outside the organization are refused", () => {
const { connection, orgId, employeeId } = setup();
const documents = new DocumentService(connection.db);
const id = documents.create({ orgId, title: "Spec", docType: "spec", content: "v1", authorAgentId: null, reviewerAgentId: null, projectId: null, goalId: null, taskId: null });
expect(() => documents.update({ orgId, documentId: id, title: "Spec", content: "v2", status: "draft", reason: "r", changedByType: "employee", changedById: "emp_stranger" }))
.toThrow('No employee "emp_stranger" in this organization.');
const knowledge = new KnowledgeService(connection.db);
const entryId = knowledge.create({
orgId, topic: "T", category: "fact", content: "c", importance: "normal",
sourceType: "employee", sourceId: null, scopeType: "organization", scopeId: null, validUntil: null, supersedesId: null,
});
expect(() => knowledge.update({ orgId, entryId, topic: "T", category: "fact", content: "c2", importance: "normal", reason: "r", changedByType: "agent", changedById: "agt_stranger" }))
.toThrow('No agent "agt_stranger" in this organization.');
const badType = { orgId, documentId: id, title: "Spec", content: "v2", status: "draft", reason: "r", changedByType: "system" as "agent", changedById: employeeId };
expect(() => documents.update(badType)).toThrow('Unknown actor type "system".');
expect(documents.versions(orgId, id)).toHaveLength(1);
expect(knowledge.revisions(orgId, entryId)).toHaveLength(1);
expect(() => documents.delete({ orgId, documentId: id, deletedByEmployeeId: "emp_stranger" })).toThrow('No employee "emp_stranger" in this organization.');
expect(connection.db.prepare("select count(*) as n from documents where id = ?").get(id)).toEqual({ n: 1 });
connection.close();
});
});
+4 -3
View File
@@ -16,16 +16,17 @@ describe("document versions", () => {
orgId = new OrganizationService(connection.db).createOrganization({ organizationName: "V", employeeName: "Y", mission: "m", goal: "g" });
});
const documents = new DocumentService(connection.db);
const employeeId = (connection.db.prepare("select id from employees where org_id = ?").get(orgId) as { id: string }).id;
const id = documents.create({ orgId, title: "Spec", docType: "spec", content: "v1 body", authorAgentId: null, reviewerAgentId: null, projectId: null, goalId: null, taskId: null });
expect(documents.update({ orgId, documentId: id, title: "Spec", content: "v2 body", status: "review", reason: "Added API", changedByType: "employee", changedById: null })).toBe(2);
expect(() => documents.update({ orgId, documentId: id, title: "Spec", content: "v3", status: "review", reason: " ", changedByType: "employee", changedById: null }))
expect(documents.update({ orgId, documentId: id, title: "Spec", content: "v2 body", status: "review", reason: "Added API", changedByType: "employee", changedById: employeeId })).toBe(2);
expect(() => documents.update({ orgId, documentId: id, title: "Spec", content: "v3", status: "review", reason: " ", changedByType: "employee", changedById: employeeId }))
.toThrow("A document update needs a reason.");
expect(documents.versions(orgId, id).map((v) => [v.version, v.content, v.status, v.reason])).toEqual([
[1, "v1 body", "draft", "created"],
[2, "v2 body", "review", "Added API"],
]);
expect(connection.db.prepare("select version, content from documents where id = ?").get(id)).toEqual({ version: 2, content: "v2 body" });
documents.delete({ orgId, documentId: id });
documents.delete({ orgId, documentId: id, deletedByEmployeeId: employeeId });
expect(documents.versions(orgId, id)).toHaveLength(2);
connection.close();
});
+2 -1
View File
@@ -39,7 +39,8 @@ describe("knowledge revisions", () => {
expect(run("revise_knowledge", { id, content: "x" })).toBe('ERROR: "reason" is required.');
expect(run("revise_knowledge", { id: "kne_missing", reason: "r" })).toBe('ERROR: No knowledge entry with id "kne_missing".');
new KnowledgeService(connection.db).delete({ orgId, entryId: id });
const employeeId = (connection.db.prepare("select id from employees where org_id = ?").get(orgId) as { id: string }).id;
new KnowledgeService(connection.db).delete({ orgId, entryId: id, deletedByEmployeeId: employeeId });
expect(run("search_knowledge", { id })).toContain(`Knowledge entry ${id} (deleted), 2 revision(s):`);
connection.close();
});