feat: Add initial implementation of Vulnerability Resolver Jobs
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

- Created project for StellaOps.Scanner.Analyzers.Native.Tests with necessary dependencies.
- Documented roles and guidelines in AGENTS.md for Scheduler module.
- Implemented IResolverJobService interface and InMemoryResolverJobService for handling resolver jobs.
- Added ResolverBacklogNotifier and ResolverBacklogService for monitoring job metrics.
- Developed API endpoints for managing resolver jobs and retrieving metrics.
- Defined models for resolver job requests and responses.
- Integrated dependency injection for resolver job services.
- Implemented ImpactIndexSnapshot for persisting impact index data.
- Introduced SignalsScoringOptions for configurable scoring weights in reachability scoring.
- Added unit tests for ReachabilityScoringService and RuntimeFactsIngestionService.
- Created dotnet-filter.sh script to handle command-line arguments for dotnet.
- Established nuget-prime project for managing package downloads.
This commit is contained in:
master
2025-11-18 07:52:15 +02:00
parent e69b57d467
commit 8355e2ff75
299 changed files with 13293 additions and 2444 deletions

View File

@@ -0,0 +1,47 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using StellaOps.Concelier.Core.Linksets;
using StellaOps.Concelier.RawModels;
using Xunit;
namespace StellaOps.Concelier.Core.Tests.Linksets;
public sealed class AdvisoryLinksetNormalizationTests
{
[Fact]
public void FromRawLinksetWithConfidence_ExtractsNotesAsConflicts()
{
var linkset = new RawLinkset
{
PackageUrls = ImmutableArray.Create("pkg:npm/foo@1.0.0"),
Notes = new Dictionary<string, string>
{
{ "severity", "disagree" }
}
};
var (normalized, confidence, conflicts) = AdvisoryLinksetNormalization.FromRawLinksetWithConfidence(linkset, 0.8);
Assert.NotNull(normalized);
Assert.Equal(0.8, confidence);
Assert.Single(conflicts);
Assert.Equal("severity", conflicts[0].Field);
Assert.Equal("disagree", conflicts[0].Reason);
}
[Theory]
[InlineData(-1, 0)]
[InlineData(2, 1)]
[InlineData(double.NaN, null)]
public void FromRawLinksetWithConfidence_ClampsConfidence(double input, double? expected)
{
var linkset = new RawLinkset
{
PackageUrls = ImmutableArray<string>.Empty
};
var (_, confidence, _) = AdvisoryLinksetNormalization.FromRawLinksetWithConfidence(linkset, input);
Assert.Equal(expected, confidence);
}
}

View File

@@ -29,20 +29,30 @@ public sealed class AdvisoryObservationQueryServiceTests
{
new AdvisoryObservationReference("advisory", "https://example.test/advisory-1")
},
createdAt: DateTimeOffset.UtcNow.AddMinutes(-5)),
CreateObservation(
observationId: "tenant-a:osv:beta:1",
tenant: "tenant-a",
aliases: new[] { "CVE-2025-0002", "GHSA-xyzz" },
purls: new[] { "pkg:pypi/package-b@2.0.0" },
cpes: Array.Empty<string>(),
references: new[]
{
new AdvisoryObservationReference("advisory", "https://example.test/advisory-2"),
new AdvisoryObservationReference("patch", "https://example.test/patch-1")
},
createdAt: DateTimeOffset.UtcNow)
};
scopes: new[] { "runtime" },
relationships: new[]
{
new RawRelationship("depends_on", "pkg:npm/package-a@1.0.0", "pkg:npm/lib@2.0.0", "sbom-a")
},
createdAt: DateTimeOffset.UtcNow.AddMinutes(-5)),
CreateObservation(
observationId: "tenant-a:osv:beta:1",
tenant: "tenant-a",
aliases: new[] { "CVE-2025-0002", "GHSA-xyzz" },
purls: new[] { "pkg:pypi/package-b@2.0.0" },
cpes: Array.Empty<string>(),
references: new[]
{
new AdvisoryObservationReference("advisory", "https://example.test/advisory-2"),
new AdvisoryObservationReference("patch", "https://example.test/patch-1")
},
scopes: new[] { "build" },
relationships: new[]
{
new RawRelationship("affects", "pkg:pypi/package-b@2.0.0", "component-x", "sbom-b")
},
createdAt: DateTimeOffset.UtcNow)
};
var lookup = new InMemoryLookup(observations);
var service = new AdvisoryObservationQueryService(lookup);
@@ -63,15 +73,22 @@ public sealed class AdvisoryObservationQueryServiceTests
Assert.Equal(new[] { "cpe:/a:vendor:product:1.0" }, result.Linkset.Cpes);
Assert.Equal(3, result.Linkset.References.Length);
Assert.Equal("advisory", result.Linkset.References[0].Type);
Assert.Equal("https://example.test/advisory-1", result.Linkset.References[0].Url);
Assert.Equal("https://example.test/advisory-2", result.Linkset.References[1].Url);
Assert.Equal("patch", result.Linkset.References[2].Type);
Assert.False(result.HasMore);
Assert.Null(result.NextCursor);
}
Assert.Equal(3, result.Linkset.References.Length);
Assert.Equal("advisory", result.Linkset.References[0].Type);
Assert.Equal("https://example.test/advisory-1", result.Linkset.References[0].Url);
Assert.Equal("https://example.test/advisory-2", result.Linkset.References[1].Url);
Assert.Equal("patch", result.Linkset.References[2].Type);
Assert.Equal(new[] { "build", "runtime" }, result.Linkset.Scopes);
Assert.Equal(2, result.Linkset.Relationships.Length);
Assert.Equal("affects", result.Linkset.Relationships[0].Type);
Assert.Equal("component-x", result.Linkset.Relationships[0].Target);
Assert.Equal("depends_on", result.Linkset.Relationships[1].Type);
Assert.Equal("pkg:npm/lib@2.0.0", result.Linkset.Relationships[1].Target);
Assert.False(result.HasMore);
Assert.Null(result.NextCursor);
}
[Fact]
public async Task QueryAsync_WithAliasFilter_UsesAliasLookupAndFilters()
@@ -218,9 +235,11 @@ public sealed class AdvisoryObservationQueryServiceTests
IEnumerable<string> purls,
IEnumerable<string> cpes,
IEnumerable<AdvisoryObservationReference> references,
DateTimeOffset createdAt)
{
var raw = JsonNode.Parse("""{"message":"payload"}""") ?? throw new InvalidOperationException("Raw payload must not be null.");
DateTimeOffset createdAt,
IEnumerable<string>? scopes = null,
IEnumerable<RawRelationship>? relationships = null)
{
var raw = JsonNode.Parse("""{"message":"payload"}""") ?? throw new InvalidOperationException("Raw payload must not be null.");
var upstream = new AdvisoryObservationUpstream(
upstreamId: observationId,
@@ -239,7 +258,9 @@ public sealed class AdvisoryObservationQueryServiceTests
Cpes = cpes.ToImmutableArray(),
References = references
.Select(static reference => new RawReference(reference.Type, reference.Url))
.ToImmutableArray()
.ToImmutableArray(),
Scopes = scopes?.ToImmutableArray() ?? ImmutableArray<string>.Empty,
Relationships = relationships?.ToImmutableArray() ?? ImmutableArray<RawRelationship>.Empty
};
return new AdvisoryObservation(

View File

@@ -0,0 +1,171 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Driver;
using StellaOps.Concelier.Core.Linksets;
using StellaOps.Concelier.Storage.Mongo;
using StellaOps.Concelier.Storage.Mongo.Linksets;
using StellaOps.Concelier.Testing;
using Xunit;
namespace StellaOps.Concelier.Storage.Mongo.Tests.Linksets;
public sealed class ConcelierMongoLinksetStoreTests : IClassFixture<MongoIntegrationFixture>
{
private readonly MongoIntegrationFixture _fixture;
public ConcelierMongoLinksetStoreTests(MongoIntegrationFixture fixture)
{
_fixture = fixture;
}
[Fact]
public void MapToDocument_StoresConfidenceAndConflicts()
{
var linkset = new AdvisoryLinkset(
"tenant",
"ghsa",
"GHSA-1234",
ImmutableArray.Create("obs-1", "obs-2"),
null,
new AdvisoryLinksetProvenance(new[] { "h1", "h2" }, "tool", "policy"),
0.82,
new List<AdvisoryLinksetConflict>
{
new("severity", "disagree", new[] { "HIGH", "MEDIUM" })
},
DateTimeOffset.UtcNow,
"job-1");
var method = typeof(ConcelierMongoLinksetStore).GetMethod(
"MapToDocument",
BindingFlags.NonPublic | BindingFlags.Static);
Assert.NotNull(method);
var document = (AdvisoryLinksetDocument)method!.Invoke(null, new object?[] { linkset })!;
Assert.Equal(linkset.Confidence, document.Confidence);
Assert.NotNull(document.Conflicts);
Assert.Single(document.Conflicts!);
Assert.Equal("severity", document.Conflicts![0].Field);
Assert.Equal("disagree", document.Conflicts![0].Reason);
}
[Fact]
public void FromDocument_RestoresConfidenceAndConflicts()
{
var doc = new AdvisoryLinksetDocument
{
TenantId = "tenant",
Source = "ghsa",
AdvisoryId = "GHSA-1234",
Observations = new List<string> { "obs-1" },
Confidence = 0.5,
Conflicts = new List<AdvisoryLinksetConflictDocument>
{
new()
{
Field = "references",
Reason = "mismatch",
Values = new List<string> { "url1", "url2" }
}
},
CreatedAt = DateTime.UtcNow
};
var method = typeof(ConcelierMongoLinksetStore).GetMethod(
"FromDocument",
BindingFlags.NonPublic | BindingFlags.Static);
Assert.NotNull(method);
var model = (AdvisoryLinkset)method!.Invoke(null, new object?[] { doc })!;
Assert.Equal(0.5, model.Confidence);
Assert.NotNull(model.Conflicts);
Assert.Single(model.Conflicts!);
Assert.Equal("references", model.Conflicts![0].Field);
}
[Fact]
public async Task FindByTenantAsync_OrdersByCreatedAtThenAdvisoryId()
{
await _fixture.Database.DropCollectionAsync(MongoStorageDefaults.Collections.AdvisoryLinksets);
var collection = _fixture.Database.GetCollection<AdvisoryLinksetDocument>(MongoStorageDefaults.Collections.AdvisoryLinksets);
var store = new ConcelierMongoLinksetStore(collection);
var now = DateTimeOffset.UtcNow;
var linksets = new[]
{
new AdvisoryLinkset("Tenant-A", "src", "ADV-002", ImmutableArray.Create("obs-1"), null, null, null, null, now, "job-1"),
new AdvisoryLinkset("Tenant-A", "src", "ADV-001", ImmutableArray.Create("obs-2"), null, null, null, null, now, "job-2"),
new AdvisoryLinkset("Tenant-A", "src", "ADV-003", ImmutableArray.Create("obs-3"), null, null, null, null, now.AddMinutes(-5), "job-3")
};
foreach (var linkset in linksets)
{
await store.UpsertAsync(linkset, CancellationToken.None);
}
var results = await store.FindByTenantAsync("TENANT-A", null, null, cursor: null, limit: 10, cancellationToken: CancellationToken.None);
Assert.Equal(new[] { "ADV-001", "ADV-002", "ADV-003" }, results.Select(r => r.AdvisoryId));
}
[Fact]
public async Task FindByTenantAsync_AppliesCursorForDeterministicPaging()
{
await _fixture.Database.DropCollectionAsync(MongoStorageDefaults.Collections.AdvisoryLinksets);
var collection = _fixture.Database.GetCollection<AdvisoryLinksetDocument>(MongoStorageDefaults.Collections.AdvisoryLinksets);
var store = new ConcelierMongoLinksetStore(collection);
var now = DateTimeOffset.UtcNow;
var firstPage = new[]
{
new AdvisoryLinkset("tenant-a", "src", "ADV-010", ImmutableArray.Create("obs-1"), null, null, null, null, now, "job-1"),
new AdvisoryLinkset("tenant-a", "src", "ADV-020", ImmutableArray.Create("obs-2"), null, null, null, null, now, "job-2"),
new AdvisoryLinkset("tenant-a", "src", "ADV-030", ImmutableArray.Create("obs-3"), null, null, null, null, now.AddMinutes(-10), "job-3")
};
foreach (var linkset in firstPage)
{
await store.UpsertAsync(linkset, CancellationToken.None);
}
var initial = await store.FindByTenantAsync("tenant-a", null, null, cursor: null, limit: 10, cancellationToken: CancellationToken.None);
var cursor = new AdvisoryLinksetCursor(initial[1].CreatedAt, initial[1].AdvisoryId);
var paged = await store.FindByTenantAsync("tenant-a", null, null, cursor, limit: 10, cancellationToken: CancellationToken.None);
Assert.Single(paged);
Assert.Equal("ADV-030", paged[0].AdvisoryId);
}
[Fact]
public async Task Upsert_NormalizesTenantToLowerInvariant()
{
await _fixture.Database.DropCollectionAsync(MongoStorageDefaults.Collections.AdvisoryLinksets);
var collection = _fixture.Database.GetCollection<AdvisoryLinksetDocument>(MongoStorageDefaults.Collections.AdvisoryLinksets);
var store = new ConcelierMongoLinksetStore(collection);
var linkset = new AdvisoryLinkset("Tenant-A", "ghsa", "GHSA-1", ImmutableArray.Create("obs-1"), null, null, null, null, DateTimeOffset.UtcNow, "job-1");
await store.UpsertAsync(linkset, CancellationToken.None);
var fetched = await collection.Find(Builders<AdvisoryLinksetDocument>.Filter.Empty).FirstOrDefaultAsync();
Assert.NotNull(fetched);
Assert.Equal("tenant-a", fetched!.TenantId);
var results = await store.FindByTenantAsync("TENANT-A", null, null, cursor: null, limit: 10, cancellationToken: CancellationToken.None);
Assert.Single(results);
Assert.Equal("GHSA-1", results[0].AdvisoryId);
}
}

View File

@@ -0,0 +1,39 @@
using System.Threading.Tasks;
using MongoDB.Bson;
using MongoDB.Driver;
using StellaOps.Concelier.Storage.Mongo.Migrations;
using StellaOps.Concelier.Testing;
using Xunit;
namespace StellaOps.Concelier.Storage.Mongo.Tests.Migrations;
[Collection("mongo-fixture")]
public sealed class EnsureAdvisoryLinksetsTenantLowerMigrationTests : IClassFixture<MongoIntegrationFixture>
{
private readonly MongoIntegrationFixture _fixture;
public EnsureAdvisoryLinksetsTenantLowerMigrationTests(MongoIntegrationFixture fixture)
{
_fixture = fixture;
}
[Fact]
public async Task ApplyAsync_LowersTenantIds()
{
var collection = _fixture.Database.GetCollection<BsonDocument>(MongoStorageDefaults.Collections.AdvisoryLinksets);
await _fixture.Database.DropCollectionAsync(MongoStorageDefaults.Collections.AdvisoryLinksets);
await collection.InsertManyAsync(new[]
{
new BsonDocument { { "TenantId", "Tenant-A" }, { "Source", "src" }, { "AdvisoryId", "ADV-1" }, { "Observations", new BsonArray() } },
new BsonDocument { { "TenantId", "tenant-b" }, { "Source", "src" }, { "AdvisoryId", "ADV-2" }, { "Observations", new BsonArray() } }
});
var migration = new EnsureAdvisoryLinksetsTenantLowerMigration();
await migration.ApplyAsync(_fixture.Database, default);
var all = await collection.Find(FilterDefinition<BsonDocument>.Empty).ToListAsync();
Assert.Contains(all, doc => doc["TenantId"] == "tenant-a");
Assert.Contains(all, doc => doc["TenantId"] == "tenant-b");
}
}

View File

@@ -56,6 +56,11 @@ public sealed class AdvisoryObservationDocumentFactoryTests
RawLinkset = new AdvisoryObservationRawLinksetDocument
{
Aliases = new List<string> { "CVE-2025-1234", "cve-2025-1234" },
Scopes = new List<string> { "runtime", "build" },
Relationships = new List<AdvisoryObservationRawRelationshipDocument>
{
new() { Type = "depends_on", Source = "componentA", Target = "componentB", Provenance = "sbom-manifest" }
},
PackageUrls = new List<string> { "pkg:generic/foo@1.0.0" },
Cpes = new List<string> { "cpe:/a:vendor:product:1" },
References = new List<AdvisoryObservationRawReferenceDocument>
@@ -78,6 +83,11 @@ public sealed class AdvisoryObservationDocumentFactoryTests
Assert.True(observation.Content.Raw?["example"]?.GetValue<bool>());
Assert.Equal(document.Linkset.References![0].Type, observation.Linkset.References[0].Type);
Assert.Equal(new[] { "CVE-2025-1234", "cve-2025-1234" }, observation.RawLinkset.Aliases);
Assert.Equal(new[] { "runtime", "build" }, observation.RawLinkset.Scopes);
Assert.Equal("depends_on", observation.RawLinkset.Relationships[0].Type);
Assert.Equal("componentA", observation.RawLinkset.Relationships[0].Source);
Assert.Equal("componentB", observation.RawLinkset.Relationships[0].Target);
Assert.Equal("sbom-manifest", observation.RawLinkset.Relationships[0].Provenance);
Assert.Equal("Advisory", observation.RawLinkset.References[0].Type);
Assert.Equal("vendor", observation.RawLinkset.References[0].Source);
Assert.Equal("note-value", observation.RawLinkset.Notes["note-key"]);

View File

@@ -0,0 +1,94 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using MongoDB.Bson;
using StellaOps.Concelier.Models.Observations;
using StellaOps.Concelier.Storage.Mongo.Observations.V1;
using Xunit;
namespace StellaOps.Concelier.Storage.Mongo.Tests.Observations;
public sealed class AdvisoryObservationV1DocumentFactoryTests
{
[Fact]
public void ObservationIdBuilder_IsDeterministic()
{
var id1 = ObservationIdBuilder.Create("TENANT", "Ghsa", "GHSA-1234", "sha256:abc");
var id2 = ObservationIdBuilder.Create("tenant", "ghsa", "GHSA-1234", "sha256:abc");
Assert.Equal(id1, id2);
}
[Fact]
public void ToModel_MapsAndNormalizes()
{
var document = new AdvisoryObservationV1Document
{
Id = new ObjectId("6710f1f1a1b2c3d4e5f60708"),
TenantId = "TENANT-01",
Source = "GHSA",
AdvisoryId = "GHSA-2025-0001",
Title = "Test title",
Summary = "Summary",
Severities = new List<ObservationSeverityDocument>
{
new() { System = "cvssv3.1", Score = 7.5, Vector = "AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N" }
},
Affected = new List<ObservationAffectedDocument>
{
new()
{
Purl = "pkg:nuget/foo@1.2.3",
Package = "foo",
Versions = new List<string>{ "1.2.3" },
Ranges = new List<ObservationVersionRangeDocument>
{
new()
{
Type = "ECOSYSTEM",
Events = new List<ObservationRangeEventDocument>
{
new(){ Event = "introduced", Value = "1.0.0" },
new(){ Event = "fixed", Value = "1.2.3" }
}
}
},
Ecosystem = "nuget",
Cpes = new List<string>{ "cpe:/a:foo:bar:1.2.3" }
}
},
References = new List<string>{ "https://example.test/advisory" },
Weaknesses = new List<string>{ "CWE-79" },
Published = new DateTime(2025, 11, 1, 0, 0, 0, DateTimeKind.Utc),
Modified = new DateTime(2025, 11, 10, 0, 0, 0, DateTimeKind.Utc),
IngestedAt = new DateTime(2025, 11, 12, 0, 0, 0, DateTimeKind.Utc),
Provenance = new ObservationProvenanceDocument
{
SourceArtifactSha = "sha256:abc",
FetchedAt = new DateTime(2025, 11, 12, 0, 0, 0, DateTimeKind.Utc),
IngestJobId = "job-1",
Signature = new ObservationSignatureDocument
{
Present = true,
Format = "dsse",
KeyId = "k1",
Signature = "sig"
}
}
};
var model = AdvisoryObservationV1DocumentFactory.ToModel(document);
Assert.Equal("6710f1f1a1b2c3d4e5f60708", model.ObservationId);
Assert.Equal("tenant-01", model.Tenant);
Assert.Equal("ghsa", model.Source);
Assert.Equal("GHSA-2025-0001", model.AdvisoryId);
Assert.Equal("Test title", model.Title);
Assert.Single(model.Severities);
Assert.Single(model.Affected);
Assert.Single(model.References);
Assert.Single(model.Weaknesses);
Assert.Equal(new DateTimeOffset(2025, 11, 12, 0, 0, 0, TimeSpan.Zero), model.IngestedAt);
Assert.NotNull(model.Provenance.Signature);
}
}

View File

@@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using MongoDB.Bson.Serialization.Attributes;
namespace StellaOps.Concelier.WebService.Tests;
/// <summary>
/// Minimal linkset document used only for seeding the Mongo collection in WebService integration tests.
/// Matches the shape written by the linkset ingestion pipeline.
/// </summary>
internal sealed class AdvisoryLinksetDocument
{
[BsonElement("tenantId")]
public string TenantId { get; init; } = string.Empty;
[BsonElement("source")]
public string Source { get; init; } = string.Empty;
[BsonElement("advisoryId")]
public string AdvisoryId { get; init; } = string.Empty;
[BsonElement("observations")]
public IReadOnlyList<string> Observations { get; init; } = Array.Empty<string>();
[BsonElement("createdAt")]
public DateTime CreatedAt { get; init; }
[BsonElement("normalized")]
public AdvisoryLinksetNormalizedDocument Normalized { get; init; } = new();
}
internal sealed class AdvisoryLinksetNormalizedDocument
{
[BsonElement("purls")]
public IReadOnlyList<string> Purls { get; init; } = Array.Empty<string>();
[BsonElement("versions")]
public IReadOnlyList<string> Versions { get; init; } = Array.Empty<string>();
}
/// <summary>
/// Shape used when reading /linksets responses in WebService endpoint tests.
/// </summary>
internal sealed class AdvisoryLinksetQueryResponse
{
public AdvisoryLinksetResponse[] Linksets { get; init; } = Array.Empty<AdvisoryLinksetResponse>();
public bool HasMore { get; init; }
public string? NextCursor { get; init; }
}
internal sealed class AdvisoryLinksetResponse
{
public string AdvisoryId { get; init; } = string.Empty;
public IReadOnlyList<string> Purls { get; init; } = Array.Empty<string>();
public IReadOnlyList<string> Versions { get; init; } = Array.Empty<string>();
}

View File

@@ -33,6 +33,7 @@ using StellaOps.Concelier.Merge.Services;
using StellaOps.Concelier.Storage.Mongo;
using StellaOps.Concelier.Storage.Mongo.Advisories;
using StellaOps.Concelier.Storage.Mongo.Observations;
using StellaOps.Concelier.Storage.Mongo.Linksets;
using StellaOps.Concelier.Core.Raw;
using StellaOps.Concelier.WebService.Jobs;
using StellaOps.Concelier.WebService.Options;
@@ -376,13 +377,12 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
var root = document.RootElement;
Assert.Equal("CVE-2025-0001", root.GetProperty("advisoryKey").GetString());
Assert.False(string.IsNullOrWhiteSpace(root.GetProperty("fingerprint").GetString()));
Assert.Equal(1, root.GetProperty("total").GetInt32());
Assert.False(root.GetProperty("truncated").GetBoolean());
var entry = Assert.Single(root.GetProperty("entries").EnumerateArray());
Assert.Equal("workaround", entry.GetProperty("type").GetString());
Assert.Equal("tenant-a:chunk:newest", entry.GetProperty("documentId").GetString());
Assert.Equal("/references/0", entry.GetProperty("fieldPath").GetString());
Assert.False(string.IsNullOrWhiteSpace(entry.GetProperty("chunkId").GetString()));
var content = entry.GetProperty("content");
@@ -391,6 +391,8 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
Assert.Equal("https://vendor.example/workaround", content.GetProperty("url").GetString());
var provenance = entry.GetProperty("provenance");
Assert.Equal("tenant-a:chunk:newest", provenance.GetProperty("documentId").GetString());
Assert.Equal("/references/0", provenance.GetProperty("observationPath").GetString());
Assert.Equal("nvd", provenance.GetProperty("source").GetString());
Assert.Equal("workaround", provenance.GetProperty("kind").GetString());
Assert.Equal("tenant-a:chunk:newest", provenance.GetProperty("value").GetString());
@@ -638,6 +640,9 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
using var client = _factory.CreateClient();
long expectedSegments = 0;
string expectedTruncatedTag = "false";
var metrics = await CaptureMetricsAsync(
AdvisoryAiMetrics.MeterName,
new[]
@@ -654,6 +659,13 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
var first = await client.GetAsync(url);
first.EnsureSuccessStatusCode();
using (var firstDocument = await first.Content.ReadFromJsonAsync<JsonDocument>())
{
Assert.NotNull(firstDocument);
expectedSegments = firstDocument!.RootElement.GetProperty("entries").GetArrayLength();
expectedTruncatedTag = firstDocument.RootElement.GetProperty("truncated").GetBoolean() ? "true" : "false";
}
var second = await client.GetAsync(url);
second.EnsureSuccessStatusCode();
});
@@ -679,7 +691,11 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
Assert.True(metrics.TryGetValue("advisory_ai_chunk_segments", out var segmentMeasurements));
Assert.Equal(2, segmentMeasurements!.Count);
Assert.Contains(segmentMeasurements!, measurement => GetTagValue(measurement, "truncated") == "false");
Assert.All(segmentMeasurements!, measurement =>
{
Assert.Equal(expectedSegments, measurement.Value);
Assert.Equal(expectedTruncatedTag, GetTagValue(measurement, "truncated"));
});
Assert.True(metrics.TryGetValue("advisory_ai_chunk_sources", out var sourceMeasurements));
Assert.Equal(2, sourceMeasurements!.Count);
@@ -2522,6 +2538,7 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
Array.Empty<string>(),
references,
Array.Empty<string>(),
Array.Empty<string>(),
new Dictionary<string, string> { ["note"] = "ingest-test" }));
}