fix(galaxy): seed platforms with one template filter per request (#864)
The gateway's shared GalaxyHierarchyProjector ANDs template_chain_contains entries, so the single request carrying both $WinPlatform and $AppEngine matched no object and the probe watcher always seeded 0 platforms. GatewayGalaxyPlatformSource now runs one paged sweep per template and unions/dedupes by tag name. The tests use a fake gateway that applies the projector's AND semantics (they fail on the old single request). Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
This commit is contained in:
+84
-18
@@ -15,33 +15,99 @@ public sealed class GatewayGalaxyPlatformSourceTests
|
||||
return o;
|
||||
}
|
||||
|
||||
/// <summary>Every request carries the platform template filter, no attributes, and the page size; replies are re-filtered.</summary>
|
||||
[Fact]
|
||||
public async Task Requests_filter_on_platform_templates_without_attributes_and_replies_are_refiltered()
|
||||
/// <summary>
|
||||
/// A fake gateway that applies the request filters with the SAME semantics as the shared
|
||||
/// <c>GalaxyHierarchyProjector.MatchesFilters</c>: every <c>template_chain_contains</c> token must
|
||||
/// match (AND, case-insensitive substring over the chain), <c>category_ids</c> is an OR set. Pages
|
||||
/// the filtered result at the request's page size.
|
||||
/// </summary>
|
||||
private sealed class FilteringGateway(params GalaxyObject[] galaxy)
|
||||
{
|
||||
var requests = new List<DiscoverHierarchyRequest>();
|
||||
Task<DiscoverHierarchyReply> Discover(DiscoverHierarchyRequest req, CancellationToken ct)
|
||||
public List<DiscoverHierarchyRequest> Requests { get; } = [];
|
||||
|
||||
public Task<DiscoverHierarchyReply> Discover(DiscoverHierarchyRequest req, CancellationToken ct)
|
||||
{
|
||||
requests.Add(req);
|
||||
Requests.Add(req.Clone());
|
||||
var matched = galaxy.Where(o => Matches(o, req)).ToList();
|
||||
var offset = req.PageToken == "" ? 0 : int.Parse(req.PageToken.Split(':')[1]);
|
||||
var reply = new DiscoverHierarchyReply();
|
||||
if (req.PageToken == "")
|
||||
reply.Objects.Add(matched.Skip(offset).Take(req.PageSize));
|
||||
if (offset + req.PageSize < matched.Count)
|
||||
{
|
||||
reply.Objects.Add([Obj("Plat01", "$WinPlatform"), Obj("Tank01", "$UserDefined")]);
|
||||
reply.NextPageToken = "p2";
|
||||
}
|
||||
else
|
||||
{
|
||||
reply.Objects.Add([Obj("Eng01", "$AppEngine", "$WinPlatform"), Obj("Plat01", "$WinPlatform"), Obj("", "$WinPlatform")]);
|
||||
reply.NextPageToken = $"{string.Join(",", req.TemplateChainContains)}:{offset + req.PageSize}";
|
||||
}
|
||||
return Task.FromResult(reply);
|
||||
}
|
||||
|
||||
var names = await GatewayGalaxyPlatformSource.QueryAsync(Discover, pageSize: 50, CancellationToken.None);
|
||||
private static bool Matches(GalaxyObject obj, DiscoverHierarchyRequest req)
|
||||
{
|
||||
if (req.CategoryIds.Count > 0 && !req.CategoryIds.Contains(obj.CategoryId)) return false;
|
||||
foreach (var filter in req.TemplateChainContains)
|
||||
{
|
||||
if (!obj.TemplateChain.Any(t => t.Contains(filter, StringComparison.OrdinalIgnoreCase))) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
names.ShouldBe(["Plat01", "Eng01"]);
|
||||
requests.Count.ShouldBe(2);
|
||||
requests.ShouldAllBe(r => r.PageSize == 50 && r.IncludeAttributes == false);
|
||||
requests.ShouldAllBe(r => r.TemplateChainContains.Contains("$WinPlatform") && r.TemplateChainContains.Contains("$AppEngine"));
|
||||
/// <summary>
|
||||
/// #864: against a gateway that ANDs template filters (as the real one does), every deployed
|
||||
/// <c>$WinPlatform</c> AND every <c>$AppEngine</c> is found — the old single request carrying both
|
||||
/// tokens matched none. Shape of the VD03 DEV galaxy: 3 platforms, 2 engines.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Finds_every_platform_and_engine_against_an_AND_filtering_gateway()
|
||||
{
|
||||
var gw = new FilteringGateway(
|
||||
Obj("CTRX_DEV", "$WinPlatform"),
|
||||
Obj("DevWinPlatform", "$WinPlatform"),
|
||||
Obj("JoePlatform", "$JoePlatformTemplate", "$WinPlatform"),
|
||||
Obj("DevAppEngine", "$AppEngine"),
|
||||
Obj("JoeAppEngine", "$AppEngine"),
|
||||
Obj("Tank01", "$UserDefined"),
|
||||
Obj("Area01", "$Area"));
|
||||
|
||||
var names = await GatewayGalaxyPlatformSource.QueryAsync(gw.Discover, pageSize: 50, CancellationToken.None);
|
||||
|
||||
names.ShouldBe(["CTRX_DEV", "DevWinPlatform", "JoePlatform", "DevAppEngine", "JoeAppEngine"]);
|
||||
gw.Requests.ShouldAllBe(r => r.TemplateChainContains.Count == 1 && r.PageSize == 50 && r.IncludeAttributes == false);
|
||||
gw.Requests.Select(r => r.TemplateChainContains[0]).Distinct().ShouldBe(["$WinPlatform", "$AppEngine"]);
|
||||
}
|
||||
|
||||
/// <summary>Each per-template sweep pages to completion independently; tag names are de-duplicated across sweeps.</summary>
|
||||
[Fact]
|
||||
public async Task Each_template_sweep_pages_independently_and_names_are_deduplicated()
|
||||
{
|
||||
var gw = new FilteringGateway(
|
||||
Obj("Plat01", "$WinPlatform"),
|
||||
Obj("Plat02", "$WinPlatform"),
|
||||
Obj("Plat03", "$WinPlatform"),
|
||||
// An object whose chain carries both templates is reported once.
|
||||
Obj("Hybrid01", "$AppEngine", "$WinPlatform"),
|
||||
Obj("Eng01", "$AppEngine"),
|
||||
Obj("Eng02", "$AppEngine"),
|
||||
Obj("", "$WinPlatform"));
|
||||
|
||||
var names = await GatewayGalaxyPlatformSource.QueryAsync(gw.Discover, pageSize: 2, CancellationToken.None);
|
||||
|
||||
names.ShouldBe(["Plat01", "Plat02", "Plat03", "Hybrid01", "Eng01", "Eng02"]);
|
||||
// $WinPlatform: 5 matches (incl. the empty tag) => 3 pages; $AppEngine: 3 matches => 2 pages.
|
||||
gw.Requests.Count(r => r.TemplateChainContains[0] == "$WinPlatform").ShouldBe(3);
|
||||
gw.Requests.Count(r => r.TemplateChainContains[0] == "$AppEngine").ShouldBe(2);
|
||||
}
|
||||
|
||||
/// <summary>A gateway that ignores the filter still yields only platforms (replies are re-filtered on the chain).</summary>
|
||||
[Fact]
|
||||
public async Task Replies_are_refiltered_on_the_template_chain()
|
||||
{
|
||||
Task<DiscoverHierarchyReply> Discover(DiscoverHierarchyRequest req, CancellationToken ct)
|
||||
{
|
||||
var reply = new DiscoverHierarchyReply();
|
||||
reply.Objects.Add([Obj("Plat01", "$WinPlatform"), Obj("Tank01", "$UserDefined"), Obj("Eng01", "$AppEngine")]);
|
||||
return Task.FromResult(reply);
|
||||
}
|
||||
|
||||
(await GatewayGalaxyPlatformSource.QueryAsync(Discover, pageSize: 50, CancellationToken.None)).ShouldBe(["Plat01", "Eng01"]);
|
||||
}
|
||||
|
||||
/// <summary>A gateway that strips the template chain is trusted to have filtered server-side.</summary>
|
||||
|
||||
Reference in New Issue
Block a user