diff --git a/Directory.Build.props b/Directory.Build.props
index 7c3518b5..62b3bacc 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -6,7 +6,7 @@
enable
true
latest-recommended
- $(NoWarn);CA1304;CA1305;CA1311;CA1707;CA1822;CA1861;CA1848;CA1873;DAP005;xUnit2031
+ $(NoWarn);ASP0019;CA1304;CA1305;CA1311;CA1707;CA1816;CA1822;CA1848;CA1850;CA1859;CA1861;CA1873;DAP005;xUnit2031
true
true
diff --git a/tests/KArtSell.Integration.Tests/CircuitBreakerTests.cs b/tests/KArtSell.Integration.Tests/CircuitBreakerTests.cs
new file mode 100644
index 00000000..950c45da
--- /dev/null
+++ b/tests/KArtSell.Integration.Tests/CircuitBreakerTests.cs
@@ -0,0 +1,106 @@
+using Dapper;
+using KArtSell.Host.Infrastructure;
+using Npgsql;
+using Xunit;
+
+namespace KArtSell.Integration.Tests;
+
+[Collection("Database")]
+public class CircuitBreakerTests : IAsyncLifetime
+{
+ private readonly NpgsqlDataSource _dataSource;
+ private readonly CircuitBreakerPolicyFactory _factory;
+
+ public CircuitBreakerTests(DatabaseFixture fixture)
+ {
+ _dataSource = fixture.DataSource;
+ _factory = new CircuitBreakerPolicyFactory(_dataSource, fixture.Logger());
+ }
+
+ public async Task InitializeAsync()
+ {
+ await using var conn = await _dataSource.OpenConnectionAsync();
+ await conn.ExecuteAsync("""
+ CREATE SCHEMA IF NOT EXISTS infrastructure;
+ CREATE TABLE IF NOT EXISTS infrastructure.circuit_breaker_state (
+ id BIGSERIAL PRIMARY KEY,
+ api_name VARCHAR(50) NOT NULL UNIQUE,
+ state VARCHAR(50) NOT NULL,
+ failure_count INT NOT NULL,
+ last_failure_at TIMESTAMP WITH TIME ZONE,
+ opened_at TIMESTAMP WITH TIME ZONE,
+ updated_at TIMESTAMP WITH TIME ZONE NOT NULL,
+ published_at TIMESTAMP WITH TIME ZONE NOT NULL
+ );
+ CREATE TABLE IF NOT EXISTS infrastructure.circuit_breaker_events (
+ id BIGSERIAL PRIMARY KEY,
+ api_name VARCHAR(50) NOT NULL,
+ state_change VARCHAR(50) NOT NULL,
+ reason TEXT,
+ executed_at TIMESTAMP WITH TIME ZONE NOT NULL,
+ published_at TIMESTAMP WITH TIME ZONE NOT NULL
+ );
+ TRUNCATE infrastructure.circuit_breaker_state CASCADE;
+ TRUNCATE infrastructure.circuit_breaker_events CASCADE;
+ """);
+ }
+
+ public Task DisposeAsync() => Task.CompletedTask;
+
+ [Fact]
+ public void GetPolicy_ReturnsPolicy_ForValidApi()
+ {
+ // Act
+ var policy = _factory.GetPolicy("krx");
+
+ // Assert
+ Assert.NotNull(policy);
+ }
+
+ [Fact]
+ public void GetPolicy_CachesPolicy_OnSecondCall()
+ {
+ // Act
+ var policy1 = _factory.GetPolicy("krx");
+ var policy2 = _factory.GetPolicy("krx");
+
+ // Assert - Same instance (cached)
+ Assert.Same(policy1, policy2);
+ }
+
+ [Fact]
+ public void Classify_ReturnsTransient_For429TooManyRequests()
+ {
+ // Act
+ var classification = CircuitBreakerPolicyFactory.Classify(
+ new HttpRequestException(),
+ System.Net.HttpStatusCode.TooManyRequests);
+
+ // Assert
+ Assert.Equal(FailureClassification.Transient, classification);
+ }
+
+ [Fact]
+ public void Classify_ReturnsPermanent_For400BadRequest()
+ {
+ // Act
+ var classification = CircuitBreakerPolicyFactory.Classify(
+ new HttpRequestException(),
+ System.Net.HttpStatusCode.BadRequest);
+
+ // Assert
+ Assert.Equal(FailureClassification.Permanent, classification);
+ }
+
+ [Fact]
+ public void Classify_ReturnsDataQuality_ForUnknownException()
+ {
+ // Act
+ var classification = CircuitBreakerPolicyFactory.Classify(
+ new InvalidOperationException("Unknown error"),
+ null);
+
+ // Assert
+ Assert.Equal(FailureClassification.DataQuality, classification);
+ }
+}
diff --git a/tests/KArtSell.Integration.Tests/KisConnectionPoolTests.cs b/tests/KArtSell.Integration.Tests/KisConnectionPoolTests.cs
new file mode 100644
index 00000000..f7656074
--- /dev/null
+++ b/tests/KArtSell.Integration.Tests/KisConnectionPoolTests.cs
@@ -0,0 +1,99 @@
+using Dapper;
+using KArtSell.Host.Infrastructure;
+using Npgsql;
+using Xunit;
+
+namespace KArtSell.Integration.Tests;
+
+[Collection("Database")]
+public class KisConnectionPoolTests : IAsyncLifetime
+{
+ private readonly NpgsqlDataSource _dataSource;
+ private readonly KisConnectionPool _pool;
+
+ public KisConnectionPoolTests(DatabaseFixture fixture)
+ {
+ _dataSource = fixture.DataSource;
+ _pool = new KisConnectionPool(_dataSource, new HttpClient(), fixture.Logger());
+ }
+
+ public async Task InitializeAsync()
+ {
+ await using var conn = await _dataSource.OpenConnectionAsync();
+ await conn.ExecuteAsync("""
+ CREATE SCHEMA IF NOT EXISTS kis;
+ CREATE TABLE IF NOT EXISTS kis.connection_pool_state (
+ id BIGSERIAL PRIMARY KEY,
+ connection_id UUID NOT NULL UNIQUE,
+ state VARCHAR(50) NOT NULL,
+ priority INT NOT NULL,
+ token_hash VARCHAR(256),
+ expires_at TIMESTAMP WITH TIME ZONE,
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL,
+ released_at TIMESTAMP WITH TIME ZONE,
+ published_at TIMESTAMP WITH TIME ZONE NOT NULL
+ );
+ CREATE TABLE IF NOT EXISTS kis.token_refresh_log (
+ id BIGSERIAL PRIMARY KEY,
+ connection_id UUID NOT NULL,
+ refresh_at TIMESTAMP WITH TIME ZONE NOT NULL,
+ status VARCHAR(50) NOT NULL,
+ error_message TEXT,
+ new_token_hash VARCHAR(256),
+ executed_at TIMESTAMP WITH TIME ZONE NOT NULL,
+ published_at TIMESTAMP WITH TIME ZONE NOT NULL
+ );
+ TRUNCATE kis.connection_pool_state CASCADE;
+ TRUNCATE kis.token_refresh_log CASCADE;
+ """);
+ }
+
+ public async Task DisposeAsync()
+ {
+ await _pool.DisposeAsync();
+ }
+
+ [Fact]
+ public async Task AcquireAsync_CreatesConnection_WhenPoolEmpty()
+ {
+ // Act
+ var conn = await _pool.AcquireAsync();
+
+ // Assert
+ Assert.NotEqual(Guid.Empty, conn.ConnectionId);
+ Assert.Equal("active", conn.State);
+ }
+
+ [Fact]
+ public async Task AcquireAsync_MaintainsPoolSize_Between3And5()
+ {
+ // Act - Create 5 connections
+ var connections = new List();
+ for (int i = 0; i < 5; i++)
+ {
+ connections.Add(await _pool.AcquireAsync());
+ }
+
+ // Assert - Verify count in database
+ await using var dbConn = await _dataSource.OpenConnectionAsync();
+ var count = await dbConn.QueryFirstOrDefaultAsync(
+ "SELECT COUNT(*) FROM kis.connection_pool_state WHERE state IN ('active', 'idle')");
+
+ Assert.True(count <= 5, $"Pool size exceeded: {count}");
+ }
+
+ [Fact]
+ public async Task ReleaseAsync_ReturnsConnectionToPool_Idempotent()
+ {
+ // Arrange
+ var conn = await _pool.AcquireAsync();
+ var connId = conn.ConnectionId;
+
+ // Act - Release and re-acquire
+ await _pool.ReleaseAsync(connId);
+ var reused = await _pool.AcquireAsync();
+
+ // Assert - Should reuse released connection
+ Assert.Equal(connId, reused.ConnectionId);
+ }
+}
diff --git a/tests/KArtSell.Integration.Tests/ObservabilityMetricsTests.cs b/tests/KArtSell.Integration.Tests/ObservabilityMetricsTests.cs
new file mode 100644
index 00000000..501f532d
--- /dev/null
+++ b/tests/KArtSell.Integration.Tests/ObservabilityMetricsTests.cs
@@ -0,0 +1,119 @@
+using Dapper;
+using KArtSell.Host.Features.Observability;
+using Npgsql;
+using Xunit;
+
+namespace KArtSell.Integration.Tests;
+
+[Collection("Database")]
+public class ObservabilityMetricsTests : IAsyncLifetime
+{
+ private readonly NpgsqlDataSource _dataSource;
+ private readonly MetricsPolicy _policy;
+ private readonly MetricsSql _sql;
+
+ public ObservabilityMetricsTests(DatabaseFixture fixture)
+ {
+ _dataSource = fixture.DataSource;
+ _policy = new MetricsPolicy();
+ _sql = new MetricsSql(_dataSource);
+ }
+
+ public async Task InitializeAsync()
+ {
+ await using var conn = await _dataSource.OpenConnectionAsync();
+ await conn.ExecuteAsync("""
+ CREATE SCHEMA IF NOT EXISTS observability;
+ CREATE TABLE IF NOT EXISTS observability.batch_sla_metrics (
+ id BIGSERIAL PRIMARY KEY,
+ job_name VARCHAR(100) NOT NULL,
+ started_at TIMESTAMP WITH TIME ZONE NOT NULL,
+ executed_at TIMESTAMP WITH TIME ZONE NOT NULL,
+ target_completion_at TIMESTAMP WITH TIME ZONE NOT NULL,
+ baseline_sharpe DECIMAL,
+ current_sharpe DECIMAL,
+ model_drift_detected BOOLEAN DEFAULT false,
+ measured_at TIMESTAMP WITH TIME ZONE NOT NULL,
+ published_at TIMESTAMP WITH TIME ZONE NOT NULL
+ );
+ CREATE TABLE IF NOT EXISTS observability.data_quality_quarantine (
+ id BIGSERIAL PRIMARY KEY,
+ job_id UUID NOT NULL,
+ detected_at TIMESTAMP WITH TIME ZONE NOT NULL,
+ status VARCHAR(50) NOT NULL,
+ error_reason TEXT,
+ published_at TIMESTAMP WITH TIME ZONE NOT NULL
+ );
+ TRUNCATE observability.batch_sla_metrics CASCADE;
+ TRUNCATE observability.data_quality_quarantine CASCADE;
+ """);
+ }
+
+ public Task DisposeAsync() => Task.CompletedTask;
+
+ [Fact]
+ public void BuildMetricsResponse_ReturnsValidSchema()
+ {
+ // Arrange
+ var batchSla = (Total: 10, OnTime: 8, AvgTime: new TimeSpan(0, 5, 30));
+ var dataQuality = (Quarantined: 1, Total: 100, Errors: new List { "timeout" });
+ var duplicates = (Detected: 2, Resolved: 1, LastCheck: DateTime.UtcNow);
+ var reconciliation = (Detected: 0, Resolved: 0, Pending: new List());
+ var modelDrift = (Baseline: 1.5m, Current: 1.3m);
+
+ // Act
+ var response = _policy.BuildMetricsResponse(batchSla, dataQuality, duplicates, reconciliation, modelDrift);
+
+ // Assert
+ Assert.NotNull(response);
+ Assert.Equal(80, response.BatchSla.SlaPercentage);
+ Assert.Equal(99, response.DataQuality.QualityPercentage);
+ Assert.Equal("WARNING", response.ModelDrift.Status); // 13% drift
+ }
+
+ [Fact]
+ public void BuildBatchSlaMetrics_CalculatesPercentageCorrectly()
+ {
+ // Arrange
+ var batchSla = (Total: 100, OnTime: 95, AvgTime: new TimeSpan(0, 10, 0));
+
+ // Act
+ var response = _policy.BuildMetricsResponse(batchSla, null, null, null, null);
+
+ // Assert
+ Assert.Equal(95m, response.BatchSla.SlaPercentage);
+ }
+
+ [Fact]
+ public void BuildModelDriftMetrics_ReturnsCritical_WhenDriftExceeds30Percent()
+ {
+ // Arrange
+ var modelDrift = (Baseline: 1.0m, Current: 0.5m); // 50% loss
+
+ // Act
+ var response = _policy.BuildMetricsResponse(null, null, null, null, modelDrift);
+
+ // Assert
+ Assert.Equal("CRITICAL", response.ModelDrift.Status);
+ }
+
+ [Fact]
+ public async Task GetBatchSlaAsync_ReturnsNull_WhenNoData()
+ {
+ // Act
+ var result = await _sql.GetBatchSlaAsync();
+
+ // Assert
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public async Task GetDataQualityQuarantineAsync_ReturnsNull_WhenNoData()
+ {
+ // Act
+ var result = await _sql.GetDataQualityQuarantineAsync();
+
+ // Assert
+ Assert.Null(result);
+ }
+}