Compare commits

...

8 Commits

Author SHA1 Message Date
Joel
4d4db95f1d chore: fix waring 2025-12-18 11:37:34 +08:00
Joel
632d247059 chore: test to it 2025-12-18 11:31:23 +08:00
Joel
6e38812414 chroe: ts problems 2025-12-18 11:28:23 +08:00
Joel
4fd20dbff9 chore: fix lint problems 2025-12-18 11:25:37 +08:00
Joel
107a464f0a chore: tests for goto anything 2025-12-17 17:47:35 +08:00
wangxiaolei
4fce99379e test(api): add a test for detect_file_encodings (#29778)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-12-17 14:33:30 +08:00
zhaobingshuang
8d1e36540a fix: detect_file_encodings TypeError: tuple indices must be integers or slices, not str (#29595)
Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>
2025-12-17 13:58:05 +08:00
Wu Tianwei
1d1351393a feat: update RAG recommended plugins hook to accept type parameter (#29735) 2025-12-17 13:48:23 +08:00
9 changed files with 349 additions and 8 deletions

View File

@@ -45,6 +45,6 @@ def detect_file_encodings(file_path: str, timeout: int = 5, sample_size: int = 1
except concurrent.futures.TimeoutError:
raise TimeoutError(f"Timeout reached while detecting encoding for {file_path}")
if all(encoding["encoding"] is None for encoding in encodings):
if all(encoding.encoding is None for encoding in encodings):
raise RuntimeError(f"Could not detect encoding for {file_path}")
return [FileEncoding(**enc) for enc in encodings if enc["encoding"] is not None]
return [enc for enc in encodings if enc.encoding is not None]

View File

@@ -0,0 +1,10 @@
import tempfile
from core.rag.extractor.helpers import FileEncoding, detect_file_encodings
def test_detect_file_encodings() -> None:
with tempfile.NamedTemporaryFile(mode="w+t", suffix=".txt") as temp:
temp.write("Shared data")
temp_path = temp.name
assert detect_file_encodings(temp_path) == [FileEncoding(encoding="utf_8", confidence=0.0, language="Unknown")]

View File

@@ -0,0 +1,84 @@
import React from 'react'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Command } from 'cmdk'
import CommandSelector from './command-selector'
import type { ActionItem } from './actions/types'
jest.mock('next/navigation', () => ({
usePathname: () => '/app',
}))
const slashCommandsMock = [{
name: 'zen',
description: 'Zen mode',
mode: 'direct',
isAvailable: () => true,
}]
jest.mock('./actions/commands/registry', () => ({
slashCommandRegistry: {
getAvailableCommands: () => slashCommandsMock,
},
}))
const createActions = (): Record<string, ActionItem> => ({
app: {
key: '@app',
shortcut: '@app',
title: 'Apps',
search: jest.fn(),
description: '',
} as ActionItem,
plugin: {
key: '@plugin',
shortcut: '@plugin',
title: 'Plugins',
search: jest.fn(),
description: '',
} as ActionItem,
})
describe('CommandSelector', () => {
test('should list contextual search actions and notify selection', async () => {
const actions = createActions()
const onSelect = jest.fn()
render(
<Command>
<CommandSelector
actions={actions}
onCommandSelect={onSelect}
searchFilter='app'
originalQuery='@app'
/>
</Command>,
)
const actionButton = screen.getByText('app.gotoAnything.actions.searchApplicationsDesc')
await userEvent.click(actionButton)
expect(onSelect).toHaveBeenCalledWith('@app')
})
test('should render slash commands when query starts with slash', async () => {
const actions = createActions()
const onSelect = jest.fn()
render(
<Command>
<CommandSelector
actions={actions}
onCommandSelect={onSelect}
searchFilter='zen'
originalQuery='/zen'
/>
</Command>,
)
const slashItem = await screen.findByText('app.gotoAnything.actions.zenDesc')
await userEvent.click(slashItem)
expect(onSelect).toHaveBeenCalledWith('/zen')
})
})

View File

@@ -0,0 +1,58 @@
import React from 'react'
import { render, screen, waitFor } from '@testing-library/react'
import { GotoAnythingProvider, useGotoAnythingContext } from './context'
let pathnameMock = '/'
jest.mock('next/navigation', () => ({
usePathname: () => pathnameMock,
}))
let isWorkflowPageMock = false
jest.mock('../workflow/constants', () => ({
isInWorkflowPage: () => isWorkflowPageMock,
}))
const ContextConsumer = () => {
const { isWorkflowPage, isRagPipelinePage } = useGotoAnythingContext()
return (
<div data-testid="status">
{String(isWorkflowPage)}|{String(isRagPipelinePage)}
</div>
)
}
describe('GotoAnythingProvider', () => {
beforeEach(() => {
isWorkflowPageMock = false
pathnameMock = '/'
})
test('should set workflow page flag when workflow path detected', async () => {
isWorkflowPageMock = true
pathnameMock = '/app/123/workflow'
render(
<GotoAnythingProvider>
<ContextConsumer />
</GotoAnythingProvider>,
)
await waitFor(() => {
expect(screen.getByTestId('status')).toHaveTextContent('true|false')
})
})
test('should detect RAG pipeline path based on pathname', async () => {
pathnameMock = '/datasets/abc/pipeline'
render(
<GotoAnythingProvider>
<ContextConsumer />
</GotoAnythingProvider>,
)
await waitFor(() => {
expect(screen.getByTestId('status')).toHaveTextContent('false|true')
})
})
})

View File

@@ -0,0 +1,173 @@
import React from 'react'
import { act, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import GotoAnything from './index'
import type { ActionItem, SearchResult } from './actions/types'
const routerPush = jest.fn()
jest.mock('next/navigation', () => ({
useRouter: () => ({
push: routerPush,
}),
usePathname: () => '/',
}))
const keyPressHandlers: Record<string, (event: any) => void> = {}
jest.mock('ahooks', () => ({
useDebounce: (value: any) => value,
useKeyPress: (keys: string | string[], handler: (event: any) => void) => {
const keyList = Array.isArray(keys) ? keys : [keys]
keyList.forEach((key) => {
keyPressHandlers[key] = handler
})
},
}))
const triggerKeyPress = (combo: string) => {
const handler = keyPressHandlers[combo]
if (handler) {
act(() => {
handler({ preventDefault: jest.fn(), target: document.body })
})
}
}
let mockQueryResult = { data: [] as SearchResult[], isLoading: false, isError: false, error: null as Error | null }
jest.mock('@tanstack/react-query', () => ({
useQuery: () => mockQueryResult,
}))
jest.mock('@/context/i18n', () => ({
useGetLanguage: () => 'en_US',
}))
const contextValue = { isWorkflowPage: false, isRagPipelinePage: false }
jest.mock('./context', () => ({
useGotoAnythingContext: () => contextValue,
GotoAnythingProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
}))
const createActionItem = (key: ActionItem['key'], shortcut: string): ActionItem => ({
key,
shortcut,
title: `${key} title`,
description: `${key} desc`,
action: jest.fn(),
search: jest.fn(),
})
const actionsMock = {
slash: createActionItem('/', '/'),
app: createActionItem('@app', '@app'),
plugin: createActionItem('@plugin', '@plugin'),
}
const createActionsMock = jest.fn(() => actionsMock)
const matchActionMock = jest.fn(() => undefined)
const searchAnythingMock = jest.fn(async () => mockQueryResult.data)
jest.mock('./actions', () => ({
__esModule: true,
createActions: () => createActionsMock(),
matchAction: () => matchActionMock(),
searchAnything: () => searchAnythingMock(),
}))
jest.mock('./actions/commands', () => ({
SlashCommandProvider: () => null,
}))
jest.mock('./actions/commands/registry', () => ({
slashCommandRegistry: {
findCommand: () => null,
getAvailableCommands: () => [],
getAllCommands: () => [],
},
}))
jest.mock('@/app/components/workflow/utils/common', () => ({
getKeyboardKeyCodeBySystem: () => 'ctrl',
isEventTargetInputArea: () => false,
isMac: () => false,
}))
jest.mock('@/app/components/workflow/utils/node-navigation', () => ({
selectWorkflowNode: jest.fn(),
}))
jest.mock('../plugins/install-plugin/install-from-marketplace', () => (props: { manifest?: { name?: string }, onClose: () => void }) => (
<div data-testid="install-modal">
<span>{props.manifest?.name}</span>
<button onClick={props.onClose}>close</button>
</div>
))
describe('GotoAnything', () => {
beforeEach(() => {
routerPush.mockClear()
Object.keys(keyPressHandlers).forEach(key => delete keyPressHandlers[key])
mockQueryResult = { data: [], isLoading: false, isError: false, error: null }
matchActionMock.mockReset()
searchAnythingMock.mockClear()
})
it('should open modal via shortcut and navigate to selected result', async () => {
mockQueryResult = {
data: [{
id: 'app-1',
type: 'app',
title: 'Sample App',
description: 'desc',
path: '/apps/1',
icon: <div data-testid="icon">🧩</div>,
data: {},
} as any],
isLoading: false,
isError: false,
error: null,
}
render(<GotoAnything />)
triggerKeyPress('ctrl.k')
const input = await screen.findByPlaceholderText('app.gotoAnything.searchPlaceholder')
await userEvent.type(input, 'app')
const result = await screen.findByText('Sample App')
await userEvent.click(result)
expect(routerPush).toHaveBeenCalledWith('/apps/1')
})
it('should open plugin installer when selecting plugin result', async () => {
mockQueryResult = {
data: [{
id: 'plugin-1',
type: 'plugin',
title: 'Plugin Item',
description: 'desc',
path: '',
icon: <div />,
data: {
name: 'Plugin Item',
latest_package_identifier: 'pkg',
},
} as any],
isLoading: false,
isError: false,
error: null,
}
render(<GotoAnything />)
triggerKeyPress('ctrl.k')
const input = await screen.findByPlaceholderText('app.gotoAnything.searchPlaceholder')
await userEvent.type(input, 'plugin')
const pluginItem = await screen.findByText('Plugin Item')
await userEvent.click(pluginItem)
expect(await screen.findByTestId('install-modal')).toHaveTextContent('Plugin Item')
})
})

View File

@@ -37,7 +37,7 @@ const useRefreshPluginList = () => {
if ((manifest && PluginCategoryEnum.tool.includes(manifest.category)) || refreshAllType) {
invalidateAllToolProviders()
invalidateAllBuiltInTools()
invalidateRAGRecommendedPlugins()
invalidateRAGRecommendedPlugins('tool')
// TODO: update suggested tools. It's a function in hook useMarketplacePlugins,handleUpdatePlugins
}

View File

@@ -52,7 +52,7 @@ const RAGToolRecommendations = ({
data: ragRecommendedPlugins,
isLoading: isLoadingRAGRecommendedPlugins,
isFetching: isFetchingRAGRecommendedPlugins,
} = useRAGRecommendedPlugins()
} = useRAGRecommendedPlugins('tool')
const recommendedPlugins = useMemo(() => {
if (ragRecommendedPlugins)

View File

@@ -4,6 +4,13 @@ import { cleanup } from '@testing-library/react'
// Fix for @headlessui/react compatibility with happy-dom
// headlessui tries to override focus properties which may be read-only in happy-dom
if (typeof window !== 'undefined') {
// Provide a minimal animations API polyfill before @headlessui/react boots
if (typeof Element !== 'undefined' && !Element.prototype.getAnimations)
Element.prototype.getAnimations = () => []
if (!document.getAnimations)
document.getAnimations = () => []
const ensureWritable = (target: object, prop: string) => {
const descriptor = Object.getOwnPropertyDescriptor(target, prop)
if (descriptor && !descriptor.writable) {

View File

@@ -330,15 +330,24 @@ export const useRemoveProviderCredentials = ({
const useRAGRecommendedPluginListKey = [NAME_SPACE, 'rag-recommended-plugins']
export const useRAGRecommendedPlugins = () => {
export const useRAGRecommendedPlugins = (type: 'tool' | 'datasource' | 'all' = 'all') => {
return useQuery<RAGRecommendedPlugins>({
queryKey: useRAGRecommendedPluginListKey,
queryFn: () => get<RAGRecommendedPlugins>('/rag/pipelines/recommended-plugins'),
queryKey: [...useRAGRecommendedPluginListKey, type],
queryFn: () => get<RAGRecommendedPlugins>('/rag/pipelines/recommended-plugins', {
params: {
type,
},
}),
})
}
export const useInvalidateRAGRecommendedPlugins = () => {
return useInvalid(useRAGRecommendedPluginListKey)
const queryClient = useQueryClient()
return (type: 'tool' | 'datasource' | 'all' = 'all') => {
queryClient.invalidateQueries({
queryKey: [...useRAGRecommendedPluginListKey, type],
})
}
}
// App Triggers API hooks