fix(overview): resolve a group's leader against its own members (scadaproj#43)

LeaderResolver searched every instance of the application, so co-located
groups (QA: ScadaBridge central-a and site-a both on vq03) got the
first-listed group's instance name on their leader chip. Resolution is now
scoped to the group's members; an unmatched address keeps its raw form.

- tests: QA co-located central/site layout; cross-group host not borrowed
- CLAUDE.md: correct the leader-resolution / 127.0.0.1 guidance
- code-reviews: Overview-002 Resolved; README regenerated

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
Joseph Doherty
2026-11-09 14:43:18 -05:00
parent c25acd6093
commit 6c81c0bc0a
5 changed files with 60 additions and 25 deletions
@@ -171,6 +171,40 @@ public class LeaderResolverTests
Assert.All(groups, g => Assert.False(g.Disagreement));
}
[Fact]
public void Resolve_CoLocatedGroups_ResolveTheLeaderWithinTheirOwnGroup()
{
// Overview-002 (scadaproj#43): the QA layout — ScadaBridge central-a and site-a both on
// vq03, central-b and site-b on vq04, all in ONE application. Resolving against every
// instance named the site group's leader "central-a" (a node in another Akka cluster).
var groups = LeaderResolver.Resolve(
[
Snapshots.Instance("central-a", "central", "http://vq03:9000", leader: "akka.tcp://scadabridge@vq03:8081"),
Snapshots.Instance("central-b", "central", "http://vq04:9000", leader: "akka.tcp://scadabridge@vq03:8081"),
Snapshots.Instance("site-a", "site", "http://vq03:8084", leader: "akka.tcp://scadabridge@vq03:8082"),
Snapshots.Instance("site-b", "site", "http://vq04:8084", leader: "akka.tcp://scadabridge@vq03:8082"),
]);
Assert.Equal("central-a", groups.Single(g => g.Name == "central").LeaderInstance);
Assert.Equal("site-a", groups.Single(g => g.Name == "site").LeaderInstance);
}
[Fact]
public void Resolve_LeaderHostOnlyInAnotherGroup_IsNotAttributedAcrossGroups()
{
// A leader that matches no member of its own group keeps its raw address rather than
// borrowing a name from a different cluster that happens to share the host.
var groups = LeaderResolver.Resolve(
[
Snapshots.Instance("central-a", "central", "http://shared-host:9000", leader: "akka.tcp://scadabridge@shared-host:8081"),
Snapshots.Instance("site-a", "site", "http://127.0.0.1:8084", leader: "akka.tcp://scadabridge@shared-host:8082"),
]);
var site = groups.Single(g => g.Name == "site");
Assert.Null(site.LeaderInstance);
Assert.Equal("akka.tcp://scadabridge@shared-host:8082", site.LeaderAddress);
}
[Theory]
[InlineData("akka.tcp://scadabridge@site-a-a:8082", "site-a-a")]
[InlineData("akka.tcp://otopcua@10.100.0.35:4053", "10.100.0.35")]