import { describe, expect, it } from "vitest"; import type { ToolcraftModelSourceBundle, ToolcraftModelSourceFile, } from "./model-import-types"; import { decodeToolcraftModelSourceBundleDescriptor, encodeToolcraftModelSourceBundleDescriptor, createToolcraftModelSourceBundleDescriptorResource, TOOLCRAFT_MODEL_SOURCE_BUNDLE_DESCRIPTOR_CONTENT_TYPE, TOOLCRAFT_MODEL_SOURCE_BUNDLE_DESCRIPTOR_MAX_BYTES, } from "./model-source-bundle-codec"; import { createModelSourceResourceRef, digestLegacyModelSourceBundle, digestModelSourceBundle, digestModelSourceBytes, } from "./model-source-digest"; import { TOOLCRAFT_GLTF_ADAPTER_VERSION } from "./formats/production-model-format-manifest"; const encoder = new TextEncoder(); const decoder = new TextDecoder(); function sourceFile( path: string, mimeType: string, bytes: Uint8Array, ): ToolcraftModelSourceFile { const contentDigest = digestModelSourceBytes(bytes); return { byteLength: bytes.byteLength, contentDigest, displayName: path.slice(path.lastIndexOf("/") + 1), mimeType, path, resourceRef: createModelSourceResourceRef(contentDigest, mimeType), }; } function addressBundle( input: Omit< ToolcraftModelSourceBundle, | "aggregateByteLength" | "aggregateDigest" | "descriptorVersion" | "diagnostics" | "packageSource" > & Partial>, ): ToolcraftModelSourceBundle { const aggregateByteLength = input.sourceFiles.reduce( (total, file) => total + file.byteLength, 0, ); return { descriptorVersion: 2, diagnostics: [], packageSource: { kind: "selected-files" }, ...input, aggregateByteLength, aggregateDigest: digestModelSourceBundle( input.adapter, input.rootPath, input.sourceFiles, ), }; } function bundleFixture(): ToolcraftModelSourceBundle { return addressBundle({ adapter: { adapterVersion: TOOLCRAFT_GLTF_ADAPTER_VERSION, format: "gltf", rootExtension: ".gltf", }, rootPath: "scene.gltf", sourceFiles: [ sourceFile( "scene.gltf", "model/gltf+json", encoder.encode('{"asset":{"version":"2.0"}}'), ), sourceFile( "buffers/geometry.bin", "application/octet-stream", new Uint8Array([1, 2, 3, 4]), ), ], }); } function metadataObject(bundle = bundleFixture()): Record { return JSON.parse( decoder.decode(encodeToolcraftModelSourceBundleDescriptor(bundle)), ) as Record; } function encodedJson(value: unknown): Uint8Array { return encoder.encode(JSON.stringify(value)); } function expectDescriptorFailure( run: () => unknown, code: string, ): void { let rejection: unknown; try { run(); } catch (error) { rejection = error; } expect(rejection).toMatchObject({ code }); } describe("Toolcraft model source bundle descriptor codec", () => { it("round-trips deterministic metadata without raw bytes and freezes the graph", () => { const bundle = bundleFixture(); const first = encodeToolcraftModelSourceBundleDescriptor(bundle); const second = encodeToolcraftModelSourceBundleDescriptor(bundle); const decoded = decodeToolcraftModelSourceBundleDescriptor(first); expect([...second]).toEqual([...first]); expect(decoder.decode(first)).not.toContain('"bytes"'); expect(metadataObject(bundle)).toMatchObject({ descriptorVersion: 2 }); expect(decoded).toEqual(bundle); expect(Object.isFrozen(decoded)).toBe(true); expect(Object.isFrozen(decoded.adapter)).toBe(true); expect(Object.isFrozen(decoded.sourceFiles)).toBe(true); expect(decoded.sourceFiles.every(Object.isFrozen)).toBe(true); }); it("keeps version-1 selected-file descriptors readable", () => { const bundle = bundleFixture(); const legacy = { adapter: bundle.adapter, aggregateByteLength: bundle.aggregateByteLength, aggregateDigest: digestLegacyModelSourceBundle( bundle.adapter, bundle.rootPath, bundle.sourceFiles, ), rootPath: bundle.rootPath, sourceFiles: bundle.sourceFiles, }; const decoded = decodeToolcraftModelSourceBundleDescriptor( encodedJson(legacy), ); expect(decoded).toMatchObject({ descriptorVersion: 1, diagnostics: [], packageSource: { kind: "selected-files" }, }); expect(JSON.parse(decoder.decode( encodeToolcraftModelSourceBundleDescriptor(decoded), ))).toEqual(legacy); }); it("creates a deterministic durable staging resource with every source dependency", () => { const bundle = bundleFixture(); const resource = createToolcraftModelSourceBundleDescriptorResource(bundle); expect(resource.sourceBundleRef).toBe( `toolcraft:model-source-bundle:${bundle.aggregateDigest}`, ); expect(resource.stageOptions).toEqual({ contentType: TOOLCRAFT_MODEL_SOURCE_BUNDLE_DESCRIPTOR_CONTENT_TYPE, dependencies: bundle.sourceFiles .map(({ resourceRef }) => resourceRef) .sort(), durable: true, }); expect(decodeToolcraftModelSourceBundleDescriptor(resource.bytes)).toEqual( bundle, ); expect(Object.isFrozen(resource)).toBe(true); expect(Object.isFrozen(resource.stageOptions)).toBe(true); expect(Object.isFrozen(resource.stageOptions.dependencies)).toBe(true); }); it("makes only the original ZIP archive a durable descriptor dependency", () => { const base = bundleFixture(); const archiveDigest = `sha256:${"f".repeat(64)}`; const archiveRef = createModelSourceResourceRef( archiveDigest, "application/zip", ); const zipped = addressBundle({ adapter: base.adapter, diagnostics: [{ affectedCount: 1, code: "ignored-model-root", explanation: "One additional model root was ignored.", severity: "info", }], packageSource: { archive: { byteLength: 128, contentDigest: archiveDigest, displayName: "package.zip", mimeType: "application/zip", resourceRef: archiveRef, }, entries: base.sourceFiles .map((file) => ({ byteLength: file.byteLength, contentDigest: file.contentDigest, mimeType: file.mimeType, path: file.path, })) .sort((left, right) => left.path.localeCompare(right.path)), kind: "zip", }, rootPath: base.rootPath, sourceFiles: base.sourceFiles, }); const resource = createToolcraftModelSourceBundleDescriptorResource(zipped); expect(resource.stageOptions.dependencies).toEqual([archiveRef]); expect(decodeToolcraftModelSourceBundleDescriptor(resource.bytes)).toEqual( zipped, ); }); it.each([ ["missing top-level field", (value: Record) => { delete value.rootPath; }], ["unknown top-level field", (value: Record) => { value.rawBytes = [1, 2, 3]; }], ["missing adapter field", (value: Record) => { delete (value.adapter as Record).adapterVersion; }], ["unknown adapter field", (value: Record) => { (value.adapter as Record).decoder = "unknown"; }], ["missing source field", (value: Record) => { delete (value.sourceFiles as Record[])[0]!.mimeType; }], ["raw source bytes", (value: Record) => { (value.sourceFiles as Record[])[0]!.bytes = [1, 2, 3]; }], ])("rejects %s", (_name, mutate) => { const value = metadataObject(); mutate(value); expectDescriptorFailure( () => decodeToolcraftModelSourceBundleDescriptor(encodedJson(value)), "source-bundle-descriptor-invalid", ); }); it("rejects invalid UTF-8 and oversized descriptor input before JSON traversal", () => { expectDescriptorFailure( () => decodeToolcraftModelSourceBundleDescriptor( new Uint8Array([0x7b, 0x22, 0xc3, 0x28, 0x22, 0x7d]), ), "source-bundle-descriptor-invalid", ); expectDescriptorFailure( () => decodeToolcraftModelSourceBundleDescriptor( new Uint8Array(TOOLCRAFT_MODEL_SOURCE_BUNDLE_DESCRIPTOR_MAX_BYTES + 1), ), "source-bundle-descriptor-limit-exceeded", ); }); it("verifies aggregate length, root membership, normalized unique paths, and root extension", () => { const bundle = bundleFixture(); const missingRoot = addressBundle({ adapter: bundle.adapter, rootPath: "missing.gltf", sourceFiles: bundle.sourceFiles, }); const decomposedPath = addressBundle({ adapter: bundle.adapter, rootPath: bundle.rootPath, sourceFiles: [ bundle.sourceFiles[0]!, { ...bundle.sourceFiles[1]!, displayName: "cafe\u0301.bin", path: "buffers/cafe\u0301.bin", }, ], }); const duplicatePath = addressBundle({ adapter: bundle.adapter, rootPath: bundle.rootPath, sourceFiles: [ bundle.sourceFiles[0]!, { ...bundle.sourceFiles[1]!, displayName: "scene.gltf", path: "scene.gltf", }, ], }); const wrongExtension = addressBundle({ adapter: { ...bundle.adapter, rootExtension: ".glb" }, rootPath: bundle.rootPath, sourceFiles: bundle.sourceFiles, }); for (const invalid of [ { ...bundle, aggregateByteLength: bundle.aggregateByteLength + 1 }, missingRoot, decomposedPath, duplicatePath, wrongExtension, ]) { expectDescriptorFailure( () => encodeToolcraftModelSourceBundleDescriptor(invalid), "source-bundle-descriptor-invalid", ); } }); it("accepts duplicate content-addressed refs for distinct paths and deduplicates dependencies", () => { const bundle = bundleFixture(); const root = bundle.sourceFiles[0]!; const sharedBytes = new Uint8Array([7, 8, 9]); const repeatedRef = addressBundle({ adapter: bundle.adapter, rootPath: bundle.rootPath, sourceFiles: [ root, sourceFile("buffers/a.bin", "application/octet-stream", sharedBytes), sourceFile("buffers/b.bin", "application/octet-stream", sharedBytes), ], }); expect(repeatedRef.sourceFiles[1]!.resourceRef).toBe( repeatedRef.sourceFiles[2]!.resourceRef, ); expect( decodeToolcraftModelSourceBundleDescriptor( encodeToolcraftModelSourceBundleDescriptor(repeatedRef), ), ).toEqual(repeatedRef); expect( createToolcraftModelSourceBundleDescriptorResource(repeatedRef) .stageOptions.dependencies, ).toEqual( [...new Set(repeatedRef.sourceFiles.map(({ resourceRef }) => resourceRef))] .sort(), ); }); it("rejects a content-addressed ref that does not match its file identity", () => { const bundle = bundleFixture(); const root = bundle.sourceFiles[0]!; const mismatchedRef = addressBundle({ adapter: bundle.adapter, rootPath: bundle.rootPath, sourceFiles: [ root, { ...bundle.sourceFiles[1]!, resourceRef: "toolcraft:model-source:sha256:" + "0".repeat(64), }, ], }); expectDescriptorFailure( () => encodeToolcraftModelSourceBundleDescriptor(mismatchedRef), "source-bundle-descriptor-invalid", ); }); it("rejects adapter identities that disagree with the production manifest", () => { const bundle = bundleFixture(); const mismatchedFormat = addressBundle({ adapter: { ...bundle.adapter, format: "glb" }, rootPath: bundle.rootPath, sourceFiles: bundle.sourceFiles, }); const staleAdapter = addressBundle({ adapter: { ...bundle.adapter, adapterVersion: "gltf-webio@stale" }, rootPath: bundle.rootPath, sourceFiles: bundle.sourceFiles, }); for (const invalid of [mismatchedFormat, staleAdapter]) { expectDescriptorFailure( () => encodeToolcraftModelSourceBundleDescriptor(invalid), "source-bundle-descriptor-invalid", ); } }); it("enforces configured bundle file and aggregate source-byte limits", () => { const bundle = bundleFixture(); expectDescriptorFailure( () => encodeToolcraftModelSourceBundleDescriptor(bundle, { limits: { maxBundleFiles: 1 }, }), "source-bundle-descriptor-limit-exceeded", ); expectDescriptorFailure( () => decodeToolcraftModelSourceBundleDescriptor( encodeToolcraftModelSourceBundleDescriptor(bundle), { limits: { maxSourceBytes: bundle.aggregateByteLength - 1 } }, ), "source-bundle-descriptor-limit-exceeded", ); }); it("recomputes and verifies the aggregate digest", () => { expectDescriptorFailure( () => encodeToolcraftModelSourceBundleDescriptor({ ...bundleFixture(), aggregateDigest: `sha256:${"0".repeat(64)}`, }), "source-bundle-digest-mismatch", ); }); });