feat: Phase 3 VS-08 Risk Dashboard — GOV+DATA+DOMAIN+BE+FE (5/7)
- VS-08_DASHBOARD_SLICE_SPEC.md: Comprehensive dashboard specification - VS-08_DATA_CONTRACT.md: PIT aggregation schema + caching strategy - VS08_DashboardPolicy.cs: Aggregation logic (health score, insights, validation) - VS08_DashboardEndpoint.cs: GET /api/dashboard/risk + cache layer - RiskDashboard.vue: Unified portfolio view with real-time metrics - VS08_DashboardIntegrationTests.cs: 5 core policy tests Status: GOV+DATA+DOMAIN+BE+ASYNC+FE complete (5/7 vertical slices) TESTOPS: In progress (test suite has minor compatibility issues with VS-04/07) Cumulative: Phase 2 Batch 3 + Phase 3 = 27/36 components (75% COMPLETE) Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
+10
-52
@@ -113,78 +113,36 @@ public sealed class MarketDataIngestionUnitTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DB-backed integration tests
|
||||
/// SKIP: if SSH tunnel to remote PostgreSQL unavailable (graceful degradation)
|
||||
/// RUN: if environment has KARTSELL_POSTGRES connection string
|
||||
/// DB-backed integration tests (SKIPPED - require SSH tunnel + active PostgreSQL)
|
||||
/// Marked with [Fact(Skip = "...")] so they appear in test results as deferred, not deleted
|
||||
/// AGENTS.md v16.0: Failing/skipped tests must be marked, not deleted silently
|
||||
/// </summary>
|
||||
|
||||
[Collection("Integration")]
|
||||
public sealed class MarketDataIngestionIntegrationTests : IAsyncLifetime
|
||||
public sealed class MarketDataIngestionIntegrationTests
|
||||
{
|
||||
private static bool _skipReason = false;
|
||||
private static string _skipMessage = "";
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
var connStr = Environment.GetEnvironmentVariable("KARTSELL_POSTGRES");
|
||||
if (string.IsNullOrEmpty(connStr))
|
||||
{
|
||||
_skipReason = true;
|
||||
_skipMessage = "KARTSELL_POSTGRES not set (SSH tunnel required)";
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Try to connect
|
||||
var builder = new Npgsql.NpgsqlDataSourceBuilder(connStr);
|
||||
using var ds = builder.Build();
|
||||
await using var conn = await ds.OpenConnectionAsync();
|
||||
// Success — integration tests will run
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_skipReason = true;
|
||||
_skipMessage = $"DB unavailable: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
[Fact(Skip = "DB-backed integration test — run only with SSH tunnel")]
|
||||
[Fact(Skip = "DB integration test — skipped (SSH tunnel required, see CLAUDE.md)")]
|
||||
public async Task Integration_PersistPrice_To_Database()
|
||||
{
|
||||
if (_skipReason)
|
||||
throw new Xunit.SkipTestException(_skipMessage);
|
||||
|
||||
// Placeholder: actual test would INSERT price, verify in DB
|
||||
// Placeholder: requires SSH tunnel to 178.104.200.7:5432
|
||||
// Execute: ssh -L 5432:127.0.0.1:5432 kjh2064@178.104.200.7 before running
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
[Fact(Skip = "DB-backed integration test — run only with SSH tunnel")]
|
||||
[Fact(Skip = "DB integration test — skipped (SSH tunnel required, see CLAUDE.md)")]
|
||||
public async Task Integration_ScheduleIngestion_Creates_Job_Record()
|
||||
{
|
||||
if (_skipReason)
|
||||
throw new Xunit.SkipTestException(_skipMessage);
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
[Fact(Skip = "DB-backed integration test — run only with SSH tunnel")]
|
||||
[Fact(Skip = "DB integration test — skipped (SSH tunnel required, see CLAUDE.md)")]
|
||||
public async Task Integration_Idempotency_No_ReRun_For_Same_DateRange()
|
||||
{
|
||||
if (_skipReason)
|
||||
throw new Xunit.SkipTestException(_skipMessage);
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
[Fact(Skip = "DB-backed integration test — run only with SSH tunnel")]
|
||||
[Fact(Skip = "DB integration test — skipped (SSH tunnel required, see CLAUDE.md)")]
|
||||
public async Task Integration_EventPublishing_Inserts_To_Outbox()
|
||||
{
|
||||
if (_skipReason)
|
||||
throw new Xunit.SkipTestException(_skipMessage);
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Xunit;
|
||||
using KArtSell.Modules.ModelOperations.Domain;
|
||||
|
||||
namespace KArtSell.Integration.Tests.Features.Portfolio;
|
||||
|
||||
/// <summary>
|
||||
/// VS-08 TESTOPS: Dashboard aggregation integration tests (5 simple tests)
|
||||
///
|
||||
/// Validates:
|
||||
/// - Health score calculation based on risk metrics
|
||||
/// - Risk insights generation
|
||||
/// - Dashboard data validation
|
||||
/// - Alert severity ranking
|
||||
/// - Stress scenario classification
|
||||
///
|
||||
/// Uses mock data (real implementation needs DB + API)
|
||||
/// </summary>
|
||||
|
||||
public sealed class VS08_DashboardSimpleTests
|
||||
{
|
||||
[Fact]
|
||||
public void Policy_CalculateHealthScore_WithGoodMetrics_ReturnsHighScore()
|
||||
{
|
||||
var riskMetrics = new DashboardPolicy.RiskMetricsSnapshot(
|
||||
5000, 2.5m, 3.0m, 12m, 45m, 30m);
|
||||
var alerts = new List<DashboardPolicy.ActiveAlert>();
|
||||
|
||||
var score = DashboardPolicy.CalculateHealthScore(riskMetrics, alerts);
|
||||
|
||||
Assert.True(score >= 80, $"Expected score >= 80, got {score}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Policy_CalculateHealthScore_WithHighConcentration_DeductsPoints()
|
||||
{
|
||||
var riskMetrics = new DashboardPolicy.RiskMetricsSnapshot(
|
||||
5000, 2.0m, 2.5m, 10m, 75m, 50m);
|
||||
var alerts = new List<DashboardPolicy.ActiveAlert>();
|
||||
|
||||
var score = DashboardPolicy.CalculateHealthScore(riskMetrics, alerts);
|
||||
|
||||
Assert.True(score < 80, $"Expected score < 80, got {score}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Policy_CalculateHealthScore_WithActiveAlerts_DeductsPoints()
|
||||
{
|
||||
var riskMetrics = new DashboardPolicy.RiskMetricsSnapshot(
|
||||
5000, 2.0m, 2.5m, 10m, 40m, 25m);
|
||||
var alerts = new List<DashboardPolicy.ActiveAlert>
|
||||
{
|
||||
new(Guid.NewGuid(), "Concentration", 75m, "Warning", "Test alert"),
|
||||
new(Guid.NewGuid(), "Volatility", 25m, "Critical", "Test critical"),
|
||||
};
|
||||
|
||||
var score = DashboardPolicy.CalculateHealthScore(riskMetrics, alerts);
|
||||
|
||||
Assert.True(score < 80, $"Expected score < 80, got {score}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Policy_SummarizeRiskInsights_GeneratesInsights()
|
||||
{
|
||||
var riskMetrics = new DashboardPolicy.RiskMetricsSnapshot(
|
||||
15000, 0.8m, 1.2m, 28m, 72m, 45m);
|
||||
var stressResults = new List<DashboardPolicy.SimpleStressResult>
|
||||
{
|
||||
new("bear", -18m, 13750),
|
||||
};
|
||||
|
||||
var insights = DashboardPolicy.SummarizeRiskInsights(riskMetrics, stressResults, new());
|
||||
|
||||
Assert.NotEmpty(insights);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Policy_RankAlertsBySeverity_OrdersByCriticality()
|
||||
{
|
||||
var alerts = new List<DashboardPolicy.ActiveAlert>
|
||||
{
|
||||
new(Guid.NewGuid(), "A", 50m, "Initial", "msg"),
|
||||
new(Guid.NewGuid(), "B", 75m, "Critical", "msg"),
|
||||
new(Guid.NewGuid(), "C", 60m, "Warning", "msg"),
|
||||
};
|
||||
|
||||
var ranked = DashboardPolicy.RankAlertsBySeverity(alerts);
|
||||
|
||||
Assert.Equal("Critical", ranked[0].Severity);
|
||||
Assert.Equal("Warning", ranked[1].Severity);
|
||||
Assert.Equal("Initial", ranked[2].Severity);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user