refactor: simplify script draft field preservation

This commit is contained in:
centdix
2026-05-20 14:42:06 +02:00
parent fb05c310bb
commit 63cf688849
2 changed files with 96 additions and 53 deletions
@@ -52,6 +52,7 @@ vi.mock('$lib/gen', async () => {
...actual,
ScriptService: wrapService(actual.ScriptService, {
existsScriptByPath: vi.fn(async () => false),
getScriptByPath: vi.fn(),
listScripts: vi.fn(async () => [])
}),
FlowService: wrapService(actual.FlowService, {
@@ -122,6 +123,7 @@ vi.mock('$lib/gen', async () => {
import { globalTools, prepareGlobalUserMessage } from './core'
import { deleteGlobalDraft, listGlobalDrafts } from './userDraftAdapter'
import { UserDraft, __resetUserDraftForTesting } from '$lib/userDraft.svelte'
import { ScriptService } from '$lib/gen'
import type { Tool, ToolCallbacks } from '../shared'
const WORKSPACE = 'global-core-test'
@@ -336,6 +338,75 @@ describe('global AI tools', () => {
expect(localStorage.getItem(`userdraft/w/${WORKSPACE}/script/f/scripts/live-ai`)).not.toBeNull()
})
it('preserves NewScript metadata from existing scripts without copying backend read fields', async () => {
vi.mocked(ScriptService.existsScriptByPath).mockResolvedValueOnce(true)
vi.mocked(ScriptService.getScriptByPath).mockResolvedValueOnce({
workspace_id: WORKSPACE,
hash: 'old-hash',
path: 'f/scripts/existing',
parent_hashes: ['older-hash'],
summary: 'Existing script',
description: 'Existing description',
content: 'export async function main() { return 1 }',
created_by: 'admin',
created_at: '2026-01-01T00:00:00Z',
archived: false,
deleted: false,
extra_perms: {},
is_template: true,
language: 'bun',
kind: 'script',
starred: true,
has_draft: true,
schema: {
type: 'object',
properties: {
name: { type: 'string' }
}
},
concurrent_limit: 3,
cache_ttl: 30,
cache_ignore_s3_path: true,
has_preprocessor: true,
assets: [{ path: 'asset.txt', kind: 's3object' }],
labels: ['important']
} as any)
await callGlobalTool('write_script', {
path: 'f/scripts/existing',
summary: 'AI update',
language: 'python3',
content: 'def main():\n return 2'
})
const draft = UserDraft.get<any>('script', 'f/scripts/existing', { workspace: WORKSPACE })
expect(draft).toMatchObject({
path: 'f/scripts/existing',
parent_hash: 'old-hash',
summary: 'AI update',
description: 'Existing description',
content: 'def main():\n return 2',
language: 'python3',
is_template: true,
schema: {
properties: {
name: { type: 'string' }
}
},
concurrent_limit: 3,
cache_ttl: 30,
cache_ignore_s3_path: true,
has_preprocessor: true,
assets: [{ path: 'asset.txt', kind: 's3object' }],
labels: ['important']
})
expect(draft).not.toHaveProperty('hash')
expect(draft).not.toHaveProperty('workspace_id')
expect(draft).not.toHaveProperty('created_by')
expect(draft).not.toHaveProperty('starred')
expect(draft).not.toHaveProperty('extra_perms')
})
it('deleteGlobalDraft clears both persisted storage and any live handle state', async () => {
const handle = UserDraft.use<any>('script', 'f/scripts/delete-live', { workspace: WORKSPACE })
@@ -15,7 +15,7 @@ import {
VariableService,
WebsocketTriggerService
} from '$lib/gen'
import { $ScriptLang } from '$lib/gen/schemas.gen'
import { $NewScript, $ScriptLang } from '$lib/gen/schemas.gen'
import type {
AppWithLastVersion,
Flow,
@@ -1720,65 +1720,37 @@ function cloneIfDefined<T>(value: T | undefined): T | undefined {
}
}
function buildScriptDraftFromBase(
base: Partial<NewScript>,
args: { path: string; summary?: string; language: ScriptLang; content: string },
overrides: Partial<NewScript> = {}
): NewScript {
const draft: NewScript = {
path: args.path,
summary: args.summary ?? base.summary ?? '',
description: base.description ?? '',
content: args.content,
schema: cloneIfDefined(base.schema) ?? emptySchema(),
is_template: base.is_template ?? false,
language: args.language,
kind: base.kind ?? 'script'
}
const newScriptKeys = new Set(Object.keys($NewScript.properties) as Array<keyof NewScript>)
const optionalKeys: (keyof NewScript)[] = [
'parent_hash',
'lock',
'tag',
'draft_only',
'envs',
'concurrent_limit',
'concurrency_time_window_s',
'cache_ttl',
'cache_ignore_s3_path',
'dedicated_worker',
'ws_error_handler_muted',
'priority',
'restart_unless_cancelled',
'timeout',
'delete_after_secs',
'deployment_message',
'concurrency_key',
'debounce_key',
'debounce_delay_s',
'debounce_args_to_accumulate',
'max_total_debouncing_time',
'max_total_debounces_amount',
'visible_to_runner_only',
'auto_kind',
'codebase',
'has_preprocessor',
'on_behalf_of_email',
'preserve_on_behalf_of',
'assets',
'modules',
'labels'
]
for (const key of optionalKeys) {
const cloned = cloneIfDefined(base[key] as never)
function pickNewScriptFields(base: Partial<NewScript> & Partial<Script>): Partial<NewScript> {
const draft: Partial<NewScript> = {}
for (const key of Object.keys(base) as Array<keyof NewScript>) {
if (!newScriptKeys.has(key)) continue
const cloned = cloneIfDefined(base[key])
if (cloned !== undefined) {
;(draft as Record<string, unknown>)[key] = cloned
}
}
return draft
}
function buildScriptDraftFromBase(
base: Partial<NewScript> & Partial<Script>,
args: { path: string; summary?: string; language: ScriptLang; content: string },
overrides: Partial<NewScript> = {}
): NewScript {
const draftBase = pickNewScriptFields(base)
return {
...draft,
...draftBase,
path: args.path,
summary: args.summary ?? draftBase.summary ?? '',
description: draftBase.description ?? '',
content: args.content,
schema: draftBase.schema ?? emptySchema(),
language: args.language,
kind: draftBase.kind ?? 'script',
is_template: draftBase.is_template ?? false,
...overrides
}
}