carlos / platform Public

edge: wake witnesses for a losing platform hold beside a ready platform route and a winning hold leaving its file (Task 5 review round 1)

A losing platform hold arriving, expiring, reviving and leaving beside a
ready platform route of the same name wakes nothing; a live winning
platform hold leaving its file, the ready fold unmoved, wakes the manager
and the parent leaves. The matrix's field comment names annie and dolly.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 1f992d20faca3574b1a76ebeb0cf7185c9f15820 parent fe9c8fc
1 file changed, +97 −1
  • internal/edge/homemap_test.go +97 −1

CI failed — run details

CI log
=== gate ===
sigwho: pid 2 ppid 1 pgid 0 sid 0; child pid 3
.amadan/test-gate.sh
gate: 2026-10-24T04:50:07Z on aarch64, 32 cpus, 62094 MB available, go1.26.8 linux/arm64
gate: +0s start launchctx
gate: +0s start vet
gate: +0s start gofmt
gate: +1s start rest
gate: +12s start internal_console@0
gate: +12s start internal_console@1
gate: +12s start internal_console@2
gate: +12s start internal_console@3
gate: +12s start internal_console@4
gate: +12s start internal_console@5
gate: +12s start internal_console@6
gate: +12s start internal_console@7
gate: +12s waiting for internal_console's shards before the next split package
gate: +172s start root@0
gate: +172s start root@1
gate: +172s start root@2
gate: +172s waiting for root's shards before the next split package

amadan: step gate failed: signal: terminated

amadan: job failed at step gate — its output is above, not at the tail of this log
diff --git a/internal/edge/homemap_test.go b/internal/edge/homemap_test.go
index 75d8c2eea..ac532fe72 100644
--- a/internal/edge/homemap_test.go
+++ b/internal/edge/homemap_test.go
@@ -2120,7 +2120,7 @@ func TestHoldingEntriesExpireFromTheFetchClock(t *testing.T) {
// faults runs before the poll at one minute; nil is no poll at all.
faults func(fx *freshScene)
wantErr bool
- // annie and carol are the fetch times the poll leaves on the hold's
+ // annie and dolly are the fetch times the poll leaves on the hold's
// file and the bystander's, after rankNow.
annie, dolly time.Duration
}
@@ -2429,3 +2429,99 @@ func TestExpiredPlatformHoldLeavesTheParentSetAndRevives(t *testing.T) {
waitWoke(t, &woke, 3)
waitParents(t, w, []string{"bdj." + apps})
}
+
+// wakeScene is a freshScene on an atomic clock (the wake reads it from its
+// own goroutine) with a real wildcard manager woken by its home map. set
+// moves the clock to d after rankNow; quiet asserts no further wake.
+type wakeScene struct {
+ *freshScene
+ w *wildcardManager
+ woke atomic.Int32
+ clock atomic.Int64
+}
+
+func newWakeScene(t *testing.T, apps string) *wakeScene {
+ t.Helper()
+ ws := &wakeScene{freshScene: newFreshScene(t, "carol")}
+ ws.set(0)
+ ws.m.now = func() time.Time { return time.Unix(0, ws.clock.Load()).UTC() }
+ ws.w = newWildcardManager(nil, apps, ws.e.deploymentHosts, nil, "", nil, nil)
+ ws.m.setHostsWake(func() { ws.w.refresh(); ws.woke.Add(1) })
+ return ws
+}
+
+func (ws *wakeScene) set(d time.Duration) { ws.clock.Store(rankNow.Add(d).UnixNano()) }
+
+func (ws *wakeScene) quiet(want int32, what string) {
+ ws.t.Helper()
+ time.Sleep(20 * time.Millisecond)
+ if n := ws.woke.Load(); n != want {
+ ws.t.Fatalf("%s: woke %d time(s), want %d", what, n, want)
+ }
+}
+
+// A platform hold that loses to a ready platform route of the same name is
+// no parent input of its own (the route already is one): its arriving, its
+// expiry and revival, and its leaving move nothing the manager derives, so
+// none of them wakes it.
+func TestLosingPlatformHoldBesideAReadyPlatformRouteWakesNothing(t *testing.T) {
+ const apps = "apps.example.com"
+ held := "cart.bdj." + apps
+ ws := newWakeScene(t, apps)
+ ready := fmt.Sprintf(`%q:{"role":"home","platform":true,"account":"bab","app":"shop","incarnation":1,"serving_seq":2}`, held)
+ ws.put("betty", "2.2.2.2", ready, "")
+ ws.mustPoll()
+ waitWoke(t, &ws.woke, 1)
+ waitParents(t, ws.w, []string{"bdj." + apps})
+
+ // The hold (1,1) arrives beside the route (1,2): the route wins.
+ ws.put("annie", "1.1.1.1", "", holdJSON(held, "mv_a", true, 1, 1))
+ ws.mustPoll()
+ if d := ws.m.resolveExact(held, ws.m.clock()); d.kind != decRemote || d.box != "betty" {
+ t.Fatalf("test bug: %s = %v %q, want betty's route over the losing hold", held, d.kind, d.box)
+ }
+ if !ws.m.snapshot().platformHolds[held] {
+ t.Fatal("test bug: the losing hold must be a platform hold candidate")
+ }
+ ws.quiet(1, "a losing platform hold arriving")
+
+ // Its file goes unread past five minutes, then is read again.
+ ws.set(holdingValidity)
+ ws.faults.set(false, false, edgesKey("annie"))
+ ws.mustPoll()
+ ws.quiet(1, "a losing platform hold expiring")
+ ws.set(6 * time.Minute)
+ ws.faults.set(false, false)
+ ws.mustPoll()
+ ws.quiet(1, "a losing platform hold revived")
+
+ // It leaves its file.
+ ws.put("annie", "1.1.1.1", "", "")
+ ws.mustPoll()
+ ws.quiet(1, "a losing platform hold leaving")
+ waitParents(t, ws.w, []string{"bdj." + apps})
+}
+
+// A live platform hold that wins its name (nothing ready behind it) is the
+// name's parent input: when it leaves its file, the files changed but the
+// ready fold did not, and the manager is woken and drops the parent.
+func TestWinningPlatformHoldLeavingItsFileWakesTheManager(t *testing.T) {
+ const apps = "apps.example.com"
+ held := "cart.bdj." + apps
+ ws := newWakeScene(t, apps)
+ bystander := readyJSON("shop.example.net", "bab", "shop", 0, 0)
+ ws.put("annie", "1.1.1.1", bystander, holdJSON(held, "mv_a", true, 1, 1))
+ ws.mustPoll()
+ waitWoke(t, &ws.woke, 1)
+ waitParents(t, ws.w, []string{"bdj." + apps})
+ readyFold := ws.m.snapshot().hosts
+
+ ws.set(time.Minute)
+ ws.put("annie", "1.1.1.1", bystander, "")
+ ws.mustPoll()
+ if !samePlatformSet(readyFold, ws.m.snapshot().hosts) {
+ t.Fatal("test bug: the ready fold's platform set must not move")
+ }
+ waitWoke(t, &ws.woke, 2)
+ waitParents(t, ws.w, nil)
+}