11import { describe , it , expect , vi , beforeEach , afterEach } from 'vitest' ;
2- import { mock } from 'bun:test' ;
32import fs from 'node:fs' ;
43import path from 'node:path' ;
54import os from 'node:os' ;
65import { writeParquet } from '../docs/write-parquet.ts' ;
76import * as fetchModule from '../docs/fetch.ts' ;
87
9- mock . module ( '@opencode-ai/plugin/tool' , ( ) => {
10- const mockSchema = {
11- string : ( ) => ( {
12- describe : ( d : string ) => ( { _type : 'string' , _description : d } ) ,
13- } ) ,
14- } ;
15- const toolFn = Object . assign ( ( input : Record < string , unknown > ) => input , {
16- schema : mockSchema ,
17- } ) ;
18- return { tool : toolFn } ;
19- } ) ;
20-
218// eslint-disable-next-line @typescript-eslint/no-explicit-any
229const plugin = ( await import ( '../index.ts' ) ) . default as any ;
2310
@@ -39,15 +26,15 @@ describe('Plugin', () => {
3926 await writeParquet (
4027 [
4128 {
42- url : '/docs/ foundry/ontology/overview/' ,
29+ url : '/foundry/ontology/overview/' ,
4330 title : 'Ontology Overview' ,
4431 content : 'This is the ontology overview content.' ,
4532 wordCount : 6 ,
4633 meta : { } ,
4734 fetchedAt : '2025-01-01T00:00:00.000Z' ,
4835 } ,
4936 {
50- url : '/docs/ foundry/actions/' ,
37+ url : '/foundry/actions/' ,
5138 title : 'Actions' ,
5239 content : 'Actions documentation content.' ,
5340 wordCount : 3 ,
@@ -59,6 +46,67 @@ describe('Plugin', () => {
5946 ) ;
6047 }
6148
49+ async function seedDatabaseMany ( ) : Promise < void > {
50+ fs . mkdirSync ( path . join ( tmpDir , 'data' ) , { recursive : true } ) ;
51+
52+ const rows : Array < {
53+ url : string ;
54+ title : string ;
55+ content : string ;
56+ wordCount : number ;
57+ meta : Record < string , unknown > ;
58+ fetchedAt : string ;
59+ } > = [ ] ;
60+
61+ // Intentionally unsorted URL insertion to prove deterministic ordering.
62+ rows . push ( {
63+ url : '/apollo/zzz/' ,
64+ title : 'Apollo ZZZ' ,
65+ content : 'apollo content' ,
66+ wordCount : 2 ,
67+ meta : { } ,
68+ fetchedAt : '2025-01-01T00:00:00.000Z' ,
69+ } ) ;
70+ rows . push ( {
71+ url : '/foundry/zzz/' ,
72+ title : 'Foundry ZZZ' ,
73+ content : 'foundry content' ,
74+ wordCount : 2 ,
75+ meta : { } ,
76+ fetchedAt : '2025-01-01T00:00:00.000Z' ,
77+ } ) ;
78+ rows . push ( {
79+ url : '/gotham/aaa/' ,
80+ title : 'Gotham AAA' ,
81+ content : 'gotham content' ,
82+ wordCount : 2 ,
83+ meta : { } ,
84+ fetchedAt : '2025-01-01T00:00:00.000Z' ,
85+ } ) ;
86+
87+ rows . push ( {
88+ url : '/foundry/compute-modules/overview/' ,
89+ title : 'Compute modules' ,
90+ content : 'Compute modules overview content.' ,
91+ wordCount : 4 ,
92+ meta : { } ,
93+ fetchedAt : '2025-01-01T00:00:00.000Z' ,
94+ } ) ;
95+
96+ for ( let i = 0 ; i < 60 ; i += 1 ) {
97+ rows . push ( {
98+ url : `/foundry/many/${ String ( i ) . padStart ( 2 , '0' ) } /` ,
99+ title : `Foundry Many ${ i } ` ,
100+ content : 'foundry many content' ,
101+ wordCount : 3 ,
102+ meta : { } ,
103+ fetchedAt : '2025-01-01T00:00:00.000Z' ,
104+ } ) ;
105+ }
106+
107+ await writeParquet ( rows , dbPath ) ;
108+ }
109+
62110 it ( 'returns Hooks with tool property containing exactly 2 tools' , async ( ) => {
63111 const hooks = await plugin ( { worktree : tmpDir } ) ;
64112
@@ -76,6 +124,8 @@ describe('Plugin', () => {
76124 expect ( getDocPage . description ) . toBeTruthy ( ) ;
77125 expect ( getDocPage . description ) . toContain ( 'documentation page' ) ;
78126 expect ( getDocPage . args ) . toHaveProperty ( 'url' ) ;
127+ expect ( getDocPage . args ) . toHaveProperty ( 'query' ) ;
128+ expect ( getDocPage . args ) . toHaveProperty ( 'scope' ) ;
79129 } ) ;
80130
81131 it ( 'list_all_docs tool has description and no required args' , async ( ) => {
@@ -84,15 +134,16 @@ describe('Plugin', () => {
84134
85135 expect ( listAllDocs . description ) . toBeTruthy ( ) ;
86136 expect ( listAllDocs . description ) . toContain ( 'documentation' ) ;
87- expect ( Object . keys ( listAllDocs . args ) ) . toHaveLength ( 0 ) ;
137+ const keys = Object . keys ( listAllDocs . args ) . sort ( ( a , b ) => a . localeCompare ( b ) ) ;
138+ expect ( keys ) . toEqual ( [ 'limit' , 'offset' , 'query' , 'scope' ] ) ;
88139 } ) ;
89140
90141 it ( 'get_doc_page execute returns page content when DB exists' , async ( ) => {
91142 await seedDatabase ( ) ;
92143 const hooks = await plugin ( { worktree : tmpDir } ) ;
93144
94145 const result = await hooks . tool [ 'get_doc_page' ] . execute (
95- { url : '/docs/ foundry/ontology/overview/' } ,
146+ { url : '/foundry/ontology/overview/' } ,
96147 { }
97148 ) ;
98149
@@ -115,9 +166,127 @@ describe('Plugin', () => {
115166
116167 const result = await hooks . tool [ 'list_all_docs' ] . execute ( { } , { } ) ;
117168
118- expect ( result ) . toContain ( 'Available Palantir Foundry Documentation (2 pages)' ) ;
119- expect ( result ) . toContain ( '- Ontology Overview (/docs/foundry/ontology/overview/)' ) ;
120- expect ( result ) . toContain ( '- Actions (/docs/foundry/actions/)' ) ;
169+ expect ( result ) . toContain ( 'Available Palantir Documentation Pages' ) ;
170+ expect ( result ) . toContain ( 'scope=foundry' ) ;
171+ expect ( result ) . toContain ( 'query=' ) ;
172+ expect ( result ) . toContain ( 'total=2' ) ;
173+ expect ( result ) . toContain ( '- Ontology Overview (/foundry/ontology/overview/)' ) ;
174+ expect ( result ) . toContain ( '- Actions (/foundry/actions/)' ) ;
175+ } ) ;
176+
177+ it ( 'list_all_docs defaults to bounded results and foundry scope' , async ( ) => {
178+ await seedDatabaseMany ( ) ;
179+ const hooks = await plugin ( { worktree : tmpDir } ) ;
180+
181+ const result = await hooks . tool [ 'list_all_docs' ] . execute ( { } , { } ) ;
182+
183+ expect ( result ) . toContain ( 'scope=foundry' ) ;
184+ expect ( result ) . toContain ( 'limit=50' ) ;
185+ expect ( result ) . toContain ( 'Next: call list_all_docs' ) ;
186+ expect ( result ) . not . toContain ( '/apollo/' ) ;
187+ expect ( result ) . not . toContain ( '/gotham/' ) ;
188+
189+ const lineCount = result . split ( '\n' ) . filter ( ( l ) => l . startsWith ( '- ' ) ) . length ;
190+ expect ( lineCount ) . toBeLessThanOrEqual ( 50 ) ;
191+ } ) ;
192+
193+ it ( 'list_all_docs pagination is deterministic (sorted by url)' , async ( ) => {
194+ await seedDatabaseMany ( ) ;
195+ const hooks = await plugin ( { worktree : tmpDir } ) ;
196+
197+ const first = await hooks . tool [ 'list_all_docs' ] . execute (
198+ { scope : 'all' , limit : 1 , offset : 0 } ,
199+ { }
200+ ) ;
201+ const second = await hooks . tool [ 'list_all_docs' ] . execute (
202+ { scope : 'all' , limit : 1 , offset : 1 } ,
203+ { }
204+ ) ;
205+
206+ const getOnlyUrl = ( text : string ) : string => {
207+ const line = text . split ( '\n' ) . find ( ( l ) => l . startsWith ( '- ' ) && l . includes ( '(/' ) ) ;
208+ expect ( line ) . toBeTruthy ( ) ;
209+ const match = line ?. match ( / \( ( \/ [ ^ ) ] + ) \) / ) ;
210+ expect ( match ?. [ 1 ] ) . toBeTruthy ( ) ;
211+ return match ?. [ 1 ] as string ;
212+ } ;
213+
214+ const url0 = getOnlyUrl ( first ) ;
215+ const url1 = getOnlyUrl ( second ) ;
216+ expect ( url0 ) . not . toBe ( url1 ) ;
217+
218+ // Because ordering is by URL, /apollo/... sorts before /foundry/... and /gotham/...
219+ expect ( url0 ) . toBe ( '/apollo/zzz/' ) ;
220+ expect ( url1 ) . toBe ( '/foundry/compute-modules/overview/' ) ;
221+ } ) ;
222+
223+ it ( 'list_all_docs scope=all includes non-foundry URLs' , async ( ) => {
224+ await seedDatabaseMany ( ) ;
225+ const hooks = await plugin ( { worktree : tmpDir } ) ;
226+
227+ const result = await hooks . tool [ 'list_all_docs' ] . execute (
228+ { scope : 'all' , limit : 5 , offset : 0 } ,
229+ { }
230+ ) ;
231+
232+ expect ( result ) . toContain ( 'scope=all' ) ;
233+ expect ( result ) . toContain ( '/apollo/zzz/' ) ;
234+ } ) ;
235+
236+ it ( 'list_all_docs query filters and ranks results' , async ( ) => {
237+ await seedDatabaseMany ( ) ;
238+ const hooks = await plugin ( { worktree : tmpDir } ) ;
239+
240+ const result = await hooks . tool [ 'list_all_docs' ] . execute (
241+ { query : 'compute modules' , scope : 'foundry' , limit : 10 , offset : 0 } ,
242+ { }
243+ ) ;
244+
245+ expect ( result ) . toContain ( 'scope=foundry' ) ;
246+ expect ( result ) . toContain ( 'query=compute modules' ) ;
247+ expect ( result ) . toContain ( '/foundry/compute-modules/overview/' ) ;
248+ } ) ;
249+
250+ it ( 'get_doc_page accepts common URL variants (missing /docs prefix, missing trailing slash)' , async ( ) => {
251+ await seedDatabaseMany ( ) ;
252+ const hooks = await plugin ( { worktree : tmpDir } ) ;
253+
254+ const result = await hooks . tool [ 'get_doc_page' ] . execute (
255+ { url : '/docs/foundry/compute-modules/overview' } ,
256+ { }
257+ ) ;
258+
259+ expect ( result ) . toContain ( 'Compute modules overview content.' ) ;
260+ } ) ;
261+
262+ it ( 'get_doc_page can resolve a page from a free-text query' , async ( ) => {
263+ await seedDatabaseMany ( ) ;
264+ const hooks = await plugin ( { worktree : tmpDir } ) ;
265+
266+ const result = await hooks . tool [ 'get_doc_page' ] . execute (
267+ { query : 'compute modules' , scope : 'foundry' } ,
268+ { }
269+ ) ;
270+
271+ expect ( result ) . toContain ( 'Matched:' ) ;
272+ expect ( result ) . toContain ( '/foundry/compute-modules/overview/' ) ;
273+ expect ( result ) . toContain ( 'Compute modules overview content.' ) ;
274+ } ) ;
275+
276+ it ( 'list_all_docs invalid args fail safely' , async ( ) => {
277+ await seedDatabaseMany ( ) ;
278+ const hooks = await plugin ( { worktree : tmpDir } ) ;
279+
280+ const badLimit = await hooks . tool [ 'list_all_docs' ] . execute ( { limit : 0 } as never , { } ) ;
281+ expect ( badLimit ) . toContain ( '[ERROR]' ) ;
282+ expect ( badLimit ) . toContain ( 'limit' ) ;
283+
284+ const badOffset = await hooks . tool [ 'list_all_docs' ] . execute ( { offset : - 1 } as never , { } ) ;
285+ expect ( badOffset ) . toContain ( '[ERROR]' ) ;
286+ expect ( badOffset ) . toContain ( 'offset' ) ;
287+
288+ const badScope = await hooks . tool [ 'list_all_docs' ] . execute ( { scope : 'nope' } as never , { } ) ;
289+ expect ( badScope ) . toContain ( '[ERROR]' ) ;
121290 } ) ;
122291
123292 it ( 'tools return helpful message when docs.db does not exist' , async ( ) => {
@@ -185,6 +354,6 @@ describe('Plugin', () => {
185354
186355 // Second call reuses cached DB — list_all_docs also works
187356 const listResult = await hooks . tool [ 'list_all_docs' ] . execute ( { } , { } ) ;
188- expect ( listResult ) . toContain ( '2 pages ' ) ;
357+ expect ( listResult ) . toContain ( 'total=2 ' ) ;
189358 } ) ;
190359} ) ;
0 commit comments