Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Darling/Darling.Tests/DarlingMcpStoreHostToolsLiveTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,16 @@ public async Task GetStoreHost_AsMcpAndViewerRoles_SurfacesAllFourVerdicts_Live(
await PgMigrations.MigrateAsync(owner, ct);
await CreateTestRolesAsync(owner, ct);

/* #5718: the polled read takes the store size from the hourly sweep's stored row (readable under each
role through the same collect SELECT grant), so a store that has never swept reports it unavailable.
Record one so the size assertion below proves the grant, not an empty table. */
await using (var seedSweep = new NpgsqlCommand(
"INSERT INTO collect.store_metrics (metric_time, object_name, object_kind, total_bytes) "
+ "VALUES ((now() AT TIME ZONE 'UTC') - interval '1 minute', 'store_5718_hostroles', 'store', 123456789)", owner))
{
await seedSweep.ExecuteNonQueryAsync(ct);
}

var bodySucceeded = false;
var root = Directory.CreateTempSubdirectory("darling-storehost-roles-");
try
Expand Down Expand Up @@ -146,6 +156,8 @@ both come off PostgreSQL/TimescaleDB reads PUBLIC can run with no Darling-author
not a thrown exception, so only an explicit not-null assertion catches it. */
var store = managed.RootElement.GetProperty("store");
Assert.NotEqual(JsonValueKind.Null, store.GetProperty("size_bytes").ValueKind);
Assert.NotEqual(JsonValueKind.Null, store.GetProperty("size_as_of").ValueKind);
Assert.NotEqual(JsonValueKind.Null, store.GetProperty("size_age_seconds").ValueKind);
Assert.NotEqual(JsonValueKind.Null, store.GetProperty("timescale_version").ValueKind);

/* uncompressed_chunk_count against OWNER-side reads of the exact same query
Expand Down Expand Up @@ -173,6 +185,9 @@ schema surface (this file's whole point) rather than trusting the row count alon
await LiveStoreCleanup.RunAsync(connectionString!, bodySucceeded, async (cleanup, cleanupCt) =>
{
await DropTestRolesAsync(cleanup, cleanupCt);
await using var dropSeed = new NpgsqlCommand(
"DELETE FROM collect.store_metrics WHERE object_name = 'store_5718_hostroles'", cleanup);
await dropSeed.ExecuteNonQueryAsync(cleanupCt);
});
}
}
Expand Down
21 changes: 21 additions & 0 deletions Darling/Darling.Tests/McpToolGuideHeads.Platform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,27 @@ public void EveryConvertedHead_CarriesItsGuardrailFact_AndThePointer()
}
}

/// <summary>
/// #5718: get_store_host's size now comes from the hourly sweep, its uncompressed-chunk figures can be null
/// on a timeout, and a slow fact fails alone. The reading rules ride the tail (get_tool_guide), not the
/// served description, because that part is held to the tools/list byte budget.
/// </summary>
[Fact]
public void StoreHost_GuideTail_ExplainsTheStoredSizeAndTheAloneFailingFacts_OffTheServedHead()
{
var served = McpToolGuideTests.Served("get_store_host");
Assert.NotNull(served.Tail);
foreach (var phrase in new[]
{
"size_as_of", "size_age_seconds", "size_unavailable_reason", "uncompressed_unavailable_reason",
"hourly sweep", "--check-settings reads the size live",
})
{
Assert.Contains(phrase, served.Tail!, StringComparison.Ordinal);
Assert.DoesNotContain(phrase, served.Served, StringComparison.Ordinal);
}
}

/// <summary>
/// get_store_metrics' TOAST and checkpointer blocks are two of its many facts and would blow the 620-char
/// head budget on their own; they stay off the shared head and ride the tail instead, verbatim (D3/D9: a
Expand Down
22 changes: 20 additions & 2 deletions Darling/Darling.Tests/SerialLoopStoreSizeSourceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,9 @@ Its only caller is DarlingCliCommands.CheckSettingsAsync (--check-settings), a u
+ "one-shot, under ServiceCommandDeadlines.CliStoreReadSeconds (10s) rather than the serial "
+ "loop's 5s bound, and never called from the collection loop or at service startup: ruling 9 "
+ "keeps every store fact that scales with the store out of the startup profile log, so "
+ "GatherStoreFactsAsync's only TWO callers are --check-settings and the get_store_host MCP "
+ "read below, neither of them the collection loop"),
+ "GatherStoreFactsAsync's live size read is paid only by --check-settings (the polled "
+ "get_store_host MCP and web read takes the hourly sweep's stored figure instead, #5718: "
+ "TheMcpStoreHostRead_IsNotAPgDatabaseSizePayer pins it), and neither is the collection loop"),
};

/// <summary>
Expand Down Expand Up @@ -354,6 +355,23 @@ than as an off-by-one on a count that says nothing about where it is. */
}
}

/// <summary>
/// #5718: the polled <c>get_store_host</c> read (MCP, and the web card through the same method) is NOT a
/// <c>pg_database_size</c> payer: it takes the stored sweep figure, so the tool file carries neither the
/// literal nor the live <c>StoreSizeSql</c> constant, and asks for the stored size. The one live payer on
/// this surface is <c>--check-settings</c>, which is why it stays in <c>s_pgDatabaseSizeOwners</c>.
/// </summary>
[Fact]
public void TheMcpStoreHostRead_IsNotAPgDatabaseSizePayer()
{
var tool = File.ReadAllText(Path.Combine(
RepoRoot(), "Darling", "PerformanceMonitor.Darling.Service", "Mcp", "DarlingMcpStoreHostTools.cs"));

Assert.DoesNotContain("pg_database_size", tool, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("StoreSizeSql", tool, StringComparison.Ordinal);
Assert.Contains("useStoredStoreSize: true", tool, StringComparison.Ordinal);
}

/// <summary>
/// The reader and the writer cannot disagree about which row is the whole-store row.
///
Expand Down
Loading
Loading