Add unit tests and implementations for MongoDB index models and OpenAPI metadata

- Implemented `MongoIndexModelTests` to verify index models for various stores.
- Created `OpenApiMetadataFactory` with methods to generate OpenAPI metadata.
- Added tests for `OpenApiMetadataFactory` to ensure expected defaults and URL overrides.
- Introduced `ObserverSurfaceSecrets` and `WebhookSurfaceSecrets` for managing secrets.
- Developed `RuntimeSurfaceFsClient` and `WebhookSurfaceFsClient` for manifest retrieval.
- Added dependency injection tests for `SurfaceEnvironmentRegistration` in both Observer and Webhook contexts.
- Implemented tests for secret resolution in `ObserverSurfaceSecretsTests` and `WebhookSurfaceSecretsTests`.
- Created `EnsureLinkNotMergeCollectionsMigrationTests` to validate MongoDB migration logic.
- Added project files for MongoDB tests and NuGet package mirroring.
This commit is contained in:
master
2025-11-17 21:21:56 +02:00
parent d3128aec24
commit 9075bad2d9
146 changed files with 152183 additions and 82 deletions

View File

@@ -0,0 +1,215 @@
using System.Text.Json;
namespace StellaOps.Scanner.Analyzers.Lang.DotNet.Internal;
/// <summary>
/// Resolves publish artifacts (deps/runtimeconfig) into deterministic entrypoint identities.
/// </summary>
public static class DotNetEntrypointResolver
{
private static readonly EnumerationOptions Enumeration = new()
{
RecurseSubdirectories = true,
IgnoreInaccessible = true,
AttributesToSkip = FileAttributes.Device | FileAttributes.ReparsePoint
};
public static ValueTask<IReadOnlyList<DotNetEntrypoint>> ResolveAsync(
LanguageAnalyzerContext context,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(context);
var depsFiles = Directory
.EnumerateFiles(context.RootPath, "*.deps.json", Enumeration)
.OrderBy(static path => path, StringComparer.Ordinal)
.ToArray();
if (depsFiles.Length == 0)
{
return ValueTask.FromResult<IReadOnlyList<DotNetEntrypoint>>(Array.Empty<DotNetEntrypoint>());
}
var results = new List<DotNetEntrypoint>(depsFiles.Length);
foreach (var depsPath in depsFiles)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
var relativeDepsPath = NormalizeRelative(context.GetRelativePath(depsPath));
var depsFile = DotNetDepsFile.Load(depsPath, relativeDepsPath, cancellationToken);
if (depsFile is null)
{
continue;
}
DotNetRuntimeConfig? runtimeConfig = null;
var runtimeConfigPath = Path.ChangeExtension(depsPath, ".runtimeconfig.json");
string? relativeRuntimeConfig = null;
if (!string.IsNullOrEmpty(runtimeConfigPath) && File.Exists(runtimeConfigPath))
{
relativeRuntimeConfig = NormalizeRelative(context.GetRelativePath(runtimeConfigPath));
runtimeConfig = DotNetRuntimeConfig.Load(runtimeConfigPath, relativeRuntimeConfig, cancellationToken);
}
var tfms = CollectTargetFrameworks(depsFile, runtimeConfig);
var rids = CollectRuntimeIdentifiers(depsFile, runtimeConfig);
var publishKind = DeterminePublishKind(depsFile);
var name = GetEntrypointName(depsPath);
var id = BuildDeterministicId(name, tfms, rids, publishKind);
results.Add(new DotNetEntrypoint(
Id: id,
Name: name,
TargetFrameworks: tfms,
RuntimeIdentifiers: rids,
RelativeDepsPath: relativeDepsPath,
RelativeRuntimeConfigPath: relativeRuntimeConfig,
PublishKind: publishKind));
}
catch (IOException)
{
continue;
}
catch (JsonException)
{
continue;
}
catch (UnauthorizedAccessException)
{
continue;
}
}
return ValueTask.FromResult<IReadOnlyList<DotNetEntrypoint>>(results);
}
private static string GetEntrypointName(string depsPath)
{
// Strip .json then any trailing .deps suffix to yield a logical entrypoint name.
var stem = Path.GetFileNameWithoutExtension(depsPath); // removes .json
if (stem.EndsWith(".deps", StringComparison.OrdinalIgnoreCase))
{
stem = stem[..^".deps".Length];
}
return stem;
}
private static IReadOnlyCollection<string> CollectTargetFrameworks(DotNetDepsFile depsFile, DotNetRuntimeConfig? runtimeConfig)
{
var tfms = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var library in depsFile.Libraries.Values)
{
foreach (var tfm in library.TargetFrameworks)
{
tfms.Add(tfm);
}
}
if (runtimeConfig is not null)
{
foreach (var tfm in runtimeConfig.Tfms)
{
tfms.Add(tfm);
}
foreach (var framework in runtimeConfig.Frameworks)
{
tfms.Add(framework);
}
}
return tfms;
}
private static IReadOnlyCollection<string> CollectRuntimeIdentifiers(DotNetDepsFile depsFile, DotNetRuntimeConfig? runtimeConfig)
{
var rids = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var library in depsFile.Libraries.Values)
{
foreach (var rid in library.RuntimeIdentifiers)
{
rids.Add(rid);
}
}
if (runtimeConfig is not null)
{
foreach (var entry in runtimeConfig.RuntimeGraph)
{
rids.Add(entry.Rid);
foreach (var fallback in entry.Fallbacks)
{
if (!string.IsNullOrWhiteSpace(fallback))
{
rids.Add(fallback);
}
}
}
}
return rids;
}
private static DotNetPublishKind DeterminePublishKind(DotNetDepsFile depsFile)
{
foreach (var library in depsFile.Libraries.Values)
{
if (library.Id.StartsWith("Microsoft.NETCore.App.Runtime.", StringComparison.OrdinalIgnoreCase) ||
library.Id.StartsWith("Microsoft.WindowsDesktop.App.Runtime.", StringComparison.OrdinalIgnoreCase))
{
return DotNetPublishKind.SelfContained;
}
}
return DotNetPublishKind.FrameworkDependent;
}
private static string BuildDeterministicId(
string name,
IReadOnlyCollection<string> tfms,
IReadOnlyCollection<string> rids,
DotNetPublishKind publishKind)
{
var tfmPart = tfms.Count == 0 ? "unknown" : string.Join('+', tfms.OrderBy(t => t, StringComparer.OrdinalIgnoreCase));
var ridPart = rids.Count == 0 ? "none" : string.Join('+', rids.OrderBy(r => r, StringComparer.OrdinalIgnoreCase));
var publishPart = publishKind.ToString().ToLowerInvariant();
return $"{name}:{tfmPart}:{ridPart}:{publishPart}";
}
private static string NormalizeRelative(string path)
{
if (string.IsNullOrWhiteSpace(path) || path == ".")
{
return ".";
}
var normalized = path.Replace('\\', '/');
return string.IsNullOrWhiteSpace(normalized) ? "." : normalized;
}
}
public sealed record DotNetEntrypoint(
string Id,
string Name,
IReadOnlyCollection<string> TargetFrameworks,
IReadOnlyCollection<string> RuntimeIdentifiers,
string RelativeDepsPath,
string? RelativeRuntimeConfigPath,
DotNetPublishKind PublishKind);
public enum DotNetPublishKind
{
Unknown = 0,
FrameworkDependent = 1,
SelfContained = 2
}

View File

@@ -0,0 +1,7 @@
namespace StellaOps.Scanner.Analyzers.Lang;
public sealed class AnalysisSnapshot
{
public IReadOnlyList<LanguageEntrypointSnapshot> Entrypoints { get; set; } = Array.Empty<LanguageEntrypointSnapshot>();
public IReadOnlyList<LanguageComponentSnapshot> Components { get; set; } = Array.Empty<LanguageComponentSnapshot>();
}

View File

@@ -0,0 +1,15 @@
using System;
namespace StellaOps.Scanner.Analyzers.Lang;
internal static class LanguageComponentEvidenceExtensions
{
/// <summary>
/// Builds a stable key for evidence items to support deterministic dictionaries.
/// </summary>
public static string ToKey(this LanguageComponentEvidence evidence)
{
ArgumentNullException.ThrowIfNull(evidence);
return $"{evidence.Kind}:{evidence.Source}:{evidence.Locator}".ToLowerInvariant();
}
}

View File

@@ -0,0 +1,96 @@
using System.Text.Json.Serialization;
namespace StellaOps.Scanner.Analyzers.Lang;
public sealed class LanguageEntrypointRecord
{
private readonly SortedDictionary<string, string?> _metadata;
private readonly SortedDictionary<string, LanguageComponentEvidence> _evidence;
public LanguageEntrypointRecord(
string id,
string name,
IEnumerable<KeyValuePair<string, string?>>? metadata = null,
IEnumerable<LanguageComponentEvidence>? evidence = null)
{
if (string.IsNullOrWhiteSpace(id))
{
throw new ArgumentException("Entrypoint id is required", nameof(id));
}
Id = id.Trim();
Name = string.IsNullOrWhiteSpace(name) ? Id : name.Trim();
_metadata = new SortedDictionary<string, string?>(StringComparer.Ordinal);
foreach (var pair in metadata ?? Array.Empty<KeyValuePair<string, string?>>())
{
_metadata[pair.Key] = pair.Value;
}
_evidence = new SortedDictionary<string, LanguageComponentEvidence>(StringComparer.Ordinal);
foreach (var item in evidence ?? Array.Empty<LanguageComponentEvidence>())
{
var key = item.ToKey();
_evidence[key] = item;
}
}
public string Id { get; }
public string Name { get; }
public IReadOnlyDictionary<string, string?> Metadata => _metadata;
public IReadOnlyCollection<LanguageComponentEvidence> Evidence => _evidence.Values;
internal LanguageEntrypointSnapshot ToSnapshot()
=> new()
{
Id = Id,
Name = Name,
Metadata = _metadata.ToDictionary(static kvp => kvp.Key, static kvp => kvp.Value, StringComparer.Ordinal),
Evidence = _evidence.Values
.Select(static item => new LanguageComponentEvidenceSnapshot
{
Kind = item.Kind,
Source = item.Source,
Locator = item.Locator,
Value = item.Value,
Sha256 = item.Sha256
})
.ToList()
};
internal static LanguageEntrypointRecord FromSnapshot(LanguageEntrypointSnapshot snapshot)
{
if (snapshot is null)
{
throw new ArgumentNullException(nameof(snapshot));
}
var evidence = snapshot.Evidence?
.Select(static e => new LanguageComponentEvidence(e.Kind, e.Source, e.Locator, e.Value, e.Sha256))
.ToArray();
return new LanguageEntrypointRecord(
snapshot.Id ?? string.Empty,
snapshot.Name ?? snapshot.Id ?? string.Empty,
snapshot.Metadata,
evidence);
}
}
public sealed class LanguageEntrypointSnapshot
{
[JsonPropertyName("id")]
public string? Id { get; set; }
[JsonPropertyName("name")]
public string? Name { get; set; }
[JsonPropertyName("metadata")]
public Dictionary<string, string?> Metadata { get; set; } = new(StringComparer.Ordinal);
[JsonPropertyName("evidence")]
public List<LanguageComponentEvidenceSnapshot> Evidence { get; set; } = new();
}

View File

@@ -0,0 +1,5 @@
# EntryTrace Tasks
| Task ID | Status | Date | Summary |
| --- | --- | --- | --- |
| SCANNER-ENG-0008 | DONE | 2025-11-16 | Documented quarterly EntryTrace heuristic cadence and workflow; attached to Sprint 0138 Execution Log. |