import { describe, expect, it } from "vitest"; import { canvasEditingModule } from "./built-ins/canvas-editing/canvas-editing-module"; import { resolveToolcraftModuleContributions } from "./testing/module-resolution-test-support"; import { resolveToolcraftModuleDependencies } from "./testing/module-resolution-test-support"; import { resolveToolcraftModulePortRequirements } from "./resolution/resolve-product-modules"; import { resolveToolcraftProductModules } from "./testing/module-resolution-test-support"; import { createInvalidTestToolcraftProductModuleRecord, unsafeCastTestOnlyToolcraftProductModuleRecord, } from "./testing/create-test-module-definition"; const opaqueDefinitionError = 'Toolcraft product module "canvas-editing" is not an opaque definition created by a Toolcraft module factory; pass the factory result without copying or mutation.'; function copiedSymbolKeys(source: object, clone: object): readonly symbol[] { const sourceSymbols = Object.getOwnPropertySymbols(source); const cloneSymbols = Object.getOwnPropertySymbols(clone); expect(cloneSymbols).toEqual(sourceSymbols); expect(cloneSymbols.length).toBeGreaterThan(0); return cloneSymbols; } function assertContributionResolutionIsDefinitionFree(): void { const definition = canvasEditingModule(); const clone = { ...definition }; const contributions: Parameters< typeof resolveToolcraftModuleContributions >[0] = definition.contributions; // @ts-expect-error Contribution resolution accepts contributions, not definitions. resolveToolcraftModuleContributions([definition]); // @ts-expect-error An inferred definition clone is not a contribution record. resolveToolcraftModuleContributions([clone]); // @ts-expect-error A contribution batch exposes no module capability definition fields. contributions.provides.includes("layers.management"); } void assertContributionResolutionIsDefinitionFree; describe("Toolcraft product module definition identity boundary", () => { it("accepts the exact constructor-owned factory identity in both resolvers", () => { const definition = canvasEditingModule(); expect( resolveToolcraftModuleDependencies([definition]).definitions[0], ).toBe(definition); expect(resolveToolcraftProductModules([definition]).plan.modules).toEqual([ { id: "canvas-editing", origin: "explicit", provides: ["canvas.editing"], requestedBy: [], requires: [], }, ]); expect( resolveToolcraftModuleContributions(definition.contributions) .canvasBehaviors, ).toEqual([ { behavior: "editing", operations: ["drag", "handles", "resize", "selection"], }, ]); expect(resolveToolcraftModulePortRequirements([definition])).toEqual([ { applicability: "always", consumers: ["canvas-editing"], id: "scene.canvasContent", }, ]); }); it("rejects an inferred spread clone in direct port aggregation", () => { const source = canvasEditingModule(); const clone = { ...source }; copiedSymbolKeys(source, clone); expect(() => resolveToolcraftModulePortRequirements([ unsafeCastTestOnlyToolcraftProductModuleRecord(clone), ]), ).toThrowError(opaqueDefinitionError); }); it("rejects an inferred semantically modified clone in port aggregation", () => { const source = canvasEditingModule(); const clone = { ...source, contributions: [] }; copiedSymbolKeys(source, clone); expect(() => resolveToolcraftModulePortRequirements([ unsafeCastTestOnlyToolcraftProductModuleRecord(clone), ]), ).toThrowError(opaqueDefinitionError); }); it("rejects an otherwise exact spread clone in both resolvers", () => { const source = canvasEditingModule(); const clone = unsafeCastTestOnlyToolcraftProductModuleRecord({ ...source }); copiedSymbolKeys(source, clone); expect(() => resolveToolcraftModuleDependencies([clone])).toThrowError( opaqueDefinitionError, ); expect(() => resolveToolcraftProductModules([clone])).toThrowError( opaqueDefinitionError, ); }); it("reports every unsupported version before checking current-version identity", () => { const currentVersionClone = unsafeCastTestOnlyToolcraftProductModuleRecord({ ...canvasEditingModule(), }); const unsupported = unsafeCastTestOnlyToolcraftProductModuleRecord( createInvalidTestToolcraftProductModuleRecord({ contractVersion: 3, id: "image-export", provides: ["artifact.image-export"], requires: [], }), ); expect(() => resolveToolcraftModuleDependencies([currentVersionClone, unsupported]), ).toThrowError( 'Toolcraft product module "image-export" uses contract version 3; expected current version 2.', ); }); it.each([ { field: "provides", mutate: (source: ReturnType) => ({ ...source, provides: ["canvas.editing", "layers.management"], }), }, { field: "requires", mutate: (source: ReturnType) => ({ ...source, requires: ["media.source"], }), }, { field: "contributions", mutate: (source: ReturnType) => ({ ...source, contributions: [], }), }, ])( "rejects a spread clone with modified $field despite copied private symbols", ({ mutate }) => { const source = canvasEditingModule(); const clone = unsafeCastTestOnlyToolcraftProductModuleRecord( mutate(source), ); copiedSymbolKeys(source, clone); expect(() => resolveToolcraftModuleDependencies([clone])).toThrowError( opaqueDefinitionError, ); expect(() => resolveToolcraftProductModules([clone])).toThrowError( opaqueDefinitionError, ); }, ); });