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
65 changes: 65 additions & 0 deletions Darling/Darling.Tests/McpToolGuideHeads.SqlCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -232,3 +232,68 @@ public void DarlingsTail_CarriesTheRollupTierDisclosure()
Assert.Contains("unique_queries=null (NOT 0", McpToolGuideTests.Served("get_daily_summary_range").Tail!, StringComparison.Ordinal);
}
}

/// <summary>
/// #3898 D2 head pins for <c>get_active_queries</c>, joining the sqlCore family. Converted out of
/// <c>DarlingMcpSessionTools</c> (Darling) and its Lite twin. Lite's twin is
/// <c>Lite.Tests/McpToolGuideHeadsSqlCoreActiveQueriesTests</c>. The head is byte-identical on both SKUs (the
/// generic cross-SKU pin in <see cref="McpToolGuideTests"/> also covers this); each product's own tail keeps its
/// own original prose, including the one line that names that product's own blocked-process-report tool.
/// </summary>
public sealed class McpToolGuideHeadsSqlCoreActiveQueriesTests
{
private static readonly (string Tool, string Fact)[] HeadFacts =
[
("get_active_queries", "database_name/blocking_only filter IN SQL"),
("get_active_queries", "total_snapshots is the filtered count"),
("get_active_queries", "truncated means over limit"),
("get_active_queries", "wait_time_ms, dop, granted_query_memory_gb, open_transaction_count: null = zero or not applicable"),
("get_active_queries", "Head blockers are never stripped"),
("get_active_queries", "not_captured, filtered, or past_page"),
("get_active_queries", "Empty: no snapshot in the window, or none matching filters"),
("get_active_queries", "not_collected (unfiltered only): engine can't run it"),
];

[Fact]
public void Head_CarriesItsGuardrailFacts_AndThePointer()
{
var served = McpToolGuideTests.Served("get_active_queries");
Assert.NotNull(served.Tail);
Assert.EndsWith(McpToolGuide.GuidePointer, served.Served, StringComparison.Ordinal);
Assert.True(served.Served.Length <= 620, $"get_active_queries: served head {served.Served.Length} is over the 620 target");
Assert.All(served.ParameterDescriptionLengths, p => Assert.True(p.Length <= 200, $"get_active_queries.{p.Parameter}: {p.Length} > 200"));

foreach (var (tool, fact) in HeadFacts)
{
Assert.Contains(fact, McpToolGuideTests.Served(tool).Served, StringComparison.Ordinal);
}
}

/// <summary>D2 trimmed <c>blocking_only</c>'s description from 238 to 134 chars to fit the 200-char
/// parameter cap; the elaboration sentence it lost moved verbatim into the tail rather than being dropped.</summary>
[Fact]
public void BlockingOnlyParameter_KeepsItsGuardrailClause_AndMovesTheRestToTheTail()
{
var served = McpToolGuideTests.Served("get_active_queries");
var blockingOnly = Assert.Single(served.ParameterDescriptionLengths, p => p.Parameter == "blocking_only");
Assert.True(blockingOnly.Length <= 200, $"blocking_only: {blockingOnly.Length} > 200");

Assert.Contains(
"Applied in SQL, so total_snapshots counts the blocking population and truncated is measured against it.",
served.Tail,
StringComparison.Ordinal);
}

/// <summary>The two blocking disclosures (D2's densest guardrail) both survive the move: the head states
/// the shape, Darling's own tail keeps the original prose that explains each reason code and names
/// Darling's own blocking tool, <c>get_blocking</c>.</summary>
[Fact]
public void BlockerNotShownCodes_AreExplainedInTheTail()
{
var tail = McpToolGuideTests.Served("get_active_queries").Tail!;
Assert.Contains("not_captured (the blocker held no running request at that capture", tail, StringComparison.Ordinal);
Assert.Contains("get_blocking has its input buffer", tail, StringComparison.Ordinal);
Assert.Contains("filtered (your database_name filter excluded it)", tail, StringComparison.Ordinal);
Assert.Contains("past_page (it is in the filtered population but beyond limit)", tail, StringComparison.Ordinal);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# DarlingMcpSessionTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks.
tool get_active_queries 1406
tool get_active_queries 616
param get_active_queries.as_of 167
param get_active_queries.blocking_only 238
param get_active_queries.blocking_only 134
param get_active_queries.database_name 156
param get_active_queries.hours_back 37
param get_active_queries.limit 149
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,13 @@ public static async Task<string> GetSessionStats(
}
}

[McpServerTool(Name = "get_active_queries"), Description("Gets active query snapshots captured from sys.dm_exec_requests. Shows what queries were running at each collection point: session ID, query text, wait type, CPU time, elapsed time, blocking info, DOP, and memory grants. Use hours_back to look at a specific time window — critical for finding what was running during a CPU spike or blocking event. EVERY FILTER IS PART OF THE QUERY: database_name and blocking_only are applied in SQL before the page is cut, total_snapshots is the count of snapshot rows in the window that pass your filters, snapshots_returned is how many you got, and truncated says the filtered population held more than limit — raise limit or narrow hours_back when it is true (NEWEST CAPTURE FIRST, highest CPU first within a capture; oldest_returned_collection_time / newest_returned_collection_time bound the page). HEAD BLOCKERS ARE NEVER STRIPPED: a session another row in the same capture names as its blocker is on the page whatever its text (including a WAITFOR shell holding locks), flagged is_head_blocker. A victim whose blocker is NOT on the page says why in blocker_not_shown: not_captured (the blocker held no running request at that capture — an idle open transaction is the classic case; get_blocking has its input buffer from the blocked-process report), filtered (your database_name filter excluded it), or past_page (it is in the filtered population but beyond limit).")]
[McpServerTool(Name = "get_active_queries"), Description("Active query snapshots (sys.dm_exec_requests) in a window ending at as_of: query text, wait, CPU/elapsed ms, blocking, DOP, memory grant GB. database_name/blocking_only filter IN SQL: total_snapshots is the filtered count; truncated means over limit. wait_time_ms, dop, granted_query_memory_gb, open_transaction_count: null = zero or not applicable. Head blockers are never stripped (is_head_blocker); a victim's blocker_not_shown is not_captured, filtered, or past_page. Empty: no snapshot in the window, or none matching filters; not_collected (unfiltered only): engine can't run it. <<GUIDE>> Gets active query snapshots captured from sys.dm_exec_requests. Shows what queries were running at each collection point: session ID, query text, wait type, CPU time, elapsed time, blocking info, DOP, and memory grants. Use hours_back to look at a specific time window — critical for finding what was running during a CPU spike or blocking event. EVERY FILTER IS PART OF THE QUERY: database_name and blocking_only are applied in SQL before the page is cut, total_snapshots is the count of snapshot rows in the window that pass your filters, snapshots_returned is how many you got, and truncated says the filtered population held more than limit — raise limit or narrow hours_back when it is true (NEWEST CAPTURE FIRST, highest CPU first within a capture; oldest_returned_collection_time / newest_returned_collection_time bound the page). Applied in SQL, so total_snapshots counts the blocking population and truncated is measured against it. HEAD BLOCKERS ARE NEVER STRIPPED: a session another row in the same capture names as its blocker is on the page whatever its text (including a WAITFOR shell holding locks), flagged is_head_blocker. A victim whose blocker is NOT on the page says why in blocker_not_shown: not_captured (the blocker held no running request at that capture — an idle open transaction is the classic case; get_blocking has its input buffer from the blocked-process report), filtered (your database_name filter excluded it), or past_page (it is in the filtered population but beyond limit).")]
public static async Task<string> GetActiveQueries(
NpgsqlDataSource postgres,
[Description("Server name or display name.")] string? server_name = null,
[Description("Hours of data to retrieve. Default 1.")] int hours_back = 1,
[Description("Filter to a specific database. Applied in SQL; a head blocker in ANOTHER database is then not on the page, and its victims say blocker_not_shown = filtered.")] string? database_name = null,
[Description("Show only queries involved in blocking: rows with blocking_session_id > 0, plus the head blockers those rows name in the same capture. Applied in SQL, so total_snapshots counts the blocking population and truncated is measured against it.")] bool blocking_only = false,
[Description("Show only queries involved in blocking: rows with blocking_session_id > 0, plus the head blockers those rows name in the same capture.")] bool blocking_only = false,
[Description("Maximum number of rows to return. Default 50. The page is bounded by limit, not by hours_back — truncated says whether the filtered window held more.")] int limit = 50,
[Description(McpHelpers.AsOfDescription)] string? as_of = null)
{
Expand Down
66 changes: 66 additions & 0 deletions Lite.Tests/McpToolGuideHeads.SqlCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -220,3 +220,69 @@ public void LitesTail_CarriesNoRollupTierDisclosure()
Assert.DoesNotContain("days_missing", McpToolGuideTests.Served("get_daily_summary_range").Tail!, StringComparison.Ordinal);
}
}

/// <summary>
/// #3898 D2 head pins for the Lite twin of <c>get_active_queries</c>, joining the sqlCore family. Darling's twin
/// is <c>Darling.Tests/McpToolGuideHeadsSqlCoreActiveQueriesTests</c>, which also holds the cross-SKU lockstep
/// pin. The head is byte-identical on both SKUs; Lite's own tail keeps its own original prose, including the one
/// line that names Lite's own blocked-process-report tool.
/// </summary>
public sealed class McpToolGuideHeadsSqlCoreActiveQueriesTests
{
private static readonly (string Tool, string Fact)[] HeadFacts =
[
("get_active_queries", "database_name/blocking_only filter IN SQL"),
("get_active_queries", "total_snapshots is the filtered count"),
("get_active_queries", "truncated means over limit"),
("get_active_queries", "wait_time_ms, dop, granted_query_memory_gb, open_transaction_count: null = zero or not applicable"),
("get_active_queries", "Head blockers are never stripped"),
("get_active_queries", "not_captured, filtered, or past_page"),
("get_active_queries", "Empty: no snapshot in the window, or none matching filters"),
("get_active_queries", "not_collected (unfiltered only): engine can't run it"),
];

[Fact]
public void Head_CarriesItsGuardrailFacts_AndThePointer()
{
var served = McpToolGuideTests.Served("get_active_queries");
Assert.NotNull(served.Tail);
Assert.EndsWith(McpToolGuide.GuidePointer, served.Served, StringComparison.Ordinal);
Assert.True(served.Served.Length <= 620, $"get_active_queries: served head {served.Served.Length} is over the 620 target");
Assert.All(served.ParameterDescriptionLengths, p => Assert.True(p.Length <= 200, $"get_active_queries.{p.Parameter}: {p.Length} > 200"));

foreach (var (tool, fact) in HeadFacts)
{
Assert.Contains(fact, McpToolGuideTests.Served(tool).Served, StringComparison.Ordinal);
}
}

/// <summary>D2 trimmed <c>blocking_only</c>'s description from 238 to 134 chars to fit the 200-char
/// parameter cap; the elaboration sentence it lost moved verbatim into the tail rather than being dropped.</summary>
[Fact]
public void BlockingOnlyParameter_KeepsItsGuardrailClause_AndMovesTheRestToTheTail()
{
var served = McpToolGuideTests.Served("get_active_queries");
var blockingOnly = Assert.Single(served.ParameterDescriptionLengths, p => p.Parameter == "blocking_only");
Assert.True(blockingOnly.Length <= 200, $"blocking_only: {blockingOnly.Length} > 200");

Assert.Contains(
"Applied in SQL, so total_snapshots counts the blocking population and truncated is measured against it.",
served.Tail,
StringComparison.Ordinal);
}

/// <summary>Lite's own tail names <c>get_blocked_process_reports</c>, never Darling's <c>get_blocking</c>;
/// the shared head names neither tool.</summary>
[Fact]
public void BlockerNotShownCodes_NameLitesOwnBlockingTool()
{
var served = McpToolGuideTests.Served("get_active_queries");
var tail = served.Tail!;
Assert.Contains("not_captured (the blocker held no running request at that capture", tail, StringComparison.Ordinal);
Assert.Contains("get_blocked_process_reports has its input buffer", tail, StringComparison.Ordinal);
Assert.DoesNotContain("get_blocking has its input buffer", tail, StringComparison.Ordinal);
Assert.Contains("filtered (your database_name filter excluded it)", tail, StringComparison.Ordinal);
Assert.Contains("past_page (it is in the filtered population but beyond limit)", tail, StringComparison.Ordinal);
Assert.DoesNotContain("get_blocking", served.Served, StringComparison.Ordinal);
}
}
4 changes: 2 additions & 2 deletions Lite.Tests/McpToolsListBudget/McpSessionTools.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# McpSessionTools: tools/list budget for #3898. Ceilings only go down; see McpToolsListBudgetTests. One block per tool, blank line between blocks.
tool get_active_queries 1421
tool get_active_queries 616
param get_active_queries.as_of 167
param get_active_queries.blocking_only 238
param get_active_queries.blocking_only 134
param get_active_queries.database_name 156
param get_active_queries.hours_back 37
param get_active_queries.limit 149
Expand Down
4 changes: 2 additions & 2 deletions Lite/Mcp/McpSessionTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ namespace PerformanceMonitorLite.Mcp;
[McpServerToolType]
public sealed class McpSessionTools
{
[McpServerTool(Name = "get_active_queries"), Description("Gets active query snapshots captured from sys.dm_exec_requests. Shows what queries were running at each collection point: session ID, query text, wait type, CPU time, elapsed time, blocking info, DOP, and memory grants. Use hours_back to look at a specific time window — critical for finding what was running during a CPU spike or blocking event. EVERY FILTER IS PART OF THE QUERY: database_name and blocking_only are applied in SQL before the page is cut, total_snapshots is the count of snapshot rows in the window that pass your filters, snapshots_returned is how many you got, and truncated says the filtered population held more than limit — raise limit or narrow hours_back when it is true (NEWEST CAPTURE FIRST, highest CPU first within a capture; oldest_returned_collection_time / newest_returned_collection_time bound the page). HEAD BLOCKERS ARE NEVER STRIPPED: a session another row in the same capture names as its blocker is on the page whatever its text (including a WAITFOR shell holding locks), flagged is_head_blocker. A victim whose blocker is NOT on the page says why in blocker_not_shown: not_captured (the blocker held no running request at that capture — an idle open transaction is the classic case; get_blocked_process_reports has its input buffer from the blocked-process report), filtered (your database_name filter excluded it), or past_page (it is in the filtered population but beyond limit).")]
[McpServerTool(Name = "get_active_queries"), Description("Active query snapshots (sys.dm_exec_requests) in a window ending at as_of: query text, wait, CPU/elapsed ms, blocking, DOP, memory grant GB. database_name/blocking_only filter IN SQL: total_snapshots is the filtered count; truncated means over limit. wait_time_ms, dop, granted_query_memory_gb, open_transaction_count: null = zero or not applicable. Head blockers are never stripped (is_head_blocker); a victim's blocker_not_shown is not_captured, filtered, or past_page. Empty: no snapshot in the window, or none matching filters; not_collected (unfiltered only): engine can't run it. <<GUIDE>> Gets active query snapshots captured from sys.dm_exec_requests. Shows what queries were running at each collection point: session ID, query text, wait type, CPU time, elapsed time, blocking info, DOP, and memory grants. Use hours_back to look at a specific time window — critical for finding what was running during a CPU spike or blocking event. EVERY FILTER IS PART OF THE QUERY: database_name and blocking_only are applied in SQL before the page is cut, total_snapshots is the count of snapshot rows in the window that pass your filters, snapshots_returned is how many you got, and truncated says the filtered population held more than limit — raise limit or narrow hours_back when it is true (NEWEST CAPTURE FIRST, highest CPU first within a capture; oldest_returned_collection_time / newest_returned_collection_time bound the page). Applied in SQL, so total_snapshots counts the blocking population and truncated is measured against it. HEAD BLOCKERS ARE NEVER STRIPPED: a session another row in the same capture names as its blocker is on the page whatever its text (including a WAITFOR shell holding locks), flagged is_head_blocker. A victim whose blocker is NOT on the page says why in blocker_not_shown: not_captured (the blocker held no running request at that capture — an idle open transaction is the classic case; get_blocked_process_reports has its input buffer from the blocked-process report), filtered (your database_name filter excluded it), or past_page (it is in the filtered population but beyond limit).")]
public static async Task<string> GetActiveQueries(
LocalDataService dataService,
ServerManager serverManager,
[Description("Server name or display name.")] string? server_name = null,
[Description("Hours of data to retrieve. Default 1.")] int hours_back = 1,
[Description("Filter to a specific database. Applied in SQL; a head blocker in ANOTHER database is then not on the page, and its victims say blocker_not_shown = filtered.")] string? database_name = null,
[Description("Show only queries involved in blocking: rows with blocking_session_id > 0, plus the head blockers those rows name in the same capture. Applied in SQL, so total_snapshots counts the blocking population and truncated is measured against it.")] bool blocking_only = false,
[Description("Show only queries involved in blocking: rows with blocking_session_id > 0, plus the head blockers those rows name in the same capture.")] bool blocking_only = false,
[Description("Maximum number of rows to return. Default 50. The page is bounded by limit, not by hours_back — truncated says whether the filtered window held more.")] int limit = 50,
[Description(McpHelpers.AsOfDescription)] string? as_of = null)
{
Expand Down
Loading