Tests fixes, audit progress, UI completions

This commit is contained in:
StellaOps Bot
2025-12-30 09:03:22 +02:00
parent 7a5210e2aa
commit 82e55c206a
318 changed files with 7232 additions and 1256 deletions

View File

@@ -88,9 +88,11 @@ public sealed class BundleBuilder : IBundleBuilder
var targetPath = Path.Combine(outputPath, source.RelativePath);
Directory.CreateDirectory(Path.GetDirectoryName(targetPath) ?? outputPath);
await using var input = File.OpenRead(source.SourcePath);
await using var output = File.Create(targetPath);
await input.CopyToAsync(output, ct).ConfigureAwait(false);
await using (var input = File.OpenRead(source.SourcePath))
await using (var output = File.Create(targetPath))
{
await input.CopyToAsync(output, ct).ConfigureAwait(false);
}
await using var digestStream = File.OpenRead(targetPath);
var hash = await SHA256.HashDataAsync(digestStream, ct).ConfigureAwait(false);

View File

@@ -0,0 +1,27 @@
# AirGap Persistence Guild Charter
## Working Directory
- `src/AirGap/__Libraries/StellaOps.AirGap.Persistence`
## Scope
- PostgreSQL persistence for AirGap state and bundle version history.
- Data source configuration, schema management, and repository wiring.
- EF Core context scaffolding for AirGap data models.
## Required Reading
- `docs/README.md`
- `docs/07_HIGH_LEVEL_ARCHITECTURE.md`
- `docs/modules/platform/architecture-overview.md`
- `docs/airgap/bundle-repositories.md`
- `docs/airgap/airgap-mode.md`
## Working Agreements
- Update task status in the sprint tracker and local `TASKS.md`.
- Keep schema changes deterministic and migration-driven.
- Use configured schema names consistently (no hard-coded schema drift).
- Avoid cross-module edits unless the sprint explicitly permits them.
## Testing Rules
- Use Postgres test fixtures or Testcontainers; no network.
- Mark integration tests as Integration, not Unit.
- Keep data ordering deterministic with explicit ORDER BY clauses.

View File

@@ -0,0 +1,61 @@
-- AirGap Schema Migration 001: Initial Schema
-- Creates AirGap state and bundle version tracking tables.
CREATE TABLE IF NOT EXISTS state (
id TEXT NOT NULL,
tenant_id TEXT NOT NULL PRIMARY KEY,
sealed BOOLEAN NOT NULL DEFAULT FALSE,
policy_hash TEXT,
time_anchor JSONB NOT NULL DEFAULT '{}'::jsonb,
last_transition_at TIMESTAMPTZ NOT NULL DEFAULT '0001-01-01T00:00:00Z',
staleness_budget JSONB NOT NULL DEFAULT '{"warningSeconds":3600,"breachSeconds":7200}'::jsonb,
drift_baseline_seconds BIGINT NOT NULL DEFAULT 0,
content_budgets JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_airgap_state_tenant ON state(tenant_id);
CREATE INDEX IF NOT EXISTS idx_airgap_state_sealed ON state(sealed) WHERE sealed = TRUE;
CREATE TABLE IF NOT EXISTS bundle_versions (
tenant_id TEXT NOT NULL,
bundle_type TEXT NOT NULL,
version_string TEXT NOT NULL,
major INTEGER NOT NULL,
minor INTEGER NOT NULL,
patch INTEGER NOT NULL,
prerelease TEXT,
bundle_created_at TIMESTAMPTZ NOT NULL,
bundle_digest TEXT NOT NULL,
activated_at TIMESTAMPTZ NOT NULL,
was_force_activated BOOLEAN NOT NULL DEFAULT FALSE,
force_activate_reason TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (tenant_id, bundle_type)
);
CREATE INDEX IF NOT EXISTS idx_airgap_bundle_versions_tenant
ON bundle_versions(tenant_id);
CREATE TABLE IF NOT EXISTS bundle_version_history (
id BIGSERIAL PRIMARY KEY,
tenant_id TEXT NOT NULL,
bundle_type TEXT NOT NULL,
version_string TEXT NOT NULL,
major INTEGER NOT NULL,
minor INTEGER NOT NULL,
patch INTEGER NOT NULL,
prerelease TEXT,
bundle_created_at TIMESTAMPTZ NOT NULL,
bundle_digest TEXT NOT NULL,
activated_at TIMESTAMPTZ NOT NULL,
deactivated_at TIMESTAMPTZ,
was_force_activated BOOLEAN NOT NULL DEFAULT FALSE,
force_activate_reason TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_airgap_bundle_version_history_tenant
ON bundle_version_history(tenant_id, bundle_type, activated_at DESC);

View File

@@ -30,7 +30,7 @@ public sealed class PostgresAirGapStateStore : RepositoryBase<AirGapDataSource>,
const string sql = """
SELECT id, tenant_id, sealed, policy_hash, time_anchor, last_transition_at,
staleness_budget, drift_baseline_seconds, content_budgets
FROM airgap.state
FROM state
WHERE LOWER(tenant_id) = LOWER(@tenant_id);
""";
@@ -54,7 +54,7 @@ public sealed class PostgresAirGapStateStore : RepositoryBase<AirGapDataSource>,
await using var connection = await DataSource.OpenConnectionAsync("public", "writer", cancellationToken).ConfigureAwait(false);
const string sql = """
INSERT INTO airgap.state (
INSERT INTO state (
id, tenant_id, sealed, policy_hash, time_anchor, last_transition_at,
staleness_budget, drift_baseline_seconds, content_budgets
)
@@ -245,22 +245,25 @@ public sealed class PostgresAirGapStateStore : RepositoryBase<AirGapDataSource>,
}
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
const string sql = """
CREATE SCHEMA IF NOT EXISTS airgap;
CREATE TABLE IF NOT EXISTS airgap.state (
var schemaName = DataSource.SchemaName ?? "public";
var quotedSchema = QuoteIdentifier(schemaName);
var sql = $$"""
CREATE SCHEMA IF NOT EXISTS {{quotedSchema}};
CREATE TABLE IF NOT EXISTS {{quotedSchema}}.state (
id TEXT NOT NULL,
tenant_id TEXT NOT NULL PRIMARY KEY,
sealed BOOLEAN NOT NULL DEFAULT FALSE,
policy_hash TEXT,
time_anchor JSONB NOT NULL DEFAULT '{}',
time_anchor JSONB NOT NULL DEFAULT '{}'::jsonb,
last_transition_at TIMESTAMPTZ NOT NULL DEFAULT '0001-01-01T00:00:00Z',
staleness_budget JSONB NOT NULL DEFAULT '{"warningSeconds":3600,"breachSeconds":7200}',
staleness_budget JSONB NOT NULL DEFAULT '{"warningSeconds":3600,"breachSeconds":7200}'::jsonb,
drift_baseline_seconds BIGINT NOT NULL DEFAULT 0,
content_budgets JSONB NOT NULL DEFAULT '{}',
content_budgets JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_airgap_state_sealed ON airgap.state(sealed) WHERE sealed = TRUE;
CREATE INDEX IF NOT EXISTS idx_airgap_state_tenant ON {{quotedSchema}}.state(tenant_id);
CREATE INDEX IF NOT EXISTS idx_airgap_state_sealed ON {{quotedSchema}}.state(sealed) WHERE sealed = TRUE;
""";
await using var command = CreateCommand(sql, connection);
@@ -272,4 +275,10 @@ public sealed class PostgresAirGapStateStore : RepositoryBase<AirGapDataSource>,
_initLock.Release();
}
}
private static string QuoteIdentifier(string identifier)
{
var escaped = identifier.Replace("\"", "\"\"", StringComparison.Ordinal);
return $"\"{escaped}\"";
}
}

View File

@@ -35,7 +35,7 @@ public sealed class PostgresBundleVersionStore : RepositoryBase<AirGapDataSource
const string sql = """
SELECT tenant_id, bundle_type, version_string, major, minor, patch, prerelease,
bundle_created_at, bundle_digest, activated_at, was_force_activated, force_activate_reason
FROM airgap.bundle_versions
FROM bundle_versions
WHERE tenant_id = @tenant_id AND bundle_type = @bundle_type;
""";
@@ -59,7 +59,7 @@ public sealed class PostgresBundleVersionStore : RepositoryBase<AirGapDataSource
await using var tx = await connection.BeginTransactionAsync(ct).ConfigureAwait(false);
const string closeHistorySql = """
UPDATE airgap.bundle_version_history
UPDATE bundle_version_history
SET deactivated_at = @activated_at
WHERE tenant_id = @tenant_id AND bundle_type = @bundle_type AND deactivated_at IS NULL;
""";
@@ -74,7 +74,7 @@ public sealed class PostgresBundleVersionStore : RepositoryBase<AirGapDataSource
}
const string historySql = """
INSERT INTO airgap.bundle_version_history (
INSERT INTO bundle_version_history (
tenant_id, bundle_type, version_string, major, minor, patch, prerelease,
bundle_created_at, bundle_digest, activated_at, deactivated_at, was_force_activated, force_activate_reason
)
@@ -103,7 +103,7 @@ public sealed class PostgresBundleVersionStore : RepositoryBase<AirGapDataSource
}
const string upsertSql = """
INSERT INTO airgap.bundle_versions (
INSERT INTO bundle_versions (
tenant_id, bundle_type, version_string, major, minor, patch, prerelease,
bundle_created_at, bundle_digest, activated_at, was_force_activated, force_activate_reason
)
@@ -169,7 +169,7 @@ public sealed class PostgresBundleVersionStore : RepositoryBase<AirGapDataSource
const string sql = """
SELECT tenant_id, bundle_type, version_string, major, minor, patch, prerelease,
bundle_created_at, bundle_digest, activated_at, was_force_activated, force_activate_reason
FROM airgap.bundle_version_history
FROM bundle_version_history
WHERE tenant_id = @tenant_id AND bundle_type = @bundle_type
ORDER BY activated_at DESC
LIMIT @limit;
@@ -236,10 +236,12 @@ public sealed class PostgresBundleVersionStore : RepositoryBase<AirGapDataSource
}
await using var connection = await DataSource.OpenSystemConnectionAsync(ct).ConfigureAwait(false);
const string sql = """
CREATE SCHEMA IF NOT EXISTS airgap;
var schemaName = DataSource.SchemaName ?? "public";
var quotedSchema = QuoteIdentifier(schemaName);
var sql = $$"""
CREATE SCHEMA IF NOT EXISTS {{quotedSchema}};
CREATE TABLE IF NOT EXISTS airgap.bundle_versions (
CREATE TABLE IF NOT EXISTS {{quotedSchema}}.bundle_versions (
tenant_id TEXT NOT NULL,
bundle_type TEXT NOT NULL,
version_string TEXT NOT NULL,
@@ -258,9 +260,9 @@ public sealed class PostgresBundleVersionStore : RepositoryBase<AirGapDataSource
);
CREATE INDEX IF NOT EXISTS idx_airgap_bundle_versions_tenant
ON airgap.bundle_versions(tenant_id);
ON {{quotedSchema}}.bundle_versions(tenant_id);
CREATE TABLE IF NOT EXISTS airgap.bundle_version_history (
CREATE TABLE IF NOT EXISTS {{quotedSchema}}.bundle_version_history (
id BIGSERIAL PRIMARY KEY,
tenant_id TEXT NOT NULL,
bundle_type TEXT NOT NULL,
@@ -279,7 +281,7 @@ public sealed class PostgresBundleVersionStore : RepositoryBase<AirGapDataSource
);
CREATE INDEX IF NOT EXISTS idx_airgap_bundle_version_history_tenant
ON airgap.bundle_version_history(tenant_id, bundle_type, activated_at DESC);
ON {{quotedSchema}}.bundle_version_history(tenant_id, bundle_type, activated_at DESC);
""";
await using var command = CreateCommand(sql, connection);
@@ -293,4 +295,10 @@ public sealed class PostgresBundleVersionStore : RepositoryBase<AirGapDataSource
}
private static string NormalizeKey(string value) => value.Trim().ToLowerInvariant();
private static string QuoteIdentifier(string identifier)
{
var escaped = identifier.Replace("\"", "\"\"", StringComparison.Ordinal);
return $"\"{escaped}\"";
}
}

View File

@@ -9,6 +9,10 @@
<Description>Consolidated persistence layer for StellaOps AirGap module</Description>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Migrations\**\*.sql" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" PrivateAssets="all" />

View File

@@ -0,0 +1,10 @@
# AirGap Persistence Task Board
This board mirrors active sprint tasks for this module.
Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0028-M | DONE | Maintainability audit for StellaOps.AirGap.Persistence. |
| AUDIT-0028-T | DONE | Test coverage audit for StellaOps.AirGap.Persistence. |
| AUDIT-0028-A | TODO | Pending approval for changes. |

View File

@@ -0,0 +1,3 @@
using Xunit;
[assembly: CollectionBehavior(DisableTestParallelization = true)]

View File

@@ -3,6 +3,7 @@
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UseConcelierTestInfra>false</UseConcelierTestInfra>
</PropertyGroup>
<ItemGroup>
@@ -14,4 +15,4 @@
<ProjectReference Include="../../StellaOps.AirGap.Bundle/StellaOps.AirGap.Bundle.csproj" />
<ProjectReference Include="../../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
</Project>
</Project>