Files
supabase/apps/studio/data/content/notebooks/notebook-operations.ts
Charis b9835e419c test(studio): mock notebook tools (#48952)
## Summary
- Adds deterministic `list_notebooks`/`get_notebook` fixtures (two
seeded notebooks, each with markdown/database/log cells) and stateful
in-memory `create_notebook`/`update_notebook` mocks to
`apps/studio/lib/ai/tools/mock-tools.ts`, so Braintrust evals can
exercise notebook tool calls without a real project.
- Both write-tool mocks force `needsApproval: false`, matching the
existing `execute_sql`/`deploy_edge_function` mock pattern — the eval
harness filters out tool-parts in `'approval-requested'` state and can
never answer an approval gate.
- All four notebook tools are wrapped from the real `getNotebookTools()`
definitions (only `execute`/`needsApproval` overridden), so evals
validate the model's arguments against the exact production schemas.
- Dedupes `describeOperationError` (previously duplicated between this
new mock and `notebook-tools.ts`) into a single exported
`describeNotebookOperationError` in `notebook-operations.ts`.

**Stacked on #48949** (`feature/notebooks-update-tool`) — this PR's base
branch is that PR, not `master`, because it reuses `update_notebook` and
the shared error helper that only exist there. Merge #48949 first, then
retarget/merge this one.

## Test plan
- [x] \`pnpm --filter studio typecheck\` passes
- [x] \`pnpm --filter studio test\` — all notebook-related suites pass
(\`mock-tools.test.ts\`, \`notebook-tools.test.ts\`,
\`data/content/notebooks/*\`)
- [x] \`eslint\` / \`prettier --check\` clean on all touched files
2026-08-12 10:34:37 -04:00

189 lines
5.6 KiB
TypeScript

import * as z from 'zod'
import {
agentCellSchema,
type AgentCell,
type CellWire,
type NotebookWire,
} from './notebook-schema'
export const CELL_ANCHOR_START = 'start'
const cellAnchorSchema = z
.string()
.describe(`The id of the cell to insert after, or "${CELL_ANCHOR_START}" for the beginning.`)
export const insertCellOperationSchema = z.object({
_tag: z.literal('insert_cell'),
after_cell_id: cellAnchorSchema,
cell: agentCellSchema,
})
export const replaceCellOperationSchema = z.object({
_tag: z.literal('replace_cell'),
cell_id: z.string().describe('The id of the existing cell to replace.'),
cell: agentCellSchema,
})
export const deleteCellOperationSchema = z.object({
_tag: z.literal('delete_cell'),
cell_id: z.string().describe('The id of the existing cell to delete.'),
})
export const moveCellOperationSchema = z.object({
_tag: z.literal('move_cell'),
cell_id: z.string().describe('The id of the existing cell to move.'),
after_cell_id: cellAnchorSchema,
})
export const notebookOperationSchema = z.discriminatedUnion('_tag', [
insertCellOperationSchema,
replaceCellOperationSchema,
deleteCellOperationSchema,
moveCellOperationSchema,
])
export const notebookOperationsSchema = z.array(notebookOperationSchema)
export type InsertCellOperation = z.infer<typeof insertCellOperationSchema>
export type ReplaceCellOperation = z.infer<typeof replaceCellOperationSchema>
export type DeleteCellOperation = z.infer<typeof deleteCellOperationSchema>
export type MoveCellOperation = z.infer<typeof moveCellOperationSchema>
export type NotebookOperation = z.infer<typeof notebookOperationSchema>
export type OperationResultCell = CellWire | AgentCell
export type NotebookOperationsResult = {
schema_version: NotebookWire['schema_version']
cells: OperationResultCell[]
}
export type NotebookOperationError =
| { _tag: 'unknown_cell_id'; cell_id: string }
| { _tag: 'conflicting_operations'; cell_id: string }
| { _tag: 'empty_result' }
export type ApplyNotebookOperationsResult =
| { success: true; notebook: NotebookOperationsResult }
| { success: false; error: NotebookOperationError }
export function describeNotebookOperationError(error: NotebookOperationError): string {
switch (error._tag) {
case 'unknown_cell_id':
return `No cell with id "${error.cell_id}" exists in this notebook.`
case 'conflicting_operations':
return `More than one operation targets cell "${error.cell_id}".`
case 'empty_result':
return 'This update would leave the notebook with no cells.'
}
}
function targetCellId(operation: NotebookOperation): string | undefined {
switch (operation._tag) {
case 'insert_cell':
return undefined
case 'replace_cell':
case 'delete_cell':
case 'move_cell':
return operation.cell_id
}
}
export function applyNotebookOperations(
notebook: NotebookWire,
operations: NotebookOperation[]
): ApplyNotebookOperationsResult {
const targetedIds = new Set<string>()
for (const operation of operations) {
const cellId = targetCellId(operation)
if (cellId === undefined) continue
if (targetedIds.has(cellId)) {
return { success: false, error: { _tag: 'conflicting_operations', cell_id: cellId } }
}
targetedIds.add(cellId)
}
const cells: OperationResultCell[] = [...notebook.cells]
const indexOfCellId = (cellId: string) =>
cells.findIndex((cell) => 'id' in cell && cell.id === cellId)
const insertedAfter = new Map<string, number>()
const insertAfter = (
anchor: string,
item: OperationResultCell
): NotebookOperationError | undefined => {
const anchorIndex = anchor === CELL_ANCHOR_START ? -1 : indexOfCellId(anchor)
if (anchor !== CELL_ANCHOR_START && anchorIndex === -1) {
return { _tag: 'unknown_cell_id', cell_id: anchor }
}
const offset = insertedAfter.get(anchor) ?? 0
cells.splice(anchorIndex + 1 + offset, 0, item)
insertedAfter.set(anchor, offset + 1)
return undefined
}
for (const operation of operations) {
switch (operation._tag) {
case 'insert_cell': {
const error = insertAfter(operation.after_cell_id, operation.cell)
if (error) return { success: false, error }
break
}
case 'replace_cell': {
const index = indexOfCellId(operation.cell_id)
if (index === -1) {
return {
success: false,
error: { _tag: 'unknown_cell_id', cell_id: operation.cell_id },
}
}
cells[index] = operation.cell
break
}
case 'delete_cell': {
const index = indexOfCellId(operation.cell_id)
if (index === -1) {
return {
success: false,
error: { _tag: 'unknown_cell_id', cell_id: operation.cell_id },
}
}
cells.splice(index, 1)
break
}
case 'move_cell': {
if (operation.after_cell_id === operation.cell_id) {
return {
success: false,
error: { _tag: 'conflicting_operations', cell_id: operation.cell_id },
}
}
const fromIndex = indexOfCellId(operation.cell_id)
if (fromIndex === -1) {
return {
success: false,
error: { _tag: 'unknown_cell_id', cell_id: operation.cell_id },
}
}
const [movedCell] = cells.splice(fromIndex, 1)
const error = insertAfter(operation.after_cell_id, movedCell)
if (error) return { success: false, error }
break
}
}
}
if (cells.length === 0) {
return { success: false, error: { _tag: 'empty_result' } }
}
return {
success: true,
notebook: { schema_version: notebook.schema_version, cells },
}
}