Repository navigation
feat: add Spark REST eval suite #482
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
mattrossman
merged 19 commits into
main
from
mattrossman/ai-1399-add-an-agent-eval-suite-for-spark-rest-api
Nov 5, 2026
Merged
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
242d44b
feat: add Spark REST eval suite
mattrossman f478e43
chore: defer Spark CI integration
mattrossman d0662e3
refactor: simplify Spark task prompt
mattrossman bfbc7f2
docs: expand Spark API skill reference
mattrossman 5db7d0d
fix: use recommended skills in Spark experiment
mattrossman 298a18e
fix: simplify Spark scorer checks
mattrossman fd31f3a
refactor: simplify Spark naming
mattrossman 412d42c
refactor: validate Spark tasks with Zod
mattrossman 4b5dc5f
refactor: clarify Spark runtime setup
mattrossman 9f0dec4
docs: clarify Spark management client
mattrossman c7409be
test: clarify Spark scorer failures
mattrossman 61e98c9
test: colocate Spark scorer tests
mattrossman a3b06af
chore: trim Spark project output
mattrossman 347d16c
style: format Spark output
mattrossman 26af6cd
fix: clarify Spark scenario context
mattrossman 6545171
chore: assign Spark suite code owners
mattrossman 1d5b242
chore: extend Spark code ownership
mattrossman 8774a27
refactor: move Spark runtime into suite
mattrossman 88a0fe3
refactor: clarify Spark eval checks
mattrossman File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,3 @@ | ||
| import { defineConfig } from 'vitest/config'; | ||
|
|
||
| export default defineConfig({ test: { name: 'cli evals', root: '../..' } }); | ||
| export default defineConfig({ test: { name: 'evals', root: '../..' } }); |
152 changes: 152 additions & 0 deletions
152
evals/spark/build-database-001-task-due-dates/EVAL.test.ts
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,152 @@ | ||
| import { afterEach, describe, expect, it, vi } from 'vitest'; | ||
| import { sparkRuntime } from '../../../experiments/spark/lib/runtime.js'; | ||
| import scorer from './EVAL.js'; | ||
|
|
||
| const tasks = [ | ||
| 'Write release notes', | ||
| 'Ship task tracker', | ||
| 'Review schema', | ||
| 'Plan next iteration', | ||
| ].map((title, index) => ({ | ||
| id: index + 1, | ||
| title, | ||
| completed: index === 2, | ||
| created_at: `2026-11-01 12:0${index}:00`, | ||
| due_date: index === 1 ? '2026-11-10' : null, | ||
| })); | ||
|
|
||
| afterEach(() => vi.unstubAllGlobals()); | ||
|
|
||
| async function score( | ||
| options: { | ||
| columns?: Record<string, unknown>[]; | ||
| tasks?: unknown; | ||
| tableStatus?: number; | ||
| reportPasses?: boolean; | ||
| } = {} | ||
| ) { | ||
| vi.stubGlobal('fetch', async (input: string | URL | Request) => { | ||
| const url = input instanceof Request ? new URL(input.url) : new URL(input); | ||
| if (url.pathname === '/v1/projects') | ||
| return Response.json({ | ||
| ref: 'project', | ||
| url: 'https://project.example', | ||
| api_keys: [ | ||
| { type: 'secret', api_key: 'secret' }, | ||
| { type: 'publishable', api_key: 'publishable' }, | ||
| ], | ||
| }); | ||
| if (url.pathname === '/database/query') | ||
| return Response.json( | ||
| options.columns ?? [{ name: 'due_date', type: 'date', not_null: 0 }] | ||
| ); | ||
| expect(url.pathname).toBe('/rest/v1/tasks'); | ||
| return options.tableStatus | ||
| ? Response.json( | ||
| { message: 'table unavailable' }, | ||
| { status: options.tableStatus } | ||
| ) | ||
| : Response.json(options.tasks ?? tasks); | ||
| }); | ||
| const session = await sparkRuntime('https://control.example').startSession( | ||
| {} | ||
| ); | ||
| const judge = vi.fn(async () => ({ passed: options.reportPasses ?? true })); | ||
| try { | ||
| const result = await scorer({ | ||
| ...session.scoringContext, | ||
| toolCalls: [], | ||
| transcript: [], | ||
| judge, | ||
| }); | ||
| return { result, judge }; | ||
| } finally { | ||
| await session.close(); | ||
| } | ||
| } | ||
|
|
||
| describe('task state checks', () => { | ||
| it('rejects missing dates, altered seed data and deleted rows', async () => { | ||
| for (const [changed, name] of [ | ||
| [ | ||
| tasks.map((task) => ({ ...task, due_date: null })), | ||
| 'Requested due date saved and other tasks undated', | ||
| ], | ||
| [ | ||
| tasks.map((task) => ({ ...task, title: 'Replaced' })), | ||
| 'Existing task data preserved', | ||
| ], | ||
| [ | ||
| tasks.map((task) => ({ ...task, created_at: '2026-11-02T00:00:00' })), | ||
| 'Existing task data preserved', | ||
| ], | ||
| [ | ||
| tasks.map((task) => ({ ...task, completed: 'false' })), | ||
| 'Existing task data preserved', | ||
| ], | ||
| [tasks.slice(1), 'Existing task data preserved'], | ||
| [[], 'Existing task data preserved'], | ||
| ]) | ||
| expect((await score({ tasks: changed })).result.checks).toContainEqual( | ||
| expect.objectContaining({ name, passed: false }) | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| describe('hosted task scorer', () => { | ||
| it('accepts the complete result and rejects an incorrect report', async () => { | ||
| expect((await score()).result.passed).toBe(true); | ||
| expect((await score({ reportPasses: false })).result.passed).toBe(false); | ||
| }); | ||
|
|
||
| it('accepts a matching date column after an unrelated date column', async () => { | ||
| const renamed = tasks.map(({ due_date, ...task }) => ({ | ||
| ...task, | ||
| deadline: due_date, | ||
| })); | ||
| expect( | ||
| ( | ||
| await score({ | ||
| columns: [ | ||
| { name: 'another_date', type: 'date', not_null: 0 }, | ||
| { name: 'deadline', type: 'date', not_null: 0 }, | ||
| ], | ||
| tasks: renamed, | ||
| }) | ||
| ).result.passed | ||
| ).toBe(true); | ||
| }); | ||
|
|
||
| it('rejects missing, non-date and required columns', async () => { | ||
| for (const columns of [ | ||
| [], | ||
| [{ name: 'due_date', type: 'text', not_null: 0 }], | ||
| [{ name: 'due_date', type: 'date', not_null: 1 }], | ||
| ]) { | ||
| const { result } = await score({ columns }); | ||
| expect(result.passed).toBe(false); | ||
| expect( | ||
| result.checks?.find( | ||
| (check) => check.name === 'Nullable date column added' | ||
| )?.passed | ||
| ).toBe(false); | ||
| } | ||
| }); | ||
|
|
||
| it('records unreadable state and still judges the report', async () => { | ||
| const { result, judge } = await score({ tableStatus: 404 }); | ||
| expect(result.passed).toBe(false); | ||
| expect(result.checks?.map((check) => check.name)).toEqual([ | ||
| 'Nullable date column added', | ||
| 'Existing task data preserved', | ||
| 'Requested due date saved and other tasks undated', | ||
| 'Open tasks reported in due-date order', | ||
| ]); | ||
| expect(result.checks?.slice(0, 3)).toEqual( | ||
| Array(3).fill( | ||
| expect.objectContaining({ passed: false, notes: expect.any(String) }) | ||
| ) | ||
| ); | ||
| expect(judge).toHaveBeenCalledOnce(); | ||
| }); | ||
| }); |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| import type { | ||
| CheckResult, | ||
| ToolScorer, | ||
| ToolScoringContext, | ||
| } from '@supabase-evals/core'; | ||
| import { z } from 'zod'; | ||
|
|
||
| const tasksSchema = z.array( | ||
| z | ||
| .object({ | ||
| id: z.number(), | ||
| title: z.string(), | ||
| completed: z.boolean(), | ||
| created_at: z.string(), | ||
| }) | ||
| .catchall(z.unknown()) | ||
| ); | ||
|
|
||
| async function readTaskState(ctx: ToolScoringContext) { | ||
| try { | ||
| const { rows: addedColumns } = await ctx.query( | ||
| `select name, type, "notnull" as not_null from pragma_table_info('tasks') where name not in ('id', 'title', 'completed', 'created_at')` | ||
| ); | ||
| const { data: rawTasks, error: taskReadError } = await ctx.client | ||
| .from('tasks') | ||
| .select('*') | ||
| .order('id'); | ||
| if (taskReadError) throw new Error(taskReadError.message); | ||
| const tasks = tasksSchema.parse(rawTasks); | ||
| const dateColumns = addedColumns.flatMap((column) => | ||
| typeof column.name === 'string' && | ||
| typeof column.type === 'string' && | ||
| column.type.toLowerCase() === 'date' && | ||
| column.not_null === 0 | ||
| ? [column.name] | ||
| : [] | ||
| ); | ||
| return { tasks, dateColumns, error: undefined }; | ||
| } catch (error) { | ||
| return { | ||
| tasks: [], | ||
| dateColumns: [], | ||
| error: error instanceof Error ? error.message : String(error), | ||
| }; | ||
| } | ||
| } | ||
|
|
||
| const scorer: ToolScorer = async (ctx) => { | ||
| const { tasks, dateColumns, error } = await readTaskState(ctx); | ||
| const titles = [ | ||
| 'Write release notes', | ||
| 'Ship task tracker', | ||
| 'Review schema', | ||
| 'Plan next iteration', | ||
| ]; | ||
| const createdAt = [ | ||
| '2026-11-01 12:00:00', | ||
| '2026-11-01 12:01:00', | ||
| '2026-11-01 12:02:00', | ||
| '2026-11-01 12:03:00', | ||
| ]; | ||
| const report = await ctx.judge({ | ||
| input: ctx.agentReport ?? '', | ||
| rubric: | ||
| 'The response must list exactly these open tasks with their due dates: Ship task tracker, November 10, 2026; Write release notes, no due date; Plan next iteration, no due date. Ship task tracker must appear before the undated tasks. Either order among the undated tasks is valid. Review schema is complete and must not appear as an open task. Task IDs are optional. Accept equivalent date formatting and descriptions of an absent date.', | ||
| }); | ||
| const checks: CheckResult[] = [ | ||
| { | ||
| name: 'Nullable date column added', | ||
| passed: dateColumns.length > 0, | ||
| notes: error, | ||
| }, | ||
| { | ||
| name: 'Existing task data preserved', | ||
| passed: | ||
| tasks.length === titles.length && | ||
| tasks.every( | ||
| (task, index) => | ||
| task.id === index + 1 && | ||
| task.title === titles[index] && | ||
| task.completed === (task.id === 3) && | ||
| task.created_at === createdAt[index] | ||
| ), | ||
| notes: error, | ||
| }, | ||
| { | ||
| name: 'Requested due date saved and other tasks undated', | ||
| passed: | ||
| tasks.length === titles.length && | ||
| dateColumns.some((column) => | ||
| tasks.every( | ||
| (task) => task[column] === (task.id === 2 ? '2026-11-10' : null) | ||
| ) | ||
| ), | ||
| notes: error, | ||
| }, | ||
| { name: 'Open tasks reported in due-date order', ...report }, | ||
| ]; | ||
| return { passed: checks.every((check) => check.passed), checks }; | ||
| }; | ||
|
|
||
| export default scorer; |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| --- | ||
| stage: build | ||
| interface: rest | ||
| product: | ||
| - database | ||
| - data-api | ||
| topic: | ||
| - migrations | ||
| motivation: https://linear.app/supabase/issue/AI-1399/add-an-agent-eval-suite-for-spark-rest-api | ||
| --- | ||
|
|
||
| Can you add optional due dates to our task tracker in the provided Spark project? Set "Ship task tracker" to November 10, 2026 and leave the other tasks undated. Then show me the open tasks with their due dates, soonest first and undated tasks last. |
12 changes: 12 additions & 0 deletions
12
evals/spark/build-database-001-task-due-dates/remote/project.sql
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| create table public.tasks ( | ||
| id integer primary key, | ||
| title text not null, | ||
| completed boolean not null default false, | ||
| created_at timestamptz not null default now() | ||
| ); | ||
|
|
||
| insert into public.tasks (id, title, completed, created_at) values | ||
| (1, 'Write release notes', false, '2026-11-01T12:00:00Z'), | ||
| (2, 'Ship task tracker', false, '2026-11-01T12:01:00Z'), | ||
| (3, 'Review schema', true, '2026-11-01T12:02:00Z'), | ||
| (4, 'Plan next iteration', false, '2026-11-01T12:03:00Z'); |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import { defineExperiment } from '@supabase-evals/core'; | ||
| import { codexGpt6Luna } from '../presets.js'; | ||
| import { sparkRuntime } from './lib/runtime.js'; | ||
|
|
||
| export default defineExperiment({ | ||
| agent: codexGpt6Luna.agent, | ||
| runtime: sparkRuntime(process.env.SPARK_API_URL ?? ''), | ||
| skills: [...codexGpt6Luna.skills, 'spark'], | ||
| }); |
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.