refactor: avoid redundant ai draft deploy fetches

This commit is contained in:
centdix
2026-06-03 12:30:31 +02:00
parent 458d898024
commit 59806bc678
4 changed files with 73 additions and 87 deletions
@@ -930,9 +930,7 @@ describe('global AI tools', () => {
})
it('deploys DB script drafts with draft metadata when no local draft exists', async () => {
vi.mocked(ScriptService.existsScriptByPath)
.mockResolvedValueOnce(true)
.mockResolvedValueOnce(true)
vi.mocked(ScriptService.existsScriptByPath).mockResolvedValueOnce(true)
vi.mocked(ScriptService.getScriptByPathWithDraft).mockResolvedValueOnce({
path: 'f/scripts/existing',
hash: 'deployed-hash',
@@ -962,22 +960,6 @@ describe('global AI tools', () => {
labels: []
}
} as any)
vi.mocked(ScriptService.getScriptByPath).mockResolvedValueOnce({
path: 'f/scripts/existing',
hash: 'deployed-hash',
summary: 'deployed summary',
description: 'deployed description',
content: 'deployed content',
schema: {},
is_template: false,
language: 'bun',
kind: 'script',
tag: 'deployed-tag',
envs: ['DEPLOYED_ENV'],
timeout: 60,
visible_to_runner_only: true,
labels: ['deployed']
} as any)
await callGlobalTool('deploy_workspace_item', {
type: 'script',
@@ -1002,10 +984,11 @@ describe('global AI tools', () => {
deployment_message: 'ship db draft'
})
})
expect(ScriptService.getScriptByPath).not.toHaveBeenCalled()
})
it('deploys DB flow drafts with draft metadata when no local draft exists', async () => {
vi.mocked(FlowService.existsFlowByPath).mockResolvedValueOnce(true).mockResolvedValueOnce(true)
vi.mocked(FlowService.existsFlowByPath).mockResolvedValueOnce(true)
vi.mocked(FlowService.getFlowByPathWithDraft).mockResolvedValueOnce({
path: 'f/flows/existing',
summary: 'deployed summary',
@@ -1040,23 +1023,6 @@ describe('global AI tools', () => {
labels: []
}
} as any)
vi.mocked(FlowService.getFlowByPath).mockResolvedValueOnce({
path: 'f/flows/existing',
summary: 'deployed summary',
description: 'deployed description',
value: {
modules: [{ id: 'deployed_step', value: { type: 'identity' } }]
},
schema: { type: 'object', properties: { deployed: { type: 'boolean' } } },
edited_by: 'admin',
edited_at: '2026-05-22T09:00:00Z',
archived: false,
extra_perms: {},
tag: 'deployed-tag',
timeout: 60,
visible_to_runner_only: true,
labels: ['deployed']
} as any)
await callGlobalTool('deploy_workspace_item', {
type: 'flow',
@@ -1082,6 +1048,7 @@ describe('global AI tools', () => {
expect(vi.mocked(FlowService.updateFlow).mock.calls[0]?.[0].requestBody.value.modules).toEqual([
{ id: 'draft_step', value: { type: 'identity' } }
])
expect(FlowService.getFlowByPath).not.toHaveBeenCalled()
})
it('applies path_prefix to local drafts before enforcing the result limit', async () => {
@@ -365,9 +365,9 @@ const deleteWorkspaceItemSchema = z.object({
.describe('Required when type is trigger. Identifies which trigger service to call.')
})
const discardLocalDraftSchema = z.object({
const discardDraftSchema = z.object({
type: itemTypeSchema,
path: z.string().describe('Workspace path of the local draft to discard.'),
path: z.string().describe('Workspace path of the draft to discard.'),
trigger_kind: triggerKindSchema
.optional()
.describe('Required when type is trigger. Must match the draft trigger kind.')
@@ -428,7 +428,7 @@ const testRunScriptSchema = z.object({
const testRunScriptToolDef = createToolDef(
testRunScriptSchema,
'test_run_script',
'Execute a preview-style test run of a script by path, preferring local draft content when it exists.',
'Execute a preview-style test run of a script by path, preferring draft content when it exists.',
{ strict: false }
)
@@ -440,7 +440,7 @@ const testRunFlowSchema = z.object({
const testRunFlowToolDef = createToolDef(
testRunFlowSchema,
'test_run_flow',
'Execute a preview-style test run of a flow by path, preferring local draft content when it exists.',
'Execute a preview-style test run of a flow by path, preferring draft content when it exists.',
{ strict: false }
)
@@ -453,7 +453,7 @@ const testRunStepSchema = z.object({
const testRunStepToolDef = createToolDef(
testRunStepSchema,
'test_run_step',
'Execute a test run of one step in a flow by path, preferring local draft flow/script content when it exists.',
'Execute a test run of one step in a flow by path, preferring draft flow/script content when it exists.',
{ strict: false }
)
@@ -610,7 +610,7 @@ Rules:
- Variable values are never readable. For secrets, create a secret variable and reference it from resources as "$var:path/to/variable".
- Use search_resource_types before write_resource.
- Use get_instructions before writing scripts, flows, resources, or apps. For scripts, pass the target language.
- After creating or editing a script or flow draft, run test_run_script, test_run_flow, or test_run_step with representative args before reporting that it works. These tools prefer local drafts, so testing does not require deployment.
- After creating or editing a script or flow draft, run test_run_script, test_run_flow, or test_run_step with representative args before reporting that it works. These tools prefer drafts, so testing does not require deployment.
- When a required decision is ambiguous, use askUserQuestion with two to ten clear proposed answer strings instead of guessing. The user can also type a custom answer when none of the proposed answers fit.
- Keep context targeted.${
previewTools
@@ -1212,7 +1212,7 @@ function getFlowInstructions(): string {
- \`read_workspace_item\` and \`patch_flow_json\` operate on a **compact view** of the flow: every rawscript module's \`value.content\` is replaced with the placeholder \`"inline_script.<moduleId>"\` so inline script bodies don't bloat tool I/O. Schema, groups, preprocessor_module and failure_module are all shown in this view.
- Inline rawscript content is **not** part of the JSON \`patch_flow_json\` sees. Edits to inline bodies happen via dedicated tools:
- \`read_flow_module_code(path, module_id)\` — returns the raw inline script content for one module.
- \`set_flow_module_code(path, module_id, code)\` — overwrites that module's inline script content; saves to the local draft.
- \`set_flow_module_code(path, module_id, code)\` — overwrites that module's inline script content; saves to the draft.
- Use \`patch_flow_json\` for *structural* edits: module ids, paths, input_transforms, branch arrangement, summaries, preprocessor/failure swaps, schema/groups. Use \`set_flow_module_code\` for changes inside a specific rawscript body.
- \`write_flow\` is for full overwrites / create-from-scratch. Its \`modules\`, \`preprocessor_module\`, and \`failure_module\` arguments use **non-compact** flow modules (rawscript content is the actual code, not a placeholder).
@@ -1364,7 +1364,7 @@ export const globalTools: Tool<{}>[] = [
def: createToolDef(
listWorkspaceItemsSchema,
'list_workspace_items',
'List workspace items and local drafts. Returns metadata only.'
'List workspace items and drafts. Returns metadata only.'
),
fn: async ({ args, workspace, toolId, toolCallbacks }) => {
const parsed = listWorkspaceItemsSchema.parse(args)
@@ -1406,7 +1406,7 @@ export const globalTools: Tool<{}>[] = [
def: createToolDef(
readWorkspaceItemSchema,
'read_workspace_item',
'Read one workspace item or local draft.'
'Read one workspace item or draft.'
),
fn: async ({ args, workspace, toolId, toolCallbacks }) => {
const parsed = readWorkspaceItemSchema.parse(args)
@@ -1418,7 +1418,7 @@ export const globalTools: Tool<{}>[] = [
const draft = getGlobalDraft(workspace, parsed.type, parsed.path, parsed.trigger_kind)
if (draft) {
toolCallbacks.setToolStatus(toolId, {
content: `Read local draft ${parsed.type} "${parsed.path}"`
content: `Read draft ${parsed.type} "${parsed.path}"`
})
return JSON.stringify(serializeWorkspaceItemForRead(draft), null, 2)
}
@@ -1432,11 +1432,7 @@ export const globalTools: Tool<{}>[] = [
}
},
{
def: createToolDef(
writeScriptSchema,
'write_script',
'Create or overwrite a local draft script.'
),
def: createToolDef(writeScriptSchema, 'write_script', 'Create or overwrite a draft script.'),
showDetails: true,
streamArguments: true,
showFade: true,
@@ -1446,7 +1442,7 @@ export const globalTools: Tool<{}>[] = [
}
},
{
def: createToolDef(writeFlowSchema, 'write_flow', 'Create or overwrite a local draft flow.'),
def: createToolDef(writeFlowSchema, 'write_flow', 'Create or overwrite a draft flow.'),
showDetails: true,
streamArguments: true,
showFade: true,
@@ -1506,7 +1502,7 @@ export const globalTools: Tool<{}>[] = [
def: createToolDef(
editScriptSchema,
'edit_script',
'Find/replace exact text in a script and save a local draft.'
'Find/replace exact text in a script and save a draft.'
),
showDetails: true,
streamArguments: true,
@@ -1520,7 +1516,7 @@ export const globalTools: Tool<{}>[] = [
def: createToolDef(
patchFlowJsonSchema,
'patch_flow_json',
'Find/replace exact text in compact flow JSON and save a local draft.'
'Find/replace exact text in compact flow JSON and save a draft.'
),
showDetails: true,
streamArguments: true,
@@ -1596,7 +1592,7 @@ export const globalTools: Tool<{}>[] = [
},
{
def: createToolDef(
discardLocalDraftSchema,
discardDraftSchema,
'discard_local_draft',
'Discard a draft only. Does not mutate deployed workspace items, but clears the matching open editor draft if one is mounted.'
),
@@ -1605,8 +1601,8 @@ export const globalTools: Tool<{}>[] = [
requiresConfirmation: true,
confirmationMessage: 'Discard draft',
fn: async (ctx) => {
const parsed = discardLocalDraftSchema.parse(ctx.args)
return discardLocalDraft(parsed, ctx)
const parsed = discardDraftSchema.parse(ctx.args)
return discardDraft(parsed, ctx)
}
},
{
@@ -1683,7 +1679,7 @@ export const globalTools: Tool<{}>[] = [
def: createToolDef(
setFlowModuleCodeSchema,
'set_flow_module_code',
'Overwrite inline script code in one flow module and save a local draft.'
'Overwrite inline script code in one flow module and save a draft.'
),
showDetails: true,
streamArguments: true,
@@ -1697,7 +1693,7 @@ export const globalTools: Tool<{}>[] = [
def: createToolDef(
initAppSchema,
'init_app',
'Initialize a local draft raw app from a framework template.',
'Initialize a draft raw app from a framework template.',
{ strict: false }
),
showDetails: true,
@@ -1722,7 +1718,7 @@ export const globalTools: Tool<{}>[] = [
def: createToolDef(
writeAppFileSchema,
'write_app_file',
'Create or overwrite a frontend file in a local app draft.'
'Create or overwrite a frontend file in an app draft.'
),
showDetails: true,
streamArguments: true,
@@ -1736,7 +1732,7 @@ export const globalTools: Tool<{}>[] = [
def: createToolDef(
deleteAppFileSchema,
'delete_app_file',
'Remove a frontend file from a local app draft.'
'Remove a frontend file from an app draft.'
),
fn: async (ctx) => {
const parsed = deleteAppFileSchema.parse(ctx.args)
@@ -1747,7 +1743,7 @@ export const globalTools: Tool<{}>[] = [
def: createToolDef(
patchAppFileSchema,
'patch_app_file',
'Find/replace exact text in a raw app file and save a local draft.'
'Find/replace exact text in a raw app file and save a draft.'
),
showDetails: true,
streamArguments: true,
@@ -1761,7 +1757,7 @@ export const globalTools: Tool<{}>[] = [
def: createToolDef(
writeAppRunnableSchema,
'write_app_runnable',
'Create or overwrite a backend runnable in a local app draft.',
'Create or overwrite a backend runnable in an app draft.',
{ strict: false }
),
showDetails: true,
@@ -1776,7 +1772,7 @@ export const globalTools: Tool<{}>[] = [
def: createToolDef(
deleteAppRunnableSchema,
'delete_app_runnable',
'Remove a backend runnable from a local app draft.'
'Remove a backend runnable from an app draft.'
),
fn: async (ctx) => {
const parsed = deleteAppRunnableSchema.parse(ctx.args)
@@ -2480,8 +2476,20 @@ async function loadScriptForFlowStep(
moduleValue: { path: string; hash?: string },
workspace: string
): Promise<{ content: string; language: ScriptLang }> {
const deployableDraft = await loadWorkspaceDraftForDeploy(workspace, 'script', moduleValue.path)
const draft = deployableDraft?.item
const localDraft = getGlobalDraft(workspace, 'script', moduleValue.path)
if (localDraft) {
if (typeof localDraft.value !== 'string' || !localDraft.language) {
throw new Error(`Draft script "${moduleValue.path}" is missing content or language.`)
}
return { content: localDraft.value, language: localDraft.language }
}
if (!moduleValue.hash) {
const script = await loadScriptWithDbDraft(moduleValue.path, workspace)
return { content: script.content, language: script.language }
}
const draft = (await loadWorkspaceDraftForDeploy(workspace, 'script', moduleValue.path))?.item
if (draft) {
if (typeof draft.value !== 'string' || !draft.language) {
throw new Error(`Draft script "${moduleValue.path}" is missing content or language.`)
@@ -2489,9 +2497,7 @@ async function loadScriptForFlowStep(
return { content: draft.value, language: draft.language }
}
const script = moduleValue.hash
? await ScriptService.getScriptByHash({ workspace, hash: moduleValue.hash })
: await ScriptService.getScriptByPath({ workspace, path: moduleValue.path })
const script = await ScriptService.getScriptByHash({ workspace, hash: moduleValue.hash })
return { content: script.content, language: script.language }
}
@@ -2625,7 +2631,7 @@ async function initApp(
if (getGlobalDraft(workspace, 'app', path)) {
throw new Error(
`A local draft for app "${path}" already exists. Use write_app_file / write_app_runnable to modify it, or delete the existing draft first.`
`A draft for app "${path}" already exists. Use write_app_file / write_app_runnable to modify it, or delete the existing draft first.`
)
}
if (await AppService.existsApp({ workspace, path })) {
@@ -2972,7 +2978,7 @@ function createOpenVariableAction(path: string): ToolDisplayAction {
}
}
async function discardLocalDraft(
async function discardDraft(
args: { type: WorkspaceItemType; path: string; trigger_kind?: TriggerKind },
ctx: WriteDraftCtx
): Promise<string> {
@@ -3047,6 +3053,7 @@ async function deployDraft(
workspace,
path,
draft,
existing: deployableDraft.existingScript,
deploymentMessage,
draftMetadata: deployableDraft.scriptMetadata
})
@@ -3057,6 +3064,7 @@ async function deployDraft(
workspace,
path,
draft,
existing: deployableDraft.existingFlow,
deploymentMessage,
draftMetadata: deployableDraft.flowMetadata
})
@@ -3114,6 +3122,7 @@ async function deployDraft(
workspace,
path,
draft,
appExists: deployableDraft.appExists,
deploymentMessage,
onStatus: (content) => {
toolCallbacks.setToolStatus(toolId, { content })
@@ -72,7 +72,7 @@ function pickDeployMetadata<T extends object, K extends keyof T>(
export function buildScriptDeployRequestBody(
path: string,
draft: WorkspaceItem,
existing: Script | undefined,
existing: ScriptDeployMetadata | undefined,
deploymentMessage: string | undefined,
draftMetadata?: ScriptDeployMetadata
): NewScript {
@@ -80,8 +80,7 @@ export function buildScriptDeployRequestBody(
throw new Error(`Draft script "${path}" is missing content or language.`)
}
const existingWithMetadata = existing as ScriptDeployMetadata | undefined
const metadata = mergeDeployMetadata(existingWithMetadata, draftMetadata)
const metadata = mergeDeployMetadata(existing, draftMetadata)
const onBehalfOfEmail = metadata.on_behalf_of_email
return {
@@ -111,7 +110,7 @@ export function buildFlowDeployRequestBody(
path: string,
draftSummary: string | undefined,
flowDraft: FlowDraftValue,
existing: Flow | undefined,
existing: FlowDeployMetadata | undefined,
deploymentMessage: string | undefined,
draftMetadata?: FlowDeployMetadata
): FlowDeployRequestBody {
@@ -33,8 +33,11 @@ export type DbDraftWorkspaceItemType = Extract<WorkspaceItemType, 'script' | 'fl
export type DeployableDraft = {
item: WorkspaceItem
existingScript?: ScriptDeployMetadata
scriptMetadata?: ScriptDeployMetadata
existingFlow?: FlowDeployMetadata
flowMetadata?: FlowDeployMetadata
appExists?: boolean
}
type DeployStatusHandler = (content: string) => void
@@ -187,6 +190,7 @@ export async function loadDbDraftForDeploy(
const draftScript = script.draft ? { ...script, ...script.draft, path: script.path } : script
return {
item: scriptToItem(draftScript, true, true),
existingScript: script,
scriptMetadata: draftScript
}
}
@@ -197,6 +201,7 @@ export async function loadDbDraftForDeploy(
const draftFlow = flow.draft ? { ...flow, ...flow.draft, path: flow.path } : flow
return {
item: flowToItem(draftFlow, true, true),
existingFlow: flow,
flowMetadata: draftFlow
}
}
@@ -212,7 +217,8 @@ export async function loadDbDraftForDeploy(
summary: value.summary,
value,
isDraft: true
}
},
appExists: true
}
}
}
@@ -251,13 +257,21 @@ export async function loadWorkspaceDraftForDeploy(
case 'script':
return {
item,
existingScript: (await ScriptService.existsScriptByPath({ workspace, path }))
? await ScriptService.getScriptByPath({ workspace, path })
: undefined,
scriptMetadata: UserDraft.get<NewScript>('script', storagePath, { workspace })
}
case 'flow':
return {
item,
existingFlow: (await FlowService.existsFlowByPath({ workspace, path }))
? await FlowService.getFlowByPath({ workspace, path })
: undefined,
flowMetadata: UserDraft.get<Flow>('flow', storagePath, { workspace })
}
case 'app':
return { item, appExists: await AppService.existsApp({ workspace, path }) }
default:
return { item }
}
@@ -270,13 +284,11 @@ export async function deployScriptDraft(args: {
workspace: string
path: string
draft: WorkspaceItem
existing?: ScriptDeployMetadata
deploymentMessage?: string
draftMetadata?: ScriptDeployMetadata
}): Promise<void> {
const { workspace, path, draft, deploymentMessage, draftMetadata } = args
const existing = (await ScriptService.existsScriptByPath({ workspace, path }))
? await ScriptService.getScriptByPath({ workspace, path })
: undefined
const { workspace, path, draft, existing, deploymentMessage, draftMetadata } = args
const requestBody = buildScriptDeployRequestBody(
path,
draft,
@@ -298,14 +310,12 @@ export async function deployFlowDraft(args: {
workspace: string
path: string
draft: WorkspaceItem
existing?: FlowDeployMetadata
deploymentMessage?: string
draftMetadata?: FlowDeployMetadata
}): Promise<void> {
const { workspace, path, draft, deploymentMessage, draftMetadata } = args
const { workspace, path, draft, existing, deploymentMessage, draftMetadata } = args
const flowDraft = draft.value as FlowDraftValue
const existing = (await FlowService.existsFlowByPath({ workspace, path }))
? await FlowService.getFlowByPath({ workspace, path })
: undefined
const requestBody = buildFlowDeployRequestBody(
path,
draft.summary,
@@ -325,10 +335,11 @@ export async function deployAppDraft(args: {
workspace: string
path: string
draft: WorkspaceItem
appExists?: boolean
deploymentMessage?: string
onStatus?: DeployStatusHandler
}): Promise<void> {
const { workspace, path, draft, deploymentMessage, onStatus } = args
const { workspace, path, draft, appExists, deploymentMessage, onStatus } = args
const appDraft = draft.value as AppDraftValue
const appValue: AppDraftValue = {
...appDraft,
@@ -365,7 +376,7 @@ export async function deployAppDraft(args: {
data: appValue.data ?? { ...DEFAULT_RAW_APP_DATA }
}
const summary = appValue.summary ?? draft.summary ?? ''
if (await AppService.existsApp({ workspace, path })) {
if (appExists ?? (await AppService.existsApp({ workspace, path }))) {
// Omit custom_path on update for now. The backend preserves it when absent, while
// sending it requires admin privileges; this chat deploy path does not yet mirror
// the raw app editor's user/admin-specific custom_path handling.