mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-21 00:03:31 +00:00
* fix(pipelines): live materialize/dataset edits reflect on the graph; no phantom draft after deploy Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(pipelines): Save all deploys the open pane's live buffer, not the stale draft snapshot Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(pipelines): pin deployedFromPane to the shipped content so mid-deploy keystrokes still promote to a draft Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(pipelines): draft rename ping-pong loop, stale rename deploys, inactive-draft input lineage Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(pipelines): dedupe inferred-lineage overlay against accumulated edges; first draft teardown still captures reads Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(pipelines): record an authoritative empty read capture on uncaptured draft entries Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(pipelines): teardown skip compares lineage too, so access-only overrides still persist Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * style(pipelines): compress persist-back guard comments to the invariant Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
107 lines
4.4 KiB
TypeScript
107 lines
4.4 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
import { PipelineEditorState } from './pipelineEditorState.svelte'
|
|
import type { PipelineDraft } from './pipelineAiHelpers'
|
|
import type { AssetKind } from '$lib/gen'
|
|
|
|
// handleDraftPersist defers its commit a microtask (so a same-batch discard can
|
|
// win); flush that microtask before asserting.
|
|
const flushMicrotasks = () => new Promise<void>((resolve) => queueMicrotask(() => resolve()))
|
|
|
|
function draft(content: string, outputAssets?: { kind: AssetKind; path: string }[]): PipelineDraft {
|
|
return {
|
|
localId: 'pe-1',
|
|
script: { path: 'f/x/n', language: 'duckdb', content } as PipelineDraft['script'],
|
|
outputAssets
|
|
}
|
|
}
|
|
|
|
describe('PipelineEditorState.handleDraftPersist', () => {
|
|
// Regression: a no-output draft has `outputAssets: undefined`; the details pane
|
|
// infers an empty `writes: []`. Both mean "no writes", so persisting unchanged
|
|
// content+writes must be a no-op. The earlier `undefined === 0` length check made
|
|
// it false, so every persist re-wrote the drafts Map with an equivalent object —
|
|
// re-triggering the pane's emit → graph re-derive → persist, an infinite
|
|
// microtask loop that froze the tab. The Map reference must stay identical.
|
|
it('is idempotent for a no-output draft (undefined outputAssets vs empty inferred writes)', async () => {
|
|
const pe = new PipelineEditorState()
|
|
pe.drafts = new Map([['f/x/n', draft('SELECT 1', undefined)]])
|
|
const before = pe.drafts
|
|
pe.handleDraftPersist('f/x/n', { content: 'SELECT 1', writes: [] })
|
|
await flushMicrotasks()
|
|
expect(pe.drafts).toBe(before)
|
|
})
|
|
|
|
it('re-writes the drafts Map when the content actually changes', async () => {
|
|
const pe = new PipelineEditorState()
|
|
pe.drafts = new Map([['f/x/n', draft('SELECT 1', undefined)]])
|
|
const before = pe.drafts
|
|
pe.handleDraftPersist('f/x/n', { content: 'SELECT 2', writes: [] })
|
|
await flushMicrotasks()
|
|
expect(pe.drafts).not.toBe(before)
|
|
expect(pe.drafts.get('f/x/n')?.script.content).toBe('SELECT 2')
|
|
})
|
|
|
|
it('re-writes the drafts Map when the inferred writes actually change', async () => {
|
|
const pe = new PipelineEditorState()
|
|
pe.drafts = new Map([['f/x/n', draft('SELECT 1', undefined)]])
|
|
const before = pe.drafts
|
|
pe.handleDraftPersist('f/x/n', {
|
|
content: 'SELECT 1',
|
|
writes: [{ kind: 'resource' as AssetKind, path: 'f/x/out' }]
|
|
})
|
|
await flushMicrotasks()
|
|
expect(pe.drafts).not.toBe(before)
|
|
expect(pe.drafts.get('f/x/n')?.outputAssets).toEqual([{ kind: 'resource', path: 'f/x/out' }])
|
|
})
|
|
|
|
it('stays idempotent when outputAssets and inferred writes match (non-empty)', async () => {
|
|
const pe = new PipelineEditorState()
|
|
pe.drafts = new Map([
|
|
['f/x/n', draft('SELECT 1', [{ kind: 'resource' as AssetKind, path: 'f/x/out' }])]
|
|
])
|
|
const before = pe.drafts
|
|
pe.handleDraftPersist('f/x/n', {
|
|
content: 'SELECT 1',
|
|
writes: [{ kind: 'resource' as AssetKind, path: 'f/x/out' }]
|
|
})
|
|
await flushMicrotasks()
|
|
expect(pe.drafts).toBe(before)
|
|
})
|
|
})
|
|
|
|
describe('PipelineEditorState.handleDraftPersist — read capture', () => {
|
|
it('records an authoritative empty capture on an uncaptured entry (undefined → [])', async () => {
|
|
const pe = new PipelineEditorState()
|
|
pe.drafts = new Map([['f/x/n', draft('SELECT 1', undefined)]])
|
|
const before = pe.drafts
|
|
pe.handleDraftPersist('f/x/n', { content: 'SELECT 1', writes: [], reads: [] })
|
|
await flushMicrotasks()
|
|
// Must rewrite: undefined means "fall back to the session cache", [] means
|
|
// "reads nothing" — staying undefined would keep stale cached read edges
|
|
// alive after the pane closes.
|
|
expect(pe.drafts).not.toBe(before)
|
|
expect(pe.drafts.get('f/x/n')?.inputAssets).toEqual([])
|
|
})
|
|
|
|
it('is idempotent once the capture matches ([] vs reads: [])', async () => {
|
|
const pe = new PipelineEditorState()
|
|
const d = draft('SELECT 1', undefined)
|
|
d.inputAssets = []
|
|
pe.drafts = new Map([['f/x/n', d]])
|
|
const before = pe.drafts
|
|
pe.handleDraftPersist('f/x/n', { content: 'SELECT 1', writes: [], reads: [] })
|
|
await flushMicrotasks()
|
|
expect(pe.drafts).toBe(before)
|
|
})
|
|
|
|
it('is idempotent when a legacy caller omits reads', async () => {
|
|
const pe = new PipelineEditorState()
|
|
pe.drafts = new Map([['f/x/n', draft('SELECT 1', undefined)]])
|
|
const before = pe.drafts
|
|
pe.handleDraftPersist('f/x/n', { content: 'SELECT 1', writes: [] })
|
|
await flushMicrotasks()
|
|
expect(pe.drafts).toBe(before)
|
|
expect(pe.drafts.get('f/x/n')?.inputAssets).toBeUndefined()
|
|
})
|
|
})
|