fix(inspect): a list or object workflow node type answers malformed
The R1 overwritten-text check looked every UI workflow node's `type` up among the subgraph IDs. A `type` that is a JSON list or object raised TypeError there, so the inspect route answered 500 "Prompt inspection failed". Such a workflow is now malformed evidence: the resolution is unresolved with reason_code `malformed` and the inspector's own "Prompt evidence is malformed" line, never exception text. The browser port answers the same, and the parity corpus gains the list, object, nested-subgraph and skipped-ID shapes.
This commit is contained in:
+1
-1
File diff suppressed because one or more lines are too long
@@ -108,3 +108,12 @@ test('committed server answers equal the local evidence', () => {
|
||||
}
|
||||
assert.ok(compared > 140, `compared ${compared}`);
|
||||
});
|
||||
|
||||
test('a list or object workflow node type is malformed evidence, as on the server (#286)', () => {
|
||||
const graph = base({'6': {class_type: 'PromptManager', inputs: {text: 'a fox', clip: ['4', 1]}}});
|
||||
for (const type of [['PromptManager'], {name: 'PromptManager'}]) {
|
||||
const evidence = resolver.resolveLocalEvidence({prompt: JSON.stringify(graph), workflow: JSON.stringify({nodes: [{id: 6, type}]})}, semantics);
|
||||
assert.deepEqual(evidence.resolution, {state: 'unresolved', sampler_id: null, reason: 'Prompt evidence is malformed', reason_code: 'malformed', node: null});
|
||||
assert.equal(evidence.prompts.negative.message, 'Prompt metadata in this file is malformed.');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -833,6 +833,33 @@ def golden_cases():
|
||||
_graph(["6", 0], ["7", 0], overwritten),
|
||||
workflow="{bad",
|
||||
)
|
||||
# A list or object node ``type`` is malformed evidence (#286), also inside a
|
||||
# subgraph and after a node the walk skips; one with an invalid ID is skipped.
|
||||
for label, kind in (("list", ["PromptManager"]), ("dict", {"a": 1})):
|
||||
add(
|
||||
f"workflow-type-{label}",
|
||||
_graph(["6", 0], ["7", 0], overwritten),
|
||||
workflow=json.dumps(
|
||||
{"nodes": [{"id": 6, "type": "PromptManager"}, {"id": 8, "type": kind}]}
|
||||
),
|
||||
)
|
||||
add(
|
||||
"workflow-type-in-subgraph",
|
||||
_graph(["1:6", 0], ["1:7", 0], nested_graph),
|
||||
workflow=json.dumps(
|
||||
{
|
||||
**nested,
|
||||
"definitions": {
|
||||
"subgraphs": [{"id": "sub-a", "nodes": [{"id": 2, "type": [1]}]}]
|
||||
},
|
||||
}
|
||||
),
|
||||
)
|
||||
add(
|
||||
"workflow-type-without-id",
|
||||
_graph(["6", 0], ["7", 0], overwritten),
|
||||
workflow=json.dumps({"nodes": [{"id": True, "type": ["x"]}, "skipped"]}),
|
||||
)
|
||||
add(
|
||||
"several-samplers",
|
||||
_graph(
|
||||
|
||||
@@ -1643,6 +1643,43 @@ class TestPromptInspector(ImageAPITestCase):
|
||||
str(path), False, report_ambiguity=True
|
||||
)
|
||||
|
||||
async def test_unhashable_workflow_node_type_answers_malformed(self):
|
||||
"""A UI workflow node whose ``type`` is a list or dict is malformed (#286).
|
||||
|
||||
The R1 check looks each node's ``type`` up among subgraph IDs; an
|
||||
unhashable ``type`` used to raise there and the route answered 500.
|
||||
"""
|
||||
source = graph()
|
||||
source["1"]["class_type"] = "PromptManager"
|
||||
for kind in (["PromptManager"], {"name": "PromptManager"}):
|
||||
with self.subTest(kind=type(kind).__name__):
|
||||
workflow = json.dumps(
|
||||
{"nodes": [{"id": 1, "type": kind, "widgets_values": ["x"]}]}
|
||||
)
|
||||
png(
|
||||
self.output_dir / "image.png",
|
||||
prompt=json.dumps(source),
|
||||
workflow=workflow,
|
||||
)
|
||||
response, result = await self.inspect()
|
||||
self.assertEqual(response.status, 200)
|
||||
self.assertEqual(
|
||||
result["resolution"],
|
||||
{
|
||||
"state": "unresolved",
|
||||
"sampler_id": None,
|
||||
"reason": "Prompt evidence is malformed",
|
||||
"reason_code": "malformed",
|
||||
"node": None,
|
||||
},
|
||||
)
|
||||
for side in result["prompts"].values():
|
||||
self.assertEqual(side["status"], "unresolved")
|
||||
self.assertEqual(
|
||||
side["message"], "Prompt metadata in this file is malformed."
|
||||
)
|
||||
self.assertNotIn("unhashable", json.dumps(result))
|
||||
|
||||
|
||||
class TestInspectorImageTags(ImageAPITestCase):
|
||||
"""B3: the inspector envelope's ``image_tags`` facet (spec amendment S1)."""
|
||||
|
||||
@@ -20,6 +20,7 @@ from .prompt_pair_resolver import (
|
||||
_FOOTER,
|
||||
EMPTY_POSITIVE_REASON,
|
||||
INSPECTION_PROFILE,
|
||||
MALFORMED_EVIDENCE_REASON,
|
||||
ResolverProfile,
|
||||
resolve_prompt_pair,
|
||||
)
|
||||
@@ -548,7 +549,7 @@ def inspect_prompt_evidence(
|
||||
resolution = {
|
||||
"state": "unresolved",
|
||||
"sampler_id": None,
|
||||
"reason": "Prompt evidence is malformed",
|
||||
"reason": MALFORMED_EVIDENCE_REASON,
|
||||
"reason_code": "malformed",
|
||||
"node": None,
|
||||
}
|
||||
|
||||
@@ -1753,7 +1753,12 @@ def _resolve(
|
||||
)
|
||||
overwritten = frozenset()
|
||||
if semantics is not None and "prompt" in metadata:
|
||||
overwritten = _overwritten_prompt_nodes(metadata.get("workflow"), normalized)
|
||||
try:
|
||||
overwritten = _overwritten_prompt_nodes(
|
||||
metadata.get("workflow"), normalized
|
||||
)
|
||||
except _Unresolved as error:
|
||||
return _result("unresolved", reason=str(error), reason_code=error.code)
|
||||
resolver = _GraphResolver(
|
||||
normalized,
|
||||
adapters or {},
|
||||
@@ -1832,6 +1837,8 @@ def _shared_pair(candidates: list) -> Optional[Dict[str, Any]]:
|
||||
|
||||
# Bounds for walking a UI workflow's subgraph instances (R1 only).
|
||||
_MAX_SUBGRAPH_DEPTH = 8
|
||||
# The inspector's own line for evidence it cannot read (``inspect_prompt_evidence``).
|
||||
MALFORMED_EVIDENCE_REASON = "Prompt evidence is malformed"
|
||||
_MAX_WORKFLOW_NODES = 100_000
|
||||
|
||||
|
||||
@@ -1862,7 +1869,8 @@ def _prompt_manager_widgets(workflow: Any) -> Dict[str, str]:
|
||||
|
||||
Nodes inside a subgraph run as ``"<instance>:<inner>"`` API IDs (nested
|
||||
instances chain their IDs); a definition without an instance runs nothing.
|
||||
The walk is bounded in depth and in nodes visited.
|
||||
The walk is bounded in depth and in nodes visited. A node ``type`` that is
|
||||
a list or an object makes the workflow malformed (``_Unresolved``).
|
||||
"""
|
||||
nodes = workflow.get("nodes") if isinstance(workflow, dict) else None
|
||||
if not isinstance(nodes, list):
|
||||
@@ -1882,6 +1890,9 @@ def _prompt_manager_widgets(workflow: Any) -> Dict[str, str]:
|
||||
if not isinstance(node_id, (str, int)) or isinstance(node_id, bool):
|
||||
continue
|
||||
api_id = f"{prefix}{node_id}"
|
||||
if isinstance(kind, (list, dict)):
|
||||
# JSON's unhashable values are never a node type (#286).
|
||||
raise _Unresolved(MALFORMED_EVIDENCE_REASON, "malformed")
|
||||
if kind in subgraphs and depth < _MAX_SUBGRAPH_DEPTH:
|
||||
stack.append((subgraphs[kind], api_id + ":", depth + 1))
|
||||
continue
|
||||
|
||||
@@ -38,6 +38,7 @@ export const MAX_BRANCH_CANDIDATES = 32;
|
||||
export const RUNTIME_REASON = 'Text is decided at run time';
|
||||
export const EMPTY_POSITIVE_REASON = 'Positive prompt is empty';
|
||||
export const MULTISTAGE_UNATTRIBUTED_REASON = 'Multistage sampler ancestry is ambiguous';
|
||||
export const MALFORMED_EVIDENCE_REASON = 'Prompt evidence is malformed';
|
||||
export const SEMANTICS_UNAVAILABLE_REASON = 'Inspection node semantics are unavailable';
|
||||
export const INCOMPATIBLE_SLOT = 'Output slot has an incompatible type';
|
||||
export const OVERWRITTEN_PROMPT_REASON = 'Saved PromptManager text was overwritten by another node';
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
GUIDER_ABSENT_REASON,
|
||||
INCOMPATIBLE_SLOT,
|
||||
LocalResolverGap,
|
||||
MALFORMED_EVIDENCE_REASON,
|
||||
MAX_BRANCH_CANDIDATES,
|
||||
MAX_JOIN_INPUTS,
|
||||
MAX_SUBGRAPH_DEPTH,
|
||||
@@ -632,9 +633,9 @@ function noSampler(graph) {
|
||||
return result('unresolved', { reason, reasonCode: 'no-sampler' });
|
||||
}
|
||||
|
||||
/** Python `kind in mapping` for a workflow `type`: an unhashable list or dict raises `TypeError` there. */
|
||||
/** A workflow `type` that is a list or an object is malformed evidence on the server too (#286). */
|
||||
function hashable(value) {
|
||||
if (isList(value) || isDict(value)) throw new TypeError("unhashable type in workflow node 'type'");
|
||||
if (isList(value) || isDict(value)) throw new Unresolved(MALFORMED_EVIDENCE_REASON, 'malformed');
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -701,7 +702,13 @@ function resolve(metadata, semantics) {
|
||||
if (!isDict(graph) || !graph.size) return result('unresolved', { reason: 'API graph is missing or malformed', reasonCode: 'malformed' });
|
||||
if (graph.size > PROFILE.nodeCeiling) return result('unresolved', { reason: 'Prompt graph exceeds the inspection node ceiling', reasonCode: 'graph-ceiling' });
|
||||
if (semantics === null) return result('unresolved', { reason: SEMANTICS_UNAVAILABLE_REASON });
|
||||
const overwritten = metadata.has('prompt') ? overwrittenPromptNodes(pyGet(metadata, 'workflow'), graph) : new Set();
|
||||
let overwritten = new Set();
|
||||
if (metadata.has('prompt')) {
|
||||
try { overwritten = overwrittenPromptNodes(pyGet(metadata, 'workflow'), graph); } catch (error) {
|
||||
if (!(error instanceof Unresolved)) throw error;
|
||||
return result('unresolved', { reason: error.message, reasonCode: error.code });
|
||||
}
|
||||
}
|
||||
const resolver = new GraphResolver(graph, metadata.has('prompt') ? metadata : null, semantics, overwritten);
|
||||
const samplerIds = [...graph].filter(([, node]) => isDict(node) && typeof pyGet(node, 'class_type') === 'string' && resolver.samplerClasses.has(node.get('class_type'))).map(([id]) => id);
|
||||
if (!samplerIds.length) return noSampler(graph);
|
||||
@@ -744,7 +751,7 @@ function parsedWorkflow(value) {
|
||||
export function resolveLocalEvidence(records, semantics) {
|
||||
const source = records !== null && typeof records === 'object' ? records : {};
|
||||
const metadata = new Map(Object.entries(source).filter(([key]) => ROLE_METADATA_KEYS.has(key)));
|
||||
const malformedResolution = { state: 'unresolved', sampler_id: null, reason: 'Prompt evidence is malformed', reason_code: 'malformed', node: null };
|
||||
const malformedResolution = { state: 'unresolved', sampler_id: null, reason: MALFORMED_EVIDENCE_REASON, reason_code: 'malformed', node: null };
|
||||
if (!metadata.size) {
|
||||
return {
|
||||
prompts: { positive: sideOf('absent'), negative: sideOf('absent') },
|
||||
|
||||
Reference in New Issue
Block a user