fix: keep a test run readable when the drafts cannot be listed

Round 9 nits:

- `loadAppDraftValue` is shared by the app write tools and test_run_app_runnable,
  so demanding resolution there made a listing outage abort a test run that
  used to work. Only the writes ask for it.
- The rebase tools resolved their draft's key and then let deleteGlobalDraft
  resolve it again; they pass the key they already have.
- `loadAppDraftValue` asked the backend for a draft its own resolution had
  just fetched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Guilhem Lemouel
2026-09-23 15:24:23 +02:00
co-authored by Claude Opus 5
parent fed768279b
commit bce2315016
2 changed files with 23 additions and 15 deletions
@@ -1867,13 +1867,18 @@ async function loadAppValueForRead(path: string, workspace: string): Promise<App
return appSourceToDraftValue(app, app)
}
async function loadAppDraftValue(path: string, workspace: string): Promise<LoadedAppDraftValue> {
async function loadAppDraftValue(
path: string,
workspace: string,
/** A write must know which draft a name belongs to; a test run reads whatever the path
* reaches, as it did before names were resolved at all. */
opts: { forWrite?: boolean } = {}
): Promise<LoadedAppDraftValue> {
// One resolution for the read and the write that follows it: resolving again at save
// time could answer differently and write the app's files onto another draft.
const { storagePath } = await resolveGlobalDraft(workspace, 'app', path, undefined, {
forWrite: true
})
const draft = await getGlobalDraftAt(workspace, 'app', storagePath)
const resolved = await resolveGlobalDraft(workspace, 'app', path, undefined, opts)
const storagePath = resolved.storagePath
const draft = await getGlobalDraftAt(workspace, 'app', storagePath, undefined, resolved)
if (draft && draft.value && typeof draft.value === 'object' && 'files' in draft.value) {
return { value: draft.value as AppDraftValue, storagePath }
}
@@ -6518,7 +6523,7 @@ async function writeAppFile(
content: `Writing ${target.filePath} to app "${args.path}"...`
})
const { value, storagePath } = await loadAppDraftValue(args.path, workspace)
const { value, storagePath } = await loadAppDraftValue(args.path, workspace, { forWrite: true })
value.files = { ...value.files, [target.filePath]: args.content }
const result = await saveAppDraft(workspace, args.path, value, storagePath)
return finishAppDraftWrite(result, ctx, () => ({
@@ -6544,7 +6549,7 @@ async function deleteAppFile(
content: `Deleting ${target.filePath} from app "${args.path}"...`
})
const { value, storagePath } = await loadAppDraftValue(args.path, workspace)
const { value, storagePath } = await loadAppDraftValue(args.path, workspace, { forWrite: true })
if (!(target.filePath in value.files)) {
throw new Error(`Frontend file "${target.filePath}" not found in app "${args.path}".`)
}
@@ -6584,7 +6589,7 @@ async function patchAppFile(
content: `Patching ${target.filePath} in app "${path}"...`
})
const { value, storagePath } = await loadAppDraftValue(path, workspace)
const { value, storagePath } = await loadAppDraftValue(path, workspace, { forWrite: true })
let currentContent: string
let runnable: PersistedRunnable | undefined
@@ -6652,7 +6657,7 @@ async function writeAppRunnable(
content: `Writing runnable "${key}" to app "${path}"...`
})
const { value, storagePath } = await loadAppDraftValue(path, workspace)
const { value, storagePath } = await loadAppDraftValue(path, workspace, { forWrite: true })
const existing = value.runnables[key] as PersistedRunnable | undefined
const persisted = buildPersistedRunnable(input, existing)
value.runnables = { ...value.runnables, [key]: persisted }
@@ -6763,7 +6768,7 @@ async function deleteAppRunnable(
content: `Removing runnable "${key}" from app "${path}"...`
})
const { value, storagePath } = await loadAppDraftValue(path, workspace)
const { value, storagePath } = await loadAppDraftValue(path, workspace, { forWrite: true })
if (!(key in value.runnables)) {
throw new Error(`Backend runnable "${key}" not found in app "${path}".`)
}
@@ -6971,7 +6976,7 @@ async function rebaseScriptDraft(path: string, ctx: WriteDraftCtx): Promise<stri
// Discard the stale draft rather than resetting it to latest: the next write
// re-bases on the current head, and a premature deploy fails cleanly ("no
// draft") instead of silently shipping the latest unchanged and losing the work.
await deleteGlobalDraft(workspace, 'script', path)
await deleteGlobalDraft(workspace, 'script', path, undefined, { storagePath: basePath })
toolCallbacks.setToolStatus(toolId, {
content: `Discarded stale draft "${path}"`,
@@ -7047,7 +7052,7 @@ async function rebaseFlowDraft(path: string, ctx: WriteDraftCtx): Promise<string
// Discard the stale draft (see rebaseScriptDraft): the next write re-bases on
// the current head, and a premature deploy fails cleanly instead of shipping
// the latest unchanged.
await deleteGlobalDraft(workspace, 'flow', path)
await deleteGlobalDraft(workspace, 'flow', path, undefined, { storagePath: basePath })
toolCallbacks.setToolStatus(toolId, {
content: `Discarded stale draft "${path}"`,
@@ -7134,7 +7139,7 @@ async function rebaseAppDraft(path: string, ctx: WriteDraftCtx): Promise<string>
// Discard the stale draft (see rebaseScriptDraft): the next write re-projects
// the deployed app into a fresh draft (re-pinning parent_version to the head),
// and a premature deploy fails cleanly instead of shipping the latest unchanged.
await deleteGlobalDraft(workspace, 'app', path)
await deleteGlobalDraft(workspace, 'app', path, undefined, { storagePath: basePath })
toolCallbacks.setToolStatus(toolId, {
content: `Discarded stale draft "${path}"`,
@@ -637,11 +637,14 @@ export async function getGlobalDraftAt(
workspace: string,
type: WorkspaceItemType,
storagePath: string,
triggerKind?: TriggerKind
triggerKind?: TriggerKind,
/** What the caller's own resolution already found at this key, so it is not fetched
* a second time. */
found: { value?: unknown; fetched?: boolean } = {}
): Promise<WorkspaceItem | undefined> {
const itemKind = itemKindFor(type, triggerKind)
if (!itemKind) return undefined
return draftItemAt(workspace, itemKind, storagePath, {})
return draftItemAt(workspace, itemKind, storagePath, found)
}
async function draftItemAt(