diff --git a/Darling/Darling.Tests/DarlingMcpToolClassification.cs b/Darling/Darling.Tests/DarlingMcpToolClassification.cs
index 6da2438b77..c9ebb37335 100644
--- a/Darling/Darling.Tests/DarlingMcpToolClassification.cs
+++ b/Darling/Darling.Tests/DarlingMcpToolClassification.cs
@@ -50,7 +50,7 @@ internal static class DarlingMcpToolClassification
"get_pg_replication_slots", "get_pg_replication_stats", "get_pg_server_config",
"get_pg_server_config_changes", "get_pg_session_states", "get_pg_table_bloat", "get_pg_top_queries",
"get_pg_wait_sampling", "get_pg_wait_stats", "get_pg_wait_trend", "get_pg_wraparound_risk",
- "get_pg_write_stats", "get_pg_xmin_horizon", "get_plan_cache_bloat", "get_plan_corrections", "get_plan_xml",
+ "get_pg_write_stats", "get_pg_xmin_horizon", "get_plan_cache_bloat", "get_plan_corrections", "get_plan_force_precheck", "get_plan_xml",
"get_procedure_duration_trend", "get_procedure_plan_xml", "get_pvs_stats", "get_query_duration_trend",
"get_query_heatmap", "get_query_repro_script", "get_query_store_clutter", "get_query_store_duration_trend",
"get_query_store_health", "get_query_store_plan_xml", "get_query_store_query_history",
diff --git a/Darling/Darling.Tests/McpPayloadContractCensusTests.cs b/Darling/Darling.Tests/McpPayloadContractCensusTests.cs
index e73bb5b84a..1771441f94 100644
--- a/Darling/Darling.Tests/McpPayloadContractCensusTests.cs
+++ b/Darling/Darling.Tests/McpPayloadContractCensusTests.cs
@@ -2039,6 +2039,8 @@ public static readonly (string Spelling, string File, string WhatItIs)[] Severit
"audit_config's edition-NAME fallback (Enterprise / Standard / … / Unknown) — a name, spelled as the canon by coincidence"),
("Unknown", "McpAnalysisTools.cs",
"audit_config's edition-NAME fallback — the Lite twin of the above"),
+ ("unknown", "DarlingMcpPlanForcePrecheckTools.cs",
+ "get_plan_force_precheck's unknown[] section name: the checks the store could not answer, listed beside refusals and never a refusal (#5632). A section name, not a band"),
("unknown", "DarlingMcpTools.cs",
"mute_analysis_finding's kind (plain / read-only / per-database) for a resolved server that has no definition row in the store (one defined in darling.json) — a registration kind, not a band"),
("unknown", "DarlingMcpPgWaitSamplingTools.cs",
diff --git a/Darling/Darling.Tests/PlanForcePrecheckLiveTests.cs b/Darling/Darling.Tests/PlanForcePrecheckLiveTests.cs
new file mode 100644
index 0000000000..aa21e48b28
--- /dev/null
+++ b/Darling/Darling.Tests/PlanForcePrecheckLiveTests.cs
@@ -0,0 +1,213 @@
+/*
+ * Copyright (c) 2026 Erik Darling, Darling Data LLC
+ *
+ * This file is part of the SQL Server Performance Monitor.
+ *
+ * Licensed under the MIT License. See LICENSE file in the project root for full license information.
+ */
+
+using System;
+using System.Globalization;
+using System.Linq;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Npgsql;
+using NpgsqlTypes;
+using PerformanceMonitor.Collectors;
+using PerformanceMonitor.Common;
+using PerformanceMonitor.Darling.Service.Mcp;
+using PerformanceMonitor.Darling.Storage;
+using Xunit;
+
+namespace Darling.Tests;
+
+///
+/// #5632 step A2: get_plan_force_precheck over seeded store rows. Each case plants one query's Query Store rows, its text and its
+/// stored plans, reads the tool, and asserts the verdict and the stable refusal codes. as_of is fixed so no clock decides the window.
+///
+[Collection("live-postgres")]
+public sealed class PlanForcePrecheckLiveTests
+{
+ private const string ServerName = "darling-force-precheck";
+ private static readonly int ServerId = ServerIdHelper.GetDeterministicHashCode(ServerName);
+ private const string Db = "ForceDb";
+
+ private static readonly DateTime Anchor = DateTime.SpecifyKind(DarlingMcpTestData.TruncateToSeconds(DateTime.UtcNow).AddHours(-6), DateTimeKind.Unspecified);
+ private static readonly DateTime AsOf = Anchor.AddHours(1);
+
+ private static string? ConnectionString => Environment.GetEnvironmentVariable("DARLING_TEST_PG");
+
+ private static string Plan(string? value = null, string extra = "", string stmtAttrs = "", string? queryPlanAttrs = null) =>
+ ""
+ + "" + extra
+ + ""
+ + (value is null ? "" : "")
+ + "";
+
+ private sealed record Seed(long QueryId, string Text, (long PlanId, long Rows, long Execs, int LastExecMinutesBeforeAnchor, string Xml, bool Forced)[] Plans);
+
+ private static async Task RunAsync(Seed seed, Func? extra = null, long? planId = null, long? compare = null)
+ {
+ var cs = ConnectionString;
+ Assert.SkipWhen(string.IsNullOrEmpty(cs), "Set DARLING_TEST_PG to a Postgres connection string to run the live pre-check test.");
+ var ct = TestContext.Current.CancellationToken;
+ await using var connection = new NpgsqlConnection(cs);
+ await connection.OpenAsync(ct);
+ await PgMigrations.MigrateAsync(connection, ct);
+ await DeleteAsync(connection, ct);
+ await using var postgres = NpgsqlDataSource.Create(cs!);
+ var ok = false;
+ try
+ {
+ await DarlingMcpTestData.RegisterServerAsync(connection, ServerId, ServerName, ct);
+ await Exec(connection, ct, "INSERT INTO collect.query_store_text (server_id, database_name, query_id, query_sql_text, last_seen) VALUES ($1,$2,$3,$4,$5)", ServerId, Db, seed.QueryId, seed.Text, Anchor);
+ foreach (var p in seed.Plans)
+ {
+ var last = Anchor.AddMinutes(-p.LastExecMinutesBeforeAnchor);
+ await Exec(connection, ct, @"INSERT INTO query_store_stats
+(collection_id, collection_time, server_id, server_name, database_name, query_id, plan_id, execution_type_desc, runtime_stats_interval_id,
+ interval_start_time_utc, first_execution_time, last_execution_time, execution_count, avg_cpu_time_us, avg_duration_us, avg_rowcount, is_forced_plan, force_failure_count)
+VALUES ($1,$2,$3,$4,$5,$6,$7,'Regular',$7,$8,$8,$9,$10,5000,7000,$11,$12,0)",
+ CollectionIdGenerator.Next(), Anchor, ServerId, ServerName, Db, seed.QueryId, p.PlanId, last.AddHours(-1), last, p.Execs, p.Rows, p.Forced);
+ var stored = p.Xml + "";
+ var digest = PayloadDimensions.Digest(stored);
+ await using (var dim = new NpgsqlCommand("INSERT INTO query_plan_dim (digest, query_plan_xml, last_seen) VALUES ($1,$2,$3) ON CONFLICT DO NOTHING", connection))
+ {
+ dim.Parameters.Add(new NpgsqlParameter { NpgsqlDbType = NpgsqlDbType.Bytea, Value = digest });
+ dim.Parameters.AddWithValue(stored);
+ dim.Parameters.AddWithValue(NpgsqlDbType.Timestamp, Anchor);
+ await dim.ExecuteNonQueryAsync(ct);
+ }
+
+ await using var map = new NpgsqlCommand("INSERT INTO collect.query_store_plan_map (server_id, database_name, plan_id, digest, plan_hash, last_seen) VALUES ($1,$2,$3,$4,$5,$6)", connection);
+ map.Parameters.AddWithValue(ServerId);
+ map.Parameters.AddWithValue(Db);
+ map.Parameters.AddWithValue(p.PlanId);
+ map.Parameters.Add(new NpgsqlParameter { NpgsqlDbType = NpgsqlDbType.Bytea, Value = digest });
+ map.Parameters.AddWithValue("0x5632");
+ map.Parameters.AddWithValue(NpgsqlDbType.Timestamp, Anchor);
+ await map.ExecuteNonQueryAsync(ct);
+ }
+
+ if (extra is not null) await extra(connection, ct);
+ var json = await DarlingMcpPlanForcePrecheckTools.GetPlanForcePrecheck(
+ postgres, Db, seed.QueryId, planId ?? seed.Plans[0].PlanId, ServerName, compare, 24, AsOf.ToString("o", CultureInfo.InvariantCulture), cancellationToken: ct);
+ var doc = JsonDocument.Parse(json);
+ ok = true;
+ return doc;
+ }
+ finally
+ {
+ await LiveStoreCleanup.RunAsync(cs!, ok, async (c, t) => await DeleteAsync(c, t));
+ }
+ }
+
+ private static Task Exec(NpgsqlConnection c, CancellationToken ct, string sql, params object?[] values) =>
+ DarlingMcpTestData.ExecAsync(c, ct, sql, values.Select(v => v is DateTime d ? DateTime.SpecifyKind(d, DateTimeKind.Unspecified) : v).ToArray());
+
+ private static string[] Codes(JsonDocument d) =>
+ d.RootElement.GetProperty("refusals").EnumerateArray().Select(r => r.GetProperty("code").GetString()!).ToArray();
+
+ private static string[] UnknownChecks(JsonDocument d) =>
+ d.RootElement.GetProperty("unknown").EnumerateArray().Select(r => r.GetProperty("check").GetString()!).ToArray();
+
+ /* A clean query: two plans of about the same output, both well run, both recent, no hint, no temp table. The candidate is the cheap, older plan. */
+ private static Seed Clean(long q = 7001, string text = "SELECT * FROM dbo.widgets WHERE id = @p") =>
+ new(q, text, [(1, 100, 60, 30, Plan("1"), false), (2, 120, 60, 5, Plan("1"), false)]);
+
+ [Fact]
+ public async Task ACleanCandidate_HasNoRefusal_ButTheVerdictNeverSaysSafe()
+ {
+ using var doc = await RunAsync(Clean());
+ var codes = Codes(doc);
+ /* The store holds no force state, so the gate's state_unavailable is the one refusal; every check this read owns is quiet. */
+ Assert.DoesNotContain(codes, c => c is "recompile_hint" or "temp_table" or "not_run_recently" or "too_few_executions" or "psp_variant" or "output_differs" or "query_store_read_only");
+ Assert.Contains("query_store_read_only", UnknownChecks(doc));
+ Assert.DoesNotContain("safe", doc.RootElement.GetProperty("verdict").GetString(), StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public async Task Pattern1_AStatementWithOptionRecompile_IsRefused()
+ {
+ using var doc = await RunAsync(Clean(7002, "SELECT * FROM dbo.widgets WHERE id = @p OPTION (RECOMPILE)"));
+ Assert.Contains("recompile_hint", Codes(doc));
+ Assert.Equal("refuse", doc.RootElement.GetProperty("verdict").GetString());
+ }
+
+ [Fact]
+ public async Task Pattern2_PlansReturning26000AndFiftyRowsPerCall_AreRefusedAsOutputDiffers()
+ {
+ var seed = new Seed(7003, "SELECT 1", [(1, 50, 60, 30, Plan("1"), false), (2, 26_000, 60, 5, Plan("1"), false)]);
+ using var doc = await RunAsync(seed);
+ Assert.Contains("output_differs", Codes(doc));
+ }
+
+ [Fact]
+ public async Task ABlockerFromTheExistingGate_AppearsUnderItsOwnCode()
+ {
+ var seed = new Seed(7004, "SELECT 1", [(1, 100, 60, 30, Plan("1"), true), (2, 100, 60, 5, Plan("1"), false)]);
+ using var doc = await RunAsync(seed);
+ Assert.Contains("already_forced", Codes(doc));
+ }
+
+ [Fact]
+ public async Task ARunStoppedQuery_AndAThinPlan_AreRefused()
+ {
+ var seed = new Seed(7005, "SELECT 1", [(1, 100, 3, 300, Plan("1"), false), (2, 100, 60, 240, Plan("1"), false)]);
+ using var doc = await RunAsync(seed);
+ var codes = Codes(doc);
+ Assert.Contains("not_run_recently", codes);
+ Assert.Contains("too_few_executions", codes);
+ }
+
+ [Fact]
+ public async Task DifferentCompiledValues_AreShown_NeverRefused()
+ {
+ var seed = new Seed(7006, "SELECT 1", [(1, 100, 60, 30, Plan("111"), false), (2, 100, 60, 5, Plan("222"), false)]);
+ using var doc = await RunAsync(seed);
+ Assert.DoesNotContain(Codes(doc), c => c is "output_differs" or "recompile_hint" or "psp_variant");
+ var raw = doc.RootElement.GetRawText();
+ Assert.Contains("\"compiled_value\":\"111\"", raw, StringComparison.Ordinal);
+ Assert.Contains("\"compiled_value\":\"222\"", raw, StringComparison.Ordinal);
+ Assert.Equal(PerformanceMonitor.Common.McpDataDetailMasker.ReplacedValue,
+ System.Text.Json.Nodes.JsonNode.Parse(McpDataDetailMasker.MaskJson(raw, McpDataDetail.MaskLiterals))!["shown"]!["plans"]![0]!["compiled_values"]![0]!["compiled_value"]!.GetValue());
+ var meta = McpDataDetailMasker.MaskJson(raw, McpDataDetail.MetadataOnly);
+ Assert.DoesNotContain("111", meta, StringComparison.Ordinal);
+ Assert.DoesNotContain("222", meta, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public async Task ATempTable_AndAPspVariant_AreRefused_AndATableVariableIsNot()
+ {
+ var temp = Plan("1", "");
+ var tableVar = Plan("1", "");
+ using (var d1 = await RunAsync(new Seed(7007, "SELECT 1", [(1, 100, 60, 30, temp, false), (2, 100, 60, 5, Plan("1"), false)])))
+ Assert.Contains("temp_table", Codes(d1));
+ using (var d2 = await RunAsync(new Seed(7008, "SELECT 1", [(1, 100, 60, 30, tableVar, false), (2, 100, 60, 5, Plan("1"), false)])))
+ Assert.DoesNotContain("temp_table", Codes(d2));
+ using var d3 = await RunAsync(new Seed(7009, "SELECT 1", [(1, 100, 60, 30, Plan("1", queryPlanAttrs: " QueryVariantID=\"2\""), false), (2, 100, 60, 5, Plan("1"), false)]));
+ Assert.Contains("psp_variant", Codes(d3));
+ Assert.Contains("psp_variant_siblings", UnknownChecks(d3));
+ }
+
+ [Fact]
+ public async Task AReadOnlyQueryStore_IsRefused_AndNoHealthRowIsUnknown()
+ {
+ using var ro = await RunAsync(Clean(7010), async (c, ct) => await Exec(c, ct,
+ @"INSERT INTO collect.query_store_health (config_id, capture_time, server_id, server_name, database_name, actual_state, desired_state, readonly_reason)
+ VALUES ($1,$2,$3,$4,$5,'READ_ONLY','READ_WRITE',65536)", CollectionIdGenerator.Next(), Anchor, ServerId, ServerName, Db));
+ Assert.Contains("query_store_read_only", Codes(ro));
+ using var none = await RunAsync(Clean(7011));
+ Assert.DoesNotContain("query_store_read_only", Codes(none));
+ Assert.Contains("query_store_read_only", UnknownChecks(none));
+ }
+
+ private static async Task DeleteAsync(NpgsqlConnection c, CancellationToken ct)
+ {
+ foreach (var table in new[] { "query_store_stats", "collect.query_store_text", "collect.query_store_health", "collect.query_store_plan_map" })
+ await Exec(c, ct, "DELETE FROM " + table + " WHERE server_id = $1", ServerId);
+ await Exec(c, ct, "DELETE FROM query_plan_dim WHERE last_seen = $1 AND query_plan_xml LIKE '%