import { describe, expect, it } from "vitest"; import type { ToolcraftProductBase } from "./product-base"; import { resolveToolcraftProductDefinition } from "./resolve-toolcraft-product-definition"; import { resolveToolcraftMaterializedSchema } from "./resolve-toolcraft-materialized-schema"; import { createProductBaseFixture } from "./resolve-toolcraft-product-definition.test-support"; import type { ResolvedToolcraftAppIdentity } from "./types"; function assertProductBaseTypeIsClosed(): void { const canvas = { enabled: true } as const; const panels = {} as const; // @ts-expect-error Product identity is required. const missingIdentity: ToolcraftProductBase = { canvas, panels }; const duplicateSettingsIdentity: ToolcraftProductBase = { canvas, identity: { id: "typed", title: "Typed" }, panels, // @ts-expect-error Settings transfer cannot declare a second app identity. settingsTransfer: { appId: "legacy" }, }; const authoredInclude: ToolcraftProductBase = { canvas, identity: { id: "typed", title: "Typed" }, panels, // @ts-expect-error Product persistence cannot author state slices. persistence: { include: ["values"], storage: "localStorage" }, }; const authoredKey: ToolcraftProductBase = { canvas, identity: { id: "typed", title: "Typed" }, panels, // @ts-expect-error Product persistence key derives from base.identity. persistence: { key: "toolcraft:typed:state:v1", storage: "localStorage" }, }; const authoredVersion: ToolcraftProductBase = { canvas, identity: { id: "typed", title: "Typed" }, panels, // @ts-expect-error Product persistence version is runtime-owned. persistence: { storage: "localStorage", version: 1 }, }; const missingSectionId: ToolcraftProductBase = { canvas, identity: { id: "typed", title: "Typed" }, panels: { controls: { sections: [ // @ts-expect-error Product-authored sections require an explicit stable id. { controls: {}, title: "Missing id" }, ], title: "Controls", }, }, }; const missingApplicability: ToolcraftProductBase = { canvas, identity: { id: "typed", title: "Typed" }, panels: { controls: { sections: [ { controls: { // @ts-expect-error Product-authored controls require explicit applicability. value: { target: "product.value", type: "slider" }, }, id: "product", }, ], title: "Controls", }, }, }; const authoredApplicabilityOrigin: ToolcraftProductBase = { canvas, identity: { id: "typed", title: "Typed" }, panels: { controls: { sections: [ { controls: { value: { applicability: { mode: "always", // @ts-expect-error Applicability provenance is resolved by Toolcraft. origin: "explicit", }, target: "product.value", type: "slider", }, }, id: "product", }, ], title: "Controls", }, }, }; const authoredControlVisibleWhen: ToolcraftProductBase = { canvas, identity: { id: "typed", title: "Typed" }, panels: { controls: { sections: [ { controls: { value: { applicability: { mode: "always" as const }, target: "product.value", type: "slider", // @ts-expect-error Product controls use explicit applicability, not visibleWhen. visibleWhen: { equals: true, target: "product.enabled" }, }, }, id: "product", }, ], title: "Controls", }, }, }; void missingIdentity; void duplicateSettingsIdentity; void authoredInclude; void authoredKey; void authoredVersion; void missingSectionId; void missingApplicability; void authoredApplicabilityOrigin; void authoredControlVisibleWhen; } void assertProductBaseTypeIsClosed; function assertMaterializedSchemaInputRequiresProductFacts(): void { const schema = {} as Parameters< typeof resolveToolcraftMaterializedSchema >[0]["schema"]; const identity = {} as ResolvedToolcraftAppIdentity; resolveToolcraftMaterializedSchema({ identity, persistence: undefined, requiredPersistenceSlices: [], schema, }); // @ts-expect-error Product materialization always requires its resolved identity. resolveToolcraftMaterializedSchema({ persistence: undefined, requiredPersistenceSlices: [], schema, }); } void assertMaterializedSchemaInputRequiresProductFacts; function expectProductAliasRejection( value: unknown, expected: RegExp = /serializable plain data/, ): void { const base = createProductBaseFixture(); base.panels.controls!.sections[0]!.controls.intensity!.defaultValue = value; expect(() => resolveToolcraftProductDefinition({ base, modules: [] }), ).toThrow(expected); expect( base.panels.controls!.sections[0]!.controls.intensity!.defaultValue, ).toBe(value); if (value !== null && typeof value === "object") { expect(Object.isFrozen(value)).toBe(false); } } describe("ToolcraftProductBase", () => { it("keeps identity and product-owned configuration explicit", () => { const base: ToolcraftProductBase = { canvas: { enabled: true }, identity: { id: "typed", title: "Typed" }, panels: {}, }; expect(base.identity).toEqual({ id: "typed", title: "Typed" }); }); it("rejects impossible materialization provenance at runtime", () => { const { persistence: _persistence, ...schema } = createProductBaseFixture(); expect(() => resolveToolcraftMaterializedSchema({ persistence: undefined, requiredPersistenceSlices: [], schema, } as unknown as Parameters[0]), ).toThrow(/product materialization requires a resolved identity/); }); it("rejects undefined, class instances, and cyclic plain aliases", () => { class ProductValue { readonly value = 1; } const cyclicObject: Record = {}; cyclicObject.self = cyclicObject; const cyclicArray: unknown[] = []; cyclicArray.push(cyclicArray); for (const value of [ undefined, new ProductValue(), cyclicObject, cyclicArray, ]) { expectProductAliasRejection(value); } }); it("rejects array subclasses, sparse indices, and non-canonical own fields", () => { class ProductArray extends Array {} const sparse = new Array(1); const custom = [1] as unknown[] & { extra?: unknown }; custom.extra = 2; const cyclicExtra = [1] as unknown[] & { extra?: unknown }; cyclicExtra.extra = cyclicExtra; const symbolic = [1]; Object.defineProperty(symbolic, Symbol("extra"), { value: 2 }); const hidden = [1]; Object.defineProperty(hidden, "extra", { value: 2 }); const accessorIndex = [1]; Object.defineProperty(accessorIndex, "0", { get: () => 1 }); for (const value of [ new ProductArray(1), sparse, custom, symbolic, hidden, accessorIndex, ]) { expectProductAliasRejection(value); } expectProductAliasRejection(cyclicExtra, /cyclic aliases/); }); it("terminates plain-data validation before any downstream accessor read", () => { const base = createProductBaseFixture(); const control = base.panels.controls!.sections[0]!.controls.intensity!; let getterCalls = 0; Object.defineProperty(control, "target", { configurable: true, enumerable: true, get(): never { getterCalls += 1; throw new Error("downstream getter executed"); }, }); expect(() => resolveToolcraftProductDefinition({ base, modules: [] }), ).toThrow( /serializable plain data; accessors and non-enumerable fields are forbidden/, ); expect(getterCalls).toBe(0); expect(Object.isFrozen(control)).toBe(false); }); });