@@ -420,3 +420,96 @@ async fn rebuild_after_refresh_without_history_uses_inline_only() {
420420 assert_eq ! ( bundle. activity_by_node. len( ) , 1 ) ;
421421 assert ! ( bundle. activity_by_node. contains_key( "primary" ) ) ;
422422}
423+
424+ // Regression: reload_schema must prefer history.db over schema.json so
425+ // planner/activity stats survive a reload. Before this fix, reload_schema
426+ // only read schema.json and wrapped it stats-less via wrap_schema_only,
427+ // clobbering history-derived stats already in the in-memory cache.
428+ #[ tokio:: test]
429+ async fn reload_schema_prefers_history_over_json ( ) {
430+ let dir = tempfile:: TempDir :: new ( ) . unwrap ( ) ;
431+ let store = HistoryStore :: open ( & dir. path ( ) . join ( "history.db" ) ) . unwrap ( ) ;
432+ let key = SnapshotKey {
433+ project_id : dry_run_core:: history:: ProjectId ( "test" . into ( ) ) ,
434+ database_id : dry_run_core:: history:: DatabaseId ( "test-db" . into ( ) ) ,
435+ } ;
436+
437+ let schema = test_snapshot ( ) ;
438+ let schema_hash = schema. content_hash . clone ( ) ;
439+ SnapshotStore :: put ( & store, & key, & schema)
440+ . await
441+ . expect ( "seed schema" ) ;
442+ store
443+ . put_activity_stats (
444+ & key,
445+ & make_activity_row ( & schema_hash, "primary" , "primary-h1" ) ,
446+ )
447+ . await
448+ . expect ( "seed primary activity" ) ;
449+
450+ let json_path = dir. path ( ) . join ( "schema.json" ) ;
451+ std:: fs:: write ( & json_path, serde_json:: to_string ( & schema) . unwrap ( ) ) . unwrap ( ) ;
452+
453+ // Server starts with a stats-less snapshot in cache (mimicking a server
454+ // that booted before history.db was populated). schema_candidates points
455+ // at the JSON fallback. with_history wires up the primary source.
456+ let server = DryRunServer :: from_annotated_with_db (
457+ crate :: mcp:: wrap_schema_only ( test_snapshot ( ) ) ,
458+ None ,
459+ LintConfig :: default ( ) ,
460+ None ,
461+ "test" ,
462+ vec ! [ json_path] ,
463+ )
464+ . with_history ( store, Some ( key) ) ;
465+
466+ let result = server. reload_schema ( ) . await . expect ( "reload_schema" ) ;
467+ let text = format ! ( "{:?}" , result. content. first( ) . unwrap( ) ) ;
468+ assert ! (
469+ text. contains( "history.db" ) ,
470+ "reload_schema should report loading from history.db, got: {text}"
471+ ) ;
472+
473+ let annotated = server. schema . read ( ) . await . clone ( ) . unwrap ( ) ;
474+ assert ! (
475+ annotated. activity_by_node. contains_key( "primary" ) ,
476+ "reload_schema should preserve primary activity from history.db"
477+ ) ;
478+ }
479+
480+ // Regression: when history.db has no entry for the configured key,
481+ // reload_schema must still load from schema.json (DDL-only fallback).
482+ #[ tokio:: test]
483+ async fn reload_schema_falls_back_to_schema_json_when_history_empty ( ) {
484+ let dir = tempfile:: TempDir :: new ( ) . unwrap ( ) ;
485+ let store = HistoryStore :: open ( & dir. path ( ) . join ( "history.db" ) ) . unwrap ( ) ;
486+ let key = SnapshotKey {
487+ project_id : dry_run_core:: history:: ProjectId ( "test" . into ( ) ) ,
488+ database_id : dry_run_core:: history:: DatabaseId ( "test-db" . into ( ) ) ,
489+ } ;
490+
491+ let schema = test_snapshot ( ) ;
492+ let json_path = dir. path ( ) . join ( "schema.json" ) ;
493+ std:: fs:: write ( & json_path, serde_json:: to_string ( & schema) . unwrap ( ) ) . unwrap ( ) ;
494+
495+ let server = DryRunServer :: from_annotated_with_db (
496+ crate :: mcp:: wrap_schema_only ( test_snapshot ( ) ) ,
497+ None ,
498+ LintConfig :: default ( ) ,
499+ None ,
500+ "test" ,
501+ vec ! [ json_path. clone( ) ] ,
502+ )
503+ . with_history ( store, Some ( key) ) ;
504+
505+ let result = server. reload_schema ( ) . await . expect ( "reload_schema" ) ;
506+ let text = format ! ( "{:?}" , result. content. first( ) . unwrap( ) ) ;
507+ assert ! (
508+ text. contains( & format!( "{}" , json_path. display( ) ) ) ,
509+ "reload_schema should report loading from the schema.json path, got: {text}"
510+ ) ;
511+
512+ let annotated = server. schema . read ( ) . await . clone ( ) . unwrap ( ) ;
513+ assert ! ( annotated. planner. is_none( ) ) ;
514+ assert ! ( annotated. activity_by_node. is_empty( ) ) ;
515+ }
0 commit comments