mounts: clear a failed mount of its own whose source was away, and the failure of a unit it takes over, so neither is said as the machine's own failed unit (hq ADR 0263 rules 4 and 6)
mesh/merge-gate pass: builds mounts, sent nowhere; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery superseded: a newer head of the same pull request

This commit is contained in:
jochen
2026-10-09 14:26:01 +02:00
parent 9e4267bf28
commit 0f330fc57d
4 changed files with 199 additions and 5 deletions
+101
View File
@@ -0,0 +1,101 @@
package main
import (
"context"
"strings"
"testing"
"time"
)
// A mount the module took over, or one whose source was away when it was touched, is never left as a
// failed unit: the node-engine would say it as a failed unit of the machine's own, with a notification each
// time (hq ADR 0263 rules 4 and 6). A mount that failed while its source answers stays failed: a fault.
func failedLook(t *testing.T, f *fakeSystem) map[string]ShareStatus {
t.Helper()
st := f.applier(time.Date(2026, 10, 9, 14, 0, 0, 0, time.UTC)).Look(context.Background())
out := map[string]ShareStatus{}
for _, s := range st.Shares {
out[s.Name] = s
}
return out
}
func TestTheFailureOfTheUnitTheModuleTakesOverIsCleared(t *testing.T) {
f := newSystem(t)
f.write(ConfigFile, "shares=library=/srv/library\nserver=homeserver\nat=192.0.2.10\nnode=workstation\n")
f.write(ExportsFile, exportsAnswer(t, library))
f.reachable["192.0.2.10:2049"] = true
// fstab's generator made srv-library.mount for the same mount point, and it timed out at boot.
f.units["srv-library.mount"] = map[string]string{"ActiveState": "failed", "Result": "timeout"}
states := failedLook(t, f)
if !f.called("systemctl reset-failed srv-library.mount") {
t.Fatalf("the generator's failure is cleared once the module's unit is written: %q", f.calls)
}
if s := states["library"]; s.State != StateArmed {
t.Errorf("the share is judged on its own mounts, not on the generator's: %+v", s)
}
}
func TestAMountThatFailedBecauseItsSourceWasAwayIsCleared(t *testing.T) {
f := newSystem(t)
f.write(ConfigFile, "shares=library=/srv/library\nsources=games=smb://192.0.2.76/Share@/mnt/games\n"+
"server=homeserver\nat=192.0.2.10\nnode=workstation\naccount=operator\n"+
"secret=/var/lib/mesh/mounts/smb-credentials.secret\nstate=/var/lib/mesh/mounts\n")
f.write(ExportsFile, exportsAnswer(t, library))
f.write("/var/lib/mesh/mounts/smb-credentials.secret", "[games]\nusername=root\npassword="+testPassword+"\n")
a := f.applier(time.Date(2026, 10, 9, 14, 0, 0, 0, time.UTC))
a.Look(context.Background()) // writes and arms both pairs; neither source answers
f.calls = nil
// Someone opened both mount points: the gaming device is off and the server is away, so both mounts failed.
f.units["mnt-games.mount"] = map[string]string{"ActiveState": "failed", "Result": "timeout"}
f.units["srv-library.mount"] = map[string]string{"ActiveState": "failed", "Result": "timeout"}
cleared := a.clearFailed(context.Background())
for _, unit := range []string{"mnt-games.mount", "srv-library.mount"} {
if !f.called("systemctl reset-failed " + unit) {
t.Errorf("%s failed because its source was away and is cleared: %q", unit, f.calls)
}
}
if len(cleared) != 2 || !strings.Contains(strings.Join(cleared, "; "), "games was not there to mount") {
t.Errorf("each clearing is said: %q", cleared)
}
states := failedLook(t, f)
if states["games"].State != StateAbsent || states["library"].State != StateUnreachable {
t.Errorf("the module still says each as absent or unreachable: %+v", states)
}
}
func TestAMountThatFailedWhileItsSourceAnswersStaysFailed(t *testing.T) {
f := newSystem(t)
f.write(ConfigFile, "shares=library=/srv/library\nserver=homeserver\nat=192.0.2.10\nnode=workstation\n")
f.write(ExportsFile, exportsAnswer(t, library))
f.reachable["192.0.2.10:2049"] = true
a := f.applier(time.Date(2026, 10, 9, 14, 0, 0, 0, time.UTC))
a.Look(context.Background())
f.calls = nil
f.units["srv-library.mount"] = map[string]string{"ActiveState": "failed", "Result": "exit-code"}
if cleared := a.clearFailed(context.Background()); len(cleared) != 0 || f.called("systemctl reset-failed") {
t.Fatalf("a mount refused by a server that answers is a fault and is left failed: %q %q", cleared, f.calls)
}
if s := failedLook(t, f)["library"]; s.State != StateFailed {
t.Errorf("it is said failed: %+v", s)
}
}
func TestOnlyTheActingProcessClears(t *testing.T) {
f := newSystem(t)
f.write(ConfigFile, "shares=library=/srv/library\nserver=homeserver\nat=192.0.2.10\nnode=workstation\n")
f.write(ExportsFile, exportsAnswer(t, library))
f.units["srv-library.mount"] = map[string]string{"ActiveState": "failed", "Result": "timeout"}
a := f.applier(time.Now())
a.Act = false
a.Look(context.Background())
if f.called("systemctl reset-failed") {
t.Errorf("the judging process changes nothing: %q", f.calls)
}
}