Files
AgentCompany/AgentCompany-New/tests/unit/migration-service.test.ts
T

347 lines
24 KiB
TypeScript

import { randomUUID } from "node:crypto";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { DatabaseSync } from "node:sqlite";
import { describe, expect, test } from "vitest";
import { DatabaseConnection } from "../../src/main/database/connection";
import { MigrationService } from "../../src/main/services/migration-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-migration-${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.",
});
}
function createLegacyDatabase(sql: string): string {
const legacyPath = join(tmpdir(), `agentcompany-legacy-${randomUUID()}.db`);
const legacy = new DatabaseSync(legacyPath);
legacy.exec(sql);
legacy.close();
return legacyPath;
}
interface MigrationReportBody {
sourceFingerprint: string;
tableCounts: Record<string, number>;
tableChecksums: Record<string, string>;
idMaps: Record<string, string[]>;
failures: Array<{ table: string; reason: string; detail: string }>;
validationSummary: {
missingTableCount: number;
totalSourceRows: number;
rowsWithIntegrityFailures: number;
rowCoverageBasisPoints: number;
checksumCoverageBasisPoints: number;
idMapCoverageBasisPoints: number;
relationFailureCount: number;
passed: boolean;
};
rollbackPack: {
sourceDatabasePath: string;
sourceFingerprint: string;
tableChecksums: Record<string, string>;
idMaps: Record<string, string[]>;
};
}
describe("migration service", () => {
test("creates idempotent reports with checksums, id maps, rollback pack, and integrity failures", () => {
const connection = createConnection();
const organization = new OrganizationService(connection.db);
const migrations = new MigrationService(connection.db);
const snapshot = new SnapshotService(connection.db);
const legacyPath = createLegacyDatabase(`
create table organizations(id text primary key);
insert into organizations(id) values ('org-1');
create table employees(id text primary key, org_id text);
insert into employees(id, org_id) values ('emp-1', 'org-1');
create table agents(id text primary key, org_id text);
insert into agents(id, org_id) values ('agent-1', 'org-missing');
create table tasks(id text primary key, org_id text, assignee_agent_id text);
insert into tasks(id, org_id, assignee_agent_id) values ('task-1', 'org-1', 'agent-missing');
`);
let orgId = "";
let firstReportId = "";
let secondReportId = "";
connection.transaction(() => {
orgId = createOrganization(organization);
firstReportId = migrations.createReport({ orgId, sourceDatabasePath: legacyPath });
secondReportId = migrations.createReport({ orgId, sourceDatabasePath: legacyPath });
});
const state = snapshot.load();
const body = JSON.parse(state.migrationReports[0]!.reportJson) as MigrationReportBody;
expect(secondReportId).toBe(firstReportId);
expect(state.migrationReports).toHaveLength(1);
expect(body.sourceFingerprint).toHaveLength(64);
expect(body.tableCounts.organizations).toBe(1);
expect(body.tableChecksums.agents).toHaveLength(64);
expect(body.idMaps.tasks).toEqual(["task-1"]);
expect(body.rollbackPack.sourceFingerprint).toBe(body.sourceFingerprint);
expect(body.rollbackPack.tableChecksums.agents).toBe(body.tableChecksums.agents);
expect(body.rollbackPack.idMaps.tasks).toEqual(["task-1"]);
expect(body.failures).toContainEqual({ table: "agents", reason: "dangling_org_id", detail: "agent-1 -> org-missing" });
expect(body.failures).toContainEqual({ table: "tasks", reason: "dangling_assignee_agent_id", detail: "task-1 -> agent-missing" });
expect(body.validationSummary).toMatchObject({
totalSourceRows: 4,
rowsWithIntegrityFailures: 2,
rowCoverageBasisPoints: 5000,
checksumCoverageBasisPoints: 10000,
idMapCoverageBasisPoints: 10000,
relationFailureCount: 3,
passed: false,
});
connection.close();
});
test("captures rebuilt domain tables and direct relation health", () => {
const connection = createConnection();
const organization = new OrganizationService(connection.db);
const migrations = new MigrationService(connection.db);
const snapshot = new SnapshotService(connection.db);
const legacyPath = createLegacyDatabase(`
create table organizations(id text primary key);
create table org_departments(id text primary key, org_id text, lead_agent_id text);
create table hiring_policies(id text primary key, org_id text);
create table employees(id text primary key, org_id text);
create table employee_identities(id text primary key, org_id text, employee_id text);
create table service_identities(id text primary key, org_id text, agent_id text);
create table role_grants(id text primary key, org_id text, granted_by_employee_id text);
create table access_decisions(id text primary key, org_id text);
create table agents(id text primary key, org_id text, model_provider_id text, manager_agent_id text, workspace_id text);
create table tasks(id text primary key, org_id text, assignee_agent_id text, project_id text, goal_id text, workspace_id text);
create table workspaces(id text primary key, org_id text, project_id text);
create table goals(id text primary key, org_id text, owner_agent_id text);
create table projects(id text primary key, org_id text, goal_id text, lead_agent_id text);
create table task_runs(id text primary key, org_id text, task_id text, agent_id text, provider_id text, provider_request_id text, service_identity_id text);
create table approvals(id text primary key, org_id text, decided_by_employee_id text);
create table approval_routing_rules(id text primary key, org_id text);
create table approval_assignments(id text primary key, org_id text, approval_id text, routing_rule_id text);
create table feature_flags(id text primary key, org_id text);
create table slo_targets(id text primary key, org_id text);
create table operational_events(id text primary key, org_id text);
create table chaos_drills(id text primary key, org_id text);
create table chaos_drill_runs(id text primary key, org_id text, drill_id text);
create table sdlc_review_agents(id text primary key, org_id text);
create table sdlc_review_runs(id text primary key, org_id text, review_agent_id text);
create table eval_suites(id text primary key, org_id text);
create table eval_cases(id text primary key, org_id text, suite_id text);
create table eval_runs(id text primary key, org_id text, suite_id text);
create table pr_bot_gates(id text primary key, org_id text);
create table chat_spaces(id text primary key, org_id text);
create table chat_messages(id text primary key, org_id text, space_id text, thread_id text);
create table chat_threads(id text primary key, org_id text, space_id text, root_message_id text);
create table chat_memberships(space_id text, principal_type text, principal_id text);
create table chat_attachments(message_id text, file_id text);
create table chat_mentions(message_id text, target_type text, target_id text);
create table chat_slash_invocations(id text primary key, message_id text);
create table board_interventions(id text primary key, org_id text, thread_id text, message_id text, approval_id text);
create table chat_presence(principal_type text, principal_id text);
create table chat_read_state(org_id text, employee_id text, space_id text, last_read_message_id text);
create table providers(id text primary key, org_id text, credential_id text);
create table provider_requests(id text primary key, org_id text, provider_id text, workspace_id text);
create table provider_usage_windows(id text primary key, org_id text, provider_id text);
create table provider_rate_limit_observations(id text primary key, org_id text, provider_id text);
create table provider_auth_profiles(id text primary key, org_id text, provider_id text, credential_id text);
create table provider_auth_sessions(id text primary key, org_id text, provider_id text, employee_id text);
create table credentials(id text primary key, org_id text);
create table skill_definitions(id text primary key, org_id text);
create table skill_revisions(id text primary key, skill_id text);
create table skill_artifacts(id text primary key, revision_id text);
create table skill_tests(id text primary key, revision_id text);
create table skill_dependencies(revision_id text, depends_on_revision_id text);
create table skill_bindings(id text primary key, org_id text, agent_id text, skill_id text, pinned_revision_id text);
create table skill_runtime_snapshots(id text primary key, agent_id text, run_id text);
create table skill_catalog_tags(skill_id text, tag text, tag_type text);
create table file_objects(id text primary key, org_id text, space_id text);
create table file_access_grants(id text primary key, org_id text, file_id text, source_message_id text, source_thread_id text);
create table file_handoff_records(id text primary key, org_id text, message_id text, file_id text);
create table social_accounts(id text primary key, org_id text, authorized_employee_id text);
create table social_account_grants(id text primary key, org_id text, social_account_id text, employee_id text);
create table browser_session_containers(id text primary key, org_id text, social_account_id text);
create table browser_action_policies(id text primary key, org_id text, social_account_id text);
create table social_approval_routes(id text primary key, org_id text, social_account_id text, route_to_id text);
create table browser_actions(id text primary key, org_id text, social_account_id text, evidence_file_id text);
create table social_audit_records(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 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);
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');
insert into employee_identities(id, org_id, employee_id) values ('eid-1', 'org-1', 'emp-1');
insert into credentials(id, org_id) values ('cred-1', 'org-1');
insert into providers(id, org_id, credential_id) values ('prov-1', 'org-1', 'cred-1');
insert into chat_spaces(id, org_id) values ('space-1', 'org-1');
insert into file_objects(id, org_id, space_id) values ('file-1', 'org-1', 'space-1');
insert into workspaces(id, org_id, project_id) values ('wsp-1', 'org-1', 'prj-1');
insert into agents(id, org_id, model_provider_id, manager_agent_id, workspace_id) values ('agent-1', 'org-1', 'prov-1', null, 'wsp-1');
insert into org_departments(id, org_id, lead_agent_id) values ('dep-1', 'org-1', 'agent-1');
insert into hiring_policies(id, org_id) values ('hpl-1', 'org-1');
insert into service_identities(id, org_id, agent_id) values ('sid-1', 'org-1', 'agent-1');
insert into role_grants(id, org_id, granted_by_employee_id) values ('role-1', 'org-1', 'emp-1');
insert into access_decisions(id, org_id) values ('access-1', 'org-1');
insert into goals(id, org_id, owner_agent_id) values ('goal-1', 'org-1', 'agent-1');
insert into projects(id, org_id, goal_id, lead_agent_id) values ('prj-1', 'org-1', 'goal-1', 'agent-1');
insert into tasks(id, org_id, assignee_agent_id, project_id, goal_id, workspace_id) values ('task-1', 'org-1', 'agent-1', 'prj-1', 'goal-1', 'wsp-1');
insert into task_runs(id, org_id, task_id, agent_id, provider_id, provider_request_id, service_identity_id) values ('run-1', 'org-1', 'task-1', 'agent-1', 'prov-1', 'req-1', 'sid-1');
insert into approvals(id, org_id, decided_by_employee_id) values ('approval-1', 'org-1', 'emp-1');
insert into approval_routing_rules(id, org_id) values ('route-1', 'org-1');
insert into approval_assignments(id, org_id, approval_id, routing_rule_id) values ('assign-1', 'org-1', 'approval-1', 'route-1');
insert into feature_flags(id, org_id) values ('flag-1', 'org-1');
insert into slo_targets(id, org_id) values ('slo-1', 'org-1');
insert into operational_events(id, org_id) values ('op-1', 'org-1');
insert into chaos_drills(id, org_id) values ('drill-1', 'org-1');
insert into chaos_drill_runs(id, org_id, drill_id) values ('drill-run-1', 'org-1', 'drill-1');
insert into sdlc_review_agents(id, org_id) values ('reviewer-1', 'org-1');
insert into sdlc_review_runs(id, org_id, review_agent_id) values ('review-run-1', 'org-1', 'reviewer-1');
insert into eval_suites(id, org_id) values ('suite-1', 'org-1');
insert into eval_cases(id, org_id, suite_id) values ('case-1', 'org-1', 'suite-1');
insert into eval_runs(id, org_id, suite_id) values ('eval-run-1', 'org-1', 'suite-1');
insert into pr_bot_gates(id, org_id) values ('gate-1', 'org-1');
insert into chat_threads(id, org_id, space_id, root_message_id) values ('thread-1', 'org-1', 'space-1', null);
insert into chat_messages(id, org_id, space_id, thread_id) values ('msg-1', 'org-1', 'space-1', 'thread-1');
insert into chat_memberships(space_id, principal_type, principal_id) values ('space-1', 'employee', 'emp-1');
insert into chat_attachments(message_id, file_id) values ('msg-1', 'file-1');
insert into file_access_grants(id, org_id, file_id, source_message_id, source_thread_id) values ('file-grant-1', 'org-1', 'file-1', 'msg-1', 'thread-1');
insert into file_handoff_records(id, org_id, message_id, file_id) values ('handoff-1', 'org-1', 'msg-1', 'file-1');
insert into chat_mentions(message_id, target_type, target_id) values ('msg-1', 'agent', 'agent-1');
insert into chat_slash_invocations(id, message_id) values ('slash-1', 'msg-1');
insert into board_interventions(id, org_id, thread_id, message_id, approval_id) values ('bdi-1', 'org-1', 'thread-1', 'msg-1', 'approval-1');
insert into chat_presence(principal_type, principal_id) values ('agent', 'agent-1');
insert into chat_read_state(org_id, employee_id, space_id, last_read_message_id) values ('org-1', 'emp-1', 'space-1', 'msg-1');
insert into provider_requests(id, org_id, provider_id, workspace_id) values ('req-1', 'org-1', 'prov-1', 'wsp-1');
insert into provider_usage_windows(id, org_id, provider_id) values ('usage-1', 'org-1', 'prov-1');
insert into provider_rate_limit_observations(id, org_id, provider_id) values ('quota-1', 'org-1', 'prov-1');
insert into provider_auth_profiles(id, org_id, provider_id, credential_id) values ('authp-1', 'org-1', 'prov-1', 'cred-1');
insert into provider_auth_sessions(id, org_id, provider_id, employee_id) values ('auths-1', 'org-1', 'prov-1', 'emp-1');
insert into skill_definitions(id, org_id) values ('skill-1', 'org-1');
insert into skill_revisions(id, skill_id) values ('rev-1', 'skill-1'), ('rev-2', 'skill-1');
insert into skill_artifacts(id, revision_id) values ('artifact-1', 'rev-1');
insert into skill_tests(id, revision_id) values ('test-1', 'rev-1');
insert into skill_dependencies(revision_id, depends_on_revision_id) values ('rev-1', 'rev-2');
insert into skill_bindings(id, org_id, agent_id, skill_id, pinned_revision_id) values ('binding-1', 'org-1', 'agent-1', 'skill-1', 'rev-1');
insert into skill_runtime_snapshots(id, agent_id, run_id) values ('snapshot-1', 'agent-1', 'run-1');
insert into skill_catalog_tags(skill_id, tag, tag_type) values ('skill-1', 'engineering', 'department');
insert into social_accounts(id, org_id, authorized_employee_id) values ('social-1', 'org-1', 'emp-1');
insert into social_account_grants(id, org_id, social_account_id, employee_id) values ('grant-1', 'org-1', 'social-1', 'emp-1');
insert into browser_session_containers(id, org_id, social_account_id) values ('container-1', 'org-1', 'social-1');
insert into browser_action_policies(id, org_id, social_account_id) values ('browser-policy-1', 'org-1', 'social-1');
insert into social_approval_routes(id, org_id, social_account_id, route_to_id) values ('social-route-1', 'org-1', 'social-1', 'emp-1');
insert into browser_actions(id, org_id, social_account_id, evidence_file_id) values ('browser-1', 'org-1', 'social-1', 'file-1');
insert into social_audit_records(id, org_id, social_account_id, evidence_file_id) values ('social-audit-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 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');
insert into audit_events(id, org_id) values ('audit-1', 'org-1');
`);
let orgId = "";
connection.transaction(() => {
orgId = createOrganization(organization);
migrations.createReport({ orgId, sourceDatabasePath: legacyPath });
});
const body = JSON.parse(snapshot.load().migrationReports[0]!.reportJson) as MigrationReportBody;
expect(body.failures).toEqual([]);
expect(body.validationSummary).toMatchObject({
missingTableCount: 0,
rowCoverageBasisPoints: 10000,
checksumCoverageBasisPoints: 10000,
idMapCoverageBasisPoints: 10000,
relationFailureCount: 0,
passed: true,
});
expect(body.tableCounts.credentials).toBe(1);
expect(body.tableCounts.employee_identities).toBe(1);
expect(body.tableCounts.org_departments).toBe(1);
expect(body.tableCounts.hiring_policies).toBe(1);
expect(body.tableCounts.feature_flags).toBe(1);
expect(body.tableCounts.chaos_drill_runs).toBe(1);
expect(body.tableCounts.sdlc_review_runs).toBe(1);
expect(body.tableCounts.eval_cases).toBe(1);
expect(body.tableCounts.role_grants).toBe(1);
expect(body.tableCounts.skill_artifacts).toBe(1);
expect(body.tableCounts.chat_read_state).toBe(1);
expect(body.tableCounts.board_interventions).toBe(1);
expect(body.tableCounts.file_access_grants).toBe(1);
expect(body.tableCounts.file_handoff_records).toBe(1);
expect(body.tableCounts.provider_requests).toBe(1);
expect(body.tableCounts.provider_usage_windows).toBe(1);
expect(body.tableCounts.provider_rate_limit_observations).toBe(1);
expect(body.tableCounts.provider_auth_profiles).toBe(1);
expect(body.tableCounts.provider_auth_sessions).toBe(1);
expect(body.tableCounts.browser_session_containers).toBe(1);
expect(body.tableCounts.browser_action_policies).toBe(1);
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);
expect(body.tableCounts.search_documents).toBe(1);
expect(body.tableChecksums.audit_events).toHaveLength(64);
expect(body.idMaps.skill_runtime_snapshots).toEqual(["snapshot-1"]);
expect(body.idMaps.service_identities).toEqual(["sid-1"]);
expect(body.idMaps.access_decisions).toEqual(["access-1"]);
expect(body.idMaps.approval_assignments).toEqual(["assign-1"]);
expect(body.idMaps.operational_events).toEqual(["op-1"]);
expect(body.idMaps.chaos_drill_runs).toEqual(["drill-run-1"]);
expect(body.idMaps.sdlc_review_runs).toEqual(["review-run-1"]);
expect(body.idMaps.eval_runs).toEqual(["eval-run-1"]);
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"]);
expect(body.idMaps.provider_usage_windows).toEqual(["usage-1"]);
expect(body.idMaps.provider_rate_limit_observations).toEqual(["quota-1"]);
expect(body.idMaps.provider_auth_profiles).toEqual(["authp-1"]);
expect(body.idMaps.provider_auth_sessions).toEqual(["auths-1"]);
expect(body.idMaps.browser_session_containers).toEqual(["container-1"]);
expect(body.idMaps.browser_action_policies).toEqual(["browser-policy-1"]);
expect(body.idMaps.social_approval_routes).toEqual(["social-route-1"]);
expect(body.idMaps.social_audit_records).toEqual(["social-audit-1"]);
connection.close();
});
test("rejects reports for unknown target organizations", () => {
const connection = createConnection();
const migrations = new MigrationService(connection.db);
const legacyPath = createLegacyDatabase("create table organizations(id text primary key);");
expect(() => connection.transaction(() => migrations.createReport({
orgId: "org_missing",
sourceDatabasePath: legacyPath,
}))).toThrow("Organization was not found.");
connection.close();
});
});