sln build fix (again), tests fixes, audit work and doctors work

This commit is contained in:
master
2026-01-12 22:15:51 +02:00
parent 9873f80830
commit 9330c64349
812 changed files with 48051 additions and 3891 deletions

View File

@@ -1,7 +1,7 @@
# Concelier Testing Task Board
This board mirrors active sprint tasks for this module.
Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
| Task ID | Status | Notes |
| --- | --- | --- |

View File

@@ -1,7 +1,7 @@
# StellaOps.Infrastructure.Postgres.Testing Task Board
This board mirrors active sprint tasks for this module.
Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
| Task ID | Status | Notes |
| --- | --- | --- |

View File

@@ -0,0 +1,345 @@
// -----------------------------------------------------------------------------
// TestRunAttestationGenerator.cs
// Sprint: Testing Enhancement Advisory - Phase 1.3
// Description: Generates DSSE-signed attestations linking test outputs to inputs
// -----------------------------------------------------------------------------
using System.Collections.Immutable;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using StellaOps.Testing.Manifests.Models;
namespace StellaOps.Testing.Manifests.Attestation;
/// <summary>
/// Service for generating in-toto attestations for test runs.
/// Links test outputs to their inputs (SBOMs, VEX documents, feeds).
/// </summary>
public sealed class TestRunAttestationGenerator : ITestRunAttestationGenerator
{
private const string DssePayloadType = "application/vnd.in-toto+json";
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
private readonly ITestRunAttestationSigner? _signer;
private readonly TimeProvider _timeProvider;
private readonly IAttestationIdGenerator _idGenerator;
public TestRunAttestationGenerator(
ITestRunAttestationSigner? signer = null,
TimeProvider? timeProvider = null,
IAttestationIdGenerator? idGenerator = null)
{
_signer = signer;
_timeProvider = timeProvider ?? TimeProvider.System;
_idGenerator = idGenerator ?? new GuidAttestationIdGenerator();
}
/// <summary>
/// Generates a test run attestation linking outputs to inputs.
/// </summary>
public async Task<TestRunAttestation> GenerateAsync(
RunManifest manifest,
ImmutableArray<TestRunOutput> outputs,
TestRunEvidence evidence,
CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(manifest);
ArgumentNullException.ThrowIfNull(evidence);
var statement = CreateInTotoStatement(manifest, outputs, evidence);
var statementBytes = SerializeToCanonicalJson(statement);
var statementDigest = ComputeSha256Digest(statementBytes);
var envelope = await CreateDsseEnvelopeAsync(statementBytes, ct);
return new TestRunAttestation
{
AttestationId = _idGenerator.NewId(),
RunId = manifest.RunId,
CreatedAt = _timeProvider.GetUtcNow(),
Statement = statement,
StatementDigest = statementDigest,
Envelope = envelope,
Success = evidence.Success,
OutputCount = outputs.Length
};
}
/// <summary>
/// Generates attestations for multiple test runs.
/// </summary>
public async Task<ImmutableArray<TestRunAttestation>> GenerateBatchAsync(
IEnumerable<(RunManifest Manifest, ImmutableArray<TestRunOutput> Outputs, TestRunEvidence Evidence)> runs,
CancellationToken ct = default)
{
var attestations = ImmutableArray.CreateBuilder<TestRunAttestation>();
foreach (var (manifest, outputs, evidence) in runs)
{
ct.ThrowIfCancellationRequested();
var attestation = await GenerateAsync(manifest, outputs, evidence, ct);
attestations.Add(attestation);
}
return attestations.ToImmutable();
}
/// <summary>
/// Verifies a test run attestation's integrity.
/// </summary>
public Task<TestRunAttestationVerificationResult> VerifyAsync(
TestRunAttestation attestation,
ITestRunAttestationVerifier? verifier = null,
CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(attestation);
var errors = ImmutableArray.CreateBuilder<string>();
var signatureVerified = false;
string? verifiedKeyId = null;
// Verify statement digest
if (attestation.Envelope is not null)
{
try
{
var payloadBytes = Convert.FromBase64String(attestation.Envelope.Payload);
var payloadDigest = ComputeSha256Digest(payloadBytes);
if (payloadDigest != attestation.StatementDigest)
{
errors.Add($"Envelope payload digest mismatch: expected {attestation.StatementDigest}, got {payloadDigest}");
}
}
catch (FormatException)
{
errors.Add("Invalid base64 in envelope payload");
}
}
else
{
var statementBytes = SerializeToCanonicalJson(attestation.Statement);
var computedDigest = ComputeSha256Digest(statementBytes);
if (computedDigest != attestation.StatementDigest)
{
errors.Add($"Statement digest mismatch: expected {attestation.StatementDigest}, got {computedDigest}");
}
}
// Verify signature if envelope exists and verifier is provided
if (attestation.Envelope is not null && attestation.Envelope.Signatures.Length > 0)
{
if (verifier is null)
{
errors.Add("Signature verifier not provided for signed attestation");
}
else
{
// Signature verification would be done here
// For now, mark as not verified without a verifier
signatureVerified = false;
}
}
return Task.FromResult(new TestRunAttestationVerificationResult
{
IsValid = errors.Count == 0,
Errors = errors.ToImmutable(),
SignatureVerified = signatureVerified,
VerifiedKeyId = verifiedKeyId,
VerifiedAt = _timeProvider.GetUtcNow()
});
}
private TestRunInTotoStatement CreateInTotoStatement(
RunManifest manifest,
ImmutableArray<TestRunOutput> outputs,
TestRunEvidence evidence)
{
var subjects = outputs.Select(o => new TestRunSubject
{
Name = o.Name,
Digest = ImmutableDictionary<string, string>.Empty
.Add("sha256", o.Digest.Replace("sha256:", ""))
}).ToImmutableArray();
return new TestRunInTotoStatement
{
Subject = subjects,
Predicate = new TestRunPredicate
{
RunId = manifest.RunId,
ManifestSchemaVersion = manifest.SchemaVersion,
SbomDigests = manifest.SbomDigests.Select(s => s.Digest).ToImmutableArray(),
VexDigests = evidence.VexDigests,
FeedSnapshot = new TestRunFeedSnapshotRef
{
FeedId = manifest.FeedSnapshot.FeedId,
Version = manifest.FeedSnapshot.Version,
Digest = manifest.FeedSnapshot.Digest,
SnapshotAt = manifest.FeedSnapshot.SnapshotAt
},
PolicyDigest = manifest.PolicySnapshot.LatticeRulesDigest,
ToolVersions = new TestRunToolVersionsRef
{
ScannerVersion = manifest.ToolVersions.ScannerVersion,
SbomGeneratorVersion = manifest.ToolVersions.SbomGeneratorVersion,
ReachabilityEngineVersion = manifest.ToolVersions.ReachabilityEngineVersion,
AttestorVersion = manifest.ToolVersions.AttestorVersion
},
ExecutedAt = _timeProvider.GetUtcNow(),
InitiatedAt = manifest.InitiatedAt,
DurationMs = evidence.DurationMs,
TestCount = evidence.TestCount,
PassedCount = evidence.PassedCount,
FailedCount = evidence.FailedCount,
SkippedCount = evidence.SkippedCount,
Success = evidence.Success,
DeterminismVerified = evidence.DeterminismVerified,
Metadata = evidence.Metadata
}
};
}
private async Task<TestRunDsseEnvelope?> CreateDsseEnvelopeAsync(
byte[] payload,
CancellationToken ct)
{
var signatures = ImmutableArray.CreateBuilder<TestRunDsseSignature>();
if (_signer is not null)
{
var signResult = await _signer.SignAsync(payload, ct);
signatures.Add(new TestRunDsseSignature
{
KeyId = signResult.KeyId,
Sig = signResult.Signature,
Algorithm = signResult.Algorithm
});
}
if (signatures.Count == 0)
{
return null; // Return null if no signatures
}
return new TestRunDsseEnvelope
{
PayloadType = DssePayloadType,
Payload = Convert.ToBase64String(payload),
Signatures = signatures.ToImmutable()
};
}
private static byte[] SerializeToCanonicalJson<T>(T value)
{
// Use standard JSON serialization with sorted keys
// For full RFC 8785 compliance, use a dedicated canonicalizer
return JsonSerializer.SerializeToUtf8Bytes(value, JsonOptions);
}
private static string ComputeSha256Digest(byte[] data)
{
var hash = SHA256.HashData(data);
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
}
}
/// <summary>
/// Interface for test run attestation generation.
/// </summary>
public interface ITestRunAttestationGenerator
{
/// <summary>
/// Generates a test run attestation.
/// </summary>
Task<TestRunAttestation> GenerateAsync(
RunManifest manifest,
ImmutableArray<TestRunOutput> outputs,
TestRunEvidence evidence,
CancellationToken ct = default);
/// <summary>
/// Generates attestations for multiple test runs.
/// </summary>
Task<ImmutableArray<TestRunAttestation>> GenerateBatchAsync(
IEnumerable<(RunManifest Manifest, ImmutableArray<TestRunOutput> Outputs, TestRunEvidence Evidence)> runs,
CancellationToken ct = default);
/// <summary>
/// Verifies a test run attestation.
/// </summary>
Task<TestRunAttestationVerificationResult> VerifyAsync(
TestRunAttestation attestation,
ITestRunAttestationVerifier? verifier = null,
CancellationToken ct = default);
}
/// <summary>
/// Interface for signing test run attestations.
/// </summary>
public interface ITestRunAttestationSigner
{
/// <summary>
/// Signs the attestation payload.
/// </summary>
Task<TestRunSignatureResult> SignAsync(byte[] payload, CancellationToken ct = default);
}
/// <summary>
/// Interface for verifying test run attestation signatures.
/// </summary>
public interface ITestRunAttestationVerifier
{
/// <summary>
/// Verifies the attestation signature.
/// </summary>
Task<TestRunSignatureVerification> VerifyAsync(
TestRunDsseEnvelope envelope,
byte[] payload,
CancellationToken ct = default);
}
/// <summary>
/// Result of signing operation.
/// </summary>
public sealed record TestRunSignatureResult
{
public required string KeyId { get; init; }
public required string Signature { get; init; }
public string? Algorithm { get; init; }
}
/// <summary>
/// Result of signature verification.
/// </summary>
public sealed record TestRunSignatureVerification
{
public bool Verified { get; init; }
public string? Error { get; init; }
public string? KeyId { get; init; }
}
/// <summary>
/// Interface for generating attestation IDs.
/// </summary>
public interface IAttestationIdGenerator
{
string NewId();
}
/// <summary>
/// Default GUID-based attestation ID generator.
/// </summary>
public sealed class GuidAttestationIdGenerator : IAttestationIdGenerator
{
public string NewId() => Guid.NewGuid().ToString("N");
}

View File

@@ -0,0 +1,321 @@
// -----------------------------------------------------------------------------
// TestRunAttestationModels.cs
// Sprint: Testing Enhancement Advisory - Phase 1.3
// Description: Models for test run attestations linking outputs to inputs (SBOMs, VEX)
// -----------------------------------------------------------------------------
using System.Collections.Immutable;
using System.Text.Json.Serialization;
namespace StellaOps.Testing.Manifests.Attestation;
/// <summary>
/// Test run attestation linking test outputs to their inputs (SBOMs, VEX documents).
/// </summary>
public sealed record TestRunAttestation
{
/// <summary>
/// Unique identifier for this attestation.
/// </summary>
public required string AttestationId { get; init; }
/// <summary>
/// Reference to the run manifest this attestation covers.
/// </summary>
public required string RunId { get; init; }
/// <summary>
/// When the attestation was generated (UTC).
/// </summary>
public required DateTimeOffset CreatedAt { get; init; }
/// <summary>
/// The in-toto statement payload.
/// </summary>
public required TestRunInTotoStatement Statement { get; init; }
/// <summary>
/// SHA-256 digest of the canonical JSON statement.
/// </summary>
public required string StatementDigest { get; init; }
/// <summary>
/// DSSE envelope containing signed statement.
/// </summary>
public TestRunDsseEnvelope? Envelope { get; init; }
/// <summary>
/// Whether all test outputs were successfully produced.
/// </summary>
public bool Success { get; init; }
/// <summary>
/// Number of test outputs covered by this attestation.
/// </summary>
public int OutputCount { get; init; }
}
/// <summary>
/// In-toto v1 statement for test run attestation.
/// </summary>
public sealed record TestRunInTotoStatement
{
public const string StatementTypeUri = "https://in-toto.io/Statement/v1";
public const string PredicateTypeUri = "https://stellaops.io/attestation/test-run/v1";
[JsonPropertyName("_type")]
public string Type { get; init; } = StatementTypeUri;
[JsonPropertyName("subject")]
public required ImmutableArray<TestRunSubject> Subject { get; init; }
[JsonPropertyName("predicateType")]
public string PredicateType { get; init; } = PredicateTypeUri;
[JsonPropertyName("predicate")]
public required TestRunPredicate Predicate { get; init; }
}
/// <summary>
/// Subject entry in the in-toto statement (test output artifacts).
/// </summary>
public sealed record TestRunSubject
{
[JsonPropertyName("name")]
public required string Name { get; init; }
[JsonPropertyName("digest")]
public required ImmutableDictionary<string, string> Digest { get; init; }
}
/// <summary>
/// Predicate payload for test run attestation.
/// Links test outputs to input artifacts (SBOMs, VEX documents, feeds).
/// </summary>
public sealed record TestRunPredicate
{
/// <summary>
/// Unique run identifier from the manifest.
/// </summary>
[JsonPropertyName("runId")]
public required string RunId { get; init; }
/// <summary>
/// Schema version of the manifest.
/// </summary>
[JsonPropertyName("manifestSchemaVersion")]
public required string ManifestSchemaVersion { get; init; }
/// <summary>
/// Digests of SBOM inputs used in this test run.
/// </summary>
[JsonPropertyName("sbomDigests")]
public required ImmutableArray<string> SbomDigests { get; init; }
/// <summary>
/// Digests of VEX documents used in this test run.
/// </summary>
[JsonPropertyName("vexDigests")]
public ImmutableArray<string> VexDigests { get; init; } = [];
/// <summary>
/// Feed snapshot information used for vulnerability matching.
/// </summary>
[JsonPropertyName("feedSnapshot")]
public required TestRunFeedSnapshotRef FeedSnapshot { get; init; }
/// <summary>
/// Policy configuration digest.
/// </summary>
[JsonPropertyName("policyDigest")]
public required string PolicyDigest { get; init; }
/// <summary>
/// Tool versions used in the test run.
/// </summary>
[JsonPropertyName("toolVersions")]
public required TestRunToolVersionsRef ToolVersions { get; init; }
/// <summary>
/// UTC timestamp when the test run was executed.
/// </summary>
[JsonPropertyName("executedAt")]
public required DateTimeOffset ExecutedAt { get; init; }
/// <summary>
/// UTC timestamp when the test run was initiated.
/// </summary>
[JsonPropertyName("initiatedAt")]
public required DateTimeOffset InitiatedAt { get; init; }
/// <summary>
/// Duration of the test run in milliseconds.
/// </summary>
[JsonPropertyName("durationMs")]
public long DurationMs { get; init; }
/// <summary>
/// Number of test cases executed.
/// </summary>
[JsonPropertyName("testCount")]
public int TestCount { get; init; }
/// <summary>
/// Number of test cases that passed.
/// </summary>
[JsonPropertyName("passedCount")]
public int PassedCount { get; init; }
/// <summary>
/// Number of test cases that failed.
/// </summary>
[JsonPropertyName("failedCount")]
public int FailedCount { get; init; }
/// <summary>
/// Number of test cases that were skipped.
/// </summary>
[JsonPropertyName("skippedCount")]
public int SkippedCount { get; init; }
/// <summary>
/// Whether the test run completed successfully (all tests passed).
/// </summary>
[JsonPropertyName("success")]
public bool Success { get; init; }
/// <summary>
/// Determinism verification status.
/// </summary>
[JsonPropertyName("determinismVerified")]
public bool DeterminismVerified { get; init; }
/// <summary>
/// Additional metadata about the test run.
/// </summary>
[JsonPropertyName("metadata")]
public ImmutableDictionary<string, string>? Metadata { get; init; }
}
/// <summary>
/// Reference to feed snapshot used in test run.
/// </summary>
public sealed record TestRunFeedSnapshotRef
{
[JsonPropertyName("feedId")]
public required string FeedId { get; init; }
[JsonPropertyName("version")]
public required string Version { get; init; }
[JsonPropertyName("digest")]
public required string Digest { get; init; }
[JsonPropertyName("snapshotAt")]
public required DateTimeOffset SnapshotAt { get; init; }
}
/// <summary>
/// Reference to tool versions used in test run.
/// </summary>
public sealed record TestRunToolVersionsRef
{
[JsonPropertyName("scannerVersion")]
public required string ScannerVersion { get; init; }
[JsonPropertyName("sbomGeneratorVersion")]
public required string SbomGeneratorVersion { get; init; }
[JsonPropertyName("reachabilityEngineVersion")]
public required string ReachabilityEngineVersion { get; init; }
[JsonPropertyName("attestorVersion")]
public required string AttestorVersion { get; init; }
}
/// <summary>
/// DSSE envelope for test run attestation.
/// </summary>
public sealed record TestRunDsseEnvelope
{
[JsonPropertyName("payloadType")]
public required string PayloadType { get; init; }
[JsonPropertyName("payload")]
public required string Payload { get; init; }
[JsonPropertyName("signatures")]
public required ImmutableArray<TestRunDsseSignature> Signatures { get; init; }
}
/// <summary>
/// DSSE signature entry for test run attestation.
/// </summary>
public sealed record TestRunDsseSignature
{
[JsonPropertyName("keyid")]
public required string KeyId { get; init; }
[JsonPropertyName("sig")]
public required string Sig { get; init; }
[JsonPropertyName("algorithm")]
public string? Algorithm { get; init; }
}
/// <summary>
/// Test output artifact with digest.
/// </summary>
public sealed record TestRunOutput
{
/// <summary>
/// Name/identifier of the output artifact.
/// </summary>
public required string Name { get; init; }
/// <summary>
/// SHA-256 digest of the output artifact.
/// </summary>
public required string Digest { get; init; }
/// <summary>
/// Media type of the output artifact.
/// </summary>
public string? MediaType { get; init; }
/// <summary>
/// Size in bytes of the output artifact.
/// </summary>
public long? Size { get; init; }
}
/// <summary>
/// Result of test run attestation verification.
/// </summary>
public sealed record TestRunAttestationVerificationResult
{
/// <summary>
/// Whether the attestation is valid.
/// </summary>
public bool IsValid { get; init; }
/// <summary>
/// Validation errors if any.
/// </summary>
public ImmutableArray<string> Errors { get; init; } = [];
/// <summary>
/// Whether the signature was successfully verified.
/// </summary>
public bool SignatureVerified { get; init; }
/// <summary>
/// Key ID used for verification.
/// </summary>
public string? VerifiedKeyId { get; init; }
/// <summary>
/// When the verification was performed.
/// </summary>
public DateTimeOffset VerifiedAt { get; init; }
}

View File

@@ -0,0 +1,222 @@
// -----------------------------------------------------------------------------
// TestRunEvidence.cs
// Sprint: Testing Enhancement Advisory - Phase 1.3
// Description: Evidence collected during a test run for attestation generation
// -----------------------------------------------------------------------------
using System.Collections.Immutable;
namespace StellaOps.Testing.Manifests.Attestation;
/// <summary>
/// Evidence collected during a test run for attestation generation.
/// Captures test execution statistics and inputs used.
/// </summary>
public sealed record TestRunEvidence
{
/// <summary>
/// Total number of test cases executed.
/// </summary>
public required int TestCount { get; init; }
/// <summary>
/// Number of test cases that passed.
/// </summary>
public required int PassedCount { get; init; }
/// <summary>
/// Number of test cases that failed.
/// </summary>
public required int FailedCount { get; init; }
/// <summary>
/// Number of test cases that were skipped.
/// </summary>
public int SkippedCount { get; init; }
/// <summary>
/// Whether the test run completed successfully (all tests passed).
/// </summary>
public bool Success => FailedCount == 0;
/// <summary>
/// Duration of the test run in milliseconds.
/// </summary>
public long DurationMs { get; init; }
/// <summary>
/// Whether determinism was verified (multiple runs produced identical results).
/// </summary>
public bool DeterminismVerified { get; init; }
/// <summary>
/// Digests of VEX documents used in this test run.
/// </summary>
public ImmutableArray<string> VexDigests { get; init; } = [];
/// <summary>
/// Additional metadata about the test run.
/// </summary>
public ImmutableDictionary<string, string>? Metadata { get; init; }
/// <summary>
/// Test framework used (e.g., "xunit", "nunit").
/// </summary>
public string? TestFramework { get; init; }
/// <summary>
/// Test result file paths (e.g., TRX files).
/// </summary>
public ImmutableArray<string> ResultFiles { get; init; } = [];
/// <summary>
/// Categories/traits of tests that were executed.
/// </summary>
public ImmutableArray<string> ExecutedCategories { get; init; } = [];
/// <summary>
/// Git commit SHA at time of test run.
/// </summary>
public string? GitCommitSha { get; init; }
/// <summary>
/// Git branch name at time of test run.
/// </summary>
public string? GitBranch { get; init; }
/// <summary>
/// CI build number or run ID.
/// </summary>
public string? CiBuildId { get; init; }
/// <summary>
/// CI workflow name.
/// </summary>
public string? CiWorkflow { get; init; }
}
/// <summary>
/// Builder for creating TestRunEvidence instances.
/// </summary>
public sealed class TestRunEvidenceBuilder
{
private int _testCount;
private int _passedCount;
private int _failedCount;
private int _skippedCount;
private long _durationMs;
private bool _determinismVerified;
private ImmutableArray<string>.Builder _vexDigests = ImmutableArray.CreateBuilder<string>();
private ImmutableDictionary<string, string>.Builder _metadata = ImmutableDictionary.CreateBuilder<string, string>();
private string? _testFramework;
private ImmutableArray<string>.Builder _resultFiles = ImmutableArray.CreateBuilder<string>();
private ImmutableArray<string>.Builder _executedCategories = ImmutableArray.CreateBuilder<string>();
private string? _gitCommitSha;
private string? _gitBranch;
private string? _ciBuildId;
private string? _ciWorkflow;
public TestRunEvidenceBuilder WithTestCount(int total, int passed, int failed, int skipped = 0)
{
_testCount = total;
_passedCount = passed;
_failedCount = failed;
_skippedCount = skipped;
return this;
}
public TestRunEvidenceBuilder WithDuration(long durationMs)
{
_durationMs = durationMs;
return this;
}
public TestRunEvidenceBuilder WithDuration(TimeSpan duration)
{
_durationMs = (long)duration.TotalMilliseconds;
return this;
}
public TestRunEvidenceBuilder WithDeterminismVerified(bool verified)
{
_determinismVerified = verified;
return this;
}
public TestRunEvidenceBuilder AddVexDigest(string digest)
{
_vexDigests.Add(digest);
return this;
}
public TestRunEvidenceBuilder AddVexDigests(IEnumerable<string> digests)
{
_vexDigests.AddRange(digests);
return this;
}
public TestRunEvidenceBuilder AddMetadata(string key, string value)
{
_metadata[key] = value;
return this;
}
public TestRunEvidenceBuilder WithTestFramework(string framework)
{
_testFramework = framework;
return this;
}
public TestRunEvidenceBuilder AddResultFile(string path)
{
_resultFiles.Add(path);
return this;
}
public TestRunEvidenceBuilder AddExecutedCategory(string category)
{
_executedCategories.Add(category);
return this;
}
public TestRunEvidenceBuilder WithGitInfo(string? commitSha, string? branch)
{
_gitCommitSha = commitSha;
_gitBranch = branch;
return this;
}
public TestRunEvidenceBuilder WithCiInfo(string? buildId, string? workflow)
{
_ciBuildId = buildId;
_ciWorkflow = workflow;
return this;
}
public TestRunEvidence Build()
{
return new TestRunEvidence
{
TestCount = _testCount,
PassedCount = _passedCount,
FailedCount = _failedCount,
SkippedCount = _skippedCount,
DurationMs = _durationMs,
DeterminismVerified = _determinismVerified,
VexDigests = _vexDigests.ToImmutable(),
Metadata = _metadata.Count > 0 ? _metadata.ToImmutable() : null,
TestFramework = _testFramework,
ResultFiles = _resultFiles.ToImmutable(),
ExecutedCategories = _executedCategories.ToImmutable(),
GitCommitSha = _gitCommitSha,
GitBranch = _gitBranch,
CiBuildId = _ciBuildId,
CiWorkflow = _ciWorkflow
};
}
/// <summary>
/// Creates a new builder instance.
/// </summary>
public static TestRunEvidenceBuilder Create() => new();
}

View File

@@ -1,4 +1,5 @@
using System.Collections.Immutable;
using StellaOps.Testing.Manifests.Attestation;
namespace StellaOps.Testing.Manifests.Models;
@@ -72,6 +73,18 @@ public sealed record RunManifest
/// SHA-256 hash of this manifest (excluding this field).
/// </summary>
public string? ManifestDigest { get; init; }
/// <summary>
/// Optional attestation linking test outputs to this manifest's inputs.
/// Generated after test run completion to provide supply-chain linkage.
/// </summary>
public TestRunAttestation? Attestation { get; init; }
/// <summary>
/// Output artifacts produced by this test run.
/// Used as subjects in the attestation.
/// </summary>
public ImmutableArray<TestRunOutput> Outputs { get; init; } = [];
}
/// <summary>