-
Notifications
You must be signed in to change notification settings - Fork 5
added tests #73
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
wpak-ai
merged 6 commits into
cppalliance:main
from
jonathanMLDev:bugfix/add-tool-handler
May 14, 2026
Merged
added tests #73
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
893db16
added tests
52d21f4
fixed line error
10ca4f2
Merge branch 'main' into bugfix/add-tool-handler
jonathanMLDev 39873a9
fixed test errors
ef1ab2f
Merge branch 'bugfix/add-tool-handler' of https://github.com/jonathan…
742f5e0
fixed test errors
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| import { beforeEach, describe, expect, it, vi } from 'vitest'; | ||
| import { getPineconeClient } from '../client-context.js'; | ||
| import { getNamespacesWithCache } from '../namespaces-cache.js'; | ||
| import { registerGuidedQueryTool } from './guided-query-tool.js'; | ||
| import { | ||
| createMockServer, | ||
| makeNamespaceCacheEntry, | ||
| makeSearchResult, | ||
| parseToolJson, | ||
| } from './test-helpers.js'; | ||
|
|
||
| vi.mock('../client-context.js', () => ({ | ||
| getPineconeClient: vi.fn(), | ||
| })); | ||
|
|
||
| vi.mock('../namespaces-cache.js', () => ({ | ||
| getNamespacesWithCache: vi.fn(), | ||
| })); | ||
|
|
||
| /** Real `markSuggested` may call `getServerConfig()` during sweep (CI has no API key); isolate the handler. */ | ||
| vi.mock('../suggestion-flow.js', () => ({ | ||
| markSuggested: vi.fn(), | ||
| })); | ||
|
|
||
| const mockedGetNamespaces = vi.mocked(getNamespacesWithCache); | ||
| const mockedGetClient = vi.mocked(getPineconeClient); | ||
|
|
||
| describe('guided_query tool handler', () => { | ||
| const nsEntry = makeNamespaceCacheEntry('papers', { | ||
| document_number: 'string', | ||
| title: 'string', | ||
| url: 'string', | ||
| author: 'string', | ||
| chunk_text: 'string', | ||
| }); | ||
|
|
||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| mockedGetNamespaces.mockResolvedValue({ | ||
| data: [nsEntry], | ||
| cache_hit: false, | ||
| expires_at: Date.now() + 1_800_000, | ||
| }); | ||
| mockedGetClient.mockReturnValue({ | ||
| query: vi.fn().mockResolvedValue([makeSearchResult()]), | ||
| count: vi.fn().mockResolvedValue({ count: 7, truncated: false }), | ||
| } as never); | ||
| }); | ||
|
|
||
| it('runs query_detailed path on auto when user asks for content', async () => { | ||
| const server = createMockServer(); | ||
| registerGuidedQueryTool(server as never); | ||
| const query = mockedGetClient().query as ReturnType<typeof vi.fn>; | ||
|
|
||
| const body = parseToolJson( | ||
| await server.getHandler('guided_query')!({ | ||
| user_query: 'What does the paper say about contracts?', | ||
| namespace: 'papers', | ||
| top_k: 8, | ||
| preferred_tool: 'auto', | ||
| enrich_urls: false, | ||
| }) | ||
| ); | ||
|
|
||
| expect(body.status).toBe('success'); | ||
| const trace = body.decision_trace as Record<string, unknown>; | ||
| expect(trace.selected_namespace).toBe('papers'); | ||
| expect(trace.selected_tool).toBe('detailed'); | ||
| expect(query).toHaveBeenCalledWith( | ||
| expect.objectContaining({ | ||
| namespace: 'papers', | ||
| topK: 8, | ||
| useReranking: true, | ||
| }) | ||
| ); | ||
| const result = body.result as Record<string, unknown>; | ||
| expect(result.mode).toBe('query_detailed'); | ||
| }); | ||
|
|
||
| it('runs count when preferred_tool is count', async () => { | ||
| const server = createMockServer(); | ||
| registerGuidedQueryTool(server as never); | ||
| const count = mockedGetClient().count as ReturnType<typeof vi.fn>; | ||
|
|
||
| const body = parseToolJson( | ||
| await server.getHandler('guided_query')!({ | ||
| user_query: 'browse', | ||
| namespace: 'papers', | ||
| preferred_tool: 'count', | ||
| }) | ||
| ); | ||
|
|
||
| expect(count).toHaveBeenCalledWith( | ||
| expect.objectContaining({ | ||
| query: 'browse', | ||
| namespace: 'papers', | ||
| }) | ||
| ); | ||
| const result = body.result as Record<string, unknown>; | ||
| expect(result.tool).toBe('count'); | ||
| expect(result.count).toBe(7); | ||
| }); | ||
|
|
||
| it('returns error when user_query is empty', async () => { | ||
| const server = createMockServer(); | ||
| registerGuidedQueryTool(server as never); | ||
|
|
||
| const raw = await server.getHandler('guided_query')!({ | ||
| user_query: ' ', | ||
| namespace: 'papers', | ||
| }); | ||
|
|
||
| expect((raw as { isError?: boolean }).isError).toBe(true); | ||
| expect(parseToolJson(raw).message).toBe('user_query cannot be empty'); | ||
| }); | ||
|
|
||
| it('returns error when no namespace can be resolved', async () => { | ||
| mockedGetNamespaces.mockResolvedValue({ | ||
| data: [], | ||
| cache_hit: false, | ||
| expires_at: Date.now() + 1_800_000, | ||
| }); | ||
|
|
||
| const server = createMockServer(); | ||
| registerGuidedQueryTool(server as never); | ||
|
|
||
| const body = parseToolJson( | ||
| await server.getHandler('guided_query')!({ | ||
| user_query: 'hello world', | ||
| }) | ||
| ); | ||
|
|
||
| expect(body.status).toBe('error'); | ||
| expect(String(body.message)).toContain('No namespace available'); | ||
| }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| import { beforeEach, describe, expect, it, vi } from 'vitest'; | ||
| import { getNamespacesWithCache } from '../namespaces-cache.js'; | ||
| import { registerListNamespacesTool } from './list-namespaces-tool.js'; | ||
| import { createMockServer, parseToolJson } from './test-helpers.js'; | ||
|
|
||
| vi.mock('../namespaces-cache.js', () => ({ | ||
| getNamespacesWithCache: vi.fn(), | ||
| })); | ||
|
|
||
| const mockedGetNamespaces = vi.mocked(getNamespacesWithCache); | ||
|
|
||
| describe('list_namespaces tool handler', () => { | ||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| }); | ||
|
|
||
| it('returns success with namespaces on happy path', async () => { | ||
| const expiresAt = Date.now() + 1_800_000; | ||
| mockedGetNamespaces.mockResolvedValue({ | ||
| data: [ | ||
| { namespace: 'a', recordCount: 1, metadata: { title: 'string' } }, | ||
| { namespace: 'b', recordCount: 2, metadata: { url: 'string' } }, | ||
| ], | ||
| cache_hit: false, | ||
| expires_at: expiresAt, | ||
| }); | ||
|
|
||
| const server = createMockServer(); | ||
| registerListNamespacesTool(server as never); | ||
| const handler = server.getHandler('list_namespaces')!; | ||
| const raw = await handler({}); | ||
|
|
||
| const body = parseToolJson(raw); | ||
| expect(body.status).toBe('success'); | ||
| expect(body.cache_hit).toBe(false); | ||
| expect(body.count).toBe(2); | ||
| expect(body.namespaces).toEqual([ | ||
| { name: 'a', record_count: 1, metadata_fields: { title: 'string' } }, | ||
| { name: 'b', record_count: 2, metadata_fields: { url: 'string' } }, | ||
| ]); | ||
| expect(typeof body.cache_ttl_seconds).toBe('number'); | ||
| }); | ||
|
|
||
| it('propagates cache_hit when namespaces cache is warm', async () => { | ||
| mockedGetNamespaces.mockResolvedValue({ | ||
| data: [{ namespace: 'x', recordCount: 0, metadata: {} }], | ||
| cache_hit: true, | ||
| expires_at: Date.now() + 60_000, | ||
| }); | ||
|
|
||
| const server = createMockServer(); | ||
| registerListNamespacesTool(server as never); | ||
| const body = parseToolJson(await server.getHandler('list_namespaces')!({})); | ||
|
|
||
| expect(body.cache_hit).toBe(true); | ||
| expect(body.count).toBe(1); | ||
| }); | ||
|
|
||
| it('returns error payload when getNamespacesWithCache throws', async () => { | ||
| mockedGetNamespaces.mockRejectedValue(new Error('network down')); | ||
|
|
||
| const server = createMockServer(); | ||
| registerListNamespacesTool(server as never); | ||
| const raw = await server.getHandler('list_namespaces')!({}); | ||
| const payload = raw as { isError?: boolean }; | ||
|
|
||
| expect(payload.isError).toBe(true); | ||
| const body = parseToolJson(raw); | ||
| expect(body.status).toBe('error'); | ||
| expect(String(body.message)).toBe('Failed to list namespaces'); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,137 @@ | ||
| import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| import { DEFAULT_QUERY_DOCUMENTS_TOP_K, QUERY_DOCUMENTS_MAX_CHUNKS } from '../../constants.js'; | ||
| import { getPineconeClient } from '../client-context.js'; | ||
| import { reassembleByDocument } from '../reassemble-documents.js'; | ||
| import * as suggestionFlow from '../suggestion-flow.js'; | ||
| import { registerQueryDocumentsTool } from './query-documents-tool.js'; | ||
| import { createMockServer, makeSearchResult, parseToolJson } from './test-helpers.js'; | ||
|
|
||
| vi.mock('../client-context.js', () => ({ | ||
| getPineconeClient: vi.fn(), | ||
| })); | ||
|
|
||
| vi.mock('../reassemble-documents.js', () => ({ | ||
| reassembleByDocument: vi.fn(), | ||
| })); | ||
|
|
||
| const mockedGetClient = vi.mocked(getPineconeClient); | ||
| const mockedReassemble = vi.mocked(reassembleByDocument); | ||
|
|
||
| describe('query_documents tool handler', () => { | ||
| const flowOk = { | ||
| ok: true as const, | ||
| flow: { | ||
| updatedAt: Date.now(), | ||
| recommended_tool: 'detailed' as const, | ||
| suggested_fields: [], | ||
| user_query: 'q', | ||
| }, | ||
| }; | ||
|
|
||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| vi.spyOn(suggestionFlow, 'requireSuggested').mockReturnValue(flowOk); | ||
| mockedReassemble.mockReturnValue([ | ||
| { | ||
| document_id: 'D1', | ||
| merged_content: 'full doc text', | ||
| metadata: { document_number: 'D1' }, | ||
| chunk_count: 3, | ||
| best_score: 0.99, | ||
| }, | ||
| ]); | ||
| mockedGetClient.mockReturnValue({ | ||
| query: vi.fn().mockResolvedValue([makeSearchResult()]), | ||
| } as never); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| vi.restoreAllMocks(); | ||
| }); | ||
|
|
||
| it('happy path: queries chunks, reassembles, and returns documents', async () => { | ||
| const server = createMockServer(); | ||
| registerQueryDocumentsTool(server as never); | ||
| const query = mockedGetClient().query as ReturnType<typeof vi.fn>; | ||
|
|
||
| const body = parseToolJson( | ||
| await server.getHandler('query_documents')!({ | ||
| query_text: 'semantic question', | ||
| namespace: 'wg21', | ||
| top_k: DEFAULT_QUERY_DOCUMENTS_TOP_K, | ||
| }) | ||
| ); | ||
|
|
||
| const expectedTopK = Math.min(QUERY_DOCUMENTS_MAX_CHUNKS, DEFAULT_QUERY_DOCUMENTS_TOP_K * 50); | ||
| expect(query).toHaveBeenCalledWith( | ||
| expect.objectContaining({ | ||
| query: 'semantic question', | ||
| namespace: 'wg21', | ||
| topK: expectedTopK, | ||
| useReranking: true, | ||
| fields: undefined, | ||
| }) | ||
| ); | ||
| expect(mockedReassemble).toHaveBeenCalled(); | ||
| expect(body.status).toBe('success'); | ||
| const docs = body.documents as Array<{ merged_content: string }>; | ||
| expect(docs[0].merged_content).toBe('full doc text'); | ||
| }); | ||
|
|
||
| it('returns error when query_text is empty', async () => { | ||
| const server = createMockServer(); | ||
| registerQueryDocumentsTool(server as never); | ||
| const query = mockedGetClient().query as ReturnType<typeof vi.fn>; | ||
|
|
||
| const raw = await server.getHandler('query_documents')!({ | ||
| query_text: '', | ||
| namespace: 'wg21', | ||
| }); | ||
|
|
||
| expect((raw as { isError?: boolean }).isError).toBe(true); | ||
| expect(query).not.toHaveBeenCalled(); | ||
| expect(parseToolJson(raw).message).toBe('query_text cannot be empty'); | ||
| }); | ||
|
|
||
| it('returns flow error when suggest_query_params gate fails', async () => { | ||
| vi.spyOn(suggestionFlow, 'requireSuggested').mockReturnValue({ | ||
| ok: false, | ||
| message: | ||
| 'Flow requires suggest_query_params first. Call suggest_query_params with namespace and user_query before query/count tools.', | ||
| }); | ||
|
|
||
| const server = createMockServer(); | ||
| registerQueryDocumentsTool(server as never); | ||
| const query = mockedGetClient().query as ReturnType<typeof vi.fn>; | ||
|
|
||
| const body = parseToolJson( | ||
| await server.getHandler('query_documents')!({ | ||
| query_text: 'ok', | ||
| namespace: 'wg21', | ||
| }) | ||
| ); | ||
|
|
||
| expect(body.status).toBe('error'); | ||
| expect(query).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('returns TTL expiry error from requireSuggested', async () => { | ||
| vi.spyOn(suggestionFlow, 'requireSuggested').mockReturnValue({ | ||
| ok: false, | ||
| message: | ||
| 'Previous suggest_query_params context expired (30 minutes). Call suggest_query_params again before query/count tools.', | ||
| }); | ||
|
|
||
| const server = createMockServer(); | ||
| registerQueryDocumentsTool(server as never); | ||
|
|
||
| const body = parseToolJson( | ||
| await server.getHandler('query_documents')!({ | ||
| query_text: 'ok', | ||
| namespace: 'wg21', | ||
| }) | ||
| ); | ||
|
|
||
| expect(body.message).toContain('expired'); | ||
| }); | ||
| }); | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.