Skip to content

feat: add regression eval for finding Edge Function console output - #232

Open
claude[bot] wants to merge 14 commits into
mainfrom
feat/eval-edge-function-console-output
Open

feat: add regression eval for finding Edge Function console output#232
claude[bot] wants to merge 14 commits into
mainfrom
feat/eval-edge-function-console-output

Conversation

@claude

@claude claude Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Requested via Slack thread

A user asks where their Edge Function's console.log output went. The agent's only logs tool is query_logs, and its ClickHouse schemaHint lists edge_logs, postgres_logs and function_edge_logs — but not function_logs, which is the source console output actually lands in (debugging-tools.ts, logs.ts). An agent that trusts the hint queries function_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.jsonl gives checkout-quote ten request envelopes that are all POST | 200 with no console content, plus six real console lines under a different source: an expired SPRING24 coupon being silently dropped, and a pricing-gateway timeout 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_VERSION in packages/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 the function_logs source), and hand-editing apps/web/src/data. The refreshed apps/web/src/data/regression-eval-results.json in the diff is a run-evals-changed workflow commit (15a71c3), not part of the hand-authored change. Evidence: pnpm format:check clean, pnpm typecheck clean, pnpm eval:dry parses all 49 scenarios and plans this one at mode=tools under the standard experiments, and a throwaway platform-lite test (run, then deleted) confirmed the seed's console rows land under source = 'function_logs' while the envelope rows land only under function_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 test

Requested by Matt Rossman.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JACD1KWUAJ4G5BD7KfwTtk


Generated by Claude Code

@vercel

vercel Bot commented Aug 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
evals Ready Ready Preview Aug 26, 2026 1:18pm

Request Review

@barryroodt barryroodt 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.

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.

Comment thread evals/investigate-functions-002-edge-function-console-output/EVAL.ts Outdated
@claude
claude Bot requested a review from barryroodt August 25, 2026 09:30
@barryroodt
barryroodt marked this pull request as ready for review August 25, 2026 09:34
@barryroodt
barryroodt requested a review from a team August 25, 2026 09:34

@barryroodt barryroodt 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.

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 logs query passes SOURCE_DISCOVERY without satisfying SOURCE_NARROWED.
  • Required invariant: actual tool-result evidence must show that the query returned the scenario’s known console rows.
  • Related shallow matches include function_logs or source = in comments and strings, plus lower(source).
  • CI: Vercel PASS, Vercel Preview Comments PASS, CodeSmith SKIPPED, no failed checks.
  • The later workflow commit changed only generated result text. EVAL.ts did not change.

Comment thread evals/investigate-functions-002-edge-function-console-output/EVAL.ts Outdated
@claude

claude Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

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.

queryResultRows only accepted an array result. At the pinned 0.11.0, query_logs returns { result: wrapWithUntrustedDataBoundary(body) }, and that helper returns a stringJSON.stringify(body) between <untrusted-data-{uuid}> tags, prose either side (tools/debugging-tools.ts:254, tools/util.ts:89-101 @ mcp-server-supabase-v0.11.0). So Array.isArray is false and zero rows come out.

It landed. At 703356b the eval flipped:

- "passed": true                                   + "passed": false
- attempts: 1                                      + attempts: 2
- markers returned: SPRING24, pricing-gateway,     + markers returned: none
    cart_8f21ac, timed out after 3 retries

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 { result: rows } directly, without the wrapper — a shape the server never returns.

Fix: unwrap a string result once, then apply your existing { result: [rows] } rule to the parsed value (the embedded JSON is the management API body, itself { result: [rows] }). Direct array path stays. Plain JSON.parse runs first so every working shape keeps its exact behaviour, and the tag scan goes backwards because the 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. 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). test:eval-scorers 14 passed. Driving check 2 with the real envelope returns SPRING24, cart_8f21ac, pricing-gateway, timed out after 3 retries, and split-across-rows still fails.


Generated by Claude Code

@barryroodt barryroodt 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.

Both scorer blockers are resolved, and the final delta looks good to merge.

claude Bot pushed a commit that referenced this pull request Aug 25, 2026
…-mcp-server-0-11-0

Stacks PR #236 on top of the approved PR #232.
claude Bot pushed a commit that referenced this pull request Aug 25, 2026
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
claude and others added 10 commits August 25, 2026 15:41
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
claude Bot pushed a commit that referenced this pull request Aug 25, 2026
…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.
@claude
claude Bot requested a review from barryroodt August 26, 2026 13:14
@claude

claude Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

Merged main in and resolved the conflict in apps/framework/package.json — kept main's updated test:vercel-runner list alongside this branch's test:eval-scorers script. main now pins MCP_SERVER_VERSION to 0.11.0, so the version-pinning scaffolding here is obsolete: dropped the claude-code-sonnet-5-mcp-0-11 experiment, removed the skipEval clause from claude-code-sonnet-5, and restored the single skills predicate in claude-code-sonnet-5-no-skills. Format check, typechecks, and the eval scorer tests all pass locally.


Generated by Claude Code

@barryroodt barryroodt 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.

Approved

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

run-evals-changed Add to a PR to refresh only the benchmark evals that have had changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants