Skip to content

[TEST] Behavioral coverage for the three PostgreSQL engine gates (reconcile, snapshot_now, analyze_now) #2230

Closed

Description

@erikdarlingdata

What

The three engine gates added in #2213's round-3 fix (the long-query XE reconcile early-return, the snapshot_now dispatch predicate, and analyze_now's honest-tombstone arm) are covered by source-scanning pins and a live rig run (9 sweeps, zero engine-blind dispatches), but none has a BEHAVIORAL test that drives a PostgreSQL runtime through the gate and asserts the short-circuit.

What a test needs

  • analyze_now is the tractable one: fabricate the worker (GetUninitializedObject idiom, see CollectorMemoryKnobTests.SweepGate tests) with a live _postgres, a ServerLoopState with a PostgreSql runtime, invoke RunAnalyzeNowAsync, assert the analysis_state row carries the engine tombstone (not the generic insufficient_data) and the CommandOutcome is the success shape.
  • The reconcile and snapshot gates need the same fabrication plus a fake runner; the observable is absence (no connection attempt), which wants a counting seam.

From #2213 round 3 (reviewer finding 7); filed per the deferral rule during release week.

Activity

  1. added 2 commits that reference this issue on Aug 12, 2026
  2. erikdarlingdata commented on Aug 13, 2026

    @erikdarlingdata
    OwnerAuthor

    Partially delivered — one of the three gates, not all three. Recording that here rather than closing it, because the issue asks for three and #2237 covered one.

    Darling/Darling.Tests/PostgresEngineGateBehaviorTests.cs (merged in #2237, a076f49f) carries two facts, both on analyze_now — the one this issue called "the tractable one":

    • AnalyzeNow_AgainstAPostgresTarget_WritesTheEngineTombstone_AndDoesNotRunThePass
    • AnalyzeNow_AgainstASqlServerTarget_DoesNotTakeTheEngineGate

    The second is what makes the first mean anything: without a SQL Server arm, a test asserting "the pass didn't run" passes just as well if the pass never runs for anybody.

    Still uncovered behaviorally:

    1. The long-query XE reconcile early-return. Needs a fabricated worker whose reconcile path can be driven without an XE session on the other end.
    2. The snapshot_now dispatch predicate.

    Both remain covered only by the source-scanning pins and the live rig run from #2213's round 3 (9 sweeps, zero engine-blind dispatches) — which is evidence the gates work, but not a test that fails if someone removes one.

    Leaving this open at 1/3.

  3. added 2 commits that reference this issue on Aug 13, 2026
  4. erikdarlingdata commented on Aug 13, 2026

    @erikdarlingdata
    OwnerAuthor

    2 of 3 now, via #2245 (merged).

    The reconcile gate is covered behaviorally — and my earlier note here was half wrong, which is the interesting part. I had written that this gate "observes an ABSENCE (no connection attempted), which needs a counting seam that does not exist yet." It doesn't: DarlingXeSessions.ReconcileLongQueryCompletionsAsync carries its own engine precondition ("belt to the worker's braces") and is public static, so an ungated call throws where a gated one returns. No seam, no live store, no network.

    Pinned:

    fact asserts
    PG target, enabled: true returns, no throw
    PG target, enabled: false returns, no throw — the gate sits ahead of that branch, so ungated the false arm would try to DROP a session over the same impossible connection
    SQL Server engine, same connection string throws Keyword not supported: 'host'

    The third row is what makes the first two non-vacuous: the engine is the only variable, so the test can't pass because connection handling changed for some unrelated reason. runner is passed null in the PG arms deliberately — reaching it would mean the gate didn't fire.

    Remaining: snapshot_now

    This is the one that genuinely needs what I wrongly claimed for the reconcile gate. Unlike the other two it has no public precondition of its own to call, and its observable is a true absence — the dispatch predicate simply doesn't hand a PostgreSQL target to a SQL-Server-shaped collector, so a passing test looks identical to a test that never dispatched anything.

    Two ways to make it observable, neither free:

    1. A counting seam — have the dispatch path report what it dispatched (a counter, or an injectable dispatch delegate the test can record). Cheapest observable, but it widens a production surface purely to be watched.
    2. Assert through the store — drive a sweep against a PostgreSQL target and assert no SQL-Server-shaped collector wrote a row for that server_id. No production change, and it exercises the real path, but it needs a live store and it asserts an absence, which is only as strong as the list of tables it checks.

    (2) is more honest about what the gate promises; (1) is easier to keep green. Leaving this open rather than picking one, since it's a design choice about widening a surface for testability.

  5. added a commit that references this issue on Aug 14, 2026
  6. erikdarlingdata commented on Aug 14, 2026

    @erikdarlingdata
    OwnerAuthor

    3 of 3, closing. #2237 covered analyze_now, #2245 the reconcile early-return, and #2261 (merged) snapshot_now.

    The last one needed neither trick the other two used. analyze_now asserts a presence in analysis_state; the reconcile gate exploits a public precondition that throws when ungated. snapshot_now's loop already reports collectorsRun in its outcome JSON, and every run it makes writes itself to collection_log — so the gate's effect is a count and a set of rows, with nothing to infer.

    What made it a small test rather than a 49-collector one: schedule overrides disable everything except wait_stats, SQL-Server-only and dispatched through the same loop, so the two arms differ in the engine and nothing else.

    arm collectorsRun collection_log
    PostgreSQL target 0 empty
    SQL Server target, same overrides 1 wait_stats

    Asserting the row's absence rather than "not SUCCESS" is the point: a gate that dispatched and then failed would also avoid SUCCESS, while having connected to a PostgreSQL host as SQL Server. And the SQL Server arm is load-bearing rather than decorative — without it the gate assertion passes just as well against a snapshot that stopped dispatching anything at all, since "ran zero collectors" is also what a broken loop reports.

    Both ran for real against the live store rather than skipping: 1 s for the SQL arm, 149 ms for the PostgreSQL one — the split the design predicts, since only the SQL arm pays for a connect attempt.

    Note for whoever reads this next: my first version pointed the connection at an unresolvable .invalid hostname, which made the timing a property of CI's DNS resolver. Review caught it; it now uses the suite's existing Host=127.0.0.1;Port=1 idiom, and the identity host and the connection host are deliberately different values for different reasons.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions