Files
stroblmeandClaude Opus 5 062a2aac60 Let the search reach past the twentieth of a kind, and unbreak two specs
The global search capped each category before cmdk had matched anything, so
nothing past the twentieth node or widget could be found at all — this
instance has 28 nodes and 97 widgets. The cap now trims the candidates the
query could reach rather than the raw index.

The admin teardown asked /users/ for limit=1000, which the route stopped
accepting when its bounds went in; a 422 body has no `data` to iterate, so the
hook threw and took the tests it was attributed to with it. It asks for the
500 the route allows.

And `submit` folds every declared initial into a run's params, so an input the
run never passed can no longer read as the flow's own. That assertion is gone;
what the panel does show — the value the run actually started from, passed or
not — is what the test checks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CL9zvnnvcp1mvA8o7impxk
2026-09-06 14:46:59 +02:00

177 lines
5.8 KiB
TypeScript

import { expect, test } from "@playwright/test"
import { api, apiPage, deleteAll } from "./utils/api"
/**
* What the runs screen can do to a run, rather than what it can show about one.
*
* The reading half is covered by the run itself being there; these are the two
* things that change something — a delete, and a download — plus the two links
* a run makes to what produced it.
*/
const flowName = `test_runs_${Date.now().toString(36)}`
test.use({ storageState: "playwright/.auth/user.json" })
test.describe.configure({ mode: "serial" })
// Yields a curve so the run has something to draw, which is what the zoom
// below needs; the streaming port is what makes those yields a series.
const TRAIN = `def process(epochs):
for step in range(epochs):
yield {"loss": 1.0 / (step + 1)}
return {"score": epochs * 0.5}
`
test.afterAll(async ({ browser }) => {
await deleteAll(browser, [`/flows/${flowName}`])
})
test.beforeAll(async ({ browser }) => {
const page = await apiPage(browser)
await api(page, `/flows/${flowName}`, {
method: "PUT",
data: {
name: flowName,
title: "Runs under test",
// `rate` is declared and never passed, which is what puts its declared
// value in the Inputs panel below.
inputs: [
{ spec: { name: "epochs", dtype: "int" }, initial: 2 },
{ spec: { name: "rate", dtype: "float" }, initial: 0.5 },
],
nodes: [
{
id: "train",
type: "python",
requires: [{ name: "epochs", dtype: "int" }],
provides: [
{ name: "loss", dtype: "float", stream: true },
{ name: "score", dtype: "float" },
],
},
],
},
})
await api(page, `/flows/${flowName}/nodes/train/source`, {
method: "PUT",
data: { code: TRAIN },
})
const detail = await (await api(page, `/flows/${flowName}`)).json()
await api(page, `/flows/${flowName}/publish`, {
method: "POST",
data: { version: detail.definition.version },
})
for (const epochs of [4, 6]) {
const answer = await api(page, `/runs/flows/${flowName}`, {
method: "POST",
data: { params: { epochs } },
})
expect(answer.ok()).toBeTruthy()
}
await page.close()
})
/** The screen filtered to this flow, once both runs have stopped moving. */
async function openRuns(page: import("@playwright/test").Page) {
await page.goto(`/runs?flow=${flowName}`)
await expect(page.getByTestId("run-row")).toHaveCount(2)
await expect(page.getByText(/queued|running/)).toHaveCount(0, {
timeout: 30_000,
})
}
test("a run names the flow it came from, and links to it", async ({ page }) => {
await page.goto("/runs")
const link = page
.getByTestId("run-row")
.filter({ hasText: flowName })
.first()
.getByRole("link", { name: flowName })
await link.click()
await page.waitForURL(`**/flows/${flowName}`)
})
test("an input the run never passed still reads its value", async ({
page,
}) => {
await openRuns(page)
await page.getByTestId("run-link").first().click()
const inputs = page.locator("section", { hasText: "Inputs" }).last()
// `submit` folds every declared initial into the run's params, so the row is
// self-describing and the panel reads the same either way: what was passed
// and what was left alone both show the value the run actually started from.
await expect(inputs).toContainText("epochs")
await expect(inputs).toContainText("rate")
await expect(inputs).toContainText("0.5")
})
test("a chart can be dragged into and double-clicked back out of", async ({
page,
}) => {
await openRuns(page)
await page.getByTestId("run-link").first().click()
const plot = page.locator(".u-over").first()
await expect(plot).toBeVisible()
const box = await plot.boundingBox()
if (!box) throw new Error("the chart has no box to drag across")
const y = box.y + box.height / 2
await page.mouse.move(box.x + box.width * 0.3, y)
await page.mouse.down()
await page.mouse.move(box.x + box.width * 0.7, y, { steps: 8 })
await page.mouse.up()
const reset = page.getByTestId("chart-reset-zoom")
await expect(reset).toBeVisible()
await plot.dblclick()
await expect(reset).toBeHidden()
})
test("the export button downloads the selection", async ({ page }) => {
await openRuns(page)
await page.getByTestId("export-runs").click()
const download = page.waitForEvent("download")
await page.getByRole("menuitem", { name: /runs table/i }).click()
expect((await download).suggestedFilename()).toBe("runs.csv")
})
test("picked runs can be deleted", async ({ page }) => {
await openRuns(page)
for (const box of await page.getByTestId("run-select").all())
await box.click()
await page.getByTestId("delete-selected").click()
await page.getByTestId("confirm-delete").click()
await expect(page.getByTestId("run-row")).toHaveCount(0, { timeout: 30_000 })
})
test("a run is started from the form", async ({ page }) => {
await page.goto(`/runs/new?flow=${flowName}`)
await page.getByLabel("epochs", { exact: true }).fill("3")
await page.getByTestId("submit-new-run").click()
// The result card is the run just started, and it fills in as it goes.
const result = page.getByTestId("run-result")
await expect(result).toBeVisible()
await expect(result.getByText("score")).toBeVisible({ timeout: 30_000 })
await expect(page.getByTestId("recent-run").first()).toBeVisible()
})
test("a list of values is a sweep", async ({ page }) => {
await page.goto(`/runs/new?flow=${flowName}`)
await page.getByLabel("epochs", { exact: true }).fill("2,3")
const run = page.getByTestId("submit-new-run")
await expect(run).toHaveText("Run 2")
await run.click()
// Submitting a sweep lands on the history, filtered to that group.
await page.waitForURL(/\/runs\?.*group=/)
await expect(page.getByTestId("run-row")).toHaveCount(2)
})