99 */
1010
1111import { strict as assert } from 'node:assert' ;
12- import { createHash } from 'node:crypto' ;
1312import { readFileSync } from 'node:fs' ;
1413import http from 'node:http' ;
1514import net from 'node:net' ;
@@ -64,28 +63,26 @@ function propertyStructuralSnapshot(properties: Record<string, Record<string, un
6463}
6564
6665function toolSnapshot ( tool : ToolSnapshotInput ) {
66+ const properties = tool . inputSchema . properties ?? { } ;
6767 return {
6868 name : tool . name ,
6969 annotations : tool . annotations ,
7070 required : tool . inputSchema . required ?? [ ] ,
71- properties : propertyStructuralSnapshot ( tool . inputSchema . properties ?? { } ) ,
71+ propertyOrder : Object . keys ( properties ) ,
72+ properties : propertyStructuralSnapshot ( properties ) ,
7273 } ;
7374}
7475
7576function toolStructuralSnapshot ( tools : ToolSnapshotInput [ ] ) {
7677 return tools . map ( toolSnapshot ) ;
7778}
7879
79- function structuralDigest ( value : unknown ) : string {
80- return createHash ( 'sha256' ) . update ( JSON . stringify ( value ) ) . digest ( 'hex' ) ;
81- }
82-
8380function wireJson < T > ( value : T ) : T {
8481 return JSON . parse ( JSON . stringify ( value ) ) as T ;
8582}
8683
87- const FROZEN_FULL_TOOL_SNAPSHOTS = [
88- { name : 'searxng_web_search' , annotations : { readOnlyHint : true , openWorldHint : true } , required : [ 'query' ] , properties : {
84+ const FULL_TOOL_STRUCTURAL_SNAPSHOTS = [
85+ { name : 'searxng_web_search' , annotations : { readOnlyHint : true , openWorldHint : true } , required : [ 'query' ] , propertyOrder : [ 'query' , 'pageno' , 'time_range' , 'language' , 'safesearch' , 'min_score' , 'num_results' , 'categories' , 'engines' , 'response_format' , 'result_detail' ] , properties : {
8986 query : { type : 'string' , minimum : undefined , maximum : undefined , enum : undefined , default : undefined } ,
9087 pageno : { type : 'integer' , minimum : 1 , maximum : undefined , enum : undefined , default : 1 } ,
9188 time_range : { type : 'string' , minimum : undefined , maximum : undefined , enum : [ 'day' , 'week' , 'month' , 'year' ] , default : undefined } ,
@@ -98,31 +95,31 @@ const FROZEN_FULL_TOOL_SNAPSHOTS = [
9895 response_format : { type : 'string' , minimum : undefined , maximum : undefined , enum : [ 'text' , 'json' ] , default : undefined } ,
9996 result_detail : { type : 'string' , minimum : undefined , maximum : undefined , enum : [ 'compact' , 'full' ] , default : undefined } ,
10097 } } ,
101- { name : 'web_url_read' , annotations : { readOnlyHint : true , openWorldHint : true } , required : [ 'url' ] , properties : {
98+ { name : 'web_url_read' , annotations : { readOnlyHint : true , openWorldHint : true } , required : [ 'url' ] , propertyOrder : [ 'url' , 'startChar' , 'maxLength' , 'section' , 'paragraphRange' , 'readHeadings' ] , properties : {
10299 url : { type : 'string' , minimum : undefined , maximum : undefined , enum : undefined , default : undefined } ,
103100 startChar : { type : 'number' , minimum : 0 , maximum : undefined , enum : undefined , default : undefined } ,
104101 maxLength : { type : 'number' , minimum : 1 , maximum : undefined , enum : undefined , default : undefined } ,
105102 section : { type : 'string' , minimum : undefined , maximum : undefined , enum : undefined , default : undefined } ,
106103 paragraphRange : { type : 'string' , minimum : undefined , maximum : undefined , enum : undefined , default : undefined } ,
107104 readHeadings : { type : 'boolean' , minimum : undefined , maximum : undefined , enum : undefined , default : undefined } ,
108105 } } ,
109- { name : 'searxng_search_suggestions' , annotations : { readOnlyHint : true , openWorldHint : true } , required : [ 'query' ] , properties : {
106+ { name : 'searxng_search_suggestions' , annotations : { readOnlyHint : true , openWorldHint : true } , required : [ 'query' ] , propertyOrder : [ 'query' , 'language' ] , properties : {
110107 query : { type : 'string' , minimum : undefined , maximum : undefined , enum : undefined , default : undefined } ,
111108 language : { type : 'string' , minimum : undefined , maximum : undefined , enum : undefined , default : 'all' } ,
112109 } } ,
113- { name : 'searxng_instance_info' , annotations : { readOnlyHint : true , openWorldHint : true } , required : [ ] , properties : {
110+ { name : 'searxng_instance_info' , annotations : { readOnlyHint : true , openWorldHint : true } , required : [ ] , propertyOrder : [ 'includeEngines' , 'includeDisabled' , 'category' , 'refresh' ] , properties : {
114111 includeEngines : { type : 'boolean' , minimum : undefined , maximum : undefined , enum : undefined , default : false } ,
115112 includeDisabled : { type : 'boolean' , minimum : undefined , maximum : undefined , enum : undefined , default : false } ,
116113 category : { type : 'string' , minimum : undefined , maximum : undefined , enum : undefined , default : undefined } ,
117114 refresh : { type : 'boolean' , minimum : undefined , maximum : undefined , enum : undefined , default : false } ,
118115 } } ,
119116] ;
120117
121- const FROZEN_LITE_TOOL_SNAPSHOTS = [
122- { name : 'searxng_web_search' , annotations : undefined , required : [ 'query' ] , properties : { query : { type : 'string' , minimum : undefined , maximum : undefined , enum : undefined , default : undefined } } } ,
123- { name : 'web_url_read' , annotations : undefined , required : [ 'url' ] , properties : { url : { type : 'string' , minimum : undefined , maximum : undefined , enum : undefined , default : undefined } } } ,
124- { name : 'searxng_search_suggestions' , annotations : undefined , required : [ 'query' ] , properties : { query : { type : 'string' , minimum : undefined , maximum : undefined , enum : undefined , default : undefined } } } ,
125- { name : 'searxng_instance_info' , annotations : undefined , required : [ ] , properties : { } } ,
118+ const LITE_TOOL_STRUCTURAL_SNAPSHOTS = [
119+ { name : 'searxng_web_search' , annotations : undefined , required : [ 'query' ] , propertyOrder : [ 'query' ] , properties : { query : { type : 'string' , minimum : undefined , maximum : undefined , enum : undefined , default : undefined } } } ,
120+ { name : 'web_url_read' , annotations : undefined , required : [ 'url' ] , propertyOrder : [ 'url' ] , properties : { url : { type : 'string' , minimum : undefined , maximum : undefined , enum : undefined , default : undefined } } } ,
121+ { name : 'searxng_search_suggestions' , annotations : undefined , required : [ 'query' ] , propertyOrder : [ 'query' ] , properties : { query : { type : 'string' , minimum : undefined , maximum : undefined , enum : undefined , default : undefined } } } ,
122+ { name : 'searxng_instance_info' , annotations : undefined , required : [ ] , propertyOrder : [ ] , properties : { } } ,
126123] ;
127124
128125/** Spin up a fresh Client↔Server pair for each test. Call client.close() when done. */
@@ -594,21 +591,16 @@ async function runTests() {
594591 await client . close ( ) ;
595592 } , results ) ;
596593
597- await testFunction ( 'full and lite tool schemas and annotations retain their frozen structural contract ' , async ( ) => {
594+ await testFunction ( 'full and lite tools retain reviewable source, wire, and structural contracts ' , async ( ) => {
598595 const fullDefinitions = [ WEB_SEARCH_TOOL , SUGGESTIONS_TOOL , INSTANCE_INFO_TOOL , READ_URL_TOOL ] ;
599596 const liteDefinitions = [ LITE_WEB_SEARCH_TOOL , LITE_SUGGESTIONS_TOOL , LITE_INSTANCE_INFO_TOOL , LITE_READ_URL_TOOL ] ;
600- assert . equal (
601- structuralDigest ( fullDefinitions ) ,
602- '2a5a607d2614632059e9f98e5fa7468374a9086312071f8895f4ba10a3a4618a' ,
603- 'full tool definitions changed from the frozen pre-migration structure' ,
604- ) ;
605- assert . equal (
606- structuralDigest ( liteDefinitions ) ,
607- '0d0bb57c16c482517642677de2ec590cda44bff70aead56b6c5110dc6764e782' ,
608- 'lite tool definitions changed from the frozen pre-migration structure' ,
609- ) ;
610- const fullExpectedInServedOrder = [ FROZEN_FULL_TOOL_SNAPSHOTS [ 0 ] , FROZEN_FULL_TOOL_SNAPSHOTS [ 2 ] , FROZEN_FULL_TOOL_SNAPSHOTS [ 3 ] , FROZEN_FULL_TOOL_SNAPSHOTS [ 1 ] ] ;
611- const liteExpectedInServedOrder = [ FROZEN_LITE_TOOL_SNAPSHOTS [ 0 ] , FROZEN_LITE_TOOL_SNAPSHOTS [ 2 ] , FROZEN_LITE_TOOL_SNAPSHOTS [ 3 ] , FROZEN_LITE_TOOL_SNAPSHOTS [ 1 ] ] ;
597+ const fullExpectedInServedOrder = [ FULL_TOOL_STRUCTURAL_SNAPSHOTS [ 0 ] , FULL_TOOL_STRUCTURAL_SNAPSHOTS [ 2 ] , FULL_TOOL_STRUCTURAL_SNAPSHOTS [ 3 ] , FULL_TOOL_STRUCTURAL_SNAPSHOTS [ 1 ] ] ;
598+ const liteExpectedInServedOrder = [ LITE_TOOL_STRUCTURAL_SNAPSHOTS [ 0 ] , LITE_TOOL_STRUCTURAL_SNAPSHOTS [ 2 ] , LITE_TOOL_STRUCTURAL_SNAPSHOTS [ 3 ] , LITE_TOOL_STRUCTURAL_SNAPSHOTS [ 1 ] ] ;
599+
600+ const descriptionsChanged = wireJson ( fullDefinitions ) ;
601+ descriptionsChanged [ 0 ] . description = 'Updated search tool wording' ;
602+ descriptionsChanged [ 0 ] . inputSchema . properties ! . query . description = 'Updated query wording' ;
603+ assert . deepEqual ( toolStructuralSnapshot ( descriptionsChanged ) , toolStructuralSnapshot ( fullDefinitions ) ) ;
612604
613605 delete process . env . SEARXNG_LITE_TOOLS ;
614606 const full = await connect ( ) ;
0 commit comments