9056f01fae
Source: felhom.eu/documentation/audits/DIAG-agent-channel-2026-07-26.md bootstrap.DetectEndpointDrift names a controller.yaml vs bootstrap.json local_api.endpoint divergence -- one ERROR carrying BOTH values and BOTH paths, its own event type local_api_endpoint_drift, and its own Hungarian banner shown ABOVE the channel banner because drift is the cause and "agent unreachable" the symptom. It writes NOTHING: reconciling from bootstrap.json would clobber a correct controller.yaml on any half-provisioned or hand-repaired guest, so the authority ruling is deferred to R-78. Fail-safe silent on absent/unparseable/ incomplete bootstrap and on an empty endpoint (ensureLocalAPI's fill-if-missing path is untouched). Fingerprint compared as a BOOLEAN only; token never compared, logged or exposed. EffectiveProtected now gates samba on Enabled && UserSet, mirroring BOTH of reconcileSambaAt's early returns, and the doc comment is corrected in the same change -- it claimed "detection and deployment agree in both directions" while citing only !smb.Enabled, an assertion that went false when !smb.UserSet was added. Not over-suppressed: sharing on WITH a password and a dead container still alarms. Channel log: the debounce placeholder is stateUnconfirmed (rendered "unseeded") instead of "up", so a born-down channel no longer logs "up->down" and orUnseeded stops being dead code. Logging only -- the placeholder is still matched in the re-arm condition, so F2 born-down alerting is byte-for-byte unchanged and all nine pre-existing channelhealth tests pass. Tests 951 -> 959, all green. Red-proofs A (both directions), E and F. MinAgent unchanged; felhom-agent untouched.
115 lines
5.0 KiB
Go
115 lines
5.0 KiB
Go
package monitor
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/config"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/infra"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
|
)
|
|
|
|
func contains(ss []string, want string) bool {
|
|
for _, s := range ss {
|
|
if s == want {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// EffectiveProtected must drop cloudflared when no tunnel token is configured (LAN-only node), so the
|
|
// health loop doesn't report it missing forever — but keep it when a token IS configured.
|
|
func TestEffectiveProtectedDropsCloudflaredWithoutToken(t *testing.T) {
|
|
base := config.StacksConfig{Protected: []string{"traefik", "cloudflared", "felhom-controller", "filebrowser"}}
|
|
|
|
cfgNoTok := &config.Config{Stacks: base}
|
|
got := EffectiveProtected(cfgNoTok, settings.SMBSettings{})
|
|
if contains(got, "cloudflared") {
|
|
t.Errorf("cloudflared must be dropped when no tunnel token: %v", got)
|
|
}
|
|
for _, must := range []string{"traefik", "felhom-controller", "filebrowser"} {
|
|
if !contains(got, must) {
|
|
t.Errorf("%s must remain protected: %v", must, got)
|
|
}
|
|
}
|
|
|
|
cfgTok := &config.Config{Stacks: base}
|
|
cfgTok.Infrastructure.CFTunnelToken = "tok"
|
|
if !contains(EffectiveProtected(cfgTok, settings.SMBSettings{}), "cloudflared") {
|
|
t.Error("cloudflared must remain protected when a tunnel token is configured")
|
|
}
|
|
}
|
|
|
|
// R-7b Scenario E, BOTH directions. Sharing is a customer-toggled feature, so the samba container can
|
|
// never be in the golden controller.yaml — the effective set must add it dynamically when sharing is
|
|
// live (so a dead sharing service raises the same protected-container issue as a dead traefik) and must
|
|
// leave it out when sharing is OFF (so a box that never enabled it never reports a missing container).
|
|
// Red-proof: delete the samba append and the enabled case fails.
|
|
//
|
|
// R-77 TIGHTENED THE "ON" CASE, and this test was updated with it: "on" now means
|
|
// Enabled AND UserSet, because reconcileSambaAt refuses to deploy without a household password. The
|
|
// previous fixture used Enabled alone and therefore asserted the very behaviour that produced the
|
|
// live false alarm on demo-hp (2026-07-26). The three-state matrix is in
|
|
// TestScenarioE_SambaProtectedOnlyWhenActuallyDeployed below.
|
|
func TestEffectiveProtectedTracksSharingToggle(t *testing.T) {
|
|
cfg := &config.Config{Stacks: config.StacksConfig{Protected: []string{"traefik", "felhom-controller"}}}
|
|
|
|
on := EffectiveProtected(cfg, settings.SMBSettings{Enabled: true, UserSet: true})
|
|
if !contains(on, infra.SambaContainerName) {
|
|
t.Errorf("sharing ON: %q must be watched, got %v", infra.SambaContainerName, on)
|
|
}
|
|
off := EffectiveProtected(cfg, settings.SMBSettings{Enabled: false})
|
|
if contains(off, infra.SambaContainerName) {
|
|
t.Errorf("sharing OFF: %q must NOT be watched, got %v", infra.SambaContainerName, off)
|
|
}
|
|
// The base set is untouched in both directions.
|
|
for _, set := range [][]string{on, off} {
|
|
for _, must := range []string{"traefik", "felhom-controller"} {
|
|
if !contains(set, must) {
|
|
t.Errorf("%s must remain protected: %v", must, set)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// R-77 Scenario E — the samba protected-set gate must mirror reconcileSambaAt's BOTH early returns.
|
|
//
|
|
// The live false alarm (demo-hp, 2026-07-26): sharing was enabled without a household password, so
|
|
// reconcileSambaAt deliberately did not deploy the container, but EffectiveProtected added it anyway
|
|
// and the box reported health=fail for a state the controller itself had chosen.
|
|
func TestScenarioE_SambaProtectedOnlyWhenActuallyDeployed(t *testing.T) {
|
|
cfg := &config.Config{Stacks: config.StacksConfig{
|
|
Protected: []string{"traefik", "cloudflared", "felhom-controller", "filebrowser"}}}
|
|
cfg.Infrastructure.CFTunnelToken = "tok" // keep cloudflared in, so the samba change is isolated
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
smb settings.SMBSettings
|
|
want bool
|
|
why string
|
|
}{
|
|
{"(1) sharing OFF", settings.SMBSettings{Enabled: false, UserSet: false}, false,
|
|
"a box that never enabled sharing must stay quiet"},
|
|
{"(2) sharing ON, no password", settings.SMBSettings{Enabled: true, UserSet: false}, false,
|
|
"THE BUG: reconcileSambaAt refuses to deploy without a password — a deliberate state, not a fault"},
|
|
{"(3) sharing ON, password set", settings.SMBSettings{Enabled: true, UserSet: true}, true,
|
|
"the container really should be running — a dead one must STILL alarm (do not over-suppress)"},
|
|
} {
|
|
if got := contains(EffectiveProtected(cfg, tc.smb), infra.SambaContainerName); got != tc.want {
|
|
t.Errorf("%s: samba protected = %v, want %v — %s", tc.name, got, tc.want, tc.why)
|
|
}
|
|
}
|
|
|
|
// Not over-suppressed: the rest of the protected set is untouched in every sharing state.
|
|
for _, smb := range []settings.SMBSettings{
|
|
{Enabled: false}, {Enabled: true}, {Enabled: true, UserSet: true},
|
|
} {
|
|
set := EffectiveProtected(cfg, smb)
|
|
for _, must := range []string{"traefik", "cloudflared", "felhom-controller", "filebrowser"} {
|
|
if !contains(set, must) {
|
|
t.Errorf("smb=%+v: %q must always stay protected, got %v", smb, must, set)
|
|
}
|
|
}
|
|
}
|
|
}
|