feat: integrate SimpleCADAPI 2.0.2 CAD workflows

This commit is contained in:
Jerry
2026-08-03 11:17:05 +08:00
parent b5738e9109
commit c3a0f269b7
481 changed files with 110229 additions and 12826 deletions
@@ -0,0 +1,87 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
canonicalJsonBytes,
parseCanonicalJson,
parseStrictJson,
validateNormalizedProduct,
withSceneRevision,
} from "../src/index.js";
test("canonical JSON renders finite Numbers from their value", () => {
assert.equal(canonicalJsonBytes(1e30).toString("utf8"), "1e+30");
assert.equal(canonicalJsonBytes(parseStrictJson("1e30")).toString("utf8"), "1e+30");
const nested = parseStrictJson('{"large":1e30,"small":[1E-7]}');
assert.equal(
canonicalJsonBytes(nested).toString("utf8"),
'{"large":1e+30,"small":[1e-7]}',
);
assert.deepEqual(parseCanonicalJson("1e+30"), 1e30);
assert.throws(() => parseCanonicalJson("1e30"), /canonical/);
});
test("canonical JSON normalizes negative zero at root and nested positions", () => {
const root = parseStrictJson("-0");
assert.equal(Object.is(root, -0), true);
assert.equal(canonicalJsonBytes(root).toString("utf8"), "0");
const nested = parseStrictJson('{"values":[-0,0e0]}');
assert.equal(canonicalJsonBytes(nested).toString("utf8"), '{"values":[0,0]}');
assert.throws(() => parseCanonicalJson("-0"), /canonical/);
});
test("cloning parsed values does not change canonical bytes or scene revision", () => {
const scene = parseStrictJson('{"measurement":1e30,"nested":{"epsilon":1e-7}}') as Record<
string,
unknown
>;
const clone = structuredClone(scene);
assert.deepEqual(canonicalJsonBytes(clone), canonicalJsonBytes(scene));
assert.deepEqual(withSceneRevision(clone), withSceneRevision(scene));
});
test("unsafe integer JSON is rejected lexically without affecting exponent Numbers", () => {
assert.throws(() => parseStrictJson("9007199254740992"), /safe domain/);
assert.throws(() => parseStrictJson('{"value":-9007199254740992}'), /safe domain/);
assert.equal(
canonicalJsonBytes(Number.MAX_SAFE_INTEGER + 1).toString("utf8"),
"9007199254740992",
);
assert.equal(canonicalJsonBytes(parseStrictJson("1e30")).toString("utf8"), "1e+30");
});
test("strict JSON rejects unpaired surrogates in values and object keys", () => {
assert.throws(() => parseStrictJson('{"value":"\\ud800"}'), /surrogate/);
assert.throws(() => parseStrictJson('{"\\ud800":0}'), /surrogate/);
});
test("serialized JSON over the depth limit reports a budget issue", () => {
let serialized = "0";
for (let depth = 0; depth < 65; depth += 1) serialized = `{"x":${serialized}}`;
const result = validateNormalizedProduct(serialized);
assert.equal(result.firstError?.code, "resource_limit_exceeded");
assert.equal(result.firstError?.phase, "budget");
assert.equal(result.firstError?.path, "/x".repeat(65));
});
test("serialized non-finite tokens and overflow report nonfinite_json_number", () => {
for (const serialized of [
"NaN",
"Infinity",
"-Infinity",
"1e999",
'[0,NaN]',
'{"value":1e999}',
]) {
const result = validateNormalizedProduct(serialized);
assert.equal(result.firstError?.code, "nonfinite_json_number", serialized);
assert.equal(result.firstError?.phase, "parse", serialized);
}
const programmatic = validateNormalizedProduct({ metadata: { value: Number.NaN } });
assert.equal(programmatic.firstError?.code, "nonfinite_json_number");
assert.equal(programmatic.firstError?.path, "/metadata/value");
});
@@ -0,0 +1,118 @@
import assert from "node:assert/strict";
import { withSceneRevision } from "../src/index.js";
export interface ExpectedIssue { code: string; path: string }
export interface Mutation {
operation: "add" | "delete" | "set";
path: (string | number)[];
value?: unknown;
}
export interface SceneFieldCase {
name: string;
valid: boolean;
expected: ExpectedIssue | null;
mutations: Mutation[];
recompute_revision: boolean;
}
export interface PackageCase {
name: string;
valid: boolean;
expected: ExpectedIssue | null;
manifest_base64: string;
blob_uris: string[];
blob_mutations?: Record<string, string>;
}
export interface SceneShapeCase extends PackageCase {
expected_shape: SceneShapeFacts;
}
export interface SceneShapeFacts {
definition_count: number;
definition_occurrence_counts: Record<string, number>;
edge_asset_count: number;
entity_asset_count: number;
geometry_asset_count: number;
maximum_depth: number;
node_count: number;
root_node_ids: string[];
}
type JsonRecord = Record<string, unknown>;
function asRecord(value: unknown): JsonRecord {
assert.ok(value !== null && typeof value === "object" && !Array.isArray(value));
return value as JsonRecord;
}
function asRecords(value: unknown): JsonRecord[] {
assert.ok(Array.isArray(value));
return value.map(asRecord);
}
export function corpusBlobs(
pool: Record<string, string>,
item: PackageCase,
): Map<string, Uint8Array> {
const blobs = new Map<string, Uint8Array>(
item.blob_uris.map((uri) => [uri, Buffer.from(pool[uri], "base64")]),
);
for (const [uri, payload] of Object.entries(item.blob_mutations ?? {})) {
blobs.set(uri, Buffer.from(payload, "base64"));
}
return blobs;
}
export function applySceneFieldCase(
scene: JsonRecord,
item: SceneFieldCase,
): JsonRecord {
const result = structuredClone(scene);
for (const mutation of item.mutations) {
let current: JsonRecord | unknown[] = result;
for (const part of mutation.path.slice(0, -1)) {
current = Array.isArray(current)
? current[part as number] as JsonRecord | unknown[]
: current[part as string] as JsonRecord | unknown[];
}
const final = mutation.path.at(-1)!;
if (mutation.operation === "delete") {
if (Array.isArray(current)) delete current[final as number];
else delete current[final as string];
} else if (Array.isArray(current)) {
current[final as number] = structuredClone(mutation.value);
} else {
current[final as string] = structuredClone(mutation.value);
}
}
return item.recompute_revision ? withSceneRevision(result) : result;
}
export function sceneShapeFacts(scene: JsonRecord): SceneShapeFacts {
const nodes = asRecords(scene.nodes);
const definitionOccurrenceCounts: Record<string, number> = {};
for (const node of nodes) {
const definitionId = String(node.definition_id);
definitionOccurrenceCounts[definitionId] =
(definitionOccurrenceCounts[definitionId] ?? 0) + 1;
}
return {
definition_count: asRecords(scene.definitions).length,
definition_occurrence_counts: Object.fromEntries(
Object.entries(definitionOccurrenceCounts).sort(([left], [right]) =>
Buffer.compare(Buffer.from(left), Buffer.from(right))
),
),
edge_asset_count: asRecords(scene.edge_assets).length,
entity_asset_count: asRecords(scene.entity_assets).length,
geometry_asset_count: asRecords(scene.geometry_assets).length,
maximum_depth: Math.max(
...nodes.map((node) => {
const source = asRecord(node.source);
return Array.isArray(source.component_path) ? source.component_path.length : 0;
}),
),
node_count: nodes.length,
root_node_ids: nodes
.filter((node) => node.parent_node_id === null)
.map((node) => String(node.node_id)),
};
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,165 @@
import { readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import assert from "node:assert/strict";
import test from "node:test";
import {
ConnectorBindingDocument,
ContractDocumentValidationError,
EntityDocument,
type ImmutableContractDocument,
NormalizedProductDocument,
parseCanonicalJson,
parseStrictJson,
PresentationDocument,
SceneDocument,
sha256,
} from "../src/index.js";
interface ValidationCase {
valid: boolean;
payload_base64: string;
}
interface Corpus {
artifacts: Record<string, { base64: string }>;
presentation_cases: ValidationCase[];
connector_binding_cases: ValidationCase[];
normalized_product_cases: ValidationCase[];
}
interface TestFactory {
fromValue(value: unknown): ImmutableContractDocument<unknown>;
parse(data: Uint8Array | string): ImmutableContractDocument<unknown>;
}
const here = dirname(fileURLToPath(import.meta.url));
const corpus = parseStrictJson(
readFileSync(resolve(here, "../../../test/fixtures/scene-contract/corpus.json")),
) as Corpus;
const validCase = (cases: ValidationCase[]): Buffer => {
const item = cases.find((candidate) => candidate.valid);
assert.ok(item !== undefined);
return Buffer.from(item.payload_base64, "base64");
};
const documents: readonly [string, TestFactory, Buffer][] = [
[
"scene",
SceneDocument as unknown as TestFactory,
Buffer.from(corpus.artifacts.scene.base64, "base64"),
],
[
"entities",
EntityDocument as unknown as TestFactory,
Buffer.from(corpus.artifacts.entities.base64, "base64"),
],
[
"presentation",
PresentationDocument as unknown as TestFactory,
validCase(corpus.presentation_cases),
],
[
"connector binding",
ConnectorBindingDocument as unknown as TestFactory,
validCase(corpus.connector_binding_cases),
],
[
"normalized product",
NormalizedProductDocument as unknown as TestFactory,
validCase(corpus.normalized_product_cases),
],
];
function record(value: unknown): Record<string, unknown> {
assert.ok(value !== null && typeof value === "object" && !Array.isArray(value));
return value as Record<string, unknown>;
}
function assertDeepFrozen(value: unknown): void {
if (value === null || typeof value !== "object") return;
assert.equal(Object.isFrozen(value), true);
for (const child of Object.values(value)) assertDeepFrozen(child);
}
function nestedContainer(value: Record<string, unknown>): Record<string, unknown> | unknown[] {
const result = Object.values(value).find(
(child) => child !== null && typeof child === "object",
);
assert.ok(result !== undefined);
return result as Record<string, unknown> | unknown[];
}
test("all contract document factories construct, parse, validate, and round trip", () => {
for (const [name, factory, payload] of documents) {
const input = record(parseCanonicalJson(payload));
const constructed = factory.fromValue(input);
const parsed = factory.parse(payload);
assert.deepEqual(constructed.canonicalBytes, payload, name);
assert.deepEqual(parsed.canonicalBytes, payload, name);
assert.equal(constructed.canonicalHash, sha256(payload), name);
assert.deepEqual(constructed.toMutable(), parsed.toMutable(), name);
assert.deepEqual(factory.fromValue(constructed.value).canonicalBytes, payload, name);
}
});
test("contract documents resist nested mutation and isolate mutable copies", () => {
for (const [name, factory, payload] of documents) {
const input = record(parseCanonicalJson(payload));
const document = factory.fromValue(input);
const expectedHash = document.canonicalHash;
const expectedBytes = document.canonicalBytes;
assert.equal(Object.isFrozen(document), true, name);
assertDeepFrozen(document.value);
assert.throws(() => {
record(document.value).mutation = true;
}, TypeError, name);
const frozenNested = nestedContainer(record(document.value));
assert.throws(() => {
if (Array.isArray(frozenNested)) frozenNested.push(null);
else frozenNested.mutation = true;
}, TypeError, name);
input.mutation = true;
const firstCopy = record(document.toMutable());
const secondCopy = record(document.toMutable());
const mutableNested = nestedContainer(firstCopy);
if (Array.isArray(mutableNested)) mutableNested.push(null);
else mutableNested.mutation = true;
firstCopy.mutation = true;
assert.equal(Object.hasOwn(record(document.value), "mutation"), false, name);
assert.equal(Object.hasOwn(secondCopy, "mutation"), false, name);
assert.notDeepEqual(firstCopy, secondCopy, name);
const exposedBytes = document.canonicalBytes;
exposedBytes[0] ^= 0xff;
assert.deepEqual(document.canonicalBytes, expectedBytes, name);
assert.equal(document.canonicalHash, expectedHash, name);
}
});
test("document constructors reject invalid values, noncanonical input, and stale revisions", () => {
for (const [name, factory] of documents) {
assert.throws(
() => factory.fromValue({}),
ContractDocumentValidationError,
name,
);
}
assert.throws(() => SceneDocument.parse('{ "schema_version": "1.0" }'), /canonical/);
const scene = record(
parseCanonicalJson(Buffer.from(corpus.artifacts.scene.base64, "base64")),
);
scene.revision = `sha256:${"0".repeat(64)}`;
assert.throws(
() => SceneDocument.fromValue(scene as never),
(error: unknown) =>
error instanceof ContractDocumentValidationError &&
error.report.firstError?.code === "revision_mismatch",
);
});
@@ -0,0 +1,262 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import test from "node:test";
import { fileURLToPath } from "node:url";
import {
canonicalJsonBytes,
computeSceneRevision,
parseStrictJson,
sha256,
} from "../src/canonical.js";
import { report } from "../src/report.js";
import {
computePackageBudgetTotals,
packageBudgetIssues,
validateSceneManifest,
validateScenePackage,
} from "../src/validation.js";
type JsonRecord = Record<string, unknown>;
interface Artifact {
base64: string;
uri?: string;
}
interface Corpus {
artifacts: Record<string, Artifact>;
presentation_cases: { valid: boolean; payload_base64: string }[];
}
const here = dirname(fileURLToPath(import.meta.url));
const corpusPath = resolve(here, "../../../test/fixtures/scene-contract/corpus.json");
const corpus = parseStrictJson(readFileSync(corpusPath)) as Corpus;
const decode = (value: string): Buffer => Buffer.from(value, "base64");
function asRecord(value: unknown): JsonRecord {
assert.ok(value !== null && typeof value === "object" && !Array.isArray(value));
return value as JsonRecord;
}
function asRecords(value: unknown): JsonRecord[] {
assert.ok(Array.isArray(value));
return value.map(asRecord);
}
function validScene(): JsonRecord {
return asRecord(parseStrictJson(decode(corpus.artifacts.scene.base64)));
}
function artifactBlobs(): Map<string, Buffer> {
const result = new Map<string, Buffer>();
for (const artifact of Object.values(corpus.artifacts)) {
if (artifact.uri !== undefined) result.set(artifact.uri, decode(artifact.base64));
}
return result;
}
function validPresentation(): JsonRecord {
const value = corpus.presentation_cases.find((item) => item.valid);
assert.ok(value !== undefined);
return asRecord(parseStrictJson(decode(value.payload_base64)));
}
function utf8Compare(left: string, right: string): number {
return Buffer.compare(Buffer.from(left, "utf8"), Buffer.from(right, "utf8"));
}
function attachPresentation(scene: JsonRecord, blobs: Map<string, Buffer>): { presentation: JsonRecord; blobs: Map<string, Buffer> } {
const presentation = validPresentation();
const presentationId = presentation.presentation_id as string;
const expectedAppearanceIds = new Map<unknown, string>();
const sceneAppearances = asRecords(scene.appearances);
for (const authored of asRecords(presentation.appearances)) {
const evaluated: JsonRecord = {
alpha_mode: authored.alpha_mode,
base_color: authored.base_color,
double_sided: authored.double_sided,
edge_color: authored.edge_color,
metallic: authored.metallic,
name: authored.name,
roughness: authored.roughness,
sdk_metadata: {},
source: {
appearance_name: authored.name,
kind: "presentation",
presentation_id: presentationId,
},
};
const appearanceId = `appearance/evaluated/${sha256(canonicalJsonBytes(evaluated)).slice("sha256:".length)}`;
expectedAppearanceIds.set(authored.name, appearanceId);
sceneAppearances.push({ appearance_id: appearanceId, ...evaluated });
}
sceneAppearances.sort((left, right) => utf8Compare(left.appearance_id as string, right.appearance_id as string));
scene.appearances = sceneAppearances;
const nodeMap = new Map(asRecords(scene.nodes).map((node) => [node.node_id, node]));
for (const override of asRecords(presentation.node_overrides)) {
const node = nodeMap.get(override.node_id);
assert.ok(node !== undefined);
if (override.visible !== undefined) node.visible = override.visible;
if (override.appearance_name !== undefined) node.appearance_override_id = expectedAppearanceIds.get(override.appearance_name);
}
scene.cameras = asRecords(presentation.cameras).map((camera) => ({
camera_id: `camera/${presentationId}/${String(camera.name)}`,
...camera,
}));
const presentationBytes = canonicalJsonBytes(presentation);
asRecord(scene.compile_options).embed_presentation = true;
scene.presentation_source = {
artifact_hash: sha256(presentationBytes),
embedded_artifact_byte_length: presentationBytes.length,
embedded_artifact_uri: "presentation/presentation.json",
presentation_id: presentationId,
schema_version: "1.0",
};
scene.revision = computeSceneRevision(scene);
const resultBlobs = new Map(blobs);
resultBlobs.set("presentation/presentation.json", presentationBytes);
return { presentation, blobs: resultBlobs };
}
function replacePresentation(scene: JsonRecord, blobs: Map<string, Buffer>, payload: Buffer): Map<string, Buffer> {
const source = asRecord(scene.presentation_source);
source.artifact_hash = sha256(payload);
source.embedded_artifact_byte_length = payload.length;
scene.revision = computeSceneRevision(scene);
const result = new Map(blobs);
result.set("presentation/presentation.json", payload);
return result;
}
test("package budget formula and first error match Python with declarative limits", () => {
const totals = computePackageBudgetTotals({
sceneJsonBytes: 7,
glbDecodedBufferBytes: 11,
entityJsonBytes: 13,
otherImmutableJsonBytes: 17,
entityCount: 23,
entityVertexCount: 19,
triangleVertexCount: 29,
triangleCount: 31,
lineVertexCount: 37,
lineSegmentCount: 41,
});
assert.deepEqual(totals, {
staticDecodedBufferBytes: 656,
entityCount: 23,
triangleVertexTotal: 29,
triangleTotal: 31,
lineVertexTotal: 37,
lineSegmentTotal: 41,
});
assert.deepEqual(packageBudgetIssues(totals, {
static_decoded_buffer_bytes: 656,
entities_total: 23,
triangle_vertices_total: 29,
triangles_total: 31,
line_vertices_total: 37,
line_segments_total: 41,
}), []);
const result = report(packageBudgetIssues(totals, {
static_decoded_buffer_bytes: 655,
entities_total: 22,
triangle_vertices_total: 28,
triangles_total: 30,
line_vertices_total: 36,
line_segments_total: 40,
}), "package");
assert.deepEqual(
result.firstError === null
? null
: [result.firstError.code, result.firstError.path, result.firstError.message],
["resource_limit_exceeded", "", "total line GLB vertex count exceeds resource limit"],
);
});
test("embedded presentation must be canonical and resolve exactly against the scene", () => {
const scene = validScene();
const attached = attachPresentation(scene, artifactBlobs());
const validResult = validateScenePackage(canonicalJsonBytes(scene), attached.blobs);
assert.equal(validResult.valid, true, JSON.stringify(validResult.issues));
const noncanonical = Buffer.from(JSON.stringify(attached.presentation, null, 2));
const blobs = replacePresentation(scene, attached.blobs, noncanonical);
const result = validateScenePackage(canonicalJsonBytes(scene), blobs);
assert.deepEqual(
result.firstError === null ? null : [result.firstError.code, result.firstError.path],
["noncanonical_json", ""],
);
});
test("embedded presentation IDs and scene references are contextual", async (context) => {
const cases: [string, (presentation: JsonRecord) => void, string, string][] = [
["presentation ID", (presentation) => { presentation.presentation_id = "other"; }, "source_matrix_invalid", "/presentation/presentation.json/presentation_id"],
["source scene ID", (presentation) => { presentation.source_scene_id = "other"; }, "source_matrix_invalid", "/presentation/presentation.json/source_scene_id"],
["node override", (presentation) => { asRecords(presentation.node_overrides)[0].node_id = "instance/missing"; }, "source_matrix_invalid", "/presentation/presentation.json/node_overrides/0/node_id"],
["camera parent", (presentation) => { asRecords(presentation.cameras)[0].parent_node_id = "instance/missing"; }, "source_matrix_invalid", "/presentation/presentation.json/cameras/0/parent_node_id"],
];
for (const [name, mutate, code, path] of cases) await context.test(name, () => {
const scene = validScene();
const attached = attachPresentation(scene, artifactBlobs());
mutate(attached.presentation);
const blobs = replacePresentation(scene, attached.blobs, canonicalJsonBytes(attached.presentation));
const result = validateScenePackage(canonicalJsonBytes(scene), blobs);
assert.ok(result.issues.some((item) => item.code === code && item.path === path));
});
});
test("presentation appearance provenance and camera resolution are exact", () => {
const scene = validScene();
const attached = attachPresentation(scene, artifactBlobs());
const sceneAppearances = asRecords(scene.appearances);
const appearance = sceneAppearances.find((item) => item.source !== null && asRecord(item.source).kind === "presentation");
assert.ok(appearance !== undefined);
asRecord(appearance.source).appearance_name = "Other";
const draft = { ...appearance };
delete draft.appearance_id;
const appearanceId = `appearance/evaluated/${sha256(canonicalJsonBytes(draft)).slice("sha256:".length)}`;
appearance.appearance_id = appearanceId;
asRecords(scene.nodes)[0].appearance_override_id = appearanceId;
sceneAppearances.sort((left, right) => utf8Compare(left.appearance_id as string, right.appearance_id as string));
scene.appearances = sceneAppearances;
scene.cameras = [];
scene.revision = computeSceneRevision(scene);
const result = validateScenePackage(canonicalJsonBytes(scene), attached.blobs);
const appearanceIndex = asRecords(scene.appearances).indexOf(appearance);
assert.ok(result.issues.some((item) => item.code === "source_matrix_invalid" && item.path === `/appearances/${appearanceIndex}/source/appearance_name`));
assert.ok(result.issues.some((item) => item.code === "source_matrix_invalid" && item.path === "/presentation/presentation.json/cameras/0"));
});
test("product material provenance must match a Part definition root", () => {
const scene = validScene();
const appearance = asRecords(scene.appearances)[0];
appearance.source = { kind: "product_material", material_id: "steel", root_id: "other" };
const draft = { ...appearance };
delete draft.appearance_id;
const appearanceId = `appearance/evaluated/${sha256(canonicalJsonBytes(draft)).slice("sha256:".length)}`;
appearance.appearance_id = appearanceId;
asRecords(scene.definitions)[0].appearance_id = appearanceId;
scene.revision = computeSceneRevision(scene);
const result = validateSceneManifest(scene);
assert.ok(result.issues.some((item) => item.code === "source_matrix_invalid" && item.path === "/appearances/0/source/root_id"));
});
test("one URI cannot declare contradictory hash, length, or media roles", () => {
const scene = validScene();
const edge = asRecords(scene.edge_assets)[0];
const oldUri = edge.uri as string;
edge.uri = asRecords(scene.geometry_assets)[0].uri;
scene.revision = computeSceneRevision(scene);
const blobs = artifactBlobs();
blobs.delete(oldUri);
const result = validateScenePackage(canonicalJsonBytes(scene), blobs);
assert.ok(result.issues.some((item) => item.code === "package_member_set_invalid"));
assert.ok(result.issues.some((item) => item.code === "source_matrix_invalid" && item.path === "/edge_assets/0/uri"));
});
@@ -0,0 +1,81 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import test from "node:test";
import { fileURLToPath } from "node:url";
import { issue, parseStrictJson, report, validateRuleRegistry } from "../src/index.js";
const contractRoot = resolve(
dirname(fileURLToPath(import.meta.url)),
"../../../src/simplecadapi/scene/contracts",
);
function registry(): Record<string, unknown> {
return structuredClone(
parseStrictJson(readFileSync(resolve(contractRoot, "rules/scene-1.0-rules.json"))),
) as Record<string, unknown>;
}
function rules(value: Record<string, unknown>): Record<string, unknown>[] {
assert.ok(Array.isArray(value.rules));
return value.rules as Record<string, unknown>[];
}
test("rule registry rejects malformed shape, duplicates, and wrong ordering", () => {
const valid = registry();
assert.equal(validateRuleRegistry(valid).size, rules(valid).length);
const missingPrecedence = registry();
delete rules(missingPrecedence)[0].precedence;
assert.throws(() => validateRuleRegistry(missingPrecedence), /precedence/);
const duplicateId = registry();
rules(duplicateId)[1].id = rules(duplicateId)[0].id;
assert.throws(() => validateRuleRegistry(duplicateId), /duplicate rule ID/);
const duplicatePrecedence = registry();
rules(duplicatePrecedence)[1].precedence = 0;
assert.throws(() => validateRuleRegistry(duplicatePrecedence), /duplicate precedence/);
const wrongOrder = registry();
[rules(wrongOrder)[0], rules(wrongOrder)[1]] = [rules(wrongOrder)[1], rules(wrongOrder)[0]];
assert.throws(() => validateRuleRegistry(wrongOrder), /ordered by phase and precedence/);
const wrongIdOrder = registry();
[rules(wrongIdOrder)[0].id, rules(wrongIdOrder)[1].id] = [rules(wrongIdOrder)[1].id, rules(wrongIdOrder)[0].id];
assert.throws(() => validateRuleRegistry(wrongIdOrder), /unsigned UTF-8 rule-ID order/);
});
test("registered precedence orders artifact reports", () => {
const result = report([
issue("transform_invalid", "/z", "later registered rule"),
issue("bounds_invalid", "/z", "z message"),
issue("bounds_invalid", "/z", "a message"),
issue("bounds_invalid", "/a", "earlier pointer"),
], "scene");
assert.deepEqual(result.issues.map(({ code, path, message }) => [code, path, message]), [
["bounds_invalid", "/a", "earlier pointer"],
["bounds_invalid", "/z", "a message"],
["bounds_invalid", "/z", "z message"],
["transform_invalid", "/z", "later registered rule"],
]);
});
test("artifact reports reject unregistered, wrong-phase, and wrong-artifact issues", () => {
assert.throws(
() => report([issue("not_registered", "", "no authority")], "scene"),
/unregistered/,
);
assert.throws(
() => report([issue("bounds_invalid", "", "wrong phase", "package")], "scene"),
/registered phase/,
);
assert.throws(
() => report([issue("presentation_reference_invalid", "", "wrong artifact")], "scene"),
/does not apply/,
);
assert.throws(
() => report([issue("duplicate_json_key", "/value", "wrong pointer", "parse")], "scene"),
/root pointer/,
);
});
@@ -0,0 +1,91 @@
import assert from "node:assert/strict";
import test from "node:test";
import { BASE_LIMITS, preflightZipBytes } from "../src/index.js";
function declaredDeflateZip(
compressed: Uint8Array,
uncompressedSize: number,
localUncompressedSize = uncompressedSize,
): Buffer {
const name = Buffer.from("scene.json", "ascii");
const local = Buffer.alloc(30);
local.writeUInt32LE(0x04034b50, 0);
local.writeUInt16LE(20, 4);
local.writeUInt16LE(0x0800, 6);
local.writeUInt16LE(8, 8);
local.writeUInt16LE(0, 10);
local.writeUInt16LE(0x0021, 12);
local.writeUInt32LE(0, 14);
local.writeUInt32LE(compressed.byteLength, 18);
local.writeUInt32LE(localUncompressedSize, 22);
local.writeUInt16LE(name.length, 26);
local.writeUInt16LE(0, 28);
const centralOffset = local.length + name.length + compressed.byteLength;
const central = Buffer.alloc(46);
central.writeUInt32LE(0x02014b50, 0);
central.writeUInt16LE(0x0314, 4);
central.writeUInt16LE(20, 6);
central.writeUInt16LE(0x0800, 8);
central.writeUInt16LE(8, 10);
central.writeUInt16LE(0, 12);
central.writeUInt16LE(0x0021, 14);
central.writeUInt32LE(0, 16);
central.writeUInt32LE(compressed.byteLength, 20);
central.writeUInt32LE(uncompressedSize, 24);
central.writeUInt16LE(name.length, 28);
central.writeUInt16LE(0, 30);
central.writeUInt16LE(0, 32);
central.writeUInt16LE(0, 34);
central.writeUInt16LE(0, 36);
central.writeUInt32LE(0x81a40000, 38);
central.writeUInt32LE(0, 42);
const eocd = Buffer.alloc(22);
eocd.writeUInt32LE(0x06054b50, 0);
eocd.writeUInt16LE(0, 4);
eocd.writeUInt16LE(0, 6);
eocd.writeUInt16LE(1, 8);
eocd.writeUInt16LE(1, 10);
eocd.writeUInt32LE(central.length + name.length, 12);
eocd.writeUInt32LE(centralOffset, 16);
eocd.writeUInt16LE(0, 20);
return Buffer.concat([local, name, compressed, central, name, eocd]);
}
function assertError(archive: Uint8Array, expected: string): void {
assert.throws(() => preflightZipBytes(archive), (error) => {
assert.equal((error as Error).message, expected);
return true;
});
}
test("aggregate ratio uses cross-checked declarations before inflate", () => {
const compressed = Buffer.from([0]);
const template = declaredDeflateZip(compressed, 0);
const exactSize = template.length * BASE_LIMITS.compression_ratio;
assertError(
declaredDeflateZip(compressed, exactSize),
'member compression ratio exceeds limit: "scene.json"',
);
assertError(
declaredDeflateZip(compressed, exactSize + 1),
"aggregate compression ratio exceeds limit",
);
assertError(
declaredDeflateZip(compressed, exactSize + 1, exactSize),
"ZIP central/local header mismatch",
);
});
test("zero compressed size uses one as the member ratio denominator", () => {
assertError(
declaredDeflateZip(Buffer.alloc(0), BASE_LIMITS.compression_ratio),
'invalid deflate stream: "scene.json"',
);
assertError(
declaredDeflateZip(Buffer.alloc(0), BASE_LIMITS.compression_ratio + 1),
'member compression ratio exceeds limit: "scene.json"',
);
});