up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Airgap Sealed CI Smoke / sealed-smoke (push) Has been cancelled
Console CI / console-ci (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Airgap Sealed CI Smoke / sealed-smoke (push) Has been cancelled
Console CI / console-ci (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.ExportCenter.Core.DevPortalOffline;
|
||||
using StellaOps.ExportCenter.Infrastructure.DevPortalOffline;
|
||||
using StellaOps.ExportCenter.Worker;
|
||||
using StellaOps.ExportCenter.RiskBundles;
|
||||
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
|
||||
@@ -11,12 +13,27 @@ builder.Services.AddSingleton(TimeProvider.System);
|
||||
builder.Services.Configure<DevPortalOfflineWorkerOptions>(builder.Configuration.GetSection("DevPortalOffline"));
|
||||
builder.Services.Configure<DevPortalOfflineManifestSigningOptions>(builder.Configuration.GetSection("DevPortalOffline:Signing"));
|
||||
builder.Services.Configure<DevPortalOfflineStorageOptions>(builder.Configuration.GetSection("DevPortalOffline:Storage"));
|
||||
builder.Services.Configure<RiskBundleWorkerOptions>(builder.Configuration.GetSection("RiskBundles"));
|
||||
builder.Services.Configure<RiskBundleManifestSigningOptions>(builder.Configuration.GetSection("RiskBundles:Signing"));
|
||||
builder.Services.Configure<FileSystemRiskBundleStorageOptions>(builder.Configuration.GetSection("RiskBundles:Storage"));
|
||||
|
||||
builder.Services.AddSingleton<DevPortalOfflineBundleBuilder>();
|
||||
builder.Services.AddSingleton<IDevPortalOfflineManifestSigner, HmacDevPortalOfflineManifestSigner>();
|
||||
builder.Services.AddSingleton<IDevPortalOfflineObjectStore, FileSystemDevPortalOfflineObjectStore>();
|
||||
builder.Services.AddSingleton<DevPortalOfflineJob>();
|
||||
builder.Services.AddSingleton<RiskBundleBuilder>();
|
||||
builder.Services.AddSingleton<IRiskBundleManifestSigner>(sp =>
|
||||
{
|
||||
var signing = sp.GetRequiredService<IOptions<RiskBundleManifestSigningOptions>>().Value;
|
||||
var key = string.IsNullOrWhiteSpace(signing.Key) ? throw new InvalidOperationException("Risk bundle signing key is not configured.") : signing.Key;
|
||||
var keyId = string.IsNullOrWhiteSpace(signing.KeyId) ? "risk-bundle-hmac" : signing.KeyId!;
|
||||
return new HmacRiskBundleManifestSigner(key, keyId);
|
||||
});
|
||||
builder.Services.AddSingleton<IRiskBundleObjectStore, FileSystemRiskBundleObjectStore>();
|
||||
builder.Services.AddSingleton<RiskBundleJob>();
|
||||
|
||||
builder.Services.AddHostedService<Worker>();
|
||||
builder.Services.AddHostedService<RiskBundleWorker>();
|
||||
|
||||
var host = builder.Build();
|
||||
host.Run();
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.ExportCenter.RiskBundles;
|
||||
|
||||
namespace StellaOps.ExportCenter.Worker;
|
||||
|
||||
public sealed class RiskBundleWorker : BackgroundService
|
||||
{
|
||||
private readonly ILogger<RiskBundleWorker> _logger;
|
||||
private readonly RiskBundleJob _job;
|
||||
private readonly IOptions<RiskBundleWorkerOptions> _options;
|
||||
|
||||
public RiskBundleWorker(
|
||||
ILogger<RiskBundleWorker> logger,
|
||||
RiskBundleJob job,
|
||||
IOptions<RiskBundleWorkerOptions> options)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_job = job ?? throw new ArgumentNullException(nameof(job));
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
var opts = _options.Value ?? new RiskBundleWorkerOptions();
|
||||
|
||||
if (!opts.Enabled)
|
||||
{
|
||||
_logger.LogInformation("Risk bundle worker disabled. Idling.");
|
||||
await Task.Delay(Timeout.Infinite, stoppingToken).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var request = BuildRequest(opts);
|
||||
var outcome = await _job.ExecuteAsync(request, stoppingToken).ConfigureAwait(false);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Risk bundle built with {ProviderCount} providers. Stored bundle={BundleKey} manifest={ManifestKey} signature={SignatureKey}.",
|
||||
outcome.Manifest.Providers.Count,
|
||||
outcome.BundleStorage.StorageKey,
|
||||
outcome.ManifestStorage.StorageKey,
|
||||
outcome.ManifestSignatureStorage.StorageKey);
|
||||
}
|
||||
catch (Exception ex) when (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
_logger.LogError(ex, "Risk bundle job failed.");
|
||||
throw;
|
||||
}
|
||||
|
||||
await Task.Delay(Timeout.Infinite, stoppingToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static RiskBundleJobRequest BuildRequest(RiskBundleWorkerOptions options)
|
||||
{
|
||||
if (options.Providers is not { Count: > 0 })
|
||||
{
|
||||
throw new InvalidOperationException("Risk bundle worker requires at least one provider entry.");
|
||||
}
|
||||
|
||||
var bundleId = options.BundleId ?? Guid.NewGuid();
|
||||
var providers = options.Providers
|
||||
.Select(p => p.ToInput())
|
||||
.ToArray();
|
||||
|
||||
var build = new RiskBundleBuildRequest(
|
||||
bundleId,
|
||||
providers,
|
||||
BundleFileName: options.BundleFileName ?? "risk-bundle.tar.gz",
|
||||
BundlePrefix: options.StoragePrefix ?? "risk-bundles",
|
||||
ManifestFileName: options.ManifestFileName ?? "provider-manifest.json",
|
||||
ManifestDsseFileName: options.ManifestDsseFileName ?? "provider-manifest.dsse",
|
||||
AllowMissingOptional: options.AllowMissingOptional);
|
||||
|
||||
return new RiskBundleJobRequest(
|
||||
build,
|
||||
StoragePrefix: options.StoragePrefix ?? "risk-bundles",
|
||||
BundleFileName: options.BundleFileName ?? "risk-bundle.tar.gz");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using StellaOps.ExportCenter.RiskBundles;
|
||||
|
||||
namespace StellaOps.ExportCenter.Worker;
|
||||
|
||||
public sealed class RiskBundleWorkerOptions
|
||||
{
|
||||
public bool Enabled { get; set; } = false;
|
||||
|
||||
public Guid? BundleId { get; set; }
|
||||
|
||||
public string? StoragePrefix { get; set; } = "risk-bundles";
|
||||
|
||||
public string? BundleFileName { get; set; } = "risk-bundle.tar.gz";
|
||||
|
||||
public string? ManifestFileName { get; set; } = "provider-manifest.json";
|
||||
|
||||
public string? ManifestDsseFileName { get; set; } = "provider-manifest.dsse";
|
||||
|
||||
public bool AllowMissingOptional { get; set; } = true;
|
||||
|
||||
[MinLength(1)]
|
||||
public List<RiskBundleProviderOption> Providers { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class RiskBundleProviderOption
|
||||
{
|
||||
public string? ProviderId { get; set; }
|
||||
public string? SourcePath { get; set; }
|
||||
public string? Source { get; set; }
|
||||
public bool Optional { get; set; }
|
||||
public DateOnly? SnapshotDate { get; set; }
|
||||
|
||||
public RiskBundleProviderInput ToInput()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ProviderId))
|
||||
{
|
||||
throw new ValidationException("ProviderId is required for risk bundle provider options.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(SourcePath))
|
||||
{
|
||||
throw new ValidationException("SourcePath is required for risk bundle provider options.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Source))
|
||||
{
|
||||
throw new ValidationException("Source descriptor is required for risk bundle provider options.");
|
||||
}
|
||||
|
||||
return new RiskBundleProviderInput(
|
||||
ProviderId,
|
||||
SourcePath,
|
||||
Source,
|
||||
Optional,
|
||||
SnapshotDate);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class RiskBundleManifestSigningOptions
|
||||
{
|
||||
public string? Key { get; set; }
|
||||
public string? KeyId { get; set; }
|
||||
}
|
||||
|
||||
public sealed class RiskBundleStorageOptions
|
||||
{
|
||||
public string? RootPath { get; set; }
|
||||
}
|
||||
@@ -28,16 +28,19 @@
|
||||
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
|
||||
<ProjectReference Include="..\StellaOps.ExportCenter.Core\StellaOps.ExportCenter.Core.csproj"/>
|
||||
|
||||
|
||||
<ProjectReference Include="..\StellaOps.ExportCenter.Infrastructure\StellaOps.ExportCenter.Infrastructure.csproj"/>
|
||||
|
||||
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
</Project>
|
||||
<ItemGroup>
|
||||
|
||||
|
||||
<ProjectReference Include="..\StellaOps.ExportCenter.Core\StellaOps.ExportCenter.Core.csproj"/>
|
||||
|
||||
|
||||
<ProjectReference Include="..\StellaOps.ExportCenter.Infrastructure\StellaOps.ExportCenter.Infrastructure.csproj"/>
|
||||
|
||||
|
||||
<ProjectReference Include="..\..\StellaOps.ExportCenter.RiskBundles\StellaOps.ExportCenter.RiskBundles.csproj"/>
|
||||
|
||||
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,8 +1,26 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"RiskBundles": {
|
||||
"Enabled": false,
|
||||
"Storage": {
|
||||
"RootPath": "./out/risk-bundles-dev"
|
||||
},
|
||||
"Signing": {
|
||||
"Key": "dev-risk-bundle-key",
|
||||
"KeyId": "risk-bundle-hmac"
|
||||
},
|
||||
"Providers": [
|
||||
{
|
||||
"ProviderId": "cisa-kev",
|
||||
"SourcePath": "./inputs/kev.json",
|
||||
"Source": "CISA KEV",
|
||||
"Optional": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,35 @@
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"RiskBundles": {
|
||||
"Enabled": false,
|
||||
"StoragePrefix": "risk-bundles",
|
||||
"BundleFileName": "risk-bundle.tar.gz",
|
||||
"ManifestFileName": "provider-manifest.json",
|
||||
"ManifestDsseFileName": "provider-manifest.dsse",
|
||||
"AllowMissingOptional": true,
|
||||
"Storage": {
|
||||
"RootPath": "./out/risk-bundles"
|
||||
},
|
||||
"Signing": {
|
||||
"Key": "change-me-risk-bundle-key",
|
||||
"KeyId": "risk-bundle-hmac"
|
||||
},
|
||||
"Providers": [
|
||||
{
|
||||
"ProviderId": "cisa-kev",
|
||||
"SourcePath": "./inputs/kev.json",
|
||||
"Source": "CISA KEV",
|
||||
"Optional": false
|
||||
},
|
||||
{
|
||||
"ProviderId": "first-epss",
|
||||
"SourcePath": "./inputs/epss.csv",
|
||||
"Source": "FIRST EPSS",
|
||||
"Optional": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"DevPortalOffline": {
|
||||
"Enabled": false,
|
||||
"StoragePrefix": "devportal/offline",
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# StellaOps.Notify.Storage.Postgres — Agent Charter
|
||||
|
||||
## Mission
|
||||
Deliver PostgreSQL-backed persistence for Notify (channels, rules, templates, deliveries, digests, quiet hours, maintenance windows, escalations, inbox, incidents, audit) per `docs/db/SPECIFICATION.md` §5.5 and enable the Mongo → Postgres cutover.
|
||||
|
||||
## Required Reading
|
||||
- docs/modules/notify/architecture.md
|
||||
- docs/db/README.md
|
||||
- docs/db/SPECIFICATION.md (Notify schema §5.5)
|
||||
- docs/db/RULES.md
|
||||
- docs/db/VERIFICATION.md
|
||||
- docs/modules/platform/architecture-overview.md
|
||||
|
||||
## Working Agreement
|
||||
- Update related sprint rows in `docs/implplan/SPRINT_*.md` when starting/finishing work; keep statuses `TODO → DOING → DONE/BLOCKED`.
|
||||
- Follow deterministic/offline posture: stable ordering, UTC timestamps, idempotent migrations; use the curated NuGet cache at `local-nugets/`.
|
||||
- Keep schema/migrations aligned with `docs/db/SPECIFICATION.md`; add/extend tests under this project to cover repository contracts against PostgreSQL.
|
||||
- Mirror any contract change (schema, repository signatures, DI wiring) into the appropriate docs (`docs/db/SPECIFICATION.md`, module architecture) and note it in sprint Decisions & Risks.
|
||||
- Coordinate with `StellaOps.Notify.Engine` and channel connectors for behavioural changes; avoid cross-module edits unless the sprint explicitly allows and logs them.
|
||||
@@ -228,9 +228,9 @@ public sealed class SurfaceManifestStageExecutorTests
|
||||
Assert.Equal(3, cache.Entries.Count);
|
||||
}
|
||||
|
||||
private static ScanJobContext CreateContext()
|
||||
private static ScanJobContext CreateContext(Dictionary<string, string>? metadata = null)
|
||||
{
|
||||
var lease = new FakeJobLease();
|
||||
var lease = new FakeJobLease(metadata);
|
||||
return new ScanJobContext(lease, TimeProvider.System, DateTimeOffset.UtcNow, CancellationToken.None);
|
||||
}
|
||||
|
||||
@@ -379,6 +379,60 @@ public sealed class SurfaceManifestStageExecutorTests
|
||||
Assert.Contains(cache.Entries.Keys, key => key.Namespace == "surface.artifacts.deno.observation");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_WritesDeterminismPayloadWithPinsAndSettings()
|
||||
{
|
||||
var metrics = new ScannerWorkerMetrics();
|
||||
var publisher = new TestSurfaceManifestPublisher("tenant-a");
|
||||
var cache = new RecordingSurfaceCache();
|
||||
var environment = new TestSurfaceEnvironment("tenant-a");
|
||||
var hash = CreateCryptoHash();
|
||||
var determinism = new DeterminismContext(
|
||||
fixedClock: true,
|
||||
fixedInstantUtc: DateTimeOffset.Parse("2025-11-30T12:00:00Z"),
|
||||
rngSeed: 4242,
|
||||
filterLogs: true,
|
||||
concurrencyLimit: 2);
|
||||
|
||||
var executor = new SurfaceManifestStageExecutor(
|
||||
publisher,
|
||||
cache,
|
||||
environment,
|
||||
metrics,
|
||||
NullLogger<SurfaceManifestStageExecutor>.Instance,
|
||||
hash,
|
||||
new NullRubyPackageInventoryStore(),
|
||||
determinism);
|
||||
|
||||
var leaseMetadata = new Dictionary<string, string>
|
||||
{
|
||||
["determinism.feed"] = "sha256:feed",
|
||||
["determinism.policy"] = "sha256:policy"
|
||||
};
|
||||
|
||||
var context = CreateContext(leaseMetadata);
|
||||
PopulateAnalysis(context);
|
||||
|
||||
await executor.ExecuteAsync(context, CancellationToken.None);
|
||||
|
||||
var determinismPayload = Assert.Single(publisher.LastRequest!.Payloads, p => p.Kind == "determinism.json");
|
||||
using var document = JsonDocument.Parse(determinismPayload.Content);
|
||||
var root = document.RootElement;
|
||||
|
||||
Assert.True(root.GetProperty("fixedClock").GetBoolean());
|
||||
Assert.Equal("2025-11-30T12:00:00+00:00", root.GetProperty("fixedInstantUtc").GetString());
|
||||
Assert.Equal(4242, root.GetProperty("rngSeed").GetInt32());
|
||||
Assert.True(root.GetProperty("filterLogs").GetBoolean());
|
||||
Assert.Equal(2, root.GetProperty("concurrencyLimit").GetInt32());
|
||||
|
||||
var pins = root.GetProperty("pins");
|
||||
Assert.Equal("sha256:feed", pins.GetProperty("feed").GetString());
|
||||
Assert.Equal("sha256:policy", pins.GetProperty("policy").GetString());
|
||||
|
||||
Assert.True(root.TryGetProperty("merkleRoot", out var merkle));
|
||||
Assert.False(string.IsNullOrWhiteSpace(merkle.GetString()));
|
||||
}
|
||||
|
||||
private sealed class RecordingSurfaceCache : ISurfaceCache
|
||||
{
|
||||
private readonly Dictionary<SurfaceCacheKey, byte[]> _entries = new();
|
||||
@@ -601,11 +655,24 @@ public sealed class SurfaceManifestStageExecutorTests
|
||||
|
||||
private sealed class FakeJobLease : IScanJobLease
|
||||
{
|
||||
private readonly Dictionary<string, string> _metadata = new()
|
||||
private readonly Dictionary<string, string> _metadata;
|
||||
|
||||
public FakeJobLease(Dictionary<string, string>? extraMetadata = null)
|
||||
{
|
||||
["queue"] = "tests",
|
||||
["job.kind"] = "unit"
|
||||
};
|
||||
_metadata = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["queue"] = "tests",
|
||||
["job.kind"] = "unit"
|
||||
};
|
||||
|
||||
if (extraMetadata is not null)
|
||||
{
|
||||
foreach (var kvp in extraMetadata)
|
||||
{
|
||||
_metadata[kvp.Key] = kvp.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string JobId { get; } = Guid.NewGuid().ToString("n");
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ public sealed class TriggerRepository : RepositoryBase<SchedulerDataSource>, ITr
|
||||
metadata, created_at, updated_at, created_by
|
||||
FROM scheduler.triggers
|
||||
WHERE enabled = TRUE AND next_fire_at <= NOW()
|
||||
ORDER BY next_fire_at
|
||||
ORDER BY next_fire_at, tenant_id, id
|
||||
LIMIT @limit
|
||||
""";
|
||||
|
||||
|
||||
@@ -126,4 +126,56 @@ public sealed class DistributedLockRepositoryTests : IAsyncLifetime
|
||||
// Assert
|
||||
locks.Should().HaveCount(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TryAcquire_IsExclusiveAcrossConcurrentCallers()
|
||||
{
|
||||
// Arrange
|
||||
var lockKey = $"concurrent-lock-{Guid.NewGuid()}";
|
||||
var duration = TimeSpan.FromSeconds(5);
|
||||
|
||||
// Act
|
||||
var attempts = Enumerable.Range(0, 8)
|
||||
.Select(i => Task.Run(() => _repository.TryAcquireAsync(_tenantId, lockKey, $"worker-{i}", duration)))
|
||||
.ToArray();
|
||||
|
||||
var results = await Task.WhenAll(attempts);
|
||||
|
||||
// Assert
|
||||
var successIndexes = results
|
||||
.Select((acquired, index) => (acquired, index))
|
||||
.Where(tuple => tuple.acquired)
|
||||
.Select(tuple => tuple.index)
|
||||
.ToList();
|
||||
|
||||
successIndexes.Should().HaveCount(1);
|
||||
var winningHolder = $"worker-{successIndexes.Single()}";
|
||||
|
||||
var persisted = await _repository.GetAsync(lockKey);
|
||||
persisted.Should().NotBeNull();
|
||||
persisted!.HolderId.Should().Be(winningHolder);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TryAcquire_AllowsReacquireAfterExpiration()
|
||||
{
|
||||
// Arrange
|
||||
var lockKey = $"expiring-lock-{Guid.NewGuid()}";
|
||||
var shortDuration = TimeSpan.FromMilliseconds(500);
|
||||
|
||||
await _repository.TryAcquireAsync(_tenantId, lockKey, "worker-initial", shortDuration);
|
||||
|
||||
// Wait for expiration with a small safety buffer.
|
||||
await Task.Delay(1100);
|
||||
|
||||
// Act
|
||||
var reacquired = await _repository.TryAcquireAsync(_tenantId, lockKey, "worker-retry", TimeSpan.FromSeconds(5));
|
||||
|
||||
// Assert
|
||||
reacquired.Should().BeTrue();
|
||||
|
||||
var persisted = await _repository.GetAsync(lockKey);
|
||||
persisted.Should().NotBeNull();
|
||||
persisted!.HolderId.Should().Be("worker-retry");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,14 +185,55 @@ public sealed class TriggerRepositoryTests : IAsyncLifetime
|
||||
fetched.Should().BeNull();
|
||||
}
|
||||
|
||||
private TriggerEntity CreateTrigger(string name, string cron) => new()
|
||||
[Fact]
|
||||
public async Task GetDueTriggers_IsDeterministicForEqualNextFire()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = _tenantId,
|
||||
// Arrange
|
||||
var baseTime = DateTimeOffset.UtcNow.AddMinutes(-5);
|
||||
var dueAt = new DateTimeOffset(
|
||||
baseTime.Ticks - (baseTime.Ticks % TimeSpan.TicksPerMillisecond),
|
||||
baseTime.Offset);
|
||||
|
||||
const string tenantA = "tenant-a";
|
||||
const string tenantB = "tenant-b";
|
||||
|
||||
var triggerA = CreateTrigger("deterministic-a", "* * * * *", Guid.Parse("11111111-1111-1111-1111-111111111111"), dueAt, tenantA);
|
||||
var triggerB = CreateTrigger("deterministic-b", "* * * * *", Guid.Parse("22222222-2222-2222-2222-222222222222"), dueAt, tenantA);
|
||||
var triggerC = CreateTrigger("deterministic-c", "* * * * *", Guid.Parse("33333333-3333-3333-3333-333333333333"), dueAt, tenantB);
|
||||
|
||||
await _repository.CreateAsync(triggerB);
|
||||
await _repository.CreateAsync(triggerC);
|
||||
await _repository.CreateAsync(triggerA); // Insert out of order on purpose
|
||||
|
||||
var expectedOrder = new[]
|
||||
{
|
||||
triggerA.Id,
|
||||
triggerB.Id,
|
||||
triggerC.Id
|
||||
};
|
||||
|
||||
// Act
|
||||
var first = await _repository.GetDueTriggersAsync(limit: 10);
|
||||
var second = await _repository.GetDueTriggersAsync(limit: 10);
|
||||
|
||||
// Assert
|
||||
first.Select(t => t.Id).Should().Equal(expectedOrder);
|
||||
second.Select(t => t.Id).Should().Equal(expectedOrder);
|
||||
}
|
||||
|
||||
private TriggerEntity CreateTrigger(
|
||||
string name,
|
||||
string cron,
|
||||
Guid? id = null,
|
||||
DateTimeOffset? nextFireAt = null,
|
||||
string? tenantId = null) => new()
|
||||
{
|
||||
Id = id ?? Guid.NewGuid(),
|
||||
TenantId = tenantId ?? _tenantId,
|
||||
Name = name,
|
||||
JobType = "test-job",
|
||||
CronExpression = cron,
|
||||
Enabled = true,
|
||||
NextFireAt = DateTimeOffset.UtcNow.AddHours(1)
|
||||
NextFireAt = nextFireAt ?? DateTimeOffset.UtcNow.AddHours(1)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
| --- | --- | --- | --- | --- |
|
||||
| TASKRUN-41-001 | DONE (2025-11-30) | SPRINT_0157_0001_0001_taskrunner_i | — | Implemented run API, Mongo/file stores, approvals, provenance manifest per architecture contract. |
|
||||
| TASKRUN-AIRGAP-56-001 | DONE (2025-11-30) | SPRINT_0157_0001_0001_taskrunner_i | TASKRUN-41-001 | Sealed-mode plan validation; depends on 41-001. |
|
||||
| TASKRUN-AIRGAP-56-002 | BLOCKED (2025-11-30) | SPRINT_0157_0001_0001_taskrunner_i | TASKRUN-AIRGAP-56-001 | Bundle ingestion helpers; depends on 56-001. |
|
||||
| TASKRUN-AIRGAP-56-002 | DOING (2025-12-01) | SPRINT_0157_0001_0001_taskrunner_i | TASKRUN-AIRGAP-56-001 | Bundle ingestion helpers; depends on 56-001. |
|
||||
| TASKRUN-AIRGAP-57-001 | BLOCKED (2025-11-30) | SPRINT_0157_0001_0001_taskrunner_i | TASKRUN-AIRGAP-56-002 | Sealed install enforcement; depends on 56-002. |
|
||||
| TASKRUN-AIRGAP-58-001 | BLOCKED (2025-11-30) | SPRINT_0157_0001_0001_taskrunner_i | TASKRUN-AIRGAP-57-001 | Evidence bundles for imports; depends on 57-001. |
|
||||
| TASKRUN-42-001 | BLOCKED (2025-11-25) | SPRINT_0157_0001_0001_taskrunner_i | — | Execution engine enhancements (loops/conditionals/maxParallel), simulation mode, policy gate integration. Blocked: loop/conditional semantics and policy-gate evaluation contract not published. |
|
||||
|
||||
8
src/UI/StellaOps.UI/TASKS.md
Normal file
8
src/UI/StellaOps.UI/TASKS.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# UI Sprint Tasks (Vulnerability Triage UX)
|
||||
|
||||
| Task ID | Status | Notes | Updated (UTC) |
|
||||
| --- | --- | --- | --- |
|
||||
| UI-TRIAGE-01-001 | BLOCKED | src/UI/StellaOps.UI contains no Angular workspace; need project files restored before UI list view can be built. | 2025-11-30 |
|
||||
| TS-10-001 | BLOCKED | TypeScript interface generation blocked: workspace missing and schemas not present locally. | 2025-11-30 |
|
||||
| TS-10-002 | BLOCKED | Same as TS-10-001; waiting on schemas + workspace. | 2025-11-30 |
|
||||
| TS-10-003 | BLOCKED | Same as TS-10-001; waiting on schemas + workspace. | 2025-11-30 |
|
||||
Reference in New Issue
Block a user