import { describe, expect, it } from "vitest"; import { z } from "zod"; import * as C from "../src/index.ts"; // 对 contracts 导出的每个 zod schema 做正反例 parse: // 正例由 sample() 按 schema 结构生成(字符串/数字按候选值逐个试到通过),个别 refine 由 OVERRIDES 修正; // 反例把一个必填字段替换成非法值(对象)或直接传 Symbol(非对象)。 interface Def { type: string; innerType?: z.ZodType; shape?: Record; options?: z.ZodType[]; entries?: Record; values?: unknown[]; element?: z.ZodType; in?: z.ZodType; defaultValue?: unknown; } const def = (s: z.ZodType) => (s as unknown as { _zod: { def: Def } })._zod.def; const UUID = "7d3f7f5e-8a8c-4b8e-9f0a-1b2c3d4e5f60"; const STRING_CANDIDATES = [ UUID, "2026-09-27T08:00:00.000Z", "2026-09-27", "https://example.test/x", "a".repeat(40), "/media/movies", "tmdb", "123", "CatalogLookupInput", "catalog.lookup", "测试 Test", "123456", `lib:${UUID}`, "30 3 * * *", "ALL", "zh-hans", ]; const NUMBER_CANDIDATES = [1, 50, 2024, 0.5, 0, 4096, 128000]; function pick(schema: z.ZodType, candidates: readonly T[]): T { const hit = candidates.find((c) => schema.safeParse(c).success); if (hit === undefined) throw new Error(`没有候选值能通过 ${def(schema).type}`); return hit; } function sample(schema: z.ZodType): unknown { const d = def(schema); switch (d.type) { case "string": return pick(schema, STRING_CANDIDATES); case "number": return pick(schema, NUMBER_CANDIDATES); case "boolean": return true; case "enum": return Object.values(d.entries!)[0]; case "literal": return d.values![0]; case "nullable": case "optional": case "default": case "prefault": case "readonly": return sample(d.innerType!); case "array": return [sample(d.element!)]; case "record": return { k: "v" }; case "unknown": return { any: 1 }; case "union": return sample(d.options![0]!); case "pipe": return sample(d.in!); case "object": { const out: Record = {}; for (const [k, v] of Object.entries(d.shape!)) out[k] = sample(v); return out; } default: throw new Error(`sample 未支持 ${d.type}`); } } /** 需要满足跨字段 refine 的正例修正 */ const OVERRIDES: Record) => Record> = { PathMappingCreate: (v) => ({ ...v, viewKind: "downloader", mediaServerInstanceId: null }), PathMappingUpdate: (v) => ({ ...v, viewKind: "downloader", mediaServerInstanceId: null }), ExternalIdentityCreate: (v) => ({ ...v, contentUnitId: null }), // 枚举键的 record 在 zod v4 里要求键齐全 SubscriptionOverviewPage: (v) => ({ ...v, counts: Object.fromEntries(C.SubscriptionGroup.options.map((g) => [g, 0])), }), PathBindingCreate: (v) => ({ ...v, bindingKind: "season", seasonNumber: 1 }), OrganizeRequest: (v) => ({ ...v, sourcePaths: undefined }), FilesListQuery: (v) => ({ ...v, filter: "video,unidentified" }), EngineRunQuery: (v) => ({ ...v, status: "running,pending", triggeredBy: "user,api_key" }), CatalogLookupInput: (v) => v, IdentityResolveInput: (v) => v, AssetLocationCreate: (v) => v, ReleaseIdentityAssign: (v) => ({ ...v, external: undefined, candidateIndex: undefined }), CacheSettings: (v) => ({ ...v, hardThresholdPercent: 90, resumePercent: 85, cleanupTargetPercent: 80 }), GlobalSettings: () => C.DEFAULT_SETTINGS, // 方案至少 2 个(id 不同) AgentProposeOptionsInput: (v) => { const o = (v.options as Record[])[0]!; return { ...v, options: [o, { ...o, id: "b" }] }; }, EngineStats: (v) => ({ ...v, byStatus: { pending: 1 } }), TypedEventEnvelope: () => ({ seq: 1, at: "2026-09-27T08:00:00Z", type: "system.heartbeat", payload: { role: "all" }, }), }; const isSchema = (v: unknown): v is z.ZodType => v instanceof z.ZodType; const schemas: [string, z.ZodType][] = []; for (const [k, v] of Object.entries(C) as [string, unknown][]) if (isSchema(v)) schemas.push([k, v]); describe("所有导出 schema 的正反例", () => { it("导出了足够多的 schema", () => { expect(schemas.length).toBeGreaterThan(150); }); for (const [name, schema] of schemas) { it(name, () => { let good = sample(schema); const fix = OVERRIDES[name]; if (fix && good && typeof good === "object") good = fix(good as Record); const ok = schema.safeParse(good); if (!ok.success) throw new Error(`${name} 正例失败: ${ok.error.message}\n${JSON.stringify(good)}`); // 反例 const d = def(schema); if (d.type === "object") { const shape = d.shape!; const key = Object.keys(shape).find( (k) => !shape[k]!.safeParse(undefined).success && def(shape[k]!).type !== "unknown", ); const bad = key ? { ...(good as object), [key]: Symbol("bad") } : Symbol("bad"); expect(schema.safeParse(bad).success, `${name} 反例应失败(${key ?? "整体"})`).toBe(false); } else { expect(schema.safeParse(Symbol("bad")).success, `${name} 反例应失败`).toBe(false); } }); } }); describe("关键合同行为", () => { it("Slot 状态集合与 REQ §6.1 一致", () => { expect(C.SlotStatus.options).toEqual([ "empty", "reserved", "downloading", "verifying", "satisfied", "upgrade_pending", "attention", ]); }); it("CacheSettings 拒绝恢复水位 ≥ 硬阈值", () => { expect(C.CacheSettings.safeParse({ ...C.DEFAULT_SETTINGS.cache, resumePercent: 95 }).success).toBe(false); }); it("GlobalSettingsUpdate 部分更新不会注入默认值,mergeSettings 保留现值", () => { const patch = C.GlobalSettingsUpdate.parse({ automation: { enabled: false }, pi: { contextBudget: 64_000 } }); expect(patch).toEqual({ automation: { enabled: false }, pi: { contextBudget: 64_000 } }); const merged = C.mergeSettings(C.DEFAULT_SETTINGS, patch); expect(merged.automation).toEqual({ enabled: false, readOnlyReconcile: false }); expect(merged.pi).toEqual({ ...C.DEFAULT_SETTINGS.pi, contextBudget: 64_000 }); expect(C.GlobalSettings.safeParse(merged).success).toBe(true); }); it("OrganizationChangesQuery 接受查询串并给默认值", () => { expect(C.OrganizationChangesQuery.parse({ after: "42", limit: "10", includeFiles: "false" })).toEqual({ after: "42", limit: 10, includeFiles: false, }); expect(C.OrganizationChangesQuery.safeParse({ after: "-1" }).success).toBe(false); }); it("ExternalIdentityCreate 要求且只能有一个本地目标", () => { const base = { provider: "tmdb", entityType: "movie", externalId: "1" }; expect(C.ExternalIdentityCreate.safeParse({ ...base, mediaEntityId: UUID }).success).toBe(true); expect(C.ExternalIdentityCreate.safeParse({ ...base, mediaEntityId: UUID, contentUnitId: UUID }).success).toBe( false, ); }); it("能力目录覆盖首批 14 个能力且 schema 引用都存在", () => { expect(Object.keys(C.CAPABILITY_CATALOG).sort()).toEqual([...C.CAPABILITY_IDS].sort()); const exported = C as Record; for (const cap of Object.values(C.CAPABILITY_CATALOG)) { expect(C.CapabilityDescriptor.safeParse(cap).success).toBe(true); expect(isSchema(exported[cap.inputSchema]), cap.inputSchema).toBe(true); expect(isSchema(exported[cap.outputSchema]), cap.outputSchema).toBe(true); expect(exported[cap.inputSchema]).toBe(C.CAPABILITY_IO[cap.id as C.KnownCapabilityId].input); expect(exported[cap.outputSchema]).toBe(C.CAPABILITY_IO[cap.id as C.KnownCapabilityId].output); } }); it("capabilitySpec 组装目录元数据与真实 schema;有副作用的类别不可重放", () => { for (const id of C.CAPABILITY_IDS) { const spec = C.capabilitySpec(id); const { inputSchema: _i, outputSchema: _o, ...meta } = C.CAPABILITY_CATALOG[id]; expect(spec).toMatchObject(meta); expect(spec.inputSchema).toBe(C.CAPABILITY_IO[id].input); expect(spec.outputSchema).toBe(C.CAPABILITY_IO[id].output); // 例外:site.health_check 只读首页并写回健康列,幂等可重跑 if (spec.kind !== "read" && spec.kind !== "preview" && id !== "site.health_check") expect(spec.replay, id).toBe("never"); } }); it("事件 payload 表覆盖全部事件类型", () => { expect(Object.keys(C.EVENT_PAYLOADS).sort()).toEqual(Object.values(C.EventType).sort()); expect( C.parseEventPayload("task.status_changed", { taskId: UUID, kind: "ingestFeed", from: null, to: "running", error: null, }), ).not.toBeNull(); expect(C.parseEventPayload("task.status_changed", { taskId: "x" })).toBeNull(); expect(C.isEventType("nope.x")).toBe(false); }); it("API 端点路径唯一且 buildPath 填参", () => { const eps = C.listEndpoints(); const keys = eps.map((e) => `${e.method} ${e.path}`); expect(new Set(keys).size).toBe(keys.length); expect(C.buildPath(C.Api.tasks.cancel.path, { id: UUID })).toBe(`/api/v1/tasks/${UUID}/cancel`); expect(() => C.buildPath(C.Api.tasks.get.path)).toThrow(); // 公开整理记录读写分权 expect(C.Api.organization.changes.scope).toBe("organize.read"); expect(C.Api.organization.create.scope).toBe("organize.execute"); }); });