fix: resolve TypeScript errors in goto-anything tests and workflow (#32122)

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
qiuqiua
2026-02-09 15:12:32 +08:00
committed by crazywoola
parent 481c707fab
commit 75d3e0c790
9 changed files with 205 additions and 123 deletions

View File

@@ -709,6 +709,17 @@ def parse_vibe_response(content: str) -> dict[str, Any]:
"raw_content": content[:500], # First 500 chars for debugging
}
# Handle double-encoded JSON (when json.loads returns a string)
if isinstance(data, str):
try:
data = json.loads(data)
except json.JSONDecodeError:
return {
"intent": "error",
"error": "Failed to parse double-encoded JSON",
"raw_content": data[:500],
}
# Validate and normalize
if "intent" not in data:
data["intent"] = "generate" # Default assumption

View File

@@ -6,7 +6,11 @@ from collections.abc import Sequence
import json_repair
from core.model_manager import ModelManager
from core.model_runtime.entities.message_entities import SystemPromptMessage, UserPromptMessage
from core.model_runtime.entities.message_entities import (
SystemPromptMessage,
TextPromptMessageContent,
UserPromptMessage,
)
from core.model_runtime.entities.model_entities import ModelType
from core.workflow.generator.prompts.builder_prompts import (
BUILDER_SYSTEM_PROMPT,
@@ -105,7 +109,31 @@ class WorkflowGenerator:
model_parameters=model_parameters,
stream=False,
)
# Extract text content from response
plan_content = response.message.content
if isinstance(plan_content, list):
# Extract text from content list
text_parts = []
for content in plan_content:
if isinstance(content, TextPromptMessageContent):
text_parts.append(content.data)
plan_content = "".join(text_parts)
elif plan_content is None:
plan_content = ""
# Check if LLM returned empty content
if not plan_content or not plan_content.strip():
usage = response.usage if hasattr(response, "usage") else "N/A"
logger.error("LLM returned empty content. Usage: %s", usage)
return {
"intent": "error",
"error": (
"LLM model returned empty response. This may indicate: "
"(1) Model refusal/content policy, (2) Model configuration issue, "
"(3) Plugin communication error. Try a different model or check model settings."
),
}
# Reuse parse_vibe_response logic or simple load
plan_data = parse_vibe_response(plan_content)
except Exception as e:
@@ -212,13 +240,52 @@ class WorkflowGenerator:
stream=False,
)
# Builder output is raw JSON nodes/edges
# Extract text content from response
build_content = build_res.message.content
if isinstance(build_content, list):
# Extract text from content list
text_parts = []
for content in build_content:
if isinstance(content, TextPromptMessageContent):
text_parts.append(content.data)
build_content = "".join(text_parts)
elif build_content is None:
build_content = ""
match = re.search(r"```(?:json)?\s*([\s\S]+?)```", build_content)
if match:
build_content = match.group(1)
# Check if LLM returned empty content
if not build_content or not build_content.strip():
usage = build_res.usage if hasattr(build_res, "usage") else "N/A"
logger.error("Builder LLM returned empty content. Usage: %s", usage)
raise ValueError(
"LLM model returned empty response. This may indicate: "
"(1) Model refusal/content policy, (2) Model configuration issue, "
"(3) Plugin communication error. Try a different model or check model settings."
)
workflow_data = json_repair.loads(build_content)
# Handle double-encoded JSON (when json_repair.loads returns a string)
# Keep decoding until we get a dict
max_decode_attempts = 3
decode_attempts = 0
while isinstance(workflow_data, str) and decode_attempts < max_decode_attempts:
workflow_data = json_repair.loads(workflow_data)
decode_attempts += 1
# If still a string, it's not valid JSON structure
if not isinstance(workflow_data, dict):
logger.error(
"workflow_data is not a dict after %s decode attempts. Type: %s, Value preview: %s",
decode_attempts,
type(workflow_data),
str(workflow_data)[:200],
)
raise ValueError(f"Expected dict, got {type(workflow_data).__name__}")
if "nodes" not in workflow_data:
workflow_data["nodes"] = []

View File

@@ -1,4 +1,4 @@
import type { ActionItem } from '../../app/components/goto-anything/actions/types'
import type { ScopeDescriptor } from '../../app/components/goto-anything/actions/types'
import { fireEvent, render, screen } from '@testing-library/react'
import * as React from 'react'
import CommandSelector from '../../app/components/goto-anything/command-selector'
@@ -20,36 +20,37 @@ vi.mock('cmdk', () => ({
}))
describe('CommandSelector', () => {
const mockActions: Record<string, ActionItem> = {
app: {
key: '@app',
const mockScopes: ScopeDescriptor[] = [
{
id: 'app',
shortcut: '@app',
title: 'Search Applications',
description: 'Search apps',
search: vi.fn(),
},
knowledge: {
key: '@knowledge',
{
id: 'knowledge',
shortcut: '@kb',
aliases: ['@knowledge'],
title: 'Search Knowledge',
description: 'Search knowledge bases',
search: vi.fn(),
},
plugin: {
key: '@plugin',
{
id: 'plugin',
shortcut: '@plugin',
title: 'Search Plugins',
description: 'Search plugins',
search: vi.fn(),
},
node: {
key: '@node',
{
id: 'node',
shortcut: '@node',
title: 'Search Nodes',
description: 'Search workflow nodes',
search: vi.fn(),
},
}
]
const mockOnCommandSelect = vi.fn()
const mockOnCommandValueChange = vi.fn()
@@ -62,7 +63,7 @@ describe('CommandSelector', () => {
it('should render all actions when no filter is provided', () => {
render(
<CommandSelector
actions={mockActions}
scopes={mockScopes}
onCommandSelect={mockOnCommandSelect}
/>,
)
@@ -76,7 +77,7 @@ describe('CommandSelector', () => {
it('should render empty filter as showing all actions', () => {
render(
<CommandSelector
actions={mockActions}
scopes={mockScopes}
onCommandSelect={mockOnCommandSelect}
searchFilter=""
/>,
@@ -93,7 +94,7 @@ describe('CommandSelector', () => {
it('should filter actions based on searchFilter - single match', () => {
render(
<CommandSelector
actions={mockActions}
scopes={mockScopes}
onCommandSelect={mockOnCommandSelect}
searchFilter="k"
/>,
@@ -108,7 +109,7 @@ describe('CommandSelector', () => {
it('should filter actions with multiple matches', () => {
render(
<CommandSelector
actions={mockActions}
scopes={mockScopes}
onCommandSelect={mockOnCommandSelect}
searchFilter="p"
/>,
@@ -123,7 +124,7 @@ describe('CommandSelector', () => {
it('should be case-insensitive when filtering', () => {
render(
<CommandSelector
actions={mockActions}
scopes={mockScopes}
onCommandSelect={mockOnCommandSelect}
searchFilter="APP"
/>,
@@ -136,7 +137,7 @@ describe('CommandSelector', () => {
it('should match partial strings', () => {
render(
<CommandSelector
actions={mockActions}
scopes={mockScopes}
onCommandSelect={mockOnCommandSelect}
searchFilter="od"
/>,
@@ -153,7 +154,7 @@ describe('CommandSelector', () => {
it('should show empty state when no matches found', () => {
render(
<CommandSelector
actions={mockActions}
scopes={mockScopes}
onCommandSelect={mockOnCommandSelect}
searchFilter="xyz"
/>,
@@ -171,7 +172,7 @@ describe('CommandSelector', () => {
it('should not show empty state when filter is empty', () => {
render(
<CommandSelector
actions={mockActions}
scopes={mockScopes}
onCommandSelect={mockOnCommandSelect}
searchFilter=""
/>,
@@ -185,7 +186,7 @@ describe('CommandSelector', () => {
it('should call onCommandValueChange when filter changes and first item differs', () => {
const { rerender } = render(
<CommandSelector
actions={mockActions}
scopes={mockScopes}
onCommandSelect={mockOnCommandSelect}
searchFilter=""
commandValue="@app"
@@ -195,7 +196,7 @@ describe('CommandSelector', () => {
rerender(
<CommandSelector
actions={mockActions}
scopes={mockScopes}
onCommandSelect={mockOnCommandSelect}
searchFilter="k"
commandValue="@app"
@@ -209,7 +210,7 @@ describe('CommandSelector', () => {
it('should not call onCommandValueChange if current value still exists', () => {
const { rerender } = render(
<CommandSelector
actions={mockActions}
scopes={mockScopes}
onCommandSelect={mockOnCommandSelect}
searchFilter=""
commandValue="@app"
@@ -219,7 +220,7 @@ describe('CommandSelector', () => {
rerender(
<CommandSelector
actions={mockActions}
scopes={mockScopes}
onCommandSelect={mockOnCommandSelect}
searchFilter="a"
commandValue="@app"
@@ -233,7 +234,7 @@ describe('CommandSelector', () => {
it('should handle onCommandSelect callback correctly', () => {
render(
<CommandSelector
actions={mockActions}
scopes={mockScopes}
onCommandSelect={mockOnCommandSelect}
searchFilter="k"
/>,
@@ -250,7 +251,7 @@ describe('CommandSelector', () => {
it('should handle empty actions object', () => {
render(
<CommandSelector
actions={{}}
scopes={[]}
onCommandSelect={mockOnCommandSelect}
searchFilter=""
/>,
@@ -262,7 +263,7 @@ describe('CommandSelector', () => {
it('should handle special characters in filter', () => {
render(
<CommandSelector
actions={mockActions}
scopes={mockScopes}
onCommandSelect={mockOnCommandSelect}
searchFilter="@"
/>,
@@ -277,7 +278,7 @@ describe('CommandSelector', () => {
it('should handle undefined onCommandValueChange gracefully', () => {
const { rerender } = render(
<CommandSelector
actions={mockActions}
scopes={mockScopes}
onCommandSelect={mockOnCommandSelect}
searchFilter=""
/>,
@@ -286,7 +287,7 @@ describe('CommandSelector', () => {
expect(() => {
rerender(
<CommandSelector
actions={mockActions}
scopes={mockScopes}
onCommandSelect={mockOnCommandSelect}
searchFilter="k"
/>,
@@ -299,7 +300,7 @@ describe('CommandSelector', () => {
it('should work without searchFilter prop (backward compatible)', () => {
render(
<CommandSelector
actions={mockActions}
scopes={mockScopes}
onCommandSelect={mockOnCommandSelect}
/>,
)
@@ -313,7 +314,7 @@ describe('CommandSelector', () => {
it('should work without commandValue and onCommandValueChange props', () => {
render(
<CommandSelector
actions={mockActions}
scopes={mockScopes}
onCommandSelect={mockOnCommandSelect}
searchFilter="k"
/>,

View File

@@ -1,5 +1,5 @@
import type { Mock } from 'vitest'
import type { ActionItem } from '../../app/components/goto-anything/actions/types'
import type { ScopeDescriptor } from '../../app/components/goto-anything/actions/types'
// Import after mocking to get mocked version
import { matchAction } from '../../app/components/goto-anything/actions'
@@ -13,10 +13,11 @@ vi.mock('../../app/components/goto-anything/actions', () => ({
vi.mock('../../app/components/goto-anything/actions/commands/registry')
// Implement the actual matchAction logic for testing
const actualMatchAction = (query: string, actions: Record<string, ActionItem>) => {
const result = Object.values(actions).find((action) => {
const actualMatchAction = (query: string, scopes: ScopeDescriptor[]) => {
const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
return scopes.find((scope) => {
// Special handling for slash commands
if (action.key === '/') {
if (scope.id === 'slash' || scope.shortcut === '/') {
// Get all registered commands from the registry
const allCommands = slashCommandRegistry.getAllCommands()
@@ -33,39 +34,41 @@ const actualMatchAction = (query: string, actions: Record<string, ActionItem>) =
})
}
const reg = new RegExp(`^(${action.key}|${action.shortcut})(?:\\s|$)`)
const shortcuts = [scope.shortcut, ...(scope.aliases || [])].map(escapeRegExp)
const reg = new RegExp(`^(${shortcuts.join('|')})(?:\\s|$)`)
return reg.test(query)
})
return result
}
// Replace mock with actual implementation
;(matchAction as Mock).mockImplementation(actualMatchAction)
describe('matchAction Logic', () => {
const mockActions: Record<string, ActionItem> = {
app: {
key: '@app',
shortcut: '@a',
const mockScopes: ScopeDescriptor[] = [
{
id: 'app',
shortcut: '@app',
aliases: ['@a'],
title: 'Search Applications',
description: 'Search apps',
search: vi.fn(),
},
knowledge: {
key: '@knowledge',
{
id: 'knowledge',
shortcut: '@kb',
aliases: ['@knowledge'],
title: 'Search Knowledge',
description: 'Search knowledge bases',
search: vi.fn(),
},
slash: {
key: '/',
{
id: 'slash',
shortcut: '/',
title: 'Commands',
description: 'Execute commands',
search: vi.fn(),
},
}
]
beforeEach(() => {
vi.clearAllMocks()
@@ -81,32 +84,32 @@ describe('matchAction Logic', () => {
describe('@ Actions Matching', () => {
it('should match @app with key', () => {
const result = matchAction('@app', mockActions)
expect(result).toBe(mockActions.app)
const result = matchAction('@app', mockScopes)
expect(result).toBe(mockScopes[0])
})
it('should match @app with shortcut', () => {
const result = matchAction('@a', mockActions)
expect(result).toBe(mockActions.app)
const result = matchAction('@a', mockScopes)
expect(result).toBe(mockScopes[0])
})
it('should match @knowledge with key', () => {
const result = matchAction('@knowledge', mockActions)
expect(result).toBe(mockActions.knowledge)
const result = matchAction('@knowledge', mockScopes)
expect(result).toBe(mockScopes[1])
})
it('should match @knowledge with shortcut @kb', () => {
const result = matchAction('@kb', mockActions)
expect(result).toBe(mockActions.knowledge)
const result = matchAction('@kb', mockScopes)
expect(result).toBe(mockScopes[1])
})
it('should match with text after action', () => {
const result = matchAction('@app search term', mockActions)
expect(result).toBe(mockActions.app)
const result = matchAction('@app search term', mockScopes)
expect(result).toBe(mockScopes[0])
})
it('should not match partial @ actions', () => {
const result = matchAction('@ap', mockActions)
const result = matchAction('@ap', mockScopes)
expect(result).toBeUndefined()
})
})
@@ -114,47 +117,47 @@ describe('matchAction Logic', () => {
describe('Slash Commands Matching', () => {
describe('Direct Mode Commands', () => {
it('should not match direct mode commands', () => {
const result = matchAction('/docs', mockActions)
const result = matchAction('/docs', mockScopes)
expect(result).toBeUndefined()
})
it('should not match direct mode with arguments', () => {
const result = matchAction('/docs something', mockActions)
const result = matchAction('/docs something', mockScopes)
expect(result).toBeUndefined()
})
it('should not match any direct mode command', () => {
expect(matchAction('/community', mockActions)).toBeUndefined()
expect(matchAction('/feedback', mockActions)).toBeUndefined()
expect(matchAction('/account', mockActions)).toBeUndefined()
expect(matchAction('/community', mockScopes)).toBeUndefined()
expect(matchAction('/feedback', mockScopes)).toBeUndefined()
expect(matchAction('/account', mockScopes)).toBeUndefined()
})
})
describe('Submenu Mode Commands', () => {
it('should match submenu mode commands exactly', () => {
const result = matchAction('/theme', mockActions)
expect(result).toBe(mockActions.slash)
const result = matchAction('/theme', mockScopes)
expect(result).toBe(mockScopes[2])
})
it('should match submenu mode with arguments', () => {
const result = matchAction('/theme dark', mockActions)
expect(result).toBe(mockActions.slash)
const result = matchAction('/theme dark', mockScopes)
expect(result).toBe(mockScopes[2])
})
it('should match all submenu commands', () => {
expect(matchAction('/language', mockActions)).toBe(mockActions.slash)
expect(matchAction('/language en', mockActions)).toBe(mockActions.slash)
expect(matchAction('/language', mockScopes)).toBe(mockScopes[2])
expect(matchAction('/language en', mockScopes)).toBe(mockScopes[2])
})
})
describe('Slash Without Command', () => {
it('should not match single slash', () => {
const result = matchAction('/', mockActions)
const result = matchAction('/', mockScopes)
expect(result).toBeUndefined()
})
it('should not match unregistered commands', () => {
const result = matchAction('/unknown', mockActions)
const result = matchAction('/unknown', mockScopes)
expect(result).toBeUndefined()
})
})
@@ -162,28 +165,28 @@ describe('matchAction Logic', () => {
describe('Edge Cases', () => {
it('should handle empty query', () => {
const result = matchAction('', mockActions)
const result = matchAction('', mockScopes)
expect(result).toBeUndefined()
})
it('should handle whitespace only', () => {
const result = matchAction(' ', mockActions)
const result = matchAction(' ', mockScopes)
expect(result).toBeUndefined()
})
it('should handle regular text without actions', () => {
const result = matchAction('search something', mockActions)
const result = matchAction('search something', mockScopes)
expect(result).toBeUndefined()
})
it('should handle special characters', () => {
const result = matchAction('#tag', mockActions)
const result = matchAction('#tag', mockScopes)
expect(result).toBeUndefined()
})
it('should handle multiple @ or /', () => {
expect(matchAction('@@app', mockActions)).toBeUndefined()
expect(matchAction('//theme', mockActions)).toBeUndefined()
expect(matchAction('@@app', mockScopes)).toBeUndefined()
expect(matchAction('//theme', mockScopes)).toBeUndefined()
})
})
@@ -193,7 +196,7 @@ describe('matchAction Logic', () => {
{ name: 'test', mode: 'direct' },
])
const result = matchAction('/test', mockActions)
const result = matchAction('/test', mockScopes)
expect(result).toBeUndefined()
})
@@ -202,8 +205,8 @@ describe('matchAction Logic', () => {
{ name: 'test', mode: 'submenu' },
])
const result = matchAction('/test', mockActions)
expect(result).toBe(mockActions.slash)
const result = matchAction('/test', mockScopes)
expect(result).toBe(mockScopes[2])
})
it('should treat undefined mode as submenu', () => {
@@ -211,25 +214,25 @@ describe('matchAction Logic', () => {
{ name: 'test' }, // No mode specified
])
const result = matchAction('/test', mockActions)
expect(result).toBe(mockActions.slash)
const result = matchAction('/test', mockScopes)
expect(result).toBe(mockScopes[2])
})
})
describe('Registry Integration', () => {
it('should call getAllCommands when matching slash', () => {
matchAction('/theme', mockActions)
matchAction('/theme', mockScopes)
expect(slashCommandRegistry.getAllCommands).toHaveBeenCalled()
})
it('should not call getAllCommands for @ actions', () => {
matchAction('@app', mockActions)
matchAction('@app', mockScopes)
expect(slashCommandRegistry.getAllCommands).not.toHaveBeenCalled()
})
it('should handle empty command list', () => {
;(slashCommandRegistry.getAllCommands as Mock).mockReturnValue([])
const result = matchAction('/anything', mockActions)
const result = matchAction('/anything', mockScopes)
expect(result).toBeUndefined()
})
})

View File

@@ -9,7 +9,7 @@ import type { MockedFunction } from 'vitest'
* 4. Ensure errors don't propagate to UI layer causing "search failed"
*/
import { Actions, searchAnything } from '@/app/components/goto-anything/actions'
import { appScope, knowledgeScope, pluginScope, searchAnything } from '@/app/components/goto-anything/actions'
import { fetchAppList } from '@/service/apps'
import { postMarketplace } from '@/service/base'
import { fetchDatasets } from '@/service/datasets'
@@ -57,10 +57,8 @@ describe('GotoAnything Search Error Handling', () => {
// Mock marketplace API failure (403 permission denied)
mockPostMarketplace.mockRejectedValue(new Error('HTTP 403: Forbidden'))
const pluginAction = Actions.plugin
// Directly call plugin action's search method
const result = await pluginAction.search('@plugin', 'test', 'en')
const result = await pluginScope.search('@plugin', 'test', 'en')
// Should return empty array instead of throwing error
expect(result).toEqual([])
@@ -80,8 +78,7 @@ describe('GotoAnything Search Error Handling', () => {
data: { plugins: [] },
})
const pluginAction = Actions.plugin
const result = await pluginAction.search('@plugin', '', 'en')
const result = await pluginScope.search('@plugin', '', 'en')
expect(result).toEqual([])
})
@@ -92,8 +89,7 @@ describe('GotoAnything Search Error Handling', () => {
data: null,
})
const pluginAction = Actions.plugin
const result = await pluginAction.search('@plugin', 'test', 'en')
const result = await pluginScope.search('@plugin', 'test', 'en')
expect(result).toEqual([])
})
@@ -104,8 +100,7 @@ describe('GotoAnything Search Error Handling', () => {
// Mock app API failure
mockFetchAppList.mockRejectedValue(new Error('API Error'))
const appAction = Actions.app
const result = await appAction.search('@app', 'test', 'en')
const result = await appScope.search('@app', 'test', 'en')
expect(result).toEqual([])
})
@@ -114,8 +109,7 @@ describe('GotoAnything Search Error Handling', () => {
// Mock knowledge API failure
mockFetchDatasets.mockRejectedValue(new Error('API Error'))
const knowledgeAction = Actions.knowledge
const result = await knowledgeAction.search('@knowledge', 'test', 'en')
const result = await knowledgeScope.search('@knowledge', 'test', 'en')
expect(result).toEqual([])
})
@@ -128,19 +122,20 @@ describe('GotoAnything Search Error Handling', () => {
mockFetchDatasets.mockResolvedValue({ data: [], has_more: false, limit: 10, page: 1, total: 0 })
mockPostMarketplace.mockRejectedValue(new Error('Plugin API failed'))
const result = await searchAnything('en', 'test')
const allScopes = [appScope, knowledgeScope, pluginScope]
const result = await searchAnything('en', 'test', undefined, allScopes)
// Should return successful results even if plugin search fails
expect(result).toEqual([])
expect(console.warn).toHaveBeenCalledWith('Plugin search failed:', expect.any(Error))
expect(console.warn).toHaveBeenCalled()
})
it('@plugin dedicated search should return empty array when API fails', async () => {
// Mock plugin API failure
mockPostMarketplace.mockRejectedValue(new Error('Plugin service unavailable'))
const pluginAction = Actions.plugin
const result = await searchAnything('en', '@plugin test', pluginAction)
const allScopes = [appScope, knowledgeScope, pluginScope]
const result = await searchAnything('en', '@plugin test', pluginScope, allScopes)
// Should return empty array instead of throwing error
expect(result).toEqual([])
@@ -150,8 +145,8 @@ describe('GotoAnything Search Error Handling', () => {
// Mock app API failure
mockFetchAppList.mockRejectedValue(new Error('App service unavailable'))
const appAction = Actions.app
const result = await searchAnything('en', '@app test', appAction)
const allScopes = [appScope, knowledgeScope, pluginScope]
const result = await searchAnything('en', '@app test', appScope, allScopes)
expect(result).toEqual([])
})
@@ -165,13 +160,13 @@ describe('GotoAnything Search Error Handling', () => {
mockFetchDatasets.mockRejectedValue(new Error('Dataset API failed'))
const actions = [
{ name: '@plugin', action: Actions.plugin },
{ name: '@app', action: Actions.app },
{ name: '@knowledge', action: Actions.knowledge },
{ name: '@plugin', scope: pluginScope },
{ name: '@app', scope: appScope },
{ name: '@knowledge', scope: knowledgeScope },
]
for (const { name, action } of actions) {
const result = await action.search(name, 'test', 'en')
for (const { name, scope } of actions) {
const result = await scope.search(name, 'test', 'en')
expect(result).toEqual([])
}
})
@@ -181,7 +176,8 @@ describe('GotoAnything Search Error Handling', () => {
it('empty search term should be handled properly', async () => {
mockPostMarketplace.mockResolvedValue({ data: { plugins: [] } })
const result = await searchAnything('en', '@plugin ', Actions.plugin)
const allScopes = [appScope, knowledgeScope, pluginScope]
const result = await searchAnything('en', '@plugin ', pluginScope, allScopes)
expect(result).toEqual([])
})
@@ -191,7 +187,8 @@ describe('GotoAnything Search Error Handling', () => {
mockPostMarketplace.mockRejectedValue(timeoutError)
const result = await searchAnything('en', '@plugin test', Actions.plugin)
const allScopes = [appScope, knowledgeScope, pluginScope]
const result = await searchAnything('en', '@plugin test', pluginScope, allScopes)
expect(result).toEqual([])
})
@@ -199,7 +196,8 @@ describe('GotoAnything Search Error Handling', () => {
const parseError = new SyntaxError('Unexpected token in JSON')
mockPostMarketplace.mockRejectedValue(parseError)
const result = await searchAnything('en', '@plugin test', Actions.plugin)
const allScopes = [appScope, knowledgeScope, pluginScope]
const result = await searchAnything('en', '@plugin test', pluginScope, allScopes)
expect(result).toEqual([])
})
})

View File

@@ -1,16 +1,18 @@
import { isInWorkflowPage, VIBE_COMMAND_EVENT } from '@/app/components/workflow/constants'
import i18n from '@/i18n-config/i18next-config'
import { bananaCommand } from './banana'
import { registerCommands, unregisterCommands } from './command-bus'
vi.mock('@/i18n-config/i18next-config', () => ({
default: {
t: vi.fn((key: string, options?: Record<string, unknown>) => {
if (!options)
return key
return `${key}:${JSON.stringify(options)}`
}),
},
// Mock i18n for testing
const mockI18n = {
t: vi.fn((key: string, options?: Record<string, unknown>) => {
if (!options)
return key
return `${key}:${JSON.stringify(options)}`
}),
}
vi.mock('react-i18next', () => ({
getI18n: () => mockI18n,
}))
vi.mock('@/app/components/workflow/constants', async () => {
@@ -31,7 +33,7 @@ vi.mock('./command-bus', () => ({
const mockedIsInWorkflowPage = vi.mocked(isInWorkflowPage)
const mockedRegisterCommands = vi.mocked(registerCommands)
const mockedUnregisterCommands = vi.mocked(unregisterCommands)
const mockedT = vi.mocked(i18n.t)
const mockedT = mockI18n.t
type CommandArgs = { dsl?: string }
type CommandMap = Record<string, (args?: CommandArgs) => void | Promise<void>>

View File

@@ -25,7 +25,7 @@ const nodeDefault: NodeDefault<CodeNodeType> = {
const { code, variables } = payload
if (!errorMessages && variables.filter(v => !v.variable).length > 0)
errorMessages = t(`${i18nPrefix}.fieldRequired`, { ns: 'workflow', field: t(`${i18nPrefix}.fields.variable`, { ns: 'workflow' }) })
if (!errorMessages && variables.filter(v => !v.value_selector.length).length > 0)
if (!errorMessages && variables.filter(v => !v.value_selector || !v.value_selector.length).length > 0)
errorMessages = t(`${i18nPrefix}.fieldRequired`, { ns: 'workflow', field: t(`${i18nPrefix}.fields.variableValue`, { ns: 'workflow' }) })
if (!errorMessages && !code)
errorMessages = t(`${i18nPrefix}.fieldRequired`, { ns: 'workflow', field: t(`${i18nPrefix}.fields.code`, { ns: 'workflow' }) })

View File

@@ -95,7 +95,7 @@ const nodeDefault: NodeDefault<LLMNodeType> = {
payload.prompt_config?.jinja2_variables.forEach((i) => {
if (!errorMessages && !i.variable)
errorMessages = t(`${i18nPrefix}.fieldRequired`, { ns: 'workflow', field: t(`${i18nPrefix}.fields.variable`, { ns: 'workflow' }) })
if (!errorMessages && !i.value_selector.length)
if (!errorMessages && (!i.value_selector || !i.value_selector.length))
errorMessages = t(`${i18nPrefix}.fieldRequired`, { ns: 'workflow', field: t(`${i18nPrefix}.fields.variableValue`, { ns: 'workflow' }) })
})
}

View File

@@ -24,7 +24,7 @@ const nodeDefault: NodeDefault<TemplateTransformNodeType> = {
if (!errorMessages && variables.filter(v => !v.variable).length > 0)
errorMessages = t(`${i18nPrefix}.fieldRequired`, { ns: 'workflow', field: t(`${i18nPrefix}.fields.variable`, { ns: 'workflow' }) })
if (!errorMessages && variables.filter(v => !v.value_selector.length).length > 0)
if (!errorMessages && variables.filter(v => !v.value_selector || !v.value_selector.length).length > 0)
errorMessages = t(`${i18nPrefix}.fieldRequired`, { ns: 'workflow', field: t(`${i18nPrefix}.fields.variableValue`, { ns: 'workflow' }) })
if (!errorMessages && !template)
errorMessages = t(`${i18nPrefix}.fieldRequired`, { ns: 'workflow', field: t('nodes.templateTransform.code', { ns: 'workflow' }) })