From 6e210301db22685eb5b1df132c93e87c1a6e1614 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Thu, 8 Oct 2026 21:53:26 -0400 Subject: [PATCH] Q136: remove rollback records that marked restores nothing performed Co-Authored-By: Claude Opus 5.5 --- src/main/app/ipc-contracts/platform.ts | 3 - src/main/app/ipc-handlers.ts | 5 - .../migrations/099-drop-rollback-records.ts | 10 + src/main/database/schema.ts | 2 + src/main/services/compliance-service.ts | 4 - .../migration-cutover-journal-service.ts | 103 ----- src/main/services/migration-service.ts | 31 +- src/main/services/rollback-service.ts | 369 ------------------ src/main/services/snapshot-service.ts | 5 - src/preload/index.ts | 3 - src/shared/commands/governance.ts | 19 - src/shared/desktop-api.ts | 6 - src/shared/domain.ts | 1 - src/shared/ipc.ts | 3 - src/shared/rollback-domain.ts | 44 --- src/shared/snapshot-domain.ts | 4 - tests/unit/domain-services.test.ts | 8 +- tests/unit/migration-service.test.ts | 23 +- tests/unit/rollback-service.test.ts | 132 ------- ...ts => unverified-controls-removal.test.ts} | 13 + 20 files changed, 31 insertions(+), 757 deletions(-) create mode 100644 src/main/database/migrations/099-drop-rollback-records.ts delete mode 100644 src/main/services/migration-cutover-journal-service.ts delete mode 100644 src/main/services/rollback-service.ts delete mode 100644 src/shared/rollback-domain.ts delete mode 100644 tests/unit/rollback-service.test.ts rename tests/unit/{sdlc-removal.test.ts => unverified-controls-removal.test.ts} (64%) diff --git a/src/main/app/ipc-contracts/platform.ts b/src/main/app/ipc-contracts/platform.ts index 4f1e263f..fd39d098 100644 --- a/src/main/app/ipc-contracts/platform.ts +++ b/src/main/app/ipc-contracts/platform.ts @@ -52,9 +52,6 @@ export const platformContracts = { openFile: z.strictObject({ orgId: id, fileId: id }) satisfies z.ZodType, setFilePinned: z.strictObject({ orgId: id, fileId: id, pinned: z.boolean() }) satisfies z.ZodType, createMigrationReport: z.strictObject({ orgId: id, sourceDatabasePath: z.string() }) satisfies z.ZodType, - createRollbackSnapshot: z.strictObject({ orgId: id, migrationReportId: id, createdByEmployeeId: id }) satisfies z.ZodType, - setRollbackSnapshotActive: z.strictObject({ orgId: id, snapshotId: id, active: z.boolean(), employeeId: id }) satisfies z.ZodType, - resolveMigrationReconciliation: z.strictObject({ orgId: id, reconciliationItemId: id, employeeId: id }) satisfies z.ZodType, setFeatureFlag: z.strictObject({ orgId: id, flagId: id, enabled: z.boolean() }) satisfies z.ZodType, createComplianceExport: z.strictObject({ orgId: id, subjectType: z.enum(["employee", "agent"]), subjectId: id, exportType: z.enum(COMPLIANCE_EXPORT_TYPE), requestedByEmployeeId: id, diff --git a/src/main/app/ipc-handlers.ts b/src/main/app/ipc-handlers.ts index 731e788d..e592962a 100644 --- a/src/main/app/ipc-handlers.ts +++ b/src/main/app/ipc-handlers.ts @@ -19,7 +19,6 @@ import { ComplianceService } from "../services/compliance-service"; import { SearchService } from "../services/search-service"; import { IdentityAccessService } from "../services/identity-access-service"; import { OperationsService } from "../services/operations-service"; -import { RollbackService } from "../services/rollback-service"; import { TaskExecutionService } from "../services/task-execution-service"; import { DocumentService } from "../services/document-service"; import { KnowledgeService } from "../services/knowledge-service"; @@ -76,7 +75,6 @@ export function registerIpcHandlers(deps: IpcDeps): void { const search = new SearchService(deps.connection.db); const identityAccess = new IdentityAccessService(deps.connection.db); const operations = new OperationsService(deps.connection.db); - const rollback = new RollbackService(deps.connection.db); const documents = new DocumentService(deps.connection.db); const knowledge = new KnowledgeService(deps.connection.db); const automation = new AutomationService(deps.connection.db); @@ -370,9 +368,6 @@ export function registerIpcHandlers(deps: IpcDeps): void { }); handle("createMigrationReport", (input) => mutate(() => migrations.createReport(input))); - handle("createRollbackSnapshot", (input) => mutate(() => rollback.createSnapshot(input))); - handle("setRollbackSnapshotActive", (input) => mutate(() => rollback.setActive(input))); - handle("resolveMigrationReconciliation", (input) => mutate(() => rollback.resolveReconciliationItem(input))); handle("setFeatureFlag", (input) => mutate(() => operations.setFeatureFlag(input))); handle("createComplianceExport", (input) => mutate(() => compliance.createExport(input))); handle("createLegalHold", (input) => mutate(() => compliance.createLegalHold(input))); diff --git a/src/main/database/migrations/099-drop-rollback-records.ts b/src/main/database/migrations/099-drop-rollback-records.ts new file mode 100644 index 00000000..b6f50638 --- /dev/null +++ b/src/main/database/migrations/099-drop-rollback-records.ts @@ -0,0 +1,10 @@ +// Rollback snapshots, cutover events and reconciliation items only recorded states; nothing ever restored a database, file or outside action, so reports drop their restore promise too. +export const dropRollbackRecordsSchema = ` + drop table if exists migration_reconciliation_items; + drop table if exists migration_cutover_events; + drop table if exists rollback_snapshots; + update migration_reports set report_json = json_remove(report_json, '$.rollbackPack'); + + insert into schema_migrations(version, name, applied_at) + values (99, 'drop_rollback_records', datetime('now')); +`; diff --git a/src/main/database/schema.ts b/src/main/database/schema.ts index f546931c..cd483d14 100644 --- a/src/main/database/schema.ts +++ b/src/main/database/schema.ts @@ -97,6 +97,7 @@ import { dropPrBotGatesSchema } from "./migrations/095-drop-pr-bot-gates"; import { dropChaosDrillsSchema } from "./migrations/096-drop-chaos-drills"; import { dropDrillOnlySloSchema } from "./migrations/097-drop-drill-only-slo"; import { dropAuditReplaysSchema } from "./migrations/098-drop-audit-replays"; +import { dropRollbackRecordsSchema } from "./migrations/099-drop-rollback-records"; interface Migration { version: number; @@ -203,6 +204,7 @@ const MIGRATIONS: Migration[] = [ { version: 96, name: "drop_chaos_drills", sql: dropChaosDrillsSchema }, { version: 97, name: "drop_drill_only_slo", sql: dropDrillOnlySloSchema }, { version: 98, name: "drop_audit_replays", sql: dropAuditReplaysSchema }, + { version: 99, name: "drop_rollback_records", sql: dropRollbackRecordsSchema }, ]; const SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1].version; diff --git a/src/main/services/compliance-service.ts b/src/main/services/compliance-service.ts index 7bbbfb40..020da137 100644 --- a/src/main/services/compliance-service.ts +++ b/src/main/services/compliance-service.ts @@ -200,10 +200,6 @@ export class ComplianceService { .run(orgId, subjectId); this.db.prepare("update retention_freezes set created_by_employee_id = null where org_id = ? and created_by_employee_id = ?") .run(orgId, subjectId); - this.db.prepare("update rollback_snapshots set created_by_employee_id = null where org_id = ? and created_by_employee_id = ?") - .run(orgId, subjectId); - this.db.prepare("update migration_reconciliation_items set resolved_by_employee_id = null where org_id = ? and resolved_by_employee_id = ?") - .run(orgId, subjectId); this.db.prepare("delete from approval_assignments where org_id = ? and assignee_type = 'employee' and assignee_id = ?") .run(orgId, subjectId); this.db.prepare("delete from role_grants where org_id = ? and principal_type = 'employee' and principal_id = ?") diff --git a/src/main/services/migration-cutover-journal-service.ts b/src/main/services/migration-cutover-journal-service.ts deleted file mode 100644 index 04f5b92d..00000000 --- a/src/main/services/migration-cutover-journal-service.ts +++ /dev/null @@ -1,103 +0,0 @@ -import type { DatabaseSync } from "node:sqlite"; -import { createId } from "../../shared/ids"; - -const REPLAYABLE_ENTITY_TYPES = new Set([ - "agent", - "approval", - "browser_action", - "chat_message", - "chat_space", - "file", - "scheduled_post", - "social_account", - "task", - "workspace", -]); - -export interface CutoverDeltaEvent { - id: string; - entityType: string; - entityId: string; - actorType: string; - actorId: string | null; - action: string; - payload: Record; - createdAt: string; -} - -export interface RecordCutoverEventInput { - orgId: string; - migrationReportId: string; - rollbackSnapshotId: string | null; - eventType: string; - actorType: string; - actorId: string | null; - payload: Record; - createdAt?: string; -} - -export class MigrationCutoverJournalService { - constructor(private readonly db: DatabaseSync) {} - - recordEvent(input: RecordCutoverEventInput): string { - const id = createId("mce"); - this.db.prepare(` - insert into migration_cutover_events( - id, org_id, migration_report_id, rollback_snapshot_id, event_type, - actor_type, actor_id, payload_json, created_at - ) values (?, ?, ?, ?, ?, ?, ?, ?, ?) - `).run( - id, - input.orgId, - input.migrationReportId, - input.rollbackSnapshotId, - input.eventType, - input.actorType, - input.actorId, - JSON.stringify(input.payload), - input.createdAt ?? new Date().toISOString(), - ); - return id; - } - - queueReconciliation(input: { - orgId: string; - migrationReportId: string; - rollbackSnapshotId: string; - events: CutoverDeltaEvent[]; - createdAt?: string; - }): number { - let queued = 0; - const createdAt = input.createdAt ?? new Date().toISOString(); - const statement = this.db.prepare(` - insert or ignore into migration_reconciliation_items( - id, org_id, migration_report_id, rollback_snapshot_id, source_event_id, - target_type, target_id, reason, state, payload_json, created_at, resolved_at, resolved_by_employee_id - ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, null, null) - `); - for (const event of input.events) { - if (!REPLAYABLE_ENTITY_TYPES.has(event.entityType)) continue; - const result = statement.run( - createId("mri"), - input.orgId, - input.migrationReportId, - input.rollbackSnapshotId, - event.id, - event.entityType, - event.entityId, - "manual_delta_replay", - "open", - JSON.stringify({ - action: event.action, - actorType: event.actorType, - actorId: event.actorId, - payload: event.payload, - createdAt: event.createdAt, - }), - createdAt, - ); - queued += Number(result.changes); - } - return queued; - } -} diff --git a/src/main/services/migration-service.ts b/src/main/services/migration-service.ts index df873007..e075abd7 100644 --- a/src/main/services/migration-service.ts +++ b/src/main/services/migration-service.ts @@ -70,9 +70,6 @@ const LEGACY_TABLES = [ "legal_holds", "retention_freezes", "search_documents", - "rollback_snapshots", - "migration_cutover_events", - "migration_reconciliation_items", "audit_events", ]; @@ -98,14 +95,6 @@ interface MigrationReportBody { missingTables: string[]; failures: MigrationFailure[]; validationSummary: MigrationValidationSummary; - rollbackPack: { - sourceDatabasePath: string; - capturedAt: string; - restoreStrategy: string; - sourceFingerprint: string; - tableChecksums: Record; - idMaps: Record; - }; } const RELATION_CHECKS: RelationCheck[] = [ @@ -238,16 +227,6 @@ const RELATION_CHECKS: RelationCheck[] = [ { table: "legal_holds", column: "created_by_employee_id", referencedTable: "employees", referencedColumn: "id", reason: "dangling_creator_employee_id" }, { table: "retention_freezes", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" }, { table: "retention_freezes", column: "created_by_employee_id", referencedTable: "employees", referencedColumn: "id", reason: "dangling_creator_employee_id" }, - { table: "rollback_snapshots", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" }, - { table: "rollback_snapshots", column: "migration_report_id", referencedTable: "migration_reports", referencedColumn: "id", reason: "dangling_migration_report_id" }, - { table: "rollback_snapshots", column: "created_by_employee_id", referencedTable: "employees", referencedColumn: "id", reason: "dangling_creator_employee_id" }, - { table: "migration_cutover_events", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" }, - { table: "migration_cutover_events", column: "migration_report_id", referencedTable: "migration_reports", referencedColumn: "id", reason: "dangling_migration_report_id" }, - { table: "migration_cutover_events", column: "rollback_snapshot_id", referencedTable: "rollback_snapshots", referencedColumn: "id", reason: "dangling_rollback_snapshot_id" }, - { table: "migration_reconciliation_items", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" }, - { table: "migration_reconciliation_items", column: "migration_report_id", referencedTable: "migration_reports", referencedColumn: "id", reason: "dangling_migration_report_id" }, - { table: "migration_reconciliation_items", column: "rollback_snapshot_id", referencedTable: "rollback_snapshots", referencedColumn: "id", reason: "dangling_rollback_snapshot_id" }, - { table: "migration_reconciliation_items", column: "resolved_by_employee_id", referencedTable: "employees", referencedColumn: "id", reason: "dangling_resolver_employee_id" }, { table: "audit_events", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" }, ]; @@ -314,14 +293,6 @@ export class MigrationService { missingTables, failures, validationSummary, - rollbackPack: { - sourceDatabasePath, - capturedAt: now, - restoreStrategy: "legacy_snapshot_restore", - sourceFingerprint, - tableChecksums, - idMaps, - }, }; this.db.prepare(` insert into migration_reports(id, org_id, source_ref, report_json, created_at) @@ -446,7 +417,7 @@ function quoteIdentifier(identifier: string): string { return `"${identifier.replaceAll('"', '""')}"`; } -function fingerprintSource(body: Omit): string { +function fingerprintSource(body: Omit): string { return createHash("sha256").update(JSON.stringify(body)).digest("hex"); } diff --git a/src/main/services/rollback-service.ts b/src/main/services/rollback-service.ts deleted file mode 100644 index 944f4afa..00000000 --- a/src/main/services/rollback-service.ts +++ /dev/null @@ -1,369 +0,0 @@ -import { createHash } from "node:crypto"; -import { closeSync, existsSync, openSync, readSync, realpathSync } from "node:fs"; -import type { DatabaseSync } from "node:sqlite"; -import type { CreateRollbackSnapshotInput, ResolveMigrationReconciliationInput, SetRollbackSnapshotActiveInput } from "../../shared/commands"; -import { createId } from "../../shared/ids"; -import { AuditService } from "./audit-service"; -import { MigrationCutoverJournalService, type CutoverDeltaEvent } from "./migration-cutover-journal-service"; - -interface MigrationReportRow { - id: string; - source_ref: string; - report_json: string; - created_at: string; -} - -interface MigrationReportBody { - sourceFingerprint: string; - rollbackPack: { - sourceDatabasePath: string; - restoreStrategy: string; - sourceFingerprint: string; - }; -} - -interface WorkspaceRow { - id: string; - name: string; - local_path: string; - repo_url: string; - repo_ref: string; - is_primary: number; - updated_at: string; -} - -interface RollbackSnapshotRow { - id: string; - migration_report_id: string; - source_fingerprint: string; - delta_journal_json: string; -} - -interface MigrationReconciliationItemRow { - id: string; - migration_report_id: string; - rollback_snapshot_id: string | null; - target_type: string; - target_id: string; - state: string; -} - -interface FileRow { - id: string; - name: string; - size_bytes: number; - sha256: string; - storage_path: string; - scan_state: string; - updated_at: string | null; -} - -interface AuditRow { - id: string; - entity_type: string; - entity_id: string; - actor_type: string; - actor_id: string | null; - action: string; - payload_json: string; - created_at: string; -} - -export class RollbackService { - private readonly audit: AuditService; - private readonly cutoverJournal: MigrationCutoverJournalService; - - constructor(private readonly db: DatabaseSync) { - this.audit = new AuditService(db); - this.cutoverJournal = new MigrationCutoverJournalService(db); - } - - createSnapshot(input: CreateRollbackSnapshotInput): string { - this.requireEmployee(input.orgId, input.createdByEmployeeId); - const report = this.requireReport(input.orgId, input.migrationReportId); - const body = parseMigrationReport(report.report_json); - const sourceDatabasePath = realSourcePath(body.rollbackPack.sourceDatabasePath); - if (sourceDatabasePath !== realSourcePath(report.source_ref)) throw new Error("Rollback source path does not match the migration report."); - if (body.sourceFingerprint !== body.rollbackPack.sourceFingerprint) throw new Error("Rollback pack fingerprint is invalid."); - const sourceDatabaseSha256 = fileSha256(sourceDatabasePath); - const workspaceManifestJson = JSON.stringify(this.workspaceManifest(input.orgId)); - const workspaceManifestSha256 = sha256(workspaceManifestJson); - const deltaJournalJson = JSON.stringify(this.deltaJournal(input.orgId, report.created_at)); - const restorePlanJson = JSON.stringify({ - migrationReportId: report.id, - sourceDatabasePath, - sourceFingerprint: body.sourceFingerprint, - sourceDatabaseSha256, - workspaceManifestSha256, - restoreStrategy: body.rollbackPack.restoreStrategy, - }); - const id = createId("rbs"); - const now = new Date().toISOString(); - this.db.prepare(` - insert into rollback_snapshots( - id, org_id, migration_report_id, source_database_path, source_fingerprint, - source_database_sha256, workspace_manifest_json, workspace_manifest_sha256, - delta_journal_json, restore_plan_json, active, created_by_employee_id, created_at, activated_at - ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, null) - `).run( - id, - input.orgId, - report.id, - sourceDatabasePath, - body.sourceFingerprint, - sourceDatabaseSha256, - workspaceManifestJson, - workspaceManifestSha256, - deltaJournalJson, - restorePlanJson, - input.createdByEmployeeId, - now, - ); - this.audit.record({ - orgId: input.orgId, - entityType: "rollback_snapshot", - entityId: id, - actorType: "employee", - actorId: input.createdByEmployeeId, - action: "created", - payload: { migrationReportId: report.id, sourceFingerprint: body.sourceFingerprint }, - }); - this.cutoverJournal.recordEvent({ - orgId: input.orgId, - migrationReportId: report.id, - rollbackSnapshotId: id, - eventType: "rollback_snapshot_created", - actorType: "employee", - actorId: input.createdByEmployeeId, - payload: { sourceFingerprint: body.sourceFingerprint, sourceDatabaseSha256, workspaceManifestSha256 }, - createdAt: now, - }); - return id; - } - - setActive(input: SetRollbackSnapshotActiveInput): void { - this.requireEmployee(input.orgId, input.employeeId); - const snapshot = this.db.prepare(` - select id, migration_report_id, source_fingerprint, delta_journal_json - from rollback_snapshots - where id = ? and org_id = ? - `).get(input.snapshotId, input.orgId) as RollbackSnapshotRow | undefined; - if (!snapshot) throw new Error("Rollback snapshot was not found."); - const now = new Date().toISOString(); - if (input.active) this.db.prepare("update rollback_snapshots set active = 0, activated_at = null where org_id = ?").run(input.orgId); - const result = this.db.prepare("update rollback_snapshots set active = ?, activated_at = ? where id = ? and org_id = ?") - .run(input.active ? 1 : 0, input.active ? now : null, input.snapshotId, input.orgId); - if (result.changes === 0) throw new Error("Rollback snapshot was not found."); - this.audit.record({ - orgId: input.orgId, - entityType: "rollback_snapshot", - entityId: input.snapshotId, - actorType: "employee", - actorId: input.employeeId, - action: input.active ? "activated" : "deactivated", - payload: { active: input.active }, - }); - const queuedReconciliationItems = input.active - ? this.cutoverJournal.queueReconciliation({ - orgId: input.orgId, - migrationReportId: snapshot.migration_report_id, - rollbackSnapshotId: snapshot.id, - events: parseDeltaJournal(snapshot.delta_journal_json), - createdAt: now, - }) - : 0; - this.cutoverJournal.recordEvent({ - orgId: input.orgId, - migrationReportId: snapshot.migration_report_id, - rollbackSnapshotId: snapshot.id, - eventType: input.active ? "rollback_snapshot_activated" : "rollback_snapshot_deactivated", - actorType: "employee", - actorId: input.employeeId, - payload: { active: input.active, sourceFingerprint: snapshot.source_fingerprint, queuedReconciliationItems }, - createdAt: now, - }); - } - - resolveReconciliationItem(input: ResolveMigrationReconciliationInput): void { - this.requireEmployee(input.orgId, input.employeeId); - const item = this.db.prepare(` - select id, migration_report_id, rollback_snapshot_id, target_type, target_id, state - from migration_reconciliation_items - where id = ? and org_id = ? - `).get(input.reconciliationItemId, input.orgId) as MigrationReconciliationItemRow | undefined; - if (!item) throw new Error("Migration reconciliation item was not found."); - if (item.state === "resolved") throw new Error("Migration reconciliation item is already resolved."); - const now = new Date().toISOString(); - const result = this.db.prepare(` - update migration_reconciliation_items - set state = ?, resolved_at = ?, resolved_by_employee_id = ? - where id = ? and org_id = ? and state = ? - `).run("resolved", now, input.employeeId, item.id, input.orgId, "open"); - if (result.changes === 0) throw new Error("Migration reconciliation item could not be resolved."); - this.audit.record({ - orgId: input.orgId, - entityType: "migration_reconciliation_item", - entityId: item.id, - actorType: "employee", - actorId: input.employeeId, - action: "resolved", - payload: { targetType: item.target_type, targetId: item.target_id, rollbackSnapshotId: item.rollback_snapshot_id }, - }); - this.cutoverJournal.recordEvent({ - orgId: input.orgId, - migrationReportId: item.migration_report_id, - rollbackSnapshotId: item.rollback_snapshot_id, - eventType: "migration_reconciliation_resolved", - actorType: "employee", - actorId: input.employeeId, - payload: { reconciliationItemId: item.id, targetType: item.target_type, targetId: item.target_id }, - createdAt: now, - }); - } - - private requireReport(orgId: string, reportId: string): MigrationReportRow { - const report = this.db.prepare("select id, source_ref, report_json, created_at from migration_reports where id = ? and org_id = ?") - .get(reportId, orgId) as MigrationReportRow | undefined; - if (!report) throw new Error("Migration report was not found."); - return report; - } - - private requireEmployee(orgId: string, employeeId: string): void { - const employee = this.db.prepare("select id from employees where id = ? and org_id = ?").get(employeeId, orgId); - if (!employee) throw new Error("Rollback employee was not found."); - } - - private workspaceManifest(orgId: string): { workspaces: unknown[]; files: unknown[] } { - const workspaces = this.db.prepare(` - select id, name, local_path, repo_url, repo_ref, is_primary, updated_at - from workspaces - where org_id = ? - order by id asc - `).all(orgId) as unknown as WorkspaceRow[]; - const files = this.db.prepare(` - select id, name, size_bytes, sha256, storage_path, scan_state, updated_at - from file_objects - where org_id = ? - order by id asc - `).all(orgId) as unknown as FileRow[]; - return { - workspaces: workspaces.map((row) => ({ - id: row.id, - name: row.name, - localPath: row.local_path, - repoUrl: row.repo_url, - repoRef: row.repo_ref, - isPrimary: row.is_primary === 1, - updatedAt: row.updated_at, - })), - files: files.map((row) => ({ - id: row.id, - name: row.name, - sizeBytes: row.size_bytes, - sha256: row.sha256, - storagePath: row.storage_path, - scanState: row.scan_state, - updatedAt: row.updated_at, - })), - }; - } - - private deltaJournal(orgId: string, since: string): unknown[] { - const rows = this.db.prepare(` - select id, entity_type, entity_id, actor_type, actor_id, action, payload_json, created_at - from audit_events - where org_id = ? and created_at >= ? - order by created_at asc, id asc - `).all(orgId, since) as unknown as AuditRow[]; - return rows.map((row) => ({ - id: row.id, - entityType: row.entity_type, - entityId: row.entity_id, - actorType: row.actor_type, - actorId: row.actor_id, - action: row.action, - payload: parseRecord(row.payload_json), - createdAt: row.created_at, - })); - } -} - -function parseMigrationReport(reportJson: string): MigrationReportBody { - const parsed = parseRecord(reportJson); - const rollbackPack = parsed.rollbackPack; - if (typeof parsed.sourceFingerprint !== "string") throw new Error("Migration report is missing a source fingerprint."); - if (typeof rollbackPack !== "object" || rollbackPack === null || Array.isArray(rollbackPack)) throw new Error("Migration report is missing a rollback pack."); - const pack = rollbackPack as Record; - if (typeof pack.sourceDatabasePath !== "string" || typeof pack.restoreStrategy !== "string" || typeof pack.sourceFingerprint !== "string") { - throw new Error("Migration rollback pack is invalid."); - } - return { - sourceFingerprint: parsed.sourceFingerprint, - rollbackPack: { - sourceDatabasePath: pack.sourceDatabasePath, - restoreStrategy: pack.restoreStrategy, - sourceFingerprint: pack.sourceFingerprint, - }, - }; -} - -function realSourcePath(path: string): string { - if (!existsSync(path)) throw new Error("Rollback source database was not found."); - return realpathSync.native(path); -} - -function fileSha256(path: string): string { - const fd = openSync(path, "r"); - try { - const hash = createHash("sha256"); - const buffer = Buffer.allocUnsafe(1024 * 1024); - for (;;) { - const bytes = readSync(fd, buffer, 0, buffer.length, null); - if (bytes === 0) return hash.digest("hex"); - hash.update(buffer.subarray(0, bytes)); - } - } finally { - closeSync(fd); - } -} - -function sha256(value: string): string { - return createHash("sha256").update(value).digest("hex"); -} - -function parseRecord(json: string): Record { - const parsed = JSON.parse(json) as unknown; - if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("JSON record is invalid."); - return parsed as Record; -} - -function parseDeltaJournal(json: string): CutoverDeltaEvent[] { - const parsed = JSON.parse(json) as unknown; - if (!Array.isArray(parsed)) throw new Error("Rollback delta journal is invalid."); - return parsed.map((item) => { - if (typeof item !== "object" || item === null || Array.isArray(item)) throw new Error("Rollback delta journal entry is invalid."); - const record = item as Record; - if ( - typeof record.id !== "string" - || typeof record.entityType !== "string" - || typeof record.entityId !== "string" - || typeof record.actorType !== "string" - || typeof record.action !== "string" - || typeof record.createdAt !== "string" - ) { - throw new Error("Rollback delta journal entry is invalid."); - } - const payload = record.payload; - if (typeof payload !== "object" || payload === null || Array.isArray(payload)) throw new Error("Rollback delta journal entry is invalid."); - return { - id: record.id, - entityType: record.entityType, - entityId: record.entityId, - actorType: record.actorType, - actorId: typeof record.actorId === "string" ? record.actorId : null, - action: record.action, - payload: payload as Record, - createdAt: record.createdAt, - }; - }); -} diff --git a/src/main/services/snapshot-service.ts b/src/main/services/snapshot-service.ts index f92042f4..f2a45df4 100644 --- a/src/main/services/snapshot-service.ts +++ b/src/main/services/snapshot-service.ts @@ -79,8 +79,6 @@ export class SnapshotService { .map((flag) => ({ ...flag, enabled: toBool(flag.enabled) })); const operationalEvents = list(this.db, "operational_events", "created_at desc") .map((event) => ({ ...event, success: toBool(event.success) })); - const rollbackSnapshots = list(this.db, "rollback_snapshots", "created_at desc") - .map((snapshot) => ({ ...snapshot, active: toBool(snapshot.active) })); return { activeOrgId, @@ -139,9 +137,6 @@ export class SnapshotService { socialAuditRecords: list(this.db, "social_audit_records", "created_at desc"), policyRules: list(this.db, "policy_rules", "created_at asc"), migrationReports: list(this.db, "migration_reports", "created_at desc"), - rollbackSnapshots, - migrationCutoverEvents: list(this.db, "migration_cutover_events", "created_at desc"), - migrationReconciliationItems: list(this.db, "migration_reconciliation_items", "created_at desc"), complianceExports: list(this.db, "compliance_exports", "created_at desc"), legalHolds, retentionFreezes, diff --git a/src/preload/index.ts b/src/preload/index.ts index 123b95c3..43d4d1fb 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -76,9 +76,6 @@ const api: DesktopApi = { pickDirectory: () => invoke(IPC_CHANNELS.pickDirectory), setFilePinned: (input) => invoke(IPC_CHANNELS.setFilePinned, input), createMigrationReport: (input) => invoke(IPC_CHANNELS.createMigrationReport, input), - createRollbackSnapshot: (input) => invoke(IPC_CHANNELS.createRollbackSnapshot, input), - setRollbackSnapshotActive: (input) => invoke(IPC_CHANNELS.setRollbackSnapshotActive, input), - resolveMigrationReconciliation: (input) => invoke(IPC_CHANNELS.resolveMigrationReconciliation, input), setFeatureFlag: (input) => invoke(IPC_CHANNELS.setFeatureFlag, input), createComplianceExport: (input) => invoke(IPC_CHANNELS.createComplianceExport, input), createLegalHold: (input) => invoke(IPC_CHANNELS.createLegalHold, input), diff --git a/src/shared/commands/governance.ts b/src/shared/commands/governance.ts index 4089c343..2626bf6d 100644 --- a/src/shared/commands/governance.ts +++ b/src/shared/commands/governance.ts @@ -10,25 +10,6 @@ export interface CreateMigrationReportInput { sourceDatabasePath: string; } -export interface CreateRollbackSnapshotInput { - orgId: string; - migrationReportId: string; - createdByEmployeeId: string; -} - -export interface SetRollbackSnapshotActiveInput { - orgId: string; - snapshotId: string; - active: boolean; - employeeId: string; -} - -export interface ResolveMigrationReconciliationInput { - orgId: string; - reconciliationItemId: string; - employeeId: string; -} - export interface CreateComplianceExportInput { orgId: string; subjectType: ComplianceSubjectType; diff --git a/src/shared/desktop-api.ts b/src/shared/desktop-api.ts index b76c56e0..7a40737f 100644 --- a/src/shared/desktop-api.ts +++ b/src/shared/desktop-api.ts @@ -51,7 +51,6 @@ import type { CreateOrganizationInput, CreateProviderResponseInput, CreateRetentionFreezeInput, - CreateRollbackSnapshotInput, CreateScheduledPostInput, CreateSocialAccountInput, CreateTaskInput, @@ -63,7 +62,6 @@ import type { OpenFileInput, GrantRoleInput, LaunchSocialLoginInput, - ResolveMigrationReconciliationInput, RunTaskInput, RevokeProviderCredentialInput, SaveProviderCredentialInput, @@ -81,7 +79,6 @@ import type { SetFilePinnedInput, SetFeatureFlagInput, SetLegalHoldStateInput, - SetRollbackSnapshotActiveInput, SetRetentionFreezeStateInput, SetScheduledPostStateInput, StreamProviderResponseInput, @@ -182,9 +179,6 @@ export interface DesktopApi { pickDirectory(): Promise>; setFilePinned(input: SetFilePinnedInput): Promise>; createMigrationReport(input: CreateMigrationReportInput): Promise>; - createRollbackSnapshot(input: CreateRollbackSnapshotInput): Promise>; - setRollbackSnapshotActive(input: SetRollbackSnapshotActiveInput): Promise>; - resolveMigrationReconciliation(input: ResolveMigrationReconciliationInput): Promise>; setFeatureFlag(input: SetFeatureFlagInput): Promise>; createComplianceExport(input: CreateComplianceExportInput): Promise>; createLegalHold(input: CreateLegalHoldInput): Promise>; diff --git a/src/shared/domain.ts b/src/shared/domain.ts index f6e67812..b2a7dd15 100644 --- a/src/shared/domain.ts +++ b/src/shared/domain.ts @@ -9,7 +9,6 @@ export type { ProviderAuthProfileRecord, ProviderAuthSessionRecord } from "./pro export { PROVIDER_DATA_POLICY, PROVIDER_EXECUTION_MODE, type ProviderDataPolicy, type ProviderExecutionMode } from "./provider-policy-domain"; export { PROVIDER_REQUEST_STATE, type ProviderRequestRecord, type ProviderRequestState, type ProviderStreamRendererEvent } from "./provider-request-domain"; export type { ProviderRateLimitObservationRecord, ProviderRateLimitStateRecord } from "./provider-runtime-domain"; -export type { MigrationCutoverEventRecord, MigrationReconciliationItemRecord, RollbackSnapshotRecord } from "./rollback-domain"; export { SEARCH_ENTITY_TYPES, type SearchEntityType, type SearchResultRecord } from "./search-domain"; export type { BrowserActionPolicyRecord, BrowserActionRecord, BrowserSessionContainerRecord, SocialAccountGrantRecord, SocialAccountRecord, SocialApprovalRouteRecord, SocialAuditRecord } from "./social-domain"; export type { AppSnapshot, UiLanguage, UiPreferences, UiTheme } from "./snapshot-domain"; diff --git a/src/shared/ipc.ts b/src/shared/ipc.ts index 24240088..8b3b93ff 100644 --- a/src/shared/ipc.ts +++ b/src/shared/ipc.ts @@ -49,9 +49,6 @@ export const IPC_CHANNELS = { pickDirectory: "agentcompany:pick-directory", setFilePinned: "agentcompany:set-file-pinned", createMigrationReport: "agentcompany:create-migration-report", - createRollbackSnapshot: "agentcompany:create-rollback-snapshot", - setRollbackSnapshotActive: "agentcompany:set-rollback-snapshot-active", - resolveMigrationReconciliation: "agentcompany:resolve-migration-reconciliation", setFeatureFlag: "agentcompany:set-feature-flag", createComplianceExport: "agentcompany:create-compliance-export", createLegalHold: "agentcompany:create-legal-hold", diff --git a/src/shared/rollback-domain.ts b/src/shared/rollback-domain.ts deleted file mode 100644 index ce582753..00000000 --- a/src/shared/rollback-domain.ts +++ /dev/null @@ -1,44 +0,0 @@ -export interface RollbackSnapshotRecord { - id: string; - orgId: string; - migrationReportId: string; - sourceDatabasePath: string; - sourceFingerprint: string; - sourceDatabaseSha256: string; - workspaceManifestJson: string; - workspaceManifestSha256: string; - deltaJournalJson: string; - restorePlanJson: string; - active: boolean; - createdByEmployeeId: string | null; - createdAt: string; - activatedAt: string | null; -} - -export interface MigrationCutoverEventRecord { - id: string; - orgId: string; - migrationReportId: string; - rollbackSnapshotId: string | null; - eventType: string; - actorType: string; - actorId: string | null; - payloadJson: string; - createdAt: string; -} - -export interface MigrationReconciliationItemRecord { - id: string; - orgId: string; - migrationReportId: string; - rollbackSnapshotId: string | null; - sourceEventId: string; - targetType: string; - targetId: string; - reason: string; - state: string; - payloadJson: string; - createdAt: string; - resolvedAt: string | null; - resolvedByEmployeeId: string | null; -} diff --git a/src/shared/snapshot-domain.ts b/src/shared/snapshot-domain.ts index 0e2ec79e..ec063602 100644 --- a/src/shared/snapshot-domain.ts +++ b/src/shared/snapshot-domain.ts @@ -47,7 +47,6 @@ import type { FeatureFlagRecord, OperationalEventRecord, SloTargetRecord } from import type { ProviderAuthProfileRecord, ProviderAuthSessionRecord } from "./provider-auth-domain"; import type { ProviderRequestRecord } from "./provider-request-domain"; import type { ProviderRateLimitObservationRecord, ProviderRateLimitStateRecord } from "./provider-runtime-domain"; -import type { MigrationCutoverEventRecord, MigrationReconciliationItemRecord, RollbackSnapshotRecord } from "./rollback-domain"; import type { BrowserActionPolicyRecord, BrowserActionRecord, @@ -118,9 +117,6 @@ export interface AppSnapshot { socialAuditRecords: SocialAuditRecord[]; policyRules: PolicyRuleRecord[]; migrationReports: MigrationReportRecord[]; - rollbackSnapshots: RollbackSnapshotRecord[]; - migrationCutoverEvents: MigrationCutoverEventRecord[]; - migrationReconciliationItems: MigrationReconciliationItemRecord[]; complianceExports: ComplianceExportRecord[]; legalHolds: LegalHoldRecord[]; retentionFreezes: RetentionFreezeRecord[]; diff --git a/tests/unit/domain-services.test.ts b/tests/unit/domain-services.test.ts index 821b733f..61d79f34 100644 --- a/tests/unit/domain-services.test.ts +++ b/tests/unit/domain-services.test.ts @@ -1,4 +1,4 @@ -import { mkdirSync, realpathSync, rmSync, writeFileSync } from "node:fs"; +import { mkdirSync, rmSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { createHash, randomUUID } from "node:crypto"; @@ -35,7 +35,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, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98]); + expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99]); connection.close(); }); @@ -592,14 +592,14 @@ describe("domain services", () => { idMaps: Record; failures: Array<{ table: string; reason: string }>; validationSummary: { missingTableCount: number; passed: boolean }; - rollbackPack: { sourceDatabasePath: string }; }; expect(report.tableChecksums.agents).toHaveLength(64); expect(report.idMaps.agents).toEqual(["agent-1"]); expect(report.failures.some((failure) => failure.reason === "missing_table")).toBe(true); expect(report.validationSummary.missingTableCount).toBeGreaterThan(0); expect(report.validationSummary.passed).toBe(false); - expect(report.rollbackPack.sourceDatabasePath).toBe(realpathSync.native(legacyPath)); + // A report validates the source; it promises no restore it cannot perform. + expect(Object.keys(report)).not.toContain("rollbackPack"); connection.close(); }); diff --git a/tests/unit/migration-service.test.ts b/tests/unit/migration-service.test.ts index 995a6eaa..3e979ecc 100644 --- a/tests/unit/migration-service.test.ts +++ b/tests/unit/migration-service.test.ts @@ -45,16 +45,10 @@ interface MigrationReportBody { relationFailureCount: number; passed: boolean; }; - rollbackPack: { - sourceDatabasePath: string; - sourceFingerprint: string; - tableChecksums: Record; - idMaps: Record; - }; } describe("migration service", () => { - test("creates idempotent reports with checksums, id maps, rollback pack, and integrity failures", () => { + test("creates idempotent reports with checksums, id maps, and integrity failures", () => { const connection = createConnection(); const organization = new OrganizationService(connection.db); const migrations = new MigrationService(connection.db); @@ -87,9 +81,6 @@ describe("migration service", () => { 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({ @@ -171,9 +162,6 @@ describe("migration service", () => { 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 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'); @@ -236,9 +224,6 @@ describe("migration service", () => { 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 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 = ""; @@ -279,9 +264,6 @@ describe("migration service", () => { 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.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"]); @@ -292,9 +274,6 @@ describe("migration service", () => { 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.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"]); diff --git a/tests/unit/rollback-service.test.ts b/tests/unit/rollback-service.test.ts deleted file mode 100644 index fd938323..00000000 --- a/tests/unit/rollback-service.test.ts +++ /dev/null @@ -1,132 +0,0 @@ -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 { AuditService } from "../../src/main/services/audit-service"; -import { MigrationService } from "../../src/main/services/migration-service"; -import { OrganizationService } from "../../src/main/services/organization-service"; -import { RollbackService } from "../../src/main/services/rollback-service"; -import { SnapshotService } from "../../src/main/services/snapshot-service"; - -function createConnection(): DatabaseConnection { - return new DatabaseConnection(join(tmpdir(), `agentcompany-new-rollback-${randomUUID()}.db`)); -} - -function createLegacyDatabase(): string { - const legacyPath = join(tmpdir(), `agentcompany-legacy-${randomUUID()}.db`); - const legacy = new DatabaseSync(legacyPath); - legacy.exec(` - create table organizations(id text primary key); - insert into organizations(id) values ('legacy-org'); - `); - legacy.close(); - return legacyPath; -} - -describe("rollback service", () => { - test("creates immutable rollback snapshots from migration reports and workspace manifests", () => { - const connection = createConnection(); - const organization = new OrganizationService(connection.db); - const migrations = new MigrationService(connection.db); - const rollback = new RollbackService(connection.db); - const snapshot = new SnapshotService(connection.db); - const audit = new AuditService(connection.db); - const legacyPath = createLegacyDatabase(); - let orgId = ""; - let employeeId = ""; - let reportId = ""; - let rollbackId = ""; - let reconciliationId = ""; - - 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; - seedWorkspace(connection.db, orgId); - reportId = migrations.createReport({ orgId, sourceDatabasePath: legacyPath }); - audit.record({ - orgId, - entityType: "task", - entityId: "task-cutover", - actorType: "employee", - actorId: employeeId, - action: "created", - payload: { title: "Reconcile release delta" }, - }); - rollbackId = rollback.createSnapshot({ orgId, migrationReportId: reportId, createdByEmployeeId: employeeId }); - rollback.setActive({ orgId, snapshotId: rollbackId, active: true, employeeId }); - reconciliationId = (connection.db.prepare("select id from migration_reconciliation_items where org_id = ? limit 1") - .get(orgId) as { id: string }).id; - rollback.resolveReconciliationItem({ orgId, reconciliationItemId: reconciliationId, employeeId }); - }); - - const state = snapshot.load(); - const record = state.rollbackSnapshots[0]; - if (!record) throw new Error("Rollback snapshot was not created."); - const workspaceManifest = JSON.parse(record.workspaceManifestJson) as { workspaces: unknown[]; files: unknown[] }; - const deltaJournal = JSON.parse(record.deltaJournalJson) as unknown[]; - const restorePlan = JSON.parse(record.restorePlanJson) as { migrationReportId: string; restoreStrategy: string }; - expect(record).toMatchObject({ orgId, migrationReportId: reportId, active: true, createdByEmployeeId: employeeId }); - expect(record.sourceDatabaseSha256).toHaveLength(64); - expect(record.workspaceManifestSha256).toHaveLength(64); - expect(workspaceManifest.workspaces).toHaveLength(1); - expect(workspaceManifest.files).toHaveLength(1); - expect(deltaJournal.length).toBeGreaterThan(0); - expect(restorePlan).toMatchObject({ migrationReportId: reportId, restoreStrategy: "legacy_snapshot_restore" }); - expect(state.migrationCutoverEvents.map((event) => event.eventType).sort()).toEqual([ - "migration_reconciliation_resolved", - "rollback_snapshot_activated", - "rollback_snapshot_created", - ]); - expect(state.migrationReconciliationItems).toContainEqual(expect.objectContaining({ - orgId, - migrationReportId: reportId, - rollbackSnapshotId: rollbackId, - targetType: "task", - targetId: "task-cutover", - reason: "manual_delta_replay", - state: "resolved", - resolvedByEmployeeId: employeeId, - })); - expect(() => connection.transaction(() => rollback.resolveReconciliationItem({ orgId, reconciliationItemId: reconciliationId, employeeId }))) - .toThrow("Migration reconciliation item is already resolved."); - expect(state.auditEvents.some((event) => event.entityType === "rollback_snapshot" && event.action === "activated")).toBe(true); - expect(state.auditEvents.some((event) => event.entityType === "migration_reconciliation_item" && event.action === "resolved")).toBe(true); - connection.close(); - }); -}); - -function seedWorkspace(db: DatabaseSync, orgId: string): void { - const now = new Date().toISOString(); - db.prepare(` - insert into workspaces(id, org_id, name, local_path, repo_url, repo_ref, project_id, is_primary, created_at, updated_at) - values (?, ?, ?, ?, ?, ?, null, 1, ?, ?) - `).run("wsp-rollback", orgId, "Primary", "C:\\repo\\workspace", "https://example.invalid/repo.git", "main", now, now); - db.prepare(` - insert into file_objects( - id, org_id, name, kind, size_bytes, sha256, storage_path, owner_type, owner_id, - created_at, pinned, space_id, updated_at, scan_state, scan_labels_json, scanned_at - ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, null, ?, ?, ?, ?) - `).run( - "file-rollback", - orgId, - "release.txt", - "document", - 12, - "0".repeat(64), - "artifacts/release.txt", - "system", - null, - now, - now, - "approved", - "[]", - now, - ); -} diff --git a/tests/unit/sdlc-removal.test.ts b/tests/unit/unverified-controls-removal.test.ts similarity index 64% rename from tests/unit/sdlc-removal.test.ts rename to tests/unit/unverified-controls-removal.test.ts index be9b7522..4432b605 100644 --- a/tests/unit/sdlc-removal.test.ts +++ b/tests/unit/unverified-controls-removal.test.ts @@ -23,3 +23,16 @@ describe("SDLC controls", () => { connection.close(); }); }); + +describe("rollback records", () => { + test("offer no snapshot, cutover or reconciliation that marks a restore it never performs", () => { + const connection = new DatabaseConnection(join(tmpdir(), `agentcompany-new-rollback-${randomUUID()}.db`)); + const tables = connection.db.prepare(` + select name from sqlite_master + where type = 'table' and name in ('rollback_snapshots', 'migration_cutover_events', 'migration_reconciliation_items') + `).all(); + expect(tables).toEqual([]); + expect(Object.keys(IPC_CHANNELS).filter((name) => ["createRollbackSnapshot", "setRollbackSnapshotActive", "resolveMigrationReconciliation"].includes(name))).toEqual([]); + connection.close(); + }); +});