fix(review): full code-review remediation — 5 High + Medium/Low across 16 modules
Remediation from the full per-module code review at 4307c381 (findings recorded
separately in code-reviews/).
Highs fixed:
- DeploymentManager-025/SiteRuntime-031: stop broadcasting notification lists + SMTP
configs (incl. credentials) to sites; site purges already-persisted rows on apply
(enforces the central-only delivery design; clears plaintext SMTP creds at rest).
- DataConnectionLayer-023: guard the native-alarm subscribe path against the
mid-flight-unsubscribe adapter-feed leak (mirrors the DCL-021 tag-path fix).
- SiteEventLogging-024: normalize From/To query bounds to UTC (the -016 fix the
audit trail claimed but never committed).
- KpiHistory-001: add an in-flight guard to the recorder sample tick.
- ScriptAnalysis-001: harden the trust analyzer's TPA-absent fallback (resolve
forbidden anchors in the minimal reference set; warn on degraded mode) — anchors
added to validation references only, never the compile gate.
(InboundAPI-026 left to the feat/ipsen-movein effort per owner decision.)
Medium/Low: DM-026 deterministic deploy-status tiebreaker; SR-027/028/029/030
native-alarm leak/phantom-active/delete-during-redeploy fixes; AL-013/014/016;
TE-024 (folder-mutation audit rows now persisted)/025; SF-025 gauge-provider
clear-on-stop; ESG-025/026; SEC-023/024/025; SCA-007/008/009; plus doc/test
accuracy COM-023/024, HOST-025/026, HM-024/025, NS-027/028.
Full-solution build 0 warnings; ~3560 tests across 18 touched suites green.
This commit is contained in:
@@ -370,7 +370,7 @@ public class DatabaseGatewayTests
|
||||
[MemberData(nameof(TransientNonSqlOutages))]
|
||||
public async Task CachedWrite_NonSqlOutage_ClassifiedTransient_BuffersNotCrash(Exception outage)
|
||||
{
|
||||
// [1] A live outage that is NOT a SqlException must be classified TRANSIENT
|
||||
// [3] A live outage that is NOT a SqlException must be classified TRANSIENT
|
||||
// (buffered for retry), NOT escape unclassified to crash the script actor,
|
||||
// and NOT be returned as a permanent Failed result.
|
||||
var conn = new DatabaseConnectionDefinition("testDb", "Server=localhost;Database=test")
|
||||
@@ -401,7 +401,7 @@ public class DatabaseGatewayTests
|
||||
[Fact]
|
||||
public async Task CachedWrite_CancellationRequested_PropagatesOperationCanceled_NotReclassified()
|
||||
{
|
||||
// [2] OperationCanceledException raised while the caller's token is
|
||||
// [1] OperationCanceledException raised while the caller's token is
|
||||
// cancelled must propagate UNCHANGED — never reclassified as a transient
|
||||
// DB error and never buffered. Mirrors the HTTP path's first catch:
|
||||
// `catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) throw;`
|
||||
@@ -449,7 +449,7 @@ public class DatabaseGatewayTests
|
||||
[Fact]
|
||||
public async Task DeliverBuffered_NonSqlOutage_RethrowsAsTransient_SoEngineRetries()
|
||||
{
|
||||
// [1] on the RETRY path: a non-SqlException outage during delivery must be
|
||||
// [3] on the RETRY path: a non-SqlException outage during delivery must be
|
||||
// classified transient and propagate (as TransientDatabaseException) so
|
||||
// the S&F engine schedules another retry — it must NOT crash/park.
|
||||
var conn = new DatabaseConnectionDefinition("testDb", "Server=localhost;Database=test") { Id = 1 };
|
||||
@@ -473,6 +473,71 @@ public class DatabaseGatewayTests
|
||||
() => gateway.DeliverBufferedAsync(message));
|
||||
}
|
||||
|
||||
// ── ExternalSystemGateway-025: a caller-token cancel that surfaces from the SQL
|
||||
// driver as a SqlException (mid-flight cancel) must propagate as
|
||||
// OperationCanceledException — never reclassified as a permanent DB error.
|
||||
// The fix re-checks the caller's token at the TOP of `catch (SqlException)`
|
||||
// via cancellationToken.ThrowIfCancellationRequested(), so the cancel wins
|
||||
// regardless of the driver's exception shape (version-independent). ──
|
||||
|
||||
[Fact]
|
||||
public async Task CachedWrite_CancellationSurfacingAsSqlException_PropagatesCanceled_NotReclassifiedPermanent()
|
||||
{
|
||||
var conn = new DatabaseConnectionDefinition("testDb", "Server=localhost;Database=test") { Id = 1 };
|
||||
StubConnection(conn);
|
||||
|
||||
var (sf, connStr, keepAlive) = NewStoreAndForward();
|
||||
using var _ = keepAlive;
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
cts.Cancel();
|
||||
|
||||
// The SQL driver raises a SqlException on a mid-flight cancel (error
|
||||
// number 0, not in the transient set — pre-fix it was reclassified as a
|
||||
// PERMANENT DB error). The raw seam throws it through the production
|
||||
// ExecuteWriteAsync classification so the new ThrowIfCancellationRequested
|
||||
// guard at the top of `catch (SqlException)` runs end-to-end.
|
||||
var sqlException = FabricateSqlException("Operation cancelled by user.", number: 0);
|
||||
var gateway = new RawExecuteStubGateway(_repository, sf, onRunSql: () => throw sqlException);
|
||||
|
||||
await Assert.ThrowsAsync<OperationCanceledException>(
|
||||
() => gateway.CachedWriteAsync("testDb", "INSERT INTO t VALUES (1)", cancellationToken: cts.Token));
|
||||
|
||||
// The cancel won — it must NOT have been classified as transient (buffered)
|
||||
// nor returned as a permanent Failed result.
|
||||
Assert.Equal(0, ReadBufferDepth(connStr));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fabricates a <see cref="Microsoft.Data.SqlClient.SqlException"/> with a given
|
||||
/// message and error number via the driver's internal <c>CreateException</c>
|
||||
/// factory (the type has no public constructor). Used only to drive the
|
||||
/// <c>catch (SqlException)</c> branch of <c>ExecuteWriteAsync</c> in tests.
|
||||
/// </summary>
|
||||
private static Microsoft.Data.SqlClient.SqlException FabricateSqlException(string message, int number)
|
||||
{
|
||||
var errorCtor = typeof(Microsoft.Data.SqlClient.SqlError).GetConstructors(
|
||||
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)
|
||||
.First(c => c.GetParameters().Length == 8
|
||||
&& c.GetParameters()[7].ParameterType == typeof(Exception));
|
||||
var sqlError = (Microsoft.Data.SqlClient.SqlError)errorCtor.Invoke(
|
||||
new object?[] { number, (byte)0, (byte)0, "server", message, "procedure", 0, null });
|
||||
|
||||
var collectionCtor = typeof(Microsoft.Data.SqlClient.SqlErrorCollection).GetConstructors(
|
||||
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic).First();
|
||||
var collection = (Microsoft.Data.SqlClient.SqlErrorCollection)collectionCtor.Invoke(Array.Empty<object?>());
|
||||
typeof(Microsoft.Data.SqlClient.SqlErrorCollection)
|
||||
.GetMethod("Add", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!
|
||||
.Invoke(collection, new object?[] { sqlError });
|
||||
|
||||
var createException = typeof(Microsoft.Data.SqlClient.SqlException).GetMethod(
|
||||
"CreateException",
|
||||
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic,
|
||||
new[] { typeof(Microsoft.Data.SqlClient.SqlErrorCollection), typeof(string) })!;
|
||||
return (Microsoft.Data.SqlClient.SqlException)createException.Invoke(
|
||||
null, new object?[] { collection, "6.0.0" })!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the current buffered-message count off the S&F SQLite DB by
|
||||
/// counting <c>sf_messages</c> rows (the engine's persistence table).
|
||||
|
||||
Reference in New Issue
Block a user