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
2 changes: 1 addition & 1 deletion Darling/Darling.Tests/AnalysisAsOfAnchorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ await DarlingMcpTools.GetAnalysisFindings(
bad anchor must never fall back to "now" and then run a real, persisting analysis. */
Assert.StartsWith(
"Invalid as_of",
await DarlingMcpTools.AnalyzeServer(service, postgres, ServerName, 1, "last tuesday"),
McpHelpers.ErrorMessageOf(await DarlingMcpTools.AnalyzeServer(service, postgres, ServerName, 1, "last tuesday")),
StringComparison.Ordinal);
Assert.Contains(
"future",
Expand Down
2 changes: 1 addition & 1 deletion Darling/Darling.Tests/AsOfWindowAnchorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -571,7 +571,7 @@ the totals are answers to a question nobody asked. */
/* 5. Refusals reach the caller as the tool's own message, not as a silently-different answer. */
var future = await DarlingMcpDataTools.GetWaitStats(postgres, ServerName, 4, 20, DateTime.UtcNow.AddDays(1).ToString("o"));
Assert.Contains("future", future, StringComparison.Ordinal);
Assert.StartsWith("Invalid as_of", await DarlingMcpDataTools.GetWaitStats(postgres, ServerName, 4, 20, "last tuesday"), StringComparison.Ordinal);
Assert.StartsWith("Invalid as_of", McpHelpers.ErrorMessageOf(await DarlingMcpDataTools.GetWaitStats(postgres, ServerName, 4, 20, "last tuesday")), StringComparison.Ordinal);

/* 6. An anchor older than anything the store holds is the read's honest empty, not a refusal. */
Assert.Equal(
Expand Down
7 changes: 4 additions & 3 deletions Darling/Darling.Tests/DarlingDailySummaryRangeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using System.Threading.Tasks;
using Npgsql;
using PerformanceMonitor.Collectors;
using PerformanceMonitor.Common;
using PerformanceMonitor.Darling.Service.Mcp;
using PerformanceMonitor.Darling.Storage;
using Xunit;
Expand Down Expand Up @@ -154,18 +155,18 @@ range rather than widening it. */
/* ── the span is bounded, and refused rather than clamped ── */
Assert.StartsWith(
"Invalid days_back value '0'",
await DarlingMcpHealthTools.GetDailySummaryRange(dataSource, ServerName, 0),
McpHelpers.ErrorMessageOf(await DarlingMcpHealthTools.GetDailySummaryRange(dataSource, ServerName, 0)),
StringComparison.Ordinal);
Assert.StartsWith(
"Invalid days_back value '367'",
await DarlingMcpHealthTools.GetDailySummaryRange(dataSource, ServerName, 367),
McpHelpers.ErrorMessageOf(await DarlingMcpHealthTools.GetDailySummaryRange(dataSource, ServerName, 367)),
StringComparison.Ordinal);

/* A bad anchor is refused too, rather than silently answered as of now — the failure the whole
parameter exists to remove. */
Assert.StartsWith(
"Invalid as_of",
await DarlingMcpHealthTools.GetDailySummaryRange(dataSource, ServerName, 30, "last tuesday"),
McpHelpers.ErrorMessageOf(await DarlingMcpHealthTools.GetDailySummaryRange(dataSource, ServerName, 30, "last tuesday")),
StringComparison.Ordinal);

bodySucceeded = true;
Expand Down
2 changes: 1 addition & 1 deletion Darling/Darling.Tests/DarlingMcpBlockingToolsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,7 @@ await DarlingMcpTestData.ExecAsync(connection, ct,
DarlingMcpTestData.AssertEnvelope(await DarlingMcpBlockingTools.GetDeadlockTrend(postgres, ServerName), ServerName, "trend");

/* Unknown server resolves to the listing error. */
Assert.StartsWith("Could not resolve server.", await DarlingMcpBlockingTools.GetDeadlocks(postgres, "darling-no-such-server"), StringComparison.Ordinal);
Assert.StartsWith("Could not resolve server.", McpHelpers.ErrorMessageOf(await DarlingMcpBlockingTools.GetDeadlocks(postgres, "darling-no-such-server")), StringComparison.Ordinal);

/* Empty store → the "empty" miss. */
await DeleteRowsAsync(connection, ct, keepServer: true);
Expand Down
3 changes: 2 additions & 1 deletion Darling/Darling.Tests/DarlingMcpDataToolsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1032,7 +1032,8 @@ public async Task DataTools_ReadPlantedRows_AgainstDevPostgres()

/* ---- server resolution flows through: an unknown name returns the listing error. */
var unknown = await DarlingMcpDataTools.GetMemoryStats(postgres, "darling-no-such-server");
Assert.StartsWith("Could not resolve server.", unknown, StringComparison.Ordinal);
Assert.True(McpHelpers.IsRefusalEnvelope(unknown), unknown);
Assert.StartsWith("Could not resolve server.", McpHelpers.ErrorMessageOf(unknown), StringComparison.Ordinal);

/* ---- an EMPTY store for a tool returns the #1224 miss, not a throw. */
await DeleteRowsAsync(connection, ct, keepServer: true);
Expand Down
2 changes: 1 addition & 1 deletion Darling/Darling.Tests/DarlingMcpDefaultTraceToolsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ await DarlingMcpTestData.ExecAsync(connection, ct,
JsonAssert.Contains("\"total_events\": 2", json);

/* Unknown server → the listing error; empty store → the miss. */
Assert.StartsWith("Could not resolve server.", await DarlingMcpDefaultTraceTools.GetDefaultTraceEvents(postgres, "darling-no-such-server"), StringComparison.Ordinal);
Assert.StartsWith("Could not resolve server.", McpHelpers.ErrorMessageOf(await DarlingMcpDefaultTraceTools.GetDefaultTraceEvents(postgres, "darling-no-such-server")), StringComparison.Ordinal);
await DeleteRowsAsync(connection, ct, keepServer: true);
Assert.Equal("empty", DarlingMcpTestData.StatusOf(await DarlingMcpDefaultTraceTools.GetDefaultTraceEvents(postgres, ServerName)));

Expand Down
6 changes: 4 additions & 2 deletions Darling/Darling.Tests/DarlingMcpFleetSweepToolsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,9 @@ public async Task BadInput_IsRefused_WithoutTouchingTheStore(

var result = await DarlingMcpFleetSweepTools.GetSweepReports(dead, NullLogger.Instance, hoursBack, asOf, sweepId, watchState);

Assert.Contains(expectedFragment, result, StringComparison.Ordinal);
/* The refusal is the `invalid` envelope since #3739; the fragments are pinned on its sentence. */
Assert.True(McpHelpers.IsRefusalEnvelope(result), result);
Assert.Contains(expectedFragment, McpHelpers.ErrorMessageOf(result), StringComparison.Ordinal);
/* A refusal, not a swallowed store error — the dead store was never reached. */
Assert.DoesNotContain("Error during get_sweep_reports", result, StringComparison.Ordinal);
}
Expand All @@ -170,7 +172,7 @@ public async Task ASweepIdAsk_IsParsedBeforeAnyWindowBinding()
/* The out-of-range hours_back would refuse FIRST if the window were bound on this path. */
var result = await DarlingMcpFleetSweepTools.GetSweepReports(dead, NullLogger.Instance, 999, null, "abc", null);

Assert.Contains("Invalid sweep_id value 'abc'", result, StringComparison.Ordinal);
Assert.Contains("Invalid sweep_id value 'abc'", McpHelpers.ErrorMessageOf(result), StringComparison.Ordinal);
}

/* ---------------- the shared builders: lane 3's shape pins cover this surface ---------------- */
Expand Down
2 changes: 1 addition & 1 deletion Darling/Darling.Tests/DarlingMcpHealthParserToolsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@ await DarlingMcpTestData.ExecAsync(connection, ct,
Assert.Equal(t.ToString("o"), scheduler.GetProperty("last_captured_at").GetString());

/* an unknown server resolves to the listing error. */
Assert.StartsWith("Could not resolve server.", await DarlingMcpHealthParserTools.GetSystemHealth(postgres, "darling-no-such-server"), StringComparison.Ordinal);
Assert.StartsWith("Could not resolve server.", McpHelpers.ErrorMessageOf(await DarlingMcpHealthParserTools.GetSystemHealth(postgres, "darling-no-such-server")), StringComparison.Ordinal);

/* An empty store is rung 4: nothing of any type was ever captured, so this is NOT a clean bill —
"unavailable" with source_observed false (#3541 A12). It used to answer "empty", the same word
Expand Down
2 changes: 1 addition & 1 deletion Darling/Darling.Tests/DarlingMcpHealthToolsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ public void SummaryDate_IsParsedExactly_OrRefusedNamingTheFormat(string input, b
else
{
Assert.Null(date);
Assert.StartsWith($"Invalid summary_date value '{input}'", error, StringComparison.Ordinal);
Assert.StartsWith($"Invalid summary_date value '{input}'", McpHelpers.ErrorMessageOf(error!), StringComparison.Ordinal);
Assert.Contains("yyyy-MM-dd", error, StringComparison.Ordinal);
}
}
Expand Down
10 changes: 7 additions & 3 deletions Darling/Darling.Tests/DarlingMcpPlanToolsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -250,9 +250,13 @@ public void PlanColumns_ExistInTheGeneratedCollectorTables()
[InlineData("")]
[InlineData(" ")]
[InlineData(null)]
public void AnalyzePlanXml_EmptyOrWhitespace_ReturnsBareMessage(string? input)
public void AnalyzePlanXml_EmptyOrWhitespace_IsRefused(string? input)
{
Assert.Equal("No plan XML provided.", DarlingMcpPlanTools.AnalyzePlanXml(input!));
/* The refusal is McpHelpers.Refusal's `invalid` envelope since #3739 (it was this bare sentence); the
sentence is unchanged inside it and the parameter is named. */
var refusal = DarlingMcpPlanTools.AnalyzePlanXml(input!);
Assert.Equal(McpHelpers.Refusal("plan_xml", "No plan XML provided."), refusal);
Assert.Equal("No plan XML provided.", McpHelpers.ErrorMessageOf(refusal));
}

[Theory]
Expand Down Expand Up @@ -495,7 +499,7 @@ public async Task PlanTools_FetchAndAnalyzeStoredPlans_AgainstDevPostgres()

/* ---- server resolution flows through the tool: an unknown name returns the listing error. */
var unknown = await DarlingMcpPlanTools.AnalyzeQueryPlan(postgres, QueryHash, "darling-mcp-no-such-server");
Assert.StartsWith("Could not resolve server.", unknown, StringComparison.Ordinal);
Assert.StartsWith("Could not resolve server.", McpHelpers.ErrorMessageOf(unknown), StringComparison.Ordinal);

bodySucceeded = true;
}
Expand Down
10 changes: 6 additions & 4 deletions Darling/Darling.Tests/DarlingMcpToolsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -244,9 +244,11 @@ public void Resolver_NoName_MultipleServers_ErrorsListingAll()

Assert.Equal(default, resolved);
Assert.NotNull(error);
Assert.StartsWith("Could not resolve server.", error, StringComparison.Ordinal);
Assert.Contains("SQL2022", error, StringComparison.Ordinal);
Assert.Contains("Production (PROD1)", error, StringComparison.Ordinal);
Assert.True(McpHelpers.IsRefusalEnvelope(error), error);
var sentence = McpHelpers.ErrorMessageOf(error!);
Assert.StartsWith("Could not resolve server.", sentence, StringComparison.Ordinal);
Assert.Contains("SQL2022", sentence, StringComparison.Ordinal);
Assert.Contains("Production (PROD1)", sentence, StringComparison.Ordinal);
}

[Fact]
Expand Down Expand Up @@ -588,7 +590,7 @@ the shared "empty" Status envelope. */

var unknown = await DarlingMcpTools.GetAnalysisFindings(
analysisService, postgres, "darling-mcp-no-such-server", 24);
Assert.StartsWith("Could not resolve server.", unknown, StringComparison.Ordinal);
Assert.StartsWith("Could not resolve server.", McpHelpers.ErrorMessageOf(unknown), StringComparison.Ordinal);
Assert.Contains(TestServerName, unknown, StringComparison.Ordinal);

/* ---- mute via the tool: the muted envelope comes back and the row lands in
Expand Down
2 changes: 1 addition & 1 deletion Darling/Darling.Tests/DarlingMcpTrendToolsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -690,7 +690,7 @@ await DarlingMcpTestData.ExecAsync(connection, ct,
Assert.Equal("not_collected", DarlingMcpTestData.StatusOf(await DarlingMcpTrendTools.GetPerfmonTrend(postgres, "Page life expectancy", ServerName)));

/* an unknown server resolves to the listing error. */
Assert.StartsWith("Could not resolve server.", await DarlingMcpTrendTools.GetMemoryTrend(postgres, "darling-no-such-server"), StringComparison.Ordinal);
Assert.StartsWith("Could not resolve server.", McpHelpers.ErrorMessageOf(await DarlingMcpTrendTools.GetMemoryTrend(postgres, "darling-no-such-server")), StringComparison.Ordinal);

/* an EMPTY store returns the miss, not a throw. */
await DeleteRowsAsync(connection, ct, keepServer: true);
Expand Down
68 changes: 44 additions & 24 deletions Darling/Darling.Tests/DarlingPeerDisclosureTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using PerformanceMonitor.Common;
using PerformanceMonitor.Darling.Service;
using PerformanceMonitor.Darling.Service.Mcp;
using Xunit;
Expand Down Expand Up @@ -529,6 +530,17 @@ no mention of the siblings is the strongest version of the wrong conclusion. */
private const string MissWithoutPeers =
"Could not resolve server. Available servers:\nprod-sql-use1-beta-01";

/// <summary>The miss travels as the <c>invalid</c> envelope since #3739 (<c>McpHelpers.Refusal</c>, so the
/// ~190 tools that <c>return error;</c> put a status word on the wire); these pins are about the SENTENCE,
/// so each reads it back out of the envelope — and asserts, on the way, that the envelope is the one the
/// recognizer the web surface branches on actually fires for.</summary>
private static string Sentence(string? error)
{
Assert.NotNull(error);
Assert.True(McpHelpers.IsRefusalEnvelope(error), "the resolver's miss is not the `invalid` envelope: " + error);
return McpHelpers.ErrorMessageOf(error!);
}

[Fact]
public void ResolutionMiss_IsByteForByteUnchangedWithNothingDeclared()
{
Expand All @@ -538,7 +550,15 @@ public void ResolutionMiss_IsByteForByteUnchangedWithNothingDeclared()
DarlingPeerDirectory.Snapshot.Empty);

Assert.Equal(default, resolved);
Assert.Equal(MissWithoutPeers, error);
Assert.Equal(MissWithoutPeers, Sentence(error));

/* The envelope around it, whole (#3739): the refusal word, the sentence as message, and the parameter
the caller has to fix — byte-equal to the shared builder, so this is the one shape and not a
look-alike. */
Assert.Equal(McpHelpers.Refusal("server_name", MissWithoutPeers), error);
using var envelope = JsonDocument.Parse(error!);
Assert.Equal("invalid", envelope.RootElement.GetProperty("status").GetString());
Assert.Equal("server_name", envelope.RootElement.GetProperty("hints").GetProperty("parameter").GetString());
}

[Fact]
Expand All @@ -550,20 +570,20 @@ public void ResolutionMiss_NamesThePeerWhoseDeclaredCoverageMatches()
TwoPeers());

Assert.Equal(default, resolved);
Assert.NotNull(error);
var sentence = Sentence(error);

/* The prefix and the local listing survive: 'Could not resolve server.' is what callers key off, and
the local list is still the right answer to the commonest miss (a typo). */
Assert.StartsWith(MissWithoutPeers, error, StringComparison.Ordinal);
Assert.StartsWith(MissWithoutPeers, sentence, StringComparison.Ordinal);

Assert.Contains("'prod-sql-use2-beta-01' is not monitored HERE", error, StringComparison.Ordinal);
Assert.Contains("matches the declared coverage of peer store prod-sql-use2-monitor-01", error, StringComparison.Ordinal);
Assert.Contains("That is a SEPARATE Darling store", error, StringComparison.Ordinal);
Assert.Contains("this server cannot read it", error, StringComparison.Ordinal);
Assert.Contains($"This store covers: {Use1Covers}.", error, StringComparison.Ordinal);
Assert.Contains("'prod-sql-use2-beta-01' is not monitored HERE", sentence, StringComparison.Ordinal);
Assert.Contains("matches the declared coverage of peer store prod-sql-use2-monitor-01", sentence, StringComparison.Ordinal);
Assert.Contains("That is a SEPARATE Darling store", sentence, StringComparison.Ordinal);
Assert.Contains("this server cannot read it", sentence, StringComparison.Ordinal);
Assert.Contains($"This store covers: {Use1Covers}.", sentence, StringComparison.Ordinal);

/* The peer that declared no patterns must not be blamed for a name it never claimed. */
Assert.DoesNotContain("prod-sql-pg-monitor-01", error, StringComparison.Ordinal);
Assert.DoesNotContain("prod-sql-pg-monitor-01", sentence, StringComparison.Ordinal);
}

[Fact]
Expand All @@ -574,14 +594,14 @@ public void ResolutionMiss_WithNoMatchingPeer_ListsThemWithoutClaimingUnmonitore
"some-other-box",
TwoPeers());

Assert.NotNull(error);
Assert.Contains("'some-other-box' is not monitored HERE", error, StringComparison.Ordinal);
Assert.Contains("matches no declared peer store's coverage either", error, StringComparison.Ordinal);
var sentence = Sentence(error);
Assert.Contains("'some-other-box' is not monitored HERE", sentence, StringComparison.Ordinal);
Assert.Contains("matches no declared peer store's coverage either", sentence, StringComparison.Ordinal);

/* Both peers are still disclosed: the declarations are prose plus optional patterns, not a live
registry, so "no pattern matched" is not evidence the server is unmonitored. */
Assert.Contains("prod-sql-use2-monitor-01", error, StringComparison.Ordinal);
Assert.Contains("prod-sql-pg-monitor-01", error, StringComparison.Ordinal);
Assert.Contains("prod-sql-use2-monitor-01", sentence, StringComparison.Ordinal);
Assert.Contains("prod-sql-pg-monitor-01", sentence, StringComparison.Ordinal);
}

[Fact]
Expand All @@ -601,10 +621,10 @@ sentence must not say "That is a SEPARATE store" about a list of two. */
var (_, error) = DarlingServerResolver.ResolveOrError(
new[] { Registered("prod-sql-use1-beta-01") }, "prod-sql-use2-beta-01", overlapping);

Assert.NotNull(error);
Assert.Contains("these peer stores: box2 — the replicas; box3 — the archive replicas", error, StringComparison.Ordinal);
Assert.Contains("Those are SEPARATE Darling stores", error, StringComparison.Ordinal);
Assert.DoesNotContain("That is a SEPARATE Darling store", error, StringComparison.Ordinal);
var sentence = Sentence(error);
Assert.Contains("these peer stores: box2 — the replicas; box3 — the archive replicas", sentence, StringComparison.Ordinal);
Assert.Contains("Those are SEPARATE Darling stores", sentence, StringComparison.Ordinal);
Assert.DoesNotContain("That is a SEPARATE Darling store", sentence, StringComparison.Ordinal);
}

[Fact]
Expand All @@ -617,10 +637,10 @@ disclosure must list rather than accuse. */
" ",
TwoPeers());

Assert.NotNull(error);
Assert.StartsWith("Could not resolve server.", error, StringComparison.Ordinal);
Assert.Contains("That server is not monitored HERE", error, StringComparison.Ordinal);
Assert.DoesNotContain("matches the declared coverage", error, StringComparison.Ordinal);
var sentence = Sentence(error);
Assert.StartsWith("Could not resolve server.", sentence, StringComparison.Ordinal);
Assert.Contains("That server is not monitored HERE", sentence, StringComparison.Ordinal);
Assert.DoesNotContain("matches the declared coverage", sentence, StringComparison.Ordinal);
}

[Fact]
Expand All @@ -644,7 +664,7 @@ public void ResolutionMiss_ReadsTheAmbientDeclaration_ThroughTheTwoArgOverload()
new[] { Registered("prod-sql-use1-beta-01") },
"prod-sql-use2-beta-01");

Assert.Contains("peer store box2 — the replicas", error, StringComparison.Ordinal);
Assert.Contains("peer store box2 — the replicas", Sentence(error), StringComparison.Ordinal);
}
finally
{
Expand All @@ -657,6 +677,6 @@ public void ResolutionMiss_ReadsTheAmbientDeclaration_ThroughTheTwoArgOverload()
new[] { Registered("prod-sql-use1-beta-01") },
"prod-sql-use2-beta-01");

Assert.Equal(MissWithoutPeers, afterReset);
Assert.Equal(MissWithoutPeers, Sentence(afterReset));
}
}
Loading
Loading