feat: add regression eval for finding Edge Function console output - #232
feat: add regression eval for finding Edge Function console output#232claude[bot] wants to merge 14 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
barryroodt
left a comment
There was a problem hiding this comment.
Requesting changes for one blocker: the query check can accept the assistant’s narration instead of an actual query_logs query. Please scope it to real SQL inputs and make the intended policy explicit, including whether a broad unified-logs query that finds the correct rows should pass.
The scenario, seed, and version isolation otherwise look sound.
For agents
Exact evidence + requested invariant
Pinned head: 15a71c30169813ee80afeef94f378cf3890c5fea.
In evals/investigate-functions-002-edge-function-console-output/EVAL.ts, lines 14-24 combine every string tool-call input with serializeTranscript(..., { includeToolCallInputs: true }). The pinned serializer includes every message’s text unconditionally at packages/core/src/index.ts:580-582. Line 39 then matches /function_logs/i anywhere in that combined text, line 50 gates check 2 on the match, and line 62 gates the eval through checks.every. Raw tool outputs are excluded unless the assistant restates them.
Requested invariant: check only SQL inputs from actual query_logs calls, then apply the chosen source policy to those inputs. Please decide whether a broad unified-logs query that surfaces the correct rows satisfies that policy.
Verification status: Vercel Preview Comments passed; CodeSmith skipped. GitHub exposes no format, typecheck, or dry-run check results.
Non-blocking wording note: experiments/claude-code-sonnet-5-mcp-0-11.ts:17 says “past 0.10.0”, though query_logs exists at 0.10.0.
barryroodt
left a comment
There was a problem hiding this comment.
The original blocker is fixed: queryLogsSql now reads only real query_logs calls. couldReturnConsoleRows leaves one replacement blocker, so REQUEST_CHANGES stays. The rest of the delta looks sound.
For agents
Delta review details
- Head:
3e37545a2494c16b4fe91ff88166182a69ecb09f - Mechanism: a metadata-only grouped
logsquery passesSOURCE_DISCOVERYwithout satisfyingSOURCE_NARROWED. - Required invariant: actual tool-result evidence must show that the query returned the scenario’s known console rows.
- Related shallow matches include
function_logsorsource =in comments and strings, pluslower(source). - CI: Vercel PASS, Vercel Preview Comments PASS, CodeSmith SKIPPED, no failed checks.
- The later workflow commit changed only generated result text.
EVAL.tsdid not change.
|
Your hardening was right and is kept as-is: findings must co-occur in one row, and only row values count. Both close real holes. Pushed 85b1011 to fix the row extractor under it.
It landed. At 703356b the eval flipped: The judge check still PASSED on that same run, naming SPRING24 and the pricing-gateway timeout — so the agent did read and report the rows. False negative. The unit test stayed green because the fixture built Fix: unwrap a string Fixtures now build the real wrapped envelope. Verified all six of your reject cases still reject against both implementations, so nothing was relaxed; added two more (wrapped error body, markers quoted in the boundary prose). Generated by Claude Code |
barryroodt
left a comment
There was a problem hiding this comment.
Both scorer blockers are resolved, and the final delta looks good to merge.
With MCP_SERVER_VERSION now pinned to 0.11.0, the workaround that #232 added to run the console-output eval against a newer MCP server is dead. Worse than redundant: its skipEval clauses would wrongly exclude investigate-functions-002-edge-function-console-output from the default experiments. - Delete experiments/claude-code-sonnet-5-mcp-0-11.ts. - Drop the investigate-functions-002 skipEval from claude-code-sonnet-5. - Reduce claude-code-sonnet-5-no-skills' skipEval to the pre-existing `skills?.length === 0` clause. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SxPoSQH7JY8rjV3CVHFbZW
Adds investigate-functions-002-edge-function-console-output, which asks an agent to pull up the console.log output of a deployed Edge Function. The only logs tool it has is query_logs, whose ClickHouse schema hint lists edge_logs, postgres_logs and function_edge_logs but not function_logs, which is where console output actually lands. The seed makes the two streams separable: the request envelopes are all 200s with no console content, so an agent that stops at function_edge_logs can only report that the invocations succeeded. query_logs first ships in @supabase/mcp-server-supabase 0.10.0, which is newer than the repo-wide MCP_SERVER_VERSION pin, so the scenario runs under a new version-pinned experiment (claude-code-sonnet-5-mcp-0-11) and is skipped by the two shared regression experiments. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JACD1KWUAJ4G5BD7KfwTtk
Check 2 read `queriedSources`, which appended
`serializeTranscript(..., { includeToolCallInputs: true })`. That helper
emits every message's text unconditionally, before it looks at the flag
(packages/core/src/index.ts:580-582), so an assistant sentence like "the
logs are in function_logs" satisfied `/function_logs/i` and passed the
check with no query behind it — and check 2 gates the eval via
`checks.every`.
Read the SQL only from `ctx.toolCalls` entries whose `tool.toolName` is
`query_logs`, taking the `sql` argument (the parameter name in the pinned
0.11.0 server). No `query_logs` call at all now fails. The
source-discovery diagnostic note reads the same real SQL, so narration
cannot satisfy it either.
Policy, stated in the code: a query passes if it names `function_logs`,
or if it reads the unified `logs` stream without narrowing `source`. A
broad unified-logs query that surfaces the console rows passes on
purpose — the check exists to stop a pass built on narration alone, not
to demand a particular WHERE clause, and check 1 (the judge) already
decides whether the console content was reported.
Also fixes the experiment comment: `query_logs` exists at 0.10.0, so the
experiment can go once MCP_SERVER_VERSION reaches 0.10.0 or newer, not
"past" it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JACD1KWUAJ4G5BD7KfwTtk
The second check inspected the shape of the agent's SQL: accept it if it
named `function_logs`, or if it read the unified `logs` stream without
narrowing `source`. That is unbounded, and it was wrong in both
directions.
`SELECT source, count(*) FROM logs GROUP BY source` reads no log row at
all, yet it matched `logs`, avoided the `select distinct source`
rejection and never narrowed `source` — so it passed as a "broad query",
letting an agent supply the expected narration without ever reading a
console row. In the other direction the regexes matched `function_logs`
or `source =` inside comments and string literals, and could not see
through wrappers like `lower(source)`.
Assert on what came back instead. The check still filters to real
`query_logs` tool calls, then requires at least one call whose result
contains two distinct markers that occur only in the scenario's
`edge-function-runtime` console rows (SPRING24, pricing-gateway,
cart_8f21ac, exec-3b91d2f0, "timed out after 3 retries") and never in
the `edge-function` request-envelope rows. Two markers rather than one
so a marker echoed back inside agent-authored SQL is not by itself
evidence; every console row carrying a finding the judge asks for clears
that bar on message text alone.
`ToolCallRecord.result` is populated for CLI harnesses: the Claude Code
parser emits `tool_result` events with the raw content, and
`adaptTranscript` pairs them onto the call by `tool.id`. Its shape is
harness-specific, so the result is stringified rather than assumed to be
any one of string / MCP content-block array / `{ result: rows }`
envelope.
Drops `SOURCE_NARROWED` and `couldReturnConsoleRows`. Keeps the
non-gating source-discovery diagnostic, still derived from real
tool-call SQL, and adds the markers actually returned to it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JACD1KWUAJ4G5BD7KfwTtk
The per-row, values-only matching added in d82b8a7 is right and is kept unchanged. Its row extractor is not: it only accepted a payload whose `result` was an array. At the pinned `@supabase/mcp-server-supabase@0.11.0`, `query_logs` returns `{ result: wrapWithUntrustedDataBoundary(body) }`, and that helper returns a STRING with `JSON.stringify(body)` embedded between `<untrusted-data-{uuid}>` tags and surrounded by prose (tools/debugging-tools.ts:254 and tools/util.ts:89-101 at tag mcp-server-supabase-v0.11.0). `Array.isArray` is therefore false, no rows are extracted, and the check reports "markers returned: none". That is a false negative, and it landed: at 703356b the eval flipped to passed=false / attempts=2 with check 2 reporting no markers, while the judge check still PASSED and named SPRING24 and the pricing-gateway timeout. The previous refresh, on the same scenario, reported all four markers. The unit test stayed green because its fixture built `{ result: rows }` directly, omitting the boundary wrapper — a shape the real server never returns. So: unwrap a string `result` once, then apply the existing `{ result: [rows] }` rule to the parsed value (the JSON the server embeds is the management API body, which is itself `{ result: [rows] }`). The direct array path stays. Plain `JSON.parse` is tried first so every already-working shape keeps its exact behaviour, and the tag scan runs backwards because the wrapper prose names the tag on both sides of the real block — only bytes between the real tags count, so a marker quoted in the prose is not evidence. Fixtures now build the real wrapped envelope. All of d82b8a7's reject cases still reject (verified against both implementations), plus new ones for a wrapped error body and markers quoted in the boundary prose. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JACD1KWUAJ4G5BD7KfwTtk
Left over from replacing the boundary regex with a position scan. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JACD1KWUAJ4G5BD7KfwTtk
b7f0e26 to
00c17bf
Compare
…ation The two `git revert -m 1` commits that unstacked PR #232 also removed the `interface:` frontmatter work that landed on main in #228, because the rebased #232 branch carried it in as its second parent. Restore those paths to main's versions so the branch diff is the pin bump alone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SxPoSQH7JY8rjV3CVHFbZW
Resolve the apps/framework/package.json scripts conflict by keeping both the updated test:vercel-runner list from main and the test:eval-scorers script from this branch. main now pins MCP_SERVER_VERSION to 0.11.0, so the version-pinning scaffolding on this branch is no longer needed: drop the claude-code-sonnet-5-mcp-0-11 experiment, remove the skipEval clause from claude-code-sonnet-5, and restore the single skills predicate in claude-code-sonnet-5-no-skills.
|
Merged Generated by Claude Code |
Requested via Slack thread
A user asks where their Edge Function's
console.logoutput went. The agent's only logs tool isquery_logs, and its ClickHouseschemaHintlistsedge_logs,postgres_logsandfunction_edge_logs— but notfunction_logs, which is the source console output actually lands in (debugging-tools.ts,logs.ts). An agent that trusts the hint queriesfunction_edge_logs, finds nothing but request envelopes, and tells the user their function is fine. This adds one scenario that catches that.The seed makes the two streams cleanly separable.
investigate-functions-002-edge-function-console-output/remote/logs.jsonlgivescheckout-quoteten request envelopes that are allPOST | 200with no console content, plus six real console lines under a different source: an expiredSPRING24coupon being silently dropped, and apricing-gatewaytimeout that falls back to zero tax. Both are things a user would want to know and neither is visible from the envelopes, so "reported only 200s" and "found the console output" are distinguishable outcomes. A second, quiet function keeps the scenario from being single-function trivial.The eval needs
query_logs, which the repo-wide pin (MCP_SERVER_VERSIONinpackages/core/src/index.ts) now provides at 0.11.0, so the scenario runs under the standard experiments.Scope is one eval; non-goals are changing the pin, changing
platform-lite(it already serves the ClickHouse route and thefunction_logssource), and hand-editingapps/web/src/data. The refreshedapps/web/src/data/regression-eval-results.jsonin the diff is arun-evals-changedworkflow commit (15a71c3), not part of the hand-authored change. Evidence:pnpm format:checkclean,pnpm typecheckclean,pnpm eval:dryparses all 49 scenarios and plans this one atmode=toolsunder the standard experiments, and a throwawayplatform-litetest (run, then deleted) confirmed the seed's console rows land undersource = 'function_logs'while the envelope rows land only underfunction_edge_logs/edge_logs.pnpm format:check pnpm typecheck pnpm eval:dry -- --eval investigate-functions-002-edge-function-console-output pnpm --filter @supabase-evals/core testRequested by Matt Rossman.
🤖 Generated with Claude Code
https://claude.ai/code/session_01JACD1KWUAJ4G5BD7KfwTtk
Generated by Claude Code