Skip to content

test(mcp): pin owner refusals as typed isError problems - #2319

Merged
ScriptedAlchemy merged 1 commit into
masterfrom
fleet/red-tests-refusal-shape
Sep 27, 2026
Merged

ScriptedAlchemy merged 1 commit into
masterfrom
fleet/red-tests-refusal-shape

Conversation

@ScriptedAlchemy

Copy link
Copy Markdown
Owner

What was wrong

mcp_suite was red on origin/master: 511 passed, 74 failed of 585 (reproduced on f1d4e56631, with the CLI binary built first and the test process run outside any memory scope). This includes the two named tests (automation_run_list_refuses_a_non_directory_dashboard_root, schema_test::schema_required_arguments_match_representative_handler_parsers) and the four graph_query_test tests (redundancy_reports_ranked_repository_exact_families_with_bounded_pages, test_grep_missing_pattern_errors, test_rank_invalid_direction, tracedecay_by_qualified_name_returns_the_symbol_at_that_exact_name).

All 74 have one root cause. 21552e3 removed the adapter that flattened a code-only graph-tool problem into a JSON-RPC error: every graph-tool-owner refusal now keeps the owner's whole problem record and is rendered as an isError tool result. That covers arguments the typed parser rejects and route/unavailable problems. Its unit test graph_tool_adapter_preserves_a_code_only_problem_record pins that intent.

Decision per test: production correct, pins stale (all 74)

No test showed a genuinely different outcome, so no production change was needed. Each stale pin now asserts the literal kind, code and exact message of the problem, e.g.

kind    = "invalid_request"
code    = "application.surface.invalid_request"
message = "invalid arguments for tracedecay_search: missing field `query`"   (the "tool execution failed: config error: " prefix is gone)

Route problems keep their reason code, for example automation_run_list_refuses_a_non_directory_dashboard_root now asserts unavailable / automation_run_ledger_unavailable / automation run ledger is unavailable during list: config error: automation dashboard root is not a directory.

Four paths still answer JSON-RPC errors, and their pins now state that explicitly instead of assuming one shape:

  • Non-object arguments: rejected at the MCP boundary before any owner parses them (changelog, files, node, diff_context).
  • Retained tools: fact_store_*, lcm_expand.
  • Source edits: insert_at.
  • Other application-surface tools: callers.

schema_test now declares the expected refusal shape per tool.

Shared helpers are in tests/mcp_suite/support.rs. refusal_problem reads the transport result (isError, with problem or structuredContent.problem). tool_result_problem / expect_tool_refusal read a handler ToolResult, which carries the semantic-error mark the transport later renders as isError.

Evidence

  • Before, origin/master f1d4e56631, full mcp_suite: test result: FAILED. 511 passed; 74 failed; 0 ignored.
  • After, this branch, full mcp_suite: test result: ok. 585 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 276.97s.
  • The updated pins are falsifiable. Each asserts isError/semantic error plus the exact problem literal, so reverting production to JSON-RPC errors (or changing kind, code or message) fails them.
  • cargo fmt --all -- --check: clean.
  • cargo clippy -p tracedecay --all-targets --features test-transport,test-helpers -- -D warnings: see the comment below. At the time of writing master is red on clippy::result_large_err in tracedecay-daemon-protocol / tracedecay-runtime-core, owned by the in-flight large-error fix. No allow was added.

Test-only change; no Windows-specific code touched.

Since 21552e3 every graph-tool-owner refusal, including arguments the
typed request parser rejects, answers as an isError tool result carrying
the owner's whole problem record (an MCP 2025-11-25 tool-execution
error) instead of a JSON-RPC error. 74 mcp_suite tests still pinned the
JSON-RPC shape; they now assert the problem's literal kind, code and
message. Non-object arguments, retained tools, source edits and callers
still answer JSON-RPC errors, and their pins say so explicitly.
@changeset-bot

changeset-bot Bot commented Sep 27, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: f165e1b

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-27T10:54:39.605255Z f165e1b PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f165e1b65d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1193 to +1195
let problem = match &result["structuredContent"]["problem"] {
Value::Null => &result["problem"],
transported => transported,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require wire problems under structuredContent

Transport-level tests now call this helper, but the fallback to result["problem"] also accepts the dispatcher-private shape that rmcp is supposed to remove and relocate to structuredContent.problem. If that transport conversion regresses, these tests still pass even though MCP clients cannot read the problem from the standard field; use separate helpers for handler and wire results, with the wire helper requiring structuredContent.problem.

AGENTS.md reference: AGENTS.md:L213-L214

Useful? React with 👍 / 👎.

Comment on lines +217 to +220
assert_unavailable(
&retrieve(&fixture, json!({"handle": SHORT_HANDLE, "max_chars": 0})).await,
"response_handle_invalid_page_size",
"tracedecay_retrieve max_chars must be at least 1",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Classify an invalid page size as invalid_request

When a caller supplies max_chars: 0, the request is rejected by input validation before any authority becomes unavailable, and the previous assertion correctly treated it as non-retryable invalid parameters. Requiring kind == "unavailable" now pins a misleading typed state, causing clients to handle a correctable request as a service outage; map this refusal to invalid_request while retaining its specific code and message.

AGENTS.md reference: AGENTS.md:L9-L12

Useful? React with 👍 / 👎.

Comment on lines +183 to +187
fn assert_unavailable(response: &Value, code: &str, message: &str) {
let problem = crate::support::refusal_problem(&response["result"]);
assert_eq!(problem["kind"], "unavailable");
assert_eq!(problem["code"], code);
assert_eq!(problem["message"], message);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve retryability checks in refusal assertions

This helper is used for both invalid page/offset refusals, which were asserted as retryable: false, and corrupt/unreadable cache failures, which were asserted as retryable: true, but it no longer checks retryability at all. A regression that removes or inverts the retry directive would therefore leave the suite green while changing client recovery behavior; accept the expected retryability as an argument and assert the typed retry fields.

AGENTS.md reference: AGENTS.md:L213-L214

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant