From a22fb6e60b2560fcd68324bae63d8055d122233c Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sun, 15 Nov 2026 07:58:59 +0800 Subject: [PATCH] fix(model): report malformed GGUF and Safetensors headers as invalid_format through ParseFailure (#10667) * refactor(model): report GGUF and Safetensors format failures through ParseFailure * fix(model): report malformed model headers as invalid_format through the shared mapping Malformed and truncated GGUF and Safetensors headers now take the shared ParseFailure mapping (invalid_request, invalid_format on path) instead of the model provider's own BinaryTargetError file_format_rejected. Unsupported forms stay unsupported_target and model allowances keep their resource-constraint recovery. --- docs/model-artifacts.md | 7 +- .../verify/architecture-guard-baselines.json | 2 - src/model/GgufHeader.ts | 13 ++-- src/model/ModelArtifactProvider.ts | 69 ++++++++--------- src/model/ModelFormat.ts | 63 ++++++++------- src/model/ModelHeaderReader.ts | 7 +- src/model/SafetensorsHeader.ts | 12 ++- tests/acceptance/modelArtifactCli.test.ts | 47 +++++++----- tests/boundary/model/modelArtifact.test.ts | 76 +++++++++++-------- 9 files changed, 160 insertions(+), 136 deletions(-) diff --git a/docs/model-artifacts.md b/docs/model-artifacts.md index 71464e5902..a1a761190e 100644 --- a/docs/model-artifacts.md +++ b/docs/model-artifacts.md @@ -165,8 +165,11 @@ unknown fields on tensor entries are ignored within the reader's 32-level JSON nesting limit, while the required dtype, shape and data offsets are still validated. JSON duplicate keys, invalid UTF-8, out-of-file ranges, overlaps, gaps, trailing unindexed data and known dtype/shape byte-length -contradictions are rejected with a typed `file_format_rejected` constraint (public code `target_unavailable`, category `unsupported_target`). -A missing, non-regular or otherwise unusable selected path is also `target_unavailable`; +contradictions are rejected as an invalid `path` (public code `invalid_request`, +category `invalid_input`, issue reason `invalid_format`), like any other +malformed or truncated model header. A form outside the reader's coverage, such +as a PyTorch pickle, is `unsupported_target`. +A missing, non-regular or otherwise unusable selected path is `target_unavailable`; host I/O or resource failures on a valid selection, such as `EMFILE` or `EIO`, are `execution_failure` with the path and reason in `details.diagnostics`. Empty tensors and scalar shapes are supported. The reader retains shape dimensions as diff --git a/scripts/verify/architecture-guard-baselines.json b/scripts/verify/architecture-guard-baselines.json index 41455ba456..bf7ba82fe9 100644 --- a/scripts/verify/architecture-guard-baselines.json +++ b/scripts/verify/architecture-guard-baselines.json @@ -191,7 +191,6 @@ "src/il2cpp/Il2cppBinaryFormat.ts": 1, "src/il2cpp/Il2cppMetadataFormat.ts": 1, "src/javascript/sourceMaps/SourceMapFormat.ts": 1, - "src/model/ModelFormat.ts": 1, "src/native/AppleMachoSelection.ts": 1, "src/native/MachoCommandNames.ts": 1, "src/native/apple/AppleIps.errors.ts": 1, @@ -240,7 +239,6 @@ "src/dotnet/ManagedStaticProvider.ts": 1, "src/firmware/FirmwareCommand.ts": 2, "src/il2cpp/Il2cppMetadataFile.ts": 2, - "src/model/ModelFormat.ts": 2, "src/model/ModelHeaderReader.ts": 1, "src/native/AppleDispatchMetadata.ts": 3, "src/native/AppleMachoFixups.ts": 48, diff --git a/src/model/GgufHeader.ts b/src/model/GgufHeader.ts index 8ab1b78852..55ed527b23 100644 --- a/src/model/GgufHeader.ts +++ b/src/model/GgufHeader.ts @@ -8,10 +8,12 @@ import type { ModelArtifactPartialTensor, } from "../domain/model/modelArtifact.js"; import { ModelHeaderReader } from "./ModelHeaderReader.js"; +import { unsupportedInput } from "../domain/parseFailure.js"; import { MODEL_ARRAY_PREVIEW, MODEL_METADATA_NESTING, - ModelFormatFailure, + MODEL_FORMAT, + modelCapacityFailure, ModelReportBudget, rejectModel, } from "./ModelFormat.js"; @@ -125,10 +127,9 @@ export class GgufHeader { // occupies one, so charge it on entry instead of on a child visit. const frames = enclosingFrames + 1; if (frames > MODEL_METADATA_NESTING) - throw new ModelFormatFailure( - "capacity", - `GGUF metadata nesting exceeds ${String(MODEL_METADATA_NESTING)} array frames`, + throw modelCapacityFailure( "metadata-nesting", + `GGUF metadata nesting exceeds ${String(MODEL_METADATA_NESTING)} array frames`, ); const element = Number(await this.#unsigned(4)); const rawCount = await this.#unsigned(8); @@ -262,8 +263,8 @@ export class GgufHeader { this.#little = false; version = bigVersion; } else { - throw new ModelFormatFailure( - "unsupported", + throw unsupportedInput( + MODEL_FORMAT, `GGUF version bytes ${versionBytes.toString("hex")} are unsupported (little-endian ${String(littleVersion)}, big-endian ${String(bigVersion)}; byte order unknown); supported versions are 2 and 3`, ); } diff --git a/src/model/ModelArtifactProvider.ts b/src/model/ModelArtifactProvider.ts index d5faaace31..dd9310b7f2 100644 --- a/src/model/ModelArtifactProvider.ts +++ b/src/model/ModelArtifactProvider.ts @@ -21,9 +21,7 @@ import { AnalysisAccessDeniedError, AnalysisCancelledError, AnalysisResourceConstraintError, - AnalysisUnsupportedTargetError, } from "../domain/analysisErrorCore.js"; -import { BinaryTargetError } from "../domain/configurationErrors.js"; import { ProviderAdapterError } from "../domain/providerAdapterError.js"; import { err, ok, type Result } from "../domain/result.js"; import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; @@ -40,12 +38,15 @@ import { ModelHeaderReader } from "./ModelHeaderReader.js"; import { GgufHeader } from "./GgufHeader.js"; import { readSafetensorsHeader } from "./SafetensorsHeader.js"; import { - ModelFormatFailure, + modelCapacityFailure, + modelCapacityLimitOf, MODEL_REPORT_BYTES, modelCapacityRemediation, modelCapacityReportedLimits, } from "./ModelFormat.js"; import { jsonValueByteLength } from "../domain/jsonValueByteLength.js"; +import { ParseFailureError } from "../domain/parseFailure.js"; +import { parseFailureAnalysisError } from "../domain/parseFailureAnalysisError.js"; import { ProviderCleanupError } from "../domain/providerCleanupError.js"; import { selectedFileTargetError } from "../artifacts/RegularFileErrorProjection.js"; import { @@ -107,36 +108,33 @@ export class ModelArtifactProvider implements ModelArtifactPort { ); } catch (cause) { const { cause: primary, origin } = artifactFilePrimaryFacts(cause); + const capacity = + primary instanceof ParseFailureError + ? modelCapacityLimitOf(primary) + : undefined; + const formatError = parseFailureAnalysisError( + OPERATION, + input.path, + primary, + ); let error: AnalysisError; if (origin === "owned-or-classified" && primary instanceof AnalysisError) error = primary; - else if (primary instanceof ModelFormatFailure) { - const limit = primary.kind === "capacity" ? primary.limit : undefined; - error = - limit !== undefined - ? new AnalysisResourceConstraintError( - OPERATION, - "memory", - primary.message, - modelCapacityReportedLimits(limit), - { - cause, - retryable: false, - remediationAction: modelCapacityRemediation(limit), - }, - ) - : primary.kind === "unsupported" - ? new AnalysisUnsupportedTargetError( - OPERATION, - input.path, - primary.message, - { cause }, - ) - : new BinaryTargetError(input.path, primary.message, { - cause, - constraint: "file_format_rejected", - }); - } else if ( + else if (primary instanceof ParseFailureError && capacity !== undefined) + // A model allowance names its own recovery. + error = new AnalysisResourceConstraintError( + OPERATION, + "memory", + primary.message, + modelCapacityReportedLimits(capacity), + { + cause, + retryable: false, + remediationAction: modelCapacityRemediation(capacity), + }, + ); + else if (formatError !== undefined) error = formatError; + else if ( origin === "owned-or-classified" && primary instanceof ArtifactReaderFailure && primary.reason === "cancelled" @@ -245,10 +243,9 @@ export class ModelArtifactProvider implements ModelArtifactPort { ): Promise { const initial = await file.stat({ bigint: true }); if (initial.size > BigInt(Number.MAX_SAFE_INTEGER)) - throw new ModelFormatFailure( - "capacity", - "Model file exceeds exact host offset precision", + throw modelCapacityFailure( "exact-offset", + "Model file exceeds exact host offset precision", ); const reader = new ModelHeaderReader(file, Number(initial.size), signal); const first = await reader.take(4); @@ -430,11 +427,7 @@ export class ModelArtifactProvider implements ModelArtifactPort { ], }; if (jsonValueByteLength(report, MODEL_REPORT_BYTES) > MODEL_REPORT_BYTES) - throw new ModelFormatFailure( - "capacity", - "Model report exceeds 32 MiB", - "report-bytes", - ); + throw modelCapacityFailure("report-bytes", "Model report exceeds 32 MiB"); return modelArtifactSchema.parse(report); } } diff --git a/src/model/ModelFormat.ts b/src/model/ModelFormat.ts index f6fc0dee60..7076355bb6 100644 --- a/src/model/ModelFormat.ts +++ b/src/model/ModelFormat.ts @@ -1,3 +1,9 @@ +import { + malformedInput, + ParseFailureError, + unsupportedInput, +} from "../domain/parseFailure.js"; + /** Producing constraint for a model capacity refusal. Recovery uses this, not message text. */ export const MODEL_CAPACITY_LIMITS = [ "header-bytes", @@ -11,34 +17,40 @@ export const MODEL_CAPACITY_LIMITS = [ /** Which fixed model allowance refused the request. */ export type ModelCapacityLimit = (typeof MODEL_CAPACITY_LIMITS)[number]; -/** Format failures stay separate from filesystem, capacity and cancellation failures. */ -export class ModelFormatFailure extends Error { - /** Set only for `kind: "capacity"`. */ - readonly limit: ModelCapacityLimit | undefined; - constructor(kind: "format" | "unsupported", message: string); - constructor(kind: "capacity", message: string, limit: ModelCapacityLimit); - constructor( - readonly kind: "format" | "unsupported" | "capacity", - message: string, - limit?: ModelCapacityLimit, - ) { - super(message); - this.name = "ModelFormatFailure"; - if (kind === "capacity" && limit === undefined) - throw new TypeError("Model capacity failures require a limit kind"); - if (kind !== "capacity" && limit !== undefined) - throw new TypeError("Only model capacity failures carry a limit kind"); - this.limit = limit; - } -} +/** The ParseFailure format of GGUF and Safetensors model headers. */ +export const MODEL_FORMAT = "model"; + +/** A fixed model allowance refused the input; the limit name is its {@link ModelCapacityLimit}. */ +export const modelCapacityFailure = ( + limit: ModelCapacityLimit, + detail: string, +): ParseFailureError<"limit"> => + new ParseFailureError({ + kind: "limit", + format: MODEL_FORMAT, + detail, + limit: { + name: limit, + maximum: Object.values(modelCapacityReportedLimits(limit))[0] ?? 0, + }, + }); + +/** The model allowance a failure names, or undefined for any other failure. */ +export const modelCapacityLimitOf = ( + cause: ParseFailureError, +): ModelCapacityLimit | undefined => { + const { failure } = cause; + if (failure.kind !== "limit") return undefined; + return MODEL_CAPACITY_LIMITS.find((limit) => limit === failure.limit.name); +}; export function rejectModel(message: string): never { - throw new ModelFormatFailure("format", message); + throw malformedInput(MODEL_FORMAT, message); } export function rejectPickle(): never { - throw new ModelFormatFailure( - "unsupported", + throw unsupportedInput( + MODEL_FORMAT, "PyTorch pickle/ZIP model deserialization is unsupported; supply GGUF or safetensors instead", ); } @@ -108,10 +120,9 @@ export class ModelReportBudget { #remaining = MODEL_REPORT_BYTES; charge(bytes: number): void { if (!Number.isSafeInteger(bytes) || bytes < 0 || bytes > this.#remaining) - throw new ModelFormatFailure( - "capacity", - "Model facts exceed the 32 MiB retained representation budget. This release has no runtime override.", + throw modelCapacityFailure( "representation-bytes", + "Model facts exceed the 32 MiB retained representation budget. This release has no runtime override.", ); this.#remaining -= bytes; } diff --git a/src/model/ModelHeaderReader.ts b/src/model/ModelHeaderReader.ts index 4320f4501d..0ec2f2bca0 100644 --- a/src/model/ModelHeaderReader.ts +++ b/src/model/ModelHeaderReader.ts @@ -3,7 +3,7 @@ import type { FileHandle } from "node:fs/promises"; import { assertArtifactReadActive } from "../artifacts/ArtifactFileOperation.js"; import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js"; import { - ModelFormatFailure, + modelCapacityFailure, MODEL_HEADER_BYTES, rejectModel, } from "./ModelFormat.js"; @@ -58,10 +58,9 @@ export class ModelHeaderReader { if (!Number.isSafeInteger(end) || end < 0 || end > this.size) rejectModel("Declared header extent is outside the file"); if (end - this.#headerPrefixBytes > MODEL_HEADER_BYTES) - throw new ModelFormatFailure( - "capacity", - "Model header exceeds 100,000,000 bytes. This release has no runtime override.", + throw modelCapacityFailure( "header-bytes", + "Model header exceeds 100,000,000 bytes. This release has no runtime override.", ); } diff --git a/src/model/SafetensorsHeader.ts b/src/model/SafetensorsHeader.ts index 3a463423c2..f9073fd5c7 100644 --- a/src/model/SafetensorsHeader.ts +++ b/src/model/SafetensorsHeader.ts @@ -10,7 +10,7 @@ import { ModelHeaderReader } from "./ModelHeaderReader.js"; import { MODEL_HEADER_BYTES, MODEL_JSON_NESTING, - ModelFormatFailure, + modelCapacityFailure, ModelReportBudget, rejectModel, rejectPickle, @@ -73,10 +73,9 @@ class HeaderJson { this.#space(); this.budget.charge(64); if (depth > MODEL_JSON_NESTING) - throw new ModelFormatFailure( - "capacity", - `Safetensors JSON nesting exceeds ${String(MODEL_JSON_NESTING)} levels`, + throw modelCapacityFailure( "json-nesting", + `Safetensors JSON nesting exceeds ${String(MODEL_JSON_NESTING)} levels`, ); const c = this.text[this.#position]; if (c === '"') return this.#string(); @@ -190,10 +189,9 @@ const integersOf = (value: HeaderValue | undefined): bigint[] => { }; const exactOffset = (value: bigint): number => { if (value > BigInt(Number.MAX_SAFE_INTEGER)) - throw new ModelFormatFailure( - "capacity", - "Safetensors data offset exceeds exact host offset precision", + throw modelCapacityFailure( "exact-offset", + "Safetensors data offset exceeds exact host offset precision", ); return Number(value); }; diff --git a/tests/acceptance/modelArtifactCli.test.ts b/tests/acceptance/modelArtifactCli.test.ts index fcf58c8c25..7f7dff80d9 100644 --- a/tests/acceptance/modelArtifactCli.test.ts +++ b/tests/acceptance/modelArtifactCli.test.ts @@ -24,6 +24,18 @@ import { import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; import { cliTest, createSharedMcpSession } from "../support/cli/cliFixture.js"; +/** A malformed model header is an invalid `path` on both adapters. */ +const MALFORMED_MODEL = { + code: "invalid_request", + category: "invalid_input", + details: { + operation: "inspect_model_artifact", + issues: [ + expect.objectContaining({ path: ["path"], reason: "invalid_format" }), + ], + }, +} as const; + // Model queries are target-free; share immutable registration and schema setup. // Portable export keeps its separate post-CLI connection below. const modelMcp = createSharedMcpSession(async (registerCleanup) => { @@ -519,12 +531,17 @@ cliTest( // The raw extent is outside this empty file, and that check runs before // the offset is converted to a host number. expect(offsetFailure.error).toMatchObject({ - code: "target_unavailable", - category: "unsupported_target", + code: "invalid_request", + category: "invalid_input", details: { - constraint: "file_format_rejected", - path, - reason: "Declared extent is outside the file", + operation: "inspect_model_artifact", + issues: [ + { + path: ["path"], + reason: "invalid_format", + message: `Selected artifact ${path}: Declared extent is outside the file`, + }, + ], }, }); }, @@ -1085,17 +1102,13 @@ cliTest( }); expect(malformedCli.exitCode).toBe(1); expect(malformedCli.json).toMatchObject({ - code: "target_unavailable", - category: "unsupported_target", - details: { constraint: "file_format_rejected" }, + ...MALFORMED_MODEL, }); const malformedMcp = parseMcpToolError( await call("inspect_model_artifact", { path: malformedPath }), ); expect(malformedMcp.error).toMatchObject({ - code: "target_unavailable", - category: "unsupported_target", - details: { constraint: "file_format_rejected" }, + ...MALFORMED_MODEL, }); } }, @@ -1194,17 +1207,13 @@ cliTest( }); expect(cliResult.exitCode).toBe(1); expect(cliResult.json).toMatchObject({ - code: "target_unavailable", - category: "unsupported_target", - details: { constraint: "file_format_rejected" }, + ...MALFORMED_MODEL, }); const mcpResult = parseMcpToolError( await call("inspect_model_artifact", { path }), ); expect(mcpResult.error).toMatchObject({ - code: "target_unavailable", - category: "unsupported_target", - details: { constraint: "file_format_rejected" }, + ...MALFORMED_MODEL, }); } }, @@ -1283,9 +1292,7 @@ cliTest( await call("inspect_model_artifact", { path }), ); expect(malformed.error).toMatchObject({ - code: "target_unavailable", - category: "unsupported_target", - details: { constraint: "file_format_rejected" }, + ...MALFORMED_MODEL, }); }, ); diff --git a/tests/boundary/model/modelArtifact.test.ts b/tests/boundary/model/modelArtifact.test.ts index 2f5f46313a..7cfc583b12 100644 --- a/tests/boundary/model/modelArtifact.test.ts +++ b/tests/boundary/model/modelArtifact.test.ts @@ -8,11 +8,11 @@ import { MODEL_JSON_NESTING, MODEL_METADATA_NESTING, MODEL_REPORT_BYTES, - ModelFormatFailure, modelCapacityRemediation, modelCapacityReportedLimits, } from "../../../src/model/ModelFormat.js"; import { readSafetensorsHeader } from "../../../src/model/SafetensorsHeader.js"; +import { ParseFailureError } from "../../../src/domain/parseFailure.js"; import { GgufHeader } from "../../../src/model/GgufHeader.js"; import { createModelArtifactService } from "../../../src/composition/model.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; @@ -25,6 +25,14 @@ import { safetensorsFixture, } from "../../fixtures/model/model.js"; +/** A malformed model header is an invalid `path`, like every format failure. */ +const INVALID_FORMAT = { + _tag: "AnalysisInputError", + issues: [ + expect.objectContaining({ path: ["path"], reason: "invalid_format" }), + ], +} as const; + const hash = (bytes: Buffer) => createHash("sha256").update(bytes).digest("hex"); const providerAt = async (bytes: Buffer) => { @@ -82,8 +90,7 @@ it("continues refusing the unsupported big-endian GGUF v2 layout", async () => { const result = await provider.inspect({ path, hash: "header" }); if (result.ok) throw new Error("Big-endian GGUF v2 admitted"); expect(result.error).toMatchObject({ - _tag: "BinaryTargetError", - constraint: "file_format_rejected", + ...INVALID_FORMAT, }); }); @@ -212,7 +219,7 @@ it.each([ const result = await provider.inspect({ path, hash: "header" }); if (result.ok) throw new Error("Expected format refusal"); expect(result.error).toMatchObject({ - constraint: "file_format_rejected", + ...INVALID_FORMAT, }); }, ); @@ -263,8 +270,7 @@ it.each([0, 1, 2, 4, 12])( const result = await provider.inspect({ path, hash: "header" }); if (result.ok) throw new Error("Invalid alignment accepted"); expect(result.error).toMatchObject({ - _tag: "BinaryTargetError", - constraint: "file_format_rejected", + ...INVALID_FORMAT, }); }, ); @@ -289,8 +295,7 @@ it.each([ const result = await provider.inspect({ path, hash: "header" }); if (result.ok) throw new Error("Invalid metadata key accepted"); expect(result.error).toMatchObject({ - _tag: "BinaryTargetError", - constraint: "file_format_rejected", + ...INVALID_FORMAT, }); }, ); @@ -340,14 +345,18 @@ it("retains the absolute header budget for GGUF without allocating that extent", await file.truncate(100_000_009); const reader = new ModelHeaderReader(file, 100_000_009); expect(() => reader.guarantee(100_000_000)).not.toThrow(); - expect(() => reader.guarantee(100_000_001)).toThrow(ModelFormatFailure); + expect(() => reader.guarantee(100_000_001)).toThrow(ParseFailureError); try { reader.guarantee(100_000_001); } catch (cause) { - expect(cause).toBeInstanceOf(ModelFormatFailure); - if (!(cause instanceof ModelFormatFailure)) + expect(cause).toBeInstanceOf(ParseFailureError); + if (!(cause instanceof ParseFailureError)) throw new Error("Expected a model header capacity failure"); - expect(cause.limit).toBe("header-bytes"); + expect(cause.failure).toMatchObject({ + kind: "limit", + format: "model", + limit: { name: "header-bytes", maximum: 100_000_000 }, + }); expect(cause.message).toContain("100,000,000 bytes"); expect(cause.message).not.toContain("Safetensors"); } @@ -379,7 +388,7 @@ it("separates impossible string lengths from nesting capacity", async () => { const invalid = await providerAt(oversized.bytes); expect( await invalid.provider.inspect({ path: invalid.path, hash: "header" }), - ).toMatchObject({ ok: false, error: { _tag: "BinaryTargetError" } }); + ).toMatchObject({ ok: false, error: INVALID_FORMAT }); for (const [label, bytes, allowance] of [ ["nesting", deep.bytes, "32 array frames"], ] as const) { @@ -656,8 +665,13 @@ it.each(ggufKLayouts)( const rejected = await provider.inspect({ path, hash: "header" }); if (rejected.ok) throw new Error("Incomplete common K row admitted"); expect(rejected.error).toMatchObject({ - constraint: "file_format_rejected", - reason: expect.stringContaining("multiple of 64 elements"), + ...INVALID_FORMAT, + issues: [ + expect.objectContaining({ + reason: "invalid_format", + message: expect.stringContaining("multiple of 64 elements"), + }), + ], }); }, ); @@ -778,8 +792,7 @@ it("validates booleans beyond an array preview without allocating the full value const result = await provider.inspect({ path, hash: "header" }); if (result.ok) throw new Error("Invalid omitted boolean was accepted"); expect(result.error).toMatchObject({ - _tag: "BinaryTargetError", - constraint: "file_format_rejected", + ...INVALID_FORMAT, }); }); @@ -861,9 +874,13 @@ it.each([ const result = await provider.inspect({ path, hash: "header" }); if (result.ok) throw new Error("Invalid omitted UTF-8 string was accepted"); expect(result.error).toMatchObject({ - _tag: "BinaryTargetError", - constraint: "file_format_rejected", - message: expect.stringContaining("GGUF string is not valid UTF-8"), + ...INVALID_FORMAT, + issues: [ + expect.objectContaining({ + reason: "invalid_format", + message: expect.stringContaining("GGUF string is not valid UTF-8"), + }), + ], }); }, ); @@ -1116,14 +1133,13 @@ const malformed = [ ], ] as const; it.each(malformed)( - "rejects %s with typed file_format_rejected", + "rejects %s as an invalid_format path", async (_name, make) => { const { provider, path } = await providerAt(make()); const result = await provider.inspect({ path, hash: "header" }); if (result.ok) throw new Error("Malformed model was accepted"); expect(result.error).toMatchObject({ - _tag: "BinaryTargetError", - constraint: "file_format_rejected", + ...INVALID_FORMAT, }); }, ); @@ -1172,8 +1188,7 @@ it("retains GGUF declarations and metadata when storage overlap is refused", asy const result = await provider.inspect({ path, hash: "header" }); if (result.ok) throw new Error("Overlapping tensors admitted"); expect(result.error).toMatchObject({ - _tag: "BinaryTargetError", - constraint: "file_format_rejected", + ...INVALID_FORMAT, }); const evidence = parseEvidence(result.error.partialObservation); expect(evidence.subject).toBeNull(); @@ -1322,8 +1337,7 @@ it("reports a truncated safetensors header whose length resembles a pickle marke const result = await provider.inspect({ path, hash: "header" }); if (result.ok) throw new Error("Truncated model was accepted"); expect(result.error).toMatchObject({ - _tag: "BinaryTargetError", - constraint: "file_format_rejected", + ...INVALID_FORMAT, }); }); @@ -1333,7 +1347,7 @@ it("bounds expansion before allocation and distinguishes unsupported pickle and const { provider, path, root } = await providerAt(fixture.bytes); const huge = await provider.inspect({ path, hash: "header" }); if (huge.ok) throw new Error("Huge count admitted"); - expect(huge.error._tag).toBe("BinaryTargetError"); + expect(huge.error).toMatchObject(INVALID_FORMAT); await writeFile(path, Buffer.from([0x80, 4, 0, 0])); const pickle = await provider.inspect({ path, hash: "header" }); if (pickle.ok) throw new Error("Pickle admitted"); @@ -1585,7 +1599,7 @@ it.each([false, true])( const { provider, path } = await providerAt(bytes); expect(await provider.inspect({ path, hash: "header" })).toMatchObject({ ok: false, - error: { _tag: "BinaryTargetError" }, + error: INVALID_FORMAT, }); } }, @@ -1606,7 +1620,7 @@ it("rejects impossible safetensors header and data extents before numeric conver const { provider, path } = await providerAt(bytes); expect(await provider.inspect({ path, hash: "header" })).toMatchObject({ ok: false, - error: { _tag: "BinaryTargetError" }, + error: INVALID_FORMAT, }); } }); @@ -1620,7 +1634,7 @@ it("rejects a tensor directory whose old 20-byte estimate fits but fixed fields await truncate(path, 24 + 65537 * 20); expect(await provider.inspect({ path, hash: "header" })).toMatchObject({ ok: false, - error: { _tag: "BinaryTargetError" }, + error: INVALID_FORMAT, }); });