Add unit tests for PackRunAttestation and SealedInstallEnforcer
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
release-manifest-verify / verify (push) Has been cancelled

- Implement comprehensive tests for PackRunAttestationService, covering attestation generation, verification, and event emission.
- Add tests for SealedInstallEnforcer to validate sealed install requirements and enforcement logic.
- Introduce a MonacoLoaderService stub for testing purposes to prevent Monaco workers/styles from loading during Karma runs.
This commit is contained in:
StellaOps Bot
2025-12-06 22:25:30 +02:00
parent dd0067ea0b
commit 4042fc2184
110 changed files with 20084 additions and 639 deletions

View File

@@ -9,8 +9,8 @@
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.Scheduler.Queue\StellaOps.Scheduler.Queue.csproj" />
<ProjectReference Include="..\StellaOps.Scheduler.Storage.Postgres\StellaOps.Scheduler.Storage.Postgres.csproj" />
<ProjectReference Include="..\StellaOps.Scheduler.Worker\StellaOps.Scheduler.Worker.csproj" />
<ProjectReference Include="..\__Libraries\StellaOps.Scheduler.Queue\StellaOps.Scheduler.Queue.csproj" />
<ProjectReference Include="..\__Libraries\StellaOps.Scheduler.Storage.Postgres\StellaOps.Scheduler.Storage.Postgres.csproj" />
<ProjectReference Include="..\__Libraries\StellaOps.Scheduler.Worker\StellaOps.Scheduler.Worker.csproj" />
</ItemGroup>
</Project>

View File

@@ -99,18 +99,6 @@ Global
{382FA1C0-5F5F-424A-8485-7FED0ADE9F6B}.Release|x64.Build.0 = Release|Any CPU
{382FA1C0-5F5F-424A-8485-7FED0ADE9F6B}.Release|x86.ActiveCfg = Release|Any CPU
{382FA1C0-5F5F-424A-8485-7FED0ADE9F6B}.Release|x86.Build.0 = Release|Any CPU
{33770BC5-6802-45AD-A866-10027DD360E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{33770BC5-6802-45AD-A866-10027DD360E2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{33770BC5-6802-45AD-A866-10027DD360E2}.Debug|x64.ActiveCfg = Debug|Any CPU
{33770BC5-6802-45AD-A866-10027DD360E2}.Debug|x64.Build.0 = Debug|Any CPU
{33770BC5-6802-45AD-A866-10027DD360E2}.Debug|x86.ActiveCfg = Debug|Any CPU
{33770BC5-6802-45AD-A866-10027DD360E2}.Debug|x86.Build.0 = Debug|Any CPU
{33770BC5-6802-45AD-A866-10027DD360E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{33770BC5-6802-45AD-A866-10027DD360E2}.Release|Any CPU.Build.0 = Release|Any CPU
{33770BC5-6802-45AD-A866-10027DD360E2}.Release|x64.ActiveCfg = Release|Any CPU
{33770BC5-6802-45AD-A866-10027DD360E2}.Release|x64.Build.0 = Release|Any CPU
{33770BC5-6802-45AD-A866-10027DD360E2}.Release|x86.ActiveCfg = Release|Any CPU
{33770BC5-6802-45AD-A866-10027DD360E2}.Release|x86.Build.0 = Release|Any CPU
{56209C24-3CE7-4F8E-8B8C-F052CB919DE2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{56209C24-3CE7-4F8E-8B8C-F052CB919DE2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{56209C24-3CE7-4F8E-8B8C-F052CB919DE2}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -363,18 +351,6 @@ Global
{7C22F6B7-095E-459B-BCCF-87098EA9F192}.Release|x64.Build.0 = Release|Any CPU
{7C22F6B7-095E-459B-BCCF-87098EA9F192}.Release|x86.ActiveCfg = Release|Any CPU
{7C22F6B7-095E-459B-BCCF-87098EA9F192}.Release|x86.Build.0 = Release|Any CPU
{972CEB4D-510B-4701-B4A2-F14A85F11CC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{972CEB4D-510B-4701-B4A2-F14A85F11CC7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{972CEB4D-510B-4701-B4A2-F14A85F11CC7}.Debug|x64.ActiveCfg = Debug|Any CPU
{972CEB4D-510B-4701-B4A2-F14A85F11CC7}.Debug|x64.Build.0 = Debug|Any CPU
{972CEB4D-510B-4701-B4A2-F14A85F11CC7}.Debug|x86.ActiveCfg = Debug|Any CPU
{972CEB4D-510B-4701-B4A2-F14A85F11CC7}.Debug|x86.Build.0 = Debug|Any CPU
{972CEB4D-510B-4701-B4A2-F14A85F11CC7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{972CEB4D-510B-4701-B4A2-F14A85F11CC7}.Release|Any CPU.Build.0 = Release|Any CPU
{972CEB4D-510B-4701-B4A2-F14A85F11CC7}.Release|x64.ActiveCfg = Release|Any CPU
{972CEB4D-510B-4701-B4A2-F14A85F11CC7}.Release|x64.Build.0 = Release|Any CPU
{972CEB4D-510B-4701-B4A2-F14A85F11CC7}.Release|x86.ActiveCfg = Release|Any CPU
{972CEB4D-510B-4701-B4A2-F14A85F11CC7}.Release|x86.Build.0 = Release|Any CPU
{7B4C9EAC-316E-4890-A715-7BB9C1577F96}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7B4C9EAC-316E-4890-A715-7BB9C1577F96}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7B4C9EAC-316E-4890-A715-7BB9C1577F96}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -441,7 +417,6 @@ Global
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{382FA1C0-5F5F-424A-8485-7FED0ADE9F6B} = {41F15E67-7190-CF23-3BC4-77E87134CADD}
{33770BC5-6802-45AD-A866-10027DD360E2} = {41F15E67-7190-CF23-3BC4-77E87134CADD}
{56209C24-3CE7-4F8E-8B8C-F052CB919DE2} = {41F15E67-7190-CF23-3BC4-77E87134CADD}
{167198F1-43CF-42F4-BEF2-5ABC87116A37} = {41F15E67-7190-CF23-3BC4-77E87134CADD}
{6A62C12A-8742-4D1E-AEA7-8DDC3C722AC4} = {41F15E67-7190-CF23-3BC4-77E87134CADD}
@@ -451,7 +426,6 @@ Global
{5ED2BF16-72CE-4DF1-917C-6D832427AE6F} = {56BCE1BF-7CBA-7CE8-203D-A88051F1D642}
{2F097B4B-8F38-45C3-8A42-90250E912F0C} = {56BCE1BF-7CBA-7CE8-203D-A88051F1D642}
{7C22F6B7-095E-459B-BCCF-87098EA9F192} = {56BCE1BF-7CBA-7CE8-203D-A88051F1D642}
{972CEB4D-510B-4701-B4A2-F14A85F11CC7} = {56BCE1BF-7CBA-7CE8-203D-A88051F1D642}
{7B4C9EAC-316E-4890-A715-7BB9C1577F96} = {56BCE1BF-7CBA-7CE8-203D-A88051F1D642}
{B13D1DF0-1B9E-4557-919C-0A4E0FC9A8C7} = {56BCE1BF-7CBA-7CE8-203D-A88051F1D642}
{D640DBB2-4251-44B3-B949-75FC6BF02B71} = {56BCE1BF-7CBA-7CE8-203D-A88051F1D642}

View File

@@ -1,9 +1,12 @@
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Npgsql;
using Scheduler.Backfill;
using StellaOps.Scheduler.Models;
using StellaOps.Scheduler.Storage.Postgres;
using StellaOps.Scheduler.Storage.Postgres.Repositories;
using StellaOps.Infrastructure.Postgres.Options;
var parsed = ParseArgs(args);
var options = BackfillOptions.From(parsed.PostgresConnection, parsed.BatchSize, parsed.DryRun);
@@ -91,7 +94,7 @@ internal sealed class BackfillRunner
SchemaName = "scheduler",
CommandTimeoutSeconds = 30,
AutoMigrate = false
}));
}), NullLogger<SchedulerDataSource>.Instance);
_graphJobRepository = new GraphJobRepository(_dataSource);
}
@@ -106,7 +109,7 @@ internal sealed class BackfillRunner
return;
}
await using var conn = await _dataSource.OpenConnectionAsync();
await using var conn = await _dataSource.OpenSystemConnectionAsync(CancellationToken.None);
await using var tx = await conn.BeginTransactionAsync();
// Example: seed an empty job to validate wiring

View File

@@ -1,11 +1,10 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Scheduler.Models;
using StellaOps.Scheduler.Worker.Planning;
namespace StellaOps.Scheduler.Queue;
@@ -156,9 +155,9 @@ public sealed class RunnerSegmentQueueMessage
public static readonly IReadOnlyDictionary<TKey, TValue> Instance =
new ReadOnlyDictionary<TKey, TValue>(new Dictionary<TKey, TValue>(0, EqualityComparer<TKey>.Default));
}
}
public readonly record struct SchedulerQueueEnqueueResult(string MessageId, bool Deduplicated);
}
public readonly record struct SchedulerQueueEnqueueResult(string MessageId, bool Deduplicated);
public sealed class SchedulerQueueLeaseRequest
{
@@ -215,12 +214,32 @@ public sealed class SchedulerQueueClaimOptions
MinIdleTime = minIdleTime;
}
public string ClaimantConsumer { get; }
public int BatchSize { get; }
public TimeSpan MinIdleTime { get; }
}
public string ClaimantConsumer { get; }
public int BatchSize { get; }
public TimeSpan MinIdleTime { get; }
}
/// <summary>
/// Minimal pointer to a Surface.FS manifest associated with an image digest.
/// Kept local to avoid coupling queue contracts to worker assemblies.
/// </summary>
public sealed record SurfaceManifestPointer
{
public SurfaceManifestPointer(string manifestDigest, string? tenant)
{
ManifestDigest = manifestDigest ?? throw new ArgumentNullException(nameof(manifestDigest));
Tenant = tenant;
}
[JsonPropertyName("manifestDigest")]
public string ManifestDigest { get; init; }
[JsonPropertyName("tenant")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Tenant { get; init; }
}
public enum SchedulerQueueReleaseDisposition
{

View File

@@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.Text.Json;
using Dapper;
using Npgsql;
using StellaOps.Infrastructure.Postgres;
@@ -10,12 +9,10 @@ namespace StellaOps.Scheduler.Storage.Postgres.Repositories;
public sealed class GraphJobRepository : IGraphJobRepository
{
private readonly SchedulerDataSource _dataSource;
private readonly JsonSerializerOptions _json;
public GraphJobRepository(SchedulerDataSource dataSource)
{
_dataSource = dataSource;
_json = CanonicalJsonSerializer.Options;
}
public async ValueTask InsertAsync(GraphBuildJob job, CancellationToken cancellationToken)
@@ -24,16 +21,16 @@ public sealed class GraphJobRepository : IGraphJobRepository
(id, tenant_id, type, status, payload, created_at, updated_at, correlation_id)
VALUES (@Id, @TenantId, @Type, @Status, @Payload, @CreatedAt, @UpdatedAt, @CorrelationId);";
await using var conn = await _dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var conn = await _dataSource.OpenConnectionAsync(job.TenantId, cancellationToken).ConfigureAwait(false);
await conn.ExecuteAsync(sql, new
{
job.Id,
job.TenantId,
Type = (short)GraphJobQueryType.Build,
Status = (short)job.Status,
Payload = JsonSerializer.Serialize(job, _json),
Payload = CanonicalJsonSerializer.Serialize(job),
job.CreatedAt,
UpdatedAt = job.UpdatedAt ?? job.CreatedAt,
UpdatedAt = job.CompletedAt ?? job.CreatedAt,
job.CorrelationId
});
}
@@ -44,16 +41,16 @@ public sealed class GraphJobRepository : IGraphJobRepository
(id, tenant_id, type, status, payload, created_at, updated_at, correlation_id)
VALUES (@Id, @TenantId, @Type, @Status, @Payload, @CreatedAt, @UpdatedAt, @CorrelationId);";
await using var conn = await _dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var conn = await _dataSource.OpenConnectionAsync(job.TenantId, cancellationToken).ConfigureAwait(false);
await conn.ExecuteAsync(sql, new
{
job.Id,
job.TenantId,
Type = (short)GraphJobQueryType.Overlay,
Status = (short)job.Status,
Payload = JsonSerializer.Serialize(job, _json),
Payload = CanonicalJsonSerializer.Serialize(job),
job.CreatedAt,
UpdatedAt = job.UpdatedAt ?? job.CreatedAt,
UpdatedAt = job.CompletedAt ?? job.CreatedAt,
job.CorrelationId
});
}
@@ -61,17 +58,17 @@ public sealed class GraphJobRepository : IGraphJobRepository
public async ValueTask<GraphBuildJob?> GetBuildJobAsync(string tenantId, string jobId, CancellationToken cancellationToken)
{
const string sql = "SELECT payload FROM scheduler.graph_jobs WHERE tenant_id=@TenantId AND id=@Id AND type=@Type LIMIT 1";
await using var conn = await _dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var conn = await _dataSource.OpenConnectionAsync(tenantId, cancellationToken).ConfigureAwait(false);
var payload = await conn.ExecuteScalarAsync<string?>(sql, new { TenantId = tenantId, Id = jobId, Type = (short)GraphJobQueryType.Build });
return payload is null ? null : JsonSerializer.Deserialize<GraphBuildJob>(payload, _json);
return payload is null ? null : CanonicalJsonSerializer.Deserialize<GraphBuildJob>(payload);
}
public async ValueTask<GraphOverlayJob?> GetOverlayJobAsync(string tenantId, string jobId, CancellationToken cancellationToken)
{
const string sql = "SELECT payload FROM scheduler.graph_jobs WHERE tenant_id=@TenantId AND id=@Id AND type=@Type LIMIT 1";
await using var conn = await _dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var conn = await _dataSource.OpenConnectionAsync(tenantId, cancellationToken).ConfigureAwait(false);
var payload = await conn.ExecuteScalarAsync<string?>(sql, new { TenantId = tenantId, Id = jobId, Type = (short)GraphJobQueryType.Overlay });
return payload is null ? null : JsonSerializer.Deserialize<GraphOverlayJob>(payload, _json);
return payload is null ? null : CanonicalJsonSerializer.Deserialize<GraphOverlayJob>(payload);
}
public async ValueTask<IReadOnlyCollection<GraphBuildJob>> ListBuildJobsAsync(string tenantId, GraphJobStatus? status, int limit, CancellationToken cancellationToken)
@@ -83,15 +80,15 @@ public sealed class GraphJobRepository : IGraphJobRepository
}
sql += " ORDER BY created_at DESC LIMIT @Limit";
await using var conn = await _dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var conn = await _dataSource.OpenConnectionAsync(tenantId, cancellationToken).ConfigureAwait(false);
var rows = await conn.QueryAsync<string>(sql, new
{
TenantId = tenantId,
Type = (short)GraphJobQueryType.Build,
Status = status is null ? null : (short)status,
Status = (short?)status,
Limit = limit
});
return rows.Select(r => JsonSerializer.Deserialize<GraphBuildJob>(r, _json)!).ToArray();
return rows.Select(r => CanonicalJsonSerializer.Deserialize<GraphBuildJob>(r)).ToArray();
}
public async ValueTask<IReadOnlyCollection<GraphOverlayJob>> ListOverlayJobsAsync(string tenantId, GraphJobStatus? status, int limit, CancellationToken cancellationToken)
@@ -103,15 +100,15 @@ public sealed class GraphJobRepository : IGraphJobRepository
}
sql += " ORDER BY created_at DESC LIMIT @Limit";
await using var conn = await _dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var conn = await _dataSource.OpenConnectionAsync(tenantId, cancellationToken).ConfigureAwait(false);
var rows = await conn.QueryAsync<string>(sql, new
{
TenantId = tenantId,
Type = (short)GraphJobQueryType.Overlay,
Status = status is null ? null : (short)status,
Status = (short?)status,
Limit = limit
});
return rows.Select(r => JsonSerializer.Deserialize<GraphOverlayJob>(r, _json)!).ToArray();
return rows.Select(r => CanonicalJsonSerializer.Deserialize<GraphOverlayJob>(r)).ToArray();
}
public ValueTask<IReadOnlyCollection<GraphOverlayJob>> ListOverlayJobsAsync(string tenantId, CancellationToken cancellationToken)
@@ -123,7 +120,7 @@ public sealed class GraphJobRepository : IGraphJobRepository
SET status=@NewStatus, payload=@Payload, updated_at=NOW()
WHERE tenant_id=@TenantId AND id=@Id AND status=@ExpectedStatus AND type=@Type";
await using var conn = await _dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var conn = await _dataSource.OpenConnectionAsync(job.TenantId, cancellationToken).ConfigureAwait(false);
var rows = await conn.ExecuteAsync(sql, new
{
job.TenantId,
@@ -131,7 +128,7 @@ public sealed class GraphJobRepository : IGraphJobRepository
ExpectedStatus = (short)expectedStatus,
NewStatus = (short)job.Status,
Type = (short)GraphJobQueryType.Build,
Payload = JsonSerializer.Serialize(job, _json)
Payload = CanonicalJsonSerializer.Serialize(job)
});
return rows == 1;
}
@@ -142,7 +139,7 @@ public sealed class GraphJobRepository : IGraphJobRepository
SET status=@NewStatus, payload=@Payload, updated_at=NOW()
WHERE tenant_id=@TenantId AND id=@Id AND status=@ExpectedStatus AND type=@Type";
await using var conn = await _dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var conn = await _dataSource.OpenConnectionAsync(job.TenantId, cancellationToken).ConfigureAwait(false);
var rows = await conn.ExecuteAsync(sql, new
{
job.TenantId,
@@ -150,8 +147,14 @@ public sealed class GraphJobRepository : IGraphJobRepository
ExpectedStatus = (short)expectedStatus,
NewStatus = (short)job.Status,
Type = (short)GraphJobQueryType.Overlay,
Payload = JsonSerializer.Serialize(job, _json)
Payload = CanonicalJsonSerializer.Serialize(job)
});
return rows == 1;
}
}
internal enum GraphJobQueryType : short
{
Build = 0,
Overlay = 1
}

View File

@@ -8,8 +8,11 @@
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="xunit" Version="2.6.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.6.3" />
<PackageReference Update="xunit" Version="2.9.2" />
<PackageReference Update="xunit.runner.visualstudio" Version="2.8.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>

View File

@@ -12,14 +12,14 @@
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Update="Microsoft.NET.Test.Sdk" Version="17.14.0" />
<PackageReference Include="Moq" Version="4.20.70" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<PackageReference Update="xunit" Version="2.9.2" />
<PackageReference Update="xunit.runner.visualstudio" Version="2.8.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.4">
<PackageReference Update="coverlet.collector" Version="6.0.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>