Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions .github/actions/conformance/run-client.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#!/bin/bash
# Run a client conformance suite, re-verifying unexpected failures solo.
#
# Suite mode launches every scenario's client subprocess concurrently; on a
# 2-vCPU runner that contention can push scenarios with real-time waits (the
# SSE reconnect timing in sse-retry) past their tolerances. So a scenario the
# suite run flags as an unexpected failure is re-run alone on the then-quiet
# runner: a real failure fails again and the job stays red; a contention
# artifact passes and the job goes green, with a FLAKE_RESCUED marker written
# into the --output-dir so the artifact upload preserves the evidence.
# Failures that only reproduce under concurrency are deliberately traded
# away - the suite asserts spec compliance, not behavior under parallel load.
set -uo pipefail

: "${CONFORMANCE_PKG:?set CONFORMANCE_PKG (pinned in .github/workflows/conformance.yml)}"
SOLO_ATTEMPTS="${CONFORMANCE_SOLO_ATTEMPTS:-2}"

# Relative paths in the arguments (the client command, --output-dir) resolve
# from the repo root, same contract as run-server.sh.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR/../../.." || exit 1

log="$(mktemp)"
trap 'rm -f "$log"' EXIT

npx --yes "$CONFORMANCE_PKG" client "$@" 2>&1 | tee "$log"
rc=${PIPESTATUS[0]}
if [ "$rc" -eq 0 ]; then
exit 0
fi

plain="$(sed 's/\x1b\[[0-9;]*m//g' "$log")"

# Scenarios listed under "Unexpected failures (not in baseline):". Anything
# else behind the nonzero exit (stale baseline entries, harness or infra
# errors) is not retried. The extraction is coupled to the pinned harness's
# summary wording and print order; if a pin bump changes either, the list
# comes up empty and the original failure passes through - never a false
# green.
mapfile -t scenarios < <(
printf '%s\n' "$plain" |
sed -n '/^Unexpected failures (not in baseline):$/,/^$/p' |
sed -n 's/^ ✗ //p'
)
if [ "${#scenarios[@]}" -eq 0 ]; then
exit "$rc"
fi
for scenario in "${scenarios[@]}"; do
if ! [[ "$scenario" =~ ^[A-Za-z0-9/_-]+$ ]]; then
echo "Extracted unexpected-failure name '${scenario}' does not look like a scenario name; passing the suite failure through." >&2
exit "$rc"
fi
done

# A stale baseline entry is a configuration error a solo rerun cannot excuse.
if printf '%s\n' "$plain" | grep -q '^Stale baseline entries'; then
echo "Suite also reported stale baseline entries; not retrying." >&2
exit "$rc"
fi

Check warning on line 59 in .github/actions/conformance/run-client.sh

View check run for this annotation

Claude / Claude Code Review

pipefail + grep -q makes stale-baseline guard fail open on large logs

Under `set -uo pipefail`, the stale-baseline guard `printf '%s\n' "$plain" | grep -q '^Stale baseline entries'` can return 141 (SIGPIPE on the printf side) instead of 0 when more than a pipe buffer (~64KB) of log text follows the heading — so the guard is skipped exactly when the pattern IS present, and the solo reruns can green-wash a stale-baseline configuration error. Avoid the pipe, e.g. `if grep -q '^Stale baseline entries' <<<"$plain"; then` (or grep the stripped log file directly).

# Reuse the suite invocation's arguments for the solo runs, minus the flags
# that only make sense for a suite (--scenario replaces --suite; single runs
# are judged directly, not against the baseline). Solo results are saved next
# to the suite's so the uploaded artifact carries both.
rerun_args=()
output_dir=""
skip_next=0
expect_output_dir=0
for arg in "$@"; do
if [ "$skip_next" -eq 1 ]; then
if [ "$expect_output_dir" -eq 1 ]; then
output_dir="$arg"
fi
Comment thread
claude[bot] marked this conversation as resolved.
skip_next=0
expect_output_dir=0
continue
fi
case "$arg" in
--output-dir)
skip_next=1
expect_output_dir=1
;;
--suite | --expected-failures) skip_next=1 ;;
--output-dir=*) output_dir="${arg#--output-dir=}" ;;
--suite=* | --expected-failures=*) ;;
*) rerun_args+=("$arg") ;;
esac
done
if [ -n "$output_dir" ]; then
rerun_args+=(--output-dir "${output_dir}-solo")
fi

for scenario in "${scenarios[@]}"; do
passed=0
for attempt in $(seq 1 "$SOLO_ATTEMPTS"); do
echo ""
echo "Re-running '${scenario}' solo (attempt ${attempt}/${SOLO_ATTEMPTS})..."
if npx --yes "$CONFORMANCE_PKG" client --scenario "$scenario" "${rerun_args[@]}"; then
passed=1
break
fi
done
if [ "$passed" -ne 1 ]; then
echo "'${scenario}' still fails when run alone: real failure, not suite contention." >&2
exit 1
fi
done

if [ -n "$output_dir" ]; then
mkdir -p "$output_dir"
printf '%s\n' "${scenarios[@]}" > "$output_dir/FLAKE_RESCUED"
fi
echo "All ${#scenarios[@]} unexpected failure(s) passed when re-run solo; the suite failures were parallel-run contention."
exit 0
47 changes: 42 additions & 5 deletions .github/workflows/conformance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,17 +64,20 @@ jobs:
./.github/actions/conformance/run-server.sh
--suite active
--expected-failures ./.github/actions/conformance/expected-failures.yml
--output-dir conformance-results/server-active
- name: Run server conformance (draft suite)
run: >-
./.github/actions/conformance/run-server.sh
--suite draft
--expected-failures ./.github/actions/conformance/expected-failures.yml
--output-dir conformance-results/server-draft
- name: Run server conformance (2026-07-28 wire, all suite)
run: >-
./.github/actions/conformance/run-server.sh
--suite all
--spec-version 2026-07-28
--expected-failures ./.github/actions/conformance/expected-failures.2026-07-28.yml
--output-dir conformance-results/server-2026-07-28
- name: Run server conformance (all suite, extension scenarios)
# A bare `--suite all` (no --spec-version) selects every scenario
# shipped with the pinned harness — including the extension-tagged
Expand All @@ -91,6 +94,16 @@ jobs:
./.github/actions/conformance/run-server.sh
--suite all
--expected-failures ./.github/actions/conformance/expected-failures.yml
--output-dir conformance-results/server-all
- name: Upload conformance results
# The suite summary only prints counts for warning-level findings; the
# per-check measurements live in the checks.json files saved above.
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: server-conformance-results
path: conformance-results/
if-no-files-found: ignore

client-conformance:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -118,22 +131,46 @@ jobs:
echo "CONFORMANCE_PKG=file:/tmp/conformance.tgz" >> "$GITHUB_ENV"
;;
esac
- run: uv sync --frozen --all-extras --package mcp
# --compile-bytecode: the harness spawns every scenario's client
# concurrently; without pre-compiled site-packages, ~40 fresh
# interpreters race to byte-compile the same modules on a 2-core
# runner, saturating it for the first ~20s of the suite — exactly when
# timing-sensitive scenarios take their measurements.
- run: uv sync --frozen --all-extras --package mcp --compile-bytecode
- name: Pre-compile bytecode (editable sources)
run: uv run --frozen python -m compileall -q src .github/actions/conformance
- name: Run client conformance (all suite)
# The harness runs all scenarios via unbounded Promise.all; with 40
# scenarios on a 2-core runner the slowest one (sse-retry, which has a
# real-time SSE reconnect wait) needs more than the 30s default budget.
# The client command execs the synced venv's interpreter directly:
# `uv run` would re-check the lockfile in every one of the ~40
# concurrent spawns, compounding the startup storm. run-client.sh
# re-verifies unexpected failures solo before failing the job.
run: >-
npx --yes "$CONFORMANCE_PKG" client
--command 'uv run --frozen python .github/actions/conformance/client.py'
./.github/actions/conformance/run-client.sh
--command '.venv/bin/python .github/actions/conformance/client.py'
--suite all
--timeout 60000
--expected-failures ./.github/actions/conformance/expected-failures.yml
--output-dir conformance-results/client-all
- name: Run client conformance (2026-07-28 wire, all suite)
run: >-
npx --yes "$CONFORMANCE_PKG" client
--command 'uv run --frozen python .github/actions/conformance/client.py'
./.github/actions/conformance/run-client.sh
--command '.venv/bin/python .github/actions/conformance/client.py'
--suite all
--timeout 60000
--spec-version 2026-07-28
--expected-failures ./.github/actions/conformance/expected-failures.2026-07-28.yml
--output-dir conformance-results/client-2026-07-28
- name: Upload conformance results
# The suite summary only prints counts for warning-level findings; the
# per-check measurements live in the checks.json files saved above.
# Also upload when run-client.sh rescued a flake (job green, but the
# contention evidence should not be discarded).
if: failure() || hashFiles('conformance-results/**/FLAKE_RESCUED') != ''
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: client-conformance-results
path: conformance-results/
if-no-files-found: ignore
Original file line number Diff line number Diff line change
Expand Up @@ -191,10 +191,14 @@ async def test_tool_with_progress(ctx: Context) -> str:
async def test_sampling(prompt: str, ctx: Context) -> str:
"""Tests server-initiated sampling (LLM completion request)"""
try:
# Request sampling from client
# Request sampling from client. related_request_id routes the request onto
# the originating tools/call SSE stream, which exists for the whole handler;
# without it the request targets the standalone GET stream and is dropped if
# the client has not finished opening that stream yet.
result = await ctx.session.create_message( # pyright: ignore[reportDeprecated]
messages=[SamplingMessage(role="user", content=TextContent(type="text", text=prompt))],
max_tokens=100,
related_request_id=ctx.request_id,
)

# Since we're not passing tools param, result.content is single content
Expand Down
Loading