From 49c1dd2888f4cdeee08519b8af170dc7d11c9932 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Wed, 20 May 2026 23:42:53 -0400 Subject: [PATCH] Add migration verification reports --- AgentCompany-New/src/main/app/ipc-handlers.ts | 4 ++ .../src/main/services/migration-service.ts | 65 +++++++++++++++++++ .../src/main/services/snapshot-service.ts | 1 + AgentCompany-New/src/preload/index.ts | 1 + AgentCompany-New/src/shared/commands.ts | 5 ++ AgentCompany-New/src/shared/desktop-api.ts | 2 + AgentCompany-New/src/shared/domain.ts | 9 +++ AgentCompany-New/src/shared/ipc.ts | 1 + .../tests/unit/domain-services.test.ts | 29 +++++++++ 9 files changed, 117 insertions(+) create mode 100644 AgentCompany-New/src/main/services/migration-service.ts diff --git a/AgentCompany-New/src/main/app/ipc-handlers.ts b/AgentCompany-New/src/main/app/ipc-handlers.ts index 5c29419e..aa3b730b 100644 --- a/AgentCompany-New/src/main/app/ipc-handlers.ts +++ b/AgentCompany-New/src/main/app/ipc-handlers.ts @@ -11,6 +11,7 @@ import { ProviderService } from "../services/provider-service"; import { SkillService } from "../services/skill-service"; import { SocialService } from "../services/social-service"; import { FileService } from "../services/file-service"; +import { MigrationService } from "../services/migration-service"; export interface IpcDeps { connection: DatabaseConnection; @@ -27,6 +28,7 @@ export function registerIpcHandlers(deps: IpcDeps): void { const skills = new SkillService(deps.connection.db); const social = new SocialService(deps.connection.db); const files = new FileService(deps.connection.db, deps.artifactRoot); + const migrations = new MigrationService(deps.connection.db); const mutate = (operation: () => T): DesktopResult => { try { @@ -87,6 +89,8 @@ export function registerIpcHandlers(deps: IpcDeps): void { } }); + ipcMain.handle(IPC_CHANNELS.createMigrationReport, (_event, input) => mutate(() => migrations.createReport(input))); + ipcMain.handle(IPC_CHANNELS.windowMinimize, () => deps.mainWindow.minimize()); ipcMain.handle(IPC_CHANNELS.windowMaximize, () => deps.mainWindow.isMaximized() ? deps.mainWindow.unmaximize() : deps.mainWindow.maximize()); ipcMain.handle(IPC_CHANNELS.windowClose, () => deps.mainWindow.close()); diff --git a/AgentCompany-New/src/main/services/migration-service.ts b/AgentCompany-New/src/main/services/migration-service.ts new file mode 100644 index 00000000..c906be80 --- /dev/null +++ b/AgentCompany-New/src/main/services/migration-service.ts @@ -0,0 +1,65 @@ +import { existsSync } from "node:fs"; +import { DatabaseSync, type StatementSync } from "node:sqlite"; +import type { CreateMigrationReportInput } from "../../shared/commands"; +import { createId } from "../../shared/ids"; +import { AuditService } from "./audit-service"; + +const LEGACY_TABLES = [ + "organizations", + "agents", + "tasks", + "approvals", + "chat_messages", + "providers", + "skill_definitions", + "file_objects", + "social_accounts", +]; + +export class MigrationService { + private readonly audit: AuditService; + + constructor(private readonly db: DatabaseSync) { + this.audit = new AuditService(db); + } + + createReport(input: CreateMigrationReportInput): string { + if (!existsSync(input.sourceDatabasePath)) throw new Error("Source database was not found."); + const source = new DatabaseSync(input.sourceDatabasePath, { readOnly: true }); + try { + const availableTables = new Set( + source.prepare("select name from sqlite_master where type = 'table'") + .all() + .map((row) => (row as { name: string }).name), + ); + const tableCounts = Object.fromEntries( + LEGACY_TABLES + .filter((table) => availableTables.has(table)) + .map((table) => [table, countRows(source.prepare(`select count(*) as count from ${table}`))]), + ); + const missingTables = LEGACY_TABLES.filter((table) => !availableTables.has(table)); + const reportId = createId("mig"); + const now = new Date().toISOString(); + this.db.prepare(` + insert into migration_reports(id, org_id, source_ref, report_json, created_at) + values (?, ?, ?, ?, ?) + `).run(reportId, input.orgId, input.sourceDatabasePath, JSON.stringify({ tableCounts, missingTables }), now); + this.audit.record({ + orgId: input.orgId, + entityType: "migration_report", + entityId: reportId, + actorType: "system", + actorId: null, + action: "created", + payload: { sourceRef: input.sourceDatabasePath, tableCount: Object.keys(tableCounts).length }, + }); + return reportId; + } finally { + source.close(); + } + } +} + +function countRows(statement: StatementSync): number { + return (statement.get() as { count: number }).count; +} diff --git a/AgentCompany-New/src/main/services/snapshot-service.ts b/AgentCompany-New/src/main/services/snapshot-service.ts index 7caade41..5efbfb66 100644 --- a/AgentCompany-New/src/main/services/snapshot-service.ts +++ b/AgentCompany-New/src/main/services/snapshot-service.ts @@ -57,6 +57,7 @@ export class SnapshotService { skillRuntimeSnapshots: list(this.db, "skill_runtime_snapshots", "created_at desc"), socialAccounts: list(this.db, "social_accounts", "created_at asc"), policyRules: list(this.db, "policy_rules", "created_at asc"), + migrationReports: list(this.db, "migration_reports", "created_at desc"), auditEvents: list(this.db, "audit_events", "created_at desc").slice(0, 200), }; } diff --git a/AgentCompany-New/src/preload/index.ts b/AgentCompany-New/src/preload/index.ts index 6f66cc63..4deb1e12 100644 --- a/AgentCompany-New/src/preload/index.ts +++ b/AgentCompany-New/src/preload/index.ts @@ -23,6 +23,7 @@ const api: DesktopApi = { createSocialAccount: (input) => invoke(IPC_CHANNELS.createSocialAccount, input), launchSocialLogin: (input) => invoke(IPC_CHANNELS.launchSocialLogin, input), importFile: (input) => invoke(IPC_CHANNELS.importFile, input), + createMigrationReport: (input) => invoke(IPC_CHANNELS.createMigrationReport, input), subscribe: (callback) => { const listener = (_event: Electron.IpcRendererEvent, payload: RendererEvent) => callback(payload); ipcRenderer.on(IPC_CHANNELS.rendererEvent, listener); diff --git a/AgentCompany-New/src/shared/commands.ts b/AgentCompany-New/src/shared/commands.ts index 566d00ce..ee878690 100644 --- a/AgentCompany-New/src/shared/commands.ts +++ b/AgentCompany-New/src/shared/commands.ts @@ -114,6 +114,11 @@ export interface ImportFileInput { orgId: string; } +export interface CreateMigrationReportInput { + orgId: string; + sourceDatabasePath: string; +} + export interface NativeDialogResult { canceled: boolean; filePaths: string[]; diff --git a/AgentCompany-New/src/shared/desktop-api.ts b/AgentCompany-New/src/shared/desktop-api.ts index 68584196..e31b8d1e 100644 --- a/AgentCompany-New/src/shared/desktop-api.ts +++ b/AgentCompany-New/src/shared/desktop-api.ts @@ -4,6 +4,7 @@ import type { BindSkillInput, CreateAgentInput, CreateApprovalInput, + CreateMigrationReportInput, CreateOrganizationInput, CreateProviderResponseInput, CreateSocialAccountInput, @@ -39,6 +40,7 @@ export interface DesktopApi { createSocialAccount(input: CreateSocialAccountInput): Promise>; launchSocialLogin(input: LaunchSocialLoginInput): Promise>; importFile(input: ImportFileInput): Promise>; + createMigrationReport(input: CreateMigrationReportInput): Promise>; subscribe(callback: (event: RendererEvent) => void): () => void; platform: "win32" | "darwin" | "linux"; windowMinimize(): Promise; diff --git a/AgentCompany-New/src/shared/domain.ts b/AgentCompany-New/src/shared/domain.ts index ead2f058..a37d72ec 100644 --- a/AgentCompany-New/src/shared/domain.ts +++ b/AgentCompany-New/src/shared/domain.ts @@ -275,6 +275,14 @@ export interface PolicyRuleRecord { createdAt: string; } +export interface MigrationReportRecord { + id: string; + orgId: string; + sourceRef: string; + reportJson: string; + createdAt: string; +} + export interface AppSnapshot { activeOrgId: string | null; organizations: OrganizationRecord[]; @@ -297,5 +305,6 @@ export interface AppSnapshot { skillRuntimeSnapshots: SkillRuntimeSnapshotRecord[]; socialAccounts: SocialAccountRecord[]; policyRules: PolicyRuleRecord[]; + migrationReports: MigrationReportRecord[]; auditEvents: AuditEventRecord[]; } diff --git a/AgentCompany-New/src/shared/ipc.ts b/AgentCompany-New/src/shared/ipc.ts index bc6b778d..5e1f42db 100644 --- a/AgentCompany-New/src/shared/ipc.ts +++ b/AgentCompany-New/src/shared/ipc.ts @@ -15,6 +15,7 @@ export const IPC_CHANNELS = { createSocialAccount: "agentcompany:create-social-account", launchSocialLogin: "agentcompany:launch-social-login", importFile: "agentcompany:import-file", + createMigrationReport: "agentcompany:create-migration-report", rendererEvent: "agentcompany:renderer-event", windowMinimize: "agentcompany:window-minimize", windowMaximize: "agentcompany:window-maximize", diff --git a/AgentCompany-New/tests/unit/domain-services.test.ts b/AgentCompany-New/tests/unit/domain-services.test.ts index 9b8eec33..5f2ccb1e 100644 --- a/AgentCompany-New/tests/unit/domain-services.test.ts +++ b/AgentCompany-New/tests/unit/domain-services.test.ts @@ -2,12 +2,14 @@ import { mkdirSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { randomUUID } from "node:crypto"; +import { DatabaseSync } from "node:sqlite"; import { describe, expect, test } from "vitest"; import { DatabaseConnection } from "../../src/main/database/connection"; import { OrganizationService } from "../../src/main/services/organization-service"; import { SnapshotService } from "../../src/main/services/snapshot-service"; import { SkillService } from "../../src/main/services/skill-service"; import { ChatService } from "../../src/main/services/chat-service"; +import { MigrationService } from "../../src/main/services/migration-service"; import { RISK_LEVEL, TASK_STATUS } from "../../src/shared/domain"; import { createId } from "../../src/shared/ids"; @@ -159,4 +161,31 @@ describe("domain services", () => { expect(mention.target_id).toBe("Board"); connection.close(); }); + + test("creates a read-only migration report from a legacy database", () => { + const connection = createConnection(); + const organization = new OrganizationService(connection.db); + const migrations = new MigrationService(connection.db); + const snapshot = new SnapshotService(connection.db); + const legacyPath = join(tmpdir(), `agentcompany-legacy-${randomUUID()}.db`); + const legacy = new DatabaseSync(legacyPath); + legacy.exec("create table agents(id text primary key); insert into agents(id) values ('agent-1');"); + legacy.close(); + let orgId = ""; + + connection.transaction(() => { + orgId = organization.createOrganization({ + organizationName: "Acme Operations", + employeeName: "Dana Lee", + mission: "Coordinate agents with auditable approvals.", + goal: "Ship a verified control plane.", + }); + migrations.createReport({ orgId, sourceDatabasePath: legacyPath }); + }); + + const state = snapshot.load(); + expect(state.migrationReports).toHaveLength(1); + expect(state.migrationReports[0]?.reportJson).toContain("\"agents\":1"); + connection.close(); + }); });