@@ -359,6 +359,110 @@ public async Task EverythingOnFire_BlockingAndDeadlocksPresent()
359359 return ( stories , factsByKey ) ;
360360 }
361361
362+ /* ── Anomaly Detection: CPU Spike ── */
363+
364+ [ Fact ]
365+ public async Task CpuSpikeAnomaly_DetectsCpuDeviation ( )
366+ {
367+ var ( stories , facts ) = await RunFullPipelineWithAnomaliesAsync ( s => s . SeedCpuSpikeAnomalyAsync ( ) ) ;
368+ PrintStories ( "CPU SPIKE ANOMALY" , stories ) ;
369+
370+ Assert . True ( facts . ContainsKey ( "ANOMALY_CPU_SPIKE" ) , "Should detect CPU anomaly" ) ;
371+ Assert . True ( facts [ "ANOMALY_CPU_SPIKE" ] . Severity >= 0.5 , "CPU anomaly severity should be significant" ) ;
372+ }
373+
374+ [ Fact ]
375+ public async Task CpuSpikeAnomaly_HighDeviation ( )
376+ {
377+ var ( _, facts ) = await RunFullPipelineWithAnomaliesAsync ( s => s . SeedCpuSpikeAnomalyAsync ( ) ) ;
378+
379+ var deviation = facts [ "ANOMALY_CPU_SPIKE" ] . Metadata [ "deviation_sigma" ] ;
380+ Assert . True ( deviation > 5.0 , $ "Expected large deviation (>5σ), got { deviation : F1} σ") ;
381+ }
382+
383+ [ Fact ]
384+ public async Task CpuSpikeAnomaly_AppearsAsStory ( )
385+ {
386+ var ( stories , _) = await RunFullPipelineWithAnomaliesAsync ( s => s . SeedCpuSpikeAnomalyAsync ( ) ) ;
387+
388+ Assert . Contains ( stories , s => s . RootFactKey == "ANOMALY_CPU_SPIKE" ) ;
389+ }
390+
391+ /* ── Anomaly Detection: Blocking Spike ── */
392+
393+ [ Fact ]
394+ public async Task BlockingSpikeAnomaly_DetectsBlockingBurst ( )
395+ {
396+ var ( stories , facts ) = await RunFullPipelineWithAnomaliesAsync ( s => s . SeedBlockingSpikeAnomalyAsync ( ) ) ;
397+ PrintStories ( "BLOCKING SPIKE ANOMALY" , stories ) ;
398+
399+ Assert . True ( facts . ContainsKey ( "ANOMALY_BLOCKING_SPIKE" ) , "Should detect blocking spike" ) ;
400+ Assert . True ( facts [ "ANOMALY_BLOCKING_SPIKE" ] . Severity >= 0.5 , "Blocking spike should be significant" ) ;
401+ }
402+
403+ [ Fact ]
404+ public async Task BlockingSpikeAnomaly_DetectsDeadlockSpike ( )
405+ {
406+ var ( _, facts ) = await RunFullPipelineWithAnomaliesAsync ( s => s . SeedBlockingSpikeAnomalyAsync ( ) ) ;
407+
408+ Assert . True ( facts . ContainsKey ( "ANOMALY_DEADLOCK_SPIKE" ) , "Should detect deadlock spike" ) ;
409+ }
410+
411+ /* ── Anomaly Detection: Wait Spike ── */
412+
413+ [ Fact ]
414+ public async Task WaitSpikeAnomaly_DetectsPageiolatchFlood ( )
415+ {
416+ var ( stories , facts ) = await RunFullPipelineWithAnomaliesAsync ( s => s . SeedWaitSpikeAnomalyAsync ( ) ) ;
417+ PrintStories ( "WAIT SPIKE ANOMALY" , stories ) ;
418+
419+ Assert . True ( facts . ContainsKey ( "ANOMALY_WAIT_PAGEIOLATCH_SH" ) , "Should detect PAGEIOLATCH spike" ) ;
420+ Assert . True ( facts [ "ANOMALY_WAIT_PAGEIOLATCH_SH" ] . Severity >= 0.5 , "PAGEIOLATCH anomaly should be significant" ) ;
421+ }
422+
423+ [ Fact ]
424+ public async Task WaitSpikeAnomaly_HighRatio ( )
425+ {
426+ var ( _, facts ) = await RunFullPipelineWithAnomaliesAsync ( s => s . SeedWaitSpikeAnomalyAsync ( ) ) ;
427+
428+ var ratio = facts [ "ANOMALY_WAIT_PAGEIOLATCH_SH" ] . Metadata [ "ratio" ] ;
429+ Assert . True ( ratio >= 5.0 , $ "Expected >= 5x increase, got { ratio : F1} x") ;
430+ }
431+
432+ /* ── Helpers ── */
433+
434+ private async Task < ( List < AnalysisStory > Stories , Dictionary < string , Fact > Facts ) > RunFullPipelineWithAnomaliesAsync (
435+ Func < TestDataSeeder , Task > seedAction )
436+ {
437+ await _duckDb . InitializeAsync ( ) ;
438+ await _duckDb . InitializeAnalysisSchemaAsync ( ) ;
439+
440+ var seeder = new TestDataSeeder ( _duckDb ) ;
441+ await seedAction ( seeder ) ;
442+
443+ var collector = new DuckDbFactCollector ( _duckDb ) ;
444+ var context = TestDataSeeder . CreateTestContext ( ) ;
445+ var facts = await collector . CollectFactsAsync ( context ) ;
446+
447+ // Run anomaly detection (compares analysis window against baseline)
448+ var anomalyDetector = new AnomalyDetector ( _duckDb ) ;
449+ var anomalies = await anomalyDetector . DetectAnomaliesAsync ( context ) ;
450+ facts . AddRange ( anomalies ) ;
451+
452+ var scorer = new FactScorer ( ) ;
453+ scorer . ScoreAll ( facts ) ;
454+
455+ var graph = new RelationshipGraph ( ) ;
456+ var engine = new InferenceEngine ( graph ) ;
457+ var stories = engine . BuildStories ( facts ) ;
458+
459+ var factsByKey = facts
460+ . Where ( f => f . Severity > 0 )
461+ . ToDictionary ( f => f . Key , f => f ) ;
462+
463+ return ( stories , factsByKey ) ;
464+ }
465+
362466 private static void PrintStories ( string scenario , List < AnalysisStory > stories )
363467 {
364468 var output = TestContext . Current . TestOutputHelper ! ;
0 commit comments