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

@@ -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
}