mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-18 16:03:24 +00:00
fix(sessions): spend each write marker only on the tool that wrote it
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4
This commit is contained in:
co-authored by
Claude Opus 5
parent
c9c3b23585
commit
947fd6b1cb
@@ -2160,6 +2160,27 @@ describe('global AI tools', () => {
|
||||
expect(UserDraft.takeDraftOnlyDiscard('resource', path, { workspace: WORKSPACE })).toBe(true)
|
||||
})
|
||||
|
||||
// Nothing consumes a marker outside a session, so one can outlive the item it
|
||||
// was about. Recreating the item must void it, or the next action on the
|
||||
// recreated item reads it and sends a perfectly valid editor back to its list.
|
||||
it('voids a draft-only removal marker when the item is written again', async () => {
|
||||
const path = 'f/resources/discard-then-recreate'
|
||||
await callGlobalTool('write_resource', {
|
||||
path,
|
||||
resource_type: 'postgresql',
|
||||
value: { host: 'localhost' }
|
||||
})
|
||||
await callGlobalTool('discard_local_draft', { type: 'resource', path })
|
||||
|
||||
// The marker is standing (no listener consumed it), and now the item is back.
|
||||
await callGlobalTool('write_resource', {
|
||||
path,
|
||||
resource_type: 'postgresql',
|
||||
value: { host: 'elsewhere' }
|
||||
})
|
||||
expect(UserDraft.takeDraftOnlyDiscard('resource', path, { workspace: WORKSPACE })).toBe(false)
|
||||
})
|
||||
|
||||
// "Create a resource, then never mind": delete_workspace_item must reject a path
|
||||
// that was never deployed, before the confirmation card — otherwise the user
|
||||
// confirms a workspace mutation that 404s past the draft cleanup, leaving the
|
||||
|
||||
@@ -421,6 +421,9 @@ export async function persistGlobalDraft(
|
||||
const itemKind = itemKindFor(type, opts.triggerKind)
|
||||
if (!itemKind) throw new Error(`Unsupported draft type "${type}".`)
|
||||
const storagePath = resolveDraftStoragePath(workspace, itemKind, path)
|
||||
// Writing the item back means it exists again, so any removal marker left from
|
||||
// an earlier draft-only discard is void.
|
||||
UserDraft.clearDraftOnlyDiscard(itemKind, storagePath, { workspace })
|
||||
UserDraft.seed(itemKind, storagePath, value, { workspace })
|
||||
await UserDraftDbSyncer.save({
|
||||
workspace,
|
||||
|
||||
@@ -442,6 +442,13 @@ export const UserDraft = {
|
||||
noteMarker(draftOnlyDiscards, mapKey(resolveWorkspace(opts), itemKind, path))
|
||||
},
|
||||
|
||||
/** Drop any removal marker for this cell — the item exists again, so a marker
|
||||
* still standing (nothing consumed it, because nothing was listening) would be
|
||||
* read by whatever touches it next and send its editor away. */
|
||||
clearDraftOnlyDiscard(itemKind: UserDraftItemKind, path: string, opts?: UserDraftOptions): void {
|
||||
draftOnlyDiscards.delete(mapKey(resolveWorkspace(opts), itemKind, path))
|
||||
},
|
||||
|
||||
/** Whether the last discard for this cell removed the item outright (see
|
||||
* {@link recordDraftOnlyDiscard}), clearing the record. */
|
||||
takeDraftOnlyDiscard(
|
||||
|
||||
@@ -625,12 +625,20 @@
|
||||
// editor still loading holds none yet and the seed no-opped past it. The
|
||||
// miss is recorded when it happens — asking now would be too late, since
|
||||
// the editor can have acquired the cell during the write's round-trip.
|
||||
// Each marker is read only by the kind of tool that writes it. A marker
|
||||
// left unread — nothing consumes them outside a session — would otherwise
|
||||
// be spent by whatever touched the item next, and a removal marker read by
|
||||
// a deploy sends a live editor away.
|
||||
const kind = path ? entityKindForPage(pages[0]) : undefined
|
||||
const seedReachedEditor = !kind || !path || !UserDraft.takeSeedMiss(kind, path, { workspace })
|
||||
const readable = !!kind && !!path
|
||||
const seedReachedEditor =
|
||||
!readable || entity !== 'none' || !UserDraft.takeSeedMiss(kind!, path!, { workspace })
|
||||
// A discard of a draft-only item removed the item itself, so its editor has
|
||||
// nothing left to re-read and must leave instead.
|
||||
const itemSurvives =
|
||||
!kind || !path || !UserDraft.takeDraftOnlyDiscard(kind, path, { workspace })
|
||||
!readable ||
|
||||
name !== 'discard_local_draft' ||
|
||||
!UserDraft.takeDraftOnlyDiscard(kind!, path!, { workspace })
|
||||
const effect = effectForDiscard(effectForWrite(entity, seedReachedEditor), itemSurvives)
|
||||
if (effect !== 'none') pendingMutations.push({ pages, effect, path, workspace })
|
||||
clearTimeout(reloadHandle)
|
||||
|
||||
Reference in New Issue
Block a user