fix(dify-builder): cap the test-result card's rendered output

_terminal_output json.dumps'd a node's outputs with no length limit. Node
outputs are only truncated upstream at ~100,000 chars per string, so a
report-generating workflow -- the Builder's showcase case -- could push
hundreds of KB of JSON into a dify_builder_conversation_items row, the SSE
frame, and the localizer walk on every successful build.

Cap the rendered output at 2000 chars with a visible truncation marker
before it reaches TestResultCard.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Charles YaoandClaude Opus 5 committed 2026-09-20 16:37:45 -07:00
1 parent 383d6d3624
commit 7ae152c5c2
2 files changed
+35 -2

No files matched your search

+16 -2
View File
@@ -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 ""
@@ -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,