feat(host): warn when Cluster:Roles and OTOPCUA_ROLES disagree; boot banner names the winning source

This commit is contained in:
Joseph Doherty
2026-08-20 10:08:03 -04:00
parent e46c1db5e5
commit 3314a1f98d
4 changed files with 92 additions and 14 deletions
@@ -210,4 +210,45 @@ public sealed class EffectiveRolesTests
roles.Gossiped.ShouldBe(new[] { "admin", "driver" });
}
}
// ---- DisagreementWarning (choice 5, T6) ----------------------------------------------------
[Fact]
public void Both_sources_set_and_agreeing_has_no_warning()
{
var roles = EffectiveRoles.Resolve(["driver"], "driver");
roles.DisagreementWarning.ShouldBeNull();
}
[Fact]
public void Only_one_source_set_has_no_warning()
{
EffectiveRoles.Resolve(["driver"], null).DisagreementWarning.ShouldBeNull();
EffectiveRoles.Resolve(null, "driver").DisagreementWarning.ShouldBeNull();
EffectiveRoles.Resolve(null, null).DisagreementWarning.ShouldBeNull();
}
[Fact]
public void Both_sources_set_and_disagreeing_produces_a_warning_naming_both_keys()
{
var roles = EffectiveRoles.Resolve(["admin"], "driver,cluster-SITE-A");
roles.DisagreementWarning.ShouldNotBeNull();
roles.DisagreementWarning.ShouldContain("Cluster:Roles");
roles.DisagreementWarning.ShouldContain(RoleParser.RolesEnvVar);
// Configured still wins outright — the warning is observability, not a behaviour change.
roles.Gossiped.ShouldBe(new[] { "admin" });
roles.Source.ShouldBe(RoleSource.Configured);
}
[Fact]
public void Both_sources_set_but_only_differing_by_case_do_not_warn()
{
// Same normalized set spelled differently in each source is NOT the "Cluster:Roles wins
// unintentionally" shape the warning exists for.
var roles = EffectiveRoles.Resolve(["Driver"], "driver");
roles.DisagreementWarning.ShouldBeNull();
}
}