Files
dify/api/tests/unit_tests/services/dify_builder/test_preflight.py
T
Charles YaoandClaude Opus 5 fcb7c719ef feat(dify-builder): preflight the draft in apply_repair before writing it
ESQ1-302 and ESQ1-303 both died at Graph.init -- before workflow_started,
so the Builder saw an error frame with no run id and no rows, and spent
runs and approvals discovering what a validation pass could have said
up front. apply_repair now runs that pass (preflight_errors: the same
node-data validation create_node performs, with no factory or runtime
state) over the graph it is about to write and raises PreflightError,
naming each node Graph.init would reject.

Only NEW problems count: a pre-existing broken node the repair does not
touch must not veto an unrelated fix, and a repair that heals it passes.
PreflightError is a ValueError so the handlers' existing stale-intent
guards already surface it as a card.

The real ESQ1-303 dev draft is the fixture: preflight names node2 and
its numeric value, and passes once the value is a string.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 21:21:17 -07:00

60 lines
2.0 KiB
Python

"""``preflight_errors``: the dry ``Graph.init`` a draft is put through before
it is written or run. Exercised on the REAL ESQ1-303 dev draft (app 0f11bedc,
dumped from dev on 2026-09-21), which the engine refused to start."""
import copy
import json
from pathlib import Path
from services.dify_builder.preflight import preflight_errors
_FIXTURES = Path(__file__).parent / "fixtures"
def _esq1_303_draft() -> dict:
return json.loads((_FIXTURES / "esq1_303_draft_graph.json").read_text(encoding="utf-8"))
def test_the_esq1_303_dev_draft_would_not_start_and_the_error_names_the_node():
errors = preflight_errors(_esq1_303_draft())
assert len(errors) == 1
assert errors[0].startswith("node 'node2' (if-else): ")
assert "Input should be a valid string" in errors[0] # value 60 is a JSON number
def test_the_same_draft_passes_once_the_value_is_a_string():
graph = _esq1_303_draft()
node2 = next(n for n in graph["nodes"] if n["id"] == "node2")
node2["data"]["cases"][0]["conditions"][0]["value"] = "60"
assert preflight_errors(graph) == []
def test_every_broken_node_is_reported_not_only_the_first():
graph = _esq1_303_draft()
broken_twice = copy.deepcopy(next(n for n in graph["nodes"] if n["id"] == "node2"))
broken_twice["id"] = "node2b"
graph["nodes"].append(broken_twice)
errors = preflight_errors(graph)
assert [e.split(" ")[1] for e in errors] == ["'node2'", "'node2b'"]
def test_note_widgets_are_skipped_exactly_like_graph_init_skips_them():
graph = {
"nodes": [
{"id": "note", "type": "custom-note", "data": {"type": "", "text": "remember"}},
{"id": "s", "type": "custom", "data": {"type": "start", "title": "Start", "variables": []}},
],
"edges": [],
}
assert preflight_errors(graph) == []
def test_an_empty_or_shapeless_graph_has_nothing_to_report():
assert preflight_errors({}) == []
assert preflight_errors({"nodes": [None, "junk"], "edges": []}) == []