mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-08 16:03:27 +00:00
* feat(frontend): run a linked agent's draft when testing a flow, and offer to deploy it Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017B4omp8dRgmLitbpQEqFMp * fix(frontend): settle an agent's autosave before reading it, and refresh its card on a draft save Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017B4omp8dRgmLitbpQEqFMp * fix(frontend): deploy the agent draft that was validated, and make the draft-tools flag explicit Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017B4omp8dRgmLitbpQEqFMp * fix(frontend): refuse a stale agent deploy, and warn when a never-deployed agent is kept as a draft Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017B4omp8dRgmLitbpQEqFMp * docs: record what inlining an agent draft puts in a preview job Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017B4omp8dRgmLitbpQEqFMp * fix(frontend): name the draft-changes dialog after what it lists Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017B4omp8dRgmLitbpQEqFMp * fix(frontend): refuse a draft deploy when the draft row is gone Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017B4omp8dRgmLitbpQEqFMp * fix(frontend): apply the missing-draft refusal to raw apps too Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017B4omp8dRgmLitbpQEqFMp * fix(frontend): stop reading a deployed resource row as a draft on deploy Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017B4omp8dRgmLitbpQEqFMp * fix(frontend): do not mistake an outage or a vanished draft for a deploy Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017B4omp8dRgmLitbpQEqFMp * fix(frontend): refuse an agent read whose pending draft save failed Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017B4omp8dRgmLitbpQEqFMp * fix(frontend): space the trigger badges and right-align the agent actions Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017B4omp8dRgmLitbpQEqFMp * fix(frontend): surface a failed agent-draft read instead of dropping it Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017B4omp8dRgmLitbpQEqFMp * fix(frontend): give the agent draft delete a baseline so a newer edit survives Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017B4omp8dRgmLitbpQEqFMp * fix(frontend): drop the agent draft cell locally instead of deleting twice Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017B4omp8dRgmLitbpQEqFMp * test(frontend): pass the withDraft flag the guard tests were missing Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017B4omp8dRgmLitbpQEqFMp * refactor(frontend): deploy agent drafts the way Review & Deploy does Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017B4omp8dRgmLitbpQEqFMp * fix(frontend): give the read-only flow graph its own linked-tools bucket Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017B4omp8dRgmLitbpQEqFMp * fix(frontend): base the resource draft delete on the read that promoted it Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017B4omp8dRgmLitbpQEqFMp * fix(frontend): refresh every step linking an agent when its draft is saved Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017B4omp8dRgmLitbpQEqFMp * fix(frontend): write nothing at all when a resource draft has gone Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017B4omp8dRgmLitbpQEqFMp * test(frontend): pin that the resource draft delete follows its baseline seed Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017B4omp8dRgmLitbpQEqFMp --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
87 lines
3.3 KiB
TypeScript
87 lines
3.3 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest'
|
|
|
|
// flowState pulls in the flow-module loaders; only the fetch-generation guard is under test here.
|
|
vi.mock('$lib/gen', () => ({ ResourceService: { getResource: vi.fn() } }))
|
|
vi.mock('./flowStateUtils.svelte', () => ({ loadFlowModuleState: vi.fn() }))
|
|
vi.mock('./utils.svelte', () => ({ emptyFlowModuleState: () => ({}) }))
|
|
vi.mock('./agentToolUtils', () => ({
|
|
isFlowModuleTool: () => false,
|
|
agentToolToFlowModule: (t: unknown) => t
|
|
}))
|
|
vi.mock('$lib/stores', () => ({
|
|
workspaceStore: { subscribe: (f: (v: string) => void) => (f('ws'), () => {}) }
|
|
}))
|
|
|
|
import {
|
|
claimLinkedToolsFetch,
|
|
invalidateLinkedToolsFetches,
|
|
publishLinkedAgentTools
|
|
} from './flowState'
|
|
import {
|
|
getLinkedAgentTools,
|
|
linkedToolsScope,
|
|
setLinkedAgentTools
|
|
} from './linkedAgentToolsStore.svelte'
|
|
import { ResourceService } from '$lib/gen'
|
|
import type { AgentTool } from './agentToolUtils'
|
|
|
|
const tool = (id: string) => ({ id, value: { tool_type: 'flowmodule' } }) as AgentTool
|
|
|
|
let seq = 0
|
|
const scopeFor = (name: string) => linkedToolsScope(`ws${seq++}`, name)
|
|
|
|
describe('linked tools fetch guard', () => {
|
|
it('lets only the newest fetch for a (scope, module) publish', async () => {
|
|
const scope = scopeFor('flow')
|
|
let release: ((v: unknown) => void) | undefined
|
|
vi.mocked(ResourceService.getResource)
|
|
.mockImplementationOnce(
|
|
() => new Promise((r) => (release = r)) as ReturnType<typeof ResourceService.getResource>
|
|
)
|
|
.mockResolvedValueOnce({ value: { tools: [tool('new')] } } as never)
|
|
|
|
const stale = publishLinkedAgentTools('f/a/old', 'ws', scope, 'step', false)
|
|
await publishLinkedAgentTools('f/a/new', 'ws', scope, 'step', false)
|
|
release?.({ value: { tools: [tool('old')] } })
|
|
await stale
|
|
|
|
expect(getLinkedAgentTools(scope, 'step').map((t) => t.id)).toEqual(['new'])
|
|
})
|
|
|
|
// A rename migrates the bucket to a new scope. A fetch still running against the old scope holds
|
|
// a valid generation for that key, so without invalidation it publishes there and the doc-scope
|
|
// sweep carries it forward over the link resolved since.
|
|
it('invalidates every in-flight fetch for a scope', async () => {
|
|
const scope = scopeFor('before-rename')
|
|
let release: ((v: unknown) => void) | undefined
|
|
vi.mocked(ResourceService.getResource).mockImplementationOnce(
|
|
() => new Promise((r) => (release = r)) as ReturnType<typeof ResourceService.getResource>
|
|
)
|
|
|
|
const inFlight = publishLinkedAgentTools('f/a/old', 'ws', scope, 'step', false)
|
|
setLinkedAgentTools(scope, 'step', [tool('kept')], 'u/admin/a')
|
|
|
|
invalidateLinkedToolsFetches(scope)
|
|
release?.({ value: { tools: [tool('stale')] } })
|
|
await inFlight
|
|
|
|
expect(getLinkedAgentTools(scope, 'step').map((t) => t.id)).toEqual(['kept'])
|
|
})
|
|
|
|
it('claiming supersedes an in-flight fetch for that module', async () => {
|
|
const scope = scopeFor('claimed')
|
|
let release: ((v: unknown) => void) | undefined
|
|
vi.mocked(ResourceService.getResource).mockImplementationOnce(
|
|
() => new Promise((r) => (release = r)) as ReturnType<typeof ResourceService.getResource>
|
|
)
|
|
|
|
const inFlight = publishLinkedAgentTools('f/a/old', 'ws', scope, 'step', false)
|
|
claimLinkedToolsFetch(scope, 'step')
|
|
setLinkedAgentTools(scope, 'step', [tool('direct')], 'u/admin/a')
|
|
release?.({ value: { tools: [tool('stale')] } })
|
|
await inFlight
|
|
|
|
expect(getLinkedAgentTools(scope, 'step').map((t) => t.id)).toEqual(['direct'])
|
|
})
|
|
})
|