mirror of
https://github.com/langgenius/dify.git
synced 2026-02-09 15:10:13 -05:00
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:
@@ -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
|
||||
|
||||
@@ -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"] = []
|
||||
|
||||
|
||||
@@ -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"
|
||||
/>,
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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([])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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>>
|
||||
|
||||
@@ -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' }) })
|
||||
|
||||
@@ -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' }) })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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' }) })
|
||||
|
||||
Reference in New Issue
Block a user