Two paths ended a streaming advance without a terminal `state`/`error`
frame, leaving the SSE client stalled to the deadline with a stale held
version -> its next action 409s:
1. advance_session published the terminal `state` frame outside the
try/finally and unguarded. If the post-advance view projection
(get_session_view -> read_graph) or the publish itself raised after a
SUCCESSFUL advance, the exception escaped and no terminal frame was
sent. Guard it and fall back to a terminal `error` frame.
2. submit_action_stream emitted a terminal `state` frame on settle only
for `message`. `update_model` also settles synchronously while bumping
the version, so it now emits one too -- otherwise the FE keeps the old
version and 409s on its next action.
Adds regression tests for both (projection-failure -> error frame;
update_model -> state frame).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
WorkflowService.publish_workflow returns a single Workflow (-> Workflow), but
dify_port.publish still unpacked it as (workflow, retirement_candidates) -- a
stale signature. Every other caller was updated when the return type changed
(via the main merge); only the Dify Builder port was missed, so every publish
crashed with "TypeError: cannot unpack non-iterable Workflow object", surfaced
to the user as a generic "step failed" SSE frame.
Assign the single return value (retirement_candidates was unused anyway). The
existing test_dify_port publish test mocked publish_workflow as a 2-tuple, which
masked the bug -- corrected it to the real -> Workflow signature so it now guards
the regression.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A build for a workflow app whose goal reads like a chatbot produced an empty
canvas while the assistant reported "Workflow built on the canvas.": the plan
was chatbot-shaped, build_nodes fed it to the generator (mode="workflow"), the
LLM emitted a chatflow graph (answer node / no end node), the generator
rejected it as MISSING_TERMINAL -- a class it does NOT retry -- and build_nodes
swallowed the error into []. handle_plan_approval then treated [] as a
successful build: it deleted the placeholder start and advanced to
build.execution, leaving nothing on the canvas.
Two parts:
1. build_nodes now prepends an explicit workflow-topology directive (one
start + >=1 end node, no answer nodes) to the generator instruction, and
retries ONCE on an error/empty result with the specific error fed back as
a corrective nudge -- covering the terminal-node failure the shared
generator skips.
2. handle_plan_approval refuses to fake success: when build_nodes yields zero
create_node intents it surfaces an honest error and stays in
build.plan_approval (retryable) without deleting the placeholder, instead
of emptying the canvas and claiming a build. A loop-back re-approve still
returns the fixed create intents (only filtered as already-present), so
zero create intents unambiguously means a genuine generation failure.
Adds regression tests for both. Note: part 1 improves LLM reliability
(verifiable live); part 2 is the deterministic guarantee against the
false-success empty canvas.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
It was stubbed to [] in LlmBuilderAgent, canned in PlaceholderAgent, and never
called by any handler — the Build failure path reuses Fix cognition (diagnose +
propose_repair). Removed from the DifyBuilderAgent Protocol, both implementations,
and all test doubles.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Final-review fix wave for the testdata-gate + start-selector slice:
- I1 (Important): is_input_failure's signal list included the bare
"required" (and generic "variable not found"), which also fire on
config/runtime faults unrelated to test inputs -- misrouting those
failures to the *_AWAIT_TESTDATA gate, where the diagnosed auto-repair
is never offered and re-running just loops at the gate. Narrowed
_INPUT_FAILURE_SIGNALS to ("file variable", "not provided", "missing
input"), favoring precision per the spec's ambiguous-failure ->
config-repair safe default. The real file-input E2E error ("File
variable not found for selector: [...]") still matches via "file
variable".
- M2 (Minor, latent): handle_plan_approval's from-scratch delete-then-
create-filter path could drop a generator create_node/connect that
reuses a just-deleted placeholder start's id, since _already_present
compared against the PRE-delete existing_node_ids/existing_edges --
delete with no re-create, silently vanishing the node. Deleted ids are
now excluded from that comparison set; the loop-back path (no deletes)
is unaffected.
- Cleanups: state.py's Build/Edit state-count comments (14->15, 9->10,
verified against the actual enum members), FormCard's docstring now
lists the "testdata" variant, and handle_plan_approval binds
current_graph's nodes to one local instead of calling .get() 3x and
documents the delete-placeholder branch.
Tests: extended test_is_input_failure_matches_input_signals's sibling
with a config "required" case (red before the signal-list fix, green
after), and added a plan_approval test where the generator's create_node
reuses the deleted placeholder start's id (red before the id-exclusion
fix -- a dangling connect ValueError -- green after).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Clear fc.test_input_ref and fc.verify_run_id on the four re-plan/revert
escape branches in Build and Edit (handle_review's continue_adjusting
and handle_reverted's retry_after_revert) so a rebuild+retest
regenerates fresh schema-shaped mock inputs instead of reusing the
first build's stale ones -- the approve_repair retest path is left
untouched since it must fairly retest against the same failing case.
Also corrects three doc/comment mismatches (llm_agent's 12->13 method
count, an await_repair docstring describing a branch that doesn't
exist, and state.py's Build state-count banner), and adds tests for
the run_draft-raises degrade path and Build/Edit pass-path canvas
event symmetry.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Task B2 review found that removing the old canned test_test_affected_paths_
emits_result_and_reaches_review test dropped two pass-path assertions that
none of the 4 replacement tests reproduced: that the success path fires both
the mark_test_success and mark_review_ready canvas events, and that the
emitted SummaryCard's variant is "review". Restore both onto
test_edit_test_pass_goes_to_review_with_real_run so a regression on either
point is caught again.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rewrite handle_test_affected_paths from a canned always-success stub to a
live handler mirroring Build's handle_test_and_repair (A5): derive mock
inputs once via start_schema/generate_mock_inputs and persist a TestInput
(reused on retest), run run_draft (degrading to a synthesized failed Run on
exception), and mint a real immutable Run. Pass -> edit.review with a green
TestResultCard + summary. Fail -> real diagnose/propose_repair, stage
fc.diagnosis/staged_repair/risk, emit a red TestResultCard + ErrorCard (+
ChangeSetCard when intents are proposed) -> edit.await_repair (the B1 gate).
No auto-apply; apply_repair still runs only inside handle_await_repair's
approve_repair branch.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extends the fail-routes-to-await-repair test to assert card content
(error tone, diagnosis body/node_id, change_set presence, mark_test_error
canvas event) and adds a dedicated test for the no-proposed-intents fail
branch, plus a light card-shape check on the pass path. Closes the
test-coverage gaps left by removing the old canned-arc tests in the A5
live-test rewrite.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>