infra(docker)+test(siteeventlog): close register tripwires 79 + 93 — rig data to named volumes; pin site_events schema to the cap-model calibration
Row 79 executed its own prescribed remedy without waiting for a second corruption: all ten rig data dirs (8 docker/ nodes + 2 docker-env2/ site nodes) move from virtiofs bind mounts to named docker volumes (scadabridge-<node>-data), seeded in place with both rigs stopped and live-verified (site-a localDbReplicationConnected=true / backlog 0 over the migrated files; env2 stable, 0 restarts). The feared inspectability loss is moot — OrbStack exposes volumes at ~/OrbStack/docker/volumes/. The rebuild surfaced latent env2 drift (pre-Secrets-0.6.2 image): env2 now carries the x-secrets-store-env posture + central data volumes. Row 93's silent maintenance trap is now loud: SiteEventsSchemaCalibrationTests pins the site_events columns/indexes the EventLogPurgeService size model is calibrated against, failing with a pointer to the memo §4 recalibration on any schema drift. Rows 92 (accepted by design) and 94 (standing tripwire) annotated accordingly.
This commit is contained in:
+105
@@ -0,0 +1,105 @@
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.SiteEventLogging.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Pins the <c>site_events</c> schema to the shape the storage-cap size model was calibrated
|
||||
/// against (register row 93, <c>docs/plans/2026-08-17-site-event-log-cap-semantics-decision.md</c> §4).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see cref="EventLogPurgeService.EstimateSiteEventsSize"/> hardcodes this schema twice over:
|
||||
/// the sampling SQL sums <c>octet_length</c> over the exact column list, and
|
||||
/// <c>IndexedColumnCount = 4</c> plus the fill-factor/overhead constants are empirical for
|
||||
/// exactly these four secondary indexes. A schema migration that adds a column or an index
|
||||
/// invalidates both SILENTLY — the new column's bytes are never sampled, a fifth index is
|
||||
/// never charged — so this test turns that drift into a loud failure. The fix on failure is
|
||||
/// NEVER to just update the assertions: re-run the memo §4 calibration, update the
|
||||
/// <see cref="EventLogPurgeService"/> constants and sampling SQL together, and only then
|
||||
/// re-pin the schema here.
|
||||
/// </remarks>
|
||||
public class SiteEventsSchemaCalibrationTests
|
||||
{
|
||||
private const string RecalibrationNotice =
|
||||
"site_events schema drifted from the size-model calibration. Re-run the calibration in " +
|
||||
"docs/plans/2026-08-17-site-event-log-cap-semantics-decision.md §4 and update " +
|
||||
"EventLogPurgeService's constants + sampling SQL together with this pin (register row 93).";
|
||||
|
||||
private static SqliteConnection OpenWithSchema()
|
||||
{
|
||||
var connection = new SqliteConnection("Data Source=:memory:");
|
||||
connection.Open();
|
||||
SiteEventLogSchema.Apply(connection);
|
||||
return connection;
|
||||
}
|
||||
|
||||
private static List<string> ReadStrings(SqliteConnection connection, string sql)
|
||||
{
|
||||
using var cmd = connection.CreateCommand();
|
||||
cmd.CommandText = sql;
|
||||
var values = new List<string>();
|
||||
using var reader = cmd.ExecuteReader();
|
||||
while (reader.Read())
|
||||
values.Add(reader.GetString(0));
|
||||
return values;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Columns_MatchTheSampledOctetLengthSum_Exactly()
|
||||
{
|
||||
// The sampler's payload_bytes expression sums octet_length over exactly these eight
|
||||
// columns, in this order. A ninth column would contribute real disk bytes that the
|
||||
// model never counts, under-sizing every estimate.
|
||||
using var connection = OpenWithSchema();
|
||||
|
||||
var columns = ReadStrings(connection, "SELECT name FROM pragma_table_info('site_events')");
|
||||
|
||||
Assert.True(
|
||||
columns.SequenceEqual(
|
||||
["id", "timestamp", "event_type", "severity", "instance_id", "source", "message", "details"]),
|
||||
$"{RecalibrationNotice} Actual columns: {string.Join(", ", columns)}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Indexes_AreExactlyTheFourTheModelCharges_PlusThePkAutoindex()
|
||||
{
|
||||
// IndexedColumnCount = 4 charges four secondary index entries per row; the TEXT
|
||||
// primary key's autoindex is absorbed into the empirically-measured fill factors.
|
||||
// A fifth secondary index would be silently under-counted.
|
||||
using var connection = OpenWithSchema();
|
||||
|
||||
var indexes = ReadStrings(
|
||||
connection, "SELECT name FROM pragma_index_list('site_events') ORDER BY name");
|
||||
|
||||
Assert.True(
|
||||
indexes.SequenceEqual(
|
||||
[
|
||||
"idx_events_instance",
|
||||
"idx_events_severity",
|
||||
"idx_events_timestamp",
|
||||
"idx_events_type",
|
||||
"sqlite_autoindex_site_events_1",
|
||||
]),
|
||||
$"{RecalibrationNotice} Actual indexes: {string.Join(", ", indexes)}");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("idx_events_timestamp", "timestamp")]
|
||||
[InlineData("idx_events_type", "event_type")]
|
||||
[InlineData("idx_events_instance", "instance_id")]
|
||||
[InlineData("idx_events_severity", "severity")]
|
||||
public void EachIndex_CoversExactlyTheColumnTheSamplerSums(string indexName, string expectedColumn)
|
||||
{
|
||||
// The sampler's indexed_bytes expression sums octet_length(timestamp) +
|
||||
// octet_length(event_type) + octet_length(severity) + octet_length(instance_id) —
|
||||
// one column per index. Widening an index to a composite (or re-pointing it) would
|
||||
// change the real entry width without changing what is sampled.
|
||||
using var connection = OpenWithSchema();
|
||||
|
||||
var columns = ReadStrings(
|
||||
connection, $"SELECT name FROM pragma_index_info('{indexName}')");
|
||||
|
||||
Assert.True(
|
||||
columns.SequenceEqual([expectedColumn]),
|
||||
$"{RecalibrationNotice} Index {indexName} covers: {string.Join(", ", columns)}");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user