Skip to content

Commit 335bda1

Browse files
jonathanMLDevzho
andauthored
added tests (cppalliance#73)
* added tests * fixed line error * fixed test errors * fixed test errors --------- Co-authored-by: zho <jornathanm910923@gmail.com>
1 parent c07279f commit 335bda1

7 files changed

Lines changed: 702 additions & 32 deletions
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest';
2+
import { getPineconeClient } from '../client-context.js';
3+
import { getNamespacesWithCache } from '../namespaces-cache.js';
4+
import { registerGuidedQueryTool } from './guided-query-tool.js';
5+
import {
6+
createMockServer,
7+
makeNamespaceCacheEntry,
8+
makeSearchResult,
9+
parseToolJson,
10+
} from './test-helpers.js';
11+
12+
vi.mock('../client-context.js', () => ({
13+
getPineconeClient: vi.fn(),
14+
}));
15+
16+
vi.mock('../namespaces-cache.js', () => ({
17+
getNamespacesWithCache: vi.fn(),
18+
}));
19+
20+
/** Real `markSuggested` may call `getServerConfig()` during sweep (CI has no API key); isolate the handler. */
21+
vi.mock('../suggestion-flow.js', () => ({
22+
markSuggested: vi.fn(),
23+
}));
24+
25+
const mockedGetNamespaces = vi.mocked(getNamespacesWithCache);
26+
const mockedGetClient = vi.mocked(getPineconeClient);
27+
28+
describe('guided_query tool handler', () => {
29+
const nsEntry = makeNamespaceCacheEntry('papers', {
30+
document_number: 'string',
31+
title: 'string',
32+
url: 'string',
33+
author: 'string',
34+
chunk_text: 'string',
35+
});
36+
37+
beforeEach(() => {
38+
vi.clearAllMocks();
39+
mockedGetNamespaces.mockResolvedValue({
40+
data: [nsEntry],
41+
cache_hit: false,
42+
expires_at: Date.now() + 1_800_000,
43+
});
44+
mockedGetClient.mockReturnValue({
45+
query: vi.fn().mockResolvedValue([makeSearchResult()]),
46+
count: vi.fn().mockResolvedValue({ count: 7, truncated: false }),
47+
} as never);
48+
});
49+
50+
it('runs query_detailed path on auto when user asks for content', async () => {
51+
const server = createMockServer();
52+
registerGuidedQueryTool(server as never);
53+
const query = mockedGetClient().query as ReturnType<typeof vi.fn>;
54+
55+
const body = parseToolJson(
56+
await server.getHandler('guided_query')!({
57+
user_query: 'What does the paper say about contracts?',
58+
namespace: 'papers',
59+
top_k: 8,
60+
preferred_tool: 'auto',
61+
enrich_urls: false,
62+
})
63+
);
64+
65+
expect(body.status).toBe('success');
66+
const trace = body.decision_trace as Record<string, unknown>;
67+
expect(trace.selected_namespace).toBe('papers');
68+
expect(trace.selected_tool).toBe('detailed');
69+
expect(query).toHaveBeenCalledWith(
70+
expect.objectContaining({
71+
namespace: 'papers',
72+
topK: 8,
73+
useReranking: true,
74+
})
75+
);
76+
const result = body.result as Record<string, unknown>;
77+
expect(result.mode).toBe('query_detailed');
78+
});
79+
80+
it('runs count when preferred_tool is count', async () => {
81+
const server = createMockServer();
82+
registerGuidedQueryTool(server as never);
83+
const count = mockedGetClient().count as ReturnType<typeof vi.fn>;
84+
85+
const body = parseToolJson(
86+
await server.getHandler('guided_query')!({
87+
user_query: 'browse',
88+
namespace: 'papers',
89+
preferred_tool: 'count',
90+
})
91+
);
92+
93+
expect(count).toHaveBeenCalledWith(
94+
expect.objectContaining({
95+
query: 'browse',
96+
namespace: 'papers',
97+
})
98+
);
99+
const result = body.result as Record<string, unknown>;
100+
expect(result.tool).toBe('count');
101+
expect(result.count).toBe(7);
102+
});
103+
104+
it('returns error when user_query is empty', async () => {
105+
const server = createMockServer();
106+
registerGuidedQueryTool(server as never);
107+
108+
const raw = await server.getHandler('guided_query')!({
109+
user_query: ' ',
110+
namespace: 'papers',
111+
});
112+
113+
expect((raw as { isError?: boolean }).isError).toBe(true);
114+
expect(parseToolJson(raw).message).toBe('user_query cannot be empty');
115+
});
116+
117+
it('returns error when no namespace can be resolved', async () => {
118+
mockedGetNamespaces.mockResolvedValue({
119+
data: [],
120+
cache_hit: false,
121+
expires_at: Date.now() + 1_800_000,
122+
});
123+
124+
const server = createMockServer();
125+
registerGuidedQueryTool(server as never);
126+
127+
const body = parseToolJson(
128+
await server.getHandler('guided_query')!({
129+
user_query: 'hello world',
130+
})
131+
);
132+
133+
expect(body.status).toBe('error');
134+
expect(String(body.message)).toContain('No namespace available');
135+
});
136+
});
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest';
2+
import { getNamespacesWithCache } from '../namespaces-cache.js';
3+
import { registerListNamespacesTool } from './list-namespaces-tool.js';
4+
import { createMockServer, parseToolJson } from './test-helpers.js';
5+
6+
vi.mock('../namespaces-cache.js', () => ({
7+
getNamespacesWithCache: vi.fn(),
8+
}));
9+
10+
const mockedGetNamespaces = vi.mocked(getNamespacesWithCache);
11+
12+
describe('list_namespaces tool handler', () => {
13+
beforeEach(() => {
14+
vi.clearAllMocks();
15+
});
16+
17+
it('returns success with namespaces on happy path', async () => {
18+
const expiresAt = Date.now() + 1_800_000;
19+
mockedGetNamespaces.mockResolvedValue({
20+
data: [
21+
{ namespace: 'a', recordCount: 1, metadata: { title: 'string' } },
22+
{ namespace: 'b', recordCount: 2, metadata: { url: 'string' } },
23+
],
24+
cache_hit: false,
25+
expires_at: expiresAt,
26+
});
27+
28+
const server = createMockServer();
29+
registerListNamespacesTool(server as never);
30+
const handler = server.getHandler('list_namespaces')!;
31+
const raw = await handler({});
32+
33+
const body = parseToolJson(raw);
34+
expect(body.status).toBe('success');
35+
expect(body.cache_hit).toBe(false);
36+
expect(body.count).toBe(2);
37+
expect(body.namespaces).toEqual([
38+
{ name: 'a', record_count: 1, metadata_fields: { title: 'string' } },
39+
{ name: 'b', record_count: 2, metadata_fields: { url: 'string' } },
40+
]);
41+
expect(typeof body.cache_ttl_seconds).toBe('number');
42+
});
43+
44+
it('propagates cache_hit when namespaces cache is warm', async () => {
45+
mockedGetNamespaces.mockResolvedValue({
46+
data: [{ namespace: 'x', recordCount: 0, metadata: {} }],
47+
cache_hit: true,
48+
expires_at: Date.now() + 60_000,
49+
});
50+
51+
const server = createMockServer();
52+
registerListNamespacesTool(server as never);
53+
const body = parseToolJson(await server.getHandler('list_namespaces')!({}));
54+
55+
expect(body.cache_hit).toBe(true);
56+
expect(body.count).toBe(1);
57+
});
58+
59+
it('returns error payload when getNamespacesWithCache throws', async () => {
60+
mockedGetNamespaces.mockRejectedValue(new Error('network down'));
61+
62+
const server = createMockServer();
63+
registerListNamespacesTool(server as never);
64+
const raw = await server.getHandler('list_namespaces')!({});
65+
const payload = raw as { isError?: boolean };
66+
67+
expect(payload.isError).toBe(true);
68+
const body = parseToolJson(raw);
69+
expect(body.status).toBe('error');
70+
expect(String(body.message)).toBe('Failed to list namespaces');
71+
});
72+
});
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2+
import { DEFAULT_QUERY_DOCUMENTS_TOP_K, QUERY_DOCUMENTS_MAX_CHUNKS } from '../../constants.js';
3+
import { getPineconeClient } from '../client-context.js';
4+
import { reassembleByDocument } from '../reassemble-documents.js';
5+
import * as suggestionFlow from '../suggestion-flow.js';
6+
import { registerQueryDocumentsTool } from './query-documents-tool.js';
7+
import { createMockServer, makeSearchResult, parseToolJson } from './test-helpers.js';
8+
9+
vi.mock('../client-context.js', () => ({
10+
getPineconeClient: vi.fn(),
11+
}));
12+
13+
vi.mock('../reassemble-documents.js', () => ({
14+
reassembleByDocument: vi.fn(),
15+
}));
16+
17+
const mockedGetClient = vi.mocked(getPineconeClient);
18+
const mockedReassemble = vi.mocked(reassembleByDocument);
19+
20+
describe('query_documents tool handler', () => {
21+
const flowOk = {
22+
ok: true as const,
23+
flow: {
24+
updatedAt: Date.now(),
25+
recommended_tool: 'detailed' as const,
26+
suggested_fields: [],
27+
user_query: 'q',
28+
},
29+
};
30+
31+
beforeEach(() => {
32+
vi.clearAllMocks();
33+
vi.spyOn(suggestionFlow, 'requireSuggested').mockReturnValue(flowOk);
34+
mockedReassemble.mockReturnValue([
35+
{
36+
document_id: 'D1',
37+
merged_content: 'full doc text',
38+
metadata: { document_number: 'D1' },
39+
chunk_count: 3,
40+
best_score: 0.99,
41+
},
42+
]);
43+
mockedGetClient.mockReturnValue({
44+
query: vi.fn().mockResolvedValue([makeSearchResult()]),
45+
} as never);
46+
});
47+
48+
afterEach(() => {
49+
vi.restoreAllMocks();
50+
});
51+
52+
it('happy path: queries chunks, reassembles, and returns documents', async () => {
53+
const server = createMockServer();
54+
registerQueryDocumentsTool(server as never);
55+
const query = mockedGetClient().query as ReturnType<typeof vi.fn>;
56+
57+
const body = parseToolJson(
58+
await server.getHandler('query_documents')!({
59+
query_text: 'semantic question',
60+
namespace: 'wg21',
61+
top_k: DEFAULT_QUERY_DOCUMENTS_TOP_K,
62+
})
63+
);
64+
65+
const expectedTopK = Math.min(QUERY_DOCUMENTS_MAX_CHUNKS, DEFAULT_QUERY_DOCUMENTS_TOP_K * 50);
66+
expect(query).toHaveBeenCalledWith(
67+
expect.objectContaining({
68+
query: 'semantic question',
69+
namespace: 'wg21',
70+
topK: expectedTopK,
71+
useReranking: true,
72+
fields: undefined,
73+
})
74+
);
75+
expect(mockedReassemble).toHaveBeenCalled();
76+
expect(body.status).toBe('success');
77+
const docs = body.documents as Array<{ merged_content: string }>;
78+
expect(docs[0].merged_content).toBe('full doc text');
79+
});
80+
81+
it('returns error when query_text is empty', async () => {
82+
const server = createMockServer();
83+
registerQueryDocumentsTool(server as never);
84+
const query = mockedGetClient().query as ReturnType<typeof vi.fn>;
85+
86+
const raw = await server.getHandler('query_documents')!({
87+
query_text: '',
88+
namespace: 'wg21',
89+
});
90+
91+
expect((raw as { isError?: boolean }).isError).toBe(true);
92+
expect(query).not.toHaveBeenCalled();
93+
expect(parseToolJson(raw).message).toBe('query_text cannot be empty');
94+
});
95+
96+
it('returns flow error when suggest_query_params gate fails', async () => {
97+
vi.spyOn(suggestionFlow, 'requireSuggested').mockReturnValue({
98+
ok: false,
99+
message:
100+
'Flow requires suggest_query_params first. Call suggest_query_params with namespace and user_query before query/count tools.',
101+
});
102+
103+
const server = createMockServer();
104+
registerQueryDocumentsTool(server as never);
105+
const query = mockedGetClient().query as ReturnType<typeof vi.fn>;
106+
107+
const body = parseToolJson(
108+
await server.getHandler('query_documents')!({
109+
query_text: 'ok',
110+
namespace: 'wg21',
111+
})
112+
);
113+
114+
expect(body.status).toBe('error');
115+
expect(query).not.toHaveBeenCalled();
116+
});
117+
118+
it('returns TTL expiry error from requireSuggested', async () => {
119+
vi.spyOn(suggestionFlow, 'requireSuggested').mockReturnValue({
120+
ok: false,
121+
message:
122+
'Previous suggest_query_params context expired (30 minutes). Call suggest_query_params again before query/count tools.',
123+
});
124+
125+
const server = createMockServer();
126+
registerQueryDocumentsTool(server as never);
127+
128+
const body = parseToolJson(
129+
await server.getHandler('query_documents')!({
130+
query_text: 'ok',
131+
namespace: 'wg21',
132+
})
133+
);
134+
135+
expect(body.message).toContain('expired');
136+
});
137+
});

0 commit comments

Comments
 (0)