From db150d1a43dde8620bbeb0c56cedff9a46faef0f Mon Sep 17 00:00:00 2001 From: Charles Yao Date: Sun, 30 Aug 2026 06:08:17 -0700 Subject: [PATCH] feat(dify-builder): Edit live test reuses prepared input, routes input-failures to testdata gate Co-Authored-By: Claude Opus 4.8 --- api/core/dify_builder/handlers_edit.py | 53 +++++++++++++++++-- .../core/dify_builder/test_handlers_edit.py | 48 +++++++++++++++++ 2 files changed, 98 insertions(+), 3 deletions(-) diff --git a/api/core/dify_builder/handlers_edit.py b/api/core/dify_builder/handlers_edit.py index 2c53f4a3f9c..e977e0377a3 100644 --- a/api/core/dify_builder/handlers_edit.py +++ b/api/core/dify_builder/handlers_edit.py @@ -37,6 +37,7 @@ from core.dify_builder.handlers_fix import ( build_form_fields, emit_canvas, first_failed_node, + is_input_failure, merge_known_keys, mint_checkpoint, perform_revert, @@ -322,13 +323,13 @@ def handle_test_affected_paths(env: Env, turn: Turn, s: Session, fc: DifyBuilder edit.review; failure -> real diagnose + propose_repair, staged for the edit.await_repair approval gate. No auto-apply (human-gated).""" graph, _hash = env.dify.read_graph(s.app_id, turn.actor) - if fc.test_input_ref == "": + if fc.test_input_ref: + inputs = env.repo.get_test_input(fc.test_input_ref).inputs + else: # defensive: the gate normally prepares inputs first inputs = env.agent.generate_mock_inputs(start_schema(graph), {}) ti = TestInput(session_id=s.id, source="mock", inputs=inputs) env.repo.save_test_input(ti) fc.test_input_ref = ti.id - else: - inputs = env.repo.get_test_input(fc.test_input_ref).inputs emit = env.emit if env.emit is not None else (lambda _e: None) try: @@ -390,6 +391,52 @@ def handle_test_affected_paths(env: Env, turn: Turn, s: Session, fc: DifyBuilder run.culprit_node_id = first_failed_node(per_node) fc.verify_run_id = run.id emit_canvas(env, "mark_test_error") + + if is_input_failure(run): + # the run failed on its INPUT, not the config -- route back to the + # testdata gate instead of the config-repair gate; clear the stale + # input ref (and the verify_run_id we just set) so a fresh + # provide_testdata cycle starts clean. + fc.test_input_ref = "" + fc.verify_run_id = "" + test_items = append_card( + fc, + TestResultCard( + title="Affected-path tests", + subtitle="Failed", + tone="error", + stats=[TestStat(value="1", label="runs"), TestStat(value="1", label="errors")], + run_ids=[run.id], + ), + ) + form_items = append_card( + fc, + FormCard( + variant="testdata", + fields=testdata_form_fields(start_schema(graph)), + values={}, + frozen=False, + ), + ) + turn_items = append_card( + fc, + AssistantTurnItem( + turn_id=str(uuid.uuid4()), + stage_id="edit.await_testdata", + trace=Trace(status="completed", steps=[]), + reply_text="The run failed on its inputs — provide test data and retry.", + cards=["test_result", "form"], + ), + ) + return StepResult( + next=PcState.EDIT_AWAIT_TESTDATA, + context=fc, + items=[*test_items, *form_items, *turn_items], + run=run, + run_id_sink=[run.id], + ) + + # config failure: existing diagnose + propose_repair -> EDIT_AWAIT_REPAIR diagnosis = env.agent.diagnose(run, graph, per_node) intents, risk = env.agent.propose_repair(diagnosis, graph) fc.diagnosis = diagnosis diff --git a/api/tests/unit_tests/core/dify_builder/test_handlers_edit.py b/api/tests/unit_tests/core/dify_builder/test_handlers_edit.py index 6e5a1793489..54b5ae096d1 100644 --- a/api/tests/unit_tests/core/dify_builder/test_handlers_edit.py +++ b/api/tests/unit_tests/core/dify_builder/test_handlers_edit.py @@ -351,6 +351,54 @@ def test_edit_test_run_draft_raises_routes_to_await_repair_failed(): assert result.run.status == "failed" +def test_edit_test_input_failure_routes_to_testdata_gate(): + """An INPUT-caused run failure (missing/invalid test data, per + is_input_failure's signal match) must clear the stale input ref and route + back to the testdata gate -- not the config-repair gate.""" + from core.dify_builder.handlers_edit import handle_test_affected_paths + from core.dify_builder.models import TestInput + + env, _ = _new_env() + env.dify.verify_pass = False + env.dify.fail_error = "File variable not found for selector: ['start', 'document']" + s = _session(entry_mode=EntryMode.EDIT, current_state=PcState.EDIT_TEST_AFFECTED_PATHS) + env.repo.save_test_input(TestInput(id="ti-1", session_id=s.id, source="mock", inputs={})) + fc = DifyBuilderContext(edit_target_node_ids=["llm"], test_input_ref="ti-1") + + result = handle_test_affected_paths(env, Turn(actor=_actor()), s, fc) + + assert result.next == PcState.EDIT_AWAIT_TESTDATA + assert result.context.test_input_ref == "" # stale input cleared + assert result.context.verify_run_id == "" + kinds = [i.kind for i in result.items] + assert "form" in kinds + assert "change_set" not in kinds # gate, not repair + test_result = next(i for i in result.items if i.kind == "test_result") + assert test_result.payload["tone"] == "error" + assistant = next(i for i in result.items if i.kind == "assistant_turn") + assert assistant.payload["stage_id"] == "edit.await_testdata" + + +def test_edit_test_config_failure_still_routes_to_repair_gate(): + """A config-caused run failure (the fake's default error, which matches no + input-failure signal) must still route to the config-repair gate, + unchanged.""" + from core.dify_builder.handlers_edit import handle_test_affected_paths + from core.dify_builder.models import TestInput + + env, _ = _new_env(agent=StubAgent()) + env.dify.verify_pass = False # default error "boom" -> config, not input + s = _session(entry_mode=EntryMode.EDIT, current_state=PcState.EDIT_TEST_AFFECTED_PATHS) + env.repo.save_test_input(TestInput(id="ti-1", session_id=s.id, source="mock", inputs={})) + fc = DifyBuilderContext(edit_target_node_ids=["llm"], test_input_ref="ti-1") + + result = handle_test_affected_paths(env, Turn(actor=_actor()), s, fc) + + assert result.next == PcState.EDIT_AWAIT_REPAIR + assert result.context.staged_repair # StubAgent proposes a repair + assert result.context.test_input_ref == "ti-1" # untouched on the config path + + def test_review_publish_reaches_terminal_edit_publish_with_publish_card(): from core.dify_builder.handlers_edit import handle_review