feat: add global ai chat test tools

This commit is contained in:
centdix
2026-06-01 12:01:42 +02:00
parent eadeac248b
commit 669984d53f
5 changed files with 757 additions and 106 deletions
@@ -61,7 +61,12 @@ import { runChatLoop } from './chatLoop'
import type { ReviewChangesOpts } from './monaco-adapter'
import { getCurrentModel, tryGetCurrentModel, getCombinedCustomPrompt } from '$lib/aiStore'
import type { WorkspaceMutationTarget } from './workspaceTools'
import { globalToolsFor, prepareGlobalSystemMessage, prepareGlobalUserMessage } from './global/core'
import {
globalToolsFor,
prepareGlobalSystemMessage,
prepareGlobalUserMessage,
type GlobalToolHelpers
} from './global/core'
import { isGlobalAiEnabled } from './global/gate'
// If the estimated token usage is greater than the model context window - the threshold, we delete the oldest message
@@ -512,7 +517,11 @@ export class AIChatManager {
previewTools: this.isSessionChat
})
this.tools = globalToolsFor({ sessionPreview: this.isSessionChat })
this.helpers = this.isSessionChat ? { sessionId: this.sessionId } : {}
this.helpers = {
...(this.isSessionChat ? { sessionId: this.sessionId } : {}),
testActiveFlow: async (args?: Record<string, any>) =>
this.flowAiChatHelpers?.testFlow(args, this.sessionId)
} satisfies GlobalToolHelpers
} else if (mode === AIMode.APP) {
const customPrompt = getCombinedCustomPrompt(mode)
this.systemMessage = prepareAppSystemMessage(customPrompt)
@@ -1,10 +1,4 @@
import {
ScriptService,
type FlowModule,
type InputTransform,
type RawScript,
JobService
} from '$lib/gen'
import { type FlowModule, type InputTransform, type RawScript } from '$lib/gen'
import type {
ChatCompletionSystemMessageParam,
ChatCompletionUserMessageParam
@@ -30,7 +24,8 @@ import {
formatScriptLintResult,
type ScriptLintResult,
createSearchWorkspaceTool,
createGetRunnableDetailsTool
createGetRunnableDetailsTool,
executeFlowStepTestRun
} from '../shared'
import { createWorkspaceMutationTools } from '../workspaceTools'
import type { ContextElement } from '../context'
@@ -442,99 +437,14 @@ export const flowTools: Tool<FlowAIChatHelpers>[] = [
const stepId = args.stepId
const stepArgs = args.args || {}
// Find the step in the flow (includes preprocessor/failure modules)
let targetModule: FlowModule | undefined = findModuleInFlow(flow.value, stepId) ?? undefined
if (!targetModule) {
toolCallbacks.setToolStatus(toolId, {
content: `Step '${stepId}' not found in flow`,
error: `Step with id '${stepId}' does not exist in the current flow`
})
throw new Error(
`Step with id '${stepId}' not found in flow. Available steps: ${(flow.value.modules ?? []).map((m: FlowModule) => m.id).join(', ')}`
)
}
const module = targetModule
const moduleValue = module.value
if (moduleValue.type === 'rawscript') {
// Test raw script step
return executeTestRun({
jobStarter: () =>
JobService.runScriptPreview({
workspace: workspace,
requestBody: {
content: moduleValue.content ?? '',
language: moduleValue.language,
args:
module.id === SPECIAL_MODULE_IDS.PREPROCESSOR
? { _ENTRYPOINT_OVERRIDE: 'preprocessor', ...stepArgs }
: stepArgs
}
}),
workspace,
toolCallbacks,
toolId,
startMessage: `Starting test run of step '${stepId}'...`,
contextName: 'script'
})
} else if (moduleValue.type === 'script') {
// Test script step - need to get the script content
const script = moduleValue.hash
? await ScriptService.getScriptByHash({
workspace: workspace,
hash: moduleValue.hash
})
: await ScriptService.getScriptByPath({
workspace: workspace,
path: moduleValue.path
})
return executeTestRun({
jobStarter: () =>
JobService.runScriptPreview({
workspace: workspace,
requestBody: {
content: script.content,
language: script.language,
args:
module.id === SPECIAL_MODULE_IDS.PREPROCESSOR
? { _ENTRYPOINT_OVERRIDE: 'preprocessor', ...stepArgs }
: stepArgs
}
}),
workspace,
toolCallbacks,
toolId,
startMessage: `Starting test run of script step '${stepId}'...`,
contextName: 'script'
})
} else if (moduleValue.type === 'flow') {
// Test flow step
return executeTestRun({
jobStarter: () =>
JobService.runFlowByPath({
workspace: workspace,
path: moduleValue.path,
requestBody: stepArgs
}),
workspace,
toolCallbacks,
toolId,
startMessage: `Starting test run of flow step '${stepId}'...`,
contextName: 'flow'
})
} else {
toolCallbacks.setToolStatus(toolId, {
content: `Step type '${moduleValue.type}' not supported for testing`,
error: `Cannot test step of type '${moduleValue.type}'`
})
throw new Error(
`Cannot test step of type '${moduleValue.type}'. Supported types: rawscript, script, flow`
)
}
return executeFlowStepTestRun({
flowValue: flow.value,
stepId,
args: stepArgs,
workspace,
toolCallbacks,
toolId
})
},
requiresConfirmation: true,
confirmationMessage: 'Run flow step test',
@@ -45,11 +45,30 @@ vi.mock('$lib/gen', async () => {
ScriptService: wrapService(actual.ScriptService, {
existsScriptByPath: vi.fn(async () => false),
createScript: vi.fn(async () => 'created'),
getScriptByPath: vi.fn(async () => {
throw new Error('getScriptByPath mock not configured')
}),
getScriptByHash: vi.fn(async () => {
throw new Error('getScriptByHash mock not configured')
}),
getScriptByPathWithDraft: vi.fn(async () => {
throw new Error('getScriptByPathWithDraft mock not configured')
}),
queryHubScripts: vi.fn(async () => []),
getHubScriptContentByPath: vi.fn(async () => ''),
listScripts: vi.fn(async () => [])
}),
JobService: wrapService(actual.JobService, {
runScriptPreview: vi.fn(async () => 'job-script-preview'),
runFlowPreview: vi.fn(async () => 'job-flow-preview'),
runFlowByPath: vi.fn(async () => 'job-flow-by-path'),
getJob: vi.fn(async () => ({
type: 'CompletedJob',
success: true,
result: { ok: true },
logs: 'test logs'
}))
}),
FlowService: wrapService(actual.FlowService, {
existsFlowByPath: vi.fn(async () => false),
createFlow: vi.fn(async () => 'created'),
@@ -124,6 +143,7 @@ import {
AppService,
FlowService,
HttpTriggerService,
JobService,
ResourceService,
ScheduleService,
ScriptService,
@@ -170,6 +190,17 @@ function localStorageSnapshot(): string {
return values.join('\n')
}
async function withCompletedTestJob<T>(run: () => Promise<T>): Promise<T> {
vi.useFakeTimers()
try {
const promise = run()
await vi.advanceTimersByTimeAsync(1000)
return await promise
} finally {
vi.useRealTimers()
}
}
describe('global AI tools', () => {
beforeEach(() => {
__resetUserDraftForTesting()
@@ -178,6 +209,41 @@ describe('global AI tools', () => {
vi.clearAllMocks()
})
it('exposes hub search and path-aware test tools', () => {
const names = globalTools.map((tool) => tool.def.function.name)
expect(names).toContain('search_hub_scripts')
expect(names).toContain('test_run_script')
expect(names).toContain('test_run_flow')
expect(names).toContain('test_run_step')
})
it('searches hub scripts without fetching script contents', async () => {
vi.mocked(ScriptService.queryHubScripts).mockResolvedValueOnce([
{
version_id: 7,
app: 'slack',
summary: 'Send Message'
}
] as any)
const raw = await callGlobalTool('search_hub_scripts', {
query: 'slack message'
})
expect(ScriptService.queryHubScripts).toHaveBeenCalledWith({
text: 'slack message',
kind: 'script'
})
expect(ScriptService.getHubScriptContentByPath).not.toHaveBeenCalled()
expect(JSON.parse(raw)).toEqual([
{
path: 'hub/7/slack/send_message',
summary: 'Send Message'
}
])
})
it('redacts variable draft values when reading workspace items', async () => {
await callGlobalTool('write_variable', {
path: 'f/secrets/api_key',
@@ -1266,6 +1332,284 @@ describe('global AI tools', () => {
expect(item.value.value).toBeUndefined()
})
it('test_run_script previews local draft script content by path', async () => {
const content = 'export async function main(name: string) {\n\treturn `hello ${name}`\n}'
await callGlobalTool('write_script', {
path: 'f/scripts/draft-test',
summary: 'Draft test script',
language: 'bun',
content
})
const result = await withCompletedTestJob(() =>
callGlobalTool('test_run_script', {
path: 'f/scripts/draft-test',
args: { name: 'Ada' }
})
)
expect(JobService.runScriptPreview).toHaveBeenCalledWith({
workspace: WORKSPACE,
requestBody: {
path: 'f/scripts/draft-test',
content,
args: { name: 'Ada' },
language: 'bun'
}
})
expect(ScriptService.getScriptByPath).not.toHaveBeenCalled()
expect(result).toContain('Result (SUCCESS)')
expect(result).toContain('test logs')
})
it('test_run_script previews deployed script content when no local draft exists', async () => {
vi.mocked(ScriptService.getScriptByPath).mockResolvedValueOnce({
path: 'f/scripts/deployed-test',
summary: 'Deployed test script',
content: 'def main(name):\n return name',
language: 'python3'
} as any)
await withCompletedTestJob(() =>
callGlobalTool('test_run_script', {
path: 'f/scripts/deployed-test',
args: { name: 'Grace' }
})
)
expect(ScriptService.getScriptByPath).toHaveBeenCalledWith({
workspace: WORKSPACE,
path: 'f/scripts/deployed-test'
})
expect(JobService.runScriptPreview).toHaveBeenCalledWith({
workspace: WORKSPACE,
requestBody: {
path: 'f/scripts/deployed-test',
content: 'def main(name):\n return name',
args: { name: 'Grace' },
language: 'python3'
}
})
})
it('test_run_flow previews local draft flow content by path', async () => {
const modules = [{ id: 'start', value: { type: 'identity' } }]
await callGlobalTool('write_flow', {
path: 'f/flows/draft-test',
summary: 'Draft test flow',
modules: JSON.stringify(modules)
})
await withCompletedTestJob(() =>
callGlobalTool('test_run_flow', {
path: 'f/flows/draft-test',
args: { name: 'Ada' }
})
)
expect(FlowService.getFlowByPath).not.toHaveBeenCalled()
expect(JobService.runFlowPreview).toHaveBeenCalledWith({
workspace: WORKSPACE,
requestBody: {
path: 'f/flows/draft-test',
value: { modules },
args: { name: 'Ada' }
}
})
})
it('test_run_flow previews deployed flow content when no local draft exists', async () => {
const modules = [{ id: 'deployed_start', value: { type: 'identity' } }]
vi.mocked(FlowService.getFlowByPath).mockResolvedValueOnce({
path: 'f/flows/deployed-test',
summary: 'Deployed test flow',
value: { modules },
schema: {}
} as any)
await withCompletedTestJob(() =>
callGlobalTool('test_run_flow', {
path: 'f/flows/deployed-test',
args: { name: 'Grace' }
})
)
expect(FlowService.getFlowByPath).toHaveBeenCalledWith({
workspace: WORKSPACE,
path: 'f/flows/deployed-test'
})
expect(JobService.runFlowPreview).toHaveBeenCalledWith({
workspace: WORKSPACE,
requestBody: {
path: 'f/flows/deployed-test',
value: { modules },
args: { name: 'Grace' }
}
})
})
it('test_run_flow uses the live flow editor test hook when the active editor matches the path', async () => {
UserDraft.save(
'flow',
'',
{
path: 'u/admin/live_flow',
summary: 'Live flow',
value: { modules: [{ id: 'live_step', value: { type: 'identity' } }] },
schema: {},
edited_by: '',
edited_at: '',
archived: false,
extra_perms: {}
},
{ workspace: WORKSPACE }
)
UserDraft.setLiveEditorDraft({
workspace: WORKSPACE,
itemKind: 'flow',
storagePath: '',
effectivePath: 'u/admin/live_flow'
})
const testActiveFlow = vi.fn(async () => 'job-live-flow')
const result = await withCompletedTestJob(() =>
callGlobalTool(
'test_run_flow',
{
path: 'u/admin/live_flow',
args: { name: 'Ada' }
},
toolCallbacks,
{ testActiveFlow }
)
)
expect(testActiveFlow).toHaveBeenCalledWith({ name: 'Ada' })
expect(FlowService.getFlowByPath).not.toHaveBeenCalled()
expect(JobService.runFlowPreview).not.toHaveBeenCalled()
expect(result).toContain('Result (SUCCESS)')
})
it('test_run_step previews rawscript steps from the local draft flow', async () => {
const content = 'export async function main(name: string) {\n\treturn name.toUpperCase()\n}'
await callGlobalTool('write_flow', {
path: 'f/flows/rawscript-step',
summary: 'Flow with rawscript',
modules: JSON.stringify([
{
id: 'format_name',
value: {
type: 'rawscript',
language: 'bun',
content,
input_transforms: {}
}
}
])
})
await withCompletedTestJob(() =>
callGlobalTool('test_run_step', {
path: 'f/flows/rawscript-step',
stepId: 'format_name',
args: { name: 'Ada' }
})
)
expect(JobService.runScriptPreview).toHaveBeenCalledWith({
workspace: WORKSPACE,
requestBody: {
content,
language: 'bun',
args: { name: 'Ada' }
}
})
})
it('test_run_step prefers local script drafts for script steps', async () => {
const content = 'export async function main(name: string) {\n\treturn `draft ${name}`\n}'
await callGlobalTool('write_script', {
path: 'f/scripts/step-script',
summary: 'Step script',
language: 'bun',
content
})
await callGlobalTool('write_flow', {
path: 'f/flows/script-step',
summary: 'Flow with script step',
modules: JSON.stringify([
{
id: 'call_script',
value: {
type: 'script',
path: 'f/scripts/step-script',
input_transforms: {}
}
}
])
})
await withCompletedTestJob(() =>
callGlobalTool('test_run_step', {
path: 'f/flows/script-step',
stepId: 'call_script',
args: { name: 'Ada' }
})
)
expect(ScriptService.getScriptByPath).not.toHaveBeenCalled()
expect(JobService.runScriptPreview).toHaveBeenCalledWith({
workspace: WORKSPACE,
requestBody: {
path: 'f/scripts/step-script',
content,
language: 'bun',
args: { name: 'Ada' }
}
})
})
it('test_run_step previews local draft subflows for flow steps', async () => {
const nestedModules = [{ id: 'nested_start', value: { type: 'identity' } }]
await callGlobalTool('write_flow', {
path: 'f/flows/nested-draft',
summary: 'Nested draft flow',
modules: JSON.stringify(nestedModules)
})
await callGlobalTool('write_flow', {
path: 'f/flows/parent-flow',
summary: 'Parent flow',
modules: JSON.stringify([
{
id: 'call_flow',
value: {
type: 'flow',
path: 'f/flows/nested-draft',
input_transforms: {}
}
}
])
})
await withCompletedTestJob(() =>
callGlobalTool('test_run_step', {
path: 'f/flows/parent-flow',
stepId: 'call_flow',
args: { name: 'Ada' }
})
)
expect(JobService.runFlowByPath).not.toHaveBeenCalled()
expect(JobService.runFlowPreview).toHaveBeenCalledWith({
workspace: WORKSPACE,
requestBody: {
path: 'f/flows/nested-draft',
value: { modules: nestedModules },
args: { name: 'Ada' }
}
})
})
it('asks the user a question and returns the selected answer', async () => {
const callbacks: ToolCallbacks = {
setToolStatus: vi.fn(),
@@ -1384,6 +1728,9 @@ describe('prepareGlobalSystemMessage', () => {
expect(content).toContain(
'Use discard_local_draft to remove an unsaved local draft, including the matching open editor draft'
)
expect(content).toContain(
'After creating or editing a script or flow draft, run test_run_script, test_run_flow, or test_run_step'
)
expect(content).toContain('If the user message includes an ACTIVE EDITOR section')
expect(content).not.toContain('AI draft')
expect(content).not.toContain('UserDraft')
@@ -4,6 +4,7 @@ import {
FlowService,
GcpTriggerService,
HttpTriggerService,
JobService,
KafkaTriggerService,
MqttTriggerService,
NatsTriggerService,
@@ -55,6 +56,9 @@ import type {
import { z } from 'zod'
import {
createToolDef,
createSearchHubScriptsTool,
executeFlowStepTestRun,
executeTestRun,
findAndReplace,
type CreatedResourceTriggerKind,
type Tool,
@@ -387,6 +391,49 @@ const patchFlowJsonSchema = z.object({
)
})
const testRunArgsSchema = z
.record(z.string(), z.any())
.nullable()
.optional()
.describe('Arguments to pass to the runnable. Omit or pass null when no arguments are needed.')
const testRunScriptSchema = z.object({
path: z.string().describe('Workspace path of the script to test.'),
args: testRunArgsSchema
})
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.',
{ strict: false }
)
const testRunFlowSchema = z.object({
path: z.string().describe('Workspace path of the flow to test.'),
args: testRunArgsSchema
})
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.',
{ strict: false }
)
const testRunStepSchema = z.object({
path: z.string().describe('Workspace path of the flow containing the step to test.'),
stepId: z.string().describe('The id of the step/module to test.'),
args: testRunArgsSchema
})
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.',
{ strict: false }
)
// ============= App tools (raw apps) =============
const backendRunnableSchema = z
@@ -553,6 +600,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.
- 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
@@ -1332,6 +1380,7 @@ export const globalTools: Tool<{}>[] = [
return getInstructions(parsed.subject, parsed.language)
}
},
createSearchHubScriptsTool(false),
{
def: createToolDef(
askUserQuestionSchema,
@@ -1556,6 +1605,39 @@ export const globalTools: Tool<{}>[] = [
return patchFlowJson(parsed, ctx)
}
},
{
def: testRunScriptToolDef,
fn: async (ctx) => {
const parsed = testRunScriptSchema.parse(ctx.args)
return testRunScriptByPath(parsed, ctx)
},
requiresConfirmation: true,
confirmationMessage: 'Run script test',
showDetails: true,
autoCollapseDetails: false
},
{
def: testRunFlowToolDef,
fn: async (ctx) => {
const parsed = testRunFlowSchema.parse(ctx.args)
return testRunFlowByPath(parsed, ctx)
},
requiresConfirmation: true,
confirmationMessage: 'Run flow test',
showDetails: true,
autoCollapseDetails: false
},
{
def: testRunStepToolDef,
fn: async (ctx) => {
const parsed = testRunStepSchema.parse(ctx.args)
return testRunFlowStepByPath(parsed, ctx)
},
requiresConfirmation: true,
confirmationMessage: 'Run flow step test',
showDetails: true,
autoCollapseDetails: false
},
{
def: createToolDef(
deployWorkspaceItemSchema,
@@ -1834,9 +1916,23 @@ type WriteDraftCtx = {
// handlers below would route a backgrounded session's tool call to whatever
// session the user happens to be viewing.
export type SessionToolHelpers = { sessionId?: string }
export type GlobalToolHelpers = SessionToolHelpers & {
testActiveFlow?: (args?: Record<string, any>) => Promise<string | undefined>
}
function sessionIdFromCtx(ctx: { helpers?: unknown }): string | undefined {
return (ctx.helpers as SessionToolHelpers | undefined)?.sessionId
return (ctx.helpers as GlobalToolHelpers | undefined)?.sessionId
}
function activeFlowTestFromCtx(
ctx: { workspace: string; helpers?: unknown },
path: string
): GlobalToolHelpers['testActiveFlow'] | undefined {
const activeEditor = getActiveGlobalEditorContext(ctx.workspace)
if (activeEditor?.type !== 'flow' || activeEditor.path !== path) {
return undefined
}
return (ctx.helpers as GlobalToolHelpers | undefined)?.testActiveFlow
}
export type OpenPreviewHandler = (req: {
@@ -2468,6 +2564,136 @@ async function setFlowModuleCode(
)
}
function normalizeTestRunArgs(args: Record<string, any> | null | undefined): Record<string, any> {
return args ?? {}
}
function flowDraftValueForPreview(flowDraft: FlowDraftValue): FlowValue {
return flowDraftAsEditableInput(flowDraft).value
}
async function loadScriptForFlowStep(
moduleValue: { path: string; hash?: string },
workspace: string
): Promise<{ content: string; language: ScriptLang }> {
const draft = getGlobalDraft(workspace, 'script', moduleValue.path)
if (draft) {
if (typeof draft.value !== 'string' || !draft.language) {
throw new Error(`Draft script "${moduleValue.path}" is missing content or language.`)
}
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 })
return { content: script.content, language: script.language }
}
async function loadDraftFlowPreviewValue(
path: string,
workspace: string
): Promise<FlowValue | undefined> {
if (!getGlobalDraft(workspace, 'flow', path)) {
return undefined
}
const nestedFlow = await loadFlowDraftValue(path, workspace)
return flowDraftValueForPreview(nestedFlow.flow)
}
async function testRunScriptByPath(
args: z.infer<typeof testRunScriptSchema>,
ctx: WriteDraftCtx
): Promise<string> {
const { workspace, toolId, toolCallbacks } = ctx
const script = await loadScriptForEdit(args.path, workspace)
const testArgs = normalizeTestRunArgs(args.args)
return executeTestRun({
jobStarter: () =>
JobService.runScriptPreview({
workspace,
requestBody: {
path: args.path,
content: script.content,
args: testArgs,
language: script.language
}
}),
workspace,
toolCallbacks,
toolId,
startMessage: `Running test for script "${args.path}"...`,
contextName: 'script'
})
}
async function testRunFlowByPath(
args: z.infer<typeof testRunFlowSchema>,
ctx: WriteDraftCtx
): Promise<string> {
const { workspace, toolId, toolCallbacks } = ctx
const testArgs = normalizeTestRunArgs(args.args)
const testActiveFlow = activeFlowTestFromCtx(ctx, args.path)
if (testActiveFlow) {
return executeTestRun({
jobStarter: async () => {
const jobId = await testActiveFlow(testArgs)
if (!jobId) {
throw new Error('Failed to start test run - active flow editor returned undefined')
}
return jobId
},
workspace,
toolCallbacks,
toolId,
startMessage: `Starting live editor flow test run for "${args.path}"...`,
contextName: 'flow'
})
}
const flow = await loadFlowDraftValue(args.path, workspace)
return executeTestRun({
jobStarter: () =>
JobService.runFlowPreview({
workspace,
requestBody: {
path: args.path,
value: flowDraftValueForPreview(flow.flow),
args: testArgs
}
}),
workspace,
toolCallbacks,
toolId,
startMessage: `Starting flow test run for "${args.path}"...`,
contextName: 'flow'
})
}
async function testRunFlowStepByPath(
args: z.infer<typeof testRunStepSchema>,
ctx: WriteDraftCtx
): Promise<string> {
const { workspace, toolId, toolCallbacks } = ctx
const flow = await loadFlowDraftValue(args.path, workspace)
const flowValue = flowDraftValueForPreview(flow.flow)
const testArgs = normalizeTestRunArgs(args.args)
return executeFlowStepTestRun({
flowValue,
stepId: args.stepId,
args: testArgs,
workspace,
toolCallbacks,
toolId,
loadScript: loadScriptForFlowStep,
loadFlowPreviewValue: loadDraftFlowPreviewValue
})
}
async function initApp(
args: {
path: string
@@ -3203,7 +3429,8 @@ export function prepareGlobalUserMessage(
(context) => context.type === 'workspace_script' || context.type === 'workspace_flow'
)
const activeEditor =
options.activeEditor ?? (options.workspace ? getActiveGlobalEditorContext(options.workspace) : undefined)
options.activeEditor ??
(options.workspace ? getActiveGlobalEditorContext(options.workspace) : undefined)
let content = ''
if (activeEditor) {
@@ -19,7 +19,7 @@ import { get } from 'svelte/store'
import type { CodePieceElement, ContextElement, FlowModuleCodePieceElement } from './context'
import { workspaceStore } from '$lib/stores'
import type { ExtendedOpenFlow } from '$lib/components/flows/types'
import { findModuleInModules } from '$lib/components/flows/flowTree'
import { findModuleInFlow, findModuleInModules } from '$lib/components/flows/flowTree'
import type { FunctionParameters } from 'openai/resources/shared.mjs'
import { z } from 'zod'
import {
@@ -27,7 +27,9 @@ import {
FlowService,
JobService,
type CompletedJob,
type FlowValue,
type FlowModule,
type ScriptLang,
type Script,
type Flow
} from '$lib/gen'
@@ -1082,6 +1084,162 @@ export async function executeTestRun(config: TestRunConfig): Promise<string> {
}
}
type FlowStepScriptLoader = (
moduleValue: { path: string; hash?: string },
workspace: string
) => Promise<{ content: string; language: ScriptLang }>
type FlowStepPreviewLoader = (path: string, workspace: string) => Promise<FlowValue | undefined>
export type FlowStepTestRunConfig = {
flowValue: FlowValue
stepId: string
args?: Record<string, any> | null
workspace: string
toolCallbacks: ToolCallbacks
toolId: string
loadScript?: FlowStepScriptLoader
loadFlowPreviewValue?: FlowStepPreviewLoader
}
function normalizeFlowStepArgs(args: Record<string, any> | null | undefined): Record<string, any> {
return args ?? {}
}
function flowStepArgsForModule(moduleId: string, args: Record<string, any>): Record<string, any> {
return moduleId === SPECIAL_MODULE_IDS.PREPROCESSOR
? { _ENTRYPOINT_OVERRIDE: 'preprocessor', ...args }
: args
}
function getAvailableFlowStepIds(flowValue: FlowValue): string {
return [
...(flowValue.modules ?? []).map((module: FlowModule) => module.id),
...(flowValue.preprocessor_module ? [flowValue.preprocessor_module.id] : []),
...(flowValue.failure_module ? [flowValue.failure_module.id] : [])
].join(', ')
}
async function loadDeployedScriptForFlowStep(
moduleValue: { path: string; hash?: string },
workspace: string
): Promise<{ content: string; language: ScriptLang }> {
const script = moduleValue.hash
? await ScriptService.getScriptByHash({ workspace, hash: moduleValue.hash })
: await ScriptService.getScriptByPath({ workspace, path: moduleValue.path })
return { content: script.content, language: script.language }
}
export async function executeFlowStepTestRun({
flowValue,
stepId,
args,
workspace,
toolCallbacks,
toolId,
loadScript = loadDeployedScriptForFlowStep,
loadFlowPreviewValue
}: FlowStepTestRunConfig): Promise<string> {
const targetModule = findModuleInFlow(flowValue, stepId) ?? undefined
if (!targetModule) {
toolCallbacks.setToolStatus(toolId, {
content: `Step "${stepId}" not found in flow`,
error: `Step with id "${stepId}" does not exist in the current flow`
})
throw new Error(
`Step with id "${stepId}" not found in flow. Available steps: ${getAvailableFlowStepIds(flowValue)}`
)
}
const moduleValue = targetModule.value
const stepArgs = normalizeFlowStepArgs(args)
if (moduleValue.type === 'rawscript') {
return executeTestRun({
jobStarter: () =>
JobService.runScriptPreview({
workspace,
requestBody: {
content: moduleValue.content ?? '',
language: moduleValue.language,
args: flowStepArgsForModule(targetModule.id, stepArgs)
}
}),
workspace,
toolCallbacks,
toolId,
startMessage: `Starting test run of step "${stepId}"...`,
contextName: 'script'
})
}
if (moduleValue.type === 'script') {
const script = await loadScript(moduleValue, workspace)
return executeTestRun({
jobStarter: () =>
JobService.runScriptPreview({
workspace,
requestBody: {
path: moduleValue.path,
content: script.content,
language: script.language,
args: flowStepArgsForModule(targetModule.id, stepArgs)
}
}),
workspace,
toolCallbacks,
toolId,
startMessage: `Starting test run of script step "${stepId}"...`,
contextName: 'script'
})
}
if (moduleValue.type === 'flow') {
const previewValue = await loadFlowPreviewValue?.(moduleValue.path, workspace)
if (previewValue) {
return executeTestRun({
jobStarter: () =>
JobService.runFlowPreview({
workspace,
requestBody: {
path: moduleValue.path,
value: previewValue,
args: stepArgs
}
}),
workspace,
toolCallbacks,
toolId,
startMessage: `Starting test run of draft flow step "${stepId}"...`,
contextName: 'flow'
})
}
return executeTestRun({
jobStarter: () =>
JobService.runFlowByPath({
workspace,
path: moduleValue.path,
requestBody: stepArgs
}),
workspace,
toolCallbacks,
toolId,
startMessage: `Starting test run of flow step "${stepId}"...`,
contextName: 'flow'
})
}
toolCallbacks.setToolStatus(toolId, {
content: `Step type "${moduleValue.type}" not supported for testing`,
error: `Cannot test step of type "${moduleValue.type}"`
})
throw new Error(
`Cannot test step of type "${moduleValue.type}". Supported types: rawscript, script, flow`
)
}
function formatLogs(logs: string | undefined): undefined | string {
if (logs && logs.trim()) {
if (logs.length <= MAX_LOG_LENGTH) {