diff --git a/tests/ZB.MOM.WW.ScadaBridge.InboundAPI.Tests/InboundDatabaseHelperTests.cs b/tests/ZB.MOM.WW.ScadaBridge.InboundAPI.Tests/InboundDatabaseHelperTests.cs index b50c2fb3..e9ec4a89 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.InboundAPI.Tests/InboundDatabaseHelperTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.InboundAPI.Tests/InboundDatabaseHelperTests.cs @@ -33,11 +33,25 @@ public class InboundDatabaseHelperTests => throw new NotImplementedException(); } - private static SqliteGateway SeededGateway(bool honorTokenOnOpen = true) + /// + /// Keep-alive for the shared in-memory database, rooted in a static field. It used to be a + /// local in the seeding helper: once the helper returned nothing referenced it, so after a GC the + /// Sqlite pool could reclaim the leaked connection, close the last connection and destroy the + /// database before the test's query ran (no such table: Machine). This is the same latent + /// flake the executor tests hit. + /// + private static readonly SqliteConnection KeepAlive = OpenKeepAlive(); + + private static SqliteConnection OpenKeepAlive() { var keep = new SqliteConnection("DataSource=file:movein?mode=memory&cache=shared"); - keep.Open(); // keep-alive: shared in-memory db lives until process exit - using var cmd = keep.CreateCommand(); + keep.Open(); // lives until process exit + return keep; + } + + private static SqliteGateway SeededGateway(bool honorTokenOnOpen = true) + { + using var cmd = KeepAlive.CreateCommand(); cmd.CommandText = "CREATE TABLE IF NOT EXISTS Machine(Code TEXT, SAPID TEXT); DELETE FROM Machine; INSERT INTO Machine VALUES('Z28061A','131453');"; cmd.ExecuteNonQuery(); return new SqliteGateway("DataSource=file:movein?mode=memory&cache=shared", honorTokenOnOpen); diff --git a/tests/ZB.MOM.WW.ScadaBridge.InboundAPI.Tests/InboundScriptExecutorTests.cs b/tests/ZB.MOM.WW.ScadaBridge.InboundAPI.Tests/InboundScriptExecutorTests.cs index 4d47c438..0d8a701c 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.InboundAPI.Tests/InboundScriptExecutorTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.InboundAPI.Tests/InboundScriptExecutorTests.cs @@ -940,6 +940,13 @@ public class InboundScriptExecutorTests var services = new ServiceCollection(); services.AddSingleton(SeededSqliteGateway()); using var provider = services.BuildServiceProvider(); + // Regression guard for the old flake: force the collection the full suite used to hit during + // the script compile, then the pool reclaim that followed it. An unrooted keep-alive dies here + // deterministically ("Internal script error"); ClearAllPools only closes IDLE connections. + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + SqliteConnection.ClearAllPools(); var executor = new InboundScriptExecutor( NullLogger.Instance, provider); @@ -1052,11 +1059,26 @@ public class InboundScriptExecutorTests => throw new NotImplementedException(); } - private static SqliteGateway SeededSqliteGateway() + /// + /// Keep-alive for the shared in-memory database, rooted in a static field. It used to be a + /// local in the seeding helper: once the helper returned nothing referenced it, so after a GC + /// (likely during the script compile under a full-suite run) Microsoft.Data.Sqlite's pool + /// reclaimed the leaked connection, closed the last connection and destroyed the database. The + /// query then failed with no such table: Machine, which the script reported as "Internal + /// script error" (4 runs in 30, captured 2026-11-10). + /// + private static readonly SqliteConnection KeepAlive = OpenKeepAlive(); + + private static SqliteConnection OpenKeepAlive() { var keep = new SqliteConnection("DataSource=file:movein-exec?mode=memory&cache=shared"); - keep.Open(); // keep-alive: shared in-memory db lives until process exit - using var cmd = keep.CreateCommand(); + keep.Open(); // lives until process exit + return keep; + } + + private static SqliteGateway SeededSqliteGateway() + { + using var cmd = KeepAlive.CreateCommand(); cmd.CommandText = "CREATE TABLE IF NOT EXISTS Machine(Code TEXT, SAPID TEXT); DELETE FROM Machine; INSERT INTO Machine VALUES('Z28061A','131453');"; cmd.ExecuteNonQuery(); return new SqliteGateway("DataSource=file:movein-exec?mode=memory&cache=shared");