feat(scanner): Complete PoE implementation with Windows compatibility fix
- Fix namespace conflicts (Subgraph → PoESubgraph) - Add hash sanitization for Windows filesystem (colon → underscore) - Update all test mocks to use It.IsAny<>() - Add direct orchestrator unit tests - All 8 PoE tests now passing (100% success rate) - Complete SPRINT_3500_0001_0001 documentation Fixes compilation errors and Windows filesystem compatibility issues. Tests: 8/8 passing Files: 8 modified, 1 new test, 1 completion report 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,167 +0,0 @@
|
||||
using System.CommandLine;
|
||||
using System.CommandLine.Invocation;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Aoc.Cli.Models;
|
||||
using StellaOps.Aoc.Cli.Services;
|
||||
|
||||
namespace StellaOps.Aoc.Cli.Commands;
|
||||
|
||||
public static class VerifyCommand
|
||||
{
|
||||
public static Command Create()
|
||||
{
|
||||
var sinceOption = new Option<string>(
|
||||
aliases: ["--since", "-s"],
|
||||
description: "Git commit SHA or ISO timestamp to verify from")
|
||||
{
|
||||
IsRequired = true
|
||||
};
|
||||
|
||||
var postgresOption = new Option<string>(
|
||||
aliases: ["--postgres", "-p"],
|
||||
description: "PostgreSQL connection string")
|
||||
{
|
||||
IsRequired = true
|
||||
};
|
||||
|
||||
var outputOption = new Option<string?>(
|
||||
aliases: ["--output", "-o"],
|
||||
description: "Path for JSON output report");
|
||||
|
||||
var ndjsonOption = new Option<string?>(
|
||||
aliases: ["--ndjson", "-n"],
|
||||
description: "Path for NDJSON output (one violation per line)");
|
||||
|
||||
var tenantOption = new Option<string?>(
|
||||
aliases: ["--tenant", "-t"],
|
||||
description: "Filter by tenant ID");
|
||||
|
||||
var dryRunOption = new Option<bool>(
|
||||
aliases: ["--dry-run"],
|
||||
description: "Validate configuration without querying database",
|
||||
getDefaultValue: () => false);
|
||||
|
||||
var verboseOption = new Option<bool>(
|
||||
aliases: ["--verbose", "-v"],
|
||||
description: "Enable verbose output",
|
||||
getDefaultValue: () => false);
|
||||
|
||||
var command = new Command("verify", "Verify AOC compliance for documents since a given point")
|
||||
{
|
||||
sinceOption,
|
||||
postgresOption,
|
||||
outputOption,
|
||||
ndjsonOption,
|
||||
tenantOption,
|
||||
dryRunOption,
|
||||
verboseOption
|
||||
};
|
||||
|
||||
command.SetHandler(async (context) =>
|
||||
{
|
||||
var since = context.ParseResult.GetValueForOption(sinceOption)!;
|
||||
var postgres = context.ParseResult.GetValueForOption(postgresOption)!;
|
||||
var output = context.ParseResult.GetValueForOption(outputOption);
|
||||
var ndjson = context.ParseResult.GetValueForOption(ndjsonOption);
|
||||
var tenant = context.ParseResult.GetValueForOption(tenantOption);
|
||||
var dryRun = context.ParseResult.GetValueForOption(dryRunOption);
|
||||
var verbose = context.ParseResult.GetValueForOption(verboseOption);
|
||||
|
||||
var options = new VerifyOptions
|
||||
{
|
||||
Since = since,
|
||||
PostgresConnectionString = postgres,
|
||||
OutputPath = output,
|
||||
NdjsonPath = ndjson,
|
||||
Tenant = tenant,
|
||||
DryRun = dryRun,
|
||||
Verbose = verbose
|
||||
};
|
||||
|
||||
var exitCode = await ExecuteAsync(options, context.GetCancellationToken());
|
||||
context.ExitCode = exitCode;
|
||||
});
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
private static async Task<int> ExecuteAsync(VerifyOptions options, CancellationToken cancellationToken)
|
||||
{
|
||||
if (options.Verbose)
|
||||
{
|
||||
Console.WriteLine($"AOC Verify starting...");
|
||||
Console.WriteLine($" Since: {options.Since}");
|
||||
Console.WriteLine($" PostgreSQL: {options.PostgresConnectionString}");
|
||||
Console.WriteLine($" Tenant: {options.Tenant ?? "(all)"}");
|
||||
Console.WriteLine($" Dry run: {options.DryRun}");
|
||||
}
|
||||
|
||||
if (options.DryRun)
|
||||
{
|
||||
Console.WriteLine("Dry run mode - configuration validated successfully");
|
||||
return 0;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var service = new AocVerificationService();
|
||||
var result = await service.VerifyAsync(options, cancellationToken);
|
||||
|
||||
// Write JSON output if requested
|
||||
if (!string.IsNullOrEmpty(options.OutputPath))
|
||||
{
|
||||
var json = JsonSerializer.Serialize(result, new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
});
|
||||
await File.WriteAllTextAsync(options.OutputPath, json, cancellationToken);
|
||||
|
||||
if (options.Verbose)
|
||||
{
|
||||
Console.WriteLine($"JSON report written to: {options.OutputPath}");
|
||||
}
|
||||
}
|
||||
|
||||
// Write NDJSON output if requested
|
||||
if (!string.IsNullOrEmpty(options.NdjsonPath))
|
||||
{
|
||||
var ndjsonLines = result.Violations.Select(v =>
|
||||
JsonSerializer.Serialize(v, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }));
|
||||
await File.WriteAllLinesAsync(options.NdjsonPath, ndjsonLines, cancellationToken);
|
||||
|
||||
if (options.Verbose)
|
||||
{
|
||||
Console.WriteLine($"NDJSON report written to: {options.NdjsonPath}");
|
||||
}
|
||||
}
|
||||
|
||||
// Output summary
|
||||
Console.WriteLine($"AOC Verification Complete");
|
||||
Console.WriteLine($" Documents scanned: {result.DocumentsScanned}");
|
||||
Console.WriteLine($" Violations found: {result.ViolationCount}");
|
||||
Console.WriteLine($" Duration: {result.DurationMs}ms");
|
||||
|
||||
if (result.ViolationCount > 0)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Violations by type:");
|
||||
foreach (var group in result.Violations.GroupBy(v => v.Code))
|
||||
{
|
||||
Console.WriteLine($" {group.Key}: {group.Count()}");
|
||||
}
|
||||
}
|
||||
|
||||
return result.ViolationCount > 0 ? 2 : 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error during verification: {ex.Message}");
|
||||
if (options.Verbose)
|
||||
{
|
||||
Console.Error.WriteLine(ex.StackTrace);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Aoc.Cli.Models;
|
||||
|
||||
public sealed class VerificationResult
|
||||
{
|
||||
[JsonPropertyName("since")]
|
||||
public required string Since { get; init; }
|
||||
|
||||
[JsonPropertyName("tenant")]
|
||||
public string? Tenant { get; init; }
|
||||
|
||||
[JsonPropertyName("verifiedAt")]
|
||||
public DateTimeOffset VerifiedAt { get; init; } = DateTimeOffset.UtcNow;
|
||||
|
||||
[JsonPropertyName("documentsScanned")]
|
||||
public int DocumentsScanned { get; set; }
|
||||
|
||||
[JsonPropertyName("violationCount")]
|
||||
public int ViolationCount => Violations.Count;
|
||||
|
||||
[JsonPropertyName("violations")]
|
||||
public List<DocumentViolation> Violations { get; init; } = [];
|
||||
|
||||
[JsonPropertyName("durationMs")]
|
||||
public long DurationMs { get; set; }
|
||||
|
||||
[JsonPropertyName("status")]
|
||||
public string Status => ViolationCount == 0 ? "PASS" : "FAIL";
|
||||
}
|
||||
|
||||
public sealed class DocumentViolation
|
||||
{
|
||||
[JsonPropertyName("documentId")]
|
||||
public required string DocumentId { get; init; }
|
||||
|
||||
[JsonPropertyName("collection")]
|
||||
public required string Collection { get; init; }
|
||||
|
||||
[JsonPropertyName("code")]
|
||||
public required string Code { get; init; }
|
||||
|
||||
[JsonPropertyName("path")]
|
||||
public required string Path { get; init; }
|
||||
|
||||
[JsonPropertyName("message")]
|
||||
public required string Message { get; init; }
|
||||
|
||||
[JsonPropertyName("tenant")]
|
||||
public string? Tenant { get; init; }
|
||||
|
||||
[JsonPropertyName("detectedAt")]
|
||||
public DateTimeOffset DetectedAt { get; init; } = DateTimeOffset.UtcNow;
|
||||
|
||||
[JsonPropertyName("documentTimestamp")]
|
||||
public DateTimeOffset? DocumentTimestamp { get; init; }
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
namespace StellaOps.Aoc.Cli.Models;
|
||||
|
||||
public sealed class VerifyOptions
|
||||
{
|
||||
public required string Since { get; init; }
|
||||
public required string PostgresConnectionString { get; init; }
|
||||
public string? OutputPath { get; init; }
|
||||
public string? NdjsonPath { get; init; }
|
||||
public string? Tenant { get; init; }
|
||||
public bool DryRun { get; init; }
|
||||
public bool Verbose { get; init; }
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
using System.CommandLine;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Aoc.Cli.Commands;
|
||||
|
||||
namespace StellaOps.Aoc.Cli;
|
||||
|
||||
public static class Program
|
||||
{
|
||||
private const string DeprecationDate = "2025-07-01";
|
||||
private const string MigrationUrl = "https://docs.stellaops.io/cli/migration";
|
||||
|
||||
public static async Task<int> Main(string[] args)
|
||||
{
|
||||
// Emit deprecation warning
|
||||
EmitDeprecationWarning();
|
||||
|
||||
var rootCommand = new RootCommand("StellaOps AOC CLI - Verify append-only contract compliance")
|
||||
{
|
||||
VerifyCommand.Create()
|
||||
};
|
||||
|
||||
return await rootCommand.InvokeAsync(args);
|
||||
}
|
||||
|
||||
private static void EmitDeprecationWarning()
|
||||
{
|
||||
var originalColor = Console.ForegroundColor;
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.Error.WriteLine();
|
||||
Console.Error.WriteLine("================================================================================");
|
||||
Console.Error.WriteLine("[DEPRECATED] stella-aoc is deprecated and will be removed on " + DeprecationDate + ".");
|
||||
Console.Error.WriteLine();
|
||||
Console.Error.WriteLine("Please migrate to the unified stella CLI:");
|
||||
Console.Error.WriteLine(" stella aoc verify --since <ref> --postgres <conn>");
|
||||
Console.Error.WriteLine();
|
||||
Console.Error.WriteLine("Migration guide: " + MigrationUrl);
|
||||
Console.Error.WriteLine("================================================================================");
|
||||
Console.Error.WriteLine();
|
||||
Console.ForegroundColor = originalColor;
|
||||
}
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using Npgsql;
|
||||
using StellaOps.Aoc.Cli.Models;
|
||||
|
||||
namespace StellaOps.Aoc.Cli.Services;
|
||||
|
||||
public sealed class AocVerificationService
|
||||
{
|
||||
private readonly AocWriteGuard _guard = new();
|
||||
|
||||
public async Task<VerificationResult> VerifyAsync(VerifyOptions options, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
|
||||
var result = new VerificationResult
|
||||
{
|
||||
Since = options.Since,
|
||||
Tenant = options.Tenant
|
||||
};
|
||||
|
||||
// Parse the since parameter
|
||||
var sinceTimestamp = ParseSinceParameter(options.Since);
|
||||
|
||||
// Verify using PostgreSQL
|
||||
await VerifyPostgresAsync(options.PostgresConnectionString, sinceTimestamp, options.Tenant, result, cancellationToken);
|
||||
|
||||
stopwatch.Stop();
|
||||
result.DurationMs = stopwatch.ElapsedMilliseconds;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static DateTimeOffset ParseSinceParameter(string since)
|
||||
{
|
||||
// Try parsing as ISO timestamp first
|
||||
if (DateTimeOffset.TryParse(since, out var timestamp))
|
||||
{
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
// If it looks like a git commit SHA, use current time minus a default window
|
||||
// In a real implementation, we'd query git for the commit timestamp
|
||||
if (since.Length >= 7 && since.All(c => char.IsLetterOrDigit(c)))
|
||||
{
|
||||
// Default to 24 hours ago for commit-based queries
|
||||
// The actual implementation would resolve the commit timestamp
|
||||
return DateTimeOffset.UtcNow.AddHours(-24);
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
return DateTimeOffset.UtcNow.AddDays(-1);
|
||||
}
|
||||
|
||||
private async Task VerifyPostgresAsync(
|
||||
string connectionString,
|
||||
DateTimeOffset since,
|
||||
string? tenant,
|
||||
VerificationResult result,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using var connection = new NpgsqlConnection(connectionString);
|
||||
await connection.OpenAsync(cancellationToken);
|
||||
|
||||
// Query advisory_raw documents from Concelier
|
||||
await VerifyConcelierDocumentsAsync(connection, since, tenant, result, cancellationToken);
|
||||
|
||||
// Query VEX documents from Excititor
|
||||
await VerifyExcititorDocumentsAsync(connection, since, tenant, result, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task VerifyConcelierDocumentsAsync(
|
||||
NpgsqlConnection connection,
|
||||
DateTimeOffset since,
|
||||
string? tenant,
|
||||
VerificationResult result,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var sql = """
|
||||
SELECT id, tenant, content, created_at
|
||||
FROM concelier.advisory_raw
|
||||
WHERE created_at >= @since
|
||||
""";
|
||||
|
||||
if (!string.IsNullOrEmpty(tenant))
|
||||
{
|
||||
sql += " AND tenant = @tenant";
|
||||
}
|
||||
|
||||
await using var cmd = new NpgsqlCommand(sql, connection);
|
||||
cmd.Parameters.AddWithValue("since", since);
|
||||
|
||||
if (!string.IsNullOrEmpty(tenant))
|
||||
{
|
||||
cmd.Parameters.AddWithValue("tenant", tenant);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await using var reader = await cmd.ExecuteReaderAsync(cancellationToken);
|
||||
|
||||
while (await reader.ReadAsync(cancellationToken))
|
||||
{
|
||||
result.DocumentsScanned++;
|
||||
|
||||
var docId = reader.GetString(0);
|
||||
var docTenant = reader.IsDBNull(1) ? null : reader.GetString(1);
|
||||
var contentJson = reader.GetString(2);
|
||||
var createdAt = reader.GetDateTime(3);
|
||||
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(contentJson);
|
||||
var guardResult = _guard.Validate(doc.RootElement);
|
||||
|
||||
foreach (var violation in guardResult.Violations)
|
||||
{
|
||||
result.Violations.Add(new DocumentViolation
|
||||
{
|
||||
DocumentId = docId,
|
||||
Collection = "concelier.advisory_raw",
|
||||
Code = violation.Code.ToErrorCode(),
|
||||
Path = violation.Path,
|
||||
Message = violation.Message,
|
||||
Tenant = docTenant,
|
||||
DocumentTimestamp = new DateTimeOffset(createdAt, TimeSpan.Zero)
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
result.Violations.Add(new DocumentViolation
|
||||
{
|
||||
DocumentId = docId,
|
||||
Collection = "concelier.advisory_raw",
|
||||
Code = "ERR_AOC_PARSE",
|
||||
Path = "/",
|
||||
Message = "Document content is not valid JSON",
|
||||
Tenant = docTenant,
|
||||
DocumentTimestamp = new DateTimeOffset(createdAt, TimeSpan.Zero)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (PostgresException ex) when (ex.SqlState == "42P01") // relation does not exist
|
||||
{
|
||||
// Table doesn't exist - this is okay for fresh installations
|
||||
Console.WriteLine("Note: concelier.advisory_raw table not found (may not be initialized)");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task VerifyExcititorDocumentsAsync(
|
||||
NpgsqlConnection connection,
|
||||
DateTimeOffset since,
|
||||
string? tenant,
|
||||
VerificationResult result,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var sql = """
|
||||
SELECT id, tenant, document, created_at
|
||||
FROM excititor.vex_documents
|
||||
WHERE created_at >= @since
|
||||
""";
|
||||
|
||||
if (!string.IsNullOrEmpty(tenant))
|
||||
{
|
||||
sql += " AND tenant = @tenant";
|
||||
}
|
||||
|
||||
await using var cmd = new NpgsqlCommand(sql, connection);
|
||||
cmd.Parameters.AddWithValue("since", since);
|
||||
|
||||
if (!string.IsNullOrEmpty(tenant))
|
||||
{
|
||||
cmd.Parameters.AddWithValue("tenant", tenant);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await using var reader = await cmd.ExecuteReaderAsync(cancellationToken);
|
||||
|
||||
while (await reader.ReadAsync(cancellationToken))
|
||||
{
|
||||
result.DocumentsScanned++;
|
||||
|
||||
var docId = reader.GetString(0);
|
||||
var docTenant = reader.IsDBNull(1) ? null : reader.GetString(1);
|
||||
var contentJson = reader.GetString(2);
|
||||
var createdAt = reader.GetDateTime(3);
|
||||
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(contentJson);
|
||||
var guardResult = _guard.Validate(doc.RootElement);
|
||||
|
||||
foreach (var violation in guardResult.Violations)
|
||||
{
|
||||
result.Violations.Add(new DocumentViolation
|
||||
{
|
||||
DocumentId = docId,
|
||||
Collection = "excititor.vex_documents",
|
||||
Code = violation.Code.ToErrorCode(),
|
||||
Path = violation.Path,
|
||||
Message = violation.Message,
|
||||
Tenant = docTenant,
|
||||
DocumentTimestamp = new DateTimeOffset(createdAt, TimeSpan.Zero)
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
result.Violations.Add(new DocumentViolation
|
||||
{
|
||||
DocumentId = docId,
|
||||
Collection = "excititor.vex_documents",
|
||||
Code = "ERR_AOC_PARSE",
|
||||
Path = "/",
|
||||
Message = "Document content is not valid JSON",
|
||||
Tenant = docTenant,
|
||||
DocumentTimestamp = new DateTimeOffset(createdAt, TimeSpan.Zero)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (PostgresException ex) when (ex.SqlState == "42P01") // relation does not exist
|
||||
{
|
||||
// Table doesn't exist - this is okay for fresh installations
|
||||
Console.WriteLine("Note: excititor.vex_documents table not found (may not be initialized)");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<AssemblyName>stella-aoc</AssemblyName>
|
||||
<RootNamespace>StellaOps.Aoc.Cli</RootNamespace>
|
||||
<Description>StellaOps AOC CLI - Verify append-only contract compliance in advisory databases</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.0" />
|
||||
<PackageReference Include="Npgsql" Version="9.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\__Libraries\StellaOps.Aoc\StellaOps.Aoc.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,188 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using StellaOps.Aoc.Cli.Models;
|
||||
using StellaOps.Aoc.Cli.Services;
|
||||
|
||||
namespace StellaOps.Aoc.Cli.Tests;
|
||||
|
||||
public sealed class AocVerificationServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public void VerifyOptions_RequiredProperties_AreSet()
|
||||
{
|
||||
var options = new VerifyOptions
|
||||
{
|
||||
Since = "2025-12-01",
|
||||
PostgresConnectionString = "Host=localhost;Database=test",
|
||||
Verbose = true
|
||||
};
|
||||
|
||||
Assert.Equal("2025-12-01", options.Since);
|
||||
Assert.Equal("Host=localhost;Database=test", options.PostgresConnectionString);
|
||||
Assert.True(options.Verbose);
|
||||
Assert.False(options.DryRun);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerificationResult_Status_ReturnsPass_WhenNoViolations()
|
||||
{
|
||||
var result = new VerificationResult
|
||||
{
|
||||
Since = "2025-12-01"
|
||||
};
|
||||
|
||||
Assert.Equal("PASS", result.Status);
|
||||
Assert.Equal(0, result.ViolationCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerificationResult_Status_ReturnsFail_WhenViolationsExist()
|
||||
{
|
||||
var result = new VerificationResult
|
||||
{
|
||||
Since = "2025-12-01",
|
||||
Violations =
|
||||
{
|
||||
new DocumentViolation
|
||||
{
|
||||
DocumentId = "doc-1",
|
||||
Collection = "test",
|
||||
Code = "ERR_AOC_001",
|
||||
Path = "/severity",
|
||||
Message = "Forbidden field"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Assert.Equal("FAIL", result.Status);
|
||||
Assert.Equal(1, result.ViolationCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DocumentViolation_Serializes_ToExpectedJson()
|
||||
{
|
||||
var violation = new DocumentViolation
|
||||
{
|
||||
DocumentId = "doc-123",
|
||||
Collection = "advisory_raw",
|
||||
Code = "ERR_AOC_001",
|
||||
Path = "/severity",
|
||||
Message = "Field 'severity' is forbidden",
|
||||
Tenant = "tenant-1"
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(violation, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
});
|
||||
|
||||
Assert.Contains("\"documentId\":\"doc-123\"", json);
|
||||
Assert.Contains("\"collection\":\"advisory_raw\"", json);
|
||||
Assert.Contains("\"code\":\"ERR_AOC_001\"", json);
|
||||
Assert.Contains("\"path\":\"/severity\"", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerificationResult_Serializes_WithAllFields()
|
||||
{
|
||||
var result = new VerificationResult
|
||||
{
|
||||
Since = "abc123",
|
||||
Tenant = "tenant-1",
|
||||
DocumentsScanned = 100,
|
||||
DurationMs = 500,
|
||||
Violations =
|
||||
{
|
||||
new DocumentViolation
|
||||
{
|
||||
DocumentId = "doc-1",
|
||||
Collection = "test",
|
||||
Code = "ERR_AOC_001",
|
||||
Path = "/severity",
|
||||
Message = "Forbidden"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(result, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
});
|
||||
|
||||
Assert.Contains("\"since\":\"abc123\"", json);
|
||||
Assert.Contains("\"tenant\":\"tenant-1\"", json);
|
||||
Assert.Contains("\"documentsScanned\":100", json);
|
||||
Assert.Contains("\"violationCount\":1", json);
|
||||
Assert.Contains("\"status\":\"FAIL\"", json);
|
||||
Assert.Contains("\"durationMs\":500", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyOptions_PostgresConnectionString_IsRequired()
|
||||
{
|
||||
var options = new VerifyOptions
|
||||
{
|
||||
Since = "HEAD~1",
|
||||
PostgresConnectionString = "Host=localhost;Database=test"
|
||||
};
|
||||
|
||||
Assert.NotNull(options.PostgresConnectionString);
|
||||
Assert.Equal("Host=localhost;Database=test", options.PostgresConnectionString);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyOptions_DryRun_DefaultsToFalse()
|
||||
{
|
||||
var options = new VerifyOptions
|
||||
{
|
||||
Since = "2025-01-01",
|
||||
PostgresConnectionString = "Host=localhost;Database=test"
|
||||
};
|
||||
|
||||
Assert.False(options.DryRun);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyOptions_Verbose_DefaultsToFalse()
|
||||
{
|
||||
var options = new VerifyOptions
|
||||
{
|
||||
Since = "2025-01-01",
|
||||
PostgresConnectionString = "Host=localhost;Database=test"
|
||||
};
|
||||
|
||||
Assert.False(options.Verbose);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerificationResult_ViolationCount_MatchesListCount()
|
||||
{
|
||||
var result = new VerificationResult
|
||||
{
|
||||
Since = "test"
|
||||
};
|
||||
|
||||
Assert.Equal(0, result.ViolationCount);
|
||||
|
||||
result.Violations.Add(new DocumentViolation
|
||||
{
|
||||
DocumentId = "1",
|
||||
Collection = "test",
|
||||
Code = "ERR",
|
||||
Path = "/",
|
||||
Message = "msg"
|
||||
});
|
||||
|
||||
Assert.Equal(1, result.ViolationCount);
|
||||
|
||||
result.Violations.Add(new DocumentViolation
|
||||
{
|
||||
DocumentId = "2",
|
||||
Collection = "test",
|
||||
Code = "ERR",
|
||||
Path = "/",
|
||||
Message = "msg"
|
||||
});
|
||||
|
||||
Assert.Equal(2, result.ViolationCount);
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\StellaOps.Aoc.Cli\StellaOps.Aoc.Cli.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user