- Capabilities: all 14 registered (AO: inventory.query, download.plan/submit, organize.plan/execute,
policy.preview/apply; agent: task.inspect/retry/cancel); startup asserts count == 14.
Registry injects server-side authorize (automation_grant by action/scope/policy version/budget,
one-shot approval bound to plan id + payload hash + revision + expiry; model claims ignored;
automation switch), audit_event, server idempotency keys and a capability_invocation op ledger
(migration 0021) used for crash reconciliation. download.plan creates draft plans only;
download.submit promotes draft -> reserved after authorization.
- Agent session API: POST/GET /agent/runs, GET /agent/runs/{id} (events by seq, messages, report),
POST .../messages (wake), POST .../abort; automation-grants, approvals, capabilities invoke, tasks.
- runAgent task: Pi or fake provider from settings (apiKey via secrets), events persisted to
agent_run_event then published, resume + ledger reconciliation when pendingOperations > 0,
waits for goal-linked downloads/imports and reports from business tables (submitted != imported).
- Background triggers: decision.created -> bounded plan_only investigation (dedupe per decision);
decision.updated(resolved) -> resolveDecision executes chosen option.
- W2 closure: all OpenAPI routes registered in contracts api.ts (+ subset test); TMDB key from
settings secret_ref with env fallback; PUT /sites/{id}/credentials documented as alias of
/site-accounts; completion events documented as existing status events.
- e2e (real PG + OpenWorkflow + testkit mocks): RSS->library, agent delegation with grant/deny,
background continuation, worker kill -9 recovery (single qB add), auto investigation dedupe.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
238 lines
9.3 KiB
TypeScript
238 lines
9.3 KiB
TypeScript
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 },
|
||
AgentRunMessage: { row: C.AgentRunMessage },
|
||
CapabilityInvocation: { row: C.CapabilityInvocation },
|
||
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,
|
||
AgentRunMode: C.AgentRunMode,
|
||
AgentRunMessageStatus: C.AgentRunMessageStatus,
|
||
CapabilityInvocationStatus: C.CapabilityInvocationStatus,
|
||
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);
|
||
}
|
||
});
|
||
});
|