import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; import type { z } from "zod"; import * as C from "../src/index.ts"; // 与 packages/db/src/types.ts(从迁移反射生成)做文本级对照: // 1) 每张表的列 ⇔ 对应 Row schema 的字段(camelCase),只允许显式列出的差异; // 2) 每个 CHECK 枚举 ⇔ contracts 中对应 z.enum 的取值。 // 只读文件,不引入 @umt/db 依赖。 const typesSrc = readFileSync(new URL("../../db/src/types.ts", import.meta.url), "utf8"); const camel = (s: string) => s.replace(/_([a-z0-9])/g, (_, c: string) => c.toUpperCase()); function tableColumns(): Map { const out = new Map(); for (const m of typesSrc.matchAll(/export interface (\w+)Table \{\n([\s\S]*?)\n\}/g)) { const cols = [...m[2]!.matchAll(/^ {2}(\w+):/gm)].map((x) => x[1]!); out.set(m[1]!, cols); } return out; } function enumAliases(): Map { const out = new Map(); for (const m of typesSrc.matchAll(/export type (\w+) =([^;]+);/g)) { if (m[1] === "Timestamptz" || m[1] === "Json" || m[2]!.includes("<")) continue; out.set( m[1]!, [...m[2]!.matchAll(/"([^"]*)"/g)].map((x) => x[1]!), ); } return out; } type Obj = z.ZodObject; /** 表(PascalCase)→ Row schema 与允许的差异 */ const ROWS: Record = { AcquisitionPlan: { row: C.AcquisitionPlan }, AcquisitionSlot: { row: C.AcquisitionSlot }, AgentRun: { row: C.AgentRun }, AgentRunEvent: { row: C.AgentRunEventRow }, ApiKey: { row: C.ApiKey, dbOnly: ["keyHash"] }, Approval: { row: C.Approval }, Artifact: { row: C.Artifact }, AssetLocation: { row: C.AssetLocation }, AssetOperation: { row: C.AssetOperation }, AssetUnitCoverage: { row: C.AssetUnitCoverage }, AuditEvent: { row: C.AuditEvent }, AutomationGrant: { row: C.AutomationGrant }, ContentUnit: { row: C.ContentUnit }, Decision: { row: C.Decision }, DownloadTask: { row: C.DownloadTask }, DownloaderInstance: { row: C.DownloaderInstance }, ExternalIdentity: { row: C.ExternalIdentity }, FeedSource: { row: C.FeedSource }, IdentityRevision: { row: C.IdentityRevision }, LibraryRoot: { row: C.LibraryRoot }, MediaAsset: { row: C.MediaAsset }, MediaEntity: { row: C.MediaEntity }, MediaServerInstance: { row: C.MediaServerInstance }, Observation: { row: C.Observation }, OrganizationFileResult: { row: C.OrganizationFileResult }, OrganizationRecord: { row: C.OrganizationRecord, dbOnly: ["changeXid"] }, OutboxEvent: { row: C.OutboxEvent }, PathBinding: { row: C.PathBinding }, PathMapping: { row: C.PathMapping }, PolicyRevision: { row: C.PolicyRevision }, PreferenceSuppression: { row: C.PreferenceSuppression }, PreferenceTag: { row: C.PreferenceTag }, ReconcileRun: { row: C.ReconcileRun }, Secret: { row: C.Secret, dbOnly: ["ciphertext", "nonce", "authTag"] }, ServerMapping: { row: C.ServerMapping }, Settings: { row: C.SettingEntry }, Site: { row: C.Site }, SiteAccount: { row: C.SiteAccount }, SourceRelease: { row: C.SourceRelease }, Subscription: { row: C.Subscription }, Task: { row: C.Task }, TitleAlias: { row: C.TitleAlias }, }; /** 引擎自有表(@umt/tasks 内部使用,不经 API 暴露),不要求 Row schema */ const INTERNAL_TABLES = [ "TasksRunMeta", "TasksFact", "TasksOpLedger", // W2-A 登录门禁与 SSE 总线的服务端内部表:经 contracts/auth.ts 的专用 DTO 暴露,不做 Row 对照 "AdminUser", "AdminSession", "LoginAttempt", "SseEvent", ]; const INTERNAL_ENUMS = ["TasksOpLedgerStatus"]; /** DB 枚举类型名 → contracts 枚举 */ const ENUMS: Record = { AcquisitionPlanPurpose: C.PlanPurpose, AcquisitionPlanStatus: C.PlanStatus, AcquisitionPlanPriority: C.AcquisitionPriority, AcquisitionSlotStatus: C.SlotStatus, AgentRunTrigger: C.AgentTrigger, AgentRunStatus: C.AgentRunStatus, AgentRunEventType: C.AgentRunEventType, ApprovalStatus: C.ApprovalStatus, ArtifactKind: C.ArtifactKind, AssetLocationLocationKind: C.LocationKind, AssetLocationStatus: C.LocationStatus, AssetOperationPhase: C.OperationPhase, AssetOperationKind: C.OperationKind, AssetOperationStatus: C.OperationStatus, AssetUnitCoverageCoverage: C.CoverageKind, AssetUnitCoverageSegment: C.Segment, AutomationGrantStatus: C.GrantStatus, ContentUnitKind: C.ContentUnitKind, ContentUnitContentType: C.ContentType, ContentUnitSegment: C.Segment, ContentUnitStatus: C.ContentUnitStatus, DecisionKind: C.DecisionKind, DecisionStatus: C.DecisionStatus, DecisionSeverity: C.Severity, DownloadTaskOrigin: C.DownloadOrigin, DownloadTaskStatus: C.DownloadStatus, DownloadTaskPauseReason: C.PauseReason, DownloaderInstanceKind: C.DownloaderKind, DownloaderInstanceHealthStatus: C.InstanceHealth, ExternalIdentityEntityType: C.ExternalEntityType, ExternalIdentityStatus: C.IdentityStatus, ExternalIdentityEvidenceLevel: C.EvidenceLevel, ExternalIdentityVerifyStatus: C.IdentityVerifyStatus, FeedSourcePurpose: C.FeedPurpose, FeedSourceKind: C.FeedKind, FeedSourceHealthStatus: C.FeedHealth, IdentityRevisionAction: C.IdentityRevisionAction, IdentityRevisionFromStatus: C.IdentityStatus, IdentityRevisionToStatus: C.IdentityStatus, IdentityRevisionActorType: C.ActorType, LibraryRootMediaKind: C.LibraryMediaKind, LibraryRootManagementMode: C.ManagementMode, LibraryRootHealthStatus: C.InstanceHealth, MediaAssetKind: C.AssetKind, MediaAssetStatus: C.AssetStatus, MediaEntityKind: C.MediaKind, MediaEntityStatus: C.MediaEntityStatus, MediaServerInstanceKind: C.MediaServerKind, MediaServerInstanceHealthStatus: C.InstanceHealth, ObservationStatus: C.ObservationStatus, OrganizationFileResultRole: C.FileRole, OrganizationFileResultMode: C.OrganizationAction, OrganizationFileResultStatus: C.FileResultStatus, OrganizationRecordTrigger: C.OrganizationTrigger, OrganizationRecordActorType: C.ActorType, OrganizationRecordSourcePathView: C.SourcePathView, OrganizationRecordAction: C.OrganizationAction, OrganizationRecordStatus: C.OrganizationStatus, OutboxEventStatus: C.OutboxStatus, PathBindingBindingKind: C.BindingKind, PathBindingStatus: C.BindingStatus, PathMappingViewKind: C.PathViewKind, PathMappingVerifyStatus: C.PathMappingVerifyStatus, PolicyRevisionKind: C.PolicyKind, PolicyRevisionStatus: C.PolicyStatus, PreferenceTagKind: C.PreferenceKind, PreferenceTagOrigin: C.PreferenceOrigin, PreferenceTagStatus: C.PreferenceStatus, ReconcileRunKind: C.ReconcileKind, ReconcileRunMode: C.ReconcileMode, ReconcileRunStatus: C.ReconcileStatus, SecretPurpose: C.SecretPurpose, ServerMappingItemType: C.ServerItemType, ServerMappingMatchStatus: C.ServerMatchStatus, SiteAdapter: C.SiteAdapter, SiteHealthStatus: C.SiteHealth, SiteCircuitState: C.CircuitState, SiteAccountAuthKind: C.SiteAuthKind, SiteAccountStatus: C.SiteAccountStatus, SourceReleaseResolutionStatus: C.ResolutionStatus, SourceReleaseAvailability: C.ReleaseAvailability, SubscriptionMediaKind: C.SubscriptionMediaKind, SubscriptionMode: C.SubscriptionMode, SubscriptionSeasonScope: C.SeasonScope, SubscriptionOrigin: C.SubscriptionOrigin, SubscriptionPriority: C.AcquisitionPriority, SubscriptionStatus: C.SubscriptionStatus, TaskStatus: C.TaskStatus, TitleAliasKind: C.AliasKind, TitleAliasSource: C.AliasSource, }; describe("contracts ⇔ db 表结构同步", () => { const tables = tableColumns(); it("每张表都有 Row schema", () => { expect([...tables.keys()].filter((t) => !INTERNAL_TABLES.includes(t)).sort()).toEqual(Object.keys(ROWS).sort()); }); for (const [table, spec] of Object.entries(ROWS)) { it(`${table} 列与 Row 字段一致`, () => { const dbCols = (tables.get(table) ?? []).map(camel).filter((c) => !spec.dbOnly?.includes(c)); const apiCols = Object.keys(spec.row.shape).filter((c) => !spec.apiOnly?.includes(c)); expect(apiCols.sort()).toEqual(dbCols.sort()); }); } it("每个 DB 枚举都有对应 contracts 枚举且取值一致", () => { const aliases = enumAliases(); for (const internal of INTERNAL_ENUMS) aliases.delete(internal); expect([...aliases.keys()].sort()).toEqual(Object.keys(ENUMS).sort()); for (const [name, values] of aliases) { expect(ENUMS[name]!.options, name).toEqual(values); } }); });