feat: Implement PackRunApprovalDecisionService for handling approval decisions
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Added PackRunApprovalDecisionService to manage approval workflows for pack runs. - Introduced PackRunApprovalDecisionRequest and PackRunApprovalDecisionResult records. - Implemented logic to apply approval decisions and schedule run resumes based on approvals. - Updated related tests to validate approval decision functionality. test: Enhance tests for PackRunApprovalDecisionService - Created PackRunApprovalDecisionServiceTests to cover various approval scenarios. - Added in-memory stores for approvals and states to facilitate testing. - Validated behavior for applying approvals, including handling missing states. test: Add FilesystemPackRunArtifactUploaderTests for artifact uploads - Implemented tests for FilesystemPackRunArtifactUploader to ensure correct file handling. - Verified that missing files are recorded without exceptions and outputs are written as expected. fix: Update PackRunState creation to include plan reference - Modified PackRunState creation logic to include the plan in the state. chore: Refactor service registration in Program.cs - Updated service registrations in Program.cs to include new approval store and dispatcher services. - Ensured proper dependency injection for PackRunApprovalDecisionService. chore: Enhance TaskRunnerServiceOptions for approval store paths - Added ApprovalStorePath and other paths to TaskRunnerServiceOptions for better configuration. chore: Update PackRunWorkerService to handle artifact uploads - Integrated artifact uploading into PackRunWorkerService upon successful run completion. docs: Update TASKS.md for sprint progress - Documented progress on approvals workflow and related tasks in TASKS.md.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
namespace StellaOps.Scheduler.WebService.GraphJobs;
|
||||
|
||||
internal readonly record struct GraphJobUpdateResult<TJob>(bool Updated, TJob Job) where TJob : class
|
||||
public readonly record struct GraphJobUpdateResult<TJob>(bool Updated, TJob Job) where TJob : class
|
||||
{
|
||||
public static GraphJobUpdateResult<TJob> UpdatedResult(TJob job) => new(true, job);
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ internal sealed class PolicySimulationMetricsProvider : IPolicySimulationMetrics
|
||||
private readonly Histogram<double> _latencyHistogram;
|
||||
private readonly object _snapshotLock = new();
|
||||
private IReadOnlyDictionary<string, long> _latestQueueSnapshot = new Dictionary<string, long>(StringComparer.Ordinal);
|
||||
private string _latestTenantId = string.Empty;
|
||||
private bool _disposed;
|
||||
|
||||
public PolicySimulationMetricsProvider(IPolicyRunJobRepository repository, TimeProvider? timeProvider = null)
|
||||
@@ -83,9 +84,12 @@ internal sealed class PolicySimulationMetricsProvider : IPolicySimulationMetrics
|
||||
totalQueueDepth += count;
|
||||
}
|
||||
|
||||
var snapshot = new Dictionary<string, long>(queueCounts, StringComparer.Ordinal);
|
||||
|
||||
lock (_snapshotLock)
|
||||
{
|
||||
_latestQueueSnapshot = queueCounts;
|
||||
_latestQueueSnapshot = snapshot;
|
||||
_latestTenantId = tenantId;
|
||||
}
|
||||
|
||||
var sampleSize = 200;
|
||||
@@ -113,7 +117,7 @@ internal sealed class PolicySimulationMetricsProvider : IPolicySimulationMetrics
|
||||
Average(durations));
|
||||
|
||||
return new PolicySimulationMetricsResponse(
|
||||
new PolicySimulationQueueDepth(totalQueueDepth, queueCounts),
|
||||
new PolicySimulationQueueDepth(totalQueueDepth, snapshot),
|
||||
latencyMetrics);
|
||||
}
|
||||
|
||||
@@ -134,16 +138,21 @@ internal sealed class PolicySimulationMetricsProvider : IPolicySimulationMetrics
|
||||
private IEnumerable<Measurement<long>> ObserveQueueDepth()
|
||||
{
|
||||
IReadOnlyDictionary<string, long> snapshot;
|
||||
string tenantId;
|
||||
lock (_snapshotLock)
|
||||
{
|
||||
snapshot = _latestQueueSnapshot;
|
||||
tenantId = _latestTenantId;
|
||||
}
|
||||
|
||||
tenantId = string.IsNullOrWhiteSpace(tenantId) ? "unknown" : tenantId;
|
||||
|
||||
foreach (var pair in snapshot)
|
||||
{
|
||||
yield return new Measurement<long>(
|
||||
pair.Value,
|
||||
new KeyValuePair<string, object?>("status", pair.Key));
|
||||
new KeyValuePair<string, object?>("status", pair.Key),
|
||||
new KeyValuePair<string, object?>("tenantId", tenantId));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,12 +29,13 @@
|
||||
## Policy Studio (Sprint 27)
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
| SCHED-CONSOLE-27-001 | DONE (2025-11-03) | Scheduler WebService Guild, Policy Registry Guild | SCHED-WEB-16-103, REGISTRY-API-27-005 | Provide policy batch simulation orchestration endpoints (`/policies/simulations` POST/GET) exposing run creation, shard status, SSE progress, cancellation, and retries with RBAC enforcement. | API handles shard lifecycle with SSE heartbeats + retry headers; unauthorized requests rejected; integration tests cover submit/cancel/resume flows. |
|
||||
| SCHED-CONSOLE-27-002 | DONE (2025-11-05) | Scheduler WebService Guild, Observability Guild | SCHED-CONSOLE-27-001 | Emit telemetry endpoints/metrics (`policy_simulation_queue_depth`, `policy_simulation_latency_seconds`) and webhook callbacks for completion/failure consumed by Registry. | Metrics exposed via gateway, dashboards seeded, webhook contract documented, integration tests validate metrics emission. |
|
||||
> 2025-11-05: Resuming to align instrumentation naming with architecture spec, exercise latency recording in SSE flows, and ensure registry webhook contract (samples/docs) reflects terminal result behaviour.
|
||||
> 2025-11-05: Histogram renamed to `policy_simulation_latency_seconds`, queue gauge kept stable, new unit tests cover metrics capture/latency recording, and docs updated. Local `dotnet test` build currently blocked by existing GraphJobs visibility errors (see `StellaOps.Scheduler.WebService/GraphJobs/IGraphJobStore.cs`).
|
||||
|
||||
## Vulnerability Explorer (Sprint 29)
|
||||
| SCHED-CONSOLE-27-001 | DONE (2025-11-03) | Scheduler WebService Guild, Policy Registry Guild | SCHED-WEB-16-103, REGISTRY-API-27-005 | Provide policy batch simulation orchestration endpoints (`/policies/simulations` POST/GET) exposing run creation, shard status, SSE progress, cancellation, and retries with RBAC enforcement. | API handles shard lifecycle with SSE heartbeats + retry headers; unauthorized requests rejected; integration tests cover submit/cancel/resume flows. |
|
||||
| SCHED-CONSOLE-27-002 | DONE (2025-11-05) | Scheduler WebService Guild, Observability Guild | SCHED-CONSOLE-27-001 | Emit telemetry endpoints/metrics (`policy_simulation_queue_depth`, `policy_simulation_latency_seconds`) and webhook callbacks for completion/failure consumed by Registry. | Metrics exposed via gateway, dashboards seeded, webhook contract documented, integration tests validate metrics emission. |
|
||||
> 2025-11-05: Resuming to align instrumentation naming with architecture spec, exercise latency recording in SSE flows, and ensure registry webhook contract (samples/docs) reflects terminal result behaviour.
|
||||
> 2025-11-05: Histogram renamed to `policy_simulation_latency_seconds`, queue gauge kept stable, new unit tests cover metrics capture/latency recording, and docs updated. Local `dotnet test` build currently blocked by existing GraphJobs visibility errors (see `StellaOps.Scheduler.WebService/GraphJobs/IGraphJobStore.cs`).
|
||||
> 2025-11-06: Added tenant-aware tagging to `policy_simulation_queue_depth` gauge samples and refreshed metrics provider snapshot coverage.
|
||||
|
||||
## Vulnerability Explorer (Sprint 29)
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
| SCHED-VULN-29-001 | TODO | Scheduler WebService Guild, Findings Ledger Guild | SCHED-WEB-16-103, SBOM-VULN-29-001 | Expose resolver job APIs (`POST /vuln/resolver/jobs`, `GET /vuln/resolver/jobs/{id}`) to trigger candidate recomputation per artifact/policy change with RBAC and rate limits. | Resolver APIs documented; integration tests cover submit/status/cancel; unauthorized requests rejected. |
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"schema": "scheduler-impact-index@1",
|
||||
"generatedAt": "2025-10-01T00:00:00Z",
|
||||
"image": {
|
||||
"repository": "registry.stellaops.test/team/sample-service",
|
||||
"digest": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
|
||||
"tag": "1.0.0"
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
"purl": "pkg:docker/sample-service@1.0.0",
|
||||
"usage": [
|
||||
"runtime"
|
||||
]
|
||||
},
|
||||
{
|
||||
"purl": "pkg:pypi/requests@2.31.0",
|
||||
"usage": [
|
||||
"usedByEntrypoint"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
<ProjectReference Include="../StellaOps.Scheduler.Models/StellaOps.Scheduler.Models.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Fixtures\**\*.json" />
|
||||
<EmbeddedResource Include="..\..\samples\scanner\images\**\bom-index.json"
|
||||
Link="Fixtures\%(RecursiveDir)%(Filename)%(Extension)" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Scheduler.ImpactIndex;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scheduler.WebService.Tests;
|
||||
|
||||
public sealed class ImpactIndexFixtureTests
|
||||
{
|
||||
[Fact]
|
||||
public void FixtureDirectoryExists()
|
||||
{
|
||||
var fixtureDirectory = GetFixtureDirectory();
|
||||
Assert.True(Directory.Exists(fixtureDirectory), $"Fixture directory not found: {fixtureDirectory}");
|
||||
|
||||
var files = Directory.EnumerateFiles(fixtureDirectory, "bom-index.json", SearchOption.AllDirectories).ToArray();
|
||||
Assert.NotEmpty(files);
|
||||
|
||||
var sampleFile = Path.Combine(fixtureDirectory, "sample", "bom-index.json");
|
||||
Assert.Contains(sampleFile, files);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FixtureImpactIndexLoadsSampleImage()
|
||||
{
|
||||
var fixtureDirectory = GetFixtureDirectory();
|
||||
var options = new ImpactIndexStubOptions
|
||||
{
|
||||
FixtureDirectory = fixtureDirectory,
|
||||
SnapshotId = "tests/impact-index-stub"
|
||||
};
|
||||
|
||||
var index = new FixtureImpactIndex(options, TimeProvider.System, NullLogger<FixtureImpactIndex>.Instance);
|
||||
var selector = new Selector(SelectorScope.AllImages);
|
||||
|
||||
var impactSet = await index.ResolveAllAsync(selector, usageOnly: false);
|
||||
|
||||
Assert.True(impactSet.Total > 0, "Expected the fixture impact index to load at least one image.");
|
||||
}
|
||||
|
||||
private static string GetFixtureDirectory()
|
||||
{
|
||||
var assemblyLocation = typeof(SchedulerWebApplicationFactory).Assembly.Location;
|
||||
var assemblyDirectory = Path.GetDirectoryName(assemblyLocation)
|
||||
?? AppContext.BaseDirectory;
|
||||
|
||||
return Path.GetFullPath(Path.Combine(assemblyDirectory, "seed-data", "impact-index"));
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.Metrics;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Scheduler.Storage.Mongo.Repositories;
|
||||
using StellaOps.Scheduler.WebService.PolicySimulations;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scheduler.WebService.Tests;
|
||||
|
||||
@@ -12,14 +20,14 @@ public sealed class PolicySimulationMetricsProviderTests
|
||||
public async Task CaptureAsync_ComputesQueueDepthAndLatency()
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var queueCounts = new Dictionary<PolicyRunJobStatus, long>
|
||||
var counts = new Dictionary<PolicyRunJobStatus, long>
|
||||
{
|
||||
[PolicyRunJobStatus.Pending] = 2,
|
||||
[PolicyRunJobStatus.Dispatching] = 1,
|
||||
[PolicyRunJobStatus.Submitted] = 1
|
||||
};
|
||||
|
||||
var jobs = new List<PolicyRunJob>
|
||||
var jobs = new[]
|
||||
{
|
||||
CreateJob(
|
||||
status: PolicyRunJobStatus.Completed,
|
||||
@@ -34,8 +42,9 @@ public sealed class PolicySimulationMetricsProviderTests
|
||||
cancelledAt: now.AddSeconds(-20))
|
||||
};
|
||||
|
||||
await using var provider = new PolicySimulationMetricsProvider(
|
||||
new StubPolicyRunJobRepository(queueCounts, jobs));
|
||||
var repository = new StubPolicyRunJobRepository(counts, jobs);
|
||||
|
||||
using var provider = new PolicySimulationMetricsProvider(repository);
|
||||
|
||||
var response = await provider.CaptureAsync("tenant-alpha", CancellationToken.None);
|
||||
|
||||
@@ -49,17 +58,88 @@ public sealed class PolicySimulationMetricsProviderTests
|
||||
Assert.Equal(20.0, response.Latency.P50);
|
||||
Assert.Equal(28.0, response.Latency.P90);
|
||||
Assert.Equal(29.0, response.Latency.P95);
|
||||
Assert.Equal(30.0, response.Latency.P99);
|
||||
Assert.True(response.Latency.P99.HasValue);
|
||||
Assert.Equal(29.8, response.Latency.P99.Value, 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CaptureAsync_UpdatesSnapshotAndEmitsTenantTaggedGauge()
|
||||
{
|
||||
var repository = new StubPolicyRunJobRepository();
|
||||
repository.QueueCounts[PolicyRunJobStatus.Pending] = 3;
|
||||
repository.QueueCounts[PolicyRunJobStatus.Dispatching] = 1;
|
||||
repository.QueueCounts[PolicyRunJobStatus.Submitted] = 2;
|
||||
|
||||
var now = DateTimeOffset.Parse("2025-11-06T10:00:00Z", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal);
|
||||
repository.Jobs.Add(CreateJob(
|
||||
status: PolicyRunJobStatus.Completed,
|
||||
queuedAt: now.AddMinutes(-30),
|
||||
submittedAt: now.AddMinutes(-28),
|
||||
completedAt: now.AddMinutes(-5),
|
||||
id: "job-1",
|
||||
runId: "run-job-1"));
|
||||
repository.Jobs.Add(CreateJob(
|
||||
status: PolicyRunJobStatus.Failed,
|
||||
queuedAt: now.AddMinutes(-20),
|
||||
submittedAt: now.AddMinutes(-18),
|
||||
completedAt: now.AddMinutes(-2),
|
||||
id: "job-2",
|
||||
runId: "run-job-2",
|
||||
lastError: "policy engine timeout"));
|
||||
|
||||
using var provider = new PolicySimulationMetricsProvider(repository);
|
||||
|
||||
var measurements = new List<(string Status, string Tenant, long Value)>();
|
||||
using var listener = new MeterListener
|
||||
{
|
||||
InstrumentPublished = (instrument, meterListener) =>
|
||||
{
|
||||
if (instrument.Meter.Name == "StellaOps.Scheduler.WebService.PolicySimulations" &&
|
||||
instrument.Name == "policy_simulation_queue_depth")
|
||||
{
|
||||
meterListener.EnableMeasurementEvents(instrument);
|
||||
}
|
||||
}
|
||||
};
|
||||
listener.SetMeasurementEventCallback<long>((instrument, measurement, tags, state) =>
|
||||
{
|
||||
var status = string.Empty;
|
||||
var tenant = string.Empty;
|
||||
|
||||
foreach (var tag in tags)
|
||||
{
|
||||
if (string.Equals(tag.Key, "status", StringComparison.Ordinal))
|
||||
{
|
||||
status = tag.Value?.ToString() ?? string.Empty;
|
||||
}
|
||||
else if (string.Equals(tag.Key, "tenantId", StringComparison.Ordinal))
|
||||
{
|
||||
tenant = tag.Value?.ToString() ?? string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
measurements.Add((status, tenant, measurement));
|
||||
});
|
||||
listener.Start();
|
||||
|
||||
var response = await provider.CaptureAsync("tenant-alpha", CancellationToken.None);
|
||||
Assert.Equal(6, response.QueueDepth.Total);
|
||||
|
||||
listener.RecordObservableInstruments();
|
||||
|
||||
Assert.Contains(measurements, item =>
|
||||
item.Status == "pending" &&
|
||||
item.Tenant == "tenant-alpha" &&
|
||||
item.Value == 3);
|
||||
listener.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RecordLatency_EmitsHistogramMeasurement()
|
||||
{
|
||||
var repo = new StubPolicyRunJobRepository(
|
||||
counts: new Dictionary<PolicyRunJobStatus, long>(),
|
||||
jobs: Array.Empty<PolicyRunJob>());
|
||||
var repository = new StubPolicyRunJobRepository();
|
||||
|
||||
using var provider = new PolicySimulationMetricsProvider(repo);
|
||||
using var provider = new PolicySimulationMetricsProvider(repository);
|
||||
|
||||
var measurements = new List<double>();
|
||||
using var listener = new MeterListener
|
||||
@@ -81,64 +161,50 @@ public sealed class PolicySimulationMetricsProviderTests
|
||||
measurements.Add(measurement);
|
||||
}
|
||||
});
|
||||
|
||||
listener.Start();
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var status = new PolicyRunStatus(
|
||||
runId: "run-1",
|
||||
tenantId: "tenant-alpha",
|
||||
policyId: "policy-alpha",
|
||||
policyVersion: 1,
|
||||
mode: PolicyRunMode.Simulate,
|
||||
status: PolicyRunExecutionStatus.Succeeded,
|
||||
priority: PolicyRunPriority.Normal,
|
||||
var latencyJob = CreateJob(
|
||||
status: PolicyRunJobStatus.Completed,
|
||||
queuedAt: now.AddSeconds(-12),
|
||||
startedAt: now.AddSeconds(-10),
|
||||
finishedAt: now,
|
||||
stats: PolicyRunStats.Empty,
|
||||
inputs: PolicyRunInputs.Empty,
|
||||
determinismHash: null,
|
||||
errorCode: null,
|
||||
error: null,
|
||||
attempts: 1,
|
||||
traceId: null,
|
||||
explainUri: null,
|
||||
metadata: ImmutableSortedDictionary<string, string>.Empty,
|
||||
cancellationRequested: false,
|
||||
cancellationRequestedAt: null,
|
||||
cancellationReason: null,
|
||||
schemaVersion: null);
|
||||
submittedAt: now.AddSeconds(-10),
|
||||
completedAt: now,
|
||||
id: "job-latency",
|
||||
runId: "run-1");
|
||||
var status = PolicyRunStatusFactory.Create(latencyJob, now);
|
||||
|
||||
provider.RecordLatency(status, now);
|
||||
|
||||
listener.Dispose();
|
||||
|
||||
Assert.Single(measurements);
|
||||
Assert.Equal(12, measurements[0], precision: 6);
|
||||
|
||||
listener.Dispose();
|
||||
}
|
||||
|
||||
private static PolicyRunJob CreateJob(
|
||||
PolicyRunJobStatus status,
|
||||
DateTimeOffset queuedAt,
|
||||
DateTimeOffset submittedAt,
|
||||
DateTimeOffset? submittedAt,
|
||||
DateTimeOffset? completedAt,
|
||||
DateTimeOffset? cancelledAt = null)
|
||||
DateTimeOffset? cancelledAt = null,
|
||||
string? id = null,
|
||||
string? runId = null,
|
||||
string? lastError = null)
|
||||
{
|
||||
var id = Guid.NewGuid().ToString("N");
|
||||
var runId = $"run:{id}";
|
||||
var updatedAt = completedAt ?? cancelledAt ?? submittedAt;
|
||||
var jobId = id ?? Guid.NewGuid().ToString("N");
|
||||
var resolvedRunId = runId ?? $"run:{jobId}";
|
||||
var updatedAt = completedAt ?? cancelledAt ?? submittedAt ?? queuedAt;
|
||||
|
||||
return new PolicyRunJob(
|
||||
SchemaVersion: SchedulerSchemaVersions.PolicyRunJob,
|
||||
Id: id,
|
||||
Id: jobId,
|
||||
TenantId: "tenant-alpha",
|
||||
PolicyId: "policy-alpha",
|
||||
PolicyVersion: 1,
|
||||
Mode: PolicyRunMode.Simulate,
|
||||
Priority: PolicyRunPriority.Normal,
|
||||
PriorityRank: 0,
|
||||
RunId: runId,
|
||||
RunId: resolvedRunId,
|
||||
RequestedBy: "tester",
|
||||
CorrelationId: null,
|
||||
Metadata: ImmutableSortedDictionary<string, string>.Empty,
|
||||
@@ -146,8 +212,8 @@ public sealed class PolicySimulationMetricsProviderTests
|
||||
QueuedAt: queuedAt,
|
||||
Status: status,
|
||||
AttemptCount: 1,
|
||||
LastAttemptAt: submittedAt,
|
||||
LastError: null,
|
||||
LastAttemptAt: submittedAt ?? completedAt ?? queuedAt,
|
||||
LastError: lastError,
|
||||
CreatedAt: queuedAt,
|
||||
UpdatedAt: updatedAt,
|
||||
AvailableAt: queuedAt,
|
||||
@@ -163,15 +229,32 @@ public sealed class PolicySimulationMetricsProviderTests
|
||||
|
||||
private sealed class StubPolicyRunJobRepository : IPolicyRunJobRepository
|
||||
{
|
||||
private readonly IReadOnlyDictionary<PolicyRunJobStatus, long> _counts;
|
||||
private readonly IReadOnlyList<PolicyRunJob> _jobs;
|
||||
public StubPolicyRunJobRepository()
|
||||
{
|
||||
}
|
||||
|
||||
public StubPolicyRunJobRepository(
|
||||
IReadOnlyDictionary<PolicyRunJobStatus, long> counts,
|
||||
IReadOnlyList<PolicyRunJob> jobs)
|
||||
IDictionary<PolicyRunJobStatus, long> counts,
|
||||
IEnumerable<PolicyRunJob> jobs)
|
||||
{
|
||||
_counts = counts;
|
||||
_jobs = jobs;
|
||||
foreach (var pair in counts)
|
||||
{
|
||||
QueueCounts[pair.Key] = pair.Value;
|
||||
}
|
||||
|
||||
Jobs.AddRange(jobs);
|
||||
}
|
||||
|
||||
public Dictionary<PolicyRunJobStatus, long> QueueCounts { get; } = new();
|
||||
public List<PolicyRunJob> Jobs { get; } = new();
|
||||
|
||||
public Task InsertAsync(
|
||||
PolicyRunJob job,
|
||||
IClientSessionHandle? session = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Jobs.Add(job);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<long> CountAsync(
|
||||
@@ -180,12 +263,17 @@ public sealed class PolicySimulationMetricsProviderTests
|
||||
IReadOnlyCollection<PolicyRunJobStatus> statuses,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (statuses is null || statuses.Count == 0)
|
||||
{
|
||||
return Task.FromResult(QueueCounts.Values.Sum());
|
||||
}
|
||||
|
||||
long total = 0;
|
||||
foreach (var status in statuses)
|
||||
{
|
||||
if (_counts.TryGetValue(status, out var value))
|
||||
if (QueueCounts.TryGetValue(status, out var count))
|
||||
{
|
||||
total += value;
|
||||
total += count;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,44 +287,53 @@ public sealed class PolicySimulationMetricsProviderTests
|
||||
IReadOnlyCollection<PolicyRunJobStatus>? statuses = null,
|
||||
DateTimeOffset? queuedAfter = null,
|
||||
int limit = 50,
|
||||
MongoDB.Driver.IClientSessionHandle? session = null,
|
||||
IClientSessionHandle? session = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(_jobs);
|
||||
{
|
||||
IEnumerable<PolicyRunJob> query = Jobs;
|
||||
|
||||
Task IPolicyRunJobRepository.InsertAsync(
|
||||
PolicyRunJob job,
|
||||
MongoDB.Driver.IClientSessionHandle? session,
|
||||
CancellationToken cancellationToken)
|
||||
=> throw new NotSupportedException();
|
||||
if (statuses is { Count: > 0 })
|
||||
{
|
||||
query = query.Where(job => statuses.Contains(job.Status));
|
||||
}
|
||||
|
||||
Task<PolicyRunJob?> IPolicyRunJobRepository.GetAsync(
|
||||
if (queuedAfter is not null)
|
||||
{
|
||||
query = query.Where(job => (job.QueuedAt ?? job.CreatedAt) >= queuedAfter.Value);
|
||||
}
|
||||
|
||||
var result = query.Take(limit).ToList().AsReadOnly();
|
||||
return Task.FromResult<IReadOnlyList<PolicyRunJob>>(result);
|
||||
}
|
||||
|
||||
public Task<PolicyRunJob?> GetAsync(
|
||||
string tenantId,
|
||||
string jobId,
|
||||
MongoDB.Driver.IClientSessionHandle? session,
|
||||
CancellationToken cancellationToken)
|
||||
=> throw new NotSupportedException();
|
||||
IClientSessionHandle? session = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult<PolicyRunJob?>(Jobs.FirstOrDefault(job => job.Id == jobId));
|
||||
|
||||
Task<PolicyRunJob?> IPolicyRunJobRepository.GetByRunIdAsync(
|
||||
public Task<PolicyRunJob?> GetByRunIdAsync(
|
||||
string tenantId,
|
||||
string runId,
|
||||
MongoDB.Driver.IClientSessionHandle? session,
|
||||
CancellationToken cancellationToken)
|
||||
=> throw new NotSupportedException();
|
||||
IClientSessionHandle? session = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult<PolicyRunJob?>(Jobs.FirstOrDefault(job => string.Equals(job.RunId, runId, StringComparison.Ordinal)));
|
||||
|
||||
Task<PolicyRunJob?> IPolicyRunJobRepository.LeaseAsync(
|
||||
public Task<PolicyRunJob?> LeaseAsync(
|
||||
string leaseOwner,
|
||||
DateTimeOffset now,
|
||||
TimeSpan leaseDuration,
|
||||
int maxAttempts,
|
||||
MongoDB.Driver.IClientSessionHandle? session,
|
||||
CancellationToken cancellationToken)
|
||||
=> throw new NotSupportedException();
|
||||
IClientSessionHandle? session = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult<PolicyRunJob?>(null);
|
||||
|
||||
Task<bool> IPolicyRunJobRepository.ReplaceAsync(
|
||||
public Task<bool> ReplaceAsync(
|
||||
PolicyRunJob job,
|
||||
string? expectedLeaseOwner,
|
||||
MongoDB.Driver.IClientSessionHandle? session,
|
||||
CancellationToken cancellationToken)
|
||||
=> throw new NotSupportedException();
|
||||
string? expectedLeaseOwner = null,
|
||||
IClientSessionHandle? session = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,16 +11,16 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Scheduler.Queue;
|
||||
using StellaOps.Scheduler.Storage.Mongo.Repositories;
|
||||
|
||||
namespace StellaOps.Scheduler.WebService.Tests;
|
||||
|
||||
public sealed class RunEndpointTests : IClassFixture<WebApplicationFactory<Program>>
|
||||
{
|
||||
private readonly WebApplicationFactory<Program> _factory;
|
||||
|
||||
public RunEndpointTests(WebApplicationFactory<Program> factory)
|
||||
{
|
||||
_factory = factory;
|
||||
|
||||
namespace StellaOps.Scheduler.WebService.Tests;
|
||||
|
||||
public sealed class RunEndpointTests : IClassFixture<WebApplicationFactory<Program>>
|
||||
{
|
||||
private readonly WebApplicationFactory<Program> _factory;
|
||||
|
||||
public RunEndpointTests(WebApplicationFactory<Program> factory)
|
||||
{
|
||||
_factory = factory;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -100,13 +100,13 @@ public sealed class RunEndpointTests : IClassFixture<WebApplicationFactory<Progr
|
||||
var scheduleId = scheduleJson.GetProperty("schedule").GetProperty("id").GetString();
|
||||
Assert.False(string.IsNullOrEmpty(scheduleId));
|
||||
|
||||
var previewResponse = await client.PostAsJsonAsync("/api/v1/scheduler/runs/preview", new
|
||||
{
|
||||
scheduleId,
|
||||
usageOnly = true,
|
||||
sampleSize = 3
|
||||
});
|
||||
|
||||
var previewResponse = await client.PostAsJsonAsync("/api/v1/scheduler/runs/preview", new
|
||||
{
|
||||
scheduleId,
|
||||
usageOnly = true,
|
||||
sampleSize = 3
|
||||
});
|
||||
|
||||
previewResponse.EnsureSuccessStatusCode();
|
||||
var preview = await previewResponse.Content.ReadFromJsonAsync<JsonElement>();
|
||||
Assert.True(preview.GetProperty("total").GetInt32() >= 0);
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using StellaOps.Scheduler.WebService.Options;
|
||||
using StellaOps.Scheduler.WebService.Runs;
|
||||
using StellaOps.Scheduler.ImpactIndex;
|
||||
|
||||
namespace StellaOps.Scheduler.WebService.Tests;
|
||||
|
||||
@@ -15,6 +18,8 @@ public sealed class SchedulerWebApplicationFactory : WebApplicationFactory<Progr
|
||||
{
|
||||
builder.ConfigureAppConfiguration((_, configuration) =>
|
||||
{
|
||||
var fixtureDirectory = GetFixtureDirectory();
|
||||
|
||||
configuration.AddInMemoryCollection(new[]
|
||||
{
|
||||
new KeyValuePair<string, string?>("Scheduler:Authority:Enabled", "false"),
|
||||
@@ -27,12 +32,22 @@ public sealed class SchedulerWebApplicationFactory : WebApplicationFactory<Progr
|
||||
new KeyValuePair<string, string?>("Scheduler:Events:Webhooks:Excitor:Enabled", "true"),
|
||||
new KeyValuePair<string, string?>("Scheduler:Events:Webhooks:Excitor:HmacSecret", "excitor-secret"),
|
||||
new KeyValuePair<string, string?>("Scheduler:Events:Webhooks:Excitor:RateLimitRequests", "20"),
|
||||
new KeyValuePair<string, string?>("Scheduler:Events:Webhooks:Excitor:RateLimitWindowSeconds", "60")
|
||||
new KeyValuePair<string, string?>("Scheduler:Events:Webhooks:Excitor:RateLimitWindowSeconds", "60"),
|
||||
new KeyValuePair<string, string?>("Scheduler:ImpactIndex:FixtureDirectory", fixtureDirectory)
|
||||
});
|
||||
});
|
||||
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
var fixtureDirectory = GetFixtureDirectory();
|
||||
|
||||
services.RemoveAll<ImpactIndexStubOptions>();
|
||||
services.AddSingleton(new ImpactIndexStubOptions
|
||||
{
|
||||
FixtureDirectory = fixtureDirectory,
|
||||
SnapshotId = "tests/impact-index-stub"
|
||||
});
|
||||
|
||||
services.Configure<SchedulerEventsOptions>(options =>
|
||||
{
|
||||
options.Webhooks ??= new SchedulerInboundWebhooksOptions();
|
||||
@@ -52,4 +67,14 @@ public sealed class SchedulerWebApplicationFactory : WebApplicationFactory<Progr
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private static string GetFixtureDirectory()
|
||||
{
|
||||
var assemblyLocation = typeof(SchedulerWebApplicationFactory).Assembly.Location;
|
||||
var assemblyDirectory = Path.GetDirectoryName(assemblyLocation)
|
||||
?? AppContext.BaseDirectory;
|
||||
|
||||
var fixtureDirectory = Path.Combine(assemblyDirectory, "seed-data", "impact-index");
|
||||
return Path.GetFullPath(fixtureDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,4 +18,9 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../StellaOps.Scheduler.WebService/StellaOps.Scheduler.WebService.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="seed-data/impact-index/**">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"schema": "scheduler-impact-index@1",
|
||||
"generatedAt": "2025-10-01T00:00:00Z",
|
||||
"image": {
|
||||
"repository": "registry.stellaops.test/team/sample-service",
|
||||
"digest": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
|
||||
"tag": "1.0.0"
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
"purl": "pkg:docker/sample-service@1.0.0",
|
||||
"usage": [
|
||||
"runtime"
|
||||
]
|
||||
},
|
||||
{
|
||||
"purl": "pkg:pypi/requests@2.31.0",
|
||||
"usage": [
|
||||
"usedByEntrypoint"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user