@@ -124,6 +124,7 @@ describe("http-server — health + tools catalog", () => {
124124 expect ( body . tools . map ( ( t ) => t . name ) ) . toContain ( "query" ) ;
125125 expect ( body . tools . map ( ( t ) => t . name ) ) . toContain ( "audit" ) ;
126126 expect ( body . tools . map ( ( t ) => t . name ) ) . toContain ( "affected" ) ;
127+ expect ( body . tools . map ( ( t ) => t . name ) ) . toContain ( "trace" ) ;
127128 } ) ;
128129
129130 it ( "404 for unknown route" , async ( ) => {
@@ -405,6 +406,141 @@ describe("http-server — POST /tool/{other tools}", () => {
405406 expect ( r . json . error ) . not . toContain ( "--changed-since" ) ;
406407 } ) ;
407408
409+ function seedTraceGraph ( ) {
410+ writeFileSync (
411+ join ( benchDir , "src" , "trace.ts" ) ,
412+ "export function alpha() {\n return beta();\n}\nexport function beta() {\n return 1;\n}\n" ,
413+ ) ;
414+ const db = openDb ( ) ;
415+ try {
416+ db . run (
417+ `INSERT INTO files (path, content_hash, size, line_count, language, last_modified, indexed_at)
418+ VALUES ('src/trace.ts', 'ht', 100, 6, 'typescript', 1, 1)` ,
419+ ) ;
420+ db . run (
421+ `INSERT INTO symbols (name, kind, file_path, line_start, line_end, signature, is_exported, parent_name, visibility)
422+ VALUES ('alpha', 'function', 'src/trace.ts', 1, 3, 'alpha()', 1, NULL, 'export'),
423+ ('beta', 'function', 'src/trace.ts', 4, 6, 'beta()', 1, NULL, 'export')` ,
424+ ) ;
425+ db . run (
426+ `INSERT INTO calls (file_path, caller_name, caller_scope, callee_name, line_start, column_start, column_end)
427+ VALUES ('src/trace.ts', 'alpha', 'alpha', 'beta', 2, 0, 0)` ,
428+ ) ;
429+ } finally {
430+ closeDb ( db ) ;
431+ }
432+ }
433+
434+ it ( "trace returns path and snippets" , async ( ) => {
435+ seedTraceGraph ( ) ;
436+ serverHandle = await startServer ( ) ;
437+ const r = await postTool ( serverHandle . port , "trace" , {
438+ from : "alpha" ,
439+ to : "beta" ,
440+ } ) ;
441+ expect ( r . status ) . toBe ( 200 ) ;
442+ expect ( r . json . path ) . toHaveLength ( 1 ) ;
443+ expect ( r . json . snippets . length ) . toBeGreaterThan ( 0 ) ;
444+ expect ( r . json . truncated ) . toBe ( false ) ;
445+ } ) ;
446+
447+ it ( "explore merges neighborhoods" , async ( ) => {
448+ seedTraceGraph ( ) ;
449+ serverHandle = await startServer ( ) ;
450+ const r = await postTool ( serverHandle . port , "explore" , {
451+ names : [ "alpha" , "beta" ] ,
452+ } ) ;
453+ expect ( r . status ) . toBe ( 200 ) ;
454+ expect ( r . json . names ) . toEqual ( [ "alpha" , "beta" ] ) ;
455+ expect ( r . json . rows . length ) . toBeGreaterThan ( 0 ) ;
456+ } ) ;
457+
458+ it ( "node returns center + neighborhood" , async ( ) => {
459+ seedTraceGraph ( ) ;
460+ serverHandle = await startServer ( ) ;
461+ const r = await postTool ( serverHandle . port , "node" , {
462+ name : "alpha" ,
463+ include_snippets : true ,
464+ } ) ;
465+ expect ( r . status ) . toBe ( 200 ) ;
466+ expect ( r . json . center . matches [ 0 ] ?. name ) . toBe ( "alpha" ) ;
467+ expect (
468+ r . json . neighborhood . some ( ( row : { name : string } ) => row . name === "beta" ) ,
469+ ) . toBe ( true ) ;
470+ } ) ;
471+
472+ it ( "trace with non-integer max_depth → 400 (Zod rejects)" , async ( ) => {
473+ serverHandle = await startServer ( ) ;
474+ const r = await postTool ( serverHandle . port , "trace" , {
475+ from : "a" ,
476+ to : "b" ,
477+ max_depth : 1.5 ,
478+ } ) ;
479+ expect ( r . status ) . toBe ( 400 ) ;
480+ expect ( r . json . error ) . toContain ( '"trace"' ) ;
481+ } ) ;
482+
483+ it ( "explore with empty names → 400 (Zod rejects)" , async ( ) => {
484+ serverHandle = await startServer ( ) ;
485+ const r = await postTool ( serverHandle . port , "explore" , { names : [ ] } ) ;
486+ expect ( r . status ) . toBe ( 400 ) ;
487+ expect ( r . json . error ) . toContain ( '"explore"' ) ;
488+ } ) ;
489+
490+ it ( "trace sets truncated when budget_chars is tiny" , async ( ) => {
491+ seedTraceGraph ( ) ;
492+ serverHandle = await startServer ( ) ;
493+ const r = await postTool ( serverHandle . port , "trace" , {
494+ from : "alpha" ,
495+ to : "beta" ,
496+ budget_chars : 1 ,
497+ } ) ;
498+ expect ( r . status ) . toBe ( 200 ) ;
499+ expect ( r . json . truncated ) . toBe ( true ) ;
500+ expect ( r . json . truncation ?. snippets ) . toBe ( true ) ;
501+ } ) ;
502+
503+ it ( "records recipe recency after trace" , async ( ) => {
504+ seedTraceGraph ( ) ;
505+ serverHandle = await startServer ( ) ;
506+ const r = await postTool ( serverHandle . port , "trace" , {
507+ from : "alpha" ,
508+ to : "beta" ,
509+ } ) ;
510+ expect ( r . status ) . toBe ( 200 ) ;
511+ const db = openDb ( ) ;
512+ try {
513+ const row = db
514+ . query < { run_count : number } > (
515+ "SELECT run_count FROM recipe_recency WHERE recipe_id = 'call-path'" ,
516+ )
517+ . get ( ) ;
518+ expect ( row ?. run_count ) . toBeGreaterThanOrEqual ( 1 ) ;
519+ } finally {
520+ closeDb ( db ) ;
521+ }
522+ } ) ;
523+
524+ it ( "records recipe recency after explore" , async ( ) => {
525+ seedTraceGraph ( ) ;
526+ serverHandle = await startServer ( ) ;
527+ const r = await postTool ( serverHandle . port , "explore" , {
528+ names : [ "alpha" ] ,
529+ } ) ;
530+ expect ( r . status ) . toBe ( 200 ) ;
531+ const db = openDb ( ) ;
532+ try {
533+ const row = db
534+ . query < { run_count : number } > (
535+ "SELECT run_count FROM recipe_recency WHERE recipe_id = 'symbol-neighborhood'" ,
536+ )
537+ . get ( ) ;
538+ expect ( row ?. run_count ) . toBeGreaterThanOrEqual ( 1 ) ;
539+ } finally {
540+ closeDb ( db ) ;
541+ }
542+ } ) ;
543+
408544 it ( "list_baselines returns array (empty when none saved)" , async ( ) => {
409545 serverHandle = await startServer ( ) ;
410546 const r = await postTool ( serverHandle . port , "list_baselines" , { } ) ;
@@ -582,6 +718,29 @@ describe("http-server — Zod input validation at HTTP boundary", () => {
582718 } ) ;
583719 expect ( r . status ) . toBe ( 400 ) ;
584720 } ) ;
721+
722+ it ( "trace without from → 400 with structured error" , async ( ) => {
723+ serverHandle = await startServer ( ) ;
724+ const r = await postTool ( serverHandle . port , "trace" , { to : "bar" } ) ;
725+ expect ( r . status ) . toBe ( 400 ) ;
726+ expect ( r . json . error ) . toContain ( '"trace"' ) ;
727+ expect ( r . json . error ) . toContain ( "from" ) ;
728+ } ) ;
729+
730+ it ( "trace without to → 400 with structured error" , async ( ) => {
731+ serverHandle = await startServer ( ) ;
732+ const r = await postTool ( serverHandle . port , "trace" , { from : "foo" } ) ;
733+ expect ( r . status ) . toBe ( 400 ) ;
734+ expect ( r . json . error ) . toContain ( '"trace"' ) ;
735+ expect ( r . json . error ) . toContain ( "to" ) ;
736+ } ) ;
737+
738+ it ( "node with name=number → 400 (not deep handler crash)" , async ( ) => {
739+ serverHandle = await startServer ( ) ;
740+ const r = await postTool ( serverHandle . port , "node" , { name : 1 } ) ;
741+ expect ( r . status ) . toBe ( 400 ) ;
742+ expect ( r . json . error ) . toContain ( "name" ) ;
743+ } ) ;
585744} ) ;
586745
587746describe ( "http-server — GET /resources" , ( ) => {
0 commit comments