Name no node in claude-code's fixtures (issue 1838)

This commit is contained in:
jochen
2026-10-22 21:55:50 +02:00
parent ba3635307c
commit 30e6435dea
8 changed files with 350 additions and 350 deletions
@@ -290,11 +290,11 @@ func TestAnAppliedBindingSaysWhyAndWho(t *testing.T) {
p, w := node(t, "laptop")
var asked []string
done, err := OnBinding(p, &BindingState{Licence: "personal", Kind: "subscription", Generation: 7,
Why: "the switch verb", By: "g14.node-tools"}, seat(t, "personal", "at-7", 7, &asked), writer(w))
Why: "the switch verb", By: "node-b.node-tools"}, seat(t, "personal", "at-7", 7, &asked), writer(w))
if err != nil {
t.Fatal(err)
}
for _, want := range []string{`"generation":7`, `"why":"the switch verb"`, `"by":"g14.node-tools"`} {
for _, want := range []string{`"generation":7`, `"why":"the switch verb"`, `"by":"node-b.node-tools"`} {
if !strings.Contains(done, want) {
t.Fatalf("the line %s lacks %s", done, want)
}
@@ -355,8 +355,8 @@ func TestAPathOrAKeyThatIsNotWhatItSaysIsRefused(t *testing.T) {
}
func TestAllIsEveryNodeAndANameThatCannotBeAKeyIsRefused(t *testing.T) {
nodes, err := ExpandNodes([]string{"all"}, func() ([]string, error) { return []string{"ace", "g14"}, nil })
if err != nil || strings.Join(nodes, ",") != "ace,g14" {
nodes, err := ExpandNodes([]string{"all"}, func() ([]string, error) { return []string{"node-a", "node-b"}, nil })
if err != nil || strings.Join(nodes, ",") != "node-a,node-b" {
t.Fatalf("%v %v", nodes, err)
}
p, w := node(t, "laptop")
@@ -51,13 +51,13 @@ func suggestedCall(t *testing.T, msg string) map[string]any {
// Issue 1757: the Stop hook's suggested watched call carries `by`, and the tool accepts it as printed.
func TestTheStopHooksSuggestedWatchedCallIsAcceptedByTheTool(t *testing.T) {
p, _ := node(t, "g14")
p, _ := node(t, "node-b")
store := newMemWaits()
waiting = &Waiting{Store: store, Now: func() time.Time { return t0 }}
t.Cleanup(func() { waiting = nil })
s := serve(t, p, &bus{kept: map[string]map[string]any{}}, memConfig{})
text, isErr := s.call(t, "claude_code_wait", map[string]any{"op": "add", "session": "verify-0489-test", "by": "fix-1757@g14",
text, isErr := s.call(t, "claude_code_wait", map[string]any{"op": "add", "session": "verify-0489-test", "by": "fix-1757@node-b",
"what": "the operator's Approve", "read": map[string]any{"address": "mesh-delivery.recheck"}, "bound_minutes": 10})
if isErr {
t.Fatalf("add: %s", text)
@@ -81,14 +81,14 @@ func TestTheStopHooksSuggestedWatchedCallIsAcceptedByTheTool(t *testing.T) {
// Issue 1757: the Stop hook's suggested said call (past its bound) carries `by` too.
func TestTheStopHooksSuggestedSaidCallIsAcceptedByTheTool(t *testing.T) {
p, _ := node(t, "g14")
p, _ := node(t, "node-b")
store := newMemWaits()
clock := t0
waiting = &Waiting{Store: store, Now: func() time.Time { return clock }}
t.Cleanup(func() { waiting = nil })
s := serve(t, p, &bus{kept: map[string]map[string]any{}}, memConfig{})
text, isErr := s.call(t, "claude_code_wait", map[string]any{"op": "add", "session": "verify-0489-test", "by": "fix-1757@g14",
text, isErr := s.call(t, "claude_code_wait", map[string]any{"op": "add", "session": "verify-0489-test", "by": "fix-1757@node-b",
"what": "the operator's Approve", "read": map[string]any{"address": "mesh-delivery.recheck"}, "bound_minutes": 1})
if isErr {
t.Fatalf("add: %s", text)
@@ -114,20 +114,20 @@ func TestTheStopHooksSuggestedSaidCallIsAcceptedByTheTool(t *testing.T) {
// Issue 1757: the SessionStart hook's suggested adopt call already carries `by` — checked, as asked, so a
// regression here is caught the same way as the Stop hook's.
func TestTheSessionStartHooksSuggestedAdoptCallIsAcceptedByTheTool(t *testing.T) {
p, _ := node(t, "g14")
p, _ := node(t, "node-b")
store := newMemWaits()
waiting = &Waiting{Store: store, Now: func() time.Time { return t0 }}
t.Cleanup(func() { waiting = nil })
s := serve(t, p, &bus{kept: map[string]map[string]any{}}, memConfig{})
text, isErr := s.call(t, "claude_code_wait", map[string]any{"op": "add", "session": "sess-old", "by": "fix-1757@g14",
text, isErr := s.call(t, "claude_code_wait", map[string]any{"op": "add", "session": "sess-old", "by": "fix-1757@node-b",
"what": "the operator's Approve", "read": map[string]any{"address": "mesh-delivery.recheck"}, "bound_minutes": 10})
if isErr {
t.Fatalf("add: %s", text)
}
msg := SessionStartMessage("sess-new", WaitsDirPath(p), t0.Add(time.Minute))
if !strings.Contains(msg, "\"by\":\"fix-1757@g14\"") {
if !strings.Contains(msg, "\"by\":\"fix-1757@node-b\"") {
t.Fatalf("the suggested adopt call does not carry by: %s", msg)
}
args := suggestedCall(t, msg)
+215 -215
View File
@@ -211,10 +211,10 @@ const project = "-home-op-projects-novox-hq"
func twoNodes(t *testing.T) (*memState, *clock, *memNode, *memNode) {
state, c := newMemState(), &clock{now: time.Date(2026, 10, 13, 12, 0, 0, 0, time.UTC)}
root := t.TempDir()
return state, c, newMemNode(t, root, "g14", HomeOperator, state, c), newMemNode(t, root, "shanks", HomeOperator, state, c)
return state, c, newMemNode(t, root, "node-b", HomeOperator, state, c), newMemNode(t, root, "node-d", HomeOperator, state, c)
}
// carried writes a file on g14 and carries it to shanks.
// carried writes a file on node-b and carries it to node-d.
func carriedOnBoth(t *testing.T, state *memState, c *clock, a, b *memNode, file, text string) {
t.Helper()
a.write(t, project, file, text, c.now)
@@ -229,39 +229,39 @@ func carriedOnBoth(t *testing.T, state *memState, c *clock, a, b *memNode, file,
// §5, §6: a memory file written on one node appears unchanged on another, at any depth, and is carried once.
func TestAMemoryFileWrittenOnOneNodeAppearsUnchangedOnAnother(t *testing.T) {
state, c, g14, shanks := twoNodes(t)
g14.write(t, project, "MEMORY.md", "- [state](state.md) — where things stand\n", c.now)
g14.write(t, project, "notes/state.md", "ADR 0343 proposed.\n", c.now)
g14.look(t)
state, c, nodeB, nodeD := twoNodes(t)
nodeB.write(t, project, "MEMORY.md", "- [state](state.md) — where things stand\n", c.now)
nodeB.write(t, project, "notes/state.md", "ADR 0343 proposed.\n", c.now)
nodeB.look(t)
state.pump(t)
for file, want := range map[string]string{"MEMORY.md": "- [state](state.md) — where things stand\n", "notes/state.md": "ADR 0343 proposed.\n"} {
if got, ok := shanks.read(t, project, file); !ok || got != want {
t.Fatalf("%s on shanks: %q, %v", file, got, ok)
if got, ok := nodeD.read(t, project, file); !ok || got != want {
t.Fatalf("%s on node-d: %q, %v", file, got, ok)
}
}
before := state.rev
shanks.look(t)
g14.look(t)
nodeD.look(t)
nodeB.look(t)
state.pump(t)
if state.rev != before {
t.Fatalf("a carried file was carried again: %d changes", state.rev-before)
}
// A change on shanks comes back to g14, over the version it replaced, which g14 keeps.
// A change on node-d comes back to node-b, over the version it replaced, which node-b keeps.
c.now = c.now.Add(time.Minute)
shanks.write(t, project, "notes/state.md", "ADR 0343 accepted.\n", c.now)
shanks.look(t)
nodeD.write(t, project, "notes/state.md", "ADR 0343 accepted.\n", c.now)
nodeD.look(t)
state.pump(t)
if got, _ := g14.read(t, project, "notes/state.md"); got != "ADR 0343 accepted.\n" {
t.Fatalf("the change did not reach g14: %q", got)
if got, _ := nodeB.read(t, project, "notes/state.md"); got != "ADR 0343 accepted.\n" {
t.Fatalf("the change did not reach node-b: %q", got)
}
if !holdsText(g14.copies(t), "ADR 0343 proposed.\n") {
t.Fatal("the version replaced on g14 was not kept")
if !holdsText(nodeB.copies(t), "ADR 0343 proposed.\n") {
t.Fatal("the version replaced on node-b was not kept")
}
if len(g14.notices)+len(shanks.notices) != 0 {
t.Fatalf("a plain change warned: %v %v", g14.notices, shanks.notices)
if len(nodeB.notices)+len(nodeD.notices) != 0 {
t.Fatalf("a plain change warned: %v %v", nodeB.notices, nodeD.notices)
}
// No value in the state is anything but a version of a memory file, and no tool answer carries the text.
raw, _ := json.Marshal(g14.Status())
raw, _ := json.Marshal(nodeB.Status())
if strings.Contains(string(raw), "accepted") {
t.Fatalf("the memory tool answered the text: %s", raw)
}
@@ -270,44 +270,44 @@ func TestAMemoryFileWrittenOnOneNodeAppearsUnchangedOnAnother(t *testing.T) {
// §1, §2: only the memory folders are carried; a symbolic link, an oversized file and a secret's shape are not, and are
// listed with why.
func TestOnlyTheMemoryFoldersAreCarried(t *testing.T) {
state, c, g14, shanks := twoNodes(t)
projectDir := filepath.Join(g14.Home, ".claude", "projects", project)
g14.write(t, project, "MEMORY.md", "index\n", c.now)
state, c, nodeB, nodeD := twoNodes(t)
projectDir := filepath.Join(nodeB.Home, ".claude", "projects", project)
nodeB.write(t, project, "MEMORY.md", "index\n", c.now)
for path, text := range map[string]string{
filepath.Join(projectDir, "session.jsonl"): "{}\n",
filepath.Join(g14.Home, ".claude", "settings.json"): "{}\n",
filepath.Join(g14.Home, ".claude", "history.jsonl"): "{}\n",
filepath.Join(projectDir, "session.jsonl"): "{}\n",
filepath.Join(nodeB.Home, ".claude", "settings.json"): "{}\n",
filepath.Join(nodeB.Home, ".claude", "history.jsonl"): "{}\n",
} {
if err := os.WriteFile(path, []byte(text), 0o600); err != nil {
t.Fatal(err)
}
}
g14.write(t, project, "key.md", "-----BEGIN OPENSSH PRIVATE KEY-----\nabc\n", c.now)
g14.write(t, project, "url.md", "https://op:hunter2@git.example.org/x\n", c.now)
g14.write(t, project, "big.md", strings.Repeat("x", MemoryMostBytes+1), c.now)
g14.write(t, project, "hash.md", "head 1fe6ea4ee47379007598e163784dbd7ac50b7f79\n", c.now)
nodeB.write(t, project, "key.md", "-----BEGIN OPENSSH PRIVATE KEY-----\nabc\n", c.now)
nodeB.write(t, project, "url.md", "https://op:hunter2@git.example.org/x\n", c.now)
nodeB.write(t, project, "big.md", strings.Repeat("x", MemoryMostBytes+1), c.now)
nodeB.write(t, project, "hash.md", "head 1fe6ea4ee47379007598e163784dbd7ac50b7f79\n", c.now)
outside := filepath.Join(t.TempDir(), "elsewhere.md")
_ = os.WriteFile(outside, []byte("not memory\n"), 0o600)
if err := os.Symlink(outside, g14.path(project, "link.md")); err != nil {
if err := os.Symlink(outside, nodeB.path(project, "link.md")); err != nil {
t.Fatal(err)
}
g14.look(t)
nodeB.look(t)
state.pump(t)
for _, file := range []string{"key.md", "url.md", "big.md", "link.md"} {
if _, ok := shanks.read(t, project, file); ok {
if _, ok := nodeD.read(t, project, file); ok {
t.Errorf("%s was carried", file)
}
}
for _, file := range []string{"MEMORY.md", "hash.md"} {
if _, ok := shanks.read(t, project, file); !ok {
if _, ok := nodeD.read(t, project, file); !ok {
t.Errorf("%s was not carried", file)
}
}
if _, err := os.Stat(filepath.Join(shanks.Home, ".claude", "projects", project, "session.jsonl")); err == nil {
if _, err := os.Stat(filepath.Join(nodeD.Home, ".claude", "projects", project, "session.jsonl")); err == nil {
t.Error("a session was carried")
}
for _, f := range []string{"settings.json", "history.jsonl"} {
if _, err := os.Stat(filepath.Join(shanks.Home, ".claude", f)); err == nil {
if _, err := os.Stat(filepath.Join(nodeD.Home, ".claude", f)); err == nil {
t.Errorf("%s was carried", f)
}
}
@@ -315,7 +315,7 @@ func TestOnlyTheMemoryFoldersAreCarried(t *testing.T) {
if len(keys) != 2 {
t.Fatalf("the state holds %d keys, not the two memory files: %v", len(keys), keys)
}
not, _ := g14.Status()["not carried"].(map[string]string)
not, _ := nodeB.Status()["not carried"].(map[string]string)
for _, file := range []string{"key.md", "url.md", "big.md", "link.md"} {
if not[project+"/"+file] == "" {
t.Errorf("%s is not listed as not carried: %v", file, not)
@@ -327,8 +327,8 @@ func TestOnlyTheMemoryFoldersAreCarried(t *testing.T) {
func TestHomesOfTheTwoKindsNeverTakeEachOthersMemory(t *testing.T) {
state, c := newMemState(), &clock{now: time.Date(2026, 10, 13, 12, 0, 0, 0, time.UTC)}
root := t.TempDir()
laptop := newMemNode(t, root, "g14", HomeOperator, state, c)
control := newMemNode(t, root, "novox", HomeAgent, state, c)
laptop := newMemNode(t, root, "node-b", HomeOperator, state, c)
control := newMemNode(t, root, "node-c", HomeAgent, state, c)
control.write(t, project, "MEMORY.md", "from a confined agent\n", c.now)
control.look(t)
state.pump(t)
@@ -343,7 +343,7 @@ func TestHomesOfTheTwoKindsNeverTakeEachOthersMemory(t *testing.T) {
}
// A value of the other kind put under this kind's key is refused too.
key := MemoryKey(HomeOperator, project, "x.md")
raw, _ := json.Marshal(MemoryVersion{Kind: HomeAgent, Project: project, Path: "x.md", Text: "x", Digest: digestOf([]byte("x")), Node: "novox"})
raw, _ := json.Marshal(MemoryVersion{Kind: HomeAgent, Project: project, Path: "x.md", Text: "x", Digest: digestOf([]byte("x")), Node: "node-c"})
if err := laptop.Take(key, "put", raw, 99); err == nil {
t.Fatal("a value of another kind was taken")
}
@@ -362,31 +362,31 @@ func TestHomesOfTheTwoKindsNeverTakeEachOthersMemory(t *testing.T) {
// §7, §8: a file changed on two nodes since it was carried is kept twice in one file on both, the newer first, with
// one warning; it clears once the file is tidied.
func TestAFileChangedOnTwoNodesIsKeptTwiceTheNewerFirstWithAWarning(t *testing.T) {
state, c, g14, shanks := twoNodes(t)
carriedOnBoth(t, state, c, g14, shanks, "MEMORY.md", "- base\n")
g14.write(t, project, "MEMORY.md", "- base\n- learned on g14\n", c.now.Add(time.Minute))
shanks.write(t, project, "MEMORY.md", "- base\n- learned on shanks\n", c.now.Add(2*time.Minute))
state, c, nodeB, nodeD := twoNodes(t)
carriedOnBoth(t, state, c, nodeB, nodeD, "MEMORY.md", "- base\n")
nodeB.write(t, project, "MEMORY.md", "- base\n- learned on node-b\n", c.now.Add(time.Minute))
nodeD.write(t, project, "MEMORY.md", "- base\n- learned on node-d\n", c.now.Add(2*time.Minute))
c.now = c.now.Add(3 * time.Minute)
g14.look(t)
shanks.look(t)
nodeB.look(t)
nodeD.look(t)
state.pump(t)
g14.look(t)
shanks.look(t)
nodeB.look(t)
nodeD.look(t)
state.pump(t)
a, _ := g14.read(t, project, "MEMORY.md")
b, _ := shanks.read(t, project, "MEMORY.md")
a, _ := nodeB.read(t, project, "MEMORY.md")
b, _ := nodeD.read(t, project, "MEMORY.md")
if a != b {
t.Fatalf("the nodes did not end on one text:\n%s\n----\n%s", a, b)
}
newer, older := strings.Index(a, "learned on shanks"), strings.Index(a, "learned on g14")
newer, older := strings.Index(a, "learned on node-d"), strings.Index(a, "learned on node-b")
marker := strings.Index(a, "<!-- novox mesh: this file was changed on two nodes")
if newer < 0 || older < 0 || marker < 0 || !(newer < marker && marker < older) {
t.Fatalf("not both versions, the newer first, with the line between:\n%s", a)
}
if !strings.Contains(a[marker:], "changed on shanks") || !strings.Contains(a[marker:], "changed on g14") {
if !strings.Contains(a[marker:], "changed on node-d") || !strings.Contains(a[marker:], "changed on node-b") {
t.Fatalf("the line does not name both nodes:\n%s", a)
}
notices := append(append([]map[string]any{}, g14.notices...), shanks.notices...)
notices := append(append([]map[string]any{}, nodeB.notices...), nodeD.notices...)
if len(notices) != 1 || notices[0]["severity"] != "warning" || notices[0]["clear"] != nil {
t.Fatalf("not one warning: %v", notices)
}
@@ -406,22 +406,22 @@ func TestAFileChangedOnTwoNodesIsKeptTwiceTheNewerFirstWithAWarning(t *testing.T
if h, _ := notices[0]["headline"].(string); len([]rune(h)) > 60 {
t.Errorf("the headline is longer than sixty characters: %q", h)
}
all := append(g14.copies(t), shanks.copies(t)...)
for _, v := range []string{"- base\n- learned on g14\n", "- base\n- learned on shanks\n"} {
all := append(nodeB.copies(t), nodeD.copies(t)...)
for _, v := range []string{"- base\n- learned on node-b\n", "- base\n- learned on node-d\n"} {
if !holdsText(all, v) {
t.Errorf("the version %q was not kept", v)
}
}
if st := g14.Status(); len(st["conflicts"].([]conflict))+len(shanks.Status()["conflicts"].([]conflict)) != 1 {
if st := nodeB.Status(); len(st["conflicts"].([]conflict))+len(nodeD.Status()["conflicts"].([]conflict)) != 1 {
t.Fatalf("the conflict is not listed: %v", st)
}
// Tidied on either node, the warning clears.
c.now = c.now.Add(time.Minute)
g14.write(t, project, "MEMORY.md", "- base\n- learned on g14\n- learned on shanks\n", c.now)
g14.look(t)
nodeB.write(t, project, "MEMORY.md", "- base\n- learned on node-b\n- learned on node-d\n", c.now)
nodeB.look(t)
state.pump(t)
cleared := false
for _, n := range append(g14.notices, shanks.notices...) {
for _, n := range append(nodeB.notices, nodeD.notices...) {
if n["clear"] == true {
cleared = true
}
@@ -429,39 +429,39 @@ func TestAFileChangedOnTwoNodesIsKeptTwiceTheNewerFirstWithAWarning(t *testing.T
if !cleared {
t.Fatal("the warning did not clear once the file was tidied")
}
if got, _ := shanks.read(t, project, "MEMORY.md"); got != "- base\n- learned on g14\n- learned on shanks\n" {
t.Fatalf("the tidied file did not reach shanks: %q", got)
if got, _ := nodeD.read(t, project, "MEMORY.md"); got != "- base\n- learned on node-b\n- learned on node-d\n" {
t.Fatalf("the tidied file did not reach node-d: %q", got)
}
}
// The order of the two versions: the later change first, equal times by node name.
func TestMergeVersionsPutsTheNewerFirst(t *testing.T) {
at := time.Date(2026, 10, 13, 12, 0, 0, 0, time.UTC)
m := string(MergeVersions([]byte("old\n"), "g14", at, []byte("new\n"), "shanks", at.Add(time.Second)))
m := string(MergeVersions([]byte("old\n"), "node-b", at, []byte("new\n"), "node-d", at.Add(time.Second)))
if strings.Index(m, "new") > strings.Index(m, "old") {
t.Fatalf("the newer is not first: %s", m)
}
m = string(MergeVersions([]byte("from shanks\n"), "shanks", at, []byte("from ace\n"), "ace", at))
if strings.Index(m, "from ace") > strings.Index(m, "from shanks") {
m = string(MergeVersions([]byte("from node-d\n"), "node-d", at, []byte("from node-a\n"), "node-a", at))
if strings.Index(m, "from node-a") > strings.Index(m, "from node-d") {
t.Fatalf("equal times are not ordered by node name: %s", m)
}
}
// §9: a removal moves the other node's unchanged file into the copies.
func TestARemovalMovesTheUnchangedFileIntoTheCopies(t *testing.T) {
state, c, g14, shanks := twoNodes(t)
carriedOnBoth(t, state, c, g14, shanks, "old.md", "an old note\n")
carriedOnBoth(t, state, c, g14, shanks, "MEMORY.md", "index\n")
if err := os.Remove(g14.path(project, "old.md")); err != nil {
state, c, nodeB, nodeD := twoNodes(t)
carriedOnBoth(t, state, c, nodeB, nodeD, "old.md", "an old note\n")
carriedOnBoth(t, state, c, nodeB, nodeD, "MEMORY.md", "index\n")
if err := os.Remove(nodeB.path(project, "old.md")); err != nil {
t.Fatal(err)
}
g14.look(t)
nodeB.look(t)
state.pump(t)
if _, ok := shanks.read(t, project, "old.md"); ok {
t.Fatal("the removal did not reach shanks")
if _, ok := nodeD.read(t, project, "old.md"); ok {
t.Fatal("the removal did not reach node-d")
}
if !holdsText(shanks.copies(t), "an old note\n") {
t.Fatal("the removed version was not kept on shanks")
if !holdsText(nodeD.copies(t), "an old note\n") {
t.Fatal("the removed version was not kept on node-d")
}
raw, _, ok, _ := state.Get(MemoryKey(HomeOperator, project, "old.md"))
var v MemoryVersion
@@ -469,7 +469,7 @@ func TestARemovalMovesTheUnchangedFileIntoTheCopies(t *testing.T) {
if !ok || !v.Removed || v.Text != "" {
t.Fatalf("the removal is not a marker in the state: %s", raw)
}
if got, _ := shanks.read(t, project, "MEMORY.md"); got != "index\n" {
if got, _ := nodeD.read(t, project, "MEMORY.md"); got != "index\n" {
t.Fatal("another file was touched")
}
}
@@ -477,23 +477,23 @@ func TestARemovalMovesTheUnchangedFileIntoTheCopies(t *testing.T) {
// §9: a change outlives a removal, whichever reaches the state first.
func TestAChangeOutlivesARemoval(t *testing.T) {
for _, removalFirst := range []bool{true, false} {
state, c, g14, shanks := twoNodes(t)
carriedOnBoth(t, state, c, g14, shanks, "note.md", "v1\n")
_ = os.Remove(g14.path(project, "note.md"))
shanks.write(t, project, "note.md", "v2\n", c.now.Add(time.Minute))
state, c, nodeB, nodeD := twoNodes(t)
carriedOnBoth(t, state, c, nodeB, nodeD, "note.md", "v1\n")
_ = os.Remove(nodeB.path(project, "note.md"))
nodeD.write(t, project, "note.md", "v2\n", c.now.Add(time.Minute))
if removalFirst {
g14.look(t)
nodeB.look(t)
state.pump(t)
shanks.look(t)
nodeD.look(t)
} else {
shanks.look(t)
g14.look(t)
nodeD.look(t)
nodeB.look(t)
}
state.pump(t)
g14.look(t)
shanks.look(t)
nodeB.look(t)
nodeD.look(t)
state.pump(t)
for _, n := range []*memNode{g14, shanks} {
for _, n := range []*memNode{nodeB, nodeD} {
if got, _ := n.read(t, project, "note.md"); got != "v2\n" {
t.Fatalf("removal first %v: the change did not outlive the removal on %s: %q", removalFirst, n.Node, got)
}
@@ -503,51 +503,51 @@ func TestAChangeOutlivesARemoval(t *testing.T) {
// §9: a missing folder, a state that lost its keys and a deleted key remove nothing.
func TestAMissingFolderOrAnAbsentKeyRemovesNothing(t *testing.T) {
state, c, g14, shanks := twoNodes(t)
carriedOnBoth(t, state, c, g14, shanks, "MEMORY.md", "index\n")
state, c, nodeB, nodeD := twoNodes(t)
carriedOnBoth(t, state, c, nodeB, nodeD, "MEMORY.md", "index\n")
key := MemoryKey(HomeOperator, project, "MEMORY.md")
// The folder gone on g14.
if err := os.RemoveAll(filepath.Join(g14.Home, ".claude", "projects", project, "memory")); err != nil {
// The folder gone on node-b.
if err := os.RemoveAll(filepath.Join(nodeB.Home, ".claude", "projects", project, "memory")); err != nil {
t.Fatal(err)
}
g14.look(t)
nodeB.look(t)
state.pump(t)
if got, _ := shanks.read(t, project, "MEMORY.md"); got != "index\n" {
if got, _ := nodeD.read(t, project, "MEMORY.md"); got != "index\n" {
t.Fatal("a missing folder removed the file elsewhere")
}
// A deleted key.
_ = state.Delete(key)
state.pump(t)
if got, _ := shanks.read(t, project, "MEMORY.md"); got != "index\n" {
if got, _ := nodeD.read(t, project, "MEMORY.md"); got != "index\n" {
t.Fatal("a deleted key removed the file")
}
// The state lost: the file is carried again, not removed.
shanks.look(t)
nodeD.look(t)
state.pump(t)
if _, _, ok, _ := state.Get(key); !ok {
t.Fatal("a file whose key the state lost was not carried again")
}
if got, _ := shanks.read(t, project, "MEMORY.md"); got != "index\n" {
t.Fatalf("the file was touched on shanks: %q", got)
if got, _ := nodeD.read(t, project, "MEMORY.md"); got != "index\n" {
t.Fatalf("the file was touched on node-d: %q", got)
}
}
// §10: on a first carrying, two nodes' different versions are both kept, and each node's own files reach the other.
func TestTheFirstCarryingKeepsEveryVersion(t *testing.T) {
state, c, g14, shanks := twoNodes(t)
g14.write(t, project, "MEMORY.md", "- laptop's index\n", c.now)
g14.write(t, project, "laptop.md", "only on the laptop\n", c.now)
shanks.write(t, project, "MEMORY.md", "- workstation's index\n", c.now.Add(time.Hour))
shanks.write(t, project, "workstation.md", "only on the workstation\n", c.now)
shanks.write(t, project, "same.md", "same\n", c.now)
g14.write(t, project, "same.md", "same\n", c.now)
g14.look(t)
state, c, nodeB, nodeD := twoNodes(t)
nodeB.write(t, project, "MEMORY.md", "- laptop's index\n", c.now)
nodeB.write(t, project, "laptop.md", "only on the laptop\n", c.now)
nodeD.write(t, project, "MEMORY.md", "- workstation's index\n", c.now.Add(time.Hour))
nodeD.write(t, project, "workstation.md", "only on the workstation\n", c.now)
nodeD.write(t, project, "same.md", "same\n", c.now)
nodeB.write(t, project, "same.md", "same\n", c.now)
nodeB.look(t)
state.pump(t)
shanks.look(t)
nodeD.look(t)
state.pump(t)
g14.look(t)
nodeB.look(t)
state.pump(t)
for _, n := range []*memNode{g14, shanks} {
for _, n := range []*memNode{nodeB, nodeD} {
index, _ := n.read(t, project, "MEMORY.md")
if !strings.Contains(index, "laptop's index") || !strings.Contains(index, "workstation's index") ||
strings.Index(index, "workstation's index") > strings.Index(index, "laptop's index") {
@@ -559,23 +559,23 @@ func TestTheFirstCarryingKeepsEveryVersion(t *testing.T) {
}
}
}
if n := len(g14.notices) + len(shanks.notices); n != 1 {
if n := len(nodeB.notices) + len(nodeD.notices); n != 1 {
t.Fatalf("%d warnings for one conflict", n)
}
}
// Nothing is written through a symbolic link: a memory folder that is one is neither read nor written.
func TestNothingIsWrittenThroughASymbolicLink(t *testing.T) {
state, c, g14, shanks := twoNodes(t)
state, c, nodeB, nodeD := twoNodes(t)
elsewhere := t.TempDir()
if err := os.MkdirAll(filepath.Join(shanks.Home, ".claude", "projects", project), 0o700); err != nil {
if err := os.MkdirAll(filepath.Join(nodeD.Home, ".claude", "projects", project), 0o700); err != nil {
t.Fatal(err)
}
if err := os.Symlink(elsewhere, filepath.Join(shanks.Home, ".claude", "projects", project, "memory")); err != nil {
if err := os.Symlink(elsewhere, filepath.Join(nodeD.Home, ".claude", "projects", project, "memory")); err != nil {
t.Fatal(err)
}
g14.write(t, project, "MEMORY.md", "index\n", c.now)
g14.look(t)
nodeB.write(t, project, "MEMORY.md", "index\n", c.now)
nodeB.look(t)
state.pump(t)
if entries, _ := os.ReadDir(elsewhere); len(entries) != 0 {
t.Fatalf("written through a symbolic link: %v", entries)
@@ -619,24 +619,24 @@ func (s *memState) replay(t *testing.T, n *memNode) {
// §6: a node that was away while a file changed twice takes the newest version, whose base it never saw, because
// its own file holds no change it did not carry; the version replaced is kept.
func TestANodeThatWasAwayTakesTheNewestVersion(t *testing.T) {
state, c, g14, shanks := twoNodes(t)
carriedOnBoth(t, state, c, g14, shanks, "MEMORY.md", "v1\n")
state.watchers = []*MemoryCarrier{g14.MemoryCarrier}
state, c, nodeB, nodeD := twoNodes(t)
carriedOnBoth(t, state, c, nodeB, nodeD, "MEMORY.md", "v1\n")
state.watchers = []*MemoryCarrier{nodeB.MemoryCarrier}
for _, v := range []string{"v2\n", "v3\n"} {
c.now = c.now.Add(time.Minute)
g14.write(t, project, "MEMORY.md", v, c.now)
g14.look(t)
nodeB.write(t, project, "MEMORY.md", v, c.now)
nodeB.look(t)
state.pump(t)
}
state.watchers = append(state.watchers, shanks.MemoryCarrier)
state.replay(t, shanks)
if got, _ := shanks.read(t, project, "MEMORY.md"); got != "v3\n" {
state.watchers = append(state.watchers, nodeD.MemoryCarrier)
state.replay(t, nodeD)
if got, _ := nodeD.read(t, project, "MEMORY.md"); got != "v3\n" {
t.Fatalf("the node that was away holds %q", got)
}
if !holdsText(shanks.copies(t), "v1\n") {
if !holdsText(nodeD.copies(t), "v1\n") {
t.Fatal("the version replaced was not kept")
}
if len(g14.notices)+len(shanks.notices) != 0 {
if len(nodeB.notices)+len(nodeD.notices) != 0 {
t.Fatal("taking a newer version warned")
}
}
@@ -644,24 +644,24 @@ func TestANodeThatWasAwayTakesTheNewestVersion(t *testing.T) {
// §2, §7: two versions too large to share one file are kept in two: the newer in the file, the older beside it,
// both carried; and a text that would not fit in one value of the state is not carried.
func TestTwoVersionsTooLargeForOneFileAreKeptInTwo(t *testing.T) {
state, c, g14, shanks := twoNodes(t)
carriedOnBoth(t, state, c, g14, shanks, "big.md", "base\n")
older := strings.Repeat("from g14 ", MemoryMostBytes/9-100) + "\n"
newer := strings.Repeat("from shanks ", MemoryMostBytes/12-100) + "\n"
g14.write(t, project, "big.md", older, c.now.Add(time.Minute))
shanks.write(t, project, "big.md", newer, c.now.Add(2*time.Minute))
state, c, nodeB, nodeD := twoNodes(t)
carriedOnBoth(t, state, c, nodeB, nodeD, "big.md", "base\n")
older := strings.Repeat("from node-b ", MemoryMostBytes/12-100) + "\n"
newer := strings.Repeat("from node-d ", MemoryMostBytes/12-100) + "\n"
nodeB.write(t, project, "big.md", older, c.now.Add(time.Minute))
nodeD.write(t, project, "big.md", newer, c.now.Add(2*time.Minute))
c.now = c.now.Add(3 * time.Minute)
g14.look(t)
shanks.look(t)
nodeB.look(t)
nodeD.look(t)
state.pump(t)
g14.look(t)
shanks.look(t)
nodeB.look(t)
nodeD.look(t)
state.pump(t)
beside := OlderBeside("big.md", "g14")
if beside != "big.older-from-g14.md" {
beside := OlderBeside("big.md", "node-b")
if beside != "big.older-from-node-b.md" {
t.Fatalf("the older is named %q", beside)
}
for _, n := range []*memNode{g14, shanks} {
for _, n := range []*memNode{nodeB, nodeD} {
if got, _ := n.read(t, project, "big.md"); got != newer {
t.Fatalf("%s's file does not hold the newer version", n.Node)
}
@@ -669,18 +669,18 @@ func TestTwoVersionsTooLargeForOneFileAreKeptInTwo(t *testing.T) {
t.Fatalf("%s does not hold the older version beside it", n.Node)
}
}
notices := append(append([]map[string]any{}, g14.notices...), shanks.notices...)
notices := append(append([]map[string]any{}, nodeB.notices...), nodeD.notices...)
if len(notices) != 1 || !strings.Contains(notices[0]["explanation"].(string), "second file beside it") {
t.Fatalf("not one warning saying the older is beside it: %v", notices)
}
// Under the size limit, but too large once encoded: not carried, and said.
g14.write(t, project, "angles.md", strings.Repeat("<", MemoryMostBytes-10), c.now)
g14.look(t)
nodeB.write(t, project, "angles.md", strings.Repeat("<", MemoryMostBytes-10), c.now)
nodeB.look(t)
state.pump(t)
if _, ok := shanks.read(t, project, "angles.md"); ok {
if _, ok := nodeD.read(t, project, "angles.md"); ok {
t.Fatal("a text too large for one value was carried")
}
not, _ := g14.Status()["not carried"].(map[string]string)
not, _ := nodeB.Status()["not carried"].(map[string]string)
if not[project+"/angles.md"] == "" {
t.Fatalf("it is not listed as not carried: %v", not)
}
@@ -713,8 +713,8 @@ func (s *memState) putRaw(t *testing.T, v MemoryVersion) string {
// Review of #276, 1: a value whose project or path leads out of a memory folder is never taken, and nothing is
// written for it.
func TestAValueLeadingOutOfAMemoryFolderIsNeverTaken(t *testing.T) {
state, c, g14, shanks := twoNodes(t)
root := filepath.Dir(filepath.Dir(shanks.Home))
state, c, nodeB, nodeD := twoNodes(t)
root := filepath.Dir(filepath.Dir(nodeD.Home))
for _, bad := range []struct{ project, path string }{
{"../../../escaped", "x.md"}, {"..", "x.md"}, {"a/b", "x.md"}, {"", "x.md"},
{project, "../x.md"}, {project, "/abs.md"}, {project, "a//x.md"}, {project, "./x.md"}, {project, "a/../../x.md"},
@@ -727,8 +727,8 @@ func TestAValueLeadingOutOfAMemoryFolderIsNeverTaken(t *testing.T) {
}
}
state.pump(t)
g14.look(t)
shanks.look(t)
nodeB.look(t)
nodeD.look(t)
_ = filepath.WalkDir(root, func(p string, d os.DirEntry, err error) error {
if err == nil && !d.IsDir() {
if raw, _ := os.ReadFile(p); string(raw) == "planted\n" {
@@ -748,21 +748,21 @@ func TestWhatCannotBeReadIsNeverTakenForGone(t *testing.T) {
if os.Geteuid() == 0 {
t.Skip("root reads a file whatever its mode")
}
state, c, g14, shanks := twoNodes(t)
carriedOnBoth(t, state, c, g14, shanks, "MEMORY.md", "index\n")
carriedOnBoth(t, state, c, g14, shanks, "notes/deep.md", "deep\n")
file, folder := g14.path(project, "MEMORY.md"), filepath.Dir(g14.path(project, "notes/deep.md"))
state, c, nodeB, nodeD := twoNodes(t)
carriedOnBoth(t, state, c, nodeB, nodeD, "MEMORY.md", "index\n")
carriedOnBoth(t, state, c, nodeB, nodeD, "notes/deep.md", "deep\n")
file, folder := nodeB.path(project, "MEMORY.md"), filepath.Dir(nodeB.path(project, "notes/deep.md"))
_ = os.Chmod(file, 0)
_ = os.Chmod(folder, 0)
t.Cleanup(func() { _ = os.Chmod(file, 0o644); _ = os.Chmod(folder, 0o755) })
g14.look(t)
nodeB.look(t)
state.pump(t)
for _, f := range []string{"MEMORY.md", "notes/deep.md"} {
if _, ok := shanks.read(t, project, f); !ok {
t.Errorf("%s, unreadable on g14, was removed on shanks", f)
if _, ok := nodeD.read(t, project, f); !ok {
t.Errorf("%s, unreadable on node-b, was removed on node-d", f)
}
}
not, _ := g14.Status()["not carried"].(map[string]string)
not, _ := nodeB.Status()["not carried"].(map[string]string)
if len(not) == 0 {
t.Fatalf("nothing is said as not carried: %v", not)
}
@@ -782,33 +782,33 @@ func TestNoUnscreenedTextReachesTheState(t *testing.T) {
token := "ghp_" + strings.Repeat("A1b2C3d4", 5) + "\n"
big := strings.Repeat("x", MemoryMostBytes+10) + "\n"
for _, text := range []string{token, big} {
// The removal path: removed on g14, changed on shanks to what is never carried.
state, c, g14, shanks := twoNodes(t)
carriedOnBoth(t, state, c, g14, shanks, "note.md", "v1\n")
_ = os.Remove(g14.path(project, "note.md"))
shanks.write(t, project, "note.md", text, c.now.Add(time.Minute))
g14.look(t)
// The removal path: removed on node-b, changed on node-d to what is never carried.
state, c, nodeB, nodeD := twoNodes(t)
carriedOnBoth(t, state, c, nodeB, nodeD, "note.md", "v1\n")
_ = os.Remove(nodeB.path(project, "note.md"))
nodeD.write(t, project, "note.md", text, c.now.Add(time.Minute))
nodeB.look(t)
state.pump(t)
if state.holds(text[:20]) {
t.Errorf("the removal path put text that is never carried into the state")
}
if got, _ := shanks.read(t, project, "note.md"); got != text {
t.Errorf("the change on shanks was not kept")
if got, _ := nodeD.read(t, project, "note.md"); got != text {
t.Errorf("the change on node-d was not kept")
}
// The conflict path: changed on g14, and on shanks to what is never carried.
state, c, g14, shanks = twoNodes(t)
carriedOnBoth(t, state, c, g14, shanks, "note.md", "v1\n")
g14.write(t, project, "note.md", "v2\n", c.now.Add(time.Minute))
shanks.write(t, project, "note.md", text, c.now.Add(2*time.Minute))
g14.look(t)
// The conflict path: changed on node-b, and on node-d to what is never carried.
state, c, nodeB, nodeD = twoNodes(t)
carriedOnBoth(t, state, c, nodeB, nodeD, "note.md", "v1\n")
nodeB.write(t, project, "note.md", "v2\n", c.now.Add(time.Minute))
nodeD.write(t, project, "note.md", text, c.now.Add(2*time.Minute))
nodeB.look(t)
state.pump(t)
shanks.look(t)
nodeD.look(t)
state.pump(t)
if state.holds(text[:20]) {
t.Errorf("the conflict path put text that is never carried into the state")
}
if got, _ := shanks.read(t, project, "note.md"); got != text {
t.Errorf("shanks's own version was not kept as it was")
if got, _ := nodeD.read(t, project, "note.md"); got != text {
t.Errorf("node-d's own version was not kept as it was")
}
}
}
@@ -819,24 +819,24 @@ func TestAConflictThatCannotBeWrittenIsNotMergedAgainAndAgain(t *testing.T) {
if os.Geteuid() == 0 {
t.Skip("root writes a folder whatever its mode")
}
state, c, g14, shanks := twoNodes(t)
carriedOnBoth(t, state, c, g14, shanks, "MEMORY.md", "base\n")
g14.write(t, project, "MEMORY.md", "base\nfrom g14\n", c.now.Add(time.Minute))
shanks.write(t, project, "MEMORY.md", "base\nfrom shanks\n", c.now.Add(2*time.Minute))
folder := filepath.Dir(shanks.path(project, "MEMORY.md"))
state, c, nodeB, nodeD := twoNodes(t)
carriedOnBoth(t, state, c, nodeB, nodeD, "MEMORY.md", "base\n")
nodeB.write(t, project, "MEMORY.md", "base\nfrom node-b\n", c.now.Add(time.Minute))
nodeD.write(t, project, "MEMORY.md", "base\nfrom node-d\n", c.now.Add(2*time.Minute))
folder := filepath.Dir(nodeD.path(project, "MEMORY.md"))
_ = os.Chmod(folder, 0o500)
t.Cleanup(func() { _ = os.Chmod(folder, 0o700) })
g14.look(t)
nodeB.look(t)
state.pump(t)
for i := 0; i < 5; i++ {
_ = shanks.Look()
_ = nodeD.Look()
state.pump(t)
}
if state.holds("<!-- novox mesh") {
t.Fatal("a merge that could not be written here reached the state")
}
unwritten := 0
for _, n := range shanks.notices {
for _, n := range nodeD.notices {
if strings.HasSuffix(n["key"].(string), ".unwritten") {
unwritten++
}
@@ -845,13 +845,13 @@ func TestAConflictThatCannotBeWrittenIsNotMergedAgainAndAgain(t *testing.T) {
t.Fatalf("%d warnings that the file could not be written", unwritten)
}
_ = os.Chmod(folder, 0o700)
shanks.look(t)
nodeD.look(t)
state.pump(t)
g14.look(t)
nodeB.look(t)
state.pump(t)
for _, n := range []*memNode{g14, shanks} {
for _, n := range []*memNode{nodeB, nodeD} {
got, _ := n.read(t, project, "MEMORY.md")
if strings.Count(got, "<!-- novox mesh") != 1 || !strings.Contains(got, "from g14") || !strings.Contains(got, "from shanks") {
if strings.Count(got, "<!-- novox mesh") != 1 || !strings.Contains(got, "from node-b") || !strings.Contains(got, "from node-d") {
t.Fatalf("%s does not hold both versions once:\n%s", n.Node, got)
}
}
@@ -860,24 +860,24 @@ func TestAConflictThatCannotBeWrittenIsNotMergedAgainAndAgain(t *testing.T) {
// Review of #276, minor: a file that changed between its reading and its writing is not written over; text that is
// not UTF-8 is not carried.
func TestAFileChangedBeforeItsWritingIsNotWrittenOver(t *testing.T) {
state, c, g14, shanks := twoNodes(t)
state, c, nodeB, nodeD := twoNodes(t)
_ = state
g14.write(t, project, "a.md", "read\n", c.now)
g14.write(t, project, "a.md", "changed meanwhile\n", c.now)
err := g14.writeLocal(project, "a.md", []byte("incoming\n"), "replaced", digestOf([]byte("read\n")))
nodeB.write(t, project, "a.md", "read\n", c.now)
nodeB.write(t, project, "a.md", "changed meanwhile\n", c.now)
err := nodeB.writeLocal(project, "a.md", []byte("incoming\n"), "replaced", digestOf([]byte("read\n")))
if !errors.Is(err, errChangedHere) {
t.Fatalf("written over a change: %v", err)
}
if got, _ := g14.read(t, project, "a.md"); got != "changed meanwhile\n" {
if got, _ := nodeB.read(t, project, "a.md"); got != "changed meanwhile\n" {
t.Fatalf("the change was replaced: %q", got)
}
g14.write(t, project, "latin1.md", "caf\xe9\n", c.now)
g14.look(t)
nodeB.write(t, project, "latin1.md", "caf\xe9\n", c.now)
nodeB.look(t)
state.pump(t)
if _, ok := shanks.read(t, project, "latin1.md"); ok {
if _, ok := nodeD.read(t, project, "latin1.md"); ok {
t.Fatal("text that is not UTF-8 was carried")
}
not, _ := g14.Status()["not carried"].(map[string]string)
not, _ := nodeB.Status()["not carried"].(map[string]string)
if !strings.Contains(not[project+"/latin1.md"], "UTF-8") {
t.Fatalf("it is not said why: %v", not)
}
@@ -886,28 +886,28 @@ func TestAFileChangedBeforeItsWritingIsNotWrittenOver(t *testing.T) {
// §9: a node that was away takes a removal of a version newer than the one it holds, when it holds no change of
// its own: the removed file does not come back.
func TestARemovalReachesANodeThatWasAway(t *testing.T) {
state, c, g14, shanks := twoNodes(t)
carriedOnBoth(t, state, c, g14, shanks, "old.md", "v1\n")
state.watchers = []*MemoryCarrier{g14.MemoryCarrier}
state, c, nodeB, nodeD := twoNodes(t)
carriedOnBoth(t, state, c, nodeB, nodeD, "old.md", "v1\n")
state.watchers = []*MemoryCarrier{nodeB.MemoryCarrier}
c.now = c.now.Add(time.Minute)
g14.write(t, project, "old.md", "v2\n", c.now)
g14.look(t)
nodeB.write(t, project, "old.md", "v2\n", c.now)
nodeB.look(t)
state.pump(t)
_ = os.Remove(g14.path(project, "old.md"))
g14.look(t)
_ = os.Remove(nodeB.path(project, "old.md"))
nodeB.look(t)
state.pump(t)
state.watchers = append(state.watchers, shanks.MemoryCarrier)
state.replay(t, shanks)
shanks.look(t)
state.watchers = append(state.watchers, nodeD.MemoryCarrier)
state.replay(t, nodeD)
nodeD.look(t)
state.pump(t)
g14.look(t)
if _, ok := shanks.read(t, project, "old.md"); ok {
nodeB.look(t)
if _, ok := nodeD.read(t, project, "old.md"); ok {
t.Fatal("the removal did not reach the node that was away")
}
if _, ok := g14.read(t, project, "old.md"); ok {
if _, ok := nodeB.read(t, project, "old.md"); ok {
t.Fatal("the removed file came back")
}
if !holdsText(shanks.copies(t), "v1\n") {
if !holdsText(nodeD.copies(t), "v1\n") {
t.Fatal("the version removed on the node that was away was not kept")
}
}
@@ -924,25 +924,25 @@ func (s slowState) Get(key string) (json.RawMessage, uint64, bool, error) {
}
// Issue 367, seen live: the runtime hands a watch its current values one at a time and gives them all ten seconds;
// shanks's handler read the state and waited for its look for each of a few hundred values, so the watch never
// started and nothing written on g14 reached it. The handler now only notes a change: it returns at once while a
// node-d's handler read the state and waited for its look for each of a few hundred values, so the watch never
// started and nothing written on node-b reached it. The handler now only notes a change: it returns at once while a
// look holds the carrier and while the state is slow to answer, and Drain applies what it noted.
func TestTheWatchHandlerReturnsAtOnceAndDrainAppliesWhatItNoted(t *testing.T) {
state, c, g14, shanks := twoNodes(t)
g14.write(t, project, "MEMORY.md", "index\n", c.now)
g14.look(t)
state, c, nodeB, nodeD := twoNodes(t)
nodeB.write(t, project, "MEMORY.md", "index\n", c.now)
nodeB.look(t)
state.mu.Lock()
state.pending = nil
state.mu.Unlock()
slow := slowState{memState: state, gate: make(chan struct{})}
shanks.Store = slow
nodeD.Store = slow
key := MemoryKey(HomeOperator, project, "MEMORY.md")
raw, rev, _, _ := state.Get(key)
shanks.mu.Lock() // a look running
nodeD.mu.Lock() // a look running
done := make(chan error, 1)
start := time.Now()
for i := 0; i < 400; i++ {
go func() { done <- shanks.Take(key, "put", raw, rev) }()
go func() { done <- nodeD.Take(key, "put", raw, rev) }()
}
for i := 0; i < 400; i++ {
select {
@@ -957,12 +957,12 @@ func TestTheWatchHandlerReturnsAtOnceAndDrainAppliesWhatItNoted(t *testing.T) {
if took := time.Since(start); took > time.Second {
t.Fatalf("400 values took %s to hand over", took)
}
shanks.mu.Unlock()
nodeD.mu.Unlock()
close(slow.gate)
if err := shanks.Drain(); err != nil {
if err := nodeD.Drain(); err != nil {
t.Fatal(err)
}
if got, _ := shanks.read(t, project, "MEMORY.md"); got != "index\n" {
if got, _ := nodeD.read(t, project, "MEMORY.md"); got != "index\n" {
t.Fatalf("what the watch noted was not applied: %q", got)
}
}
@@ -55,7 +55,7 @@ type router struct {
func newRouter() *router {
return &router{published: map[string]asks.Ask{}, records: map[string]json.RawMessage{},
kinds: []stdio.KindHeld{{Seat: "channel", Kind: "telegram", Module: "telegram", Node: "novox",
kinds: []stdio.KindHeld{{Seat: "channel", Kind: "telegram", Module: "telegram", Node: "node-c",
Capabilities: []string{"choice", "exact-render", "verified-sender"}}}}
}
@@ -106,7 +106,7 @@ const handover = "Hand-over from the laptop.\n\nThe controller's merge of the as
func proposeHandover(t *testing.T, p Paths, store *memProposals, r *router) string {
t.Helper()
out, err := Propose(p, ProposeInput{Scope: ScopeMesh, Name: "handoff-from-g14", Text: handover}, store, r, t0)
out, err := Propose(p, ProposeInput{Scope: ScopeMesh, Name: "handoff-from-node-b", Text: handover}, store, r, t0)
if err != nil {
t.Fatal(err)
}
@@ -136,7 +136,7 @@ func renderedInstructions(t *testing.T, p Paths) string {
}
func TestAProposalAsksTheOperatorAtTheLevelApproveAndChangesNothing(t *testing.T) {
p, _ := node(t, "g14")
p, _ := node(t, "node-b")
store, r := &memProposals{m: map[string]json.RawMessage{}}, newRouter()
id := proposeHandover(t, p, store, r)
@@ -153,10 +153,10 @@ func TestAProposalAsksTheOperatorAtTheLevelApproveAndChangesNothing(t *testing.T
asks.Glance(a.Explanation) != nil {
t.Errorf("the phone does not say what is asked at a glance: %q", a.Explanation)
}
if a.Headline != "Add Claude instructions 'handoff-from-g14'?" {
if a.Headline != "Add Claude instructions 'handoff-from-node-b'?" {
t.Errorf("headline: %q", a.Headline)
}
if !strings.HasPrefix(a.Details, "The text:\n"+strings.TrimRight(handover, "\n")+"\nProposed through the claude-code module on g14 at ") {
if !strings.HasPrefix(a.Details, "The text:\n"+strings.TrimRight(handover, "\n")+"\nProposed through the claude-code module on node-b at ") {
t.Errorf("a short text is not shown whole: %q", a.Details)
}
if len(a.Options) != 2 {
@@ -180,26 +180,26 @@ func TestAProposalAsksTheOperatorAtTheLevelApproveAndChangesNothing(t *testing.T
}
func TestApprovedTextReachesEveryNodeOnlyOnAWarrantEachNodeProves(t *testing.T) {
g14, _ := node(t, "g14")
shanks, _ := node(t, "shanks")
nodeB, _ := node(t, "node-b")
nodeD, _ := node(t, "node-d")
store, r := &memProposals{m: map[string]json.RawMessage{}}, newRouter()
id := proposeHandover(t, g14, store, r)
id := proposeHandover(t, nodeB, store, r)
r.close(r.chose(id, OptionApprove, t0.Add(time.Hour)))
rec := &recorded{}
changed, said, err := Reconcile(shanks, store, r, rec.record, t0.Add(time.Hour+time.Minute))
changed, said, err := Reconcile(nodeD, store, r, rec.record, t0.Add(time.Hour+time.Minute))
if err != nil || !changed {
t.Fatalf("shanks did not take the approved text: %v %v", err, said)
t.Fatalf("node-d did not take the approved text: %v %v", err, said)
}
got := renderedInstructions(t, shanks)
if !strings.Contains(got, "## Instructions the operator approved") || !strings.Contains(got, "### handoff-from-g14") ||
got := renderedInstructions(t, nodeD)
if !strings.Contains(got, "## Instructions the operator approved") || !strings.Contains(got, "### handoff-from-node-b") ||
!strings.Contains(got, "Do not merge it before the gate is green.") {
t.Fatalf("a new session on shanks does not read the text:\n%s", got)
t.Fatalf("a new session on node-d does not read the text:\n%s", got)
}
if len(rec.asks) != 1 || rec.asks[0] != id {
t.Errorf("the warrant was not handed to the hand-act log: %v", rec.asks)
}
// Heard again, or replayed: decided once, recorded once.
if changed, _, _ := Reconcile(shanks, store, r, rec.record, t0.Add(2*time.Hour)); changed || len(rec.asks) != 1 {
if changed, _, _ := Reconcile(nodeD, store, r, rec.record, t0.Add(2*time.Hour)); changed || len(rec.asks) != 1 {
t.Errorf("a warrant heard twice acted twice (changed %v, recorded %v)", changed, rec.asks)
}
}
@@ -259,7 +259,7 @@ func TestNothingBecomesActiveWithoutAValidWarrant(t *testing.T) {
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
p, _ := node(t, "shanks")
p, _ := node(t, "node-d")
store, r := &memProposals{m: map[string]json.RawMessage{}}, newRouter()
id := proposeHandover(t, p, store, r)
c.tamper(r, store, id)
@@ -278,7 +278,7 @@ func TestNothingBecomesActiveWithoutAValidWarrant(t *testing.T) {
}
func TestARemovalNeedsTheSameApprovalAndARecordDeletedRemovesNothing(t *testing.T) {
p, _ := node(t, "shanks")
p, _ := node(t, "node-d")
store, r := &memProposals{m: map[string]json.RawMessage{}}, newRouter()
id := proposeHandover(t, p, store, r)
r.close(r.chose(id, OptionApprove, t0.Add(time.Hour)))
@@ -289,31 +289,31 @@ func TestARemovalNeedsTheSameApprovalAndARecordDeletedRemovesNothing(t *testing.
if _, _, err := Reconcile(p, store, r, nil, t0.Add(2*time.Hour)); err != nil {
t.Fatal(err)
}
if !strings.Contains(renderedInstructions(t, p), "### handoff-from-g14") {
if !strings.Contains(renderedInstructions(t, p), "### handoff-from-node-b") {
t.Fatal("deleting the bus record removed an approved section")
}
out, err := Propose(p, ProposeInput{Scope: ScopeMesh, Name: "handoff-from-g14", Remove: true}, store, r, t0.Add(3*time.Hour))
out, err := Propose(p, ProposeInput{Scope: ScopeMesh, Name: "handoff-from-node-b", Remove: true}, store, r, t0.Add(3*time.Hour))
if err != nil {
t.Fatal(err)
}
rid := out["proposed"].(string)
if r.published[rid].Explanation != "Every Claude session on the mesh stops reading the instructions 'handoff-from-g14' from its next start." {
if r.published[rid].Explanation != "Every Claude session on the mesh stops reading the instructions 'handoff-from-node-b' from its next start." {
t.Errorf("removal ask: %q", r.published[rid].Explanation)
}
if !strings.Contains(renderedInstructions(t, p), "### handoff-from-g14") {
if !strings.Contains(renderedInstructions(t, p), "### handoff-from-node-b") {
t.Fatal("proposing a removal removed it")
}
r.close(r.chose(rid, OptionApprove, t0.Add(4*time.Hour)))
if changed, _, _ := Reconcile(p, store, r, nil, t0.Add(4*time.Hour)); !changed {
t.Fatal("the approved removal changed nothing")
}
if strings.Contains(renderedInstructions(t, p), "handoff-from-g14") {
if strings.Contains(renderedInstructions(t, p), "handoff-from-node-b") {
t.Fatal("the approved removal left the section")
}
// The first approval, replayed into the bus again, brings nothing back: it was decided here.
r2 := r.records[id]
_ = store.Put(id, Proposal{ID: id, Action: ActionAdd, Scope: ScopeMesh, Name: "handoff-from-g14", Text: handover,
Digest: textDigest(handover), From: "g14", At: t0})
_ = store.Put(id, Proposal{ID: id, Action: ActionAdd, Scope: ScopeMesh, Name: "handoff-from-node-b", Text: handover,
Digest: textDigest(handover), From: "node-b", At: t0})
r.records[id] = r2
if changed, _, _ := Reconcile(p, store, r, nil, t0.Add(5*time.Hour)); changed {
t.Fatal("a replayed approval brought a removed section back")
@@ -321,22 +321,22 @@ func TestARemovalNeedsTheSameApprovalAndARecordDeletedRemovesNothing(t *testing.
}
func TestANodeScopedSectionReachesOnlyItsNodes(t *testing.T) {
ace, _ := node(t, "ace")
shanks, _ := node(t, "shanks")
nodeA, _ := node(t, "node-a")
nodeD, _ := node(t, "node-d")
store, r := &memProposals{m: map[string]json.RawMessage{}}, newRouter()
out, err := Propose(ace, ProposeInput{Scope: ScopeNode, Nodes: []string{"shanks"}, Name: "builds", Text: "Builds run here.\n"}, store, r, t0)
out, err := Propose(nodeA, ProposeInput{Scope: ScopeNode, Nodes: []string{"node-d"}, Name: "builds", Text: "Builds run here.\n"}, store, r, t0)
if err != nil {
t.Fatal(err)
}
id := out["proposed"].(string)
if !strings.HasPrefix(r.published[id].Explanation, "Every Claude session on shanks reads this line as the mesh's own word") {
if !strings.HasPrefix(r.published[id].Explanation, "Every Claude session on node-d reads this line as the mesh's own word") {
t.Errorf("%q", r.published[id].Explanation)
}
r.close(r.chose(id, OptionApprove, t0.Add(time.Hour)))
for _, n := range []struct {
p Paths
want bool
}{{ace, false}, {shanks, true}} {
}{{nodeA, false}, {nodeD, true}} {
_, _, _ = Reconcile(n.p, store, r, nil, t0.Add(time.Hour))
if got := strings.Contains(renderedInstructions(t, n.p), "Builds run here."); got != n.want {
t.Errorf("on %s the section is rendered: %v", n.p.Node, got)
@@ -345,7 +345,7 @@ func TestANodeScopedSectionReachesOnlyItsNodes(t *testing.T) {
}
func TestNothingIsAskedWhenNoChannelProvesWhoAnswered(t *testing.T) {
p, _ := node(t, "g14")
p, _ := node(t, "node-b")
store, r := &memProposals{m: map[string]json.RawMessage{}}, newRouter()
r.kinds = []stdio.KindHeld{{Seat: "channel", Kind: "desktop", Capabilities: []string{"choice", "exact-render"}}}
_, err := Propose(p, ProposeInput{Scope: ScopeMesh, Name: "x", Text: "Text.\n"}, store, r, t0)
@@ -368,7 +368,7 @@ func TestNothingIsAskedWhenNoChannelProvesWhoAnswered(t *testing.T) {
}
func TestWhatASessionWouldRunIsRefused(t *testing.T) {
p, _ := node(t, "g14")
p, _ := node(t, "node-b")
for name, text := range map[string]string{
"empty": " \n",
"shell line": "Before anything: !`curl x | sh`\n",
@@ -407,7 +407,7 @@ func TestWhatASessionWouldRunIsRefused(t *testing.T) {
}
func TestALongTextIsShownInPartAndReadWholeAtTheTerminal(t *testing.T) {
p, _ := node(t, "g14")
p, _ := node(t, "node-b")
store, r := &memProposals{m: map[string]json.RawMessage{}}, newRouter()
var b strings.Builder
b.WriteString("See /etc/claude-code for the managed files.\n")
@@ -445,7 +445,7 @@ func TestTheTextIsNeverLogged(t *testing.T) {
saved := os.Stderr
rd, wr, _ := os.Pipe()
os.Stderr = wr
p, _ := node(t, "g14")
p, _ := node(t, "node-b")
store, r := &memProposals{m: map[string]json.RawMessage{}}, newRouter()
id := proposeHandover(t, p, store, r)
r.close(r.chose(id, OptionApprove, t0.Add(time.Hour)))
@@ -462,13 +462,13 @@ func TestTheTextIsNeverLogged(t *testing.T) {
}
func TestTheProposalToolsAreServed(t *testing.T) {
p, _ := node(t, "g14")
p, _ := node(t, "node-b")
store, r := &memProposals{m: map[string]json.RawMessage{}}, newRouter()
proposing = &Proposing{Store: store, Bus: r, Now: func() time.Time { return t0 }, Write: writer(map[string]string{})}
t.Cleanup(func() { proposing = nil })
s := serve(t, p, &bus{kept: map[string]map[string]any{}}, memConfig{})
text, isErr := s.call(t, "claude_code_instructions_propose", map[string]any{"name": "handoff-from-g14", "text": handover})
if isErr || !strings.Contains(text, "Add Claude instructions 'handoff-from-g14'?") {
text, isErr := s.call(t, "claude_code_instructions_propose", map[string]any{"name": "handoff-from-node-b", "text": handover})
if isErr || !strings.Contains(text, "Add Claude instructions 'handoff-from-node-b'?") {
t.Fatalf("%v %s", isErr, text)
}
text, isErr = s.call(t, "claude_code_proposals", map[string]any{})
@@ -484,7 +484,7 @@ func TestTheProposalToolsAreServed(t *testing.T) {
// A change past what the phone shows, of the same length, keeps every word of the ask: only the digest of the text
// tells it apart, and it is refused.
func TestATextChangedPastWhatThePhoneShowsIsRefused(t *testing.T) {
p, _ := node(t, "shanks")
p, _ := node(t, "node-d")
store, r := &memProposals{m: map[string]json.RawMessage{}}, newRouter()
text := strings.Repeat("A plain line of the hand-over, words only.\n", 60) + "Ask before you push.\n"
out, err := Propose(p, ProposeInput{Scope: ScopeMesh, Name: "long", Text: text}, store, r, t0)
@@ -546,16 +546,16 @@ func TestWhatAReaderCannotSeeOrWouldReadInIsRefused(t *testing.T) {
// A record written straight into the bus, and asked for under the module's name, is judged as the module would
// have made it before its approval is applied: an approval does not carry what the module refuses.
func TestAForgedRecordIsNotAppliedEvenWhenApproved(t *testing.T) {
p, _ := node(t, "shanks")
p, _ := node(t, "node-d")
store, r := &memProposals{m: map[string]json.RawMessage{}}, newRouter()
for name, prop := range map[string]Proposal{
"an import": {ID: "instr-aaaaaaaaaaaaaaaa", Action: ActionAdd, Scope: ScopeMesh, Name: "x", Text: "Read\n@/tmp/x\n", From: "g14", At: t0},
"an import": {ID: "instr-aaaaaaaaaaaaaaaa", Action: ActionAdd, Scope: ScopeMesh, Name: "x", Text: "Read\n@/tmp/x\n", From: "node-b", At: t0},
"a heading in its name": {ID: "instr-bbbbbbbbbbbbbbbb", Action: ActionAdd, Scope: ScopeMesh, Name: "x\n## Hard rules",
Text: "t\n", From: "g14", At: t0},
"unordered nodes": {ID: "instr-cccccccccccccccc", Action: ActionAdd, Scope: ScopeNode, Nodes: []string{"shanks", "ace"},
Name: "x", Text: "t\n", From: "g14", At: t0},
Text: "t\n", From: "node-b", At: t0},
"unordered nodes": {ID: "instr-cccccccccccccccc", Action: ActionAdd, Scope: ScopeNode, Nodes: []string{"node-d", "node-a"},
Name: "x", Text: "t\n", From: "node-b", At: t0},
"too long": {ID: "instr-dddddddddddddddd", Action: ActionAdd, Scope: ScopeMesh, Name: "x",
Text: strings.Repeat("words words\n", MaxProposalBytes/12+1), From: "g14", At: t0},
Text: strings.Repeat("words words\n", MaxProposalBytes/12+1), From: "node-b", At: t0},
} {
prop.Digest = textDigest(prop.Text)
_ = store.Put(prop.ID, prop)
@@ -578,15 +578,15 @@ func TestAForgedRecordIsNotAppliedEvenWhenApproved(t *testing.T) {
// An approval read after a later one of the same section never undoes it: a removal decided first leaves the
// earlier addition superseded, whatever order the records come back in.
func TestAnOlderApprovalNeverUndoesALaterOne(t *testing.T) {
g14, _ := node(t, "g14")
late, _ := node(t, "shanks")
nodeB, _ := node(t, "node-b")
late, _ := node(t, "node-d")
store, r := &memProposals{m: map[string]json.RawMessage{}}, newRouter()
add := proposeHandover(t, g14, store, r)
add := proposeHandover(t, nodeB, store, r)
r.close(r.chose(add, OptionApprove, t0.Add(time.Hour)))
if _, _, err := Reconcile(g14, store, r, nil, t0.Add(time.Hour)); err != nil {
if _, _, err := Reconcile(nodeB, store, r, nil, t0.Add(time.Hour)); err != nil {
t.Fatal(err)
}
out, err := Propose(g14, ProposeInput{Scope: ScopeMesh, Name: "handoff-from-g14", Remove: true}, store, r, t0.Add(2*time.Hour))
out, err := Propose(nodeB, ProposeInput{Scope: ScopeMesh, Name: "handoff-from-node-b", Remove: true}, store, r, t0.Add(2*time.Hour))
if err != nil {
t.Fatal(err)
}
@@ -602,7 +602,7 @@ func TestAnOlderApprovalNeverUndoesALaterOne(t *testing.T) {
if changed, _, _ := Reconcile(late, store, r, nil, t0.Add(5*time.Hour)); changed {
t.Fatal("the earlier addition, read after the removal, was applied")
}
if strings.Contains(renderedInstructions(t, late), "handoff-from-g14") {
if strings.Contains(renderedInstructions(t, late), "handoff-from-node-b") {
t.Fatal("a removed section came back")
}
if got := outcomeHere(t, late, add); !strings.Contains(got, "superseded") {
@@ -611,7 +611,7 @@ func TestAnOlderApprovalNeverUndoesALaterOne(t *testing.T) {
}
func TestAnUnreadableRecordIsNeverTakenForAnEmptyOne(t *testing.T) {
p, _ := node(t, "shanks")
p, _ := node(t, "node-d")
store, r := &memProposals{m: map[string]json.RawMessage{}}, newRouter()
id := proposeHandover(t, p, store, r)
r.close(r.chose(id, OptionApprove, t0.Add(time.Hour)))
@@ -630,27 +630,27 @@ func TestAnUnreadableRecordIsNeverTakenForAnEmptyOne(t *testing.T) {
// A section for some nodes is its own: a proposal of the same name for other nodes adds another, and replaces
// or removes nothing of the first.
func TestANodeSectionIsNotReplacedFromOtherNodes(t *testing.T) {
ace, _ := node(t, "ace")
nodeA, _ := node(t, "node-a")
store, r := &memProposals{m: map[string]json.RawMessage{}}, newRouter()
out, _ := Propose(ace, ProposeInput{Scope: ScopeNode, Nodes: []string{"ace"}, Name: "builds", Text: "On ace.\n"}, store, r, t0)
out, _ := Propose(nodeA, ProposeInput{Scope: ScopeNode, Nodes: []string{"node-a"}, Name: "builds", Text: "On node-a.\n"}, store, r, t0)
r.close(r.chose(out["proposed"].(string), OptionApprove, t0.Add(time.Hour)))
_, _, _ = Reconcile(ace, store, r, nil, t0.Add(time.Hour))
out, err := Propose(ace, ProposeInput{Scope: ScopeNode, Nodes: []string{"shanks"}, Name: "builds", Text: "On shanks.\n"}, store, r, t0.Add(2*time.Hour))
_, _, _ = Reconcile(nodeA, store, r, nil, t0.Add(time.Hour))
out, err := Propose(nodeA, ProposeInput{Scope: ScopeNode, Nodes: []string{"node-d"}, Name: "builds", Text: "On node-d.\n"}, store, r, t0.Add(2*time.Hour))
if err != nil {
t.Fatal(err)
}
if out["action"] != ActionAdd {
t.Errorf("a section for other nodes was proposed as %v", out["action"])
}
if _, err := Propose(ace, ProposeInput{Scope: ScopeNode, Nodes: []string{"shanks"}, Name: "builds", Remove: true}, store, r, t0.Add(2*time.Hour)); err == nil {
t.Error("a removal for other nodes was taken for the section on ace")
if _, err := Propose(nodeA, ProposeInput{Scope: ScopeNode, Nodes: []string{"node-d"}, Name: "builds", Remove: true}, store, r, t0.Add(2*time.Hour)); err == nil {
t.Error("a removal for other nodes was taken for the section on node-a")
}
}
// A module that only asks is issued no channel kinds (they come with a kinded bench a module uses or watches): an
// empty list is no refusal, and the router, which judges, is asked (the live failure of 2026-10-10).
func TestAnAskerIssuedNoKindsLeavesTheJudgingToTheRouter(t *testing.T) {
p, _ := node(t, "novox")
p, _ := node(t, "node-c")
store, r := &memProposals{m: map[string]json.RawMessage{}}, newRouter()
r.kinds = nil
out, err := Propose(p, ProposeInput{Scope: ScopeMesh, Name: "x", Text: "Text.\n"}, store, r, t0)
@@ -664,7 +664,7 @@ func TestAnAskerIssuedNoKindsLeavesTheJudgingToTheRouter(t *testing.T) {
// The router's refusal reaches the proposer at once, and discards the proposal on every node that hears it.
func TestTheRoutersRefusalIsSaidAndDiscards(t *testing.T) {
p, _ := node(t, "novox")
p, _ := node(t, "node-c")
store := &memProposals{m: map[string]json.RawMessage{}}
r := newRouter()
r.kinds = nil
@@ -687,7 +687,7 @@ func TestTheRoutersRefusalIsSaidAndDiscards(t *testing.T) {
// A proposal whose ask ended and that the router keeps no record of is discarded, never left waiting.
func TestAnAskTheRouterKeepsNoRecordOfIsDiscardedAfterItsTime(t *testing.T) {
p, _ := node(t, "novox")
p, _ := node(t, "node-c")
store, r := &memProposals{m: map[string]json.RawMessage{}}, newRouter()
id := proposeHandover(t, p, store, r)
delete(r.records, id)
@@ -726,7 +726,7 @@ func (r *refusingRouter) Publish(subject string, body any, id string) error {
// A refusal heard, and then the same ask taken after all and approved: the approval is taken, not lost in silence.
func TestAnAskTakenAfterItsRefusalIsStillAnswered(t *testing.T) {
p, _ := node(t, "novox")
p, _ := node(t, "node-c")
store, r := &memProposals{m: map[string]json.RawMessage{}}, newRouter()
id := proposeHandover(t, p, store, r)
if _, err := HeardEnd(p, store, asks.Warrant{Ask: id, Asker: Module, Outcome: asks.OutcomeRefused, Words: "no channel", At: t0}, t0); err != nil {
@@ -747,9 +747,9 @@ func TestAnAskTakenAfterItsRefusalIsStillAnswered(t *testing.T) {
// A proposer's summary is the explanation, held to a glance; a proposal made before issue 639 is composed as it was
// asked, so the warrant for it still holds after the change.
func TestAProposalIsAskedAtAGlanceAndAnOlderOneAsItWasAsked(t *testing.T) {
p, _ := node(t, "g14")
p, _ := node(t, "node-b")
store, r := &memProposals{m: map[string]json.RawMessage{}}, newRouter()
out, err := Propose(p, ProposeInput{Scope: ScopeMesh, Name: "handoff-from-g14", Text: handover,
out, err := Propose(p, ProposeInput{Scope: ScopeMesh, Name: "handoff-from-node-b", Text: handover,
Summary: "Every session learns how hand-overs are written, so the next one reads them the same way."}, store, r, t0)
if err != nil {
t.Fatal(err)
@@ -26,7 +26,7 @@ func mergeTonight() QuestionInput {
},
Why: "it is reviewed and the checks are green",
OnExpiry: "I leave the pull request open",
By: "fix-589@g14",
By: "fix-589@node-b",
}
}
@@ -40,7 +40,7 @@ func askMergeTonight(t *testing.T, p Paths, store *memQuestions, r *router, at t
}
func TestAQuestionIsAskedUnderTheModulesNameAtTheLevelApprove(t *testing.T) {
p, _ := node(t, "g14")
p, _ := node(t, "node-b")
store, r := newMemQuestions(), newRouter()
out, err := AskQuestion(p, mergeTonight(), store, r, t0)
if err != nil {
@@ -72,7 +72,7 @@ func TestAQuestionIsAskedUnderTheModulesNameAtTheLevelApprove(t *testing.T) {
if !strings.HasSuffix(a.Explanation, "\n• Recommended: Merge now, because it is reviewed and the checks are green.") || asks.Glance(a.Explanation) != nil {
t.Errorf("the explanation does not read at a glance:\n%s", a.Explanation)
}
for _, want := range []string{"the agent session fix-589 on g14", "performs nothing by itself"} {
for _, want := range []string{"the agent session fix-589 on node-b", "performs nothing by itself"} {
if !strings.Contains(a.Details, want) || strings.Contains(a.Explanation, want) {
t.Errorf("the details do not say %q:\n%s", want, a.Details)
}
@@ -83,7 +83,7 @@ func TestAQuestionIsAskedUnderTheModulesNameAtTheLevelApprove(t *testing.T) {
}
func TestTheChosenAnswerIsReadBackByItsKey(t *testing.T) {
p, _ := node(t, "g14")
p, _ := node(t, "node-b")
store, r := newMemQuestions(), newRouter()
key := askMergeTonight(t, p, store, r, t0)
out, err := ReadAnswer(store, r, key, t0.Add(time.Minute))
@@ -105,7 +105,7 @@ func TestTheChosenAnswerIsReadBackByItsKey(t *testing.T) {
}
func TestAnAnswerThatDoesNotHoldToTheQuestionIsNotProven(t *testing.T) {
p, _ := node(t, "g14")
p, _ := node(t, "node-b")
store, r := newMemQuestions(), newRouter()
key := askMergeTonight(t, p, store, r, t0)
w := r.chose(key, "o1", t0.Add(time.Minute))
@@ -140,7 +140,7 @@ func TestAnAnswerThatDoesNotHoldToTheQuestionIsNotProven(t *testing.T) {
}
func TestAQuestionIsRefusedOutsideItsBounds(t *testing.T) {
p, _ := node(t, "g14")
p, _ := node(t, "node-b")
for name, change := range map[string]func(*QuestionInput){
"one option": func(in *QuestionInput) { in.Options = in.Options[:1] },
"five options": func(in *QuestionInput) { in.Options = append(in.Options, in.Options[1], in.Options[1], in.Options[1]) },
@@ -154,7 +154,7 @@ func TestAQuestionIsRefusedOutsideItsBounds(t *testing.T) {
"an option unsaid": func(in *QuestionInput) { in.Options[1].Means = "" },
"too short": func(in *QuestionInput) { in.Expires = time.Minute },
"too long": func(in *QuestionInput) { in.Expires = 2 * 24 * time.Hour },
"another node": func(in *QuestionInput) { in.By = "fix-589@shanks" },
"another node": func(in *QuestionInput) { in.By = "fix-589@node-d" },
"an invisible char": func(in *QuestionInput) { in.Context += "\u200b" },
"invisible label": func(in *QuestionInput) { in.Options[0].Label = "Merge\u200bnow" },
"invisible means": func(in *QuestionInput) { in.Options[1].Means += "\u202e" },
@@ -182,7 +182,7 @@ func TestAQuestionIsRefusedOutsideItsBounds(t *testing.T) {
}
func TestAtMostTwoQuestionsWaitSoAProposalFindsRoom(t *testing.T) {
p, _ := node(t, "g14")
p, _ := node(t, "node-b")
store, r := newMemQuestions(), newRouter()
first := askMergeTonight(t, p, store, r, t0)
askMergeTonight(t, p, store, r, t0)
@@ -219,7 +219,7 @@ func (r *questionRefusingRouter) Publish(subject string, body any, id string) er
}
func TestTheRoutersRefusalIsSaidAndKept(t *testing.T) {
p, _ := node(t, "g14")
p, _ := node(t, "node-b")
store := newMemQuestions()
refusing := &questionRefusingRouter{router: newRouter(), store: store}
within, every := RouterAnswersWithin, RouterAnswersEvery
@@ -244,7 +244,7 @@ func TestTheRoutersRefusalIsSaidAndKept(t *testing.T) {
}
func TestAnExpiredQuestionSaysWhatTheSessionDoes(t *testing.T) {
p, _ := node(t, "g14")
p, _ := node(t, "node-b")
store, r := newMemQuestions(), newRouter()
key := askMergeTonight(t, p, store, r, t0)
r.close(asks.Warrant{Ask: key, Outcome: asks.OutcomeExpired, At: t0.Add(QuestionDefault)})
@@ -255,7 +255,7 @@ func TestAnExpiredQuestionSaysWhatTheSessionDoes(t *testing.T) {
}
func TestTheQuestionToolsAreServed(t *testing.T) {
p, _ := node(t, "g14")
p, _ := node(t, "node-b")
r := newRouter()
questions := newMemQuestions()
proposing = &Proposing{Store: &memProposals{m: map[string]json.RawMessage{}}, Bus: r, Now: func() time.Time { return t0 },
@@ -268,7 +268,7 @@ func TestTheQuestionToolsAreServed(t *testing.T) {
map[string]any{"label": "Merge now", "means": "I merge it", "recommended": true},
map[string]any{"label": "Wait", "means": "I leave it open"},
},
"why": "it is reviewed", "on_expiry": "I leave it open", "by": "fix-589@g14", "expires_minutes": 30,
"why": "it is reviewed", "on_expiry": "I leave it open", "by": "fix-589@node-b", "expires_minutes": 30,
})
if isErr || !strings.Contains(text, `"key"`) {
t.Fatalf("%v %s", isErr, text)
@@ -292,7 +292,7 @@ func TestAReadWaitsNoLongerThanItsBound(t *testing.T) {
if AnswerWaitsAtMost != 60*time.Second {
t.Fatalf("a read may wait %s; ADR 0332 says 60 s", AnswerWaitsAtMost)
}
p, _ := node(t, "g14")
p, _ := node(t, "node-b")
store, r := newMemQuestions(), newRouter()
key := askMergeTonight(t, p, store, r, t0)
most, every := AnswerWaitsAtMost, AnswerLooksEvery
@@ -369,7 +369,7 @@ func (s *memQuestions) Put(key string, value any) error {
// The edges of ADR 0332 §2 that are allowed: a 60-character headline and 1430 minutes.
func TestTheEdgesOfAQuestionAreAsked(t *testing.T) {
p, _ := node(t, "g14")
p, _ := node(t, "node-b")
in := mergeTonight()
in.Headline = strings.Repeat("x", 59) + "?"
in.Expires = 1430 * time.Minute
@@ -380,7 +380,7 @@ func TestTheEdgesOfAQuestionAreAsked(t *testing.T) {
// A warrant naming another asker or another ask, or an option the question did not offer, is no answer to it.
func TestAWarrantForAnotherAskOrOptionIsNotProven(t *testing.T) {
p, _ := node(t, "g14")
p, _ := node(t, "node-b")
for name, change := range map[string]func(*asks.Warrant){
"another asker": func(w *asks.Warrant) { w.Asker = "mesh-controller" },
"another ask": func(w *asks.Warrant) { w.Ask = "q-0000000000000000" },
@@ -400,7 +400,7 @@ func TestAWarrantForAnotherAskOrOptionIsNotProven(t *testing.T) {
// Two nodes asking at the same moment take two slots at most: of three sessions racing, one is refused.
func TestTheCapHoldsAcrossNodesAskingAtOnce(t *testing.T) {
store, r := newMemQuestions(), &lockedRouter{router: newRouter()}
nodes := []string{"g14", "shanks", "ace"}
nodes := []string{"node-b", "node-d", "node-a"}
var wg sync.WaitGroup
errs := make([]error, len(nodes))
for i, name := range nodes {
@@ -430,7 +430,7 @@ func TestTheCapHoldsAcrossNodesAskingAtOnce(t *testing.T) {
// A slot that cannot be read refuses the question: the cap fails closed.
func TestTheCapFailsClosed(t *testing.T) {
p, _ := node(t, "g14")
p, _ := node(t, "node-b")
store, r := newMemQuestions(), newRouter()
store.failGet = "slot-"
if _, err := AskQuestion(p, mergeTonight(), store, r, t0); err == nil || !strings.Contains(err.Error(), "could not be counted") {
@@ -444,7 +444,7 @@ func TestTheCapFailsClosed(t *testing.T) {
// A question whose refusal was not heard here, and of which the router holds no record, keeps its slot and reads as
// waiting until it expires — the router may still take it from its queue — and only then frees its slot and ends.
func TestAQuestionTheRouterHasNotRecordedHoldsItsSlotUntilItExpires(t *testing.T) {
p, _ := node(t, "g14")
p, _ := node(t, "node-b")
store, r := newMemQuestions(), newRouter()
first := askMergeTonight(t, p, store, r, t0)
askMergeTonight(t, p, store, r, t0)
@@ -470,13 +470,13 @@ func TestAQuestionTheRouterHasNotRecordedHoldsItsSlotUntilItExpires(t *testing.T
// A slot is taken only at the revision read: when another node takes both slots between this node's read and its
// write, this node's writes are refused and the question is refused — a plain write would take a slot it lost.
func TestASlotIsTakenOnlyAtTheRevisionRead(t *testing.T) {
p, _ := node(t, "g14")
p, _ := node(t, "node-b")
store, r := newMemQuestions(), newRouter()
// Two questions that ended: their slots are free to take again.
for _, id := range []string{askMergeTonight(t, p, store, r, t0), askMergeTonight(t, p, store, r, t0)} {
r.close(asks.Warrant{Ask: id, Outcome: asks.OutcomeExpired, At: t0.Add(time.Minute)})
}
shanks, _ := node(t, "shanks")
nodeD, _ := node(t, "node-d")
other := newRouter()
store.afterGet = func(key string) {
if !strings.HasPrefix(key, "slot-") {
@@ -484,7 +484,7 @@ func TestASlotIsTakenOnlyAtTheRevisionRead(t *testing.T) {
}
// Another node asks between this node's read of the slot and its write: its question waits.
q := Question{ID: newQuestionID(), Headline: "Another", Context: "c", Why: "w", OnExpiry: "e", By: "other",
From: shanks.Node, At: t0, Expires: t0.Add(time.Hour),
From: nodeD.Node, At: t0, Expires: t0.Add(time.Hour),
Options: []QuestionOption{{ID: "o1", Label: "A", Means: "a", Recommended: true}, {ID: "o2", Label: "B", Means: "b"}}}
_ = store.Put(q.ID, q)
_ = other.Publish(asks.AskSubject(Module), q.Ask(), q.ID)
@@ -122,7 +122,7 @@ func TestSessionStartRejectsAnUnsafeSessionID(t *testing.T) {
func TestSessionStartListsEveryNameOnTheNodeWithOpenWaits(t *testing.T) {
waitsDir := t.TempDir()
lastCall := time.Date(2026, 10, 21, 9, 55, 0, 0, time.UTC)
idx := NodeIndex{Node: "g14", At: lastCall, Names: []NodeIndexEntry{
idx := NodeIndex{Node: "node-b", At: lastCall, Names: []NodeIndexEntry{
{Name: "fix-1629", Open: 2, CurrentSession: "sess-old", LastCall: lastCall},
}}
raw, err := indented(idx)
@@ -135,7 +135,7 @@ func TestSessionStartListsEveryNameOnTheNodeWithOpenWaits(t *testing.T) {
if !strings.Contains(msg, "fix-1629") || !strings.Contains(msg, "2 open wait") {
t.Fatalf("the name and its count are not listed:\n%s", msg)
}
want := `claude_code_wait {"op":"adopt","by":"fix-1629@g14","session":"sess-brand-new"}`
want := `claude_code_wait {"op":"adopt","by":"fix-1629@node-b","session":"sess-brand-new"}`
if !strings.Contains(msg, want) {
t.Fatalf("the exact adopt call is not given:\n%s\nwant it to contain:\n%s", msg, want)
}
@@ -77,13 +77,13 @@ func addApproveWait(t *testing.T, p Paths, store WaitStore, name, session string
// 1629/AC1: add answers an id; list answers the open waits with their age and whether each is past its bound.
func TestAddAnswersAnIDAndListShowsItsAgeAndPastBound(t *testing.T) {
p, _ := node(t, "g14")
p, _ := node(t, "node-b")
store := newMemWaits()
id := addApproveWait(t, p, store, "fix-1629", "sess-1", t0)
if !strings.HasPrefix(id, WaitPrefix) {
t.Fatalf("id %q does not start with %q", id, WaitPrefix)
}
out, err := ListWaits(p, "sess-1", "fix-1629@g14", store, t0.Add(time.Minute))
out, err := ListWaits(p, "sess-1", "fix-1629@node-b", store, t0.Add(time.Minute))
if err != nil {
t.Fatal(err)
}
@@ -91,7 +91,7 @@ func TestAddAnswersAnIDAndListShowsItsAgeAndPastBound(t *testing.T) {
if len(open) != 1 || open[0]["id"] != id || open[0]["past_bound"] != false || open[0]["age_seconds"] != 60 {
t.Fatalf("%+v", open)
}
out, err = ListWaits(p, "sess-1", "fix-1629@g14", store, t0.Add(11*time.Minute))
out, err = ListWaits(p, "sess-1", "fix-1629@node-b", store, t0.Add(11*time.Minute))
if err != nil {
t.Fatal(err)
}
@@ -104,9 +104,9 @@ func TestAddAnswersAnIDAndListShowsItsAgeAndPastBound(t *testing.T) {
// 1629/AC2: watched, done and said change the wait; said is refused for a wait not past its bound; done removes
// it from the open list.
func TestWatchedDoneAndSaidChangeTheWait(t *testing.T) {
p, _ := node(t, "g14")
p, _ := node(t, "node-b")
store := newMemWaits()
by := "fix-1629@g14"
by := "fix-1629@node-b"
id := addApproveWait(t, p, store, "fix-1629", "sess-2", t0)
if _, err := SaidWait(p, "sess-2", by, id, store, t0.Add(time.Minute)); err == nil || !strings.Contains(err.Error(), "not past its bound") {
@@ -143,9 +143,9 @@ func TestWatchedDoneAndSaidChangeTheWait(t *testing.T) {
// 1629/AC3: every change rewrites the session's waits file whole, atomically; it holds exactly the open waits
// and the write time.
func TestEveryChangeRewritesTheWaitsFileWhole(t *testing.T) {
p, _ := node(t, "g14")
p, _ := node(t, "node-b")
store := newMemWaits()
by := "fix-1629@g14"
by := "fix-1629@node-b"
id1 := addApproveWait(t, p, store, "fix-1629", "sess-3", t0)
f, err := readWaitsFile(waitsFilePath(p, "sess-3"))
if err != nil {
@@ -179,9 +179,9 @@ func TestEveryChangeRewritesTheWaitsFileWhole(t *testing.T) {
// 1629/AC4: the waits are read again after the module "restarts" (the state is kept) and for the same name
// after a /clear that keeps calling with the same session id; a different name lists none of them.
func TestWaitsSurviveARestartAndAreNeverSeenByAnotherName(t *testing.T) {
p, _ := node(t, "g14")
p, _ := node(t, "node-b")
store := newMemWaits()
by := "fix-1629@g14"
by := "fix-1629@node-b"
addApproveWait(t, p, store, "fix-1629", "sess-4", t0)
// A restart: a fresh Waiting over the same store reads the same waits.
@@ -199,7 +199,7 @@ func TestWaitsSurviveARestartAndAreNeverSeenByAnotherName(t *testing.T) {
}
// A different name, same node: none of them.
out, err = ListWaits(p, "sess-other", "fix-other@g14", store, t0.Add(2*time.Minute))
out, err = ListWaits(p, "sess-other", "fix-other@node-b", store, t0.Add(2*time.Minute))
if err != nil || len(out["open"].([]map[string]any)) != 0 {
t.Fatalf("another name saw them: %v %v", out, err)
}
@@ -208,7 +208,7 @@ func TestWaitsSurviveARestartAndAreNeverSeenByAnotherName(t *testing.T) {
// 1629/AC5: add refuses an empty what, a bound under 1 or over 1440 minutes, a read address that is not an
// address, and more than 50 open waits per name, naming the argument.
func TestAddRefusesOutsideItsBounds(t *testing.T) {
p, _ := node(t, "g14")
p, _ := node(t, "node-b")
base := func() WaitInput {
return WaitInput{Session: "sess-5", By: "fix-1629@" + p.Node, What: "something", Read: WaitRead{Address: "mesh-delivery.recheck"}, Bound: 10}
}
@@ -247,8 +247,8 @@ func TestAddRefusesOutsideItsBounds(t *testing.T) {
// Review round 1 item (b): list, watched, done and said refuse a session id that is not a plain one (the
// same waitSessionID add already holds to), naming "session" — not silently treating it as just-another-key.
func TestListWatchedDoneAndSaidRefuseABadSessionID(t *testing.T) {
p, _ := node(t, "g14")
by := "fix-1629@g14"
p, _ := node(t, "node-b")
by := "fix-1629@node-b"
for _, bad := range []string{"", "a/b", "../x"} {
store := newMemWaits()
if _, err := ListWaits(p, bad, by, store, t0); err == nil || !strings.Contains(err.Error(), "session") {
@@ -268,13 +268,13 @@ func TestListWatchedDoneAndSaidRefuseABadSessionID(t *testing.T) {
// 1629/AC6: the tool is served through the module's own dispatch.
func TestTheWaitToolIsServedThroughTheModulesDispatch(t *testing.T) {
p, _ := node(t, "g14")
p, _ := node(t, "node-b")
store := newMemWaits()
waiting = &Waiting{Store: store, Now: func() time.Time { return t0 }}
t.Cleanup(func() { waiting = nil })
s := serve(t, p, &bus{kept: map[string]map[string]any{}}, memConfig{})
text, isErr := s.call(t, "claude_code_wait", map[string]any{"op": "add", "session": "sess-6", "by": "fix-1629@g14",
text, isErr := s.call(t, "claude_code_wait", map[string]any{"op": "add", "session": "sess-6", "by": "fix-1629@node-b",
"what": "the operator's Approve", "read": map[string]any{"address": "mesh-delivery.recheck"}, "bound_minutes": 10})
if isErr {
t.Fatalf("add: %s", text)
@@ -288,12 +288,12 @@ func TestTheWaitToolIsServedThroughTheModulesDispatch(t *testing.T) {
t.Fatalf("no id answered: %s", text)
}
text, isErr = s.call(t, "claude_code_wait", map[string]any{"op": "list", "session": "sess-6", "by": "fix-1629@g14"})
text, isErr = s.call(t, "claude_code_wait", map[string]any{"op": "list", "session": "sess-6", "by": "fix-1629@node-b"})
if isErr || !strings.Contains(text, id) {
t.Fatalf("list: %v %s", isErr, text)
}
text, isErr = s.call(t, "claude_code_wait", map[string]any{"op": "done", "session": "sess-6", "by": "fix-1629@g14",
text, isErr = s.call(t, "claude_code_wait", map[string]any{"op": "done", "session": "sess-6", "by": "fix-1629@node-b",
"id": id, "outcome": "approved"})
if isErr {
t.Fatalf("done: %s", text)
@@ -305,10 +305,10 @@ func TestTheWaitToolIsServedThroughTheModulesDispatch(t *testing.T) {
// ADR 0490 §1: a name nothing has bound yet is claimed by the first session that calls for it (add), without
// a separate adopt first.
func TestAFirstAddBindsAnUnboundNameAtOnce(t *testing.T) {
p, _ := node(t, "g14")
p, _ := node(t, "node-b")
store := newMemWaits()
id := addApproveWait(t, p, store, "fix-1629", "sess-a", t0)
out, err := ListWaits(p, "sess-a", "fix-1629@g14", store, t0)
out, err := ListWaits(p, "sess-a", "fix-1629@node-b", store, t0)
if err != nil || len(out["open"].([]map[string]any)) != 1 || out["open"].([]map[string]any)[0]["id"] != id {
t.Fatalf("%v %v", out, err)
}
@@ -317,9 +317,9 @@ func TestAFirstAddBindsAnUnboundNameAtOnce(t *testing.T) {
// ADR 0490 §1/§3: once a name is bound to a session, a different, non-adopted session calling for the same
// name (any op) is refused, naming the session that holds it and when it last called.
func TestADifferentSessionIsRefusedWithoutAdopting(t *testing.T) {
p, _ := node(t, "g14")
p, _ := node(t, "node-b")
store := newMemWaits()
by := "fix-1629@g14"
by := "fix-1629@node-b"
addApproveWait(t, p, store, "fix-1629", "sess-old", t0)
if _, err := AddWait(p, WaitInput{Session: "sess-new", By: by, What: "another", Read: WaitRead{Address: "mesh-delivery.recheck"},
@@ -341,9 +341,9 @@ func TestADifferentSessionIsRefusedWithoutAdopting(t *testing.T) {
// ADR 0490 §2/§3: adopt right after a simulated /clear binds the name to the new session id at once; the
// mirrored file of the old session id is emptied and the new one holds the waits, with no delay.
func TestAdoptRightAfterAClearBindsAtOnce(t *testing.T) {
p, _ := node(t, "g14")
p, _ := node(t, "node-b")
store := newMemWaits()
by := "fix-1629@g14"
by := "fix-1629@node-b"
id := addApproveWait(t, p, store, "fix-1629", "sess-old", t0)
out, err := AdoptName(p, by, "sess-new", store, t0.Add(time.Minute))
@@ -376,9 +376,9 @@ func TestAdoptRightAfterAClearBindsAtOnce(t *testing.T) {
// ADR 0490 §3: adopt of a name bound to another id always takes it over, however recently that id called —
// no bound protects it.
func TestAdoptAlwaysTakesOverEvenJustCalled(t *testing.T) {
p, _ := node(t, "g14")
p, _ := node(t, "node-b")
store := newMemWaits()
by := "fix-1629@g14"
by := "fix-1629@node-b"
addApproveWait(t, p, store, "fix-1629", "sess-fresh", t0)
// sess-fresh calls again right before being adopted away from.
if _, err := ListWaits(p, "sess-fresh", by, store, t0.Add(10*time.Second)); err != nil {
@@ -396,9 +396,9 @@ func TestAdoptAlwaysTakesOverEvenJustCalled(t *testing.T) {
// ADR 0490 §3: the earlier id's next claude_code_wait call (any op) answers that its name now answers to
// another session, naming it.
func TestTheEarlierIDIsToldOnItsNextCall(t *testing.T) {
p, _ := node(t, "g14")
p, _ := node(t, "node-b")
store := newMemWaits()
by := "fix-1629@g14"
by := "fix-1629@node-b"
addApproveWait(t, p, store, "fix-1629", "sess-old", t0)
if _, err := AdoptName(p, by, "sess-new", store, t0.Add(time.Minute)); err != nil {
t.Fatal(err)
@@ -413,11 +413,11 @@ func TestTheEarlierIDIsToldOnItsNextCall(t *testing.T) {
// ADR 0490 §2: the node-wide index lists every name on this node with an open wait, for the SessionStart
// hook to read directly.
func TestTheNodeWideIndexListsEveryNameWithOpenWaits(t *testing.T) {
p, _ := node(t, "g14")
p, _ := node(t, "node-b")
store := newMemWaits()
addApproveWait(t, p, store, "fix-1629", "sess-x", t0)
addApproveWait(t, p, store, "fix-1744", "sess-y", t0)
if _, err := DoneWait(p, "sess-x", "fix-1629@g14", wantsOnlyWait(t, store, p, "fix-1629", "sess-x"), "done",
if _, err := DoneWait(p, "sess-x", "fix-1629@node-b", wantsOnlyWait(t, store, p, "fix-1629", "sess-x"), "done",
store, t0.Add(time.Minute)); err != nil {
t.Fatal(err)
}
@@ -426,7 +426,7 @@ func TestTheNodeWideIndexListsEveryNameWithOpenWaits(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if idx.Node != "g14" || len(idx.Names) != 1 || idx.Names[0].Name != "fix-1744" || idx.Names[0].Open != 1 ||
if idx.Node != "node-b" || len(idx.Names) != 1 || idx.Names[0].Name != "fix-1744" || idx.Names[0].Open != 1 ||
idx.Names[0].CurrentSession != "sess-y" {
t.Fatalf("%+v", idx)
}
@@ -448,18 +448,18 @@ func wantsOnlyWait(t *testing.T, store WaitStore, p Paths, name, session string)
// claude_code_wait op=adopt, served through the module's own dispatch (1629/AC6, ADR 0490).
func TestAdoptIsServedThroughTheModulesDispatch(t *testing.T) {
p, _ := node(t, "g14")
p, _ := node(t, "node-b")
store := newMemWaits()
waiting = &Waiting{Store: store, Now: func() time.Time { return t0 }}
t.Cleanup(func() { waiting = nil })
s := serve(t, p, &bus{kept: map[string]map[string]any{}}, memConfig{})
text, isErr := s.call(t, "claude_code_wait", map[string]any{"op": "add", "session": "sess-old", "by": "fix-1629@g14",
text, isErr := s.call(t, "claude_code_wait", map[string]any{"op": "add", "session": "sess-old", "by": "fix-1629@node-b",
"what": "the operator's Approve", "read": map[string]any{"address": "mesh-delivery.recheck"}, "bound_minutes": 10})
if isErr {
t.Fatalf("add: %s", text)
}
text, isErr = s.call(t, "claude_code_wait", map[string]any{"op": "adopt", "session": "sess-new", "by": "fix-1629@g14"})
text, isErr = s.call(t, "claude_code_wait", map[string]any{"op": "adopt", "session": "sess-new", "by": "fix-1629@node-b"})
if isErr || !strings.Contains(text, "sess-old") {
t.Fatalf("adopt: %v %s", isErr, text)
}
@@ -470,7 +470,7 @@ func TestAdoptIsServedThroughTheModulesDispatch(t *testing.T) {
// Review round 2, item 2 (ADR 0490 §1): one session id with two different names bound to it holds both
// names' waits in its one mirrored file, not only the name an op last acted on.
func TestASessionsFileHoldsEveryNameBoundToIt(t *testing.T) {
p, _ := node(t, "g14")
p, _ := node(t, "node-b")
store := newMemWaits()
idX := addApproveWait(t, p, store, "fix-x", "sess-shared", t0)
idY := addApproveWait(t, p, store, "fix-y", "sess-shared", t0.Add(time.Minute))
@@ -491,12 +491,12 @@ func TestASessionsFileHoldsEveryNameBoundToIt(t *testing.T) {
// Review round 2, item 2: adopting one of two names bound to the same session away keeps that session's
// file holding the other name's waits — it is never emptied wholesale.
func TestAdoptingOneNameAwayKeepsTheSessionsOtherNamesWaits(t *testing.T) {
p, _ := node(t, "g14")
p, _ := node(t, "node-b")
store := newMemWaits()
idX := addApproveWait(t, p, store, "fix-x", "sess-shared", t0)
idY := addApproveWait(t, p, store, "fix-y", "sess-shared", t0.Add(time.Minute))
if _, err := AdoptName(p, "fix-y@g14", "sess-y-new", store, t0.Add(2*time.Minute)); err != nil {
if _, err := AdoptName(p, "fix-y@node-b", "sess-y-new", store, t0.Add(2*time.Minute)); err != nil {
t.Fatal(err)
}
@@ -513,9 +513,9 @@ func TestAdoptingOneNameAwayKeepsTheSessionsOtherNamesWaits(t *testing.T) {
// Review round 2, item b: adopt clears the refusal counter of both the earlier id and the new one, so a
// fresh three-strikes count starts under the new binding.
func TestAdoptClearsTheRefusalCounterOfBothIDs(t *testing.T) {
p, _ := node(t, "g14")
p, _ := node(t, "node-b")
store := newMemWaits()
by := "fix-1629@g14"
by := "fix-1629@node-b"
addApproveWait(t, p, store, "fix-1629", "sess-old", t0)
waitsDir := WaitsDirPath(p)
if err := writeRefusalCounters(waitsDir, "sess-old", map[string]int{"w-1": 2}); err != nil {
@@ -538,9 +538,9 @@ func TestAdoptClearsTheRefusalCounterOfBothIDs(t *testing.T) {
// Review round 2, item c: list is read-only — it never binds an unbound name, and never refuses a
// mismatched session; it answers the name's waits and who currently holds it.
func TestListNeverBindsAndAnswersWhoHoldsTheName(t *testing.T) {
p, _ := node(t, "g14")
p, _ := node(t, "node-b")
store := newMemWaits()
by := "fix-1629@g14"
by := "fix-1629@node-b"
// An unbound name: list answers no waits, and binds nothing (a later add from a different session still
// succeeds, as the first caller for an unbound name).