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 "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 # Validate and normalize
if "intent" not in data: if "intent" not in data:
data["intent"] = "generate" # Default assumption data["intent"] = "generate" # Default assumption

View File

@ -6,7 +6,11 @@ from collections.abc import Sequence
import json_repair import json_repair
from core.model_manager import ModelManager 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.model_runtime.entities.model_entities import ModelType
from core.workflow.generator.prompts.builder_prompts import ( from core.workflow.generator.prompts.builder_prompts import (
BUILDER_SYSTEM_PROMPT, BUILDER_SYSTEM_PROMPT,
@ -105,7 +109,31 @@ class WorkflowGenerator:
model_parameters=model_parameters, model_parameters=model_parameters,
stream=False, stream=False,
) )
# Extract text content from response
plan_content = response.message.content 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 # Reuse parse_vibe_response logic or simple load
plan_data = parse_vibe_response(plan_content) plan_data = parse_vibe_response(plan_content)
except Exception as e: except Exception as e:
@ -212,13 +240,52 @@ class WorkflowGenerator:
stream=False, stream=False,
) )
# Builder output is raw JSON nodes/edges # Builder output is raw JSON nodes/edges
# Extract text content from response
build_content = build_res.message.content 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) match = re.search(r"```(?:json)?\s*([\s\S]+?)```", build_content)
if match: if match:
build_content = match.group(1) 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) 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: if "nodes" not in workflow_data:
workflow_data["nodes"] = [] 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 { fireEvent, render, screen } from '@testing-library/react'
import * as React from 'react' import * as React from 'react'
import CommandSelector from '../../app/components/goto-anything/command-selector' import CommandSelector from '../../app/components/goto-anything/command-selector'
@ -20,36 +20,37 @@ vi.mock('cmdk', () => ({
})) }))
describe('CommandSelector', () => { describe('CommandSelector', () => {
const mockActions: Record<string, ActionItem> = { const mockScopes: ScopeDescriptor[] = [
app: { {
key: '@app', id: 'app',
shortcut: '@app', shortcut: '@app',
title: 'Search Applications', title: 'Search Applications',
description: 'Search apps', description: 'Search apps',
search: vi.fn(), search: vi.fn(),
}, },
knowledge: { {
key: '@knowledge', id: 'knowledge',
shortcut: '@kb', shortcut: '@kb',
aliases: ['@knowledge'],
title: 'Search Knowledge', title: 'Search Knowledge',
description: 'Search knowledge bases', description: 'Search knowledge bases',
search: vi.fn(), search: vi.fn(),
}, },
plugin: { {
key: '@plugin', id: 'plugin',
shortcut: '@plugin', shortcut: '@plugin',
title: 'Search Plugins', title: 'Search Plugins',
description: 'Search plugins', description: 'Search plugins',
search: vi.fn(), search: vi.fn(),
}, },
node: { {
key: '@node', id: 'node',
shortcut: '@node', shortcut: '@node',
title: 'Search Nodes', title: 'Search Nodes',
description: 'Search workflow nodes', description: 'Search workflow nodes',
search: vi.fn(), search: vi.fn(),
}, },
} ]
const mockOnCommandSelect = vi.fn() const mockOnCommandSelect = vi.fn()
const mockOnCommandValueChange = vi.fn() const mockOnCommandValueChange = vi.fn()
@ -62,7 +63,7 @@ describe('CommandSelector', () => {
it('should render all actions when no filter is provided', () => { it('should render all actions when no filter is provided', () => {
render( render(
<CommandSelector <CommandSelector
actions={mockActions} scopes={mockScopes}
onCommandSelect={mockOnCommandSelect} onCommandSelect={mockOnCommandSelect}
/>, />,
) )
@ -76,7 +77,7 @@ describe('CommandSelector', () => {
it('should render empty filter as showing all actions', () => { it('should render empty filter as showing all actions', () => {
render( render(
<CommandSelector <CommandSelector
actions={mockActions} scopes={mockScopes}
onCommandSelect={mockOnCommandSelect} onCommandSelect={mockOnCommandSelect}
searchFilter="" searchFilter=""
/>, />,
@ -93,7 +94,7 @@ describe('CommandSelector', () => {
it('should filter actions based on searchFilter - single match', () => { it('should filter actions based on searchFilter - single match', () => {
render( render(
<CommandSelector <CommandSelector
actions={mockActions} scopes={mockScopes}
onCommandSelect={mockOnCommandSelect} onCommandSelect={mockOnCommandSelect}
searchFilter="k" searchFilter="k"
/>, />,
@ -108,7 +109,7 @@ describe('CommandSelector', () => {
it('should filter actions with multiple matches', () => { it('should filter actions with multiple matches', () => {
render( render(
<CommandSelector <CommandSelector
actions={mockActions} scopes={mockScopes}
onCommandSelect={mockOnCommandSelect} onCommandSelect={mockOnCommandSelect}
searchFilter="p" searchFilter="p"
/>, />,
@ -123,7 +124,7 @@ describe('CommandSelector', () => {
it('should be case-insensitive when filtering', () => { it('should be case-insensitive when filtering', () => {
render( render(
<CommandSelector <CommandSelector
actions={mockActions} scopes={mockScopes}
onCommandSelect={mockOnCommandSelect} onCommandSelect={mockOnCommandSelect}
searchFilter="APP" searchFilter="APP"
/>, />,
@ -136,7 +137,7 @@ describe('CommandSelector', () => {
it('should match partial strings', () => { it('should match partial strings', () => {
render( render(
<CommandSelector <CommandSelector
actions={mockActions} scopes={mockScopes}
onCommandSelect={mockOnCommandSelect} onCommandSelect={mockOnCommandSelect}
searchFilter="od" searchFilter="od"
/>, />,
@ -153,7 +154,7 @@ describe('CommandSelector', () => {
it('should show empty state when no matches found', () => { it('should show empty state when no matches found', () => {
render( render(
<CommandSelector <CommandSelector
actions={mockActions} scopes={mockScopes}
onCommandSelect={mockOnCommandSelect} onCommandSelect={mockOnCommandSelect}
searchFilter="xyz" searchFilter="xyz"
/>, />,
@ -171,7 +172,7 @@ describe('CommandSelector', () => {
it('should not show empty state when filter is empty', () => { it('should not show empty state when filter is empty', () => {
render( render(
<CommandSelector <CommandSelector
actions={mockActions} scopes={mockScopes}
onCommandSelect={mockOnCommandSelect} onCommandSelect={mockOnCommandSelect}
searchFilter="" searchFilter=""
/>, />,
@ -185,7 +186,7 @@ describe('CommandSelector', () => {
it('should call onCommandValueChange when filter changes and first item differs', () => { it('should call onCommandValueChange when filter changes and first item differs', () => {
const { rerender } = render( const { rerender } = render(
<CommandSelector <CommandSelector
actions={mockActions} scopes={mockScopes}
onCommandSelect={mockOnCommandSelect} onCommandSelect={mockOnCommandSelect}
searchFilter="" searchFilter=""
commandValue="@app" commandValue="@app"
@ -195,7 +196,7 @@ describe('CommandSelector', () => {
rerender( rerender(
<CommandSelector <CommandSelector
actions={mockActions} scopes={mockScopes}
onCommandSelect={mockOnCommandSelect} onCommandSelect={mockOnCommandSelect}
searchFilter="k" searchFilter="k"
commandValue="@app" commandValue="@app"
@ -209,7 +210,7 @@ describe('CommandSelector', () => {
it('should not call onCommandValueChange if current value still exists', () => { it('should not call onCommandValueChange if current value still exists', () => {
const { rerender } = render( const { rerender } = render(
<CommandSelector <CommandSelector
actions={mockActions} scopes={mockScopes}
onCommandSelect={mockOnCommandSelect} onCommandSelect={mockOnCommandSelect}
searchFilter="" searchFilter=""
commandValue="@app" commandValue="@app"
@ -219,7 +220,7 @@ describe('CommandSelector', () => {
rerender( rerender(
<CommandSelector <CommandSelector
actions={mockActions} scopes={mockScopes}
onCommandSelect={mockOnCommandSelect} onCommandSelect={mockOnCommandSelect}
searchFilter="a" searchFilter="a"
commandValue="@app" commandValue="@app"
@ -233,7 +234,7 @@ describe('CommandSelector', () => {
it('should handle onCommandSelect callback correctly', () => { it('should handle onCommandSelect callback correctly', () => {
render( render(
<CommandSelector <CommandSelector
actions={mockActions} scopes={mockScopes}
onCommandSelect={mockOnCommandSelect} onCommandSelect={mockOnCommandSelect}
searchFilter="k" searchFilter="k"
/>, />,
@ -250,7 +251,7 @@ describe('CommandSelector', () => {
it('should handle empty actions object', () => { it('should handle empty actions object', () => {
render( render(
<CommandSelector <CommandSelector
actions={{}} scopes={[]}
onCommandSelect={mockOnCommandSelect} onCommandSelect={mockOnCommandSelect}
searchFilter="" searchFilter=""
/>, />,
@ -262,7 +263,7 @@ describe('CommandSelector', () => {
it('should handle special characters in filter', () => { it('should handle special characters in filter', () => {
render( render(
<CommandSelector <CommandSelector
actions={mockActions} scopes={mockScopes}
onCommandSelect={mockOnCommandSelect} onCommandSelect={mockOnCommandSelect}
searchFilter="@" searchFilter="@"
/>, />,
@ -277,7 +278,7 @@ describe('CommandSelector', () => {
it('should handle undefined onCommandValueChange gracefully', () => { it('should handle undefined onCommandValueChange gracefully', () => {
const { rerender } = render( const { rerender } = render(
<CommandSelector <CommandSelector
actions={mockActions} scopes={mockScopes}
onCommandSelect={mockOnCommandSelect} onCommandSelect={mockOnCommandSelect}
searchFilter="" searchFilter=""
/>, />,
@ -286,7 +287,7 @@ describe('CommandSelector', () => {
expect(() => { expect(() => {
rerender( rerender(
<CommandSelector <CommandSelector
actions={mockActions} scopes={mockScopes}
onCommandSelect={mockOnCommandSelect} onCommandSelect={mockOnCommandSelect}
searchFilter="k" searchFilter="k"
/>, />,
@ -299,7 +300,7 @@ describe('CommandSelector', () => {
it('should work without searchFilter prop (backward compatible)', () => { it('should work without searchFilter prop (backward compatible)', () => {
render( render(
<CommandSelector <CommandSelector
actions={mockActions} scopes={mockScopes}
onCommandSelect={mockOnCommandSelect} onCommandSelect={mockOnCommandSelect}
/>, />,
) )
@ -313,7 +314,7 @@ describe('CommandSelector', () => {
it('should work without commandValue and onCommandValueChange props', () => { it('should work without commandValue and onCommandValueChange props', () => {
render( render(
<CommandSelector <CommandSelector
actions={mockActions} scopes={mockScopes}
onCommandSelect={mockOnCommandSelect} onCommandSelect={mockOnCommandSelect}
searchFilter="k" searchFilter="k"
/>, />,

View File

@ -1,5 +1,5 @@
import type { Mock } from 'vitest' 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 after mocking to get mocked version
import { matchAction } from '../../app/components/goto-anything/actions' 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') vi.mock('../../app/components/goto-anything/actions/commands/registry')
// Implement the actual matchAction logic for testing // Implement the actual matchAction logic for testing
const actualMatchAction = (query: string, actions: Record<string, ActionItem>) => { const actualMatchAction = (query: string, scopes: ScopeDescriptor[]) => {
const result = Object.values(actions).find((action) => { const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
return scopes.find((scope) => {
// Special handling for slash commands // Special handling for slash commands
if (action.key === '/') { if (scope.id === 'slash' || scope.shortcut === '/') {
// Get all registered commands from the registry // Get all registered commands from the registry
const allCommands = slashCommandRegistry.getAllCommands() 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 reg.test(query)
}) })
return result
} }
// Replace mock with actual implementation // Replace mock with actual implementation
;(matchAction as Mock).mockImplementation(actualMatchAction) ;(matchAction as Mock).mockImplementation(actualMatchAction)
describe('matchAction Logic', () => { describe('matchAction Logic', () => {
const mockActions: Record<string, ActionItem> = { const mockScopes: ScopeDescriptor[] = [
app: { {
key: '@app', id: 'app',
shortcut: '@a', shortcut: '@app',
aliases: ['@a'],
title: 'Search Applications', title: 'Search Applications',
description: 'Search apps', description: 'Search apps',
search: vi.fn(), search: vi.fn(),
}, },
knowledge: { {
key: '@knowledge', id: 'knowledge',
shortcut: '@kb', shortcut: '@kb',
aliases: ['@knowledge'],
title: 'Search Knowledge', title: 'Search Knowledge',
description: 'Search knowledge bases', description: 'Search knowledge bases',
search: vi.fn(), search: vi.fn(),
}, },
slash: { {
key: '/', id: 'slash',
shortcut: '/', shortcut: '/',
title: 'Commands', title: 'Commands',
description: 'Execute commands', description: 'Execute commands',
search: vi.fn(), search: vi.fn(),
}, },
} ]
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks() vi.clearAllMocks()
@ -81,32 +84,32 @@ describe('matchAction Logic', () => {
describe('@ Actions Matching', () => { describe('@ Actions Matching', () => {
it('should match @app with key', () => { it('should match @app with key', () => {
const result = matchAction('@app', mockActions) const result = matchAction('@app', mockScopes)
expect(result).toBe(mockActions.app) expect(result).toBe(mockScopes[0])
}) })
it('should match @app with shortcut', () => { it('should match @app with shortcut', () => {
const result = matchAction('@a', mockActions) const result = matchAction('@a', mockScopes)
expect(result).toBe(mockActions.app) expect(result).toBe(mockScopes[0])
}) })
it('should match @knowledge with key', () => { it('should match @knowledge with key', () => {
const result = matchAction('@knowledge', mockActions) const result = matchAction('@knowledge', mockScopes)
expect(result).toBe(mockActions.knowledge) expect(result).toBe(mockScopes[1])
}) })
it('should match @knowledge with shortcut @kb', () => { it('should match @knowledge with shortcut @kb', () => {
const result = matchAction('@kb', mockActions) const result = matchAction('@kb', mockScopes)
expect(result).toBe(mockActions.knowledge) expect(result).toBe(mockScopes[1])
}) })
it('should match with text after action', () => { it('should match with text after action', () => {
const result = matchAction('@app search term', mockActions) const result = matchAction('@app search term', mockScopes)
expect(result).toBe(mockActions.app) expect(result).toBe(mockScopes[0])
}) })
it('should not match partial @ actions', () => { it('should not match partial @ actions', () => {
const result = matchAction('@ap', mockActions) const result = matchAction('@ap', mockScopes)
expect(result).toBeUndefined() expect(result).toBeUndefined()
}) })
}) })
@ -114,47 +117,47 @@ describe('matchAction Logic', () => {
describe('Slash Commands Matching', () => { describe('Slash Commands Matching', () => {
describe('Direct Mode Commands', () => { describe('Direct Mode Commands', () => {
it('should not match direct mode commands', () => { it('should not match direct mode commands', () => {
const result = matchAction('/docs', mockActions) const result = matchAction('/docs', mockScopes)
expect(result).toBeUndefined() expect(result).toBeUndefined()
}) })
it('should not match direct mode with arguments', () => { it('should not match direct mode with arguments', () => {
const result = matchAction('/docs something', mockActions) const result = matchAction('/docs something', mockScopes)
expect(result).toBeUndefined() expect(result).toBeUndefined()
}) })
it('should not match any direct mode command', () => { it('should not match any direct mode command', () => {
expect(matchAction('/community', mockActions)).toBeUndefined() expect(matchAction('/community', mockScopes)).toBeUndefined()
expect(matchAction('/feedback', mockActions)).toBeUndefined() expect(matchAction('/feedback', mockScopes)).toBeUndefined()
expect(matchAction('/account', mockActions)).toBeUndefined() expect(matchAction('/account', mockScopes)).toBeUndefined()
}) })
}) })
describe('Submenu Mode Commands', () => { describe('Submenu Mode Commands', () => {
it('should match submenu mode commands exactly', () => { it('should match submenu mode commands exactly', () => {
const result = matchAction('/theme', mockActions) const result = matchAction('/theme', mockScopes)
expect(result).toBe(mockActions.slash) expect(result).toBe(mockScopes[2])
}) })
it('should match submenu mode with arguments', () => { it('should match submenu mode with arguments', () => {
const result = matchAction('/theme dark', mockActions) const result = matchAction('/theme dark', mockScopes)
expect(result).toBe(mockActions.slash) expect(result).toBe(mockScopes[2])
}) })
it('should match all submenu commands', () => { it('should match all submenu commands', () => {
expect(matchAction('/language', mockActions)).toBe(mockActions.slash) expect(matchAction('/language', mockScopes)).toBe(mockScopes[2])
expect(matchAction('/language en', mockActions)).toBe(mockActions.slash) expect(matchAction('/language en', mockScopes)).toBe(mockScopes[2])
}) })
}) })
describe('Slash Without Command', () => { describe('Slash Without Command', () => {
it('should not match single slash', () => { it('should not match single slash', () => {
const result = matchAction('/', mockActions) const result = matchAction('/', mockScopes)
expect(result).toBeUndefined() expect(result).toBeUndefined()
}) })
it('should not match unregistered commands', () => { it('should not match unregistered commands', () => {
const result = matchAction('/unknown', mockActions) const result = matchAction('/unknown', mockScopes)
expect(result).toBeUndefined() expect(result).toBeUndefined()
}) })
}) })
@ -162,28 +165,28 @@ describe('matchAction Logic', () => {
describe('Edge Cases', () => { describe('Edge Cases', () => {
it('should handle empty query', () => { it('should handle empty query', () => {
const result = matchAction('', mockActions) const result = matchAction('', mockScopes)
expect(result).toBeUndefined() expect(result).toBeUndefined()
}) })
it('should handle whitespace only', () => { it('should handle whitespace only', () => {
const result = matchAction(' ', mockActions) const result = matchAction(' ', mockScopes)
expect(result).toBeUndefined() expect(result).toBeUndefined()
}) })
it('should handle regular text without actions', () => { it('should handle regular text without actions', () => {
const result = matchAction('search something', mockActions) const result = matchAction('search something', mockScopes)
expect(result).toBeUndefined() expect(result).toBeUndefined()
}) })
it('should handle special characters', () => { it('should handle special characters', () => {
const result = matchAction('#tag', mockActions) const result = matchAction('#tag', mockScopes)
expect(result).toBeUndefined() expect(result).toBeUndefined()
}) })
it('should handle multiple @ or /', () => { it('should handle multiple @ or /', () => {
expect(matchAction('@@app', mockActions)).toBeUndefined() expect(matchAction('@@app', mockScopes)).toBeUndefined()
expect(matchAction('//theme', mockActions)).toBeUndefined() expect(matchAction('//theme', mockScopes)).toBeUndefined()
}) })
}) })
@ -193,7 +196,7 @@ describe('matchAction Logic', () => {
{ name: 'test', mode: 'direct' }, { name: 'test', mode: 'direct' },
]) ])
const result = matchAction('/test', mockActions) const result = matchAction('/test', mockScopes)
expect(result).toBeUndefined() expect(result).toBeUndefined()
}) })
@ -202,8 +205,8 @@ describe('matchAction Logic', () => {
{ name: 'test', mode: 'submenu' }, { name: 'test', mode: 'submenu' },
]) ])
const result = matchAction('/test', mockActions) const result = matchAction('/test', mockScopes)
expect(result).toBe(mockActions.slash) expect(result).toBe(mockScopes[2])
}) })
it('should treat undefined mode as submenu', () => { it('should treat undefined mode as submenu', () => {
@ -211,25 +214,25 @@ describe('matchAction Logic', () => {
{ name: 'test' }, // No mode specified { name: 'test' }, // No mode specified
]) ])
const result = matchAction('/test', mockActions) const result = matchAction('/test', mockScopes)
expect(result).toBe(mockActions.slash) expect(result).toBe(mockScopes[2])
}) })
}) })
describe('Registry Integration', () => { describe('Registry Integration', () => {
it('should call getAllCommands when matching slash', () => { it('should call getAllCommands when matching slash', () => {
matchAction('/theme', mockActions) matchAction('/theme', mockScopes)
expect(slashCommandRegistry.getAllCommands).toHaveBeenCalled() expect(slashCommandRegistry.getAllCommands).toHaveBeenCalled()
}) })
it('should not call getAllCommands for @ actions', () => { it('should not call getAllCommands for @ actions', () => {
matchAction('@app', mockActions) matchAction('@app', mockScopes)
expect(slashCommandRegistry.getAllCommands).not.toHaveBeenCalled() expect(slashCommandRegistry.getAllCommands).not.toHaveBeenCalled()
}) })
it('should handle empty command list', () => { it('should handle empty command list', () => {
;(slashCommandRegistry.getAllCommands as Mock).mockReturnValue([]) ;(slashCommandRegistry.getAllCommands as Mock).mockReturnValue([])
const result = matchAction('/anything', mockActions) const result = matchAction('/anything', mockScopes)
expect(result).toBeUndefined() 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" * 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 { fetchAppList } from '@/service/apps'
import { postMarketplace } from '@/service/base' import { postMarketplace } from '@/service/base'
import { fetchDatasets } from '@/service/datasets' import { fetchDatasets } from '@/service/datasets'
@ -57,10 +57,8 @@ describe('GotoAnything Search Error Handling', () => {
// Mock marketplace API failure (403 permission denied) // Mock marketplace API failure (403 permission denied)
mockPostMarketplace.mockRejectedValue(new Error('HTTP 403: Forbidden')) mockPostMarketplace.mockRejectedValue(new Error('HTTP 403: Forbidden'))
const pluginAction = Actions.plugin
// Directly call plugin action's search method // 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 // Should return empty array instead of throwing error
expect(result).toEqual([]) expect(result).toEqual([])
@ -80,8 +78,7 @@ describe('GotoAnything Search Error Handling', () => {
data: { plugins: [] }, data: { plugins: [] },
}) })
const pluginAction = Actions.plugin const result = await pluginScope.search('@plugin', '', 'en')
const result = await pluginAction.search('@plugin', '', 'en')
expect(result).toEqual([]) expect(result).toEqual([])
}) })
@ -92,8 +89,7 @@ describe('GotoAnything Search Error Handling', () => {
data: null, data: null,
}) })
const pluginAction = Actions.plugin const result = await pluginScope.search('@plugin', 'test', 'en')
const result = await pluginAction.search('@plugin', 'test', 'en')
expect(result).toEqual([]) expect(result).toEqual([])
}) })
@ -104,8 +100,7 @@ describe('GotoAnything Search Error Handling', () => {
// Mock app API failure // Mock app API failure
mockFetchAppList.mockRejectedValue(new Error('API Error')) mockFetchAppList.mockRejectedValue(new Error('API Error'))
const appAction = Actions.app const result = await appScope.search('@app', 'test', 'en')
const result = await appAction.search('@app', 'test', 'en')
expect(result).toEqual([]) expect(result).toEqual([])
}) })
@ -114,8 +109,7 @@ describe('GotoAnything Search Error Handling', () => {
// Mock knowledge API failure // Mock knowledge API failure
mockFetchDatasets.mockRejectedValue(new Error('API Error')) mockFetchDatasets.mockRejectedValue(new Error('API Error'))
const knowledgeAction = Actions.knowledge const result = await knowledgeScope.search('@knowledge', 'test', 'en')
const result = await knowledgeAction.search('@knowledge', 'test', 'en')
expect(result).toEqual([]) expect(result).toEqual([])
}) })
@ -128,19 +122,20 @@ describe('GotoAnything Search Error Handling', () => {
mockFetchDatasets.mockResolvedValue({ data: [], has_more: false, limit: 10, page: 1, total: 0 }) mockFetchDatasets.mockResolvedValue({ data: [], has_more: false, limit: 10, page: 1, total: 0 })
mockPostMarketplace.mockRejectedValue(new Error('Plugin API failed')) 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 // Should return successful results even if plugin search fails
expect(result).toEqual([]) 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 () => { it('@plugin dedicated search should return empty array when API fails', async () => {
// Mock plugin API failure // Mock plugin API failure
mockPostMarketplace.mockRejectedValue(new Error('Plugin service unavailable')) mockPostMarketplace.mockRejectedValue(new Error('Plugin service unavailable'))
const pluginAction = Actions.plugin const allScopes = [appScope, knowledgeScope, pluginScope]
const result = await searchAnything('en', '@plugin test', pluginAction) const result = await searchAnything('en', '@plugin test', pluginScope, allScopes)
// Should return empty array instead of throwing error // Should return empty array instead of throwing error
expect(result).toEqual([]) expect(result).toEqual([])
@ -150,8 +145,8 @@ describe('GotoAnything Search Error Handling', () => {
// Mock app API failure // Mock app API failure
mockFetchAppList.mockRejectedValue(new Error('App service unavailable')) mockFetchAppList.mockRejectedValue(new Error('App service unavailable'))
const appAction = Actions.app const allScopes = [appScope, knowledgeScope, pluginScope]
const result = await searchAnything('en', '@app test', appAction) const result = await searchAnything('en', '@app test', appScope, allScopes)
expect(result).toEqual([]) expect(result).toEqual([])
}) })
@ -165,13 +160,13 @@ describe('GotoAnything Search Error Handling', () => {
mockFetchDatasets.mockRejectedValue(new Error('Dataset API failed')) mockFetchDatasets.mockRejectedValue(new Error('Dataset API failed'))
const actions = [ const actions = [
{ name: '@plugin', action: Actions.plugin }, { name: '@plugin', scope: pluginScope },
{ name: '@app', action: Actions.app }, { name: '@app', scope: appScope },
{ name: '@knowledge', action: Actions.knowledge }, { name: '@knowledge', scope: knowledgeScope },
] ]
for (const { name, action } of actions) { for (const { name, scope } of actions) {
const result = await action.search(name, 'test', 'en') const result = await scope.search(name, 'test', 'en')
expect(result).toEqual([]) expect(result).toEqual([])
} }
}) })
@ -181,7 +176,8 @@ describe('GotoAnything Search Error Handling', () => {
it('empty search term should be handled properly', async () => { it('empty search term should be handled properly', async () => {
mockPostMarketplace.mockResolvedValue({ data: { plugins: [] } }) 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([]) expect(result).toEqual([])
}) })
@ -191,7 +187,8 @@ describe('GotoAnything Search Error Handling', () => {
mockPostMarketplace.mockRejectedValue(timeoutError) 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([]) expect(result).toEqual([])
}) })
@ -199,7 +196,8 @@ describe('GotoAnything Search Error Handling', () => {
const parseError = new SyntaxError('Unexpected token in JSON') const parseError = new SyntaxError('Unexpected token in JSON')
mockPostMarketplace.mockRejectedValue(parseError) 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([]) expect(result).toEqual([])
}) })
}) })

View File

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

View File

@ -25,7 +25,7 @@ const nodeDefault: NodeDefault<CodeNodeType> = {
const { code, variables } = payload const { code, variables } = payload
if (!errorMessages && variables.filter(v => !v.variable).length > 0) if (!errorMessages && variables.filter(v => !v.variable).length > 0)
errorMessages = t(`${i18nPrefix}.fieldRequired`, { ns: 'workflow', field: t(`${i18nPrefix}.fields.variable`, { ns: 'workflow' }) }) 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' }) }) errorMessages = t(`${i18nPrefix}.fieldRequired`, { ns: 'workflow', field: t(`${i18nPrefix}.fields.variableValue`, { ns: 'workflow' }) })
if (!errorMessages && !code) if (!errorMessages && !code)
errorMessages = t(`${i18nPrefix}.fieldRequired`, { ns: 'workflow', field: t(`${i18nPrefix}.fields.code`, { ns: 'workflow' }) }) 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) => { payload.prompt_config?.jinja2_variables.forEach((i) => {
if (!errorMessages && !i.variable) if (!errorMessages && !i.variable)
errorMessages = t(`${i18nPrefix}.fieldRequired`, { ns: 'workflow', field: t(`${i18nPrefix}.fields.variable`, { ns: 'workflow' }) }) 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' }) }) 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) if (!errorMessages && variables.filter(v => !v.variable).length > 0)
errorMessages = t(`${i18nPrefix}.fieldRequired`, { ns: 'workflow', field: t(`${i18nPrefix}.fields.variable`, { ns: 'workflow' }) }) 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' }) }) errorMessages = t(`${i18nPrefix}.fieldRequired`, { ns: 'workflow', field: t(`${i18nPrefix}.fields.variableValue`, { ns: 'workflow' }) })
if (!errorMessages && !template) if (!errorMessages && !template)
errorMessages = t(`${i18nPrefix}.fieldRequired`, { ns: 'workflow', field: t('nodes.templateTransform.code', { ns: 'workflow' }) }) errorMessages = t(`${i18nPrefix}.fieldRequired`, { ns: 'workflow', field: t('nodes.templateTransform.code', { ns: 'workflow' }) })