diff --git a/AgentCompany-New/src/main/database/migrations/029-file-handoff-records.ts b/AgentCompany-New/src/main/database/migrations/029-file-handoff-records.ts new file mode 100644 index 00000000..adc13392 --- /dev/null +++ b/AgentCompany-New/src/main/database/migrations/029-file-handoff-records.ts @@ -0,0 +1,65 @@ +export const fileHandoffRecordsSchema = ` + create table if not exists file_handoff_records ( + id text primary key, + org_id text not null references organizations(id) on delete cascade, + message_id text not null references chat_messages(id) on delete cascade, + file_id text not null references file_objects(id) on delete cascade, + source_principal_type text not null, + source_principal_id text, + target_principal_type text not null, + target_principal_id text not null, + transfer_type text not null, + checksum text not null, + scan_state text not null, + scan_labels_json text not null, + policy_decision text not null, + object_ref text not null, + created_at text not null, + unique(message_id, file_id, target_principal_type, target_principal_id) + ); + + insert into file_handoff_records( + id, org_id, message_id, file_id, source_principal_type, source_principal_id, + target_principal_type, target_principal_id, transfer_type, checksum, scan_state, + scan_labels_json, policy_decision, object_ref, created_at + ) + select + 'fhr_' || lower(hex(randomblob(16))), + messages.org_id, + attachments.message_id, + attachments.file_id, + messages.sender_type, + messages.sender_id, + coalesce(grants.granted_to_type, 'org_group'), + coalesce(grants.granted_to_id, messages.org_id || ':workspace'), + attachments.transfer_type, + attachments.checksum, + files.scan_state, + files.scan_labels_json, + case + when files.scan_state = 'approved' then 'approved_for_handoff' + else 'blocked_by_scan' + end, + files.storage_path, + messages.created_at + from chat_attachments attachments + join chat_messages messages on messages.id = attachments.message_id + join file_objects files on files.id = attachments.file_id + left join file_access_grants grants + on grants.file_id = attachments.file_id + and grants.source_message_id = attachments.message_id + where not exists ( + select 1 from file_handoff_records records + where records.message_id = attachments.message_id + and records.file_id = attachments.file_id + and records.target_principal_type = coalesce(grants.granted_to_type, 'org_group') + and records.target_principal_id = coalesce(grants.granted_to_id, messages.org_id || ':workspace') + ); + + create index if not exists idx_file_handoff_org_created on file_handoff_records(org_id, created_at); + create index if not exists idx_file_handoff_file on file_handoff_records(org_id, file_id); + create index if not exists idx_file_handoff_target on file_handoff_records(org_id, target_principal_type, target_principal_id); + + insert into schema_migrations(version, name, applied_at) + values (29, 'file_handoff_records', datetime('now')); +`; diff --git a/AgentCompany-New/src/main/database/schema.ts b/AgentCompany-New/src/main/database/schema.ts index 74f32fb4..ff2c2f9f 100644 --- a/AgentCompany-New/src/main/database/schema.ts +++ b/AgentCompany-New/src/main/database/schema.ts @@ -27,6 +27,7 @@ import { chatMessageDeliverySchema } from "./migrations/025-chat-message-deliver import { browserPlatformGovernanceSchema } from "./migrations/026-browser-platform-governance"; import { socialAuditRecordsSchema } from "./migrations/027-social-audit-records"; import { migrationCutoverJournalSchema } from "./migrations/028-migration-cutover-journal"; +import { fileHandoffRecordsSchema } from "./migrations/029-file-handoff-records"; interface Migration { version: number; @@ -63,6 +64,7 @@ const MIGRATIONS: Migration[] = [ { version: 26, name: "browser_platform_governance", sql: browserPlatformGovernanceSchema }, { version: 27, name: "social_audit_records", sql: socialAuditRecordsSchema }, { version: 28, name: "migration_cutover_journal", sql: migrationCutoverJournalSchema }, + { version: 29, name: "file_handoff_records", sql: fileHandoffRecordsSchema }, ]; const SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1].version; diff --git a/AgentCompany-New/src/main/services/chat-service.ts b/AgentCompany-New/src/main/services/chat-service.ts index c40aa0c0..5abd1ca5 100644 --- a/AgentCompany-New/src/main/services/chat-service.ts +++ b/AgentCompany-New/src/main/services/chat-service.ts @@ -4,27 +4,26 @@ import { CHAT_THREAD_BOARD_STATE, FILE_SCAN_STATE, RISK_LEVEL, + type FileScanState, } from "../../shared/domain"; import { createId } from "../../shared/ids"; import { ApprovalService } from "./approval-service"; import { AuditService } from "./audit-service"; import { BoardInterventionService, parseBoardCommand } from "./board-intervention-service"; import { buildChatMessageStructure } from "./chat-message-structure"; -import { recordChatMentions, resolveChatMentions, type MentionTarget, type ResolvedMention } from "./chat-mentions"; +import { recordChatMentions, resolveChatMentions, type ResolvedMention } from "./chat-mentions"; +import { FileHandoffService, type FileHandoffAttachment } from "./file-handoff-service"; const SPACE_TYPES = new Set(["company", "department", "project", "direct"]); const SPACE_VISIBILITIES = new Set(["workspace", "restricted"]); const RETENTION_POLICIES = new Set(["standard_365d", "restricted_365d"]); -interface AttachmentCandidate { - fileId: string; - checksum: string; -} - interface FileAttachmentRow { id: string; sha256: string; - scan_state: string; + scan_state: FileScanState; + scan_labels_json: string; + storage_path: string; owner_type: string; owner_id: string | null; } @@ -52,11 +51,13 @@ export class ChatService { private readonly audit: AuditService; private readonly approvals: ApprovalService; private readonly boardInterventions: BoardInterventionService; + private readonly handoffs: FileHandoffService; constructor(private readonly db: DatabaseSync) { this.audit = new AuditService(db); this.approvals = new ApprovalService(db); this.boardInterventions = new BoardInterventionService(db); + this.handoffs = new FileHandoffService(db); } createSpace(input: CreateChatSpaceInput): string { @@ -141,8 +142,17 @@ export class ChatService { recordChatMentions(this.db, id, mentions); const slashResultRef = slash ? this.executeSlashCommand(input, space, sender, mentions, slash, id, threadId, now) : null; if (slash) this.recordSlashInvocation(id, slash, slashResultRef, now); - this.recordAttachments(id, attachments); - this.recordAttachmentGrants(input, id, threadId, attachments, mentions, now); + this.handoffs.record({ + orgId: input.orgId, + spaceId: input.spaceId, + messageId: id, + threadId, + senderType: input.senderType, + senderId: input.senderId, + attachments, + mentions, + now, + }); if (input.senderId) { const presenceStatus = slash?.command === "escalate" ? "waiting_approval" : "active"; this.db.prepare(` @@ -229,62 +239,6 @@ export class ChatService { `).run(createId("slash"), messageId, slash.command, JSON.stringify({ raw: slash.args }), resultRef, now); } - private recordAttachments(messageId: string, attachments: AttachmentCandidate[]): void { - for (const file of attachments) { - this.db.prepare(` - insert into chat_attachments(message_id, file_id, transfer_type, checksum, preview_key) - values (?, ?, ?, ?, ?) - `).run(messageId, file.fileId, "handoff", file.checksum, null); - } - } - - private recordAttachmentGrants( - input: SendMessageInput, - messageId: string, - threadId: string, - attachments: AttachmentCandidate[], - mentions: ResolvedMention[], - now: string, - ): void { - if (attachments.length === 0) return; - const recipients = this.attachmentGrantRecipients(input.orgId, input.spaceId, mentions); - const statement = this.db.prepare(` - insert or ignore into file_access_grants( - id, org_id, file_id, granted_to_type, granted_to_id, granted_by_type, granted_by_id, - source_message_id, source_thread_id, access_level, created_at - ) - values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `); - for (const attachment of attachments) { - for (const recipient of recipients) { - const result = statement.run( - createId("fgr"), - input.orgId, - attachment.fileId, - recipient.type, - recipient.id, - input.senderType, - input.senderId, - messageId, - threadId, - "read", - now, - ); - if (result.changes > 0) { - this.audit.record({ - orgId: input.orgId, - entityType: "file_object", - entityId: attachment.fileId, - actorType: input.senderType, - actorId: input.senderId, - action: "access_granted", - payload: { messageId, threadId, grantedToType: recipient.type, grantedToId: recipient.id }, - }); - } - } - } - } - private requireSpace(orgId: string, spaceId: string): SpaceRow { const space = this.db.prepare("select id, org_id, visibility, space_type from chat_spaces where id = ? and org_id = ?") .get(spaceId, orgId) as SpaceRow | undefined; @@ -331,18 +285,24 @@ export class ChatService { return thread; } - private requireAttachments(orgId: string, fileIds: string[], sender: PrincipalRef | null): AttachmentCandidate[] { + private requireAttachments(orgId: string, fileIds: string[], sender: PrincipalRef | null): FileHandoffAttachment[] { if (new Set(fileIds).size !== fileIds.length) throw new Error("Attachment file ids must be unique."); return fileIds.map((fileId) => { const file = this.db.prepare(` - select id, sha256, scan_state, owner_type, owner_id + select id, sha256, scan_state, scan_labels_json, storage_path, owner_type, owner_id from file_objects where id = ? and org_id = ? `).get(fileId, orgId) as FileAttachmentRow | undefined; if (!file) throw new Error("Attachment file was not found."); if (file.scan_state !== FILE_SCAN_STATE.APPROVED) throw new Error("Attachment file is not approved for handoff."); if (!this.senderCanReadFile(orgId, file, sender)) throw new Error("Attachment file access is required."); - return { fileId, checksum: file.sha256 }; + return { + fileId, + checksum: file.sha256, + scanState: file.scan_state, + scanLabelsJson: file.scan_labels_json, + objectRef: file.storage_path, + }; }); } @@ -364,35 +324,6 @@ export class ChatService { return Boolean(grant); } - private attachmentGrantRecipients(orgId: string, spaceId: string, mentions: ResolvedMention[]): MentionTarget[] { - const source = mentions.length > 0 ? mentions.map((mention) => mention.target) : this.spaceRecipients(spaceId); - const seen = new Set(); - const recipients: MentionTarget[] = []; - for (const target of source) { - const key = `${target.type}:${target.id}`; - if (seen.has(key)) continue; - seen.add(key); - recipients.push(target); - } - if (recipients.length === 0) throw new Error("Attachment grant recipient is required."); - return recipients; - } - - private spaceRecipients(spaceId: string): MentionTarget[] { - const rows = this.db.prepare(` - select principal_type, principal_id - from chat_memberships - where space_id = ? - order by principal_type asc, principal_id asc - `).all(spaceId) as Array<{ principal_type: string; principal_id: string }>; - return rows.map((row) => { - if (row.principal_type === "employee" || row.principal_type === "agent") { - return { type: row.principal_type, id: row.principal_id }; - } - throw new Error("Chat membership principal type is unsupported."); - }); - } - private requireMembership(spaceId: string, principal: PrincipalRef): void { const row = this.db.prepare("select space_id from chat_memberships where space_id = ? and principal_type = ? and principal_id = ?") .get(spaceId, principal.type, principal.id) as { space_id: string } | undefined; diff --git a/AgentCompany-New/src/main/services/file-handoff-service.ts b/AgentCompany-New/src/main/services/file-handoff-service.ts new file mode 100644 index 00000000..63242e8a --- /dev/null +++ b/AgentCompany-New/src/main/services/file-handoff-service.ts @@ -0,0 +1,149 @@ +import type { DatabaseSync } from "node:sqlite"; +import type { SendMessageInput } from "../../shared/commands"; +import type { FileScanState } from "../../shared/domain"; +import { createId } from "../../shared/ids"; +import { AuditService } from "./audit-service"; +import type { MentionTarget, ResolvedMention } from "./chat-mentions"; + +export interface FileHandoffAttachment { + fileId: string; + checksum: string; + scanState: FileScanState; + scanLabelsJson: string; + objectRef: string; +} + +interface RecordFileHandoffsInput { + orgId: string; + spaceId: string; + messageId: string; + threadId: string; + senderType: SendMessageInput["senderType"]; + senderId: string | null; + attachments: FileHandoffAttachment[]; + mentions: ResolvedMention[]; + now: string; +} + +export class FileHandoffService { + private readonly audit: AuditService; + + constructor(private readonly db: DatabaseSync) { + this.audit = new AuditService(db); + } + + record(input: RecordFileHandoffsInput): void { + if (input.attachments.length === 0) return; + const recipients = this.attachmentGrantRecipients(input.orgId, input.spaceId, input.mentions); + const attachmentStatement = this.db.prepare(` + insert into chat_attachments(message_id, file_id, transfer_type, checksum, preview_key) + values (?, ?, ?, ?, ?) + `); + const grantStatement = this.db.prepare(` + insert or ignore into file_access_grants( + id, org_id, file_id, granted_to_type, granted_to_id, granted_by_type, granted_by_id, + source_message_id, source_thread_id, access_level, created_at + ) + values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + const handoffStatement = this.db.prepare(` + insert or ignore into file_handoff_records( + id, org_id, message_id, file_id, source_principal_type, source_principal_id, + target_principal_type, target_principal_id, transfer_type, checksum, scan_state, + scan_labels_json, policy_decision, object_ref, created_at + ) + values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + for (const attachment of input.attachments) { + attachmentStatement.run(input.messageId, attachment.fileId, "handoff", attachment.checksum, null); + for (const recipient of recipients) { + const grant = grantStatement.run( + createId("fgr"), + input.orgId, + attachment.fileId, + recipient.type, + recipient.id, + input.senderType, + input.senderId, + input.messageId, + input.threadId, + "read", + input.now, + ); + handoffStatement.run( + createId("fhr"), + input.orgId, + input.messageId, + attachment.fileId, + input.senderType, + input.senderId, + recipient.type, + recipient.id, + "handoff", + attachment.checksum, + attachment.scanState, + attachment.scanLabelsJson, + "approved_for_handoff", + attachment.objectRef, + input.now, + ); + if (grant.changes > 0) this.recordGrantAudit(input, attachment.fileId, recipient); + } + } + } + + private recordGrantAudit(input: RecordFileHandoffsInput, fileId: string, recipient: MentionTarget): void { + this.audit.record({ + orgId: input.orgId, + entityType: "file_object", + entityId: fileId, + actorType: input.senderType, + actorId: input.senderId, + action: "access_granted", + payload: { + messageId: input.messageId, + threadId: input.threadId, + grantedToType: recipient.type, + grantedToId: recipient.id, + }, + }); + } + + private attachmentGrantRecipients(orgId: string, spaceId: string, mentions: ResolvedMention[]): MentionTarget[] { + const source = mentions.length > 0 ? mentions.map((mention) => mention.target) : this.spaceRecipients(spaceId); + const seen = new Set(); + const recipients: MentionTarget[] = []; + for (const target of source) { + const key = `${target.type}:${target.id}`; + if (seen.has(key)) continue; + seen.add(key); + recipients.push(target); + } + if (recipients.length === 0) throw new Error("Attachment grant recipient is required."); + return recipients.map((recipient) => recipient.type === "org_group" ? recipient : this.requireRecipient(orgId, recipient)); + } + + private requireRecipient(orgId: string, recipient: MentionTarget): MentionTarget { + if (recipient.type === "board") return recipient; + const table = recipient.type === "employee" ? "employees" : "agents"; + const row = this.db.prepare(`select id from ${table} where id = ? and org_id = ?`) + .get(recipient.id, orgId) as { id: string } | undefined; + if (!row) throw new Error("Attachment grant recipient was not found."); + return recipient; + } + + private spaceRecipients(spaceId: string): MentionTarget[] { + const rows = this.db.prepare(` + select principal_type, principal_id + from chat_memberships + where space_id = ? + order by principal_type asc, principal_id asc + `).all(spaceId) as Array<{ principal_type: string; principal_id: string }>; + return rows.map((row) => { + if (row.principal_type === "employee" || row.principal_type === "agent") { + return { type: row.principal_type, id: row.principal_id }; + } + throw new Error("Chat membership principal type is unsupported."); + }); + } +} diff --git a/AgentCompany-New/src/main/services/migration-service.ts b/AgentCompany-New/src/main/services/migration-service.ts index 6b222f9b..f32c29ac 100644 --- a/AgentCompany-New/src/main/services/migration-service.ts +++ b/AgentCompany-New/src/main/services/migration-service.ts @@ -63,6 +63,7 @@ const LEGACY_TABLES = [ "skill_catalog_tags", "file_objects", "file_access_grants", + "file_handoff_records", "social_accounts", "social_account_grants", "browser_session_containers", @@ -217,6 +218,9 @@ const RELATION_CHECKS: RelationCheck[] = [ { table: "file_access_grants", column: "file_id", referencedTable: "file_objects", referencedColumn: "id", reason: "dangling_file_id" }, { table: "file_access_grants", column: "source_message_id", referencedTable: "chat_messages", referencedColumn: "id", reason: "dangling_source_message_id" }, { table: "file_access_grants", column: "source_thread_id", referencedTable: "chat_threads", referencedColumn: "id", reason: "dangling_source_thread_id" }, + { table: "file_handoff_records", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" }, + { table: "file_handoff_records", column: "message_id", referencedTable: "chat_messages", referencedColumn: "id", reason: "dangling_message_id" }, + { table: "file_handoff_records", column: "file_id", referencedTable: "file_objects", referencedColumn: "id", reason: "dangling_file_id" }, { table: "social_accounts", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" }, { table: "social_accounts", column: "authorized_employee_id", referencedTable: "employees", referencedColumn: "id", reason: "dangling_authorized_employee_id" }, { table: "social_account_grants", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" }, diff --git a/AgentCompany-New/src/main/services/sdlc-service.ts b/AgentCompany-New/src/main/services/sdlc-service.ts index bdb15397..15dfcd64 100644 --- a/AgentCompany-New/src/main/services/sdlc-service.ts +++ b/AgentCompany-New/src/main/services/sdlc-service.ts @@ -262,7 +262,7 @@ export class SdlcService { { key: "provider_adapter_coverage", passed: missing.length === 0, value: `${adapterKeys.length}/${catalogKeys.length}` }, { key: "provider_rate_limit_registry", passed: rateLimitRegistry === 1, value: rateLimitRegistry }, { key: "provider_runtime_contract", passed: runtimeContractReady, value: runtimeContractReady }, - { key: "schema_version", passed: schemaVersion >= 28, value: schemaVersion }, + { key: "schema_version", passed: schemaVersion >= 29, value: schemaVersion }, ]; } diff --git a/AgentCompany-New/src/main/services/snapshot-service.ts b/AgentCompany-New/src/main/services/snapshot-service.ts index 4d1f477c..5f58ec60 100644 --- a/AgentCompany-New/src/main/services/snapshot-service.ts +++ b/AgentCompany-New/src/main/services/snapshot-service.ts @@ -103,6 +103,7 @@ export class SnapshotService { chatReadStates: list(this.db, "chat_read_state", "last_read_at desc"), files, fileAccessGrants: list(this.db, "file_access_grants", "created_at desc"), + fileHandoffRecords: list(this.db, "file_handoff_records", "created_at desc"), scheduledPosts: list(this.db, "scheduled_posts", "scheduled_for asc"), providers, providerAuthProfiles, diff --git a/AgentCompany-New/src/shared/domain.ts b/AgentCompany-New/src/shared/domain.ts index aa37a648..0cfaab51 100644 --- a/AgentCompany-New/src/shared/domain.ts +++ b/AgentCompany-New/src/shared/domain.ts @@ -287,6 +287,24 @@ export interface FileAccessGrantRecord { createdAt: string; } +export interface FileHandoffRecord { + id: string; + orgId: string; + messageId: string; + fileId: string; + sourcePrincipalType: "employee" | "agent" | "board" | "system"; + sourcePrincipalId: string | null; + targetPrincipalType: "employee" | "agent" | "board" | "org_group"; + targetPrincipalId: string; + transferType: string; + checksum: string; + scanState: FileScanState; + scanLabelsJson: string; + policyDecision: "approved_for_handoff" | "blocked_by_scan"; + objectRef: string; + createdAt: string; +} + export interface ScheduledPostRecord { id: string; orgId: string; @@ -432,6 +450,7 @@ export interface AppSnapshot { chatReadStates: ChatReadStateRecord[]; files: FileObjectRecord[]; fileAccessGrants: FileAccessGrantRecord[]; + fileHandoffRecords: FileHandoffRecord[]; scheduledPosts: ScheduledPostRecord[]; providers: ProviderRecord[]; providerAuthProfiles: ProviderAuthProfileRecord[]; diff --git a/AgentCompany-New/tests/unit/domain-services.test.ts b/AgentCompany-New/tests/unit/domain-services.test.ts index 30e2df2a..0e5237df 100644 --- a/AgentCompany-New/tests/unit/domain-services.test.ts +++ b/AgentCompany-New/tests/unit/domain-services.test.ts @@ -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, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28]); + 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]); connection.close(); }); @@ -208,6 +208,7 @@ describe("domain services", () => { const chat = new ChatService(connection.db); const snapshot = new SnapshotService(connection.db); let orgId = ""; + let fileId = ""; connection.transaction(() => { orgId = organization.createOrganization({ @@ -225,7 +226,7 @@ describe("domain services", () => { }); const employee = connection.db.prepare("select id from employees where org_id = ?") .get(orgId) as { id: string }; - const fileId = createId("file"); + fileId = createId("file"); connection.db.prepare(` insert into file_objects(id, org_id, name, kind, size_bytes, sha256, storage_path, owner_type, owner_id, created_at) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) @@ -249,6 +250,15 @@ describe("domain services", () => { expect(state.chatAttachments).toHaveLength(1); expect(state.files[0]?.scanState).toBe("approved"); expect(state.fileAccessGrants[0]).toMatchObject({ grantedToType: "board", grantedToId: orgId, accessLevel: "read" }); + expect(state.fileHandoffRecords[0]).toMatchObject({ + fileId, + targetPrincipalType: "board", + targetPrincipalId: orgId, + checksum: "8f3a", + scanState: "approved", + policyDecision: "approved_for_handoff", + objectRef: "artifacts/runbook.md", + }); expect(state.chatMentions[0]).toMatchObject({ targetType: "board", targetId: orgId }); expect(state.chatSlashInvocations[0]).toMatchObject({ command: "handoff" }); expect(state.chatPresence).toHaveLength(1); @@ -362,7 +372,20 @@ describe("domain services", () => { const state = snapshot.load(); const benGrant = state.fileAccessGrants.find((grant) => grant.fileId === approvedFileId && grant.grantedToId === benId); if (!benGrant) throw new Error("Agent handoff grant was not created."); + const benHandoff = state.fileHandoffRecords.find((record) => ( + record.fileId === approvedFileId && record.sourcePrincipalId === adaId && record.targetPrincipalId === benId + )); + if (!benHandoff) throw new Error("Agent handoff record was not created."); expect(benGrant).toMatchObject({ grantedToType: "agent", accessLevel: "read" }); + expect(benHandoff).toMatchObject({ + sourcePrincipalType: "agent", + sourcePrincipalId: adaId, + targetPrincipalType: "agent", + checksum: "sha-approved", + objectRef: "artifacts/release-notes.md", + scanState: "approved", + policyDecision: "approved_for_handoff", + }); expect(state.chatMessages).toHaveLength(2); expect(state.auditEvents.some((event) => event.entityType === "file_object" && event.action === "access_granted")).toBe(true); connection.close(); diff --git a/AgentCompany-New/tests/unit/migration-service.test.ts b/AgentCompany-New/tests/unit/migration-service.test.ts index b13a8e8a..9215bd70 100644 --- a/AgentCompany-New/tests/unit/migration-service.test.ts +++ b/AgentCompany-New/tests/unit/migration-service.test.ts @@ -163,6 +163,7 @@ describe("migration service", () => { 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); @@ -218,6 +219,7 @@ describe("migration service", () => { 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'); @@ -283,6 +285,7 @@ describe("migration service", () => { 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); @@ -306,6 +309,7 @@ describe("migration service", () => { 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"]);