save progress

This commit is contained in:
StellaOps Bot
2025-12-18 09:10:36 +02:00
parent b4235c134c
commit 28823a8960
169 changed files with 11995 additions and 449 deletions

View File

@@ -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();
}
}

View File

@@ -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()
{

View File

@@ -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");
}
}

View File

@@ -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))