mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-06 16:02:23 +00:00
* test(ai_evals): pipeline coverage for AI sessions + editor e2e Add a complex incremental DuckLake pipeline case, harden the two-node case, and encode the declarative pipeline contract (`-- on` triggers, `-- materialize` + bare SELECT) in the pipeline judgeChecklists so the LLM judge stops false-negativing correct nodes. Add a deterministic Playwright e2e that seeds annotated pipeline scripts and asserts the /pipeline/<folder> editor derives the lineage DAG. Fixes WIN-2229 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(ai): steer pipeline chat to DuckDB+materialize and warn on missing storage The pipeline authoring prompt (getPipelinePrompt, used by the global/session chat and the /pipeline editor) was neutral on language choice and said nothing about storage readiness. Default it to duckdb materializing into DuckLake unless the work specifically needs postgres/data-tables or bun/python, and add a storage prerequisites section: a DuckLake pipeline needs workspace object storage + a DuckLake catalog, so warn when none is configured and give role-appropriate next steps (admin: workspace settings; others: ask an admin). Drafting is not blocked. A/B on sonnet (global pipeline cases): no regression, +639 finalContext tokens. Fixes WIN-2229 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(ai_evals): address review - e2e teardown, async-edge note, merge-mode hedge Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(ai): address review nits - drop phantom list_ducklakes tool ref, trim narration comments Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(ai): add list_ducklakes chat tool for pipeline storage readiness The pipeline counterpart to list_datatables: lists the workspace's configured DuckLake catalogs so the chat can detect the storage prerequisite before building a DuckLake pipeline and warn with role-appropriate next steps when none exists (drafting stays unblocked). Wired into the global tool set and referenced from the pipeline authoring prompt. In the eval run all three pipeline cases called it unprompted with no build regression. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(ai): fix DuckDB annotation syntax in duckdb-default section (-- not //) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
104 lines
4.2 KiB
TypeScript
104 lines
4.2 KiB
TypeScript
import { test, expect, Page } from '@playwright/test'
|
|
|
|
// The pipeline surface an AI session builds into: session pipeline tools emit
|
|
// annotated scripts (`-- pipeline`, `-- on <asset>`, `-- materialize <asset>`),
|
|
// and the /pipeline/<folder> editor derives the lineage DAG from those
|
|
// annotations alone. This test seeds the scripts an AI-built DuckLake pipeline
|
|
// would produce and asserts the editor renders every derived node, asset, and
|
|
// edge (including the missing-schedule-trigger edge): a deterministic check of
|
|
// the graph the AI session relies on, without a live model in the loop.
|
|
|
|
const WORKSPACE = 'admins'
|
|
|
|
declare const process: any
|
|
|
|
function uniqueSuffix(project: string): string {
|
|
// Per-project suffix so the three browser projects don't collide on the
|
|
// shared dev instance when Playwright runs them in parallel.
|
|
return `${process.env.TEST_UNIQUE_ID ?? 'local'}_${project}`
|
|
}
|
|
|
|
async function seedScript(page: Page, path: string, content: string, summary: string) {
|
|
const res = await page.request.post(`/api/w/${WORKSPACE}/scripts/create`, {
|
|
data: { path, summary, description: '', content, language: 'duckdb', schema: {} }
|
|
})
|
|
expect(res.ok(), `seed ${path}: ${res.status()} ${await res.text()}`).toBeTruthy()
|
|
}
|
|
|
|
test.describe('Pipeline editor', () => {
|
|
// Track what the test seeds so afterAll can remove it (keeps the shared dev/CI
|
|
// instance from accumulating a folder + scripts per run).
|
|
let seeded: { folder: string; scripts: string[] } | undefined
|
|
|
|
test.afterAll(async ({ request }) => {
|
|
if (!seeded) return
|
|
for (const path of seeded.scripts) {
|
|
await request.post(`/api/w/${WORKSPACE}/scripts/delete/p/${path}`).catch(() => {})
|
|
}
|
|
await request.delete(`/api/w/${WORKSPACE}/folders/delete/${seeded.folder}`).catch(() => {})
|
|
})
|
|
|
|
test('derives the DAG from annotated pipeline scripts', async ({ page }, testInfo) => {
|
|
const suffix = uniqueSuffix(testInfo.project.name)
|
|
const folder = `pipeline_e2e_${suffix}`
|
|
const ingest = `f/${folder}/orders_ingest`
|
|
const daily = `f/${folder}/orders_daily`
|
|
const ordersTbl = `main/orders_${suffix}`
|
|
const dailyTbl = `main/orders_daily_${suffix}`
|
|
seeded = { folder, scripts: [ingest, daily] }
|
|
|
|
// Folder may already exist from a prior run; only fail on the seeds.
|
|
await page.request.post(`/api/w/${WORKSPACE}/folders/create`, { data: { name: folder } })
|
|
|
|
await seedScript(
|
|
page,
|
|
ingest,
|
|
[
|
|
'-- pipeline',
|
|
'-- on schedule',
|
|
`-- materialize ducklake://${ordersTbl}`,
|
|
"SELECT * FROM read_csv('s3://raw/orders/*.csv')"
|
|
].join('\n'),
|
|
'Ingest orders'
|
|
)
|
|
await seedScript(
|
|
page,
|
|
daily,
|
|
[
|
|
'-- pipeline',
|
|
`-- on ducklake://${ordersTbl}`,
|
|
`-- materialize ducklake://${dailyTbl}`,
|
|
`SELECT date_trunc('day', ts) AS day, count(*) AS n FROM ducklake.${ordersTbl.replace('/', '.')} GROUP BY 1`
|
|
].join('\n'),
|
|
'Daily rollup'
|
|
)
|
|
|
|
await page.goto(`/pipeline/${folder}`)
|
|
|
|
await expect(page.getByRole('heading', { name: 'Pipeline', level: 1 })).toBeVisible()
|
|
await expect(page.getByText('2 scripts', { exact: false })).toBeVisible()
|
|
|
|
// A long path truncates in the node label, so match the leaf name; the full
|
|
// paths are asserted on the edge labels below.
|
|
await expect(page.getByText('orders_ingest').first()).toBeVisible()
|
|
await expect(page.getByText('orders_daily').first()).toBeVisible()
|
|
|
|
// Two edges resolve asynchronously after the initial graph fetch (the s3 read
|
|
// is detected from the SQL body at deploy time; the missing-schedule edge is
|
|
// synthesized client-side by the page's per-script annotation sweep), so a
|
|
// cold-CI failure here points at that async timing, not a missing edge.
|
|
const edges = [
|
|
`Edge from asset:s3object:raw/orders/*.csv to script:${ingest}`,
|
|
`Edge from script:${ingest} to asset:ducklake:${ordersTbl}`,
|
|
`Edge from asset:ducklake:${ordersTbl} to script:${daily}`,
|
|
`Edge from script:${daily} to asset:ducklake:${dailyTbl}`,
|
|
`Edge from trigger:schedule:missing:${ingest} to script:${ingest}`
|
|
]
|
|
for (const name of edges) {
|
|
// Edges are SVG <g> groups (no visible box of their own), so assert they
|
|
// are rendered into the graph rather than in-viewport visible.
|
|
await expect(page.getByRole('group', { name, exact: true }).first()).toBeAttached()
|
|
}
|
|
})
|
|
})
|