Skip to content

test: Pin sync/task tool-call contracts before dispatch dedup#1073

Open
jirispilka wants to merge 2 commits into
masterfrom
claude/deduplicate-sync-task-paths-duv3zq
Open

test: Pin sync/task tool-call contracts before dispatch dedup#1073
jirispilka wants to merge 2 commits into
masterfrom
claude/deduplicate-sync-task-paths-duv3zq

Conversation

@jirispilka

Copy link
Copy Markdown
Collaborator

What we're solving

Umbrella #658 will merge the two near-duplicate tool-call paths in src/mcp/server.ts (sync CallToolRequestSchema handler and executeToolAndUpdateTask). Five handler-level contracts had no tests, so the refactor could silently change them. The riskiest is telemetry property shapes: nothing in CI guards them — the internal repo consumes none of them in code, only analytics dashboards do. Closes #1061 (step 1 of #658).

How

New tests/unit/mcp.server.tool_call_contracts.test.ts, table-driven per failure class (402 / permission-approval / generic execution error), driving the real handler with synthetic tools that throw the class fixture:

  • Task terminal-status mapping: 402 and permission-approval stored completed; generic stored failed + internalToolStatus.
  • Telemetry property shapes per class on both paths (vi.spyOn on trackToolCall; telemetry enabled with no token + allowUnauthMode, so no network).
  • Sync 402 / permission-approval result shapes (no telemetry keys leak onto the wire).
  • taskSupport pre-dispatch gate: task-augmented call to a tool without taskSupportInvalidParams, tool never invoked.
  • ACTOR_MCP-in-task-mode gap: the task path has no ACTOR_MCP branch and stores {} as completed — pinned as current behavior with a comment to flip when refactor: Extract dispatchToolCall with exhaustive TOOL_TYPE switch #1063 closes it.

Also lifts the harness the existing mcp.server.* suites copy-pasted into tests/unit/helpers/mcp_server.ts and migrates mcp.server.error_handling, mcp.server.progress_wiring, mcp.task_notifications onto it — assertion text unchanged, plumbing only. mcp.server.capability_gating deliberately not migrated (its harness differs).

Alternatives considered

  • Copy the harness locally instead of extracting — rejected in review; extraction stops the duplication spreading further.
  • vi.mock of the telemetry module — the lighter namespace spy intercepts fine, so the full mock wasn't needed.
  • Skipping the ACTOR_MCP gap until refactor: Extract dispatchToolCall with exhaustive TOOL_TYPE switch #1063 — pinning it now documents the gap and forces a conscious flip.

Run evidence

  • type-check, lint, test:unit (972 passed / 2 skipped, deterministic across repeated runs), format, check:agents all green.
  • Red-checks: flipped pinned expectations (task terminal status; task-path failure_http_status) → tests failed → restored.
  • Review: staff-review PASS after one fix round (task-path telemetry was initially unasserted); final reviewers code-review PASS (0 findings) and thermonuclear (1 minor + 2 nits, all fixed, none skipped).
  • Fulfillment: 16/16 success criteria MET.

Notes for apify-mcp-server-internal

No internal change needed: internal consumes none of the tool-call telemetry shapes in code (verified) — this suite is the only guard for them. Result shapes / _meta are pinned, not changed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LUZNJQKNCn7ntZCbYsFer5


Generated by Claude Code

claude and others added 2 commits July 10, 2026 04:07
Umbrella #658 will merge the two near-duplicate tool-call paths in
src/mcp/server.ts; five handler-level contracts had no tests, so the
refactor could silently change them. Pins, table-driven per failure
class (402 / permission-approval / generic): task terminal-status
mapping, telemetry property shapes on both paths (the only guard for
these — dashboards consume them with no CI check), sync error result
shapes, the taskSupport pre-dispatch gate, and the ACTOR_MCP-in-task
{} gap (pinned as-is, flip when #1063 lands).

Also lifts the copy-pasted test harness into
tests/unit/helpers/mcp_server.ts and migrates the three existing
mcp.server suites onto it, assertions unchanged. Alternative — copying
the harness locally — was rejected in review to stop the duplication
spreading further.

Closes #1061

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUZNJQKNCn7ntZCbYsFer5

@vojtechj-apify vojtechj-apify left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nits found by Claude during a review pass — nothing serious, all cosmetic. The substance checks out: every pinned contract assertion was traced back to the actual behavior in src/mcp/server.ts (terminal-status mapping, per-failure-class telemetry shapes on both paths, taskId absent from Segment properties, the taskSupport gate, the ACTOR_MCP task-mode gap) and is accurate. Harness extraction is faithful, the wait-for-spy in the task telemetry tests is genuinely required, and type-check/lint/unit suite (972 passed) are all green.

* `CallToolRequestSchema` catch and the `executeToolAndUpdateTask` catch). Failure classes are
* fabricated by throwing from a fake tool's `call`; both the sync path (result shapes) and the task
* path (terminal status mapping) assert the same source-of-truth per class. See DESIGN
* `.devforge/2-design.md`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit (Claude): .devforge/2-design.md is gitignored per-run scratch (the .devforge/.gitignore whitelists only config.json/registry.json), so this reference is dead for anyone reading the file later. That context fits better in the PR description than the code.

run: (server: ActorsMcpServer) => Promise<T>,
options?: Partial<ActorsMcpServerOptions>,
): Promise<T> {
const { ActorsMcpServer } = await import('../../../src/mcp/server.js');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit (Claude): the dynamic await import(...) isn't needed — none of the four consumer suites use vi.mock on this module, and the file already type-imports ActorsMcpServer. Looks like cruft carried over from the old progress_wiring harness; a static value import is simpler.

);

const handler = getHandler(server as never, 'tasks/result');
const handler = getRequestHandler(server, 'tasks/result');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit (Claude): these two tasks/result tests migrated onto getRequestHandler but kept the rest of the hand-rolled bootstrap: a dynamic await import('../../src/mcp/server.js') of a module this file already imports statically (line 6), manual new ActorsMcpServer, and await server.close() outside a finally. withServer + server.taskStore would cover both tests and finish the migration this PR started.

@vojtechj-apify vojtechj-apify left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM — contracts verified against src/mcp/server.ts behavior. The nits from the earlier review pass are non-blocking.

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.

test: Pin sync/task tool-call contracts before dispatch dedup

4 participants