Add compliance controls
This commit is contained in:
1 parent
78dbcbe7ce
commit
38c68ca4af
19 files changed
+882
-7
No files matched your search
@@ -0,0 +1,165 @@
|
||||
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 { ChatService } from "../../src/main/services/chat-service";
|
||||
import { ComplianceService } from "../../src/main/services/compliance-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-compliance-${randomUUID()}.db`));
|
||||
}
|
||||
|
||||
describe("compliance service", () => {
|
||||
test("creates GDPR export payloads with audit coverage", () => {
|
||||
const connection = createConnection();
|
||||
const organization = new OrganizationService(connection.db);
|
||||
const chat = new ChatService(connection.db);
|
||||
const compliance = new ComplianceService(connection.db);
|
||||
const snapshot = new SnapshotService(connection.db);
|
||||
let orgId = "";
|
||||
let employeeId = "";
|
||||
let spaceId = "";
|
||||
|
||||
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;
|
||||
spaceId = chat.createSpace({ orgId, spaceType: "project", name: "release", topic: "Release coordination", visibility: "workspace" });
|
||||
chat.sendMessage({
|
||||
orgId,
|
||||
spaceId,
|
||||
threadId: null,
|
||||
senderType: "employee",
|
||||
senderId: employeeId,
|
||||
bodyMd: "Exportable message",
|
||||
attachmentFileIds: [],
|
||||
});
|
||||
compliance.createExport({
|
||||
orgId,
|
||||
subjectType: "employee",
|
||||
subjectId: employeeId,
|
||||
exportType: "gdpr_export",
|
||||
requestedByEmployeeId: employeeId,
|
||||
});
|
||||
});
|
||||
|
||||
const state = snapshot.load();
|
||||
const record = state.complianceExports[0];
|
||||
if (!record) throw new Error("Compliance export was not created.");
|
||||
const payload = JSON.parse(record.payloadJson) as { profile: { display_name: string }; chatMessages: unknown[] };
|
||||
expect(record).toMatchObject({ subjectType: "employee", subjectId: employeeId, exportType: "gdpr_export", state: "completed" });
|
||||
expect(payload.profile.display_name).toBe("Dana Lee");
|
||||
expect(payload.chatMessages).toHaveLength(1);
|
||||
expect(state.auditEvents.some((event) => event.entityType === "compliance_export" && event.action === "created")).toBe(true);
|
||||
connection.close();
|
||||
});
|
||||
|
||||
test("blocks GDPR delete under holds and redacts after release", () => {
|
||||
const connection = createConnection();
|
||||
const organization = new OrganizationService(connection.db);
|
||||
const chat = new ChatService(connection.db);
|
||||
const compliance = new ComplianceService(connection.db);
|
||||
const snapshot = new SnapshotService(connection.db);
|
||||
let orgId = "";
|
||||
let employeeId = "";
|
||||
let spaceId = "";
|
||||
let holdId = "";
|
||||
|
||||
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;
|
||||
spaceId = chat.createSpace({ orgId, spaceType: "project", name: "release", topic: "Release coordination", visibility: "workspace" });
|
||||
chat.sendMessage({ orgId, spaceId, threadId: null, senderType: "employee", senderId: employeeId, bodyMd: "Delete me", attachmentFileIds: [] });
|
||||
holdId = compliance.createLegalHold({
|
||||
orgId,
|
||||
entityType: "employee",
|
||||
entityId: employeeId,
|
||||
reason: "Discovery hold",
|
||||
createdByEmployeeId: employeeId,
|
||||
});
|
||||
});
|
||||
|
||||
expect(() => connection.transaction(() => compliance.createExport({
|
||||
orgId,
|
||||
subjectType: "employee",
|
||||
subjectId: employeeId,
|
||||
exportType: "gdpr_delete",
|
||||
requestedByEmployeeId: employeeId,
|
||||
}))).toThrow("Active compliance hold prevents GDPR delete.");
|
||||
|
||||
connection.transaction(() => {
|
||||
compliance.setLegalHoldState({ orgId, legalHoldId: holdId, active: false, employeeId });
|
||||
compliance.createExport({
|
||||
orgId,
|
||||
subjectType: "employee",
|
||||
subjectId: employeeId,
|
||||
exportType: "gdpr_delete",
|
||||
requestedByEmployeeId: employeeId,
|
||||
});
|
||||
});
|
||||
|
||||
const state = snapshot.load();
|
||||
expect(state.employees[0]?.displayName).toBe("Deleted employee");
|
||||
expect(state.chatMessages[0]?.senderId).toBeNull();
|
||||
expect(state.complianceExports[0]?.exportType).toBe("gdpr_delete");
|
||||
expect(state.auditEvents.some((event) => event.entityType === "compliance_export" && event.action === "gdpr_deleted")).toBe(true);
|
||||
connection.close();
|
||||
});
|
||||
|
||||
test("blocks GDPR delete under organization retention freeze", () => {
|
||||
const connection = createConnection();
|
||||
const organization = new OrganizationService(connection.db);
|
||||
const compliance = new ComplianceService(connection.db);
|
||||
let orgId = "";
|
||||
let employeeId = "";
|
||||
let agentId = "";
|
||||
|
||||
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;
|
||||
agentId = organization.createAgent({
|
||||
orgId,
|
||||
name: "Ada",
|
||||
role: "Release reviewer",
|
||||
department: "Engineering",
|
||||
modelProviderId: null,
|
||||
modelId: null,
|
||||
budgetCents: 10000,
|
||||
managerAgentId: null,
|
||||
});
|
||||
compliance.createRetentionFreeze({
|
||||
orgId,
|
||||
scopeType: "organization",
|
||||
scopeId: orgId,
|
||||
reason: "Retention freeze",
|
||||
createdByEmployeeId: employeeId,
|
||||
});
|
||||
});
|
||||
|
||||
expect(() => connection.transaction(() => compliance.createExport({
|
||||
orgId,
|
||||
subjectType: "agent",
|
||||
subjectId: agentId,
|
||||
exportType: "gdpr_delete",
|
||||
requestedByEmployeeId: employeeId,
|
||||
}))).toThrow("Active compliance hold prevents GDPR delete.");
|
||||
connection.close();
|
||||
});
|
||||
});
|
||||
@@ -23,7 +23,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]);
|
||||
expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
|
||||
connection.close();
|
||||
});
|
||||
|
||||
|
||||
@@ -128,6 +128,9 @@ describe("migration service", () => {
|
||||
create table browser_actions(id text primary key, org_id text, social_account_id text, evidence_file_id text);
|
||||
create table scheduled_posts(id text primary key, org_id text, social_account_id text, approval_id text, created_by_employee_id text);
|
||||
create table policy_rules(id text primary key, org_id text);
|
||||
create table compliance_exports(id text primary key, org_id text, requested_by_employee_id text);
|
||||
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 audit_events(id text primary key, org_id text);
|
||||
insert into organizations(id) values ('org-1');
|
||||
insert into employees(id, org_id) values ('emp-1', 'org-1');
|
||||
@@ -166,6 +169,9 @@ describe("migration service", () => {
|
||||
insert into browser_actions(id, org_id, social_account_id, evidence_file_id) values ('browser-1', 'org-1', 'social-1', 'file-1');
|
||||
insert into scheduled_posts(id, org_id, social_account_id, approval_id, created_by_employee_id) values ('post-1', 'org-1', 'social-1', 'approval-1', 'emp-1');
|
||||
insert into policy_rules(id, org_id) values ('policy-1', 'org-1');
|
||||
insert into compliance_exports(id, org_id, requested_by_employee_id) values ('cmp-1', 'org-1', 'emp-1');
|
||||
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 audit_events(id, org_id) values ('audit-1', 'org-1');
|
||||
`);
|
||||
let orgId = "";
|
||||
@@ -183,9 +189,11 @@ describe("migration service", () => {
|
||||
expect(body.tableCounts.board_interventions).toBe(1);
|
||||
expect(body.tableCounts.file_access_grants).toBe(1);
|
||||
expect(body.tableCounts.provider_requests).toBe(1);
|
||||
expect(body.tableCounts.compliance_exports).toBe(1);
|
||||
expect(body.tableChecksums.audit_events).toHaveLength(64);
|
||||
expect(body.idMaps.skill_runtime_snapshots).toEqual(["snapshot-1"]);
|
||||
expect(body.idMaps.board_interventions).toEqual(["bdi-1"]);
|
||||
expect(body.idMaps.retention_freezes).toEqual(["freeze-1"]);
|
||||
connection.close();
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user