feat: add auth and resilience scenarios#36
Merged
Conversation
Implements v0.3.0 config-driven scenario runner with auth classification
and explicit retry categories.
Auth classification (RFC 6750 compliant):
- AUTH_REQUIRED: 401 with no credentials supplied
- AUTH_INVALID: 401 with credentials + any rejection, including
error="invalid_token" (covers expired, revoked, and malformed — too
broad for expiry-specific classification per RFC 6750 §3.1)
- AUTH_EXPIRED: 401 with credentials + unambiguous expiry indicator
only: error="expired" or error="token_expired"
- AUTH_FORBIDDEN: 403 regardless of credentials
Retry semantics (explicit opt-in, off by default):
- Four categories: rate-limit (429), server-error (5xx/REMOTE_HTTP_ERROR),
connection-failure (TRANSPORT_ERROR/CONNECT_TIMEOUT),
response-timeout (RESPONSE_TIMEOUT)
- Requires both maxAttempts > 1 and category listed in retryOn array
- Auth errors (401/403), schema errors, and malformed MCP are never retried
- Reports show retry category, attempts performed, and max attempts
Config file support (mcp-release.config.yml):
- YAML-driven scenario definitions with per-scenario header overrides
and removeHeaders
- ${VAR_NAME} placeholder resolution from environment variables at runtime
- Structured retries block: maxAttempts, backoffMs, retryOn categories
Reporting:
- Terminal: shows "2/3 attempts, rate-limit retry" on retried scenarios
- Markdown: adds Retry column to scenario summary table
- JSON: round-trips maxAttempts and retryCategory fields
- GitHub Actions: sets step outputs for config-mode runs
Other:
- HTTP 429 Retry-After parsing (integer seconds and HTTP-date)
- Rate limit detection and RATE_LIMITED finding code
- Timeout classification: CONNECT_TIMEOUT vs RESPONSE_TIMEOUT
- Credential redaction in all report formats
- 883 tests pass (41 test files)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…EOUT The outer `timeoutPromise` in `connectToMcpServer` was registered before the inner `responseTimedOut` timer in `fetchChain`. When both were set to the same delay (the default, since `responseTimeoutMs` falls back to `timeoutMs`), the outer promise won the `Promise.race` and produced CONNECT_TIMEOUT even after a TCP connection was already established. Fix: add +1 ms to the outer timer so the inner per-request timer always fires first when the delays are equal. The inner timer still fires at `responseTimeoutMs` ms; the outer backstop fires at `timeoutMs + 1` ms, so the boundary is correctly preserved when `responseTimeoutMs` is explicitly set higher than `timeoutMs`. Updated tests assert exact finding codes: - RESPONSE_TIMEOUT when inner fires first (default or responseTimeoutMs < timeoutMs+1) - CONNECT_TIMEOUT when outer fires first (responseTimeoutMs > timeoutMs+1) - SCENARIO_TIMEOUT fires at deadline and is never accompanied by RETRY_EXHAUSTED - Category isolation: RESPONSE_TIMEOUT only retried with "response-timeout", CONNECT_TIMEOUT only retried with "connection-failure" Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…lose hang Three bugs fixed: 1. AUTH_INVALID/AUTH_EXPIRED: credential test scenarios were not sending an Authorization header, so the server saw no credentials and returned AUTH_REQUIRED instead of AUTH_INVALID/AUTH_EXPIRED. Add requestHeaders with Bearer tokens in the new automated scenario-runner tests. 2. SCENARIO_TIMEOUT attempts: the retryCategory field persisted the last-used retry category even when the deadline fired before the second request started. Clear usedRetryCategory = null in the deadline branch so the result omits retryCategory when SCENARIO_TIMEOUT fires. 3. startConnectTimeoutServer hang: net.Server.close() only stops accepting new connections — it waits for all existing connections to close before invoking its callback. The server accepted TCP sockets but never closed them, causing server.close() to block indefinitely (test timeout). Track accepted sockets in a Set and destroy them explicitly when closing. Also: - Add AUTH_INVALID and AUTH_EXPIRED automated tests to scenario-runner.test.ts with explicit credential headers and negative exclusion assertions - Update cli-verify.test.ts: HTTP localhost now always produces RESPONSE_TIMEOUT (tcpConnected=true immediately), so assert RESPONSE_TIMEOUT directly Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…requests) Previously, SCENARIO_TIMEOUT was only checked at the top of each retry iteration — between requests, not during them. This meant a hanging TCP connection or slow HTTP response could blow past the scenario budget with no way to abort it. New architecture: - One AbortController is created per scenario when scenarioTimeoutMs > 0. - Its signal is passed to every runCheck() call via CheckOptions.scenarioSignal and threaded into connectToMcpServer via ConnectOptions.scenarioSignal. - In transport.ts, the signal is included in fetchChain's merged AbortSignal so it aborts any in-flight fetch immediately. - A scenarioDeadlinePromise also races alongside connectPromise and timeoutPromise, catching the case where the deadline fires before the first HTTP request starts (e.g. during TCP/TLS establishment). - When the scenario signal fires, ScenarioTimeoutTransportError is thrown, which check.ts classifies as SCENARIO_TIMEOUT (not CONNECT_TIMEOUT or RESPONSE_TIMEOUT — the outer timer also checks the signal at fire-time to ensure the scenario deadline always wins). - Backoff sleeps are replaced with abortableSleep() so a deadline expiring during backoff aborts the sleep and prevents the next request from starting. - The scenario AbortController timer is cleared in a try/finally block to prevent timer leaks after the scenario settles. Precedence rules (deterministic): scenarioTimeoutMs fires first → SCENARIO_TIMEOUT connectMs fires first, TCP not yet connected → CONNECT_TIMEOUT responseMs fires first after connect → RESPONSE_TIMEOUT New exact tests: - SCENARIO_TIMEOUT during hanging first attempt (scenarioMs < responseMs) - SCENARIO_TIMEOUT during TLS connect (scenarioMs < connectMs) - RESPONSE_TIMEOUT when responseMs fires before scenario budget - SCENARIO_TIMEOUT after retries consume budget (fires mid-attempt, attempts=2) - SCENARIO_TIMEOUT when deadline fires during backoff (attempts=1) - No RETRY_EXHAUSTED alongside SCENARIO_TIMEOUT - Subsequent scenario completes promptly (no leaked handles) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rebuilds the bundled dist after the transport/check/scenario-runner changes landed in the two preceding commits. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The old code was auth-specific but the mismatch finding fires for any scenario type (rate-limit, timeout, server-error, successful pass). The new generic code SCENARIO_MISMATCH is emitted whenever a scenario's configured expect block does not match the actual outcome, regardless of the failure category. Changes: - FindingCode enum: AUTH_SCENARIO_MISMATCH → SCENARIO_MISMATCH - scenario-runner.ts: emit SCENARIO_MISMATCH in the mismatch branch - remediation.ts, config-terminal.ts: update key/filter - Tests: rename existing assertions, add 5 new exact-code tests (unexpected 401, unexpected 403, retry exhaustion, timeout mismatch, matched negative — no SCENARIO_MISMATCH) - README, /docs: update code name in documentation - Rebuilt github-action dist bundle Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements v0.3.0 config-driven scenario runner with RFC 6750-compliant auth classification and explicit retry categories.
Auth classification
AUTH_REQUIREDAUTH_INVALIDerror="invalid_token"(covers expired, revoked, and malformed per RFC 6750 §3.1)AUTH_EXPIREDerror="expired"orerror="token_expired"AUTH_FORBIDDENThe key correctness fix:
error="invalid_token"now producesAUTH_INVALID, notAUTH_EXPIRED. RFC 6750 §3.1 explicitly defines that code as covering expired, revoked, AND malformed tokens — it is too broad to classify as expiry-specific.Retry semantics (explicit opt-in, off by default)
retryOncategoryRATE_LIMITED)rate-limitREMOTE_HTTP_ERROR)server-errorconnection-failureresponse-timeoutA retry fires only when
maxAttempts > 1AND the failure category is listed inretryOn.Config file support (
mcp-release.config.yml)${VAR_NAME}placeholders resolved from environment variables at runtimeremoveHeadersretriesblock with explicit opt-in categoriesReporting
2/3 attempts, rate-limit retryon retried scenariosRetrycolumn to scenario summary tablemaxAttemptsandretryCategoryfieldsOther additions
Retry-Afterparsing (integer seconds and HTTP-date)RATE_LIMITEDfinding code with remediation guidanceCONNECT_TIMEOUTvsRESPONSE_TIMEOUTTest plan
pnpm typecheck— cleanpnpm lint— cleanpnpm test— 883/883 pass (41 test files)pnpm build— clean, all packages includingapps/web.env.localor credentials exposedFiles changed
New files (13):
fixtures/servers/src/auth-scenarios.ts,packages/cli/src/config.ts,packages/cli/tests/config.test.ts,packages/core/src/config-report.ts,packages/core/src/rate-limit.ts,packages/core/src/scenario-runner.ts,packages/core/tests/auth-invalid.test.ts,packages/core/tests/rate-limit.test.ts,packages/core/tests/scenario-runner.test.ts,packages/reporter/src/config-json.ts,packages/reporter/src/config-markdown.ts,packages/reporter/src/config-terminal.ts,packages/reporter/tests/config-reporter.test.tsModified files (22):
README.md,apps/web/src/app/docs/page.tsx,apps/web/tests/ui/DocsPage.test.tsx, and core/cli/reporter source and test files🤖 Generated with Claude Code