diff --git a/api/core/dify_builder/handlers_build.py b/api/core/dify_builder/handlers_build.py index 380fd64d890..42428857017 100644 --- a/api/core/dify_builder/handlers_build.py +++ b/api/core/dify_builder/handlers_build.py @@ -632,17 +632,31 @@ def handle_await_testdata(env: Env, turn: Turn, s: Session, fc: DifyBuilderConte return StepResult(next=PcState.BUILD_TEST_AND_REPAIR, context=fc) +# Node outputs are only truncated upstream at ~100,000 chars per string, so a +# report-generating workflow (the Builder's showcase case) can otherwise push +# hundreds of KB of JSON into the conversation-item row, the SSE frame, and +# the localizer walk on every successful build. The card is a preview, not a +# download -- this cap keeps it one. +_MAX_TERMINAL_OUTPUT_CHARS = 2000 +_TERMINAL_OUTPUT_TRUNCATED_MARKER = "\n… (truncated)" + + def _terminal_output(per_node: list[NodeOutput]) -> str: """The last node that produced anything, as readable JSON, or "". Deliberately status-agnostic: node status spellings differ by source ("success" vs "succeeded"), and a status filter that silently misses is worse than showing the output of a run that ended badly -- showing what - ran is the point of the card. + ran is the point of the card. Capped at ``_MAX_TERMINAL_OUTPUT_CHARS`` + (see its comment) -- a big JSON blob is capped with a visible marker + rather than silently dropped. """ for node in reversed(per_node): if node.outputs: - return json.dumps(node.outputs, ensure_ascii=False, indent=2) + rendered = json.dumps(node.outputs, ensure_ascii=False, indent=2) + if len(rendered) > _MAX_TERMINAL_OUTPUT_CHARS: + return rendered[:_MAX_TERMINAL_OUTPUT_CHARS] + _TERMINAL_OUTPUT_TRUNCATED_MARKER + return rendered return "" diff --git a/api/tests/unit_tests/core/dify_builder/test_handlers_build.py b/api/tests/unit_tests/core/dify_builder/test_handlers_build.py index af143d7ca6f..ed783c1db70 100644 --- a/api/tests/unit_tests/core/dify_builder/test_handlers_build.py +++ b/api/tests/unit_tests/core/dify_builder/test_handlers_build.py @@ -600,6 +600,25 @@ def test_a_passing_test_shows_what_the_workflow_produced(): assert '"result": "42"' in test_result.payload["output"] +def test_a_passing_test_caps_an_oversized_output_with_a_marker(): + """Node outputs are only truncated upstream at ~100,000 chars per string, + so an uncapped render (e.g. a report-generating workflow, the Builder's + showcase case) could push hundreds of KB of JSON into the card, the SSE + frame, and the localizer walk on every successful build. The card must + cap the rendered output instead, with a visible truncation marker.""" + from core.dify_builder.handlers_build import _MAX_TERMINAL_OUTPUT_CHARS, handle_test_and_repair + + env, _ = _new_env() # default FakeDifyPort; verify_pass=True by default + env.dify.run_outputs = {"report": "x" * 50_000} # far beyond the cap once JSON-rendered + s = _session(entry_mode=EntryMode.BUILD, current_state=PcState.BUILD_TEST_AND_REPAIR) + fc = DifyBuilderContext(built_node_ids=["llm"]) + result = handle_test_and_repair(env, Turn(actor=_actor()), s, fc) + test_result = next(i for i in result.items if i.kind == "test_result") + output = test_result.payload["output"] + assert len(output) < _MAX_TERMINAL_OUTPUT_CHARS + 100 # capped, not the ~50KB payload + assert "truncated" in output + + def test_a_passing_test_with_no_output_shows_none(): """No output produced -> the card's output is "", not a stale/placeholder value -- a passing test that shows nothing is not evidence of anything,