import { describe, expect, it } from "vitest"; import { imageExportModule } from "./built-ins/image-export/image-export-module"; import { svgExportModule } from "./built-ins/svg-export/svg-export-module"; import { timelineModule } from "./built-ins/timeline/timeline-module"; import { videoExportModule } from "./built-ins/video-export/video-export-module"; import type { ToolcraftProductIntegrationPortId } from "./contract/integration-port"; import type { ResolvedProductModulePlan } from "./contract/module-plan"; import { resolveToolcraftProductModules } from "./testing/module-resolution-test-support"; import { getToolcraftModulePortErrors } from "./resolution/validate-module-ports"; import { permutations } from "./testing/module-contribution-test-fixtures"; const rasterPort = "scene.rasterFrameRenderer"; const vectorPort = "scene.vectorFrameRenderer"; const integrationPorts: readonly ToolcraftProductIntegrationPortId[] = [ rasterPort, vectorPort, ]; describe("getToolcraftModulePortErrors", () => { it("returns sorted missing errors with applicability reasons and all consumers", () => { const plan = resolveToolcraftProductModules([ videoExportModule(), svgExportModule(), imageExportModule(), ]).plan; expect( getToolcraftModulePortErrors({ plan, productSceneRequired: true, provided: new Set(), spatialProductSceneRequired: false, }), ).toEqual([ 'Toolcraft integration port "scene.rasterFrameRenderer" is missing; applicability "product-scene" requires it because productSceneRequired is true for consumers: image-export, video-export. Expected public field "ports.scene.rasterFrameRenderer".', 'Toolcraft integration port "scene.vectorFrameRenderer" is missing; applicability "always" requires it for consumers: svg-export. Expected public field "ports.scene.vectorFrameRenderer".', ]); expect( getToolcraftModulePortErrors({ plan, productSceneRequired: false, provided: new Set([vectorPort]), spatialProductSceneRequired: false, }), ).toEqual([]); }); it("requires raster only when an external product scene is required", () => { const plan = resolveToolcraftProductModules([imageExportModule()]).plan; expect( getToolcraftModulePortErrors({ plan, productSceneRequired: false, provided: new Set(), spatialProductSceneRequired: false, }), ).toEqual([]); expect( getToolcraftModulePortErrors({ plan, productSceneRequired: true, provided: new Set(), spatialProductSceneRequired: false, }), ).toEqual([ 'Toolcraft integration port "scene.rasterFrameRenderer" is missing; applicability "product-scene" requires it because productSceneRequired is true for consumers: image-export. Expected public field "ports.scene.rasterFrameRenderer".', ]); }); it("requires an always-applicable vector port regardless of product scene", () => { const plan = resolveToolcraftProductModules([svgExportModule()]).plan; const expected = [ 'Toolcraft integration port "scene.vectorFrameRenderer" is missing; applicability "always" requires it for consumers: svg-export. Expected public field "ports.scene.vectorFrameRenderer".', ]; expect( getToolcraftModulePortErrors({ plan, productSceneRequired: false, provided: new Set(), spatialProductSceneRequired: false, }), ).toEqual(expected); expect( getToolcraftModulePortErrors({ plan, productSceneRequired: true, provided: new Set(), spatialProductSceneRequired: false, }), ).toEqual(expected); }); it.each(integrationPorts)( "rejects unused provided port %s when no resolved requirement consumes it", (providedPort) => { const plan = resolveToolcraftProductModules([ timelineModule({ mode: "playback" }), ]).plan; expect( getToolcraftModulePortErrors({ plan, productSceneRequired: false, provided: new Set([providedPort]), spatialProductSceneRequired: false, }), ).toEqual([ `Toolcraft integration port "${providedPort}" is provided but unused; no resolved module consumes it. Public field "ports.${providedPort}" is not applicable.`, ]); }, ); it("rejects a provided inapplicable product-scene port with known consumers", () => { const plan = resolveToolcraftProductModules([ videoExportModule(), imageExportModule(), ]).plan; expect( getToolcraftModulePortErrors({ plan, productSceneRequired: false, provided: new Set([rasterPort]), spatialProductSceneRequired: false, }), ).toEqual([ 'Toolcraft integration port "scene.rasterFrameRenderer" is provided but unused; applicability "product-scene" is inapplicable because productSceneRequired is false for known consumers: image-export, video-export. Public field "ports.scene.rasterFrameRenderer" is not applicable.', ]); }); it("returns frozen sorted errors for provided and plan-order permutations", () => { const resolvedPlan = resolveToolcraftProductModules([ imageExportModule(), svgExportModule(), ]).plan; const expected = [ 'Toolcraft integration port "scene.rasterFrameRenderer" is provided but unused; applicability "product-scene" is inapplicable because productSceneRequired is false for known consumers: image-export. Public field "ports.scene.rasterFrameRenderer" is not applicable.', ].sort(); for (const portRequirements of permutations( resolvedPlan.portRequirements, )) { for (const provided of permutations(integrationPorts)) { const errors = getToolcraftModulePortErrors({ plan: { ...resolvedPlan, portRequirements }, productSceneRequired: false, provided: new Set(provided), spatialProductSceneRequired: false, }); expect(errors).toEqual(expected); expect(errors).toEqual([...errors].sort()); expect(Object.isFrozen(errors)).toBe(true); } } }); it("does not mutate or freeze caller-owned plan or provided arrays", () => { const plan: ResolvedProductModulePlan = { capabilities: [], modules: [], ownership: [], portRequirements: [ { applicability: "always", consumers: ["svg-export"], id: vectorPort, }, ], }; const provided = new Set([vectorPort]); const inputSnapshot = JSON.stringify({ plan, provided: [...provided] }); expect( getToolcraftModulePortErrors({ plan, productSceneRequired: false, provided, spatialProductSceneRequired: false, }), ).toEqual([]); expect(JSON.stringify({ plan, provided: [...provided] })).toBe(inputSnapshot); expect(Object.isFrozen(plan)).toBe(false); expect(Object.isFrozen(plan.portRequirements)).toBe(false); expect(Object.isFrozen(plan.portRequirements[0])).toBe(false); expect(Object.isFrozen(plan.portRequirements[0]?.consumers)).toBe(false); expect(Object.isFrozen(provided)).toBe(false); }); });