This commit is contained in:
master
2026-01-08 20:48:20 +02:00
7 changed files with 1504 additions and 9 deletions

View File

@@ -73,7 +73,7 @@
## Rebaseline Restart (2026-01-08)
- Tracker resequenced to current 850 csproj inventory; audits restart linearly from DevOps services.
- New findings are recorded under "Findings (Rebaseline 2026-01-08 restart)" until the pass completes.
- Revalidated AUDIT-0001 to AUDIT-0103 (SimCryptoService, SimCryptoSmoke, CryptoProLinuxApi, NuGet prime v10/v9, SDK templates, Excititor connector template, Router doc samples + tests, Determinism analyzers/tests, AuditPack tests, Auth.Security tests, Canonicalization tests, Configuration tests, Cryptography.Kms tests, OfflineVerification plugin tests, Cryptography tests, DeltaVerdict tests, Eventing tests, Evidence.Persistence tests, Evidence tests, HybridLogicalClock tests, Infrastructure.Postgres tests, Metrics tests, Microservice.AspNetCore tests, Plugin tests, Provcache tests, Provenance tests, ReachGraph tests, Replay.Core tests, Replay tests, Signals tests, Spdx3 tests, Testing.Determinism tests, Testing.Manifests tests, TestKit tests, VersionComparison tests, Audit.ReplayToken, AuditPack, Auth.Security, Canonical.Json tests, Canonical.Json, Canonicalization, Configuration, Cryptography.DependencyInjection, Cryptography.Kms, Cryptography.Plugin.BouncyCastle, Cryptography.Plugin.CryptoPro, GostCryptography third-party library/tests, Cryptography.Plugin.EIDAS.Tests, Cryptography.Plugin.EIDAS, Cryptography.Plugin.OfflineVerification, Cryptography.Plugin.OpenSslGost, Cryptography.Plugin.Pkcs11Gost, Cryptography.Plugin.PqSoft, Cryptography.Plugin.SimRemote, Cryptography.Plugin.SmRemote.Tests, Cryptography.Plugin.SmRemote, Cryptography.Plugin.SmSoft.Tests, Cryptography.Plugin.SmSoft, Cryptography.Plugin.WineCsp, Cryptography.PluginLoader.Tests, Cryptography.PluginLoader, Cryptography.Providers.OfflineVerification, Cryptography.Tests (libraries), Cryptography (library), DeltaVerdict, DependencyInjection, Determinism.Abstractions, DistroIntel, Eventing, Evidence.Bundle, Evidence.Core.Tests, Evidence.Core, Evidence.Persistence, Evidence, Facet.Tests, Facet, HybridLogicalClock Benchmarks, HybridLogicalClock Tests, HybridLogicalClock, Infrastructure.EfCore, Infrastructure.Postgres, Ingestion.Telemetry, StellaOps.Interop, IssuerDirectory.Client, StellaOps.Metrics, Orchestrator.Schemas, StellaOps.Plugin, StellaOps.Policy.Tools, PolicyAuthoritySignals.Contracts, Provcache, Provcache.Api, Provcache.Postgres, Provcache.Valkey, Provenance, ReachGraph.Cache).
- Revalidated AUDIT-0001 to AUDIT-0104 (SimCryptoService, SimCryptoSmoke, CryptoProLinuxApi, NuGet prime v10/v9, SDK templates, Excititor connector template, Router doc samples + tests, Determinism analyzers/tests, AuditPack tests, Auth.Security tests, Canonicalization tests, Configuration tests, Cryptography.Kms tests, OfflineVerification plugin tests, Cryptography tests, DeltaVerdict tests, Eventing tests, Evidence.Persistence tests, Evidence tests, HybridLogicalClock tests, Infrastructure.Postgres tests, Metrics tests, Microservice.AspNetCore tests, Plugin tests, Provcache tests, Provenance tests, ReachGraph tests, Replay.Core tests, Replay tests, Signals tests, Spdx3 tests, Testing.Determinism tests, Testing.Manifests tests, TestKit tests, VersionComparison tests, Audit.ReplayToken, AuditPack, Auth.Security, Canonical.Json tests, Canonical.Json, Canonicalization, Configuration, Cryptography.DependencyInjection, Cryptography.Kms, Cryptography.Plugin.BouncyCastle, Cryptography.Plugin.CryptoPro, GostCryptography third-party library/tests, Cryptography.Plugin.EIDAS.Tests, Cryptography.Plugin.EIDAS, Cryptography.Plugin.OfflineVerification, Cryptography.Plugin.OpenSslGost, Cryptography.Plugin.Pkcs11Gost, Cryptography.Plugin.PqSoft, Cryptography.Plugin.SimRemote, Cryptography.Plugin.SmRemote.Tests, Cryptography.Plugin.SmRemote, Cryptography.Plugin.SmSoft.Tests, Cryptography.Plugin.SmSoft, Cryptography.Plugin.WineCsp, Cryptography.PluginLoader.Tests, Cryptography.PluginLoader, Cryptography.Providers.OfflineVerification, Cryptography.Tests (libraries), Cryptography (library), DeltaVerdict, DependencyInjection, Determinism.Abstractions, DistroIntel, Eventing, Evidence.Bundle, Evidence.Core.Tests, Evidence.Core, Evidence.Persistence, Evidence, Facet.Tests, Facet, HybridLogicalClock Benchmarks, HybridLogicalClock Tests, HybridLogicalClock, Infrastructure.EfCore, Infrastructure.Postgres, Ingestion.Telemetry, StellaOps.Interop, IssuerDirectory.Client, StellaOps.Metrics, Orchestrator.Schemas, StellaOps.Plugin, StellaOps.Policy.Tools, PolicyAuthoritySignals.Contracts, Provcache, Provcache.Api, Provcache.Postgres, Provcache.Valkey, Provenance, ReachGraph.Cache, ReachGraph.Persistence).
## Findings (Rebaseline 2026-01-08 restart)
### devops/services/crypto/sim-crypto-service/SimCryptoService.csproj
- MAINT: Shared ECDsa instance is reused across requests; ECDsa is not thread-safe and can race under concurrency. `devops/services/crypto/sim-crypto-service/Program.cs`
@@ -791,6 +791,19 @@
- TEST: No tests cover in-memory repository ordering/filtering beyond a single continuation token or empty-store behavior. `src/Attestor/__Tests/StellaOps.Attestor.Infrastructure.Tests/InMemoryAttestorEntryRepositoryTests.cs` `src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Storage/InMemoryAttestorEntryRepository.cs`
- TEST: No tests validate DefaultDsseCanonicalizer behavior for empty signatures or missing payload fields. `src/Attestor/__Tests/StellaOps.Attestor.Infrastructure.Tests/DefaultDsseCanonicalizerTests.cs` `src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Submission/DefaultDsseCanonicalizer.cs`
- Disposition: revalidated 2026-01-07 (test project; apply waived); coverage extended 2026-01-08 for AUDIT-0055-A.
- MAINT: PostgresRekorSubmissionQueue generates ids with Guid.NewGuid; inject IGuidGenerator for deterministic IDs and testability. `src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Queue/PostgresRekorSubmissionQueue.cs`
- MAINT: PostgresRekorSubmissionQueue computes wait time using GetDateTime on created_at, which drops offset and can skew metrics; prefer DateTimeOffset via GetFieldValue<DateTimeOffset>. `src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Queue/PostgresRekorSubmissionQueue.cs`
- QUALITY: HttpRekorClient parses checkpoint timestamps with DateTimeOffset.TryParse without InvariantCulture, making parsing locale-dependent. `src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Rekor/HttpRekorClient.cs`
- SECURITY: HttpRekorClient VerifyInclusionAsync never validates checkpoint signatures and always reports checkpointSignatureValid=false; ensure downstream treats checkpoint as unverified or implement signature validation. `src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Rekor/HttpRekorClient.cs`
- MAINT: Rekor backend construction logic is duplicated between verification and retry worker; centralize to avoid drift. `src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Verification/AttestorVerificationService.cs` `src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Workers/RekorRetryWorker.cs`
- TEST: Infrastructure tests exist but do not cover Rekor queue persistence/backoff, archive store metadata serialization, or submission/verification flows. `src/Attestor/__Tests/StellaOps.Attestor.Infrastructure.Tests`
- Disposition: revalidated 2026-01-06; apply reopened for remaining gaps.
### src/Attestor/__Tests/StellaOps.Attestor.Infrastructure.Tests/StellaOps.Attestor.Infrastructure.Tests.csproj
- MAINT: No new issues on revalidation; tests use fixed timestamps and deterministic inputs. `src/Attestor/__Tests/StellaOps.Attestor.Infrastructure.Tests/DefaultDsseCanonicalizerTests.cs` `src/Attestor/__Tests/StellaOps.Attestor.Infrastructure.Tests/InMemoryAttestorEntryRepositoryTests.cs`
- TEST: Coverage is limited to DSSE signature ordering, missing Rekor log index failure, and continuation-token paging; no tests cover Rekor submission success/conflict, proof parsing, or inclusion verification success/failure paths. `src/Attestor/__Tests/StellaOps.Attestor.Infrastructure.Tests/HttpRekorClientTests.cs` `src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Rekor/HttpRekorClient.cs`
- TEST: No tests cover in-memory repository ordering/filtering beyond a single continuation token or empty-store behavior. `src/Attestor/__Tests/StellaOps.Attestor.Infrastructure.Tests/InMemoryAttestorEntryRepositoryTests.cs` `src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Storage/InMemoryAttestorEntryRepository.cs`
- TEST: No tests validate DefaultDsseCanonicalizer behavior for empty signatures or missing payload fields. `src/Attestor/__Tests/StellaOps.Attestor.Infrastructure.Tests/DefaultDsseCanonicalizerTests.cs` `src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Submission/DefaultDsseCanonicalizer.cs`
- Disposition: revalidated 2026-01-07 (test project; apply waived).
### src/Attestor/__Libraries/StellaOps.Attestor.Oci/StellaOps.Attestor.Oci.csproj
- QUALITY: OrasAttestationAttacher assumes imageRef.Digest is populated; when tag-only references are parsed, Digest is empty and no ResolveTagAsync call occurs, so attach/list/fetch/remove can target an empty digest. `src/Attestor/__Libraries/StellaOps.Attestor.Oci/Services/OrasAttestationAttacher.cs`
- QUALITY: ListAsync parses created timestamps with DateTimeOffset.TryParse without InvariantCulture, making ordering locale-dependent. `src/Attestor/__Libraries/StellaOps.Attestor.Oci/Services/OrasAttestationAttacher.cs`
@@ -4255,6 +4268,7 @@
- MAINT: CreateGlobRegex uses a control-character placeholder for "**", violating ASCII-only rules and making the regex brittle. `src/ReachGraph/StellaOps.ReachGraph.WebService/Services/ReachGraphSliceService.cs`
- Proposed changes (pending approval): enforce authn/z with tenant-aware policies, scope cache by tenant, inject TimeProvider, add request validation and bounds, and replace the glob placeholder with ASCII plus tests.
- Disposition: pending implementation (non-test project; revalidated 2026-01-08; apply recommendations remain open).
- Disposition: pending implementation (non-test project; revalidated 2026-01-07; apply recommendations remain open).
### src/ReachGraph/__Tests/StellaOps.ReachGraph.WebService.Tests/StellaOps.ReachGraph.WebService.Tests.csproj
- TEST: Coverage exists for upsert idempotency, get by digest/not found, slice by CVE/package, replay match, list by artifact, and delete. `src/ReachGraph/__Tests/StellaOps.ReachGraph.WebService.Tests/ReachGraphApiIntegrationTests.cs`
- TEST: No coverage for entrypoint/file slices, invalid direction/depth, missing tenant header, or replay mismatch paths. `src/ReachGraph/__Tests/StellaOps.ReachGraph.WebService.Tests/ReachGraphApiIntegrationTests.cs`
@@ -4341,6 +4355,7 @@
- TEST: No tests cover replay token endpoints, tenant header enforcement, or verdict replay endpoints. `src/Replay/StellaOps.Replay.WebService`
- Proposed changes (pending approval): enforce scopes from config, validate tenant against claims, clamp expiration, require authorization and path allowlisting for verdict replay, and add endpoint tests.
- Disposition: pending implementation (non-test project; revalidated 2026-01-08; apply recommendations remain open).
- Disposition: pending implementation (non-test project; revalidated 2026-01-07; apply recommendations remain open).
### src/__Libraries/StellaOps.Resolver/StellaOps.Resolver.csproj
- MAINT: DeterministicResolver.Run uses DateTimeOffset.UtcNow; should use injected TimeProvider or require explicit resolvedAt for deterministic runs. `src/__Libraries/StellaOps.Resolver/DeterministicResolver.cs`
- Proposed changes (pending approval): inject TimeProvider and remove the DateTimeOffset.UtcNow default.

View File

@@ -0,0 +1,284 @@
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
using System.Collections.Immutable;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using StellaOps.OpsMemory.Models;
using StellaOps.OpsMemory.Playbook;
using StellaOps.OpsMemory.Similarity;
using StellaOps.OpsMemory.Storage;
using Xunit;
namespace StellaOps.OpsMemory.Tests.Unit;
/// <summary>
/// Unit tests for PlaybookSuggestionService.
/// Sprint: SPRINT_20260107_006_004 Task OM-010
/// </summary>
[Trait("Category", "Unit")]
public class PlaybookSuggestionServiceTests
{
private readonly Mock<IOpsMemoryStore> _storeMock;
private readonly SimilarityVectorGenerator _vectorGenerator;
private readonly PlaybookSuggestionService _service;
public PlaybookSuggestionServiceTests()
{
_storeMock = new Mock<IOpsMemoryStore>();
_vectorGenerator = new SimilarityVectorGenerator();
_service = new PlaybookSuggestionService(
_storeMock.Object,
_vectorGenerator,
NullLogger<PlaybookSuggestionService>.Instance);
}
[Fact]
public async Task GetSuggestionsAsync_WithNoSimilarRecords_ReturnsEmptySuggestions()
{
// Arrange
_storeMock.Setup(s => s.FindSimilarAsync(It.IsAny<SimilarityQuery>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(Array.Empty<SimilarityMatch>());
var request = new PlaybookSuggestionRequest
{
TenantId = "tenant-1",
Situation = new SituationContext
{
CveId = "CVE-2023-12345",
Severity = "high"
}
};
// Act
var result = await _service.GetSuggestionsAsync(request);
// Assert
result.Suggestions.Should().BeEmpty();
result.AnalyzedRecords.Should().Be(0);
result.HasSuggestions.Should().BeFalse();
}
[Fact]
public async Task GetSuggestionsAsync_WithSimilarRecords_ReturnsSuggestions()
{
// Arrange
var pastRecord = CreatePastRecord(DecisionAction.Remediate, OutcomeStatus.Success);
var matches = new List<SimilarityMatch>
{
new()
{
Record = pastRecord,
SimilarityScore = 0.85
}
};
_storeMock.Setup(s => s.FindSimilarAsync(It.IsAny<SimilarityQuery>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(matches);
var request = new PlaybookSuggestionRequest
{
TenantId = "tenant-1",
Situation = new SituationContext
{
CveId = "CVE-2023-12345",
Severity = "high",
Reachability = ReachabilityStatus.Reachable
}
};
// Act
var result = await _service.GetSuggestionsAsync(request);
// Assert
result.Suggestions.Should().NotBeEmpty();
result.AnalyzedRecords.Should().Be(1);
result.HasSuggestions.Should().BeTrue();
}
[Fact]
public async Task GetSuggestionsAsync_GroupsByAction_AndRanksBySuccessRate()
{
// Arrange - multiple records with same action
var remediateSuccess1 = CreatePastRecord(DecisionAction.Remediate, OutcomeStatus.Success);
var remediateSuccess2 = CreatePastRecord(DecisionAction.Remediate, OutcomeStatus.Success);
var acceptPartial = CreatePastRecord(DecisionAction.Accept, OutcomeStatus.PartialSuccess);
var matches = new List<SimilarityMatch>
{
new() { Record = remediateSuccess1, SimilarityScore = 0.9 },
new() { Record = remediateSuccess2, SimilarityScore = 0.85 },
new() { Record = acceptPartial, SimilarityScore = 0.8 }
};
_storeMock.Setup(s => s.FindSimilarAsync(It.IsAny<SimilarityQuery>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(matches);
var request = new PlaybookSuggestionRequest
{
TenantId = "tenant-1",
Situation = new SituationContext { Severity = "high" }
};
// Act
var result = await _service.GetSuggestionsAsync(request);
// Assert
result.Suggestions.Should().NotBeEmpty();
// Remediate should rank higher due to 100% success rate
var firstSuggestion = result.Suggestions.First();
firstSuggestion.Action.Should().Be(DecisionAction.Remediate);
firstSuggestion.SuccessRate.Should().Be(1.0); // 100%
}
[Fact]
public async Task GetSuggestionsAsync_RespectsMaxSuggestionsLimit()
{
// Arrange - more actions than max suggestions
var remediate = CreatePastRecord(DecisionAction.Remediate, OutcomeStatus.Success);
var accept = CreatePastRecord(DecisionAction.Accept, OutcomeStatus.Success);
var mitigate = CreatePastRecord(DecisionAction.Mitigate, OutcomeStatus.Success);
var defer = CreatePastRecord(DecisionAction.Defer, OutcomeStatus.Success);
var matches = new List<SimilarityMatch>
{
new() { Record = remediate, SimilarityScore = 0.9 },
new() { Record = accept, SimilarityScore = 0.85 },
new() { Record = mitigate, SimilarityScore = 0.8 },
new() { Record = defer, SimilarityScore = 0.75 }
};
_storeMock.Setup(s => s.FindSimilarAsync(It.IsAny<SimilarityQuery>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(matches);
var request = new PlaybookSuggestionRequest
{
TenantId = "tenant-1",
Situation = new SituationContext { Severity = "high" },
MaxSuggestions = 2
};
// Act
var result = await _service.GetSuggestionsAsync(request);
// Assert
result.Suggestions.Should().HaveCount(2);
}
[Fact]
public async Task GetSuggestionsAsync_IncludesEvidenceRecords()
{
// Arrange
var pastRecord = CreatePastRecord(DecisionAction.Remediate, OutcomeStatus.Success);
var matches = new List<SimilarityMatch>
{
new() { Record = pastRecord, SimilarityScore = 0.9 }
};
_storeMock.Setup(s => s.FindSimilarAsync(It.IsAny<SimilarityQuery>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(matches);
var request = new PlaybookSuggestionRequest
{
TenantId = "tenant-1",
Situation = new SituationContext { Severity = "high" }
};
// Act
var result = await _service.GetSuggestionsAsync(request);
// Assert
result.Suggestions.Should().NotBeEmpty();
var suggestion = result.Suggestions.First();
suggestion.Evidence.Should().NotBeEmpty();
suggestion.Evidence.First().MemoryId.Should().Be(pastRecord.MemoryId);
}
[Fact]
public async Task GetSuggestionsAsync_CalculatesConfidence()
{
// Arrange
var pastRecord = CreatePastRecord(DecisionAction.Remediate, OutcomeStatus.Success);
var matches = new List<SimilarityMatch>
{
new() { Record = pastRecord, SimilarityScore = 0.9 }
};
_storeMock.Setup(s => s.FindSimilarAsync(It.IsAny<SimilarityQuery>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(matches);
var request = new PlaybookSuggestionRequest
{
TenantId = "tenant-1",
Situation = new SituationContext { Severity = "high" }
};
// Act
var result = await _service.GetSuggestionsAsync(request);
// Assert
result.Suggestions.Should().NotBeEmpty();
var suggestion = result.Suggestions.First();
suggestion.Confidence.Should().BeGreaterThan(0);
suggestion.Confidence.Should().BeLessOrEqualTo(1);
}
[Fact]
public async Task GetSuggestionsAsync_GeneratesRationale()
{
// Arrange
var pastRecord = CreatePastRecord(DecisionAction.Remediate, OutcomeStatus.Success);
var matches = new List<SimilarityMatch>
{
new() { Record = pastRecord, SimilarityScore = 0.9 }
};
_storeMock.Setup(s => s.FindSimilarAsync(It.IsAny<SimilarityQuery>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(matches);
var request = new PlaybookSuggestionRequest
{
TenantId = "tenant-1",
Situation = new SituationContext { Severity = "high" }
};
// Act
var result = await _service.GetSuggestionsAsync(request);
// Assert
result.Suggestions.Should().NotBeEmpty();
var suggestion = result.Suggestions.First();
suggestion.Rationale.Should().NotBeNullOrEmpty();
}
private static OpsMemoryRecord CreatePastRecord(DecisionAction action, OutcomeStatus outcome)
{
var memoryId = Guid.NewGuid().ToString("N");
return new OpsMemoryRecord
{
MemoryId = memoryId,
TenantId = "tenant-1",
RecordedAt = DateTimeOffset.UtcNow.AddDays(-7),
Situation = new SituationContext
{
CveId = "CVE-2023-44487",
Severity = "high",
Reachability = ReachabilityStatus.Reachable
},
Decision = new DecisionRecord
{
Action = action,
Rationale = "Test decision rationale",
DecidedBy = "test-user",
DecidedAt = DateTimeOffset.UtcNow.AddDays(-7)
},
Outcome = new OutcomeRecord
{
Status = outcome,
ResolutionTime = TimeSpan.FromHours(4),
RecordedBy = "test-user",
RecordedAt = DateTimeOffset.UtcNow.AddDays(-5)
}
};
}
}

View File

@@ -110,17 +110,38 @@ public sealed class WorkerRepository : RepositoryBase<SchedulerDataSource>, IWor
public async Task<WorkerEntity> UpsertAsync(WorkerEntity worker, CancellationToken cancellationToken = default)
{
const string sql = """
INSERT INTO scheduler.workers (id, tenant_id, hostname, process_id, job_types, max_concurrent_jobs, metadata)
VALUES (@id, @tenant_id, @hostname, @process_id, @job_types, @max_concurrent_jobs, @metadata::jsonb)
INSERT INTO scheduler.workers (
id,
tenant_id,
hostname,
process_id,
job_types,
max_concurrent_jobs,
current_jobs,
status,
metadata
)
VALUES (
@id,
@tenant_id,
@hostname,
@process_id,
@job_types,
@max_concurrent_jobs,
@current_jobs,
@status,
@metadata::jsonb
)
ON CONFLICT (id) DO UPDATE SET
tenant_id = EXCLUDED.tenant_id,
hostname = EXCLUDED.hostname,
process_id = EXCLUDED.process_id,
job_types = EXCLUDED.job_types,
max_concurrent_jobs = EXCLUDED.max_concurrent_jobs,
current_jobs = EXCLUDED.current_jobs,
metadata = EXCLUDED.metadata,
last_heartbeat_at = NOW(),
status = 'active'
status = EXCLUDED.status
RETURNING *
""";
@@ -133,6 +154,8 @@ public sealed class WorkerRepository : RepositoryBase<SchedulerDataSource>, IWor
AddParameter(command, "process_id", worker.ProcessId);
AddTextArrayParameter(command, "job_types", worker.JobTypes);
AddParameter(command, "max_concurrent_jobs", worker.MaxConcurrentJobs);
AddParameter(command, "current_jobs", worker.CurrentJobs);
AddParameter(command, "status", worker.Status);
AddJsonbParameter(command, "metadata", worker.Metadata);
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);

View File

@@ -12,6 +12,7 @@ namespace StellaOps.Scheduler.Persistence.Postgres.Tests;
public sealed class TriggerRepositoryTests : IAsyncLifetime
{
private readonly SchedulerPostgresFixture _fixture;
private readonly SchedulerDataSource _dataSource;
private readonly TriggerRepository _repository;
private readonly string _tenantId = Guid.NewGuid().ToString();
@@ -21,8 +22,8 @@ public sealed class TriggerRepositoryTests : IAsyncLifetime
var options = fixture.Fixture.CreateOptions();
options.SchemaName = fixture.SchemaName;
var dataSource = new SchedulerDataSource(Options.Create(options), NullLogger<SchedulerDataSource>.Instance);
_repository = new TriggerRepository(dataSource, NullLogger<TriggerRepository>.Instance);
_dataSource = new SchedulerDataSource(Options.Create(options), NullLogger<SchedulerDataSource>.Instance);
_repository = new TriggerRepository(_dataSource, NullLogger<TriggerRepository>.Instance);
}
public ValueTask InitializeAsync() => new(_fixture.TruncateAllTablesAsync());
@@ -141,6 +142,17 @@ public sealed class TriggerRepositoryTests : IAsyncLifetime
var trigger = CreateTrigger("fire-test", "* * * * *");
await _repository.CreateAsync(trigger);
var jobId = Guid.NewGuid();
var jobRepository = new JobRepository(_dataSource, NullLogger<JobRepository>.Instance);
await jobRepository.CreateAsync(new JobEntity
{
Id = jobId,
TenantId = _tenantId,
JobType = "scan",
PayloadDigest = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
IdempotencyKey = $"job-{jobId}",
CreatedAt = DateTimeOffset.UtcNow,
Payload = "{}"
});
var nextFireAt = DateTimeOffset.UtcNow.AddMinutes(1);
// Act

View File

@@ -0,0 +1,24 @@
# ReachGraph Persistence Library Charter
## Mission
- Provide PostgreSQL persistence for reachability graphs and replay logs.
## Responsibilities
- Propagate cancellation tokens and enforce query limits.
- Use deterministic serialization for stored JSON fields.
- Keep tenant scoping consistent for all queries.
## Required Reading
- docs/modules/reach-graph/architecture.md
- docs/modules/platform/architecture-overview.md
## Working Directory & Scope
- Primary: src/__Libraries/StellaOps.ReachGraph.Persistence
## Testing Expectations
- Repository tests for store/get/delete/list/find/replay flows.
- Coverage for tenant scoping, limit validation, and scope parsing.
## Working Agreement
- Update sprint status in docs/implplan/SPRINT_*.md and local TASKS.md.
- Keep outputs deterministic and ASCII-only in comments and logs.

View File

@@ -0,0 +1,10 @@
# ReachGraph Persistence Task Board
This board mirrors active sprint tasks for this module.
Source of truth: `docs/implplan/permament/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0104-M | DONE | Revalidated 2026-01-08; maintainability audit for ReachGraph.Persistence. |
| AUDIT-0104-T | DONE | Revalidated 2026-01-08; test coverage audit for ReachGraph.Persistence. |
| AUDIT-0104-A | TODO | Pending approval (revalidated 2026-01-08). |