feat(mcp): build_so_modify_ui — diff-decorated SO modify card#858
Conversation
There was a problem hiding this comment.
Pull request overview
Adds the sales-order-specific modify/delete/correct Prefab UI card, extending the modify-card redesign to SO workflows and wiring it into modification result dispatch.
Changes:
- Introduces
build_so_modify_uiwith header diffs, sub-entity sections, failed-action summaries, and apply-state state slots. - Dispatches
entity_type == "sales_order"through the new SO modify UI builder. - Adds unit and browser coverage for SO modify preview/applied/partial-failure rendering.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
katana_mcp_server/src/katana_mcp/tools/prefab_ui.py |
Adds SO modify UI builder and helper functions. |
katana_mcp_server/src/katana_mcp/tools/_modification.py |
Routes sales-order modification responses to the new UI. |
katana_mcp_server/tests/test_prefab_ui.py |
Adds direct builder tests for SO modify card behavior. |
katana_mcp_server/tests/browser/test_modification_render.py |
Adds browser render coverage for SO partial-failure applied state. |
katana_mcp_server/tests/browser/render_test_server.py |
Adds canned SO partial-failure render scenario. |
dougborg
left a comment
There was a problem hiding this comment.
Review Summary
The SO modify card implements the full diff-decorated morph pattern correctly for the standalone-applied path and the preview render. The $result.state.* envelope discipline is clean, all generated files are untouched, the footer applied_verb is a mustache template (correct), and the per-section sub-entity grouping covers all 12 wire operations. One Tier-1 state-badge morph gap is architectural and shared with PO (pre-existing), but two issues need fixing before merge.
BLOCKING (2)
1. katana_mcp_server/tests/test_prefab_ui.py — test_address_update_renders_unknown_prior_in_summary docstring contradicts the implementation
The docstring says "MUST show (prior unknown) → new" but address_style=True in _format_so_diff_pairs deliberately strips the arrow and renders field: new (bare-after form). The test assertions only check that the values appear — they pass because of the bare form, not because of the (prior unknown) arrow. The docstring is actively misleading about the rendering contract.
Why: a future dev reading this test will believe the rendered output includes the arrow, add an assertion for (prior unknown), and be confused when it fails. Worse, a refactor that changes address_style=True to the arrow form (thinking they're keeping the documented contract) would regress silently.
Fix: Rewrite the docstring to say "address updates collapse to the bare field: new form (no arrow) because Katana has no per-address GET — address_style=True skips the is_unknown_prior path." Add a negative assertion: assert '(prior unknown)' not in rendered to pin that the arrow form does NOT appear.
2. katana_mcp_server/tests/browser/test_modification_render.py — SO modify browser test only exercises standalone-applied, not the preview→Confirm morph path
test_so_modify_partial_failure_applied_renders calls render_scenario("so_modify_partial_failure_applied") which directly renders the apply response with is_preview=False. This is a different code path than the iframe morph (preview card → Confirm click → on_success SetState chain → morph). The state-driven Alert (If(Rx("applied_subentity_failed_count") > 0)) is the ONLY component that actually morphs on the SO card, and it is not exercised by the current browser test.
Why: if applied_subentity_failed_count is mis-seeded (e.g. a typo in the SetState slot name) the standalone-applied test still passes because the standalone path seeds state directly in Python; the morph test would catch it. The existing test_apply_button_morphs_card_to_applied_state (BOM) sets the precedent — SO deserves the same click-through coverage.
Fix: Add a browser test scenario that renders the SO modify preview card, mocks the apply call to return _so_modify_partial_failure_response(), clicks Confirm, and asserts the morph outcome (PARTIAL FAILURE badge, failed-action Alert body, retry coaching text). Follow the existing test_apply_button_morphs_card_to_applied_state pattern.
SUGGESTIONS (2)
3. katana_mcp_server/src/katana_mcp/tools/prefab_ui.py — Tier-1 state badge frozen at build time on in-place morph path
_render_preview_header takes Python-time applied_state_label / applied_state_variant strings. On the in-place morph path (preview → Confirm), the header badge always reads APPLIED/green even when the outcome is PARTIAL FAILURE or FAILED, because the label was baked into the tree before the Confirm click. The applied_outcome_label / applied_outcome_variant state slots and their SetState chain are seeded but nothing binds them to the header badge. The per-field ✗ glyphs and the sub-entity failed-action Alert provide partial failure signals, but the prominent Tier-1 badge doesn't morph. This is the same limitation as PO modify (#722). File a tracking issue referencing both cards rather than deferring silently.
Fix: File a GitHub issue (Workstream: Cards, P2-soon) noting that _render_preview_header needs a state-driven badge branch (Badge(label=Rx("applied_outcome_label"), variant=Rx("applied_outcome_variant"))) to complete the morph contract.
4. katana_mcp_server/src/katana_mcp/tools/prefab_ui.py — _so_subentity_failed_summary walks all actions including header actions
_so_subentity_failed_summary filters for succeeded is False across the full actions list. A failed update_header action would appear here as "Failed — update_header #42: …" in the sub-entity Alert, even though header failures are already surfaced by _render_failed_changes_block. The user would see the error in two places.
Fix: Filter to sub-entity operations only. Cheapest form: _SO_SUBENTITY_OPS = frozenset(op for _, _, ops in _SO_SUBENTITY_GROUPS for op in ops) then gate on str(a.get('operation') or '').lower() in _SO_SUBENTITY_OPS.
NITPICKS (1)
5. katana_mcp_server/src/katana_mcp/tools/prefab_ui.py — _format_so_add_fulfillment first-truthy fallback conflates status with date
status = (_so_change_new(changes, 'status') or _so_change_new(changes, 'picked_date') or 'new fulfillment') — if status is absent but picked_date is present, the line reads status 2026-05-08T14:30:00Z (label says "status", value is a timestamp). Minor UX quirk; low priority.
What Looks Good
- All
$result.*references are correctly$result.state.*— the envelope-wrap trap from #835 review is cleanly dodged. _normalize_so_prior_statecorrectly handles theorder_no→order_numberandadditional_info→noteswire-shape mismatch.applied_verb="{{ applied_verb }}"passed as mustache to_render_preview_footer— footer body morphs correctly in lockstep with the outcome Badge.- Sub-entity section bucketing (
_SO_SUBENTITY_GROUPS) covers all 12 wire operations; fallback in_format_so_action_summaryhandles unknown future ops gracefully. _SO_HEADER_FIELD_SPECtable-driven scalar diffs are clean and easy to extend.- 22 unit tests cover preview + applied-success + applied-partial-failure + applied-all-failed + per-sub-entity-type + morph-slot seeding. The failed Badge variant test walks the JSON envelope (not string-matching) — correct approach.
- No generated files touched, no
# noqa, no# type: ignore.
🤖 Generated with Claude Code
0d4b40e to
e719fe6
Compare
|
Review feedback addressed in 3fb1e870 (squashed into c349484 via autosquash). Per-finding response: BLOCKING (1) — BLOCKING (2) — SO modify browser test doesn't exercise the morph path: Fixed. Added
SUGGESTION (3) — Tier-1 state badge frozen at build time: Filed as #860 (P2-soon, Cards). Replied inline. SUGGESTION (4) — NITPICK (5) — CI gates locally clean: |
e719fe6 to
69e4ebc
Compare
1eec0e5 to
6cee158
Compare
6cee158 to
9878ca3
Compare
9878ca3 to
e1178e5
Compare
e1178e5 to
c6eda82
Compare
c6eda82 to
d313599
Compare
d313599 to
aa730a5
Compare
aa730a5 to
d88ccac
Compare
…723) Adds the per-entity SO modify card to the #721 modify-card redesign umbrella. Reuses the entity-view diff helpers shared with PO modify (_render_field_diff_line / _render_party_diff_line / _render_failed_changes_block) and follows the BOM modify card's state-driven apply-outcome morph pattern for parallel sub-entity outcomes that build-time rendering cannot predict. - New build_so_modify_ui in prefab_ui.py — dispatched via _modification.to_tool_result on entity_type == "sales_order". Covers modify_sales_order, delete_sales_order, correct_sales_order. - Header scalar field-spec (_SO_HEADER_FIELD_SPEC) drives the per-field diff line emission table-style instead of branchy code. - Per-sub-entity sections (Line items / Addresses / Fulfillments / Shipping fees) — actions render with kind gutter (+ / ~ / -) + per-action APPLIED / FAILED Badge on the applied path. Failed actions get the ✗ gutter for layout-stable per-row failure signal. - Apply-outcome morph state seeded for applied_outcome_label, applied_outcome_variant, applied_subentity_failed_count, applied_subentity_failed_summary, applied_verb so the preview iframe's on_success SetState chain reads $result.state.* (NOT $result.<field> — same envelope-wrap limitation documented on build_bom_modify_ui). - Browser scenario: so_modify_partial_failure_applied — pins the partial-failure render contract end-to-end including the consolidated sub-entity failed-action Alert with retry coaching. 22 new unit tests in TestBuildSOModifyUI mirror the PO modify coverage shape (preview, applied-success, applied-partial-failure, applied-all-failed) and add SO-specific cases (sub-entity grouping, address update unknown-prior handling, customer party diff, failed sub-entity ✗ gutter + Badge).
d88ccac to
406744d
Compare
Closes #723
Summary
build_so_modify_uias the per-entity SO modify/delete/correct card for the design(mcp): modify-card redesign umbrella — diff-decorated entity views per object type (#551 follow-up) #721 modify-card redesign umbrella._render_field_diff_line/_render_party_diff_line/_render_failed_changes_block); follows BOM modify's state-driven apply-outcome morph pattern for SO's parallel sub-entity outcomes (rows / addresses / fulfillments / shipping fees).+/~/-kind gutter + per-action APPLIED/FAILED Badge on the applied path; failed actions swap to✗for layout-stable failure signal. The consolidated sub-entity failed-action Alert at the bottom carries the textual error + a retry-coaching tail referencing the SO id._SO_HEADER_FIELD_SPEC(table-driven instead of branchy code)._modification.to_tool_resultonentity_type == "sales_order".Test plan
uv run poe agent-check— clean (ruff format/lint + ty).uv run poe test— 3669 passed.uv run poe test-browser— 35 passed (including the newso_modify_partial_failure_appliedscenario).TestBuildSOModifyUImirror PO modify's coverage shape (preview, applied-success, applied-partial-failure, applied-all-failed) plus SO-specific cases (sub-entity grouping, address-update unknown-prior, customer party diff, failed sub-entity ✗ gutter + Badge).Design notes
SO has multiple parallel-outcome sub-entities, so I followed the BOM modify card's morph pattern: build-time render paints per-action rows once, then the post-Confirm in-place morph reads from
state.*(seeded from$result.state.*on success) to surface failures the build-time tree couldn't predict. Reading$result.<field>directly is broken because$resultresolves to the apply tool's PrefabApp envelope, not the rawModificationResponse— same envelope-wrap limitation documented onbuild_bom_modify_uiand pinned bytest_apply_button_morphs_card_to_applied_state.Sub-entity action grouping is keyed off the wire
operationenum via_SO_SUBENTITY_GROUPSso future schema additions land in a sensible bucket without renderer churn.🤖 Generated with Claude Code