feat(inspect): server-identical local projection and payload
Port utils/inspection_response.py to the browser: a strict parse with Python's Decimal spelling, duplicate-key rejection, NaN/Infinity as null and integers past 2^53-1 as text; the prompt, workflow and carrier projection with the depth-64 and 20,000-node ceilings; _metadata; and the 8 MiB omission steps (workflow graph, then prompt graph, then all raw) measured as UTF-8 bytes. local-inspection.mjs builds the inspection_version 1 wire for one chosen File from the Task 1 and Task 2 readers (observation selected-local-file, interim unresolved roles, unavailable library), fits it to the response budget, and exposes localFileAdapter for the shared controller, inspector and viewer: handles are session-owned by a private token, so a tampered or hand-made descriptor is rejected and blob: URLs are never resolved through the API client. projectInspectionPayload admits selected-local-file only for a local-file identity. A Python oracle test compares projectMetadata with _metadata byte for byte over the parity PNG corpus, the committed video fixtures and tag maps, 75 committed adversarial record sets (projection-cases.json) and node-ceiling sets built at test time.
This commit is contained in:
Vendored
+15
@@ -67,6 +67,21 @@ for f in core-faststart.mp4 core-moov-end.mp4 core.mov core.m4v core.mkv core-ta
|
||||
done
|
||||
```
|
||||
|
||||
# Local Inspect projection cases
|
||||
|
||||
`local-inspect/projection-cases.json` holds synthetic record sets (prompt, workflow,
|
||||
parameters and pair-record texts, optional `record_formats`, a video `carrier_text`
|
||||
and `limited` records) that exercise every branch of the server's inspection
|
||||
projection: Python's Decimal spellings and exact-construction limits, the 4300-digit
|
||||
integer limit, duplicate keys, NaN/Infinity, the depth-64 and node ceilings, prompt
|
||||
node key filtering and the carrier rule. `records: null` stands for an unreadable
|
||||
file. `expect` pins both graph statuses; `needs` names an interpreter feature a case
|
||||
depends on. `tests/js/workspace-local-projection.test.js` runs them through
|
||||
`web/js/workspace/local-projection.mjs`, and `tests/test_local_inspect_parity.py`
|
||||
asserts that its answer equals `utils.inspection_response._metadata` byte for byte.
|
||||
All texts are original synthetic content, dedicated to the public domain under
|
||||
CC0-1.0.
|
||||
|
||||
# Inspection parity fixtures
|
||||
|
||||
`inspection_parity/` holds small synthetic PNGs (`f01b.png` ... `f24.png`, each
|
||||
|
||||
+849
@@ -0,0 +1,849 @@
|
||||
{
|
||||
"version": 1,
|
||||
"about": "Synthetic record sets for utils/inspection_response.py _metadata parity with web/js/workspace/local-projection.mjs projectMetadata. records null = an unreadable file. record_formats default to original-text for each record. expect pins both graph statuses; needs names an interpreter feature the Python side must have.",
|
||||
"cases": [
|
||||
{
|
||||
"name": "unreadable_read",
|
||||
"records": null,
|
||||
"expect": {
|
||||
"prompt_graph": "absent",
|
||||
"workflow_graph": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "absent_records",
|
||||
"records": {},
|
||||
"expect": {
|
||||
"prompt_graph": "absent",
|
||||
"workflow_graph": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "prompt_and_workflow_parsed",
|
||||
"records": {
|
||||
"prompt": "{\"1\":{\"class_type\":\"A\",\"inputs\":{\"x\":1}}}",
|
||||
"workflow": "{\"nodes\":[],\"version\":0.4}"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "parsed",
|
||||
"workflow_graph": "parsed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "all_four_records",
|
||||
"records": {
|
||||
"prompt": "{\"1\":{\"class_type\":\"A\",\"inputs\":{\"x\":1}}}",
|
||||
"workflow": "{\"nodes\":[],\"version\":0.4}",
|
||||
"parameters": "cat\nNegative prompt: dog\nSteps: 20, Sampler: Euler, Seed: 7",
|
||||
"comfyassets_prompt_pairs": "{\"version\":1,\"nodes\":{}}"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "parsed",
|
||||
"workflow_graph": "parsed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "parameters_only",
|
||||
"records": {
|
||||
"parameters": "cat\nSteps: 20"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "absent",
|
||||
"workflow_graph": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "number_spellings",
|
||||
"records": {
|
||||
"workflow": "{\"nodes\":[],\"a\":[1e3,1e0,-0.0,6.50,0.0100,1.0e2,0e5,0.0000001,0.000001,1E+3,1e-0,0.5e1,0.05e1,0.0e0,1.5e+300,123e-8,2.5e-3,1.0,-1.50,0.0,100.0,1234567e-6,0.5E-6,0.5E-5,1e22,0.000000,-0.000000e-3,7.0e-6,7.0e-7,12345678901234567890.123,9007199254740992.0]}"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "absent",
|
||||
"workflow_graph": "parsed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "integer_spellings",
|
||||
"records": {
|
||||
"workflow": "{\"a\":[9007199254740991,-9007199254740991,9007199254740992,-9007199254740992,10000000000000000,123456789012345678901234567890,0,-0,42,-7,100]}"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "absent",
|
||||
"workflow_graph": "parsed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "nonfinite_literals_are_null",
|
||||
"records": {
|
||||
"prompt": "{\"1\":{\"class_type\":\"A\",\"inputs\":{\"n\":NaN,\"i\":Infinity,\"m\":-Infinity},\"_meta\":{\"n\":NaN}}}",
|
||||
"workflow": "{\"a\":[NaN,Infinity,-Infinity],\"b\":NaN}"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "parsed",
|
||||
"workflow_graph": "parsed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "exponent_edges_valid",
|
||||
"records": {
|
||||
"workflow": "{\"a\":[1e999999999999999999,1e-1999999999999999997,0.0e1000000000000000000,1.5e999999999999999999,0.1e-1999999999999999996]}"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "absent",
|
||||
"workflow_graph": "parsed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "exponent_edge_invalid_1e1000000000000000000",
|
||||
"records": {
|
||||
"workflow": "{\"a\":1e1000000000000000000}"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "absent",
|
||||
"workflow_graph": "malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "exponent_edge_invalid_15e999999999999999999",
|
||||
"records": {
|
||||
"workflow": "{\"a\":15e999999999999999999}"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "absent",
|
||||
"workflow_graph": "malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "exponent_edge_invalid_1e_1999999999999999998",
|
||||
"records": {
|
||||
"workflow": "{\"a\":1e-1999999999999999998}"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "absent",
|
||||
"workflow_graph": "malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "exponent_edge_invalid_1_5e_1999999999999999997",
|
||||
"records": {
|
||||
"workflow": "{\"a\":1.5e-1999999999999999997}"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "absent",
|
||||
"workflow_graph": "malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "exponent_edge_invalid_0e1000000000000000000",
|
||||
"records": {
|
||||
"workflow": "{\"a\":0e1000000000000000000}"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "absent",
|
||||
"workflow_graph": "malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "exponent_edge_invalid_1e9223372036854775808",
|
||||
"records": {
|
||||
"workflow": "{\"a\":1e9223372036854775808}"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "absent",
|
||||
"workflow_graph": "malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "exponent_edge_invalid_0_0e1000000000000000001",
|
||||
"records": {
|
||||
"workflow": "{\"a\":0.0e1000000000000000001}"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "absent",
|
||||
"workflow_graph": "malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "exponent_edge_invalid_1e99999999999999999999999999",
|
||||
"records": {
|
||||
"workflow": "{\"a\":1e99999999999999999999999999}"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "absent",
|
||||
"workflow_graph": "malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "int_4300_digits_kept",
|
||||
"records": {
|
||||
"workflow": "{\"a\":7777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777}"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "absent",
|
||||
"workflow_graph": "parsed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "int_4301_digits_malformed",
|
||||
"records": {
|
||||
"workflow": "{\"a\":77777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777}"
|
||||
},
|
||||
"needs": "int-max-str-digits",
|
||||
"expect": {
|
||||
"prompt_graph": "absent",
|
||||
"workflow_graph": "malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "int_negative_4301_digits_malformed",
|
||||
"records": {
|
||||
"workflow": "{\"a\":-77777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777}"
|
||||
},
|
||||
"needs": "int-max-str-digits",
|
||||
"expect": {
|
||||
"prompt_graph": "absent",
|
||||
"workflow_graph": "malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "string_escapes",
|
||||
"records": {
|
||||
"prompt": "{\"1\":{\"class_type\":\"A\",\"inputs\":{\"t\":\"é😀\\u0000\\u001f/\\\\\\\"\\b\\f\\n\\r\\t\",\"raw\":\"é😀 —
\"}}}",
|
||||
"workflow": "{\"s\":\"\\u00E9\\uD83D\\uDE00\\uD800\\uDC00x\\u0000\\u001f\\/\\\\\\\"\\b\\f\\n\\r\\t\\uD800\\u0041\\uDC00\"}"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "parsed",
|
||||
"workflow_graph": "parsed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "lone_surrogate_in_raw_text",
|
||||
"records": {
|
||||
"prompt": "{\"1\":{\"class_type\":\"A\",\"inputs\":{\"t\":\"\\udc00\"}}}",
|
||||
"parameters": "\ud800 lone\nSteps: 1"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "parsed",
|
||||
"workflow_graph": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "keys_with_escapes_proto_and_order",
|
||||
"records": {
|
||||
"prompt": "{\"10\":{\"class_type\":\"B\",\"inputs\":{}},\"3\":{\"class_type\":\"A\",\"inputs\":{}},\"__proto__\":{\"class_type\":\"P\",\"inputs\":{\"constructor\":1}},\"constructor\":{\"class_type\":\"C\",\"inputs\":{}},\"\\u0000\":5,\"\":[]}",
|
||||
"workflow": "{\"10\":1,\"3\":2,\"b\":3,\"a\":4,\"__proto__\":{\"x\":1},\"\":{}}"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "parsed",
|
||||
"workflow_graph": "parsed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "duplicate_key_top_level",
|
||||
"records": {
|
||||
"workflow": "{\"a\":1,\"a\":2}"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "absent",
|
||||
"workflow_graph": "malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "duplicate_key_nested",
|
||||
"records": {
|
||||
"workflow": "{\"a\":{\"b\":[{\"c\":1,\"c\":1}]}}"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "absent",
|
||||
"workflow_graph": "malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "duplicate_key_in_dropped_prompt_meta",
|
||||
"records": {
|
||||
"prompt": "{\"1\":{\"class_type\":\"A\",\"inputs\":{\"a\":1},\"_meta\":{\"t\":1,\"t\":2}}}"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "malformed",
|
||||
"workflow_graph": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "duplicate_key_in_prompt_inputs",
|
||||
"records": {
|
||||
"prompt": "{\"1\":{\"class_type\":\"A\",\"inputs\":{\"a\":1,\"a\":1}}}"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "malformed",
|
||||
"workflow_graph": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "duplicate_key_after_deep_nest",
|
||||
"records": {
|
||||
"workflow": "{\"x\":[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]],\"a\":1,\"a\":2}"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "absent",
|
||||
"workflow_graph": "malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "workflow_depth_64_kept",
|
||||
"records": {
|
||||
"workflow": "{\"a\":[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]}"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "absent",
|
||||
"workflow_graph": "parsed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "workflow_depth_65_limited",
|
||||
"records": {
|
||||
"workflow": "{\"a\":[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]}"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "absent",
|
||||
"workflow_graph": "limited"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "prompt_inputs_depth_64_kept",
|
||||
"records": {
|
||||
"prompt": "{\"1\":{\"class_type\":\"A\",\"inputs\":{\"v\":[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]}}}"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "parsed",
|
||||
"workflow_graph": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "prompt_inputs_depth_65_limited",
|
||||
"records": {
|
||||
"prompt": "{\"1\":{\"class_type\":\"A\",\"inputs\":{\"v\":[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]}}}"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "limited",
|
||||
"workflow_graph": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "prompt_dropped_meta_depth_65_limited",
|
||||
"records": {
|
||||
"prompt": "{\"1\":{\"class_type\":\"A\",\"inputs\":{},\"_meta\":{\"v\":[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]}}}"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "limited",
|
||||
"workflow_graph": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "prompt_non_object_nodes_kept",
|
||||
"records": {
|
||||
"prompt": "{\"1\":5,\"2\":[1,2.50,\"x\",null,true],\"3\":\"s\",\"4\":null,\"5\":{\"inputs\":[1],\"class_type\":null,\"other\":{\"deep\":[NaN]}}}"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "parsed",
|
||||
"workflow_graph": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "prompt_root_array_malformed",
|
||||
"records": {
|
||||
"prompt": "[]"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "malformed",
|
||||
"workflow_graph": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "prompt_root_string_malformed",
|
||||
"records": {
|
||||
"prompt": "\"{}\""
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "malformed",
|
||||
"workflow_graph": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "prompt_root_number_malformed",
|
||||
"records": {
|
||||
"prompt": "5"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "malformed",
|
||||
"workflow_graph": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "prompt_root_null_malformed",
|
||||
"records": {
|
||||
"prompt": "null"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "malformed",
|
||||
"workflow_graph": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "workflow_root_array_malformed",
|
||||
"records": {
|
||||
"workflow": "[1,2]"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "absent",
|
||||
"workflow_graph": "malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "workflow_root_nan_malformed",
|
||||
"records": {
|
||||
"workflow": "NaN"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "absent",
|
||||
"workflow_graph": "malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "empty_text_malformed",
|
||||
"records": {
|
||||
"prompt": "",
|
||||
"workflow": " "
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "malformed",
|
||||
"workflow_graph": "malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "bom_malformed",
|
||||
"records": {
|
||||
"workflow": "{}"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "absent",
|
||||
"workflow_graph": "malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "trailing_data_malformed",
|
||||
"records": {
|
||||
"workflow": "{\"a\":1} x",
|
||||
"prompt": "{}\u0000"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "malformed",
|
||||
"workflow_graph": "malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "python_whitespace_only",
|
||||
"records": {
|
||||
"workflow": " \t\r\n{\n\t\"a\" :\r 1 }\n",
|
||||
"prompt": "\u000b{}"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "malformed",
|
||||
"workflow_graph": "parsed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "syntax_errors",
|
||||
"records": {
|
||||
"prompt": "{\"1\":{\"class_type\":\"A\",\"inputs\":{},}}",
|
||||
"workflow": "[1,]"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "malformed",
|
||||
"workflow_graph": "malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "bad_escape_and_control_char",
|
||||
"records": {
|
||||
"prompt": "{\"1\":\"\\x\"}",
|
||||
"workflow": "{\"a\":\"\u0001\"}"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "malformed",
|
||||
"workflow_graph": "malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "number_grammar",
|
||||
"records": {
|
||||
"prompt": "{\"1\":01}",
|
||||
"workflow": "{\"a\":1.,\"b\":.5}"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "malformed",
|
||||
"workflow_graph": "malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "nodes_not_list_counts_zero",
|
||||
"records": {
|
||||
"workflow": "{\"nodes\":{\"a\":1},\"definitions\":{\"subgraphs\":[{\"nodes\":5},7,{\"nodes\":[1,2]}]}}"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "absent",
|
||||
"workflow_graph": "parsed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "limited_workflow_record",
|
||||
"records": {
|
||||
"prompt": "{\"1\":{\"class_type\":\"A\",\"inputs\":{\"x\":1}}}",
|
||||
"workflow": "{\"nodes\":[],\"version\":0.4}"
|
||||
},
|
||||
"limited": [
|
||||
"workflow"
|
||||
],
|
||||
"expect": {
|
||||
"prompt_graph": "parsed",
|
||||
"workflow_graph": "limited"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "limited_workflow_without_text",
|
||||
"records": {
|
||||
"prompt": "{\"1\":{\"class_type\":\"A\",\"inputs\":{\"x\":1}}}"
|
||||
},
|
||||
"limited": [
|
||||
"workflow"
|
||||
],
|
||||
"expect": {
|
||||
"prompt_graph": "parsed",
|
||||
"workflow_graph": "limited"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "invalid_workflow_format",
|
||||
"records": {
|
||||
"prompt": "{\"1\":{\"class_type\":\"A\",\"inputs\":{\"x\":1}}}"
|
||||
},
|
||||
"record_formats": {
|
||||
"prompt": "original-text",
|
||||
"workflow": "invalid"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "parsed",
|
||||
"workflow_graph": "malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "reserialized_without_carrier_is_limited",
|
||||
"records": {
|
||||
"prompt": "{\"1\":{\"class_type\":\"A\",\"inputs\":{\"x\":1}}}",
|
||||
"workflow": "{\"nodes\":[],\"version\":0.4}"
|
||||
},
|
||||
"record_formats": {
|
||||
"prompt": "reserialized-json",
|
||||
"workflow": "reserialized-json"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "limited",
|
||||
"workflow_graph": "limited"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "unknown_format_dropped_but_projected",
|
||||
"records": {
|
||||
"prompt": "{\"1\":{\"class_type\":\"A\",\"inputs\":{\"x\":1}}}"
|
||||
},
|
||||
"record_formats": {
|
||||
"prompt": "weird"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "parsed",
|
||||
"workflow_graph": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "non_text_record_dropped",
|
||||
"records": {
|
||||
"prompt": 5,
|
||||
"workflow": "{\"nodes\":[],\"version\":0.4}"
|
||||
},
|
||||
"record_formats": {
|
||||
"prompt": "original-text",
|
||||
"workflow": "original-text"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "malformed",
|
||||
"workflow_graph": "parsed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "format_without_text_is_malformed",
|
||||
"records": {},
|
||||
"record_formats": {
|
||||
"prompt": "original-text"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "malformed",
|
||||
"workflow_graph": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "unknown_record_keys_ignored",
|
||||
"records": {
|
||||
"prompt": "{\"1\":{\"class_type\":\"A\",\"inputs\":{\"x\":1}}}",
|
||||
"Software": "ComfyUI",
|
||||
"WORKFLOW": "{\"nodes\":[],\"version\":0.4}"
|
||||
},
|
||||
"record_formats": {
|
||||
"prompt": "original-text",
|
||||
"Software": "original-text",
|
||||
"WORKFLOW": "original-text"
|
||||
},
|
||||
"expect": {
|
||||
"prompt_graph": "parsed",
|
||||
"workflow_graph": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "carrier_prompt_string_workflow_object",
|
||||
"records": {
|
||||
"prompt": "{\"1\":{\"class_type\":\"A\",\"inputs\":{\"x\":1}}}",
|
||||
"workflow": "{\"nodes\": [], \"v\": 1}"
|
||||
},
|
||||
"record_formats": {
|
||||
"prompt": "original-text",
|
||||
"workflow": "reserialized-json"
|
||||
},
|
||||
"carrier_text": "{\"prompt\":\"{\\\"1\\\":{\\\"class_type\\\":\\\"A\\\",\\\"inputs\\\":{\\\"x\\\":1}}}\",\"workflow\":{\"nodes\":[],\"v\":1}}",
|
||||
"expect": {
|
||||
"prompt_graph": "parsed",
|
||||
"workflow_graph": "parsed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "carrier_prompt_object_projected",
|
||||
"records": {
|
||||
"prompt": "{\"1\": {\"class_type\": \"A\", \"inputs\": {\"x\": 8.0}, \"_meta\": {\"t\": \"x\"}}}",
|
||||
"workflow": "{\"nodes\":[],\"version\":0.4}"
|
||||
},
|
||||
"record_formats": {
|
||||
"prompt": "reserialized-json",
|
||||
"workflow": "original-text"
|
||||
},
|
||||
"carrier_text": "{\"prompt\":{\"1\":{\"class_type\":\"A\",\"inputs\":{\"x\":8.0},\"_meta\":{\"t\":\"x\"}}},\"workflow\":{\"nodes\":[],\"version\":0.4},\"title\":\"n\",\"extra\":[1e3]}",
|
||||
"expect": {
|
||||
"prompt_graph": "parsed",
|
||||
"workflow_graph": "parsed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "carrier_root_array_fails_all",
|
||||
"records": {
|
||||
"prompt": "{\"1\":{\"class_type\":\"A\",\"inputs\":{\"x\":1}}}",
|
||||
"workflow": "{\"nodes\":[],\"version\":0.4}"
|
||||
},
|
||||
"carrier_text": "[1]",
|
||||
"expect": {
|
||||
"prompt_graph": "malformed",
|
||||
"workflow_graph": "malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "carrier_duplicate_key_fails_all",
|
||||
"records": {
|
||||
"prompt": "{\"1\":{\"class_type\":\"A\",\"inputs\":{\"x\":1}}}"
|
||||
},
|
||||
"carrier_text": "{\"prompt\":\"{}\",\"prompt\":\"{}\"}",
|
||||
"expect": {
|
||||
"prompt_graph": "malformed",
|
||||
"workflow_graph": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "carrier_other_member_too_deep_limited",
|
||||
"records": {
|
||||
"prompt": "{\"1\":{\"class_type\":\"A\",\"inputs\":{\"x\":1}}}"
|
||||
},
|
||||
"carrier_text": "{\"title\":[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]],\"prompt\":\"{\\\"1\\\":{\\\"class_type\\\":\\\"A\\\",\\\"inputs\\\":{\\\"x\\\":1}}}\"}",
|
||||
"expect": {
|
||||
"prompt_graph": "limited",
|
||||
"workflow_graph": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "carrier_other_member_depth_64_kept",
|
||||
"records": {
|
||||
"prompt": "{\"1\":{\"class_type\":\"A\",\"inputs\":{\"x\":1}}}"
|
||||
},
|
||||
"carrier_text": "{\"title\":[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]],\"prompt\":\"{\\\"1\\\":{\\\"class_type\\\":\\\"A\\\",\\\"inputs\\\":{\\\"x\\\":1}}}\"}",
|
||||
"expect": {
|
||||
"prompt_graph": "parsed",
|
||||
"workflow_graph": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "carrier_prompt_object_depth_shift",
|
||||
"records": {
|
||||
"prompt": "x"
|
||||
},
|
||||
"record_formats": {
|
||||
"prompt": "reserialized-json"
|
||||
},
|
||||
"carrier_text": "{\"prompt\":{\"1\":{\"class_type\":\"A\",\"inputs\":{\"v\":[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]}}}}",
|
||||
"expect": {
|
||||
"prompt_graph": "parsed",
|
||||
"workflow_graph": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "carrier_prompt_object_depth_shift_limited",
|
||||
"records": {
|
||||
"prompt": "x"
|
||||
},
|
||||
"record_formats": {
|
||||
"prompt": "reserialized-json"
|
||||
},
|
||||
"carrier_text": "{\"prompt\":{\"1\":{\"class_type\":\"A\",\"inputs\":{\"v\":[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]}}}}",
|
||||
"expect": {
|
||||
"prompt_graph": "limited",
|
||||
"workflow_graph": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "carrier_prompt_string_invalid_json",
|
||||
"records": {
|
||||
"prompt": "{",
|
||||
"workflow": "{\"nodes\":[],\"version\":0.4}"
|
||||
},
|
||||
"carrier_text": "{\"prompt\":\"{\",\"workflow\":\"{\\\"nodes\\\":[],\\\"version\\\":0.4}\"}",
|
||||
"expect": {
|
||||
"prompt_graph": "malformed",
|
||||
"workflow_graph": "parsed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "carrier_prompt_string_too_deep",
|
||||
"records": {
|
||||
"prompt": "x"
|
||||
},
|
||||
"carrier_text": "{\"prompt\":\"{\\\"1\\\":{\\\"class_type\\\":\\\"A\\\",\\\"inputs\\\":{\\\"v\\\":[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]}}}\"}",
|
||||
"expect": {
|
||||
"prompt_graph": "limited",
|
||||
"workflow_graph": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "carrier_workflow_number_malformed",
|
||||
"records": {
|
||||
"prompt": "{\"1\":{\"class_type\":\"A\",\"inputs\":{\"x\":1}}}",
|
||||
"workflow": "5"
|
||||
},
|
||||
"record_formats": {
|
||||
"prompt": "original-text",
|
||||
"workflow": "original-text"
|
||||
},
|
||||
"carrier_text": "{\"prompt\":\"{\\\"1\\\":{\\\"class_type\\\":\\\"A\\\",\\\"inputs\\\":{\\\"x\\\":1}}}\",\"workflow\":5}",
|
||||
"expect": {
|
||||
"prompt_graph": "parsed",
|
||||
"workflow_graph": "malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "carrier_prompt_nonfinite_malformed",
|
||||
"records": {
|
||||
"prompt": "NaN"
|
||||
},
|
||||
"carrier_text": "{\"prompt\":NaN}",
|
||||
"expect": {
|
||||
"prompt_graph": "malformed",
|
||||
"workflow_graph": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "carrier_prompt_bigint_malformed",
|
||||
"records": {
|
||||
"prompt": "123456789012345678901"
|
||||
},
|
||||
"carrier_text": "{\"prompt\":123456789012345678901}",
|
||||
"expect": {
|
||||
"prompt_graph": "malformed",
|
||||
"workflow_graph": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "carrier_prompt_decimal_malformed",
|
||||
"records": {
|
||||
"prompt": "1.5"
|
||||
},
|
||||
"carrier_text": "{\"prompt\":1.5}",
|
||||
"expect": {
|
||||
"prompt_graph": "malformed",
|
||||
"workflow_graph": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "carrier_missing_member_malformed",
|
||||
"records": {
|
||||
"prompt": "{\"1\":{\"class_type\":\"A\",\"inputs\":{\"x\":1}}}"
|
||||
},
|
||||
"carrier_text": "{\"workflow\":{}}",
|
||||
"expect": {
|
||||
"prompt_graph": "malformed",
|
||||
"workflow_graph": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "carrier_without_graph_records",
|
||||
"records": {
|
||||
"parameters": "cat"
|
||||
},
|
||||
"carrier_text": "{\"parameters\":\"cat\"}",
|
||||
"expect": {
|
||||
"prompt_graph": "absent",
|
||||
"workflow_graph": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "carrier_with_limited_workflow",
|
||||
"records": {
|
||||
"prompt": "{\"1\":{\"class_type\":\"A\",\"inputs\":{\"x\":1}}}"
|
||||
},
|
||||
"limited": [
|
||||
"workflow"
|
||||
],
|
||||
"carrier_text": "{\"prompt\":\"{\\\"1\\\":{\\\"class_type\\\":\\\"A\\\",\\\"inputs\\\":{\\\"x\\\":1}}}\"}",
|
||||
"expect": {
|
||||
"prompt_graph": "parsed",
|
||||
"workflow_graph": "limited"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "carrier_invalid_workflow_format",
|
||||
"records": {
|
||||
"prompt": "{\"1\":{\"class_type\":\"A\",\"inputs\":{\"x\":1}}}"
|
||||
},
|
||||
"record_formats": {
|
||||
"prompt": "original-text",
|
||||
"workflow": "invalid"
|
||||
},
|
||||
"carrier_text": "{\"prompt\":\"{\\\"1\\\":{\\\"class_type\\\":\\\"A\\\",\\\"inputs\\\":{\\\"x\\\":1}}}\",\"workflow\":null}",
|
||||
"expect": {
|
||||
"prompt_graph": "parsed",
|
||||
"workflow_graph": "malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "carrier_non_string_text_malformed",
|
||||
"records": {
|
||||
"prompt": "{\"1\":{\"class_type\":\"A\",\"inputs\":{\"x\":1}}}"
|
||||
},
|
||||
"carrier_text": "",
|
||||
"expect": {
|
||||
"prompt_graph": "malformed",
|
||||
"workflow_graph": "absent"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,14 +1,19 @@
|
||||
// The browser's local readers run over files on disk, for
|
||||
// The browser's local readers and projection run over files on disk, for
|
||||
// tests/test_local_inspect_parity.py, which compares every answer with the server's
|
||||
// utils.prompt_inspector.read_inspection_file on the same file. Each file is opened
|
||||
// as a Blob (fs.openAsBlob) so the reader slices it exactly as it slices a File.
|
||||
// stdin: [{name, path} | {name, tags, limited?}]
|
||||
// stdout: {name: {classified, result}} where result is readPngMetadataFile's
|
||||
// utils.prompt_inspector.read_inspection_file and utils.inspection_response._metadata
|
||||
// on the same file. Each file is opened as a Blob (fs.openAsBlob) so the reader
|
||||
// slices it exactly as it slices a File.
|
||||
// stdin: [{name, path} | {name, tags, limited?} | {name, records, record_formats?, carrier_text?, limited?}]
|
||||
// stdout: {name: {classified, result, metadata}} where result is readPngMetadataFile's
|
||||
// answer for a PNG, readImageHeader's for another image, for a video
|
||||
// {read: readVideoTags(...), selection: selectVideoCarrier(read.tags, {limited})}
|
||||
// (selection null unless the read completed), else null.
|
||||
// A {tags} entry answers {classified: null, result: {selection}} for the
|
||||
// carrier rule alone, as the server selects from ffprobe's tag map.
|
||||
// (selection null unless the read completed), else null; metadata is
|
||||
// projectMetadata(...) of the local read (the server's _metadata) for every
|
||||
// classified file, else null.
|
||||
// A {tags} entry answers {classified: null, result: {selection}, metadata} for
|
||||
// the carrier rule alone, as the server selects from ffprobe's tag map.
|
||||
// A {records} entry (records null = an unreadable file) answers
|
||||
// {classified: null, result: null, metadata} for the projection alone.
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { createRequire } from 'node:module';
|
||||
@@ -16,28 +21,37 @@ import { fileURLToPath } from 'node:url';
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
const require = createRequire(import.meta.url);
|
||||
const { classifyPrefix, readPngMetadataFile, readImageHeader } = require(path.join(here, '..', '..', '..', 'web', 'js', 'png-metadata.js'));
|
||||
const { readVideoTags, selectVideoCarrier } = await import(path.join(here, '..', '..', '..', 'web', 'js', 'workspace', 'local-video.mjs'));
|
||||
const pngMetadata = require(path.join(here, '..', '..', '..', 'web', 'js', 'png-metadata.js'));
|
||||
const web = path.join(here, '..', '..', '..', 'web', 'js', 'workspace');
|
||||
const { selectVideoCarrier } = await import(path.join(web, 'local-video.mjs'));
|
||||
const { projectMetadata } = await import(path.join(web, 'local-projection.mjs'));
|
||||
const { readLocalFile, toInspectionRead } = await import(path.join(web, 'local-inspection.mjs'));
|
||||
|
||||
const selectionRead = (selection) => selection.status === 'complete' ?
|
||||
{ records: selection.records, record_formats: selection.record_formats, carrierText: selection.carrierText, limited: selection.limited } : null;
|
||||
|
||||
const chunks = [];
|
||||
for await (const chunk of process.stdin) chunks.push(chunk);
|
||||
const output = {};
|
||||
for (const entry of JSON.parse(Buffer.concat(chunks).toString('utf8'))) {
|
||||
const { name } = entry;
|
||||
if (entry.records !== undefined) {
|
||||
const read = entry.records === null ? null : {
|
||||
records: entry.records,
|
||||
record_formats: entry.record_formats ?? Object.fromEntries(Object.keys(entry.records).map((key) => [key, 'original-text'])),
|
||||
carrierText: entry.carrier_text ?? null, limited: entry.limited ?? [],
|
||||
};
|
||||
output[name] = { classified: null, result: null, metadata: projectMetadata(read) };
|
||||
continue;
|
||||
}
|
||||
if (entry.path === undefined) {
|
||||
output[name] = { classified: null, result: { selection: selectVideoCarrier(entry.tags, { limited: entry.limited || [] }) } };
|
||||
const selection = selectVideoCarrier(entry.tags, { limited: entry.limited || [] });
|
||||
output[name] = { classified: null, result: { selection }, metadata: projectMetadata(selectionRead(selection)) };
|
||||
continue;
|
||||
}
|
||||
const file = await fs.openAsBlob(entry.path);
|
||||
const prefix = new Uint8Array(await file.slice(0, Math.min(32, file.size)).arrayBuffer());
|
||||
const classified = classifyPrefix(prefix);
|
||||
let result = null;
|
||||
if (classified?.kind === 'png') result = await readPngMetadataFile(file, { prefix });
|
||||
else if (classified?.media === 'image') result = await readImageHeader(file, classified.kind);
|
||||
else if (classified?.media === 'video') {
|
||||
const read = await readVideoTags(file, { prefix, kind: classified.kind });
|
||||
result = { read, selection: read.status === 'complete' ? selectVideoCarrier(read.tags, { limited: read.limited }) : null };
|
||||
}
|
||||
output[name] = { classified, result };
|
||||
const { classification, result } = await readLocalFile(file, { pngMetadata });
|
||||
const metadata = classification === null ? null : projectMetadata(toInspectionRead(classification, result).read);
|
||||
output[name] = { classified: classification, result, metadata };
|
||||
}
|
||||
process.stdout.write(JSON.stringify(output));
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
// web/js/workspace/local-inspection.mjs turns one chosen File's local reads into
|
||||
// the inspection wire the workspace already renders, fits it into the 8 MiB
|
||||
// response budget exactly as the server does, and plugs into the shared
|
||||
// inspection controller, inspector and viewer through localFileAdapter.
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { createRequire } from "node:module";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { raw, ihdr, idat, iend, tEXt, iTXt, utf8 } from "./helpers/png-bytes.mjs";
|
||||
import { createInspectionController, toInspectionRecord, projectInspectionPayload } from "../../web/js/workspace/inspection-state.mjs";
|
||||
import { inspectionDownloads } from "../../web/js/workspace/file-inspector.mjs";
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
const require = createRequire(import.meta.url);
|
||||
const PngMetadata = require("../../web/js/png-metadata.js");
|
||||
const ComfyMetadata = require("../../web/js/comfy-metadata.js");
|
||||
const { extractGenerationFacts } = ComfyMetadata;
|
||||
// The page loads both as scripts; the legacy fill and the PNG reader find them on the global.
|
||||
globalThis.PngMetadata = PngMetadata;
|
||||
globalThis.ComfyMetadata = ComfyMetadata;
|
||||
const m = await import("../../web/js/workspace/local-inspection.mjs").catch((error) => {
|
||||
if (error.code !== "ERR_MODULE_NOT_FOUND") throw error;
|
||||
return {};
|
||||
});
|
||||
|
||||
const FIXTURES = path.join(here, "..", "fixtures", "local-inspect");
|
||||
const PARITY = path.join(here, "..", "fixtures", "inspection_parity");
|
||||
const PROMPT = '{"3":{"class_type":"KSampler","inputs":{"seed":7,"steps":20,"cfg":8.0,"sampler_name":"euler","scheduler":"normal","denoise":1.0,"model":["4",0],"positive":["6",0],"negative":["7",0],"latent_image":["5",0]}},"4":{"class_type":"CheckpointLoaderSimple","inputs":{"ckpt_name":"synthetic-v1.safetensors"}},"5":{"class_type":"EmptyLatentImage","inputs":{"width":512,"height":512,"batch_size":1}},"6":{"class_type":"CLIPTextEncode","inputs":{"text":"a red fox in snow, detailed fur","clip":["4",1]}},"7":{"class_type":"CLIPTextEncode","inputs":{"text":"blurry, lowres","clip":["4",1]}}}';
|
||||
const WORKFLOW = '{"last_node_id":7,"nodes":[{"id":3,"type":"KSampler","widgets_values":[7,"fixed",20,8.0,"euler","normal",1.0]}],"links":[],"version":0.4}';
|
||||
const CONTAINER_WORKFLOW = "Original workflow JSON text is unavailable for this container record. Download metadata or the original file.";
|
||||
const LIMIT = 8388608;
|
||||
const plain = (value) => JSON.parse(JSON.stringify(value));
|
||||
const bytes = (value) => Buffer.byteLength(JSON.stringify(value), "utf8");
|
||||
|
||||
function pngFile(name, chunks, { lastModified = 1_700_000_000_123 } = {}) {
|
||||
return new File([raw(ihdr(640, 480), idat(), ...chunks, iend())], name, { type: "image/png", lastModified });
|
||||
}
|
||||
const fixtureFile = (name, directory = FIXTURES) => new File([fs.readFileSync(path.join(directory, name))], name, { lastModified: 1_700_000_000_000 });
|
||||
const classify = async (file) => PngMetadata.classifyPrefix(new Uint8Array(await file.slice(0, 32).arrayBuffer()));
|
||||
async function handleFor(file, { url = "blob:http://host.test/11111111-2222-3333-4444-555555555555", filename } = {}) {
|
||||
return m.createLocalFileHandle({ file, url, classification: await classify(file), filename });
|
||||
}
|
||||
async function recordFor(file, options) {
|
||||
const handle = await handleFor(file, options);
|
||||
const wire = await m.localFileAdapter.read(handle);
|
||||
return { handle, wire, record: m.localFileAdapter.toRecord(wire, handle, extractGenerationFacts) };
|
||||
}
|
||||
|
||||
test("contract", () => {
|
||||
for (const name of ["buildLocalPayload", "fitLocalPayload", "createLocalFileHandle", "readLocalFile", "toInspectionRead", "sanitizeLocalFilename"]) {
|
||||
assert.equal(typeof m[name], "function", `${name} is exported`);
|
||||
}
|
||||
assert.equal(typeof m.localFileAdapter, "object");
|
||||
for (const name of ["validate", "key", "scope", "inspectionPath", "read", "toRecord", "originalDownload", "mediaUrl"]) {
|
||||
assert.equal(typeof m.localFileAdapter[name], "function", `adapter.${name}`);
|
||||
}
|
||||
assert.ok(Object.isFrozen(m.localFileAdapter));
|
||||
});
|
||||
|
||||
test("a PNG File becomes the server-shaped wire with local provenance", async () => {
|
||||
const file = pngFile("Fox Render.PNG", [tEXt("prompt", PROMPT), tEXt("workflow", WORKFLOW)]);
|
||||
const { wire, record, handle } = await recordFor(file);
|
||||
assert.deepEqual(wire.file, { observation: "selected-local-file", source_label: "Selected local file", media_type: "image", extension: ".png", format: "PNG",
|
||||
size: file.size, width: 640, height: 480, modified_time: 1_700_000_000.123 });
|
||||
assert.equal(wire.success, true);
|
||||
assert.equal(wire.inspection_version, 1);
|
||||
assert.deepEqual(wire.prompts, { positive: { status: "unresolved" }, negative: { status: "unresolved" } });
|
||||
assert.deepEqual(wire.resolution, { state: "unresolved", sampler_id: null, reason: "Resolved in this browser from the workflow.", reason_code: null, node: null });
|
||||
assert.deepEqual(wire.library, { status: "unavailable", scope: "saved-library", tags: [], category_scope: null, tags_scope: null });
|
||||
assert.equal(wire.metadata.status, "available");
|
||||
assert.deepEqual(wire.metadata.raw, { prompt: PROMPT, workflow: WORKFLOW });
|
||||
assert.deepEqual(wire.metadata.record_formats, { prompt: "original-text", workflow: "original-text" });
|
||||
assert.equal(wire.metadata.prompt_graph.status, "parsed");
|
||||
assert.equal(plain(wire.metadata.prompt_graph.value)["3"].inputs.cfg, "8.0");
|
||||
assert.equal(wire.metadata.workflow_graph.status, "parsed");
|
||||
assert.equal(wire.metadata.parameters, null);
|
||||
for (const key of Object.keys(wire)) assert.ok(["success", "inspection_version", "file", "prompts", "resolution", "metadata", "library"].includes(key), key);
|
||||
// The existing projection renders it: facts from the API graph, legacy fill for the roles, no library link.
|
||||
assert.deepEqual(record.identity, { kind: "local-file", key: handle.key, path: "Fox Render.PNG" });
|
||||
assert.equal(record.file.observation, "selected-local-file");
|
||||
assert.equal(record.generation.seed.value, "7");
|
||||
assert.equal(record.generation.cfg.value, "8.0");
|
||||
assert.equal(record.prompts.positive.text, "a red fox in snow, detailed fur");
|
||||
assert.equal(record.prompts.positive.source, "graph-inferred");
|
||||
assert.equal(record.prompts.negative.text, "blurry, lowres");
|
||||
assert.equal(record.historyTarget, null);
|
||||
assert.deepEqual(record.download, { url: "blob:http://host.test/11111111-2222-3333-4444-555555555555", filename: "Fox Render.PNG" });
|
||||
const downloads = inspectionDownloads(record);
|
||||
assert.equal(downloads.metadata.available, true);
|
||||
assert.equal(downloads.workflow.bodyText, WORKFLOW);
|
||||
assert.equal(downloads.metadata.filename, "Fox Render.metadata.json");
|
||||
const exported = JSON.parse(downloads.metadata.bodyText);
|
||||
assert.equal(exported.identity.kind, "local-file");
|
||||
assert.equal(exported.file.source_label, "Selected local file");
|
||||
});
|
||||
|
||||
test("file facts follow the server: extension like Path.suffix, null when unknown", async () => {
|
||||
const cases = [["a.b.PnG", ".png"], [".hidden", ""], ["noext", ""], ["dot.", ""], ["x.tar.gz", ".gz"]];
|
||||
for (const [name, extension] of cases) {
|
||||
const wire = m.buildLocalPayload({ read: { status: "complete", records: {}, limited: [], width: 2, height: 3 }, file: { name, size: 10, lastModified: 0 }, classification: { media: "image", kind: "png" } });
|
||||
assert.equal(wire.file.extension, extension, name);
|
||||
assert.equal(wire.file.modified_time, 0);
|
||||
}
|
||||
const sparse = m.buildLocalPayload({ read: { status: "complete", records: {}, limited: [], width: 2, height: 3 }, file: { name: "a.png" }, classification: { media: "image", kind: "png" } });
|
||||
assert.deepEqual([sparse.file.size, sparse.file.modified_time], [null, null]);
|
||||
assert.equal(m.buildLocalPayload({ read: { status: "complete", records: {}, limited: [], width: 2, height: 3 }, file: { name: "a.png", size: -1, lastModified: NaN }, classification: { media: "image", kind: "png" } }).file.size, null);
|
||||
assert.throws(() => m.buildLocalPayload({ read: null, file: { name: "a" }, classification: null }), TypeError);
|
||||
assert.throws(() => m.buildLocalPayload({ read: null, file: { name: "a" }, classification: { media: "text", kind: "svg" } }), TypeError);
|
||||
});
|
||||
|
||||
test("sanitized filenames carry no separators or control characters", () => {
|
||||
assert.equal(m.sanitizeLocalFilename("../a/b\\c\u0000\u001f\u007f.png"), ".._a_b_c.png");
|
||||
assert.equal(m.sanitizeLocalFilename(" spaced .png "), "spaced .png");
|
||||
assert.equal(m.sanitizeLocalFilename(""), "file");
|
||||
assert.equal(m.sanitizeLocalFilename(undefined), "file");
|
||||
assert.equal(m.sanitizeLocalFilename("\u0000"), "file");
|
||||
assert.equal([...m.sanitizeLocalFilename("😀".repeat(300))].length, 255);
|
||||
});
|
||||
|
||||
test("unreadable PNG metadata is unreadable, not absent", async () => {
|
||||
const { wire, record } = await recordFor(pngFile("bad.png", [tEXt("prompt", PROMPT, { crc: 0 })]));
|
||||
assert.equal(wire.metadata.status, "unreadable");
|
||||
assert.deepEqual([wire.file.format, wire.file.width, wire.file.height], [null, null, null]);
|
||||
assert.equal(record.generation.seed.reason, "metadata-unreadable");
|
||||
assert.equal(record.prompts.positive.status, "unresolved");
|
||||
assert.equal(JSON.parse(inspectionDownloads(record).metadata.bodyText).metadata.status, "unreadable");
|
||||
const limited = await recordFor(pngFile("big.png", [tEXt("prompt", PROMPT), tEXt("workflow", "[" + "0,".repeat(2_097_153) + "]")]));
|
||||
assert.deepEqual(limited.wire.metadata.record_formats, { prompt: "original-text", workflow: "limited" });
|
||||
assert.equal(limited.wire.metadata.workflow_graph.status, "limited");
|
||||
assert.equal(limited.record.metadata.record_formats.workflow, "limited");
|
||||
});
|
||||
|
||||
test("JPEG and WebP previews have header facts and absent metadata", async () => {
|
||||
for (const [name, format, width, height] of [["tiny.jpg", "JPEG", 48, 32], ["tiny.webp", "WEBP", 40, 24]]) {
|
||||
const { wire, record } = await recordFor(fixtureFile(name));
|
||||
assert.deepEqual([wire.file.format, wire.file.width, wire.file.height, wire.file.media_type], [format, width, height, "image"], name);
|
||||
assert.equal(wire.metadata.status, "absent");
|
||||
assert.equal(record.generation.seed.status, "not-recorded");
|
||||
}
|
||||
});
|
||||
|
||||
test("video payloads", async () => {
|
||||
const legacy = await recordFor(fixtureFile("legacy-comment.mp4"));
|
||||
assert.deepEqual([legacy.wire.file.media_type, legacy.wire.file.format, legacy.wire.file.width, legacy.wire.file.extension], ["video", null, null, ".mp4"]);
|
||||
assert.equal(legacy.wire.metadata.record_formats.workflow, "reserialized-json");
|
||||
assert.equal(legacy.wire.metadata.record_formats.prompt, "original-text");
|
||||
assert.equal(legacy.wire.metadata.workflow_graph.status, "parsed");
|
||||
assert.equal(inspectionDownloads(legacy.record).workflow.available, false);
|
||||
assert.equal(inspectionDownloads(legacy.record).workflow.reason, CONTAINER_WORKFLOW);
|
||||
assert.equal(legacy.record.prompts.positive.text, "a red fox in snow, detailed fur");
|
||||
// Ruling M1: a Matroska COMMENT tag is not the exact-case `comment` envelope, so it is absent, as Images reports it.
|
||||
const webm = await recordFor(fixtureFile("legacy-comment.webm"));
|
||||
assert.equal(webm.wire.metadata.status, "absent");
|
||||
const tags = await recordFor(fixtureFile("core-tags.webm"));
|
||||
assert.deepEqual(tags.wire.metadata.record_formats, { prompt: "original-text", workflow: "original-text" });
|
||||
assert.equal(tags.record.generation.seed.value, "7");
|
||||
// AVI and ASF are recognised, previewable, and never read.
|
||||
const avi = new File([utf8("RIFF"), new Uint8Array([0, 0, 0, 0]), utf8("AVI LIST"), new Uint8Array(64)], "clip.avi");
|
||||
const aviRecord = await recordFor(avi);
|
||||
assert.equal(aviRecord.wire.metadata.status, "unreadable");
|
||||
assert.equal(aviRecord.wire.file.media_type, "video");
|
||||
assert.equal(aviRecord.record.generation.seed.reason, "metadata-unreadable");
|
||||
const asf = new File([new Uint8Array([0x30, 0x26, 0xb2, 0x75, 0x8e, 0x66, 0xcf, 0x11, 0xa6, 0xd9, 0x00, 0xaa, 0x00, 0x62, 0xce, 0x6c]), new Uint8Array(64)], "clip.wmv");
|
||||
assert.equal((await recordFor(asf)).wire.metadata.status, "unreadable");
|
||||
// A pre-classified video needs no PNG reader; an image without one is an error, not a silent absence.
|
||||
const clip = fixtureFile("core.mkv");
|
||||
assert.equal((await m.readLocalFile(clip, { classification: { media: "video", kind: "matroska" }, pngMetadata: null })).result.read.status, "complete");
|
||||
await assert.rejects(m.readLocalFile(clip, { pngMetadata: null }), /PNG reader/);
|
||||
await assert.rejects(m.readLocalFile(fixtureFile("tiny.jpg"), { classification: { media: "image", kind: "jpeg" }, pngMetadata: null }), /PNG reader/);
|
||||
await assert.rejects(m.readLocalFile("not a blob"), TypeError);
|
||||
assert.deepEqual(await m.readLocalFile(new File([utf8("<svg/>")], "x.png"), { pngMetadata: PngMetadata }), { classification: null, result: null });
|
||||
// A container that fails to parse is unreadable too.
|
||||
const broken = new File([utf8("\0\0\0\x08ftypisom"), utf8("\0\0\0\x01moov")], "broken.mp4");
|
||||
assert.equal((await recordFor(broken)).wire.metadata.status, "unreadable");
|
||||
});
|
||||
|
||||
test("local cap measures escaped UTF-8 atomically", async () => {
|
||||
// A 1 MiB Latin-1 prompt of 'é' (2 UTF-8 bytes, 1 code unit each) and a 4 MiB UTF-8 workflow of
|
||||
// emoji (4 bytes, 2 units): the UTF-16 length of the wire is well under the limit while its UTF-8
|
||||
// size is not, so a cap counting code units would keep too much.
|
||||
const prompt = `{"1":{"class_type":"A","inputs":{"t":"${"é".repeat(1_000_000)}"}}}`;
|
||||
const workflow = `{"nodes":[],"u":"${"😀".repeat(1_000_000)}"}`;
|
||||
const file = pngFile("wide.png", [iTXt("workflow", workflow), tEXt("prompt", prompt)]);
|
||||
const handle = await handleFor(file);
|
||||
const raw = await m.readLocalFile(handle.file, { classification: handle.classification, pngMetadata: PngMetadata });
|
||||
assert.equal(raw.result.status, "complete");
|
||||
const wire = m.buildLocalPayload({ read: raw.result, file: { name: file.name, size: file.size, lastModified: file.lastModified }, classification: handle.classification });
|
||||
const text = JSON.stringify(wire);
|
||||
assert.ok(text.length < LIMIT && bytes(wire) > LIMIT, `${text.length} code units, ${bytes(wire)} bytes`);
|
||||
const fitted = m.fitLocalPayload(wire);
|
||||
assert.ok(bytes(fitted) <= LIMIT);
|
||||
assert.equal(fitted.metadata.workflow_graph.status, "limited", "the workflow graph goes first");
|
||||
assert.equal(fitted.metadata.prompt_graph.status, "parsed");
|
||||
assert.deepEqual(Object.keys(fitted.metadata.raw).sort(), ["prompt", "workflow"], "raw is kept");
|
||||
const record = m.localFileAdapter.toRecord(fitted, handle, extractGenerationFacts);
|
||||
assert.equal(inspectionDownloads(record).metadata.available, true);
|
||||
assert.equal(inspectionDownloads(record).workflow.available, false);
|
||||
// Whole-raw omission is atomic and marks the response-size limit; the inspector offers no metadata download.
|
||||
const squeezed = m.fitLocalPayload(wire, bytes(fitted) - 1);
|
||||
assert.deepEqual([squeezed.metadata.workflow_graph.status, squeezed.metadata.prompt_graph.status], ["limited", "limited"]);
|
||||
const tiny = m.fitLocalPayload(wire, 2000);
|
||||
assert.equal(tiny.metadata.status, "limited");
|
||||
assert.equal(tiny.metadata.omitted_reason, "response-size-limit");
|
||||
assert.deepEqual(tiny.metadata.raw, {});
|
||||
assert.equal(tiny.metadata.parameters, null);
|
||||
assert.ok(bytes(tiny) <= 2000);
|
||||
const omitted = m.localFileAdapter.toRecord(tiny, handle, extractGenerationFacts);
|
||||
assert.equal(inspectionDownloads(omitted).metadata.available, false);
|
||||
assert.equal(omitted.generation.seed.reason, "projection-limited");
|
||||
assert.throws(() => m.fitLocalPayload(wire, 100), /exceeds/);
|
||||
assert.equal(wire.metadata.status, "available", "the input wire is not mutated");
|
||||
assert.equal((await m.localFileAdapter.read(handle)).metadata.workflow_graph.status, "limited", "adapter.read returns the fitted wire");
|
||||
});
|
||||
|
||||
test("adapter rejects foreign blob URLs and never resolves owned ones", async () => {
|
||||
const file = pngFile("a.png", [tEXt("prompt", PROMPT)]);
|
||||
const classification = await classify(file);
|
||||
const owned = m.createLocalFileHandle({ file, url: "blob:http://host.test/0b0e", classification });
|
||||
assert.ok(Object.isFrozen(owned));
|
||||
assert.deepEqual(m.localFileAdapter.originalDownload(owned), { url: "blob:http://host.test/0b0e", filename: "a.png" });
|
||||
assert.equal(m.localFileAdapter.mediaUrl(owned), "blob:http://host.test/0b0e");
|
||||
// The controller and the viewer session spread-copy descriptors: a copy is the same selection.
|
||||
const copy = { ...owned, occurrence: {}, reference: {} };
|
||||
assert.equal(m.localFileAdapter.key(copy), owned.key);
|
||||
assert.equal(m.localFileAdapter.mediaUrl(copy), "blob:http://host.test/0b0e");
|
||||
// A tampered URL, key, File or name, or a descriptor this module never made, is foreign.
|
||||
const tampered = [{ ...owned, url: "blob:http://host.test/other" }, { ...owned, key: "local:1" }, { ...owned, file: new File([], "x.png") }, { ...owned, filename: "other.png" },
|
||||
{ kind: "local-file", key: owned.key, filename: owned.filename, url: owned.url, file, is_video: false, classification }, null, "local:1"];
|
||||
for (const foreign of tampered) {
|
||||
for (const method of ["validate", "originalDownload", "mediaUrl", "key", "scope", "inspectionPath"]) assert.throws(() => m.localFileAdapter[method](foreign), TypeError, method);
|
||||
await assert.rejects(m.localFileAdapter.read(foreign), TypeError);
|
||||
assert.throws(() => m.localFileAdapter.toRecord({ success: true, inspection_version: 1 }, foreign, extractGenerationFacts), TypeError);
|
||||
}
|
||||
for (const url of ["http://host.test/x.png", "/prompt_manager/images/serve/a.png", "javascript:alert(1)", "data:image/png;base64,AA==", "", 5]) {
|
||||
assert.throws(() => m.createLocalFileHandle({ file, url, classification }), TypeError, String(url));
|
||||
}
|
||||
assert.throws(() => m.createLocalFileHandle({ file: "not a blob", url: "blob:http://host.test/1", classification }), TypeError);
|
||||
assert.throws(() => m.createLocalFileHandle({ file, url: "blob:http://host.test/1", classification: null }), TypeError);
|
||||
assert.throws(() => m.createLocalFileHandle({ file, url: "blob:http://host.test/1", classification: { media: "text", kind: "svg" } }), TypeError);
|
||||
// A proxy-mounted host resolves API URLs; blob URLs are session-owned and bypass it.
|
||||
const before = Object.getOwnPropertyDescriptor(globalThis, "window");
|
||||
Object.defineProperty(globalThis, "window", { configurable: true, value: { location: { href: "https://example.test/proxy/prompt_manager/workspace" }, ApiClient: { resolveUrl: (url) => `/proxy${url}` } } });
|
||||
try {
|
||||
assert.equal(m.localFileAdapter.mediaUrl(owned), "blob:http://host.test/0b0e");
|
||||
assert.equal(m.localFileAdapter.originalDownload(owned).url, "blob:http://host.test/0b0e");
|
||||
} finally {
|
||||
if (before) Object.defineProperty(globalThis, "window", before); else delete globalThis.window;
|
||||
}
|
||||
// Without an object URL the record has no download and the viewer cannot show the original.
|
||||
const previewless = m.createLocalFileHandle({ file, url: null, classification });
|
||||
assert.equal(m.localFileAdapter.mediaUrl(previewless), null);
|
||||
assert.throws(() => m.localFileAdapter.originalDownload(previewless), TypeError);
|
||||
const record = m.localFileAdapter.toRecord(await m.localFileAdapter.read(previewless), previewless, extractGenerationFacts);
|
||||
assert.equal(record.download, null);
|
||||
assert.equal(inspectionDownloads(record).metadata.filename, "a.metadata.json");
|
||||
});
|
||||
|
||||
test("select reuses the same local key", async () => {
|
||||
const file = pngFile("a.png", [tEXt("prompt", PROMPT)]);
|
||||
const classification = await classify(file);
|
||||
const first = m.createLocalFileHandle({ file, url: "blob:http://host.test/1", classification });
|
||||
const second = m.createLocalFileHandle({ file, url: "blob:http://host.test/2", classification });
|
||||
assert.notEqual(first.key, second.key, "the same bytes chosen twice are two selections");
|
||||
assert.equal(m.localFileAdapter.scope(first), "local");
|
||||
assert.equal(m.localFileAdapter.inspectionPath(first), `local-file:${first.key}`);
|
||||
assert.equal(m.localFileAdapter.key(first), first.key);
|
||||
const reads = [];
|
||||
// Until the loader seam lands (PR L2 Task 4) the controller reaches the adapter through api.get.
|
||||
const api = { get: (sentinel) => { reads.push(sentinel); return m.localFileAdapter.read(reads.length === 1 || sentinel === `local-file:${first.key}` ? first : second); } };
|
||||
const controller = createInspectionController({ api, extractFacts: extractGenerationFacts, adapter: m.localFileAdapter });
|
||||
const pending = controller.select(first);
|
||||
assert.equal(controller.select(first), pending, "the same handle returns the pending read");
|
||||
await pending;
|
||||
assert.deepEqual(reads, [`local-file:${first.key}`]);
|
||||
assert.equal(controller.state.record.identity.key, first.key);
|
||||
assert.equal(controller.state.record.prompts.positive.text, "a red fox in snow, detailed fur");
|
||||
await controller.select(second);
|
||||
assert.equal(reads.length, 2);
|
||||
assert.equal(controller.state.record.identity.key, second.key);
|
||||
await controller.select({ ...second, occurrence: {}, reference: {} });
|
||||
assert.equal(reads.length, 2, "the controller's own copy is the same selection");
|
||||
await controller.select({ ...second, url: "blob:http://host.test/evil" });
|
||||
assert.equal(controller.state.error, true, "a foreign handle is published as an error, never read");
|
||||
assert.equal(reads.length, 2);
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
test("fileFor admits selected-local-file only for a local identity", async () => {
|
||||
const file = pngFile("a.png", [tEXt("prompt", PROMPT)]);
|
||||
const { wire, handle } = await recordFor(file);
|
||||
const rooted = { root_index: 0, path: "folder/a.png", filename: "a.png", url: "/prompt_manager/images/serve/folder/a.png?root=0" };
|
||||
assert.throws(() => toInspectionRecord({ ...wire, image: { root_index: 0, path: "folder/a.png", filename: "a.png" } }, rooted, extractGenerationFacts), /Invalid inspection file/);
|
||||
assert.throws(() => projectInspectionPayload({ ...wire, file: { ...wire.file, observation: "current-file" } }, { kind: "local-file", key: handle.key, path: "a.png" }, null, extractGenerationFacts), /Invalid inspection file/);
|
||||
assert.throws(() => m.localFileAdapter.toRecord({ ...wire, success: false }, handle, extractGenerationFacts), TypeError);
|
||||
assert.throws(() => m.localFileAdapter.toRecord({ ...wire, inspection_version: 2 }, handle, extractGenerationFacts), TypeError);
|
||||
assert.throws(() => m.localFileAdapter.toRecord(null, handle, extractGenerationFacts), TypeError);
|
||||
});
|
||||
|
||||
test("the parity corpus PNGs read into renderable records", async () => {
|
||||
for (const name of ["f02.png", "f06a.png", "f23.png"]) {
|
||||
const { wire, record } = await recordFor(fixtureFile(name, PARITY));
|
||||
assert.equal(wire.metadata.status, "available", name);
|
||||
assert.equal(record.file.format, "PNG");
|
||||
assert.ok(["parsed", "limited", "malformed"].includes(wire.metadata.prompt_graph.status));
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,234 @@
|
||||
// web/js/workspace/local-projection.mjs ports utils/inspection_response.py: the
|
||||
// strict Decimal/duplicate-key parse, the prompt/workflow/carrier projection, the
|
||||
// depth and node ceilings, _metadata and the 8 MiB omission steps. These units pin
|
||||
// the rules the plan names; tests/test_local_inspect_parity.py proves byte parity
|
||||
// with the server over the committed corpus.
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
const m = await import("../../web/js/workspace/local-projection.mjs").catch((error) => {
|
||||
if (error.code !== "ERR_MODULE_NOT_FOUND") throw error;
|
||||
return {};
|
||||
});
|
||||
const CASES = JSON.parse(fs.readFileSync(path.join(here, "..", "fixtures", "local-inspect", "projection-cases.json"), "utf8")).cases;
|
||||
|
||||
const plain = (value) => JSON.parse(JSON.stringify(value));
|
||||
const bytes = (value) => Buffer.byteLength(JSON.stringify(value), "utf8");
|
||||
const graphOf = (text, kind = "workflow") => plain(m.projectGraph(text, kind));
|
||||
const nested = (depth, close = "]", open = "[") => open.repeat(depth) + close.repeat(depth);
|
||||
const nodes = (count) => `{${Array.from({ length: count }, (_, i) => `"${i}":{"class_type":"A","inputs":{}}`).join(",")}}`;
|
||||
|
||||
test("contract", () => {
|
||||
for (const name of ["projectGraph", "projectMetadata", "fitInspectionPayload", "inspectionBytes", "parseStrictJson"]) {
|
||||
assert.equal(typeof m[name], "function", `${name} is exported`);
|
||||
}
|
||||
assert.equal(m.MAX_RESPONSE_BYTES, 8388608);
|
||||
assert.equal(m.MAX_PROJECTION_DEPTH, 64);
|
||||
assert.equal(m.GRAPH_NODE_CEILING, 20000);
|
||||
});
|
||||
|
||||
test("projection spells numbers like the server", () => {
|
||||
// literal -> Python str(Decimal(literal)) (or the int when it is a safe integer)
|
||||
const table = [
|
||||
["1e3", "1E+3"], ["1e0", "1"], ["-0.0", "-0.0"], ["6.50", "6.50"], ["0.0100", "0.0100"], ["1.0e2", "1.0E+2"],
|
||||
["0e5", "0E+5"], ["0.0000001", "1E-7"], ["0.000001", "0.000001"], ["1E+3", "1E+3"], ["1e-0", "1"], ["0.5e1", "5"],
|
||||
["0.05e1", "0.5"], ["0.0e0", "0.0"], ["1.5e+300", "1.5E+300"], ["123e-8", "0.00000123"], ["2.5e-3", "0.0025"],
|
||||
["1.0", "1.0"], ["-1.50", "-1.50"], ["0.0", "0.0"], ["100.0", "100.0"], ["1234567e-6", "1.234567"], ["0.5E-6", "5E-7"],
|
||||
["0.5E-5", "0.000005"], ["1e22", "1E+22"], ["0.000000", "0.000000"], ["-0.000000e-3", "-0E-9"], ["7.0e-6", "0.0000070"],
|
||||
["7.0e-7", "7.0E-7"], ["12345678901234567890.123", "12345678901234567890.123"], ["9007199254740992.0", "9007199254740992.0"],
|
||||
["1e999999999999999999", "1E+999999999999999999"], ["1e-1999999999999999997", "1E-1999999999999999997"],
|
||||
["0.0e1000000000000000000", "0E+999999999999999999"], ["1.5e999999999999999999", "1.5E+999999999999999999"],
|
||||
["0.1e-1999999999999999996", "1E-1999999999999999997"],
|
||||
["9007199254740991", 9007199254740991], ["-9007199254740991", -9007199254740991], ["9007199254740992", "9007199254740992"],
|
||||
["-9007199254740992", "-9007199254740992"], ["10000000000000000", "10000000000000000"], ["123456789012345678901234567890", "123456789012345678901234567890"],
|
||||
["0", 0], ["-0", 0], ["42", 42], ["-7", -7],
|
||||
];
|
||||
for (const [literal, expected] of table) {
|
||||
const graph = graphOf(`{"n":${literal}}`);
|
||||
assert.equal(graph.status, "parsed", literal);
|
||||
assert.deepEqual(graph.value, { n: expected }, literal);
|
||||
}
|
||||
assert.deepEqual(graphOf('{"n":[NaN,Infinity,-Infinity]}').value, { n: [null, null, null] });
|
||||
// A 4300-digit integer is Python's int() limit; one more digit fails the parse.
|
||||
assert.deepEqual(graphOf(`{"n":${"1".repeat(4300)}}`).value, { n: "1".repeat(4300) });
|
||||
assert.equal(graphOf(`{"n":${"1".repeat(4301)}}`).status, "malformed");
|
||||
assert.equal(graphOf(`{"n":-${"1".repeat(4301)}}`).status, "malformed");
|
||||
// Exact Decimal construction fails past the C library's exponent range.
|
||||
for (const literal of ["1e1000000000000000000", "15e999999999999999999", "1e-1999999999999999998", "1.5e-1999999999999999997",
|
||||
"0e1000000000000000000", "1e9223372036854775808", "0.0e1000000000000000001", "1e99999999999999999999999999"]) {
|
||||
assert.equal(graphOf(`{"n":${literal}}`).status, "malformed", literal);
|
||||
}
|
||||
// Escapes: surrogate pairs join, lone surrogates stay, control characters decode.
|
||||
const text = graphOf('{"s":"\\u00E9\\uD83D\\uDE00\\uD800\\u0000\\u001f\\/\\\\\\"\\b\\f\\n\\r\\t\\uD800\\u0041"}').value.s;
|
||||
assert.equal(text, "é\u{1f600}\ud800\u0000\u001f/\\\"\b\f\n\r\t\ud800A");
|
||||
assert.equal(JSON.stringify(m.projectGraph('{"s":"\\ud800"}', "workflow").value), '{"s":"\\ud800"}');
|
||||
});
|
||||
|
||||
test("projection rejects what Python's json rejects and nothing else", () => {
|
||||
const rejected = ["{}", "\f1", " 1", '{"a":1,}', "[1,]", "{'a':1}", '{"a"}', "[1 2]", "tru", "nul", "-NaN", "1.", ".5", "01", "+1",
|
||||
"1e", "-", '"\\x"', '"abc', "", " ", '"\u0001"', '{"a":1}x', '"\\u12"', '{"a":1,"a":2}', '{"a":{"b":1,"b":1}}', "[1,[2,{\"k\":1,\"k\":2}]]",
|
||||
'{"a":1 "b":2}', "[,1]", "{,}", '{"a":}', "[1}", "{1:2}", "
[]", "[]\u000b", "0x10", "1_000", "--1", "1e+", "1E-", '"\\uD800\\uDC"'];
|
||||
for (const text of rejected) assert.equal(graphOf(text).status, "malformed", JSON.stringify(text));
|
||||
for (const text of [" \t\r\n{}\n\t ", '{"a":[1,{"b":null}],"c":"d"}', '{"\\u0000":1}', '{"a":1,"A":1}', '{"":[]}']) {
|
||||
assert.equal(graphOf(text).status, "parsed", JSON.stringify(text));
|
||||
}
|
||||
// Top-level values that are not objects parse but are not graphs.
|
||||
for (const text of ["[]", '"x"', "1", "null", "true", "NaN", "1.5", "-Infinity"]) assert.equal(graphOf(text).status, "malformed", text);
|
||||
assert.equal(graphOf(undefined).status, "malformed");
|
||||
assert.equal(graphOf(null).status, "malformed");
|
||||
});
|
||||
|
||||
test("projection limits", () => {
|
||||
// A workflow's root is depth 1; the 64th nested container is kept, the 65th is limited.
|
||||
assert.equal(graphOf(`{"a":${nested(63)}}`).status, "parsed");
|
||||
assert.equal(graphOf(`{"a":${nested(64)}}`).status, "limited");
|
||||
// Prompt: root 1, node 2, inputs 3; a value chain reaching depth 64 is kept, 65 is limited.
|
||||
assert.equal(graphOf(`{"1":{"class_type":"A","inputs":{"v":${nested(61)}}}}`, "prompt").status, "parsed");
|
||||
assert.equal(graphOf(`{"1":{"class_type":"A","inputs":{"v":${nested(62)}}}}`, "prompt").status, "limited");
|
||||
// Dropped node keys are still validated for depth.
|
||||
assert.equal(graphOf(`{"1":{"class_type":"A","inputs":{},"_meta":{"v":${nested(61)}}}}`, "prompt").status, "parsed");
|
||||
assert.equal(graphOf(`{"1":{"class_type":"A","inputs":{},"_meta":{"v":${nested(62)}}}}`, "prompt").status, "limited");
|
||||
// A workflow is normalized before its shape is checked, so a deep non-object is limited; a
|
||||
// prompt checks its root first. Depth never overflows the parser (100,000 nested arrays).
|
||||
assert.equal(graphOf(nested(10)).status, "malformed");
|
||||
assert.equal(graphOf(nested(5000)).status, "limited");
|
||||
assert.equal(graphOf(nested(5000), "prompt").status, "malformed");
|
||||
assert.equal(graphOf(`{"a":${nested(100000)}}`).status, "limited");
|
||||
// Node ceilings: 20,000 API nodes parse, 20,001 are limited.
|
||||
assert.equal(graphOf(nodes(20000), "prompt").status, "parsed");
|
||||
assert.equal(graphOf(nodes(20001), "prompt").status, "limited");
|
||||
const list = (count) => `[${Array.from({ length: count }, () => "{}").join(",")}]`;
|
||||
assert.equal(graphOf(`{"nodes":${list(20000)}}`).status, "parsed");
|
||||
assert.equal(graphOf(`{"nodes":${list(20001)}}`).status, "limited");
|
||||
assert.equal(graphOf(`{"nodes":${list(10001)},"definitions":{"subgraphs":[{"nodes":${list(10000)}},5,{"nodes":3}]}}`).status, "limited");
|
||||
assert.equal(graphOf(`{"nodes":${list(10000)},"definitions":{"subgraphs":[{"nodes":${list(10000)}}]}}`).status, "parsed");
|
||||
assert.equal(graphOf(`{"nodes":{"${"x".repeat(5)}":1},"definitions":[]}`).status, "parsed");
|
||||
// Duplicate keys anywhere are malformed, including inside dropped node keys.
|
||||
assert.equal(graphOf('{"1":{"class_type":"A","inputs":{},"_meta":{"t":1,"t":2}}}', "prompt").status, "malformed");
|
||||
assert.equal(graphOf('{"1":{"class_type":"A","inputs":{"a":1,"a":1}}}', "prompt").status, "malformed");
|
||||
});
|
||||
|
||||
test("prompt nodes keep only class_type and inputs on null-prototype objects", () => {
|
||||
const text = '{"3":{"class_type":"KSampler","inputs":{"seed":9007199254740993,"cfg":8.0,"n":NaN},"_meta":{"title":"x"},"is_changed":[1]},"4":5,"5":[1,2.50],"__proto__":{"class_type":"P","inputs":{}},"constructor":"c"}';
|
||||
const graph = m.projectGraph(text, "prompt");
|
||||
assert.equal(graph.status, "parsed");
|
||||
assert.equal(Object.getPrototypeOf(graph.value), null);
|
||||
assert.equal(Object.getPrototypeOf(graph.value["3"]), null);
|
||||
assert.equal(Object.getPrototypeOf(graph.value["3"].inputs), null);
|
||||
// JSON.parse, like the projection, makes "__proto__" an own key; an object literal would not.
|
||||
assert.deepEqual(plain(graph.value), JSON.parse('{"3":{"class_type":"KSampler","inputs":{"seed":"9007199254740993","cfg":"8.0","n":null}},"4":5,"5":[1,"2.50"],"__proto__":{"class_type":"P","inputs":{}},"constructor":"c"}'));
|
||||
assert.ok(Object.hasOwn(graph.value, "__proto__"));
|
||||
assert.deepEqual(plain(graph.value["__proto__"]), { class_type: "P", inputs: {} });
|
||||
assert.equal(graph.value.constructor, "c");
|
||||
for (const text of ["[]", '"3"', "5", "null", '{"nodes":[]}x']) assert.equal(m.projectGraph(text, "prompt").status, text === '{"nodes":[]}x' ? "malformed" : "malformed", text);
|
||||
// A workflow keeps everything, normalized.
|
||||
assert.deepEqual(graphOf('{"nodes":[{"id":1,"widgets_values":[7,"fixed",8.0,1e3]}],"links":[],"version":0.4}').value,
|
||||
{ nodes: [{ id: 1, widgets_values: [7, "fixed", "8.0", "1E+3"] }], links: [], version: "0.4" });
|
||||
});
|
||||
|
||||
test("projectMetadata mirrors _metadata for records, formats and carriers", () => {
|
||||
const prompt = '{"1":{"class_type":"A","inputs":{"x":1.0}}}', workflow = '{"nodes":[],"version":0.4}';
|
||||
const read = (records, record_formats, carrierText = null, limited = []) => ({ records, record_formats, carrierText, limited });
|
||||
assert.deepEqual(plain(m.projectMetadata(null)), { status: "unreadable", omitted_reason: null, raw: {}, record_formats: {}, parameters: null,
|
||||
prompt_graph: { status: "absent", value: null }, workflow_graph: { status: "absent", value: null } });
|
||||
assert.deepEqual(plain(m.projectMetadata(read({}, {}))), { status: "absent", omitted_reason: null, raw: {}, record_formats: {}, parameters: null,
|
||||
prompt_graph: { status: "absent", value: null }, workflow_graph: { status: "absent", value: null } });
|
||||
const full = plain(m.projectMetadata(read({ prompt, workflow, parameters: "cat\nSteps: 1", comfyassets_prompt_pairs: "{}", other: "x" },
|
||||
{ prompt: "original-text", workflow: "original-text", parameters: "original-text", comfyassets_prompt_pairs: "original-text", other: "original-text" })));
|
||||
assert.deepEqual(full, { status: "available", omitted_reason: null,
|
||||
raw: { prompt, workflow, parameters: "cat\nSteps: 1", comfyassets_prompt_pairs: "{}" },
|
||||
record_formats: { prompt: "original-text", workflow: "original-text", parameters: "original-text", comfyassets_prompt_pairs: "original-text" },
|
||||
parameters: "cat\nSteps: 1", prompt_graph: { status: "parsed", value: { 1: { class_type: "A", inputs: { x: "1.0" } } } },
|
||||
workflow_graph: { status: "parsed", value: { nodes: [], version: "0.4" } } });
|
||||
// limited records lose their text; invalid is malformed; reserialized without a carrier is limited.
|
||||
const limited = plain(m.projectMetadata(read({ prompt, workflow }, { prompt: "original-text" }, null, ["workflow"])));
|
||||
assert.deepEqual(limited.raw, { prompt });
|
||||
assert.deepEqual(limited.record_formats, { prompt: "original-text", workflow: "limited" });
|
||||
assert.equal(limited.workflow_graph.status, "limited");
|
||||
assert.equal(plain(m.projectMetadata(read({ prompt }, { prompt: "original-text", workflow: "invalid" }))).workflow_graph.status, "malformed");
|
||||
assert.equal(plain(m.projectMetadata(read({ workflow }, { workflow: "reserialized-json" }))).workflow_graph.status, "limited");
|
||||
assert.equal(plain(m.projectMetadata(read({ prompt: "{" }, { prompt: "original-text" }))).prompt_graph.status, "malformed");
|
||||
// Unknown formats are dropped from record_formats but the raw text still projects.
|
||||
const odd = plain(m.projectMetadata(read({ prompt }, { prompt: "weird" })));
|
||||
assert.deepEqual(odd.record_formats, {});
|
||||
assert.equal(odd.prompt_graph.status, "parsed");
|
||||
// A non-text record vanishes from raw and its named graph is malformed.
|
||||
assert.equal(plain(m.projectMetadata(read({ prompt: 5 }, { prompt: "original-text" }))).prompt_graph.status, "malformed");
|
||||
// Carriers: strings re-parse by kind, objects were projected at depth 2; a bad carrier fails every present graph.
|
||||
const envelope = JSON.stringify({ prompt, workflow: { nodes: [], v: 1 } });
|
||||
const carried = plain(m.projectMetadata(read({ prompt, workflow: '{"nodes": [], "v": 1}' }, { prompt: "original-text", workflow: "reserialized-json" }, envelope)));
|
||||
assert.deepEqual(carried.prompt_graph, { status: "parsed", value: { 1: { class_type: "A", inputs: { x: "1.0" } } } });
|
||||
assert.deepEqual(carried.workflow_graph, { status: "parsed", value: { nodes: [], v: 1 } });
|
||||
const broken = plain(m.projectMetadata(read({ prompt, workflow }, { prompt: "original-text", workflow: "original-text" }, "[1]")));
|
||||
assert.deepEqual([broken.prompt_graph.status, broken.workflow_graph.status], ["malformed", "malformed"]);
|
||||
const deep = plain(m.projectMetadata(read({ prompt }, { prompt: "original-text" }, `{"title":${nested(70)},"prompt":${JSON.stringify(prompt)}}`)));
|
||||
assert.equal(deep.prompt_graph.status, "limited");
|
||||
const partial = plain(m.projectMetadata(read({ prompt, workflow }, { prompt: "original-text", workflow: "original-text" }, '{"prompt":"{","workflow":5}')));
|
||||
assert.deepEqual([partial.prompt_graph.status, partial.workflow_graph.status], ["malformed", "malformed"]);
|
||||
const missing = plain(m.projectMetadata(read({ prompt }, { prompt: "original-text" }, '{"workflow":{}}')));
|
||||
assert.deepEqual([missing.prompt_graph.status, missing.workflow_graph.status], ["malformed", "absent"]);
|
||||
// The carrier's own prompt object is projected: depth 2, node keys filtered.
|
||||
const objectPrompt = plain(m.projectMetadata(read({ prompt: "x" }, { prompt: "reserialized-json" }, '{"prompt":{"1":{"class_type":"A","inputs":{},"_meta":{}}}}')));
|
||||
assert.deepEqual(objectPrompt.prompt_graph, { status: "parsed", value: { 1: { class_type: "A", inputs: {} } } });
|
||||
});
|
||||
|
||||
test("fitInspectionPayload omits the workflow graph, then the prompt graph, then all raw atomically", () => {
|
||||
const payload = {
|
||||
success: true, inspection_version: 1, metadata: {
|
||||
status: "available", omitted_reason: null, raw: { prompt: "é".repeat(100), workflow: "{}" }, record_formats: { prompt: "original-text", workflow: "original-text" },
|
||||
parameters: null, prompt_graph: { status: "parsed", value: { 1: { class_type: "A", inputs: { t: "\ud800".repeat(50) } } } },
|
||||
workflow_graph: { status: "parsed", value: { nodes: Array.from({ length: 50 }, (_, i) => ({ id: i })) } },
|
||||
},
|
||||
};
|
||||
const text = JSON.stringify(payload);
|
||||
// UTF-8 bytes, not UTF-16 code units: 'é' is 2 bytes, an escaped lone surrogate 6.
|
||||
assert.equal(m.inspectionBytes(payload), Buffer.byteLength(text, "utf8"));
|
||||
assert.ok(m.inspectionBytes(payload) > text.length);
|
||||
const whole = m.fitInspectionPayload(payload, bytes(payload));
|
||||
assert.equal(whole, payload, "a payload within the limit is returned as is");
|
||||
const first = m.fitInspectionPayload(payload, bytes(payload) - 1);
|
||||
assert.equal(first.metadata.workflow_graph.status, "limited");
|
||||
assert.equal(first.metadata.prompt_graph.status, "parsed");
|
||||
assert.deepEqual(first.metadata.raw, payload.metadata.raw);
|
||||
assert.ok(bytes(first) <= bytes(payload) - 1);
|
||||
const second = m.fitInspectionPayload(payload, bytes(first) - 1);
|
||||
assert.deepEqual([second.metadata.workflow_graph.status, second.metadata.prompt_graph.status], ["limited", "limited"]);
|
||||
assert.equal(second.metadata.status, "available");
|
||||
assert.deepEqual(second.metadata.raw, payload.metadata.raw);
|
||||
const third = m.fitInspectionPayload(payload, bytes(second) - 1);
|
||||
assert.deepEqual(third.metadata, { ...second.metadata, raw: {}, parameters: null, status: "limited", omitted_reason: "response-size-limit" });
|
||||
assert.ok(bytes(third) <= bytes(second) - 1);
|
||||
assert.throws(() => m.fitInspectionPayload(payload, bytes(third) - 1), /exceeds/);
|
||||
// The caller's payload is never mutated and absent graphs are left alone.
|
||||
assert.equal(JSON.stringify(payload), text);
|
||||
const absent = { ...payload, metadata: { ...payload.metadata, workflow_graph: { status: "absent", value: null } } };
|
||||
const fitted = m.fitInspectionPayload(absent, bytes(absent) - 1);
|
||||
assert.equal(fitted.metadata.workflow_graph.status, "absent");
|
||||
assert.equal(fitted.metadata.prompt_graph.status, "limited");
|
||||
assert.equal(m.fitInspectionPayload(payload).metadata.status, "available", "the default limit is 8 MiB");
|
||||
});
|
||||
|
||||
test("the committed projection corpus has every outcome and parses with the oracle entry shape", () => {
|
||||
assert.ok(CASES.length >= 40);
|
||||
const names = new Set(), statuses = new Set(), graphs = new Set();
|
||||
for (const entry of CASES) {
|
||||
assert.equal(typeof entry.name, "string");
|
||||
assert.ok(!names.has(entry.name), `duplicate case ${entry.name}`);
|
||||
names.add(entry.name);
|
||||
const read = entry.records === null ? null : { records: entry.records, record_formats: entry.record_formats ?? Object.fromEntries(Object.keys(entry.records).map((key) => [key, "original-text"])),
|
||||
carrierText: entry.carrier_text ?? null, limited: entry.limited ?? [] };
|
||||
const metadata = m.projectMetadata(read);
|
||||
statuses.add(metadata.status);
|
||||
graphs.add(metadata.prompt_graph.status);
|
||||
graphs.add(metadata.workflow_graph.status);
|
||||
if (entry.expect) {
|
||||
for (const [field, status] of Object.entries(entry.expect)) assert.equal(metadata[field].status, status, `${entry.name} ${field}`);
|
||||
}
|
||||
}
|
||||
assert.deepEqual([...statuses].sort(), ["absent", "available", "unreadable"]);
|
||||
assert.deepEqual([...graphs].sort(), ["absent", "limited", "malformed", "parsed"]);
|
||||
});
|
||||
@@ -16,6 +16,14 @@ webm}`` with their recorded ffprobe tag maps in ``video-tags-v1.json``), recorde
|
||||
and synthetic tag maps, and multi-megabyte per-key containers built at test time,
|
||||
and assert that the local carrier equals the server's, which selects from the
|
||||
same tag map through a mocked probe, or from the same file through real ffprobe.
|
||||
|
||||
``web/js/workspace/local-projection.mjs`` ports ``utils/inspection_response.py``:
|
||||
the strict Decimal/duplicate-key parse, the prompt, workflow and carrier
|
||||
projection, the depth and node ceilings and ``_metadata``. The projection tests
|
||||
hand the oracle every file above plus the committed adversarial record sets in
|
||||
``tests/fixtures/local-inspect/projection-cases.json`` and node-ceiling sets built
|
||||
at test time, and assert that ``projectMetadata`` equals ``_metadata`` byte for
|
||||
byte, as the server's ``_bytes`` writes it with sorted keys.
|
||||
"""
|
||||
|
||||
import io
|
||||
@@ -24,6 +32,7 @@ import os
|
||||
import shutil
|
||||
import struct
|
||||
import subprocess # nosec B404
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
import zlib
|
||||
@@ -33,9 +42,11 @@ from unittest.mock import patch
|
||||
from PIL import Image
|
||||
from PIL.PngImagePlugin import PngInfo
|
||||
|
||||
from utils.inspection_response import GRAPH_NODE_CEILING, _bytes, _metadata
|
||||
from utils.prompt_inspector import (
|
||||
ROLE_METADATA_BYTES,
|
||||
WORKFLOW_METADATA_BYTES,
|
||||
InspectionRead,
|
||||
MetadataReadError,
|
||||
read_inspection_file,
|
||||
)
|
||||
@@ -56,6 +67,9 @@ VIDEO_FIXTURES = HEADER_FIXTURES
|
||||
RECORDED_TAGS = json.loads(
|
||||
(VIDEO_FIXTURES / "video-tags-v1.json").read_text(encoding="utf-8")
|
||||
)["fixtures"]
|
||||
PROJECTION_CASES = json.loads(
|
||||
(HEADER_FIXTURES / "projection-cases.json").read_text(encoding="utf-8")
|
||||
)["cases"]
|
||||
NODE_TIMEOUT_SECONDS = 180
|
||||
PNG_CLASS = {"media": "image", "kind": "png"}
|
||||
_FOOTER = "Steps: 20, Sampler: Euler, Batch index: {}, Seed: 7"
|
||||
@@ -690,5 +704,238 @@ class LocalVideoCarrierParityTests(unittest.TestCase):
|
||||
read_inspection_file(files["roles_over_budget"])
|
||||
|
||||
|
||||
def _canonical(value):
|
||||
"""``value`` with every dict's keys sorted, so two orders compare as one."""
|
||||
if isinstance(value, dict):
|
||||
return {key: _canonical(value[key]) for key in sorted(value)}
|
||||
if isinstance(value, list):
|
||||
return [_canonical(item) for item in value]
|
||||
return value
|
||||
|
||||
|
||||
def _canonical_bytes(value):
|
||||
return _bytes(_canonical(value))
|
||||
|
||||
|
||||
def _server_metadata(path):
|
||||
try:
|
||||
return _metadata(read_inspection_file(path))
|
||||
except MetadataReadError:
|
||||
return _metadata(None)
|
||||
|
||||
|
||||
def _synthetic_read(case):
|
||||
if case["records"] is None:
|
||||
return None
|
||||
formats = case.get("record_formats")
|
||||
if formats is None:
|
||||
formats = {key: "original-text" for key in case["records"]}
|
||||
return InspectionRead(
|
||||
case["records"],
|
||||
formats,
|
||||
case.get("carrier_text"),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
frozenset(case.get("limited", ())),
|
||||
)
|
||||
|
||||
|
||||
def _case_supported(case):
|
||||
"""Whether this interpreter has the feature a case depends on."""
|
||||
if case.get("needs") == "int-max-str-digits":
|
||||
limit = getattr(sys, "get_int_max_str_digits", None)
|
||||
return limit is not None and limit() == 4300
|
||||
return True
|
||||
|
||||
|
||||
def _node_ceiling_cases():
|
||||
"""Record sets at and past the 20,000-node ceiling, too large to commit."""
|
||||
api = lambda count: "{%s}" % ",".join( # noqa: E731
|
||||
'"%d":{"class_type":"A","inputs":{}}' % index for index in range(count)
|
||||
)
|
||||
ui = lambda count: "[%s]" % ",".join("{}" for _ in range(count)) # noqa: E731
|
||||
half = GRAPH_NODE_CEILING // 2
|
||||
return [
|
||||
{"name": "prompt_at_ceiling", "records": {"prompt": api(GRAPH_NODE_CEILING)}},
|
||||
{
|
||||
"name": "prompt_over_ceiling",
|
||||
"records": {"prompt": api(GRAPH_NODE_CEILING + 1)},
|
||||
},
|
||||
{
|
||||
"name": "workflow_at_ceiling",
|
||||
"records": {"workflow": '{"nodes":%s}' % ui(GRAPH_NODE_CEILING)},
|
||||
},
|
||||
{
|
||||
"name": "workflow_over_ceiling",
|
||||
"records": {"workflow": '{"nodes":%s}' % ui(GRAPH_NODE_CEILING + 1)},
|
||||
},
|
||||
{
|
||||
"name": "workflow_subgraphs_over_ceiling",
|
||||
"records": {
|
||||
"workflow": '{"nodes":%s,"definitions":{"subgraphs":[{"nodes":%s},'
|
||||
'5,{"nodes":3}]}}' % (ui(half + 1), ui(half))
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "workflow_subgraphs_at_ceiling",
|
||||
"records": {
|
||||
"workflow": '{"nodes":%s,"definitions":{"subgraphs":[{"nodes":%s}]}}'
|
||||
% (ui(half), ui(half))
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "carrier_prompt_over_ceiling",
|
||||
"records": {"prompt": "x"},
|
||||
"record_formats": {"prompt": "reserialized-json"},
|
||||
"carrier_text": '{"prompt":%s}' % api(GRAPH_NODE_CEILING + 1),
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class LocalProjectionParityTests(unittest.TestCase):
|
||||
"""The local projection answers byte for byte as ``_metadata`` does."""
|
||||
|
||||
def setUp(self):
|
||||
temporary = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(temporary.cleanup)
|
||||
self.addCleanup(assert_closed, self, temporary.name)
|
||||
self.directory = Path(temporary.name)
|
||||
|
||||
def _files(self):
|
||||
files = {}
|
||||
for name in parity.COMMITTED:
|
||||
files[name] = parity.fixture_path(name)
|
||||
for name in parity.TEST_TIME:
|
||||
path = parity.fixture_path(name, self.directory)
|
||||
parity.write_png(path, parity.build(name))
|
||||
files[name] = path
|
||||
for name, write in EXTRA_SHAPES.items():
|
||||
path = self.directory / f"{name}.png"
|
||||
write(path)
|
||||
files[name] = path
|
||||
for header in ("tiny.jpg", "tiny.webp"):
|
||||
files[header] = HEADER_FIXTURES / header
|
||||
for name in RECORDED_TAGS:
|
||||
files[name] = VIDEO_FIXTURES / name
|
||||
return files
|
||||
|
||||
def test_local_metadata_matches_route_for_parity_corpus(self):
|
||||
files = self._files()
|
||||
cases = [
|
||||
case
|
||||
for case in PROJECTION_CASES + _node_ceiling_cases()
|
||||
if _case_supported(case)
|
||||
]
|
||||
self.assertGreater(len(cases), len(PROJECTION_CASES) - 3)
|
||||
entries = [{"name": name, "path": str(path)} for name, path in files.items()]
|
||||
entries += [
|
||||
{
|
||||
"name": "case:" + case["name"],
|
||||
"records": case["records"],
|
||||
"record_formats": case.get("record_formats"),
|
||||
"carrier_text": case.get("carrier_text"),
|
||||
"limited": case.get("limited", []),
|
||||
}
|
||||
for case in cases
|
||||
]
|
||||
observed = _run_oracle(self, entries)
|
||||
outcomes = set()
|
||||
for name, path in files.items():
|
||||
with self.subTest(fixture=name):
|
||||
self.assertIsNotNone(observed[name]["classified"], name)
|
||||
if path.suffix in VIDEO_CONTAINERS and not HAVE_FFMPEG:
|
||||
# Without ffprobe the server cannot read the file; the tag-map
|
||||
# vectors below cover the carrier projection instead.
|
||||
continue
|
||||
server = _server_metadata(path)
|
||||
local = observed[name]["metadata"]
|
||||
self.assertEqual(_canonical_bytes(local), _canonical_bytes(server))
|
||||
outcomes.add(server["status"])
|
||||
for field in ("prompt_graph", "workflow_graph"):
|
||||
outcomes.add(field + ":" + server[field]["status"])
|
||||
for case in cases:
|
||||
with self.subTest(case=case["name"]):
|
||||
server = _metadata(_synthetic_read(case))
|
||||
local = observed["case:" + case["name"]]["metadata"]
|
||||
self.assertEqual(_canonical_bytes(local), _canonical_bytes(server))
|
||||
expect = case.get("expect") or {}
|
||||
for field, status in expect.items():
|
||||
self.assertEqual(server[field]["status"], status, field)
|
||||
outcomes.add(server["status"])
|
||||
for field in ("prompt_graph", "workflow_graph"):
|
||||
outcomes.add(field + ":" + server[field]["status"])
|
||||
# Every projection outcome must occur, or agreement proves little.
|
||||
self.assertEqual(
|
||||
outcomes,
|
||||
{
|
||||
"available",
|
||||
"absent",
|
||||
"unreadable",
|
||||
"prompt_graph:parsed",
|
||||
"prompt_graph:limited",
|
||||
"prompt_graph:malformed",
|
||||
"prompt_graph:absent",
|
||||
"workflow_graph:parsed",
|
||||
"workflow_graph:limited",
|
||||
"workflow_graph:malformed",
|
||||
"workflow_graph:absent",
|
||||
},
|
||||
)
|
||||
|
||||
def test_recorded_video_tag_maps_project_as_the_server(self):
|
||||
"""The carrier projection on the recorded maps, with or without ffprobe."""
|
||||
vectors = [(name, tags, []) for name, tags in RECORDED_TAGS.items()]
|
||||
vectors += [
|
||||
(name, tags, limited)
|
||||
for name, tags, limited in TAG_MAP_VECTORS
|
||||
if name
|
||||
in {
|
||||
"per_key_wins_over_comment",
|
||||
"object_prompt_reserialized",
|
||||
"python_number_spellings",
|
||||
"integer_like_keys_keep_order",
|
||||
"invalid_envelope_workflow_keeps_roles",
|
||||
"workflow_only_invalid_envelope",
|
||||
"reader_limited_workflow_with_per_key",
|
||||
"envelope_workflow_over_budget_limited",
|
||||
}
|
||||
]
|
||||
observed = _run_oracle(
|
||||
self,
|
||||
[
|
||||
{"name": name, "tags": tags, "limited": limited}
|
||||
for name, tags, limited in vectors
|
||||
],
|
||||
)
|
||||
seen = set()
|
||||
for name, tags, limited in vectors:
|
||||
with self.subTest(vector=name):
|
||||
with (
|
||||
patch(
|
||||
"utils.prompt_inspector.ffprobe_path",
|
||||
return_value="/tool/ffprobe",
|
||||
),
|
||||
patch(
|
||||
"utils.prompt_inspector._probe_video_tags",
|
||||
side_effect=_fake_probe(
|
||||
tags, drop_workflow="workflow" in limited
|
||||
),
|
||||
),
|
||||
):
|
||||
try:
|
||||
server = _metadata(read_inspection_file(Path("vector.mp4")))
|
||||
except MetadataReadError:
|
||||
server = _metadata(None)
|
||||
local = observed[name]["metadata"]
|
||||
self.assertEqual(_canonical_bytes(local), _canonical_bytes(server))
|
||||
seen.update(server["record_formats"].values())
|
||||
seen.add(server["workflow_graph"]["status"])
|
||||
self.assertTrue(
|
||||
{"reserialized-json", "original-text", "invalid", "limited"} <= seen
|
||||
)
|
||||
self.assertTrue({"parsed", "malformed", "limited"} <= seen)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
+12
@@ -293,6 +293,18 @@
|
||||
"functions": 96,
|
||||
"statements": 96
|
||||
},
|
||||
"workspace/local-inspection.mjs": {
|
||||
"lines": 98,
|
||||
"branches": 85,
|
||||
"functions": 98,
|
||||
"statements": 98
|
||||
},
|
||||
"workspace/local-projection.mjs": {
|
||||
"lines": 97,
|
||||
"branches": 95,
|
||||
"functions": 98,
|
||||
"statements": 97
|
||||
},
|
||||
"workspace/local-video.mjs": {
|
||||
"lines": 98,
|
||||
"branches": 93,
|
||||
|
||||
@@ -87,8 +87,10 @@ function promptsFor(value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
function fileFor(value) {
|
||||
if (!object(value) || value.observation !== 'current-file' || typeof value.source_label !== 'string' ||
|
||||
function fileFor(value, identity) {
|
||||
// A local record observes the selected file itself; every other adapter observes a current server file.
|
||||
const observation = identity?.kind === 'local-file' ? 'selected-local-file' : 'current-file';
|
||||
if (!object(value) || value.observation !== observation || typeof value.source_label !== 'string' ||
|
||||
!['image', 'video'].includes(value.media_type) || typeof value.extension !== 'string' ||
|
||||
(value.format !== null && typeof value.format !== 'string')) throw new TypeError('Invalid inspection file');
|
||||
const file = { observation: value.observation, source_label: value.source_label, media_type: value.media_type, extension: value.extension, format: value.format };
|
||||
@@ -281,7 +283,7 @@ export function projectInspectionPayload(result, identity, download, extractFact
|
||||
};
|
||||
if (legacy) return record;
|
||||
const resolution = resolutionFor(result.resolution);
|
||||
record.file = fileFor(result.file);
|
||||
record.file = fileFor(result.file, identity);
|
||||
record.metadata = metadataFor(result.metadata);
|
||||
record.library = libraryFor(result.library);
|
||||
if (typeof extractFacts !== 'function') throw new TypeError('Inspection facts parser unavailable');
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
// One chosen local File, read entirely in this browser, as the inspection wire
|
||||
// the workspace already renders. The readers are ports of the server's
|
||||
// (png-metadata.js readPngMetadataFile/readImageHeader, local-video.mjs); the
|
||||
// projection is local-projection.mjs. Nothing here builds a URL or sends bytes:
|
||||
// the only URL a handle carries is the session-owned blob: URL it was created with.
|
||||
import { readVideoTags, selectVideoCarrier } from './local-video.mjs';
|
||||
import { projectMetadata, fitInspectionPayload, MAX_RESPONSE_BYTES } from './local-projection.mjs';
|
||||
import { projectInspectionPayload } from './inspection-state.mjs';
|
||||
|
||||
export const LOCAL_OBSERVATION = 'selected-local-file';
|
||||
export const LOCAL_SOURCE_LABEL = 'Selected local file';
|
||||
// Interim roles until PR L3 ports the inspection resolver: every role is open, and
|
||||
// the legacy fill in projectInspectionPayload shows what the legacy pages show.
|
||||
const LOCAL_RESOLUTION_REASON = 'Resolved in this browser from the workflow.';
|
||||
const LOCAL_LIBRARY = Object.freeze({ status: 'unavailable', scope: 'saved-library', tags: [], category_scope: null, tags_scope: null });
|
||||
const LOCAL_SCOPE = 'local';
|
||||
const PREFIX_BYTES = 32;
|
||||
const MAX_FILENAME_CHARS = 255;
|
||||
const MEDIA_TYPES = new Set(['image', 'video']);
|
||||
|
||||
const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
||||
const pngReader = () => globalThis.PngMetadata || globalThis.window?.PngMetadata;
|
||||
|
||||
/** A display name with no path separators or control characters; never empty. */
|
||||
export function sanitizeLocalFilename(name) {
|
||||
const text = typeof name === 'string' ? name : '';
|
||||
const cleaned = text.replace(/[\x00-\x1f\x7f]/g, '').replace(/[\\/]/g, '_').trim();
|
||||
return [...cleaned].slice(0, MAX_FILENAME_CHARS).join('') || 'file';
|
||||
}
|
||||
|
||||
/** `pathlib.Path(name).suffix.lower()`. */
|
||||
function localExtension(filename) {
|
||||
const dot = filename.lastIndexOf('.');
|
||||
return dot > 0 && dot < filename.length - 1 ? filename.slice(dot).toLowerCase() : '';
|
||||
}
|
||||
|
||||
function classificationOf(value) {
|
||||
if (!object(value) || !MEDIA_TYPES.has(value.media) || typeof value.kind !== 'string' || !value.kind) throw new TypeError('Unsupported local file');
|
||||
return Object.freeze({ media: value.media, kind: value.kind });
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the readers the server would run for these bytes: the bounded PNG reader,
|
||||
* the JPEG/WebP header reader, or the container tag readers plus the carrier rule.
|
||||
* Returns `{classification, result}` in the oracle's shape; `result` is null for
|
||||
* bytes no reader claims.
|
||||
*/
|
||||
export async function readLocalFile(file, { classification, signal, pngMetadata = pngReader() } = {}) {
|
||||
if (!(file instanceof Blob)) throw new TypeError('A local file is required');
|
||||
const reader = () => {
|
||||
if (!pngMetadata || typeof pngMetadata.classifyPrefix !== 'function') throw new TypeError('The local PNG reader is unavailable');
|
||||
return pngMetadata;
|
||||
};
|
||||
const prefix = new Uint8Array(await file.slice(0, Math.min(PREFIX_BYTES, file.size)).arrayBuffer());
|
||||
const classified = classification === undefined ? reader().classifyPrefix(prefix) : classification;
|
||||
if (classified === null) return { classification: null, result: null };
|
||||
const { media, kind } = classificationOf(classified);
|
||||
if (kind === 'png') return { classification: classified, result: await reader().readPngMetadataFile(file, { signal, prefix }) };
|
||||
if (media === 'image') return { classification: classified, result: await reader().readImageHeader(file, kind) };
|
||||
const read = await readVideoTags(file, { signal, prefix, kind });
|
||||
return { classification: classified, result: { read, selection: read.status === 'complete' ? selectVideoCarrier(read.tags, { limited: read.limited }) : null } };
|
||||
}
|
||||
|
||||
/**
|
||||
* The server's InspectionRead for a reader result: `read` is null when the server
|
||||
* would have raised MetadataReadError (unreadable), else
|
||||
* `{records, record_formats, carrierText, limited}`; `format`, `width` and
|
||||
* `height` follow `read_inspection_file` (a video has none).
|
||||
*/
|
||||
export function toInspectionRead(classification, result) {
|
||||
const { media, kind } = classificationOf(classification);
|
||||
const none = { read: null, format: null, width: null, height: null };
|
||||
if (kind === 'png') {
|
||||
if (!object(result) || result.status !== 'complete') return none;
|
||||
const limited = [...(result.limited ?? [])];
|
||||
const record_formats = Object.fromEntries(Object.keys(result.records).map(key => [key, 'original-text']));
|
||||
for (const key of limited) record_formats[key] = 'limited';
|
||||
return { read: { records: { ...result.records }, record_formats, carrierText: null, limited }, format: 'PNG', width: result.width ?? null, height: result.height ?? null };
|
||||
}
|
||||
if (media === 'image') {
|
||||
if (!object(result)) return none;
|
||||
return { read: { records: {}, record_formats: {}, carrierText: null, limited: [] }, format: result.format ?? null, width: result.width ?? null, height: result.height ?? null };
|
||||
}
|
||||
const selection = result?.selection;
|
||||
if (result?.read?.status !== 'complete' || !object(selection) || selection.status !== 'complete') return none;
|
||||
return { read: { records: { ...selection.records }, record_formats: { ...selection.record_formats }, carrierText: selection.carrierText ?? null, limited: [...(selection.limited ?? [])] }, format: null, width: null, height: null };
|
||||
}
|
||||
|
||||
const dimension = value => Number.isSafeInteger(value) && value > 0 ? value : null;
|
||||
|
||||
/** The `inspection_version: 1` wire for one local read, before the response budget. */
|
||||
export function buildLocalPayload({ read, file, classification }) {
|
||||
const classified = classificationOf(classification);
|
||||
const inspection = toInspectionRead(classified, read);
|
||||
const filename = sanitizeLocalFilename(file?.name);
|
||||
const size = Number.isSafeInteger(file?.size) && file.size >= 0 ? file.size : null;
|
||||
const modified = typeof file?.lastModified === 'number' && Number.isFinite(file.lastModified) && file.lastModified >= 0 ? file.lastModified / 1000 : null;
|
||||
return {
|
||||
success: true, inspection_version: 1,
|
||||
file: {
|
||||
observation: LOCAL_OBSERVATION, source_label: LOCAL_SOURCE_LABEL, media_type: classified.media, extension: localExtension(filename),
|
||||
format: inspection.format, size, width: dimension(inspection.width), height: dimension(inspection.height), modified_time: modified,
|
||||
},
|
||||
prompts: { positive: { status: 'unresolved' }, negative: { status: 'unresolved' } },
|
||||
resolution: { state: 'unresolved', sampler_id: null, reason: LOCAL_RESOLUTION_REASON, reason_code: null, node: null },
|
||||
metadata: projectMetadata(inspection.read),
|
||||
library: { ...LOCAL_LIBRARY, tags: [] },
|
||||
};
|
||||
}
|
||||
|
||||
/** The server's 8 MiB response budget, applied to the local wire with the same omission order. */
|
||||
export function fitLocalPayload(wire, limit = MAX_RESPONSE_BYTES) {
|
||||
return fitInspectionPayload(wire, limit);
|
||||
}
|
||||
|
||||
// Handles this module created, by a private token: the controller and the viewer
|
||||
// session spread-copy descriptors, so a copy of an owned handle is the same
|
||||
// selection, while a hand-made descriptor or a tampered URL is foreign.
|
||||
const TOKEN = Symbol('local-file');
|
||||
const OWNED = new WeakMap();
|
||||
let sequence = 0;
|
||||
|
||||
/**
|
||||
* One selection of a local file. `url` is the session's own `blob:` URL (or null
|
||||
* when none could be created); every selection gets a new key, so choosing the
|
||||
* same bytes twice never reuses a stale read.
|
||||
*/
|
||||
export function createLocalFileHandle({ file, url = null, classification, filename = file?.name }) {
|
||||
if (!(file instanceof Blob)) throw new TypeError('A local file is required');
|
||||
if (url !== null && (typeof url !== 'string' || !url.startsWith('blob:'))) throw new TypeError('Local media must be a blob: URL');
|
||||
const classified = classificationOf(classification);
|
||||
const token = Object.freeze({});
|
||||
const secret = Object.freeze({ key: `local:${++sequence}`, filename: sanitizeLocalFilename(filename), url, file, classification: classified });
|
||||
OWNED.set(token, secret);
|
||||
return Object.freeze({ kind: 'local-file', key: secret.key, filename: secret.filename, is_video: classified.media === 'video', url, file, classification: classified, [TOKEN]: token });
|
||||
}
|
||||
|
||||
/** The owned selection behind a handle (or a copy of one); throws for anything else. */
|
||||
function owned(handle) {
|
||||
const secret = object(handle) ? OWNED.get(handle[TOKEN]) : undefined;
|
||||
if (!secret || secret.key !== handle.key || secret.url !== handle.url || secret.file !== handle.file || secret.filename !== handle.filename) throw new TypeError('Unknown local file');
|
||||
return secret;
|
||||
}
|
||||
|
||||
function validate(handle) {
|
||||
owned(handle);
|
||||
}
|
||||
|
||||
function originalDownload(handle) {
|
||||
const secret = owned(handle);
|
||||
if (secret.url === null) throw new TypeError('Original media is unavailable');
|
||||
return { url: secret.url, filename: secret.filename };
|
||||
}
|
||||
|
||||
export const localFileAdapter = Object.freeze({
|
||||
validate,
|
||||
key(handle) { return owned(handle).key; },
|
||||
scope(handle) { owned(handle); return LOCAL_SCOPE; },
|
||||
// An inert sentinel: the controller's same-key check works and no URL is ever built.
|
||||
inspectionPath(handle) { return `local-file:${owned(handle).key}`; },
|
||||
async read(handle, { signal } = {}) {
|
||||
const secret = owned(handle);
|
||||
const { result } = await readLocalFile(secret.file, { classification: secret.classification, signal });
|
||||
return fitLocalPayload(buildLocalPayload({ read: result, file: { name: secret.filename, size: secret.file.size, lastModified: secret.file.lastModified }, classification: secret.classification }));
|
||||
},
|
||||
toRecord(result, handle, extractFacts) {
|
||||
const secret = owned(handle);
|
||||
if (!object(result) || result.success !== true || result.inspection_version !== 1) throw new TypeError('Invalid local inspection');
|
||||
return projectInspectionPayload(result, { kind: 'local-file', key: secret.key, path: secret.filename },
|
||||
secret.url === null ? null : originalDownload(handle), extractFacts);
|
||||
},
|
||||
originalDownload,
|
||||
// Session-owned, never resolved through the API client.
|
||||
mediaUrl(handle) { return owned(handle).url; },
|
||||
});
|
||||
@@ -0,0 +1,386 @@
|
||||
// Browser port of utils/inspection_response.py: the strict parse (Decimal
|
||||
// spelling, no duplicate keys, NaN/Infinity as null, integers past 2^53-1 as
|
||||
// text), the prompt/workflow/carrier projection with its depth and node
|
||||
// ceilings, `_metadata`, and the response-size omission steps of
|
||||
// `_encode_inspection`. A local file must project exactly as the server does for
|
||||
// the same bytes; tests/test_local_inspect_parity.py holds both to one answer.
|
||||
// Pure: no DOM, no I/O.
|
||||
|
||||
export const MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
|
||||
export const MAX_PROJECTION_DEPTH = 64;
|
||||
// Backstop, not an expected outcome: executable API nodes for a prompt; UI nodes
|
||||
// including subgraph definitions for a workflow.
|
||||
export const GRAPH_NODE_CEILING = 20000;
|
||||
export const RECORD_KEYS = Object.freeze(['prompt', 'workflow', 'parameters', 'comfyassets_prompt_pairs']);
|
||||
const FORMATS = new Set(['original-text', 'reserialized-json', 'invalid', 'limited']);
|
||||
const PROMPT_NODE_KEYS = new Set(['class_type', 'inputs']);
|
||||
const MAX_SAFE_DIGITS = '9007199254740991';
|
||||
// Python's int() refuses more digits than sys.get_int_max_str_digits() (4300).
|
||||
const INT_MAX_STR_DIGITS = 4300;
|
||||
// decimal's exact constructor (libmpdec, 64-bit) refuses values outside its
|
||||
// maximal context: adjusted exponent above MAX_EMAX or exponent below Etiny.
|
||||
const DECIMAL_MAX_EMAX = 999999999999999999n;
|
||||
const DECIMAL_ETINY = -1999999999999999997n;
|
||||
const NUMBER = /-?(?:0|[1-9][0-9]*)(\.[0-9]+)?([eE][-+]?[0-9]+)?/y;
|
||||
const STRING_STOP = /["\\\u0000-\u001f]/g;
|
||||
const HEX4 = /^[0-9a-fA-F]{4}$/;
|
||||
const ESCAPES = { '"': '"', '\\': '\\', '/': '/', b: '\b', f: '\f', n: '\n', r: '\r', t: '\t' };
|
||||
// Python's json: `null`/`true`/`false`, then the constants parse_constant receives.
|
||||
const LITERALS = [['null', null], ['true', true], ['false', false], ['NaN', null], ['Infinity', null], ['-Infinity', null]];
|
||||
|
||||
/** A present graph cannot be projected within inspection limits (`_ProjectionLimit`). */
|
||||
export class ProjectionLimit extends Error {
|
||||
constructor() {
|
||||
super('Projection exceeds inspection limits');
|
||||
this.name = 'ProjectionLimit';
|
||||
}
|
||||
}
|
||||
|
||||
/** The server's ValueError/TypeError family: the text or tree is malformed. */
|
||||
export class ProjectionError extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = 'ProjectionError';
|
||||
}
|
||||
}
|
||||
|
||||
const isArray = Array.isArray;
|
||||
const isObject = value => value !== null && typeof value === 'object' && !isArray(value);
|
||||
const isContainer = value => value !== null && typeof value === 'object';
|
||||
|
||||
/** Python's `str(Decimal(literal))` from the literal's digits and exponent, never through a float. */
|
||||
export function spellDecimal(literal) {
|
||||
const negative = literal[0] === '-';
|
||||
const body = negative ? literal.slice(1) : literal;
|
||||
const exponentAt = body.search(/[eE]/);
|
||||
const mantissa = exponentAt < 0 ? body : body.slice(0, exponentAt);
|
||||
const dot = mantissa.indexOf('.');
|
||||
const integer = dot < 0 ? mantissa : mantissa.slice(0, dot), fraction = dot < 0 ? '' : mantissa.slice(dot + 1);
|
||||
const exponent = (exponentAt < 0 ? 0n : BigInt(body.slice(exponentAt + 1))) - BigInt(fraction.length);
|
||||
const digits = (integer + fraction).replace(/^0+/, '') || '0';
|
||||
const adjusted = exponent + BigInt(digits.length) - 1n;
|
||||
if (adjusted > DECIMAL_MAX_EMAX || exponent < DECIMAL_ETINY) throw new ProjectionError('JSON decimal exceeds supported range');
|
||||
const leftDigits = exponent + BigInt(digits.length);
|
||||
const dotPlace = exponent <= 0n && leftDigits > -6n ? leftDigits : 1n;
|
||||
let intPart, fracPart;
|
||||
if (dotPlace <= 0n) {
|
||||
intPart = '0'; fracPart = '.' + '0'.repeat(Number(-dotPlace)) + digits;
|
||||
} else if (dotPlace >= BigInt(digits.length)) {
|
||||
intPart = digits + '0'.repeat(Number(dotPlace - BigInt(digits.length))); fracPart = '';
|
||||
} else {
|
||||
intPart = digits.slice(0, Number(dotPlace)); fracPart = '.' + digits.slice(Number(dotPlace));
|
||||
}
|
||||
const shift = leftDigits - dotPlace;
|
||||
const exp = shift === 0n ? '' : `E${shift > 0n ? '+' : '-'}${shift < 0n ? -shift : shift}`;
|
||||
return (negative ? '-' : '') + intPart + fracPart + exp;
|
||||
}
|
||||
|
||||
function numberValue(match) {
|
||||
const [literal, fraction, exponent] = match;
|
||||
if (fraction || exponent) return spellDecimal(literal);
|
||||
const digits = literal[0] === '-' ? literal.slice(1) : literal;
|
||||
if (digits.length > INT_MAX_STR_DIGITS) throw new ProjectionError('Exceeds the limit for integer string conversion');
|
||||
if (digits.length > MAX_SAFE_DIGITS.length || (digits.length === MAX_SAFE_DIGITS.length && digits > MAX_SAFE_DIGITS)) return literal;
|
||||
return Number(literal) || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* `json.loads(text, parse_float=Decimal, object_pairs_hook=_pairs, parse_constant=_nonfinite)`
|
||||
* with its results already normalized: objects have a null prototype, fractions and
|
||||
* exponents are Decimal text, integers past 2^53-1 are their literal text, and
|
||||
* NaN/Infinity are null. Iterative, so nesting depth never overflows the stack.
|
||||
*/
|
||||
export function parseStrictJson(text) {
|
||||
if (typeof text !== 'string') throw new ProjectionError('JSON text must be a string');
|
||||
let pos = 0;
|
||||
const fail = message => new ProjectionError(`${message} at ${pos}`);
|
||||
const skip = () => {
|
||||
while (pos < text.length) {
|
||||
const c = text.charCodeAt(pos);
|
||||
if (c !== 0x20 && c !== 0x09 && c !== 0x0a && c !== 0x0d) break;
|
||||
pos++;
|
||||
}
|
||||
};
|
||||
function string() {
|
||||
pos++;
|
||||
let out = '';
|
||||
for (;;) {
|
||||
STRING_STOP.lastIndex = pos;
|
||||
const stop = STRING_STOP.exec(text);
|
||||
if (!stop) throw fail('Unterminated string');
|
||||
out += text.slice(pos, stop.index);
|
||||
pos = stop.index + 1;
|
||||
if (stop[0] === '"') return out;
|
||||
if (stop[0] !== '\\') throw fail('Invalid control character');
|
||||
const escape = text[pos];
|
||||
if (escape === 'u') {
|
||||
const hex = text.slice(pos + 1, pos + 5);
|
||||
if (!HEX4.test(hex)) throw fail('Invalid \\uXXXX escape');
|
||||
pos += 5;
|
||||
const code = parseInt(hex, 16);
|
||||
if (code >= 0xd800 && code <= 0xdbff && text[pos] === '\\' && text[pos + 1] === 'u') {
|
||||
const low = text.slice(pos + 2, pos + 6);
|
||||
const lowCode = HEX4.test(low) ? parseInt(low, 16) : -1;
|
||||
if (lowCode >= 0xdc00 && lowCode <= 0xdfff) {
|
||||
out += String.fromCharCode(code, lowCode);
|
||||
pos += 6;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
out += String.fromCharCode(code);
|
||||
} else if (Object.hasOwn(ESCAPES, escape)) {
|
||||
out += ESCAPES[escape];
|
||||
pos++;
|
||||
} else throw fail('Invalid \\escape');
|
||||
}
|
||||
}
|
||||
function key() {
|
||||
if (text[pos] !== '"') throw fail('Expecting property name enclosed in double quotes');
|
||||
const name = string();
|
||||
skip();
|
||||
if (text[pos] !== ':') throw fail("Expecting ':' delimiter");
|
||||
pos++;
|
||||
skip();
|
||||
return name;
|
||||
}
|
||||
function scalar() {
|
||||
if (text[pos] === '"') return string();
|
||||
for (const [literal, value] of LITERALS) {
|
||||
if (text.startsWith(literal, pos)) {
|
||||
pos += literal.length;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
NUMBER.lastIndex = pos;
|
||||
const match = NUMBER.exec(text);
|
||||
if (!match) throw fail('Expecting value');
|
||||
pos += match[0].length;
|
||||
return numberValue(match);
|
||||
}
|
||||
skip();
|
||||
const stack = [];
|
||||
let result;
|
||||
for (;;) {
|
||||
let value;
|
||||
if (text[pos] === '{') {
|
||||
pos++; skip();
|
||||
const frame = { value: Object.create(null), object: true, key: null, duplicate: false };
|
||||
if (text[pos] === '}') pos++;
|
||||
else { frame.key = key(); stack.push(frame); continue; }
|
||||
value = frame.value;
|
||||
} else if (text[pos] === '[') {
|
||||
pos++; skip();
|
||||
const frame = { value: [], object: false };
|
||||
if (text[pos] === ']') pos++;
|
||||
else { stack.push(frame); continue; }
|
||||
value = frame.value;
|
||||
} else value = scalar();
|
||||
// Attach the finished value, closing containers whose last member it was.
|
||||
for (;;) {
|
||||
if (!stack.length) { result = value; break; }
|
||||
const frame = stack[stack.length - 1];
|
||||
if (frame.object) {
|
||||
if (Object.hasOwn(frame.value, frame.key)) frame.duplicate = true;
|
||||
frame.value[frame.key] = value;
|
||||
} else frame.value.push(value);
|
||||
skip();
|
||||
if (text[pos] === ',') {
|
||||
pos++; skip();
|
||||
if (frame.object) frame.key = key();
|
||||
break;
|
||||
}
|
||||
if (text[pos] !== (frame.object ? '}' : ']')) throw fail("Expecting ',' delimiter");
|
||||
pos++;
|
||||
stack.pop();
|
||||
// `_pairs` runs when the object closes, after everything inside it parsed.
|
||||
if (frame.object && frame.duplicate) throw new ProjectionError('Duplicate JSON key');
|
||||
value = frame.value;
|
||||
}
|
||||
if (!stack.length) break;
|
||||
}
|
||||
skip();
|
||||
if (pos !== text.length) throw fail('Extra data');
|
||||
return result;
|
||||
}
|
||||
|
||||
/** `_normalize`: walk once; unretained values are only checked against the depth bound. */
|
||||
function normalize(value, depth, retain) {
|
||||
if (!isContainer(value)) return retain ? value : null;
|
||||
if (depth > MAX_PROJECTION_DEPTH) throw new ProjectionLimit();
|
||||
if (isArray(value)) {
|
||||
const result = retain ? [] : null;
|
||||
for (const child of value) {
|
||||
const normalized = normalize(child, depth + 1, retain);
|
||||
if (retain) result.push(normalized);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
const result = retain ? Object.create(null) : null;
|
||||
for (const key of Object.keys(value)) {
|
||||
const normalized = normalize(value[key], depth + 1, retain);
|
||||
if (retain) result[key] = normalized;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** `_project_prompt`: each API node keeps exactly `class_type` and `inputs`; other keys are validated and dropped. */
|
||||
function projectPrompt(tree, depth) {
|
||||
if (!isObject(tree)) throw new ProjectionError('Prompt graph must be an object of nodes');
|
||||
if (depth + 1 > MAX_PROJECTION_DEPTH) throw new ProjectionLimit();
|
||||
const result = Object.create(null);
|
||||
for (const id of Object.keys(tree)) {
|
||||
const node = tree[id];
|
||||
if (!isObject(node)) {
|
||||
result[id] = normalize(node, depth + 1, true);
|
||||
continue;
|
||||
}
|
||||
const projected = Object.create(null);
|
||||
for (const key of Object.keys(node)) {
|
||||
const keep = PROMPT_NODE_KEYS.has(key);
|
||||
const normalized = normalize(node[key], depth + 2, keep);
|
||||
if (keep) projected[key] = normalized;
|
||||
}
|
||||
result[id] = projected;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function project(tree, kind, depth) {
|
||||
if (kind === 'prompt') return projectPrompt(tree, depth);
|
||||
if (kind === 'workflow') return normalize(tree, depth, true);
|
||||
if (!isObject(tree)) throw new ProjectionError('Malformed carrier');
|
||||
const result = Object.create(null);
|
||||
for (const key of Object.keys(tree)) {
|
||||
const member = tree[key];
|
||||
if (key !== 'prompt' && key !== 'workflow') normalize(member, depth + 1, false);
|
||||
else if (typeof member === 'string') result[key] = member; // projected by its own kind when read
|
||||
else if (key === 'prompt' && !isObject(member)) result[key] = normalize(member, depth + 1, true);
|
||||
else result[key] = project(member, key, depth + 1);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** `_strict_projection`: parse exactly and project by record kind ('prompt'|'workflow'|'carrier'). */
|
||||
export function strictProjection(text, kind) {
|
||||
return project(parseStrictJson(text), kind, 1);
|
||||
}
|
||||
|
||||
function nodeCount(value, workflow) {
|
||||
if (!workflow) return Object.keys(value).length;
|
||||
const nodes = value.nodes;
|
||||
let count = isArray(nodes) ? nodes.length : 0;
|
||||
const definitions = value.definitions;
|
||||
const subgraphs = isObject(definitions) ? definitions.subgraphs : null;
|
||||
for (const subgraph of isArray(subgraphs) ? subgraphs : []) {
|
||||
const inner = isObject(subgraph) ? subgraph.nodes : null;
|
||||
count += isArray(inner) ? inner.length : 0;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
function graph(value, workflow) {
|
||||
if (!isObject(value)) return { status: 'malformed', value: null };
|
||||
if (nodeCount(value, workflow) > GRAPH_NODE_CEILING) return { status: 'limited', value: null };
|
||||
return { status: 'parsed', value };
|
||||
}
|
||||
|
||||
function outcome(error) {
|
||||
if (error instanceof ProjectionLimit) return { status: 'limited', value: null };
|
||||
if (error instanceof ProjectionError) return { status: 'malformed', value: null };
|
||||
throw error;
|
||||
}
|
||||
|
||||
/**
|
||||
* One record's graph as the inspector projects it: `{status, value}` with `status`
|
||||
* `parsed` (the projected graph the facts parser receives), `limited` (past the
|
||||
* depth or node bound) or `malformed`.
|
||||
*/
|
||||
export function projectGraph(text, kind) {
|
||||
try {
|
||||
return graph(strictProjection(text, kind), kind === 'workflow');
|
||||
} catch (error) {
|
||||
return outcome(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `_metadata` for one local read: `{records, record_formats, carrierText, limited}`
|
||||
* (the server's InspectionRead) or null when the file's metadata is unreadable.
|
||||
*/
|
||||
export function projectMetadata(inspection) {
|
||||
const formats = {}, raw = {};
|
||||
if (inspection) {
|
||||
for (const [key, value] of Object.entries(inspection.record_formats ?? {})) {
|
||||
if (RECORD_KEYS.includes(key) && FORMATS.has(value)) formats[key] = value;
|
||||
}
|
||||
// A record over its read budget has no text: like `invalid`, raw omits it,
|
||||
// and its graph is `limited` rather than parsed from nothing.
|
||||
for (const key of inspection.limited ?? []) if (RECORD_KEYS.includes(key)) formats[key] = 'limited';
|
||||
for (const [key, value] of Object.entries(inspection.records ?? {})) {
|
||||
if (RECORD_KEYS.includes(key) && typeof value === 'string' && formats[key] !== 'limited') raw[key] = value;
|
||||
}
|
||||
}
|
||||
const result = {
|
||||
status: inspection === null || inspection === undefined ? 'unreadable' : Object.keys(raw).length || Object.keys(formats).length ? 'available' : 'absent',
|
||||
omitted_reason: null, raw, record_formats: formats, parameters: Object.hasOwn(raw, 'parameters') ? raw.parameters : null,
|
||||
};
|
||||
let carrier = null, failed = null;
|
||||
if (inspection && inspection.carrierText !== null && inspection.carrierText !== undefined) {
|
||||
try { carrier = strictProjection(inspection.carrierText, 'carrier'); } catch (error) { failed = outcome(error).status; }
|
||||
}
|
||||
for (const [key, field] of [['prompt', 'prompt_graph'], ['workflow', 'workflow_graph']]) {
|
||||
let value = { status: 'absent', value: null };
|
||||
if (Object.hasOwn(raw, key) || Object.hasOwn(formats, key)) {
|
||||
if (failed || formats[key] === 'limited') value.status = failed || 'limited';
|
||||
else if (formats[key] === 'invalid') value.status = 'malformed';
|
||||
else if (carrier === null && formats[key] === 'reserialized-json') value.status = 'limited';
|
||||
else if (carrier === null) value = projectGraph(raw[key], key);
|
||||
else {
|
||||
try {
|
||||
let member = carrier[key];
|
||||
if (typeof member === 'string') member = strictProjection(member, key);
|
||||
value = graph(member, key === 'workflow');
|
||||
} catch (error) { value.status = outcome(error).status; }
|
||||
}
|
||||
}
|
||||
result[field] = value;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** UTF-8 length of `JSON.stringify(value)`: the server's `_bytes` count (lone surrogates are already escaped). */
|
||||
export function inspectionBytes(value) {
|
||||
const text = JSON.stringify(value);
|
||||
let count = 0;
|
||||
for (let index = 0; index < text.length; index++) {
|
||||
const code = text.charCodeAt(index);
|
||||
if (code < 0x80) count += 1;
|
||||
else if (code < 0x800) count += 2;
|
||||
else if (code >= 0xd800 && code <= 0xdbff) { count += 4; index++; } else count += 3;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
const withMetadata = (payload, changes) => ({ ...payload, metadata: { ...payload.metadata, ...changes } });
|
||||
|
||||
/**
|
||||
* The omission steps of `_encode_inspection`: the workflow graph first, then the
|
||||
* prompt graph, then all raw records atomically, each measured as encoded bytes.
|
||||
* Returns a new payload; throws when even the bare payload is over `limit`.
|
||||
*/
|
||||
export function fitInspectionPayload(payload, limit = MAX_RESPONSE_BYTES) {
|
||||
let fitted = payload, body = inspectionBytes(payload);
|
||||
for (const field of ['workflow_graph', 'prompt_graph']) {
|
||||
if (body <= limit) break;
|
||||
if (fitted.metadata[field].status !== 'absent') {
|
||||
fitted = withMetadata(fitted, { [field]: { status: 'limited', value: null } });
|
||||
body = inspectionBytes(fitted);
|
||||
}
|
||||
}
|
||||
if (body > limit) {
|
||||
fitted = withMetadata(fitted, { raw: {}, parameters: null, status: 'limited', omitted_reason: 'response-size-limit' });
|
||||
body = inspectionBytes(fitted);
|
||||
}
|
||||
if (body > limit) throw new RangeError('Inspection response exceeds limits');
|
||||
return fitted;
|
||||
}
|
||||
Reference in New Issue
Block a user