Skip to content

test: characterize read_file behavior for unaccepted parameters#518

Closed
wonderwhy-er wants to merge 1 commit into
mainfrom
test/read-file-unknown-params
Closed

test: characterize read_file behavior for unaccepted parameters#518
wonderwhy-er wants to merge 1 commit into
mainfrom
test/read-file-unknown-params

Conversation

@wonderwhy-er

@wonderwhy-er wonderwhy-er commented Jun 17, 2026

Copy link
Copy Markdown
Owner

What

Test-only PR. Adds an integration test that documents what read_file returns today when a caller sends parameters the schema doesn't accept.

It drives the real MCP server over stdio via the SDK Client (the same path an LLM hits), so every assertion is against the actual CallToolResult the model receives — not a direct handler call.

Why

A model recently called read_file with view_range, which the schema doesn't define. The key was silently stripped, the defaults took over, and it read 1000 lines from the start — looking like the tool "ignored the range." This test pins down that behavior so it's visible in CI and so any future change to it is intentional.

Cases asserted (current behavior on main)

  1. Unknown/unaccepted params (view_range, foo_bar) → isError is falsy, the read falls back to a read-from-start, and nothing in the response tells the model a param was stripped. This is the gap.
  2. Wrong type on a known param (offset: "x") → dispatcher-shaped isError: true referencing offset.
  3. Missing required param (path) → dispatcher-shaped isError: true.

Intended follow-up (not in this PR)

The future behavior is to keep returning the normal response but append a warning that some parameters were stripped, so the model can self-correct. When that lands, the Case 1 "no warning" assertion flips — it's the regression anchor.

Notes

  • Lives in test/integration/, auto-discovered by run-all-integration-tests.js; excluded from the default run-all-tests.js suite.
  • Saves and restores editable config (the spawned server uses the real config file) and cleans up its temp dir.
  • Verified passing locally against a fresh dist built from main.

Summary by CodeRabbit

  • Tests
    • Added integration test verifying read_file behavior with various parameter scenarios, including handling of unknown parameters, type validation for known parameters, and required parameter enforcement.

Adds an integration test that drives the real MCP server over stdio (like an
LLM client) and asserts on the actual CallToolResult for three cases:

- Unknown/unaccepted params (e.g. view_range, foo_bar) are silently stripped:
  the read succeeds, isError is falsy, and nothing tells the model its param
  was ignored. This documents the current gap. The intended future behavior is
  to keep the normal response but append a warning that params were stripped;
  when that lands, the Case 1 "no warning" assertion flips and acts as the
  regression anchor.
- Wrong type on a known param returns a dispatcher-shaped isError: true.
- Missing required param (path) returns a dispatcher-shaped isError: true.

Test only; no production code changes.
@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

A new integration test file is added that launches the real MCP server over stdio and exercises read_file with three parameter scenarios: unknown extra parameters, a known parameter with the wrong type, and a missing required parameter. It provisions a temporary directory and file, patches allowedDirectories config, asserts the CallToolResult shape for each case, and cleans up after itself.

Changes

read_file unknown-params integration test

Layer / File(s) Summary
Test infrastructure: client, setup, teardown, error handler
test/integration/read-file-unknown-params.js
Documents the test purpose and gap being anchored; creates the MCP client via StdioClientTransport over dist/index.js; provisions a temp directory and numbered text file; captures and patches allowedDirectories config; restores config and removes temp files in teardown; adds a top-level catch handler that prints TEST FAILED and exits with code 1.
Core read_file parameter assertions
test/integration/read-file-unknown-params.js
Asserts unknown parameters (view_range, foo_bar) do not produce isError and produce no warning text about stripped/ignored params; asserts offset as a non-number produces isError: true with an error message referencing offset; asserts missing required path produces isError: true.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐇 A rabbit hops through temp dirs with care,
Sending bogus params to see what's there—
No warnings appear, no errors arise,
But wrong types and missing paths get the prize!
The gap is now anchored, the test is set free. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and clearly summarizes the main change: adding an integration test that documents and validates the behavior of the read_file tool when given unaccepted parameters.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch test/read-file-unknown-params

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 and usage tips.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/integration/read-file-unknown-params.js`:
- Around line 84-131: The main() function has resource leak issues because
setup() occurs before the try block and teardown() could fail before
client.close() executes. Restructure using nested try-finally blocks: move the
client creation outside, then wrap setup() in an outer try-finally that ensures
client.close() in its finally, and wrap the test cases in an inner try-finally
that ensures teardown() in its finally. This guarantees that client.close()
executes even if setup() throws, and that close() executes even if teardown()
throws, preventing stdio transport and subprocess hangs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 77dc95af-0f14-49aa-b888-0beaa1d2baf3

📥 Commits

Reviewing files that changed from the base of the PR and between 7a9b2ff and 2255a89.

📒 Files selected for processing (1)
  • test/integration/read-file-unknown-params.js

Comment on lines +84 to +131
async function main() {
console.log('===== read_file Unknown-Parameter Behavior Integration Test =====');
const client = await createMcpClient();
const originalConfig = await setup(client);

try {
// --- Case 1: unknown/unaccepted parameters are silently stripped ---
const unknown = await callTool(client, 'read_file', {
path: TEST_FILE,
view_range: [5, 10], // not a real param on main
foo_bar: true, // clearly bogus
});
const unknownText = textOf(unknown);
console.log('\n[Case 1] unknown params -> isError:', !!unknown.isError);

// 1a. The call is NOT rejected.
assert.ok(!unknown.isError, 'Case 1: unknown params should NOT cause isError (they are stripped)');
// 1b. Because the params were ignored, it falls back to defaults: a read from
// the START of the file, not lines 5-10 the caller asked for.
assert.ok(/line-1\b/.test(unknownText), 'Case 1: should read from the start (params ignored)');
assert.ok(!/^line-5$/m.test(unknownText) || /line-1\b/.test(unknownText),
'Case 1: did not honor the requested range');
// 1c. THE GAP: nothing in the response tells the model a param was stripped.
// When the future "append a warning" behavior lands, flip this assertion.
const mentionsStripped = /strip|ignored|unknown param|unrecognized|not a valid/i.test(unknownText);
assert.ok(!mentionsStripped,
'Case 1 (current behavior): response contains NO warning about stripped params');
console.log('[Case 1] PASS - params silently stripped, no warning returned (documented gap)');

// --- Case 2: wrong type on a known param -> shaped isError ---
const badType = await callTool(client, 'read_file', { path: TEST_FILE, offset: 'not-a-number' });
console.log('[Case 2] wrong type -> isError:', !!badType.isError);
assert.ok(badType.isError, 'Case 2: wrong type should return isError: true');
assert.ok(/offset/i.test(textOf(badType)), 'Case 2: error should reference offset');
console.log('[Case 2] PASS - wrong type surfaced to the model');

// --- Case 3: missing required param -> shaped isError ---
const missing = await callTool(client, 'read_file', { offset: 2 });
console.log('[Case 3] missing path -> isError:', !!missing.isError);
assert.ok(missing.isError, 'Case 3: missing path should return isError: true');
console.log('[Case 3] PASS - missing required param surfaced to the model');

console.log('\nAll assertions passed. Current read_file param behavior is documented.');
} finally {
await teardown(client, originalConfig);
await client.close();
}
}

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Ensure client cleanup even if setup or teardown fails.

The current structure has two resource leak scenarios:

  1. If setup(client) throws after the client is created (line 87), the finally block at lines 127-130 won't execute, leaving client unclosed.
  2. If teardown() throws (line 128), client.close() at line 129 won't execute.

Both scenarios could leave the stdio transport and subprocess hanging, affecting test reliability in CI environments.

🔒 Proposed fix with nested try-finally blocks
 async function main() {
   console.log('===== read_file Unknown-Parameter Behavior Integration Test =====');
   const client = await createMcpClient();
   try {
     const originalConfig = await setup(client);
-
     try {
-    // --- Case 1: unknown/unaccepted parameters are silently stripped ---
+      // --- Case 1: unknown/unaccepted parameters are silently stripped ---
...
-    console.log('\nAll assertions passed. Current read_file param behavior is documented.');
+      console.log('\nAll assertions passed. Current read_file param behavior is documented.');
+    } finally {
+      await teardown(client, originalConfig);
+    }
   } finally {
-    await teardown(client, originalConfig);
     await client.close();
   }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/integration/read-file-unknown-params.js` around lines 84 - 131, The
main() function has resource leak issues because setup() occurs before the try
block and teardown() could fail before client.close() executes. Restructure
using nested try-finally blocks: move the client creation outside, then wrap
setup() in an outer try-finally that ensures client.close() in its finally, and
wrap the test cases in an inner try-finally that ensures teardown() in its
finally. This guarantees that client.close() executes even if setup() throws,
and that close() executes even if teardown() throws, preventing stdio transport
and subprocess hangs.

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