feat: Add UI benchmark driver and scenarios for graph interactions
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled

- Introduced `ui_bench_driver.mjs` to read scenarios and fixture manifest, generating a deterministic run plan.
- Created `ui_bench_plan.md` outlining the purpose, scope, and next steps for the benchmark.
- Added `ui_bench_scenarios.json` containing various scenarios for graph UI interactions.
- Implemented tests for CLI commands, ensuring bundle verification and telemetry defaults.
- Developed schemas for orchestrator components, including replay manifests and event envelopes.
- Added mock API for risk management, including listing and statistics functionalities.
- Implemented models for risk profiles and query options to support the new API.
This commit is contained in:
StellaOps Bot
2025-12-02 01:28:17 +02:00
parent 909d9b6220
commit 44171930ff
94 changed files with 3606 additions and 271 deletions

View File

@@ -48,17 +48,43 @@ namespace StellaOps.Cli.Commands;
internal static class CommandHandlers
{
private const string KmsPassphraseEnvironmentVariable = "STELLAOPS_KMS_PASSPHRASE";
private static readonly JsonSerializerOptions KmsJsonOptions = new(JsonSerializerDefaults.Web)
{
WriteIndented = true
};
private static readonly JsonSerializerOptions KmsJsonOptions = new(JsonSerializerDefaults.Web)
{
WriteIndented = true
};
private static async Task VerifyBundleAsync(string path, ILogger logger, CancellationToken cancellationToken)
{
// Simple SHA256 check using sidecar .sha256 file if present; fail closed on mismatch.
var shaPath = path + ".sha256";
if (!File.Exists(shaPath))
{
logger.LogError("Checksum file missing for bundle {Bundle}. Expected sidecar {Sidecar}.", path, shaPath);
Environment.ExitCode = 21;
throw new InvalidOperationException("Checksum file missing");
}
var expected = (await File.ReadAllTextAsync(shaPath, cancellationToken).ConfigureAwait(false)).Trim();
using var stream = File.OpenRead(path);
var hash = await SHA256.HashDataAsync(stream, cancellationToken).ConfigureAwait(false);
var actual = Convert.ToHexString(hash).ToLowerInvariant();
if (!string.Equals(expected, actual, StringComparison.OrdinalIgnoreCase))
{
logger.LogError("Checksum mismatch for {Bundle}. Expected {Expected} but found {Actual}", path, expected, actual);
Environment.ExitCode = 22;
throw new InvalidOperationException("Checksum verification failed");
}
logger.LogInformation("Checksum verified for {Bundle}", path);
}
public static async Task HandleScannerDownloadAsync(
IServiceProvider services,
string channel,
string? output,
bool overwrite,
bool install,
public static async Task HandleScannerDownloadAsync(
IServiceProvider services,
string channel,
string? output,
bool overwrite,
bool install,
bool verbose,
CancellationToken cancellationToken)
{
@@ -88,24 +114,29 @@ internal static class CommandHandlers
CliMetrics.RecordScannerDownload(channel, result.FromCache);
if (install)
{
var installer = scope.ServiceProvider.GetRequiredService<IScannerInstaller>();
await installer.InstallAsync(result.Path, verbose, cancellationToken).ConfigureAwait(false);
CliMetrics.RecordScannerInstall(channel);
}
if (install)
{
await VerifyBundleAsync(result.Path, logger, cancellationToken).ConfigureAwait(false);
var installer = scope.ServiceProvider.GetRequiredService<IScannerInstaller>();
await installer.InstallAsync(result.Path, verbose, cancellationToken).ConfigureAwait(false);
CliMetrics.RecordScannerInstall(channel);
}
Environment.ExitCode = 0;
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to download scanner bundle.");
Environment.ExitCode = 1;
}
finally
{
verbosity.MinimumLevel = previousLevel;
}
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to download scanner bundle.");
if (Environment.ExitCode == 0)
{
Environment.ExitCode = 1;
}
}
finally
{
verbosity.MinimumLevel = previousLevel;
}
}
public static async Task HandleTaskRunnerSimulateAsync(

View File

@@ -107,9 +107,9 @@ public sealed class CliProfileStore
public Dictionary<string, CliProfile> Profiles { get; init; } = new(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Default telemetry opt-in status.
/// Default telemetry opt-in status. Defaults to false for privacy.
/// </summary>
public bool? TelemetryEnabled { get; set; }
public bool TelemetryEnabled { get; set; } = false;
}
/// <summary>
@@ -225,7 +225,7 @@ public sealed class CliProfileManager
public async Task SetTelemetryEnabledAsync(bool? enabled, CancellationToken cancellationToken = default)
{
var store = await GetStoreAsync(cancellationToken).ConfigureAwait(false);
store.TelemetryEnabled = enabled;
store.TelemetryEnabled = enabled ?? false;
await SaveStoreAsync(store, cancellationToken).ConfigureAwait(false);
}
@@ -247,7 +247,18 @@ public sealed class CliProfileManager
{
await using var stream = File.OpenRead(_profilesFilePath);
var store = await JsonSerializer.DeserializeAsync<CliProfileStore>(stream, JsonOptions, cancellationToken).ConfigureAwait(false);
return store ?? new CliProfileStore();
if (store is null)
{
return new CliProfileStore();
}
// Ensure default-off if older files had telemetryEnabled missing/null.
if (!store.TelemetryEnabled)
{
store.TelemetryEnabled = false;
}
return store;
}
catch (JsonException)
{

View File

@@ -4512,11 +4512,11 @@ internal sealed class BackendOperationsClient : IBackendOperationsClient
}
// CLI-SDK-64-001: SDK update operations
public async Task<SdkUpdateResponse> CheckSdkUpdatesAsync(SdkUpdateRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
EnsureBackendConfigured();
public async Task<SdkUpdateResponse> CheckSdkUpdatesAsync(SdkUpdateRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
EnsureBackendConfigured();
OfflineModeGuard.ThrowIfOffline("sdk update");
var queryParams = new List<string>();
@@ -4554,9 +4554,9 @@ internal sealed class BackendOperationsClient : IBackendOperationsClient
};
}
var result = await response.Content.ReadFromJsonAsync<SdkUpdateResponse>(JsonOptions, cancellationToken).ConfigureAwait(false);
return result ?? new SdkUpdateResponse { Success = false, Error = "Empty response" };
}
var result = await response.Content.ReadFromJsonAsync<SdkUpdateResponse>(JsonOptions, cancellationToken).ConfigureAwait(false);
return result ?? new SdkUpdateResponse { Success = false, Error = "Empty response" };
}
public async Task<SdkListResponse> ListInstalledSdksAsync(string? language, string? tenant, CancellationToken cancellationToken)
{

View File

@@ -0,0 +1,60 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Cli.Commands;
using Xunit;
namespace StellaOps.Cli.Tests.Commands;
public sealed class ScannerDownloadVerifyTests
{
[Fact]
public async Task VerifyBundleAsync_Succeeds_WhenHashMatches()
{
var tmp = Path.Combine(Path.GetTempPath(), $"stellaops-cli-{Guid.NewGuid():N}");
Directory.CreateDirectory(tmp);
var bundle = Path.Combine(tmp, "scanner.tgz");
await File.WriteAllTextAsync(bundle, "hello");
var hash = Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(File.ReadAllBytes(bundle))).ToLowerInvariant();
await File.WriteAllTextAsync(bundle + ".sha256", hash);
await CommandHandlersTestShim.VerifyBundlePublicAsync(bundle, NullLogger.Instance, CancellationToken.None);
}
[Fact]
public async Task VerifyBundleAsync_Throws_WhenHashMismatch()
{
var tmp = Path.Combine(Path.GetTempPath(), $"stellaops-cli-{Guid.NewGuid():N}");
Directory.CreateDirectory(tmp);
var bundle = Path.Combine(tmp, "scanner.tgz");
await File.WriteAllTextAsync(bundle, "hello");
await File.WriteAllTextAsync(bundle + ".sha256", "deadbeef");
await Assert.ThrowsAsync<InvalidOperationException>(() =>
CommandHandlersTestShim.VerifyBundlePublicAsync(bundle, NullLogger.Instance, CancellationToken.None));
}
[Fact]
public async Task VerifyBundleAsync_Throws_WhenChecksumMissing()
{
var tmp = Path.Combine(Path.GetTempPath(), $"stellaops-cli-{Guid.NewGuid():N}");
Directory.CreateDirectory(tmp);
var bundle = Path.Combine(tmp, "scanner.tgz");
await File.WriteAllTextAsync(bundle, "hello");
await Assert.ThrowsAsync<InvalidOperationException>(() =>
CommandHandlersTestShim.VerifyBundlePublicAsync(bundle, NullLogger.Instance, CancellationToken.None));
}
}
internal static class CommandHandlersTestShim
{
public static Task VerifyBundlePublicAsync(string path, ILogger logger, CancellationToken token)
=> typeof(CommandHandlers)
.GetMethod(\"VerifyBundleAsync\", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)!
.Invoke(null, new object[] { path, logger, token }) as Task
?? Task.CompletedTask;
}

View File

@@ -0,0 +1,35 @@
using System.IO;
using System.Threading.Tasks;
using StellaOps.Cli.Configuration;
using Xunit;
namespace StellaOps.Cli.Tests.Configuration;
public sealed class TelemetryDefaultsTests
{
[Fact]
public async Task NewStore_DefaultsTelemetryToOff()
{
var tempDir = Path.Combine(Path.GetTempPath(), $"stellaops-cli-telemetry-{Path.GetRandomFileName()}");
Directory.CreateDirectory(tempDir);
var manager = new CliProfileManager(tempDir);
var store = await manager.GetStoreAsync();
Assert.False(store.TelemetryEnabled);
}
[Fact]
public async Task SetTelemetryEnabled_PersistsValue()
{
var tempDir = Path.Combine(Path.GetTempPath(), $"stellaops-cli-telemetry-{Path.GetRandomFileName()}");
Directory.CreateDirectory(tempDir);
var manager = new CliProfileManager(tempDir);
await manager.SetTelemetryEnabledAsync(true);
var store = await manager.GetStoreAsync();
Assert.True(store.TelemetryEnabled);
}
}

View File

@@ -0,0 +1,41 @@
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Xunit;
namespace StellaOps.Cli.Tests.Contracts;
public sealed class CliSpecTests
{
private static readonly string SpecPath = Path.Combine("docs", "modules", "cli", "contracts", "cli-spec-v1.yaml");
[Fact]
public async Task Spec_Exists_And_Has_PrivacyDefaults()
{
Assert.True(File.Exists(SpecPath), $"Spec file missing: {SpecPath}");
var text = await File.ReadAllTextAsync(SpecPath);
Assert.Contains("defaultEnabled: false", text);
Assert.Contains("checksumRequired: true", text);
Assert.Contains("cosignVerifyDefault: true", text);
}
[Fact]
public async Task Spec_Has_Pinned_Buildx_Digest()
{
var text = await File.ReadAllLinesAsync(SpecPath);
var digestLine = text.FirstOrDefault(l => l.TrimStart().StartsWith("imageDigest:"));
Assert.False(string.IsNullOrWhiteSpace(digestLine));
Assert.DoesNotContain("TO-BE-PINNED", digestLine);
}
[Fact]
public async Task Spec_Declares_Install_ExitCodes()
{
var text = await File.ReadAllTextAsync(SpecPath);
Assert.Contains("21: checksum-file-missing", text);
Assert.Contains("22: checksum-mismatch", text);
}
}