1098 lines
45 KiB
TypeScript
1098 lines
45 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { readFileSync } from "node:fs";
|
|
import { dirname, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import assert from "node:assert/strict";
|
|
import test, { type TestContext } from "node:test";
|
|
import {
|
|
canonicalJsonBytes,
|
|
canonicalZipBytes,
|
|
BASE_LIMITS,
|
|
computeSceneRevision,
|
|
DuplicateKeyError,
|
|
parseCanonicalJson,
|
|
parseStrictJson,
|
|
preflightAggregateCompressionRatio,
|
|
preflightArchiveMemberSizes,
|
|
preflightGlb,
|
|
preflightGlbCounts,
|
|
preflightInputArchiveSize,
|
|
preflightMemberCompressionRatio,
|
|
preflightZipBytes,
|
|
profileCross,
|
|
profileF32Bits,
|
|
profileNormalize,
|
|
issue,
|
|
report,
|
|
sha256,
|
|
validateConnectorBinding,
|
|
validateEntityAsset,
|
|
validateNormalizedProduct,
|
|
validatePresentation,
|
|
validateSceneManifest,
|
|
validateScenePackage,
|
|
computePackageBudgetTotals,
|
|
jsonResourceIssues,
|
|
packageBudgetIssues,
|
|
resourceCountIssues,
|
|
withSceneRevision,
|
|
type SceneResourceLimits,
|
|
} from "../src/index.js";
|
|
import { validateSceneSchema, validateSchemaPointer, type SchemaArtifact } from "../src/schema.js";
|
|
import {
|
|
applySceneFieldCase,
|
|
corpusBlobs,
|
|
sceneShapeFacts,
|
|
type ExpectedIssue,
|
|
type PackageCase,
|
|
type SceneFieldCase,
|
|
type SceneShapeCase,
|
|
} from "./corpus-support.js";
|
|
|
|
interface ValidationCase { name: string; valid: boolean; expected: ExpectedIssue | null; payload_base64: string }
|
|
interface GlbCase { name: string; valid: boolean; error: string | null; expected_kind: "triangle" | "line"; kind: "triangle" | "line" | null; payload_base64: string }
|
|
interface ZipCase { name: string; valid: boolean; error: string | null; used_deflate: boolean | null; payload_base64: string }
|
|
interface NumericVector { name: string; operation: "f32" | "cross" | "normalize"; input_bits: string[] | string[][]; expected_bits?: string | string[]; error?: string; valid: boolean }
|
|
interface ResourceCase { name: string; operation: string; parameters: Record<string, unknown>; valid: boolean; error?: string | null; expected?: ExpectedIssue | null }
|
|
interface Artifact { base64: string; sha256: string; uri?: string; revision?: string }
|
|
interface RevisionVector { name: string; canonical_base64: string; draft_base64: string; revision: string; sha256: string }
|
|
interface SchemaMutation { operation: "delete" | "set"; path: (string | number)[]; value?: unknown }
|
|
interface SchemaFieldMatrix {
|
|
artifact: SchemaArtifact;
|
|
base: Record<string, unknown>;
|
|
cases: { mutations: SchemaMutation[]; name: string; valid: boolean }[];
|
|
schema_pointer: string;
|
|
variant: string;
|
|
}
|
|
interface Corpus {
|
|
format_version: string;
|
|
artifacts: Record<string, Artifact>;
|
|
blobs: Record<string, string>;
|
|
jcs_vectors: { name: string; input: unknown; canonical_base64: string; sha256: string }[];
|
|
manifest_cases: ValidationCase[];
|
|
entity_cases: ValidationCase[];
|
|
presentation_cases: ValidationCase[];
|
|
connector_binding_cases: ValidationCase[];
|
|
normalized_product_cases: ValidationCase[];
|
|
package_cases: PackageCase[];
|
|
glb_cases: GlbCase[];
|
|
numeric_vectors: NumericVector[];
|
|
resource_cases: ResourceCase[];
|
|
resource_limits: Record<string, number>;
|
|
revision_vectors: RevisionVector[];
|
|
scene_field_cases: SceneFieldCase[];
|
|
scene_shape_cases: SceneShapeCase[];
|
|
schema_field_matrices: SchemaFieldMatrix[];
|
|
zip_cases: ZipCase[];
|
|
}
|
|
|
|
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.from(value, "base64");
|
|
|
|
test("shared corpus has every Scene 1.0 parity section", () => {
|
|
assert.equal(corpus.format_version, "1.0");
|
|
for (const section of [
|
|
corpus.jcs_vectors,
|
|
corpus.manifest_cases,
|
|
corpus.entity_cases,
|
|
corpus.presentation_cases,
|
|
corpus.connector_binding_cases,
|
|
corpus.normalized_product_cases,
|
|
corpus.package_cases,
|
|
corpus.glb_cases,
|
|
corpus.numeric_vectors,
|
|
corpus.resource_cases,
|
|
corpus.revision_vectors,
|
|
corpus.scene_field_cases,
|
|
corpus.scene_shape_cases,
|
|
corpus.schema_field_matrices,
|
|
corpus.zip_cases,
|
|
]) assert.ok(section.length > 0);
|
|
});
|
|
|
|
function applySchemaMutations(base: Record<string, unknown>, mutations: SchemaMutation[]): Record<string, unknown> {
|
|
const result = structuredClone(base);
|
|
for (const mutation of mutations) {
|
|
let current: Record<string | number, unknown> = result;
|
|
for (const part of mutation.path.slice(0, -1)) current = current[part] as Record<string | number, unknown>;
|
|
const field = mutation.path.at(-1)!;
|
|
if (mutation.operation === "delete") delete current[field];
|
|
else current[field] = structuredClone(mutation.value);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
test("all closed schema records and discriminator variants replay shared field matrices", async (context) => {
|
|
for (const matrix of corpus.schema_field_matrices) await context.test(
|
|
`${matrix.artifact}:${matrix.schema_pointer}:${matrix.variant}`,
|
|
async (matrixContext) => {
|
|
for (const item of matrix.cases) await matrixContext.test(item.name, () => {
|
|
const value = applySchemaMutations(matrix.base, item.mutations);
|
|
assert.equal(validateSchemaPointer(matrix.artifact, matrix.schema_pointer, value), item.valid);
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
test("shared positive case names cannot drift to negative expectations", () => {
|
|
const positiveNames: string[] = [];
|
|
for (const [sectionName, value] of Object.entries(corpus)) {
|
|
if (!Array.isArray(value)) continue;
|
|
for (const item of value) {
|
|
if (item === null || typeof item !== "object" || Array.isArray(item)) continue;
|
|
const record = item as Record<string, unknown>;
|
|
if (
|
|
typeof record.name !== "string"
|
|
|| (!record.name.startsWith("valid_") && record.name !== "nullable_fields_nonnull")
|
|
) continue;
|
|
const label = `${sectionName}:${record.name}`;
|
|
positiveNames.push(label);
|
|
assert.equal(record.valid, true, label);
|
|
if (Object.hasOwn(record, "expected")) assert.equal(record.expected, null, label);
|
|
if (Object.hasOwn(record, "error")) assert.equal(record.error, null, label);
|
|
}
|
|
}
|
|
assert.equal(positiveNames.length, 30);
|
|
});
|
|
|
|
test("strict JSON rejects duplicate keys and protects ordinary object semantics", () => {
|
|
assert.throws(() => parseStrictJson(Buffer.from('{"a":1,"a":2}')), DuplicateKeyError);
|
|
const value = parseStrictJson(Buffer.from('{"__proto__":{"polluted":true}}')) as Record<string, unknown>;
|
|
assert.equal(Object.hasOwn(value, "__proto__"), true);
|
|
assert.equal(({} as { polluted?: boolean }).polluted, undefined);
|
|
});
|
|
|
|
test("canonical JSON accepts finite numbers and rejects non-finite numbers", () => {
|
|
assert.equal(canonicalJsonBytes(1e30).toString("utf8"), "1e+30");
|
|
for (const value of [Number.NaN, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY]) {
|
|
assert.throws(() => canonicalJsonBytes(value), /finite/);
|
|
}
|
|
assert.equal(canonicalJsonBytes(1.25).toString("utf8"), "1.25");
|
|
});
|
|
|
|
test("strict parsing accepts valid noncanonical JSON while canonical parsing rejects it", () => {
|
|
const bytes = Buffer.from('{"b":1, "a":2}');
|
|
assert.equal((parseStrictJson(bytes) as Record<string, unknown>).a, 2);
|
|
assert.throws(() => parseCanonicalJson(bytes), /canonical/);
|
|
});
|
|
|
|
test("public validator resource overflows remain budget issues", () => {
|
|
const scene = validScene();
|
|
const sceneBytes = canonicalJsonBytes(scene);
|
|
const collectionResult = validateSceneManifest(scene, { ...BASE_LIMITS, definitions: 0 });
|
|
const serializedResult = validateSceneManifest(scene, {
|
|
...BASE_LIMITS,
|
|
scene_json_bytes: sceneBytes.length - 1,
|
|
});
|
|
|
|
assert.deepEqual(
|
|
collectionResult.firstError === null
|
|
? null
|
|
: [collectionResult.firstError.code, collectionResult.firstError.phase, collectionResult.firstError.path],
|
|
["resource_limit_exceeded", "budget", "/definitions"],
|
|
);
|
|
assert.deepEqual(
|
|
serializedResult.firstError === null
|
|
? null
|
|
: [serializedResult.firstError.code, serializedResult.firstError.phase, serializedResult.firstError.path],
|
|
["resource_limit_exceeded", "budget", ""],
|
|
);
|
|
});
|
|
|
|
test("reports are deterministic by phase, UTF-8 rule ID, and UTF-8 pointer", () => {
|
|
const result = report([
|
|
issue("z", "/b", "later path", "semantic"),
|
|
issue("a", "/z", "later code", "semantic"),
|
|
issue("a", "/a", "earlier pointer", "semantic"),
|
|
issue("z", "/z", "structure first", "structure"),
|
|
issue("a", "/a", "duplicate is ignored", "semantic"),
|
|
]);
|
|
assert.deepEqual(
|
|
result.issues.map(({ phase, code, path }) => [phase, code, path]),
|
|
[
|
|
["structure", "z", "/z"],
|
|
["semantic", "a", "/a"],
|
|
["semantic", "a", "/z"],
|
|
["semantic", "z", "/b"],
|
|
],
|
|
);
|
|
});
|
|
|
|
test("RFC 8785 vectors reproduce exact bytes and hashes", async (context) => {
|
|
for (const vector of corpus.jcs_vectors) await context.test(vector.name, () => {
|
|
const canonical = canonicalJsonBytes(vector.input);
|
|
assert.deepEqual(canonical, decode(vector.canonical_base64));
|
|
assert.equal(sha256(canonical), vector.sha256);
|
|
});
|
|
});
|
|
|
|
test("scene artifact revision and content hash are exact", () => {
|
|
const bytes = decode(corpus.artifacts.scene.base64);
|
|
const scene = parseStrictJson(bytes) as Record<string, unknown>;
|
|
assert.equal(computeSceneRevision(scene), corpus.artifacts.scene.revision);
|
|
assert.equal(sha256(bytes), corpus.artifacts.scene.sha256);
|
|
});
|
|
|
|
test("all manifest cases match validity and expected first issue", async (context) => {
|
|
for (const item of corpus.manifest_cases) await context.test(item.name, () => {
|
|
const result = validateSceneManifest(decode(item.payload_base64));
|
|
assert.equal(result.valid, item.valid);
|
|
if (item.expected !== null) {
|
|
assert.equal(result.firstError?.code, item.expected.code);
|
|
assert.equal(result.firstError?.path, item.expected.path);
|
|
}
|
|
});
|
|
});
|
|
|
|
test("all entity cases match validity and expected first issue", async (context) => {
|
|
for (const item of corpus.entity_cases) await context.test(item.name, () => {
|
|
const result = validateEntityAsset(decode(item.payload_base64));
|
|
assert.equal(result.valid, item.valid);
|
|
if (item.expected !== null) {
|
|
assert.equal(result.firstError?.code, item.expected.code);
|
|
assert.equal(result.firstError?.path, item.expected.path);
|
|
}
|
|
});
|
|
});
|
|
|
|
async function assertValidationCases(
|
|
context: TestContext,
|
|
cases: ValidationCase[],
|
|
validate: (value: unknown) => ReturnType<typeof validateSceneManifest>,
|
|
): Promise<void> {
|
|
for (const item of cases) await context.test(item.name, () => {
|
|
const result = validate(decode(item.payload_base64));
|
|
assert.equal(result.valid, item.valid);
|
|
if (item.expected !== null) {
|
|
assert.equal(result.firstError?.code, item.expected.code);
|
|
assert.equal(result.firstError?.path, item.expected.path);
|
|
}
|
|
});
|
|
}
|
|
|
|
test("all presentation cases match validity and expected first issue", async (context) => {
|
|
await assertValidationCases(context, corpus.presentation_cases, validatePresentation);
|
|
});
|
|
|
|
test("all connector binding cases match validity and expected first issue", async (context) => {
|
|
await assertValidationCases(context, corpus.connector_binding_cases, validateConnectorBinding);
|
|
});
|
|
|
|
test("all normalized product cases match validity and expected first issue", async (context) => {
|
|
await assertValidationCases(context, corpus.normalized_product_cases, validateNormalizedProduct);
|
|
});
|
|
|
|
test("presentation rejects a non-rigid camera transform", () => {
|
|
const valid = corpus.presentation_cases.find((item) => item.valid);
|
|
assert.ok(valid !== undefined);
|
|
const presentation = asRecord(parseStrictJson(decode(valid.payload_base64)));
|
|
const camera = asRecords(presentation.cameras)[0];
|
|
asRecord(camera.transform).x_axis = [2, 0, 0];
|
|
const result = validatePresentation(presentation);
|
|
assert.equal(result.firstError?.code, "transform_invalid");
|
|
assert.equal(result.firstError?.path, "/cameras/0/transform");
|
|
});
|
|
|
|
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;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
function validScene(): JsonRecord {
|
|
return asRecord(parseStrictJson(decode(corpus.artifacts.scene.base64)));
|
|
}
|
|
|
|
function validEntities(): JsonRecord {
|
|
return asRecord(parseStrictJson(decode(corpus.artifacts.entities.base64)));
|
|
}
|
|
|
|
function operationSource(path: string, callText: string): JsonRecord {
|
|
const source: JsonRecord = {
|
|
schema_version: "1.0",
|
|
path,
|
|
path_kind: "project_relative",
|
|
line: 1,
|
|
column: 7,
|
|
end_line: 1,
|
|
end_column: 7 + Array.from(callText).length,
|
|
call_text: callText,
|
|
callsite_id: "",
|
|
assignment_targets: ["body"],
|
|
};
|
|
const material = [
|
|
source.path,
|
|
source.line,
|
|
source.column,
|
|
source.end_line,
|
|
source.end_column,
|
|
source.call_text,
|
|
].map(String).join("\x1f");
|
|
source.callsite_id = `callsite_${sha256(Buffer.from(material, "utf8")).slice("sha256:".length, "sha256:".length + 16)}`;
|
|
return source;
|
|
}
|
|
|
|
function embeddedSourcePackage(): {
|
|
blobs: Map<string, Buffer>;
|
|
manifest: JsonRecord;
|
|
model: JsonRecord;
|
|
sourcePath: string;
|
|
sourceUri: string;
|
|
} {
|
|
const scene = validScene();
|
|
const sidecar = validEntities();
|
|
makeModelScene(scene, sidecar, "shape");
|
|
|
|
const definition = asRecords(scene.definitions)[0];
|
|
const definitionId = "definition/root/shape/model/graph/node/0";
|
|
definition.definition_id = definitionId;
|
|
asRecords(scene.nodes)[0].definition_id = definitionId;
|
|
sidecar.definition_id = definitionId;
|
|
|
|
const callText = "scad.make_box_rsolid(width=1)";
|
|
const sourcePath = "models/box.py";
|
|
const sourceUri = `sources/${sourcePath}`;
|
|
const sourceBytes = Buffer.from(`body = ${callText}\n`, "utf8");
|
|
const model: JsonRecord = {
|
|
schema_version: "2.0",
|
|
graph: {
|
|
graph_id: "graph",
|
|
nodes: [{
|
|
node_id: "node",
|
|
op: "make_box_rsolid",
|
|
params: { width: 1 },
|
|
inputs: [],
|
|
output_count: 1,
|
|
source: operationSource(sourcePath, callText),
|
|
}],
|
|
},
|
|
leaf_ids: ["node"],
|
|
};
|
|
const modelBytes = canonicalJsonBytes(model);
|
|
scene.source = {
|
|
kind: "model",
|
|
graph_id: "graph",
|
|
model_schema_version: "2.0",
|
|
artifact_hash: sha256(modelBytes),
|
|
embedded_artifact_uri: "model/model.json",
|
|
embedded_artifact_byte_length: modelBytes.length,
|
|
source_files: [{
|
|
path: sourcePath,
|
|
uri: sourceUri,
|
|
media_type: "text/x-python; charset=utf-8",
|
|
byte_length: sourceBytes.length,
|
|
content_hash: sha256(sourceBytes),
|
|
}],
|
|
};
|
|
asRecord(scene.compile_options).embed_source = true;
|
|
const replaced = replaceEntitySidecar(scene, sidecar);
|
|
replaced.blobs.set("model/model.json", modelBytes);
|
|
replaced.blobs.set(sourceUri, sourceBytes);
|
|
scene.revision = computeSceneRevision(scene);
|
|
return { blobs: replaced.blobs, manifest: scene, model, sourcePath, sourceUri };
|
|
}
|
|
|
|
function replaceEmbeddedModel(
|
|
fixture: ReturnType<typeof embeddedSourcePackage>,
|
|
): void {
|
|
const payload = canonicalJsonBytes(fixture.model);
|
|
fixture.blobs.set("model/model.json", payload);
|
|
const source = asRecord(fixture.manifest.source);
|
|
source.artifact_hash = sha256(payload);
|
|
source.embedded_artifact_byte_length = payload.length;
|
|
fixture.manifest.revision = computeSceneRevision(fixture.manifest);
|
|
}
|
|
|
|
function replaceEntitySidecar(
|
|
scene: JsonRecord,
|
|
sidecar: JsonRecord,
|
|
): { manifest: Buffer; blobs: Map<string, Buffer>; entityUri: string } {
|
|
const entityBytes = canonicalJsonBytes(sidecar);
|
|
const entityHash = sha256(entityBytes);
|
|
const entityUri = `entities/sha256-${entityHash.slice("sha256:".length)}.json`;
|
|
const entityRecord = asRecords(scene.entity_assets)[0];
|
|
const oldUri = entityRecord.uri as string;
|
|
Object.assign(entityRecord, {
|
|
byte_length: entityBytes.length,
|
|
content_hash: entityHash,
|
|
entity_asset_id: entityHash,
|
|
uri: entityUri,
|
|
});
|
|
asRecords(scene.definitions)[0].entity_asset_id = entityHash;
|
|
scene.revision = computeSceneRevision(scene);
|
|
const blobs = artifactBlobs();
|
|
blobs.delete(oldUri);
|
|
blobs.set(entityUri, entityBytes);
|
|
return { manifest: canonicalJsonBytes(scene), blobs, entityUri };
|
|
}
|
|
|
|
function entityIndex(sidecar: JsonRecord, kind: string): number {
|
|
const index = asRecords(sidecar.entities).findIndex((entity) => entity.kind === kind);
|
|
assert.notEqual(index, -1);
|
|
return index;
|
|
}
|
|
|
|
function assertFirstIssue(
|
|
result: ReturnType<typeof validateSceneManifest>,
|
|
code: string,
|
|
path: string,
|
|
): void {
|
|
assert.equal(result.valid, false);
|
|
assert.equal(result.firstError?.code, code);
|
|
assert.equal(result.firstError?.path, path);
|
|
}
|
|
|
|
test("scene semantic references and content identities reject independent mutations", async (context) => {
|
|
const missingHash = `sha256:${"f".repeat(64)}`;
|
|
const cases: [string, (scene: JsonRecord) => void, string, string][] = [
|
|
["missing definition appearance", (scene) => {
|
|
asRecords(scene.definitions)[0].appearance_id = `appearance/evaluated/${"f".repeat(64)}`;
|
|
}, "reference_missing", "/definitions/0/appearance_id"],
|
|
["missing definition geometry asset", (scene) => {
|
|
asRecords(scene.definitions)[0].geometry_asset_id = missingHash;
|
|
}, "reference_missing", "/definitions"],
|
|
["missing node appearance override", (scene) => {
|
|
asRecords(scene.nodes)[0].appearance_override_id = `appearance/evaluated/${"f".repeat(64)}`;
|
|
}, "reference_missing", "/nodes/0/appearance_override_id"],
|
|
["geometry asset ID differs from content hash", (scene) => {
|
|
asRecords(scene.geometry_assets)[0].content_hash = missingHash;
|
|
}, "source_matrix_invalid", "/geometry_assets/0/asset_id"],
|
|
["entity asset ID differs from content hash", (scene) => {
|
|
asRecords(scene.entity_assets)[0].content_hash = missingHash;
|
|
}, "source_matrix_invalid", "/entity_assets/0/entity_asset_id"],
|
|
["entity asset URI is not content-derived", (scene) => {
|
|
asRecords(scene.entity_assets)[0].uri = "entities/noncanonical.json";
|
|
}, "source_matrix_invalid", "/entity_assets/0/uri"],
|
|
["geometry tessellation differs from compile options", (scene) => {
|
|
asRecord(asRecords(scene.geometry_assets)[0].tessellation).linear_tolerance = 0.02;
|
|
}, "source_matrix_invalid", "/geometry_assets/0/tessellation/linear_tolerance"],
|
|
["appearance ID is not content-derived", (scene) => {
|
|
const appearanceId = `appearance/evaluated/${"f".repeat(64)}`;
|
|
asRecords(scene.appearances)[0].appearance_id = appearanceId;
|
|
asRecords(scene.definitions)[0].appearance_id = appearanceId;
|
|
}, "source_matrix_invalid", "/appearances/0/appearance_id"],
|
|
["manual source cannot request embedding", (scene) => {
|
|
asRecord(scene.compile_options).embed_source = true;
|
|
}, "source_matrix_invalid", "/source"],
|
|
["definition ID must be source-derived", (scene) => {
|
|
asRecord(scene.source).source_id = "other";
|
|
asRecord(asRecords(scene.definitions)[0].source).source_id = "other";
|
|
}, "source_matrix_invalid", "/definitions/0/definition_id"],
|
|
];
|
|
for (const [name, mutate, code, path] of cases) await context.test(name, () => {
|
|
const scene = validScene();
|
|
mutate(scene);
|
|
scene.revision = computeSceneRevision(scene);
|
|
assertFirstIssue(validateSceneManifest(scene), code, path);
|
|
});
|
|
});
|
|
|
|
function addSceneCamera(scene: JsonRecord): JsonRecord {
|
|
const camera = {
|
|
camera_id: "camera/test",
|
|
name: "Test camera",
|
|
projection: "perspective",
|
|
parent_node_id: null,
|
|
transform: structuredClone(asRecords(scene.nodes)[0].transform),
|
|
near: 1,
|
|
far: 10,
|
|
vertical_fov_degrees: 45,
|
|
};
|
|
assert.ok(Array.isArray(scene.cameras));
|
|
scene.cameras.push(camera);
|
|
return camera;
|
|
}
|
|
|
|
test("scene cameras reject dangling parents and reversed clipping bounds independently", async (context) => {
|
|
await context.test("dangling parent", () => {
|
|
const scene = validScene();
|
|
addSceneCamera(scene).parent_node_id = "instance/missing";
|
|
scene.revision = computeSceneRevision(scene);
|
|
assertFirstIssue(validateSceneManifest(scene), "reference_missing", "/cameras/0/parent_node_id");
|
|
});
|
|
await context.test("far does not exceed near", () => {
|
|
const scene = validScene();
|
|
addSceneCamera(scene).far = 1;
|
|
scene.revision = computeSceneRevision(scene);
|
|
assertFirstIssue(validateSceneManifest(scene), "bounds_invalid", "/cameras/0/far");
|
|
});
|
|
});
|
|
|
|
test("entity kind, frame, status, position, and bounds semantics reject independent mutations", async (context) => {
|
|
const cases: [string, string, (entity: JsonRecord) => void, string, (index: number) => string][] = [
|
|
["geometry kind", "solid", (entity) => {
|
|
entity.geometry = { type: "point", position: [0, 0, 0] };
|
|
}, "entity_topology_invalid", (index) => `/entities/${index}/geometry/type`],
|
|
["required connector frame", "face", (entity) => {
|
|
entity.sdk_connector_frame = null;
|
|
}, "connector_invalid", (index) => `/entities/${index}/sdk_connector_frame`],
|
|
["render status", "vertex", (entity) => {
|
|
entity.render_status = "degenerate";
|
|
}, "entity_topology_invalid", (index) => `/entities/${index}/render_status`],
|
|
["vertex property position", "vertex", (entity) => {
|
|
const position = [...(asRecord(entity.properties).position as number[])];
|
|
position[0] += 1;
|
|
asRecord(entity.properties).position = position;
|
|
}, "bounds_invalid", (index) => `/entities/${index}/properties/position`],
|
|
["reversed bounds", "solid", (entity) => {
|
|
const bounds = asRecord(asRecord(entity.properties).bounds);
|
|
const maximum = bounds.max as number[];
|
|
const minimum = [...(bounds.min as number[])];
|
|
minimum[0] = maximum[0] + 1;
|
|
bounds.min = minimum;
|
|
}, "bounds_invalid", (index) => `/entities/${index}/properties/bounds`],
|
|
];
|
|
for (const [name, kind, mutate, code, path] of cases) await context.test(name, () => {
|
|
const sidecar = validEntities();
|
|
const index = entityIndex(sidecar, kind);
|
|
mutate(asRecords(sidecar.entities)[index]);
|
|
assertFirstIssue(validateEntityAsset(sidecar), code, path(index));
|
|
});
|
|
});
|
|
|
|
test("analytic geometry coordinates enforce the scene coordinate limit", async (context) => {
|
|
const cases: [string, string, (geometry: JsonRecord) => void][] = [
|
|
["point.position", "vertex", (geometry) => { geometry.position = [1e12 + 1, 0, 0]; }],
|
|
["line.origin", "edge", (geometry) => { geometry.origin = [0, -(1e12 + 1), 0]; }],
|
|
["plane.origin", "face", (geometry) => { geometry.origin = [0, 0, 1e12 + 1]; }],
|
|
["circle.center", "edge", (geometry) => {
|
|
for (const key of Object.keys(geometry)) delete geometry[key];
|
|
Object.assign(geometry, {
|
|
type: "circle",
|
|
center: [1e12 + 1, 0, 0],
|
|
normal: [0, 0, 1],
|
|
x_direction: [1, 0, 0],
|
|
radius: 1,
|
|
});
|
|
}],
|
|
];
|
|
for (const [name, kind, mutate] of cases) await context.test(name, () => {
|
|
const sidecar = validEntities();
|
|
const index = entityIndex(sidecar, kind);
|
|
const geometry = asRecord(asRecords(sidecar.entities)[index].geometry);
|
|
mutate(geometry);
|
|
const result = validateEntityAsset(sidecar);
|
|
assert.equal(result.valid, false);
|
|
assert.ok(result.issues.some((item) =>
|
|
item.code === "analytic_geometry_invalid" &&
|
|
item.path === `/entities/${index}/geometry/${name.split(".")[1]}`
|
|
));
|
|
});
|
|
});
|
|
|
|
test("package checks entity geometry engine version against the manifest", () => {
|
|
const scene = validScene();
|
|
const sidecar = validEntities();
|
|
asRecord(sidecar.geometry_engine).version = "different";
|
|
const { manifest, blobs } = replaceEntitySidecar(scene, sidecar);
|
|
const result = validateScenePackage(manifest, blobs);
|
|
assert.ok(result.issues.some((item) =>
|
|
item.code === "source_matrix_invalid" && item.path === "/definitions/0/entity_asset_id"
|
|
));
|
|
});
|
|
|
|
test("package rejects entity source variants incompatible with the scene source", () => {
|
|
const scene = validScene();
|
|
const sidecar = validEntities();
|
|
asRecords(sidecar.entities)[0].source = {
|
|
kind: "imported_primitive",
|
|
source_element_id: "element",
|
|
};
|
|
const { manifest, blobs, entityUri } = replaceEntitySidecar(scene, sidecar);
|
|
const result = validateScenePackage(manifest, blobs);
|
|
assert.ok(result.issues.some((item) =>
|
|
item.code === "source_matrix_invalid" && item.path === `/${entityUri}/entities/0/source`
|
|
));
|
|
});
|
|
|
|
function makeModelScene(scene: JsonRecord, sidecar: JsonRecord, definitionKind: "shape" | "part"): void {
|
|
scene.source = {
|
|
kind: "model",
|
|
graph_id: "graph",
|
|
model_schema_version: "2.0",
|
|
artifact_hash: `sha256:${"1".repeat(64)}`,
|
|
};
|
|
const definition = asRecords(scene.definitions)[0];
|
|
definition.kind = definitionKind;
|
|
definition.source = definitionKind === "part"
|
|
? {
|
|
kind: "product_model",
|
|
root_id: "root",
|
|
semantic_type: "Part",
|
|
semantic_id: "Part",
|
|
graph_id: "graph",
|
|
node_id: "node",
|
|
output_slot: 0,
|
|
}
|
|
: {
|
|
kind: "model_output",
|
|
root_id: "root",
|
|
graph_id: "graph",
|
|
node_id: "node",
|
|
output_slot: 0,
|
|
};
|
|
for (const entity of asRecords(sidecar.entities)) {
|
|
entity.source = {
|
|
kind: "model_output",
|
|
graph_id: "graph",
|
|
node_id: "node",
|
|
output_slot: 0,
|
|
};
|
|
}
|
|
}
|
|
|
|
test("package checks model entity source ownership fields", () => {
|
|
const scene = validScene();
|
|
const sidecar = validEntities();
|
|
makeModelScene(scene, sidecar, "shape");
|
|
asRecord(asRecords(sidecar.entities)[0].source).node_id = "other-node";
|
|
const { manifest, blobs, entityUri } = replaceEntitySidecar(scene, sidecar);
|
|
const result = validateScenePackage(manifest, blobs);
|
|
assert.ok(result.issues.some((item) =>
|
|
item.code === "source_matrix_invalid" && item.path === `/${entityUri}/entities/0/source`
|
|
));
|
|
});
|
|
|
|
test("embedded model and Python source package validates", () => {
|
|
const fixture = embeddedSourcePackage();
|
|
const result = validateScenePackage(fixture.manifest, fixture.blobs);
|
|
assert.equal(result.valid, true, JSON.stringify(result.issues));
|
|
});
|
|
|
|
test("manifest rejects archive-unsafe embedded Python paths", () => {
|
|
const fixture = embeddedSourcePackage();
|
|
const sourceFile = asRecords(asRecord(fixture.manifest.source).source_files)[0];
|
|
sourceFile.path = "models/../box.py";
|
|
sourceFile.uri = "sources/models/../box.py";
|
|
fixture.manifest.revision = computeSceneRevision(fixture.manifest);
|
|
|
|
const result = validateSceneManifest(fixture.manifest);
|
|
assert.ok(result.issues.some((item) =>
|
|
item.code === "source_matrix_invalid" && item.path === "/source/source_files/0/path"
|
|
));
|
|
});
|
|
|
|
test("package rejects malformed operation source mappings", () => {
|
|
const fixture = embeddedSourcePackage();
|
|
const graph = asRecord(fixture.model.graph);
|
|
const source = asRecord(asRecords(graph.nodes)[0].source);
|
|
delete source.callsite_id;
|
|
replaceEmbeddedModel(fixture);
|
|
|
|
const result = validateScenePackage(fixture.manifest, fixture.blobs);
|
|
assert.ok(result.issues.some((item) =>
|
|
item.code === "source_matrix_invalid" && item.path === "/model/model.json/graph/nodes/0/source"
|
|
));
|
|
});
|
|
|
|
test("package rejects invalid UTF-8 Python source bytes", () => {
|
|
const fixture = embeddedSourcePackage();
|
|
const payload = Buffer.from([0xff]);
|
|
fixture.blobs.set(fixture.sourceUri, payload);
|
|
const sourceFile = asRecords(asRecord(fixture.manifest.source).source_files)[0];
|
|
sourceFile.byte_length = payload.length;
|
|
sourceFile.content_hash = sha256(payload);
|
|
fixture.manifest.revision = computeSceneRevision(fixture.manifest);
|
|
|
|
const result = validateScenePackage(fixture.manifest, fixture.blobs);
|
|
assert.ok(result.issues.some((item) =>
|
|
item.code === "invalid_utf8" && item.path === `/${fixture.sourceUri}`
|
|
));
|
|
});
|
|
|
|
test("package binding status rules follow Python precedence", async (context) => {
|
|
await context.test("solid is always not_applicable", () => {
|
|
const scene = validScene();
|
|
const sidecar = validEntities();
|
|
const index = entityIndex(sidecar, "solid");
|
|
asRecords(sidecar.entities)[index].connector_binding_status = "supported";
|
|
const { manifest, blobs, entityUri } = replaceEntitySidecar(scene, sidecar);
|
|
const result = validateScenePackage(manifest, blobs);
|
|
const found = result.issues.find((item) => item.code === "connector_invalid" && item.path === `/${entityUri}/entities/${index}/connector_binding_status`);
|
|
assert.match(found?.message ?? "", /not_applicable/);
|
|
});
|
|
|
|
await context.test("non-part owner precedes scene source", () => {
|
|
const scene = validScene();
|
|
const sidecar = validEntities();
|
|
const index = entityIndex(sidecar, "face");
|
|
asRecords(sidecar.entities)[index].connector_binding_status = "source_not_model";
|
|
const { manifest, blobs, entityUri } = replaceEntitySidecar(scene, sidecar);
|
|
const result = validateScenePackage(manifest, blobs);
|
|
const found = result.issues.find((item) => item.code === "connector_invalid" && item.path === `/${entityUri}/entities/${index}/connector_binding_status`);
|
|
assert.match(found?.message ?? "", /owner_not_part/);
|
|
});
|
|
|
|
await context.test("non-model part requires source_not_model", () => {
|
|
const scene = validScene();
|
|
const sidecar = validEntities();
|
|
const definition = asRecords(scene.definitions)[0];
|
|
definition.kind = "part";
|
|
definition.source = {
|
|
kind: "product_manual",
|
|
root_id: "root",
|
|
semantic_type: "Part",
|
|
semantic_id: "Part",
|
|
};
|
|
const entities = asRecords(sidecar.entities);
|
|
for (const entity of entities) {
|
|
if (entity.kind !== "solid") entity.connector_binding_status = "source_not_model";
|
|
}
|
|
const index = entityIndex(sidecar, "face");
|
|
entities[index].connector_binding_status = "supported";
|
|
const { manifest, blobs, entityUri } = replaceEntitySidecar(scene, sidecar);
|
|
const result = validateScenePackage(manifest, blobs);
|
|
const found = result.issues.find((item) => item.code === "connector_invalid" && item.path === `/${entityUri}/entities/${index}/connector_binding_status`);
|
|
assert.match(found?.message ?? "", /source_not_model/);
|
|
});
|
|
|
|
await context.test("model part null frame requires frame_undefined", () => {
|
|
const scene = validScene();
|
|
const sidecar = validEntities();
|
|
makeModelScene(scene, sidecar, "part");
|
|
const entities = asRecords(sidecar.entities);
|
|
for (const entity of entities) {
|
|
if (entity.kind !== "solid") entity.connector_binding_status = "supported";
|
|
}
|
|
const index = entityIndex(sidecar, "edge");
|
|
entities[index].sdk_connector_frame = null;
|
|
entities[index].connector_binding_status = "owner_not_part";
|
|
const { manifest, blobs, entityUri } = replaceEntitySidecar(scene, sidecar);
|
|
const result = validateScenePackage(manifest, blobs);
|
|
const found = result.issues.find((item) => item.code === "connector_invalid" && item.path === `/${entityUri}/entities/${index}/connector_binding_status`);
|
|
assert.match(found?.message ?? "", /frame_undefined/);
|
|
});
|
|
});
|
|
|
|
test("all package cases match validity and expected first issue", async (context) => {
|
|
for (const item of corpus.package_cases) await context.test(item.name, () => {
|
|
const result = validateScenePackage(
|
|
decode(item.manifest_base64),
|
|
corpusBlobs(corpus.blobs, item),
|
|
);
|
|
assert.equal(result.valid, item.valid);
|
|
if (item.expected !== null) {
|
|
assert.equal(result.firstError?.code, item.expected.code);
|
|
assert.equal(result.firstError?.path, item.expected.path);
|
|
}
|
|
});
|
|
|
|
test("accepted package fixtures cover every connector binding status", () => {
|
|
const statuses = new Set<string>();
|
|
for (const item of [...corpus.package_cases, ...corpus.scene_shape_cases]) {
|
|
if (!item.valid) continue;
|
|
for (const [uri, payload] of corpusBlobs(corpus.blobs, item)) {
|
|
if (!uri.startsWith("entities/")) continue;
|
|
const sidecar = asRecord(parseCanonicalJson(payload));
|
|
for (const entity of asRecords(sidecar.entities)) {
|
|
statuses.add(String(entity.connector_binding_status));
|
|
}
|
|
}
|
|
}
|
|
assert.deepEqual([...statuses].sort(), [
|
|
"frame_undefined",
|
|
"not_applicable",
|
|
"owner_not_part",
|
|
"selector_ambiguous",
|
|
"selector_unstable",
|
|
"source_not_model",
|
|
"supported",
|
|
]);
|
|
});
|
|
});
|
|
|
|
test("scene field matrix replays independently", async (context) => {
|
|
const scene = validScene();
|
|
for (const item of corpus.scene_field_cases) await context.test(item.name, () => {
|
|
const result = validateSceneManifest(
|
|
canonicalJsonBytes(applySceneFieldCase(scene, item)),
|
|
);
|
|
assert.equal(result.valid, item.valid);
|
|
assert.equal(result.firstError?.code, item.expected?.code);
|
|
assert.equal(result.firstError?.path, item.expected?.path);
|
|
});
|
|
});
|
|
|
|
test("scene shapes validate, round trip, and characterize asset reuse", async (context) => {
|
|
for (const item of corpus.scene_shape_cases) await context.test(item.name, () => {
|
|
const manifest = decode(item.manifest_base64);
|
|
const scene = asRecord(parseCanonicalJson(manifest));
|
|
const result = validateScenePackage(manifest, corpusBlobs(corpus.blobs, item));
|
|
assert.equal(result.valid, item.valid);
|
|
assert.equal(result.firstError?.code, item.expected?.code);
|
|
assert.equal(result.firstError?.path, item.expected?.path);
|
|
assert.deepEqual(canonicalJsonBytes(scene), manifest);
|
|
assert.deepEqual(
|
|
canonicalJsonBytes(sceneShapeFacts(scene)),
|
|
canonicalJsonBytes(item.expected_shape),
|
|
);
|
|
|
|
const definitions = new Map(
|
|
asRecords(scene.definitions).map((definition) => [
|
|
String(definition.definition_id),
|
|
definition,
|
|
]),
|
|
);
|
|
for (const [definitionId, count] of Object.entries(
|
|
item.expected_shape.definition_occurrence_counts,
|
|
)) {
|
|
if (count <= 1) continue;
|
|
const occurrences = asRecords(scene.nodes).filter(
|
|
(node) => node.definition_id === definitionId,
|
|
);
|
|
assert.equal(occurrences.length, count);
|
|
assert.equal(new Set(occurrences.map((node) => node.definition_id)).size, 1);
|
|
assert.notEqual(definitions.get(definitionId)?.geometry_asset_id, null);
|
|
}
|
|
});
|
|
});
|
|
|
|
test("two-pass revision vectors reproduce exact drafts, revisions, and bytes", async (context) => {
|
|
for (const vector of corpus.revision_vectors) await context.test(vector.name, () => {
|
|
const draftBytes = decode(vector.draft_base64);
|
|
const draft = asRecord(parseCanonicalJson(draftBytes));
|
|
const sceneBytes = decode(vector.canonical_base64);
|
|
const scene = withSceneRevision(draft);
|
|
assert.deepEqual(canonicalJsonBytes(draft), draftBytes);
|
|
assert.equal(scene.revision, vector.revision);
|
|
assert.deepEqual(canonicalJsonBytes(scene), sceneBytes);
|
|
assert.equal(sha256(sceneBytes), vector.sha256);
|
|
});
|
|
});
|
|
|
|
test("all GLB cases match acceptance, kind, and exact rejection", async (context) => {
|
|
for (const item of corpus.glb_cases) await context.test(item.name, () => {
|
|
if (item.valid) {
|
|
const result = preflightGlb(decode(item.payload_base64), item.expected_kind);
|
|
assert.equal(result.kind, item.kind);
|
|
} else {
|
|
assert.throws(() => preflightGlb(decode(item.payload_base64), item.expected_kind), (error) => {
|
|
assert.equal((error as Error).message, item.error);
|
|
return true;
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
test("all ZIP cases match acceptance, deflate use, and exact rejection", async (context) => {
|
|
for (const item of corpus.zip_cases) await context.test(item.name, () => {
|
|
if (item.valid) {
|
|
const result = preflightZipBytes(decode(item.payload_base64));
|
|
assert.equal(result.usedDeflate, item.used_deflate);
|
|
} else {
|
|
assert.throws(() => preflightZipBytes(decode(item.payload_base64)), (error) => {
|
|
assert.equal((error as Error).message, item.error);
|
|
return true;
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
function f64FromHex(value: string): number {
|
|
return Buffer.from(value, "hex").readDoubleBE(0);
|
|
}
|
|
|
|
function f64Hex(value: number): string {
|
|
const bytes = Buffer.allocUnsafe(8);
|
|
bytes.writeDoubleBE(value, 0);
|
|
return bytes.toString("hex");
|
|
}
|
|
|
|
test("numeric profile vectors reproduce exact IEEE-754 results", async (context) => {
|
|
for (const vector of corpus.numeric_vectors) await context.test(vector.name, () => {
|
|
try {
|
|
let actual: string | string[];
|
|
if (vector.operation === "f32") {
|
|
const input = vector.input_bits as string[];
|
|
actual = profileF32Bits(f64FromHex(input[0])).toString(16).padStart(8, "0");
|
|
} else if (vector.operation === "cross") {
|
|
const [left, right] = (vector.input_bits as string[][]).map(
|
|
(input) => input.map(f64FromHex) as [number, number, number],
|
|
);
|
|
actual = profileCross(left, right).map(f64Hex);
|
|
} else {
|
|
const input = (vector.input_bits as string[][])[0].map(f64FromHex) as [number, number, number];
|
|
actual = profileNormalize(input).map((component) =>
|
|
profileF32Bits(component).toString(16).padStart(8, "0")
|
|
);
|
|
}
|
|
assert.equal(vector.valid, true);
|
|
assert.deepEqual(actual, vector.expected_bits);
|
|
} catch (error) {
|
|
assert.equal(vector.valid, false);
|
|
assert.equal((error as Error).message, vector.error);
|
|
}
|
|
});
|
|
});
|
|
|
|
test("resource profile and exact boundary cases match Python", async (context) => {
|
|
assert.deepEqual(BASE_LIMITS, Object.fromEntries(Object.entries(corpus.resource_limits)));
|
|
const scene = validScene();
|
|
|
|
for (const item of corpus.resource_cases) await context.test(item.name, () => {
|
|
let valid = false;
|
|
let error: string | null = null;
|
|
let first: ExpectedIssue | null = null;
|
|
try {
|
|
const parameters = item.parameters;
|
|
const limits = { ...BASE_LIMITS, ...(parameters.limits as Partial<SceneResourceLimits> ?? {}) };
|
|
if (item.operation === "input_archive_size") {
|
|
preflightInputArchiveSize(parameters.size as number, limits);
|
|
valid = true;
|
|
} else if (item.operation === "archive_member_sizes" || item.operation === "archive_member_count") {
|
|
let sizes: Map<string, number>;
|
|
if (item.operation === "archive_member_sizes") {
|
|
sizes = new Map(Object.entries(parameters.sizes as Record<string, number>));
|
|
} else {
|
|
sizes = new Map([["scene.json", 0]]);
|
|
for (let index = 0; index < (parameters.count as number) - 1; index += 1) {
|
|
sizes.set(`x/${index.toString().padStart(5, "0")}`, 0);
|
|
}
|
|
}
|
|
preflightArchiveMemberSizes(sizes, limits);
|
|
valid = true;
|
|
} else if (item.operation === "aggregate_compression_ratio" || item.operation === "member_compression_ratio") {
|
|
const callback = item.operation === "aggregate_compression_ratio"
|
|
? preflightAggregateCompressionRatio
|
|
: preflightMemberCompressionRatio;
|
|
callback(parameters.uncompressed_size as number, parameters.compressed_size as number, limits);
|
|
valid = true;
|
|
} else if (item.operation === "json_depth") {
|
|
const value: JsonRecord = {};
|
|
let current = value;
|
|
for (let index = 0; index < (parameters.depth as number); index += 1) {
|
|
current.x = {};
|
|
current = asRecord(current.x);
|
|
}
|
|
const result = report(jsonResourceIssues(value, limits));
|
|
valid = result.valid;
|
|
first = result.firstError === null ? null : { code: result.firstError.code, path: result.firstError.path };
|
|
} else if (item.operation === "json_domain") {
|
|
const text = parameters.text as string;
|
|
const kind = parameters.kind as string;
|
|
const field = parameters.field as string | null;
|
|
let value: JsonRecord;
|
|
if (kind === "value") value = { value: text };
|
|
else if (kind === "object_key") value = { [text]: 0 };
|
|
else if (kind === "metadata_key") value = { metadata: { [text]: 0 } };
|
|
else if (kind === "sdk_metadata_key") value = { sdk_metadata: { [text]: 0 } };
|
|
else if (kind === "identifier") value = { [field ?? "node_id"]: text };
|
|
else if (kind === "identifier_array") value = { [field ?? "component_path"]: [text] };
|
|
else if (kind === "uri") value = { uri: text };
|
|
else throw new Error(`unknown JSON domain kind: ${kind}`);
|
|
const result = report(jsonResourceIssues(value, limits));
|
|
valid = result.valid;
|
|
first = result.firstError === null ? null : { code: result.firstError.code, path: result.firstError.path };
|
|
} else if (item.operation === "resource_count") {
|
|
const count = parameters.count as number;
|
|
const kind = parameters.kind as string;
|
|
const field = parameters.field as string | null;
|
|
let value: JsonRecord;
|
|
if (kind === "collection") value = { [field!]: Array(count).fill(null) };
|
|
else if (kind === "hierarchy") value = { nodes: [{ source: { component_path: Array(count).fill("x") } }] };
|
|
else if (kind === "forwarded") {
|
|
value = {
|
|
connectors: Array.from({ length: count }, (_unused, index) => ({
|
|
anchor_kind: "forwarded",
|
|
connector_snapshot_id: `c${index}`,
|
|
forwarded_from: { source_connector_snapshot_id: `c${index + 1}` },
|
|
})),
|
|
};
|
|
} else throw new Error(`unknown resource count kind: ${kind}`);
|
|
const result = report(resourceCountIssues(value, parameters.artifact as "scene" | "entities" | "presentation", limits));
|
|
valid = result.valid;
|
|
first = result.firstError === null ? null : { code: result.firstError.code, path: result.firstError.path };
|
|
} else if (item.operation === "glb_counts") {
|
|
preflightGlbCounts(
|
|
parameters.kind as "triangle" | "line",
|
|
parameters.vertex_count as number,
|
|
parameters.index_count as number,
|
|
limits,
|
|
);
|
|
valid = true;
|
|
} else if (item.operation === "package_budget") {
|
|
const input = parameters.contributions as {
|
|
scene_json_bytes: number;
|
|
glb_decoded_buffer_bytes: number;
|
|
entity_json_bytes: number;
|
|
other_immutable_json_bytes: number;
|
|
entity_count: number;
|
|
entity_vertex_count: number;
|
|
triangle_vertex_count: number;
|
|
triangle_count: number;
|
|
line_vertex_count: number;
|
|
line_segment_count: number;
|
|
};
|
|
const totals = computePackageBudgetTotals({
|
|
sceneJsonBytes: input.scene_json_bytes,
|
|
glbDecodedBufferBytes: input.glb_decoded_buffer_bytes,
|
|
entityJsonBytes: input.entity_json_bytes,
|
|
otherImmutableJsonBytes: input.other_immutable_json_bytes,
|
|
entityCount: input.entity_count,
|
|
entityVertexCount: input.entity_vertex_count,
|
|
triangleVertexCount: input.triangle_vertex_count,
|
|
triangleCount: input.triangle_count,
|
|
lineVertexCount: input.line_vertex_count,
|
|
lineSegmentCount: input.line_segment_count,
|
|
});
|
|
const result = report(packageBudgetIssues(totals, limits), "package");
|
|
valid = result.valid;
|
|
first = result.firstError === null ? null : { code: result.firstError.code, path: result.firstError.path };
|
|
} else if (item.operation === "scene_geometry_byte_length") {
|
|
let value = structuredClone(scene);
|
|
const byteLength = Number(parameters.value);
|
|
asRecords(value.geometry_assets)[0].byte_length = byteLength;
|
|
if (byteLength <= Number.MAX_SAFE_INTEGER) value = withSceneRevision(value);
|
|
const result = validateSceneManifest(value);
|
|
valid = result.valid;
|
|
first = result.firstError === null ? null : { code: result.firstError.code, path: result.firstError.path };
|
|
} else if (item.operation === "scene_compile_option") {
|
|
const value = structuredClone(scene);
|
|
const field = parameters.field as string;
|
|
const number = parameters.value as number;
|
|
asRecord(value.compile_options)[field] = number;
|
|
asRecord(asRecords(value.geometry_assets)[0].tessellation)[field] = number;
|
|
if (field === "linear_tolerance") asRecord(asRecords(value.edge_assets)[0].tessellation)[field] = number;
|
|
const result = validateSceneManifest(withSceneRevision(value));
|
|
valid = result.valid;
|
|
first = result.firstError === null ? null : { code: result.firstError.code, path: result.firstError.path };
|
|
} else {
|
|
throw new Error(`unknown resource operation: ${item.operation}`);
|
|
}
|
|
} catch (caught) {
|
|
valid = false;
|
|
error = (caught as Error).message;
|
|
}
|
|
assert.equal(valid, item.valid);
|
|
if (Object.hasOwn(item, "error")) assert.equal(error, item.error ?? null);
|
|
if (item.expected !== undefined && item.expected !== null) {
|
|
assert.equal(first?.code, item.expected.code);
|
|
assert.equal(first?.path, item.expected.path);
|
|
}
|
|
});
|
|
});
|
|
|
|
test("canonical stored ZIP reproduces the exact shared vector", () => {
|
|
const expected = decode(corpus.artifacts.canonical_zip.base64);
|
|
const archive = preflightZipBytes(expected);
|
|
assert.equal(archive.inputSize, archive.canonicalSize);
|
|
assert.deepEqual(canonicalZipBytes(archive.members), expected);
|
|
assert.equal(`sha256:${createHash("sha256").update(expected).digest("hex")}`, corpus.artifacts.canonical_zip.sha256);
|
|
});
|