Files
UHD_Media_Tool/packages/contracts/test/db-sync.test.ts
T
rootandClaude Opus 5.5 a24bda952b feat(server): W2-B sources, identity resolution, metadata and site workbench
- Sites / accounts / feeds: registry view (/site-definitions), site CRUD with
  enable/disable (history stays queryable, marked site_disabled), accounts and
  feed URLs stored as secrets (only secret_ref / redacted urlDisplay exposed),
  feed enable/disable/reset/fetch; one shared rate limiter + adapter per site.
- Tasks: ingestFeed (FeedFetchState/FeedCursor on feed_source, observation
  feed+guid upsert with digest-change updates, source_release site+id
  aggregation across feeds, auth failure -> account invalid + decision, no
  retry, gap -> feed.gap_suspected -> budgeted backfill snapshot),
  resolveObservation (parseRelease -> local recall via title_alias pg_trgm +
  accepted identities -> TMDB -> resolveIdentity; auto binds entity /
  provisional units / identities / aliases, otherwise identity_candidate pool
  with evidence version and bounded review backoff; manual lock respected),
  refreshMetadata (metadata_field + field_override, user locks win, paths
  untouched), snapshotSite (site_snapshot coverage records).
- Events: feed.due, feed.gap_suspected, release.observed (self-consumed),
  release.resolved, decision.created/updated; scheduler in worker start().
- Identity API: entity detail with redirects, manual mapping correction with
  identity_revision, merge with identity_redirect, pool list/assign/dismiss/
  retry, content unit numbering lock, aliases, external identities.
- Decisions: list/summary (by entity and kind)/detail/resolve; identity
  options executed directly.
- Workbench: /sites/{id}/releases (snapshot first, coalesced live search with
  backoff, origin + freshness) and /sites/{id}/releases/{rid} (volatile field
  recheck times, refresh keeps data on failure).
- Capabilities: catalog.lookup, release.search, release.inspect,
  identity.resolve.
- Migrations 0018-0020: site.adapter api, source_release.resolution_locked,
  site_snapshot, identity_candidate, resolution_attempt, identity_redirect,
  metadata_field, field_override.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 20:12:01 +08:00

233 lines
9.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string, string[]> {
const out = new Map<string, string[]>();
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<string, string[]> {
const out = new Map<string, string[]>();
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<string, { row: Obj; dbOnly?: string[]; apiOnly?: string[] }> = {
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 },
// W2-B 来源/身份/元数据(0018–0020)
FieldOverride: { row: C.FieldOverride },
IdentityCandidate: { row: C.IdentityCandidate },
IdentityRedirect: { row: C.IdentityRedirect },
MetadataField: { row: C.MetadataField },
ResolutionAttempt: { row: C.ResolutionAttempt },
SiteSnapshot: { row: C.SiteSnapshot },
};
/** 引擎自有表(@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<string, z.ZodEnum> = {
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,
// W2-B(0018–0020)
FieldOverrideSource: C.MetadataSource,
FieldOverrideActorType: C.ActorType,
IdentityCandidateStatus: C.IdentityCandidateStatus,
IdentityCandidateReason: C.IdentityCandidateReason,
IdentityRedirectActorType: C.ActorType,
MetadataFieldSource: C.MetadataSource,
ResolutionAttemptOutcome: C.ResolutionOutcome,
ResolutionAttemptActorType: C.ActorType,
SiteSnapshotKind: C.SiteSnapshotKind,
SiteSnapshotStatus: C.SiteSnapshotStatus,
};
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);
}
});
});