fix: preserve db draft rename deploys

This commit is contained in:
centdix
2026-06-03 15:48:47 +02:00
parent 217e805cc0
commit 36012f7425
3 changed files with 171 additions and 18 deletions
@@ -987,6 +987,50 @@ describe('global AI tools', () => {
expect(ScriptService.getScriptByPath).not.toHaveBeenCalled()
})
it('deploys saved DB script rename drafts to the draft target path', async () => {
vi.mocked(ScriptService.existsScriptByPath).mockResolvedValueOnce(true)
vi.mocked(ScriptService.getScriptByPathWithDraft).mockResolvedValueOnce({
path: 'f/scripts/old-name',
hash: 'old-script-hash',
summary: 'deployed summary',
description: 'deployed description',
content: 'deployed content',
schema: {},
is_template: false,
language: 'bun',
kind: 'script',
draft: {
path: 'f/scripts/new-name',
summary: 'renamed draft summary',
description: 'renamed draft description',
content: 'renamed draft content',
language: 'bun',
kind: 'script'
}
} as any)
const raw = await callGlobalTool('deploy_workspace_item', {
type: 'script',
path: 'f/scripts/old-name'
})
expect(ScriptService.createScript).toHaveBeenCalledWith({
workspace: WORKSPACE,
requestBody: expect.objectContaining({
path: 'f/scripts/new-name',
parent_hash: 'old-script-hash',
summary: 'renamed draft summary',
description: 'renamed draft description',
content: 'renamed draft content'
})
})
expect(JSON.parse(raw)).toMatchObject({
success: true,
type: 'script',
path: 'f/scripts/new-name'
})
})
it('deploys DB flow drafts with draft metadata when no local draft exists', async () => {
vi.mocked(FlowService.existsFlowByPath).mockResolvedValueOnce(true)
vi.mocked(FlowService.getFlowByPathWithDraft).mockResolvedValueOnce({
@@ -1051,6 +1095,52 @@ describe('global AI tools', () => {
expect(FlowService.getFlowByPath).not.toHaveBeenCalled()
})
it('deploys saved DB flow rename drafts through the original route path', async () => {
vi.mocked(FlowService.existsFlowByPath).mockResolvedValueOnce(true)
vi.mocked(FlowService.getFlowByPathWithDraft).mockResolvedValueOnce({
path: 'f/flows/old-name',
summary: 'deployed summary',
description: 'deployed description',
value: {
modules: [{ id: 'deployed_step', value: { type: 'identity' } }]
},
schema: { type: 'object', properties: { deployed: { type: 'boolean' } } },
draft: {
path: 'f/flows/new-name',
summary: 'renamed draft summary',
description: 'renamed draft description',
value: {
modules: [{ id: 'draft_step', value: { type: 'identity' } }]
},
schema: { type: 'object', properties: { draft: { type: 'string' } } }
}
} as any)
const raw = await callGlobalTool('deploy_workspace_item', {
type: 'flow',
path: 'f/flows/old-name'
})
expect(FlowService.updateFlow).toHaveBeenCalledWith({
workspace: WORKSPACE,
path: 'f/flows/old-name',
requestBody: expect.objectContaining({
path: 'f/flows/new-name',
summary: 'renamed draft summary',
description: 'renamed draft description',
schema: { type: 'object', properties: { draft: { type: 'string' } } }
})
})
expect(vi.mocked(FlowService.updateFlow).mock.calls[0]?.[0].requestBody.value.modules).toEqual([
{ id: 'draft_step', value: { type: 'identity' } }
])
expect(JSON.parse(raw)).toMatchObject({
success: true,
type: 'flow',
path: 'f/flows/new-name'
})
})
it('applies path_prefix to local drafts before enforcing the result limit', async () => {
await callGlobalTool('write_script', {
path: 'f/other/outside',
@@ -1913,6 +2003,50 @@ describe('global AI tools', () => {
expect(UserDraft.get('raw_app', 'f/apps/report', { workspace: WORKSPACE })).toBeUndefined()
})
it('deploys saved DB app rename drafts through the original route path', async () => {
appDrafts.set('f/apps/old-report', {
path: 'f/apps/new-report',
summary: 'Renamed report',
value: {
files: { '/index.tsx': 'console.log("renamed")' },
runnables: {},
data: { tables: ['renamed'] }
},
policy: { execution_mode: 'anonymous' }
})
const raw = await callGlobalTool('deploy_workspace_item', {
type: 'app',
path: 'f/apps/old-report'
})
expect(AppService.updateAppRaw).toHaveBeenCalledWith({
workspace: WORKSPACE,
path: 'f/apps/old-report',
formData: {
app: {
path: 'f/apps/new-report',
value: {
files: { '/index.tsx': 'console.log("renamed")' },
runnables: {},
data: { tables: ['renamed'] }
},
summary: 'Renamed report',
policy: expect.objectContaining({ execution_mode: 'anonymous' }),
deployment_message: undefined
},
js: 'bundled js',
css: 'bundled css'
}
})
expect(AppService.createAppRaw).not.toHaveBeenCalled()
expect(JSON.parse(raw)).toMatchObject({
success: true,
type: 'app',
path: 'f/apps/new-report'
})
})
it('notifies the session preview (as raw_app) after deploying a raw app', async () => {
const onDeployed = vi.fn()
setDeployedInSessionHandler(onDeployed)
@@ -3072,9 +3072,11 @@ async function deployDraft(
if (draft.value === undefined) {
throw new Error(`Draft ${type} "${path}" has no value to deploy.`)
}
const deployPath =
type === 'script' || type === 'flow' || type === 'app' ? (draft.path ?? path) : path
toolCallbacks.setToolStatus(toolId, {
content: `Deploying ${type} "${path}"...`
content: `Deploying ${type} "${deployPath}"...`
})
let actions: ToolDisplayAction[] | undefined
@@ -3083,7 +3085,7 @@ async function deployDraft(
case 'script': {
await deployScriptDraft({
workspace,
path,
path: deployPath,
draft,
existing: deployableDraft.existingScript,
deploymentMessage,
@@ -3095,6 +3097,7 @@ async function deployDraft(
await deployFlowDraft({
workspace,
path,
targetPath: deployPath,
draft,
existing: deployableDraft.existingFlow,
deploymentMessage,
@@ -3153,6 +3156,7 @@ async function deployDraft(
await deployAppDraft({
workspace,
path,
targetPath: deployPath,
draft,
appExists: deployableDraft.appExists,
deploymentMessage,
@@ -3175,19 +3179,19 @@ async function deployDraft(
app: 'raw_app'
}
const kind = previewKindByType[type]
if (kind) deployedInSessionHandler?.({ sessionId, kind, path })
if (kind) deployedInSessionHandler?.({ sessionId, kind, path: deployPath })
toolCallbacks.setToolStatus(toolId, {
content: `Deployed ${type} "${path}"`,
content: `Deployed ${type} "${deployPath}"`,
result: 'Deployed',
actions
})
return JSON.stringify(
{
success: true,
message: `Deployed draft ${type} "${path}" to the workspace. Draft removed from the draft system.`,
message: `Deployed draft ${type} "${deployPath}" to the workspace. Draft removed from the draft system.`,
type,
path,
path: deployPath,
triggerKind
},
null,
@@ -188,7 +188,9 @@ export async function loadDbDraftForDeploy(
if (!(await ScriptService.existsScriptByPath({ workspace, path }))) return undefined
const script = await ScriptService.getScriptByPathWithDraft({ workspace, path })
if (!script.draft && !script.draft_only) return undefined
const draftScript = script.draft ? { ...script, ...script.draft, path: script.path } : script
const draftScript = script.draft
? { ...script, ...script.draft, path: script.draft.path ?? script.path }
: script
return {
item: scriptToItem(draftScript, true, true),
existingScript: script,
@@ -199,7 +201,9 @@ export async function loadDbDraftForDeploy(
if (!(await FlowService.existsFlowByPath({ workspace, path }))) return undefined
const flow = await FlowService.getFlowByPathWithDraft({ workspace, path })
if (!flow.draft && !flow.draft_only) return undefined
const draftFlow = flow.draft ? { ...flow, ...flow.draft, path: flow.path } : flow
const draftFlow = flow.draft
? { ...flow, ...flow.draft, path: flow.draft.path ?? flow.path }
: flow
return {
item: flowToItem(draftFlow, true, true),
existingFlow: flow,
@@ -210,11 +214,12 @@ export async function loadDbDraftForDeploy(
if (!(await AppService.existsApp({ workspace, path }))) return undefined
const app = await AppService.getAppByPathWithDraft({ workspace, path })
if (!app.draft && !app.draft_only) return undefined
const draftPath = (app.draft as { path?: string } | undefined)?.path ?? app.path
const value = appSourceToDraftValue(app.draft ?? app, app)
return {
item: {
type: 'app',
path: app.path,
path: draftPath,
summary: value.summary,
value,
isDraft: true
@@ -310,15 +315,24 @@ export async function deployScriptDraft(args: {
export async function deployFlowDraft(args: {
workspace: string
path: string
targetPath?: string
draft: WorkspaceItem
existing?: FlowDeployMetadata
deploymentMessage?: string
draftMetadata?: FlowDeployMetadata
}): Promise<void> {
const { workspace, path, draft, existing, deploymentMessage, draftMetadata } = args
const {
workspace,
path,
targetPath = path,
draft,
existing,
deploymentMessage,
draftMetadata
} = args
const flowDraft = draft.value as FlowDraftValue
const requestBody = buildFlowDeployRequestBody(
path,
targetPath,
draft.summary,
flowDraft,
existing,
@@ -335,12 +349,13 @@ export async function deployFlowDraft(args: {
export async function deployAppDraft(args: {
workspace: string
path: string
targetPath?: string
draft: WorkspaceItem
appExists?: boolean
deploymentMessage?: string
onStatus?: DeployStatusHandler
}): Promise<void> {
const { workspace, path, draft, appExists, deploymentMessage, onStatus } = args
const { workspace, path, targetPath = path, draft, appExists, deploymentMessage, onStatus } = args
const appDraft = draft.value as AppDraftValue
const appValue: AppDraftValue = {
...appDraft,
@@ -351,10 +366,10 @@ export async function deployAppDraft(args: {
await recomputeAppPolicy(appValue)
const policy = appValue.policy
if (!policy) {
throw new Error(`Draft app "${path}" has no policy to deploy.`)
throw new Error(`Draft app "${targetPath}" has no policy to deploy.`)
}
onStatus?.(`Bundling app "${path}"...`)
onStatus?.(`Bundling app "${targetPath}"...`)
const bundle = await bundleRawAppDraft({
workspace,
files: appValue.files,
@@ -365,12 +380,12 @@ export async function deployAppDraft(args: {
.filter(Boolean)
const latest = lines[lines.length - 1]
if (latest) {
onStatus?.(`Bundling app "${path}"... ${latest}`)
onStatus?.(`Bundling app "${targetPath}"... ${latest}`)
}
}
})
onStatus?.(`Deploying app "${path}"...`)
onStatus?.(`Deploying app "${targetPath}"...`)
const rawAppValue = {
files: appValue.files,
runnables: appValue.runnables,
@@ -386,7 +401,7 @@ export async function deployAppDraft(args: {
path,
formData: {
app: {
path,
path: targetPath,
value: rawAppValue,
summary,
policy,
@@ -401,7 +416,7 @@ export async function deployAppDraft(args: {
workspace,
formData: {
app: {
path,
path: targetPath,
value: rawAppValue,
summary,
policy,