save progress
This commit is contained in:
@@ -65,4 +65,62 @@ public class CallgraphNormalizationServiceTests
|
||||
edge.Confidence.Should().Be(1.0);
|
||||
edge.Evidence.Should().BeEquivalentTo(new[] { "x" });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_normalizes_gate_metadata()
|
||||
{
|
||||
var result = new CallgraphParseResult(
|
||||
Nodes: new[]
|
||||
{
|
||||
new CallgraphNode("a", "a", "fn", null, null, null),
|
||||
new CallgraphNode("b", "b", "fn", null, null, null)
|
||||
},
|
||||
Edges: new[]
|
||||
{
|
||||
new CallgraphEdge("a", "b", "call")
|
||||
{
|
||||
GateMultiplierBps = 15000,
|
||||
Gates = new List<CallgraphGate>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Type = CallgraphGateType.AuthRequired,
|
||||
GuardSymbol = " svc.main ",
|
||||
Detail = " [Authorize] ",
|
||||
DetectionMethod = " attr ",
|
||||
Confidence = 2.0,
|
||||
SourceFile = " /src/app.cs ",
|
||||
LineNumber = 0
|
||||
},
|
||||
new()
|
||||
{
|
||||
Type = CallgraphGateType.AuthRequired,
|
||||
GuardSymbol = "svc.main",
|
||||
Detail = "ignored",
|
||||
DetectionMethod = "ignored",
|
||||
Confidence = 0.1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Roots: Array.Empty<CallgraphRoot>(),
|
||||
FormatVersion: "1.0",
|
||||
SchemaVersion: "1.0",
|
||||
Analyzer: null);
|
||||
|
||||
var normalized = _service.Normalize("csharp", result);
|
||||
|
||||
normalized.Edges.Should().ContainSingle();
|
||||
var edge = normalized.Edges[0];
|
||||
edge.GateMultiplierBps.Should().Be(10000);
|
||||
edge.Gates.Should().NotBeNull();
|
||||
edge.Gates!.Should().ContainSingle();
|
||||
edge.Gates[0].Type.Should().Be(CallgraphGateType.AuthRequired);
|
||||
edge.Gates[0].GuardSymbol.Should().Be("svc.main");
|
||||
edge.Gates[0].Detail.Should().Be("[Authorize]");
|
||||
edge.Gates[0].DetectionMethod.Should().Be("attr");
|
||||
edge.Gates[0].Confidence.Should().Be(1.0);
|
||||
edge.Gates[0].SourceFile.Should().Be("/src/app.cs");
|
||||
edge.Gates[0].LineNumber.Should().BeNull();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,87 @@ using Xunit;
|
||||
|
||||
public class ReachabilityScoringServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task RecomputeAsync_applies_gate_multipliers_and_surfaces_gate_evidence()
|
||||
{
|
||||
var callgraph = new CallgraphDocument
|
||||
{
|
||||
Id = "cg-gates-1",
|
||||
Language = "dotnet",
|
||||
Component = "demo",
|
||||
Version = "1.0.0",
|
||||
Nodes = new List<CallgraphNode>
|
||||
{
|
||||
new("main", "Main", "method", null, null, null),
|
||||
new("target", "Target", "method", null, null, null)
|
||||
},
|
||||
Edges = new List<CallgraphEdge>
|
||||
{
|
||||
new("main", "target", "call")
|
||||
{
|
||||
Gates = new List<CallgraphGate>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Type = CallgraphGateType.AuthRequired,
|
||||
GuardSymbol = "main",
|
||||
Detail = "[Authorize] attribute",
|
||||
DetectionMethod = "fixture",
|
||||
Confidence = 0.9
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var callgraphRepository = new InMemoryCallgraphRepository(callgraph);
|
||||
var factRepository = new InMemoryReachabilityFactRepository();
|
||||
|
||||
var options = new SignalsOptions();
|
||||
options.Scoring.ReachableConfidence = 0.8;
|
||||
options.Scoring.UnreachableConfidence = 0.3;
|
||||
options.Scoring.MaxConfidence = 0.95;
|
||||
options.Scoring.MinConfidence = 0.1;
|
||||
options.Scoring.GateMultipliers.AuthRequiredMultiplierBps = 3000;
|
||||
|
||||
var cache = new InMemoryReachabilityCache();
|
||||
var eventsPublisher = new RecordingEventsPublisher();
|
||||
var unknowns = new InMemoryUnknownsRepository();
|
||||
|
||||
var service = new ReachabilityScoringService(
|
||||
callgraphRepository,
|
||||
factRepository,
|
||||
TimeProvider.System,
|
||||
Options.Create(options),
|
||||
cache,
|
||||
unknowns,
|
||||
eventsPublisher,
|
||||
NullLogger<ReachabilityScoringService>.Instance);
|
||||
|
||||
var request = new ReachabilityRecomputeRequest
|
||||
{
|
||||
CallgraphId = callgraph.Id,
|
||||
Subject = new ReachabilitySubject { Component = "demo", Version = "1.0.0" },
|
||||
EntryPoints = new List<string> { "main" },
|
||||
Targets = new List<string> { "target" }
|
||||
};
|
||||
|
||||
var fact = await service.RecomputeAsync(request, CancellationToken.None);
|
||||
|
||||
Assert.Single(fact.States);
|
||||
var state = fact.States[0];
|
||||
Assert.True(state.Reachable);
|
||||
Assert.Equal("direct", state.Bucket);
|
||||
Assert.Equal(3000, state.Evidence.GateMultiplierBps);
|
||||
Assert.NotNull(state.Evidence.Gates);
|
||||
Assert.Contains(state.Evidence.Gates!, gate => gate.Type == CallgraphGateType.AuthRequired);
|
||||
|
||||
// Base score: 0.8 confidence * 0.85 direct bucket = 0.68, then auth gate (30%) = 0.204
|
||||
Assert.Equal(0.204, state.Score, 3);
|
||||
Assert.Equal(0.204, fact.Score, 3);
|
||||
Assert.Equal(0.204, fact.RiskScore, 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RecomputeAsync_UsesConfiguredWeights()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Signals.Models;
|
||||
using StellaOps.Signals.Parsing;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Signals.Tests;
|
||||
|
||||
public sealed class SimpleJsonCallgraphParserGateTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ParseAsync_parses_gate_fields_on_edges()
|
||||
{
|
||||
var json = """
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"nodes": [
|
||||
{ "id": "main" },
|
||||
{ "id": "target" }
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"from": "main",
|
||||
"to": "target",
|
||||
"kind": "call",
|
||||
"gate_multiplier_bps": 3000,
|
||||
"gates": [
|
||||
{
|
||||
"type": "authRequired",
|
||||
"detail": "[Authorize] attribute",
|
||||
"guard_symbol": "main",
|
||||
"source_file": "/src/app.cs",
|
||||
"line_number": 42,
|
||||
"confidence": 0.9,
|
||||
"detection_method": "pattern"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
var parser = new SimpleJsonCallgraphParser("csharp");
|
||||
await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(json), writable: false);
|
||||
var parsed = await parser.ParseAsync(stream, CancellationToken.None);
|
||||
|
||||
parsed.Edges.Should().ContainSingle();
|
||||
var edge = parsed.Edges[0];
|
||||
edge.GateMultiplierBps.Should().Be(3000);
|
||||
edge.Gates.Should().NotBeNull();
|
||||
edge.Gates!.Should().ContainSingle();
|
||||
edge.Gates[0].Type.Should().Be(CallgraphGateType.AuthRequired);
|
||||
edge.Gates[0].GuardSymbol.Should().Be("main");
|
||||
edge.Gates[0].SourceFile.Should().Be("/src/app.cs");
|
||||
edge.Gates[0].LineNumber.Should().Be(42);
|
||||
edge.Gates[0].DetectionMethod.Should().Be("pattern");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ public sealed class UnknownsScoringIntegrationTests
|
||||
public UnknownsScoringIntegrationTests()
|
||||
{
|
||||
_timeProvider = new MockTimeProvider(new DateTimeOffset(2025, 12, 15, 12, 0, 0, TimeSpan.Zero));
|
||||
_unknownsRepo = new FullInMemoryUnknownsRepository();
|
||||
_unknownsRepo = new FullInMemoryUnknownsRepository(_timeProvider);
|
||||
_deploymentRefs = new InMemoryDeploymentRefsRepository();
|
||||
_graphMetrics = new InMemoryGraphMetricsRepository();
|
||||
_defaultOptions = new UnknownsScoringOptions();
|
||||
@@ -632,8 +632,14 @@ public sealed class UnknownsScoringIntegrationTests
|
||||
|
||||
private sealed class FullInMemoryUnknownsRepository : IUnknownsRepository
|
||||
{
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly List<UnknownSymbolDocument> _stored = new();
|
||||
|
||||
public FullInMemoryUnknownsRepository(TimeProvider timeProvider)
|
||||
{
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
}
|
||||
|
||||
public Task UpsertAsync(string subjectKey, IEnumerable<UnknownSymbolDocument> items, CancellationToken cancellationToken)
|
||||
{
|
||||
_stored.RemoveAll(x => x.SubjectKey == subjectKey);
|
||||
@@ -676,7 +682,7 @@ public sealed class UnknownsScoringIntegrationTests
|
||||
int limit,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
return Task.FromResult<IReadOnlyList<UnknownSymbolDocument>>(
|
||||
_stored
|
||||
.Where(x => x.Band == band && (x.NextScheduledRescan == null || x.NextScheduledRescan <= now))
|
||||
|
||||
Reference in New Issue
Block a user