Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions Lite.Tests/ScenarioTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,110 @@ public async Task EverythingOnFire_BlockingAndDeadlocksPresent()
return (stories, factsByKey);
}

/* ── Anomaly Detection: CPU Spike ── */

[Fact]
public async Task CpuSpikeAnomaly_DetectsCpuDeviation()
{
var (stories, facts) = await RunFullPipelineWithAnomaliesAsync(s => s.SeedCpuSpikeAnomalyAsync());
PrintStories("CPU SPIKE ANOMALY", stories);

Assert.True(facts.ContainsKey("ANOMALY_CPU_SPIKE"), "Should detect CPU anomaly");
Assert.True(facts["ANOMALY_CPU_SPIKE"].Severity >= 0.5, "CPU anomaly severity should be significant");
}

[Fact]
public async Task CpuSpikeAnomaly_HighDeviation()
{
var (_, facts) = await RunFullPipelineWithAnomaliesAsync(s => s.SeedCpuSpikeAnomalyAsync());

var deviation = facts["ANOMALY_CPU_SPIKE"].Metadata["deviation_sigma"];
Assert.True(deviation > 5.0, $"Expected large deviation (>5σ), got {deviation:F1}σ");
}

[Fact]
public async Task CpuSpikeAnomaly_AppearsAsStory()
{
var (stories, _) = await RunFullPipelineWithAnomaliesAsync(s => s.SeedCpuSpikeAnomalyAsync());

Assert.Contains(stories, s => s.RootFactKey == "ANOMALY_CPU_SPIKE");
}

/* ── Anomaly Detection: Blocking Spike ── */

[Fact]
public async Task BlockingSpikeAnomaly_DetectsBlockingBurst()
{
var (stories, facts) = await RunFullPipelineWithAnomaliesAsync(s => s.SeedBlockingSpikeAnomalyAsync());
PrintStories("BLOCKING SPIKE ANOMALY", stories);

Assert.True(facts.ContainsKey("ANOMALY_BLOCKING_SPIKE"), "Should detect blocking spike");
Assert.True(facts["ANOMALY_BLOCKING_SPIKE"].Severity >= 0.5, "Blocking spike should be significant");
}

[Fact]
public async Task BlockingSpikeAnomaly_DetectsDeadlockSpike()
{
var (_, facts) = await RunFullPipelineWithAnomaliesAsync(s => s.SeedBlockingSpikeAnomalyAsync());

Assert.True(facts.ContainsKey("ANOMALY_DEADLOCK_SPIKE"), "Should detect deadlock spike");
}

/* ── Anomaly Detection: Wait Spike ── */

[Fact]
public async Task WaitSpikeAnomaly_DetectsPageiolatchFlood()
{
var (stories, facts) = await RunFullPipelineWithAnomaliesAsync(s => s.SeedWaitSpikeAnomalyAsync());
PrintStories("WAIT SPIKE ANOMALY", stories);

Assert.True(facts.ContainsKey("ANOMALY_WAIT_PAGEIOLATCH_SH"), "Should detect PAGEIOLATCH spike");
Assert.True(facts["ANOMALY_WAIT_PAGEIOLATCH_SH"].Severity >= 0.5, "PAGEIOLATCH anomaly should be significant");
}

[Fact]
public async Task WaitSpikeAnomaly_HighRatio()
{
var (_, facts) = await RunFullPipelineWithAnomaliesAsync(s => s.SeedWaitSpikeAnomalyAsync());

var ratio = facts["ANOMALY_WAIT_PAGEIOLATCH_SH"].Metadata["ratio"];
Assert.True(ratio >= 5.0, $"Expected >= 5x increase, got {ratio:F1}x");
}

/* ── Helpers ── */

private async Task<(List<AnalysisStory> Stories, Dictionary<string, Fact> Facts)> RunFullPipelineWithAnomaliesAsync(
Func<TestDataSeeder, Task> seedAction)
{
await _duckDb.InitializeAsync();
await _duckDb.InitializeAnalysisSchemaAsync();

var seeder = new TestDataSeeder(_duckDb);
await seedAction(seeder);

var collector = new DuckDbFactCollector(_duckDb);
var context = TestDataSeeder.CreateTestContext();
var facts = await collector.CollectFactsAsync(context);

// Run anomaly detection (compares analysis window against baseline)
var anomalyDetector = new AnomalyDetector(_duckDb);
var anomalies = await anomalyDetector.DetectAnomaliesAsync(context);
facts.AddRange(anomalies);

var scorer = new FactScorer();
scorer.ScoreAll(facts);

var graph = new RelationshipGraph();
var engine = new InferenceEngine(graph);
var stories = engine.BuildStories(facts);

var factsByKey = facts
.Where(f => f.Severity > 0)
.ToDictionary(f => f.Key, f => f);

return (stories, factsByKey);
}

private static void PrintStories(string scenario, List<AnalysisStory> stories)
{
var output = TestContext.Current.TestOutputHelper!;
Expand Down
6 changes: 6 additions & 0 deletions Lite/Analysis/AnalysisService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public class AnalysisService
private readonly RelationshipGraph _graph;
private readonly InferenceEngine _engine;
private readonly DrillDownCollector _drillDown;
private readonly AnomalyDetector _anomalyDetector;
/// <summary>
/// Minimum hours of collected data required before analysis will run.
/// Short collection windows distort fraction-of-period calculations —
Expand Down Expand Up @@ -59,6 +60,7 @@ public AnalysisService(DuckDbInitializer duckDb)
_graph = new RelationshipGraph();
_engine = new InferenceEngine(_graph);
_drillDown = new DrillDownCollector(duckDb);
_anomalyDetector = new AnomalyDetector(duckDb);
}

/// <summary>
Expand Down Expand Up @@ -126,6 +128,10 @@ public async Task<List<AnalysisFinding>> AnalyzeAsync(AnalysisContext context)
return [];
}

// 1.5. Detect anomalies (compare analysis window against baseline)
var anomalies = await _anomalyDetector.DetectAnomaliesAsync(context);
facts.AddRange(anomalies);

// 2. Score facts (base severity + amplifiers)
_scorer.ScoreAll(facts);

Expand Down
Loading
Loading