Skip to content

test(blueprint): gate required graph output fields on the native surface - #11

Closed
Fl0p wants to merge 1 commit into
base/upstream-dev-flo892from
flo-892-native-graph-output-contract
Closed

Fl0p wants to merge 1 commit into
base/upstream-dev-flo892from
flo-892-native-graph-output-contract

Conversation

@Fl0p

@Fl0p Fl0p commented Sep 11, 2026

Copy link
Copy Markdown

Context

Upstream reproduced get_node_details failing on the native transport with
OUTPUT_SCHEMA_VIOLATION: Missing required parameter 'nodeId' while A/B-testing the
posX/posY fix (ChiR24#592, merged 2026-08-10), together with create_reroute_node
returning an error receipt for a Knot it had just created.

Both are already fixed in dev — no code fix is proposed here:

defect fix landed
get_node_details omits nodeId Result->SetStringField(TEXT("nodeId"), NodeId) in ...BlueprintGraphHandlersDetails.cpp f4b9c66a, 2026-08-23
create_reroute_node omits nodeGuid Result->SetStringField(TEXT("nodeGuid"), ...) in ...BlueprintGraphHandlersNodeMutations.cpp 52b5b8d2, 2026-09-04

What is missing is a gate. The same mistake landed twice in the same domain within a
month, and both times it broke the readback step of an operation that had actually
succeeded: the native execute seam projects a handler Result onto the declared output
contract and then validates it, so one unpublished required field turns a working call
into an error and the caller never receives the handle.

What this adds

tests/unit/plugin/blueprint_graph_output_field_contracts.test.ts, driven by the
generated canonical registry plus the GraphSubActions list in the domain registration
(no hand-maintained list of actions):

  1. Domain-wide — every required output field of every graph sub-action is published by
    some BlueprintGraph handler. Sources are collected recursively, because
    list_node_types lives in Context/ and a root-only scan does not see it.
  2. Handler-scoped — for the readbacks that build their own Result
    (get_node_details, create_reroute_node, get_graph_details, get_pin_details,
    list_node_types), the field must be published inside the function that claims the
    sub-action
    . create_node is deliberately excluded and documented: it finalizes through
    the shared node-creation path, so only tier 1 can speak for it.
  3. Contract-level — the projected payloads are validated against their own output
    schema via the existing schema-subset mirror, so the omission fails as
    missing-required with the exact message the gateway reports
    (Missing required parameter 'nodeId').

Verification

Host is Raspberry Pi / ARM64 with no Unreal Engine, so nothing here was editor-verified —
but this change is tests-only and the gate itself was proven to bite:

  • npx vitest run tests/unit/plugin/blueprint_graph_output_field_contracts.test.ts → 8 passed.
  • With the two one-line fixes temporarily reverted in the working tree, the handler-scoped
    test fails and names both:
    + "blueprint.get_node_details.nodeId missing from McpAutomationBridge_BlueprintGraphHandlersDetails.cpp",
    + "blueprint.create_reroute_node.nodeGuid missing from McpAutomationBridge_BlueprintGraphHandlersNodeMutations.cpp",
    
  • npx eslint on the new file → clean.
  • npx vitest run tests/unit/plugin/ → 809 passed, 3 failed. All 3 failures pre-exist on
    clean dev (80430fd2)
    — verified by re-running them with this file removed:
    source_structure_contracts (McpAutomationBridgeHelpersProjectPaths.h is 267 pure lines
    vs the 250 gate) and the two native_discovery_* parity contracts under gateway/.

🤖 Generated with Claude Code

The native execute seam projects a handler Result onto the declared output
contract and then validates it, so a graph handler that omits one required
field answers OUTPUT_SCHEMA_VIOLATION for a call that already read or mutated
the graph. get_node_details shipped without nodeId and create_reroute_node
without nodeGuid - both are published in dev today, but nothing keeps them
published.

The gate works over the BlueprintGraph handler sources and the canonical
registry: every required output field of every graph sub-action must be
published by some handler (collected recursively, so the Context/ handlers are
in scope), the self-contained readbacks must publish theirs inside the function
that claims the sub-action, and the projected payloads are checked against
their own output schema so the omission fails with the message the gateway
actually reports.

Co-Authored-By: Daedalus <daedalus@agents.flopbut.local>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 11, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 1c647ac8-d258-4962-b696-9c136832e5de

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Fl0p

Fl0p commented Sep 19, 2026

Copy link
Copy Markdown
Author

Closing: the defect class this gate memorializes no longer exists on dev.

Rebased the branch onto dev (60ad47b8) — clean, no conflicts — and ran the suite. 7 of 8 cases fail, and not because a handler regressed. The capability registry was consolidated in the meantime:

before now
blueprint.get_node_details legacy action of blueprint.inspect_graph (5 actions per record)
blueprint.create_reroute_node legacy action of blueprint.edit_graph (9 actions per record)

Both consolidated records declare output.required = ['success']. nodeId and nodeGuid are now ordinary declared properties, so OUTPUT_SCHEMA_VIOLATION: Missing required parameter 'nodeId' — the failure this gate exists to prevent — is structurally unreachable on the graph surface.

I checked whether a quieter failure replaced it, since the projection keeps only declared properties. It did not: McpNativeGatewayOutputProjection.cpp:41-83 now folds every undeclared root field into a declared details object, and all three graph records (edit_graph, inspect_graph, delete_node) declare details. The 11 root fields the graph handlers publish outside the contract — x, y, nodeComment, nodeCount, conversionNodeId and the rest — reach the caller one level down rather than being dropped.

So both defects this branch guards were resolved by a change in the shape of the contract, not by the two one-line fixes it cites. A gate pinned to per-action capability ids cannot survive that, and rewriting it against the consolidated records would be a different test with a different premise.

Worth recording for later: the class is not extinct registry-wide. 34 records still declare a required output field beyond success/message, and one is unmet today — manage_tools.list_categories requires totalCategories, but FMcpDynamicToolManager::ListCategories() (McpDynamicToolManagerQueries.cpp:29) publishes only success and categories, while its neighbour ListTools() publishes totalTools. It surfaces as a silently absent required field rather than an error, because the local dispatch path (McpNativeTransportDynamicTools.cpp:41-52) answers directly and skips output projection and validation. That is its own fix plus a registry-wide gate, not this branch.

@Fl0p Fl0p closed this Sep 19, 2026
@Fl0p
Fl0p deleted the flo-892-native-graph-output-contract branch September 19, 2026 10:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant