diff --git a/modules/betterbird/README.md b/modules/betterbird/README.md index bbc081d1..d1253dc7 100644 --- a/modules/betterbird/README.md +++ b/modules/betterbird/README.md @@ -1,6 +1,6 @@ # betterbird -Betterbird, the mail and calendar client of g14 and shanks (a Thunderbird fork, with the operator's +Betterbird, the mail and calendar client of node-b and node-d (a Thunderbird fork, with the operator's Exchange calendar through the Owl add-on), as a module (novox/hq issue 345, issue 586, ADR 0208, ADR 0316, ADR 0317, ADR 0329, to-be 56). It requires `x11-display`, so it is assigned only where a display server is held on the same machine. It installs Betterbird from a pinned upstream release, ships Owl with it through @@ -58,7 +58,7 @@ reading the new range there (ADR 0316 §8). **The policy** turns Betterbird's own updater off (`DisableAppUpdate`, which Betterbird's update check honours) and installs Owl into every profile from `file:///opt/betterbird/owl/owl.xpi` as `force_installed`, with `updates_disabled`. A profile that already runs another Owl keeps it: the policy -installs the pinned one where Owl is missing. A newer Owl in the profile (g14 runs 1.5.7) is said as a +installs the pinned one where Owl is missing. A newer Owl in the profile (node-b runs 1.5.7) is said as a fact by `betterbird_status`; Owl installed by policy cannot be removed in Betterbird, so the remedy is moving the pin. An older one is a finding of `betterbird_check`, with the same remedy. @@ -323,10 +323,10 @@ question. ## What changes when it is pushed -The module is assigned to shanks today (look-only, from issue 345's first part); g14 runs Betterbird +The module is assigned to node-d today (look-only, from issue 345's first part); node-b runs Betterbird outside the mesh. With this version, a push to a node (each waits for the operator: `upgrade: record`): -| | on a node with the community package (g14, shanks) | +| | on a node with the community package (node-b, node-d) | |---|---| | the package | `betterbird-bin` removed (quit Betterbird first) | | `/opt/betterbird` | the mesh's 153.4.0esr-bb10, in the same place, so the same profile opens | diff --git a/modules/betterbird/cmd/betterbird-tools/config_test.go b/modules/betterbird/cmd/betterbird-tools/config_test.go index c92a7288..c8efe5c1 100644 --- a/modules/betterbird/cmd/betterbird-tools/config_test.go +++ b/modules/betterbird/cmd/betterbird-tools/config_test.go @@ -412,13 +412,13 @@ func TestKeepProposesANodeLayerThroughTheController(t *testing.T) { f.write(profileDir+"/prefs.js", `user_pref("mail.uifontsize", 16);`+"\n"+`user_pref("mail.spam.manualMark", true);`+"\n") oldNode, oldAsk := nodeName, asker t.Cleanup(func() { nodeName, asker = oldNode, oldAsk }) - nodeName = func() string { return "shanks" } + nodeName = func() string { return "node-d" } var asked []map[string]any asker = func(v string, a any) (json.RawMessage, error) { args := a.(map[string]any) asked = append(asked, args) if args["propose"] == nil { - return json.RawMessage(`{"ok": true, "output": "{\"prefs\": {\"calendar.alarms.playsound\": false}, \"identities\": {\"mail\": {\"account\": \"mail\"}}}\n set at the terminal\nbetterbird on shanks, every value"}`), nil + return json.RawMessage(`{"ok": true, "output": "{\"prefs\": {\"calendar.alarms.playsound\": false}, \"identities\": {\"mail\": {\"account\": \"mail\"}}}\n set at the terminal\nbetterbird on node-d, every value"}`), nil } return json.RawMessage(`{"ok": true, "output": "proposed: p-1"}`), nil } @@ -427,7 +427,7 @@ func TestKeepProposesANodeLayerThroughTheController(t *testing.T) { if err != nil { t.Fatal(err) } - if len(asked) != 2 || asked[0]["node"] != "shanks" || asked[1]["node"] != "shanks" || asked[1]["propose"] != "true" || + if len(asked) != 2 || asked[0]["node"] != "node-d" || asked[1]["node"] != "node-d" || asked[1]["propose"] != "true" || asked[1]["module"] != "betterbird" { t.Fatalf("the node's layer read, then proposed: %v", asked) } diff --git a/modules/betterbird/cmd/betterbird-tools/derive_test.go b/modules/betterbird/cmd/betterbird-tools/derive_test.go index 549e98d9..ea1651f5 100644 --- a/modules/betterbird/cmd/betterbird-tools/derive_test.go +++ b/modules/betterbird/cmd/betterbird-tools/derive_test.go @@ -12,11 +12,11 @@ import ( // ADR 0329 §10, §12: derived from every desktop node's profile, laid out per layer, and proposed one layer at a // time, carrying the layer as it stands. -// derivingMesh is two desktop nodes, g14 (this one, f) and shanks (f2), and a server that holds no Betterbird, as the +// derivingMesh is two desktop nodes, node-b (this one, f) and node-d (f2), and a server that holds no Betterbird, as the // controller answers them; layers are the controller's layers, open the proposals that wait. type derivingMesh struct { t *testing.T - shanks *fake + nodeD *fake layers map[string]map[string]any open bool proposed []map[string]any @@ -35,17 +35,17 @@ func (d *derivingMesh) ask(key string, body any) (json.RawMessage, error) { } switch { case key == "seat:mesh-controller.nodes": - return answer(true, `[{"name": "g14"}, {"name": "novox"}, {"name": "shanks"}]`) + return answer(true, `[{"name": "node-b"}, {"name": "node-c"}, {"name": "node-d"}]`) case key == "seat:mesh-controller.node": - if args["node"] == "novox" { - return answer(true, "novox\n assigned: postgres, nats\n") + if args["node"] == "node-c" { + return answer(true, "node-c\n assigned: postgres, nats\n") } return answer(true, fmt.Sprintf("%s\n last heard from here\n assigned: adwaita, betterbird, dunst\n", args["node"])) - case key == "betterbird.betterbird_derive@shanks": + case key == "betterbird.betterbird_derive@node-d": if d.asleep { return nil, errors.New("nats: no responders available for request") } - here, err := d.shanks.deriveHere(stringsOf(args["kinds"])) + here, err := d.nodeD.deriveHere(stringsOf(args["kinds"])) if err != nil { d.t.Fatal(err) } @@ -53,10 +53,10 @@ func (d *derivingMesh) ask(key string, body any) (json.RawMessage, error) { return json.Marshal(map[string]any{"content": []any{map[string]any{"type": "text", "text": string(text)}}}) case key == "seat:mesh-controller.settings" && args["proposals"] == "true": if d.open { - return answer(true, "sab12 set betterbird on the whole mesh (accounts, identities)\n by g14.node-tools at 2026-10-13 01:00; fingerprint ab12\n waiting for the operator until 2026-10-14 01:00\n"+ - "scd34 set mounts on shanks (sources)\n by g14.node-tools at 2026-10-11 00:05; fingerprint dd0b\n chosen: done\n") + return answer(true, "sab12 set betterbird on the whole mesh (accounts, identities)\n by node-b.node-tools at 2026-10-13 01:00; fingerprint ab12\n waiting for the operator until 2026-10-14 01:00\n"+ + "scd34 set mounts on node-d (sources)\n by node-b.node-tools at 2026-10-11 00:05; fingerprint dd0b\n chosen: done\n") } - return answer(true, "scd34 set mounts on shanks (sources)\n by g14.node-tools; fingerprint dd0b\n chosen: done\n") + return answer(true, "scd34 set mounts on node-d (sources)\n by node-b.node-tools; fingerprint dd0b\n chosen: done\n") case key == "seat:mesh-controller.settings" && args["propose"] == "true": d.proposed = append(d.proposed, args) return answer(true, "proposal sab12: Set the settings of betterbird?\n fingerprint ab12") @@ -107,23 +107,23 @@ func TestDeriveLaysEachEntryOutPerLayerAndProposesOneLayerAtATime(t *testing.T) f2 := newFake(t) f2.profileWithAccounts() f2.profileWithConfiguration() - // shanks holds the same accounts and calendars, another signature, another tag colour and a calendar of its own. + // node-d holds the same accounts and calendars, another signature, another tag colour and a calendar of its own. f2.write(profileDir+"/prefs.js", strings.NewReplacer(`"Regards"`, `"Cheers"`, `"#FF0000"`, `"#CC0000"`).Replace(f2.read(profileDir+"/prefs.js"))+ `user_pref("calendar.registry.s1.type", "caldav");`+"\n"+ `user_pref("calendar.registry.s1.uri", "https://dav.example.org/cal/standup/");`+"\n"+ `user_pref("calendar.registry.s1.name", "Standup");`+"\n") f2.declareAccounts(adoptable) - mesh := &derivingMesh{t: t, shanks: f2, layers: map[string]map[string]any{"": {"accounts": map[string]any{"mail": "as approved"}}}} + mesh := &derivingMesh{t: t, nodeD: f2, layers: map[string]map[string]any{"": {"accounts": map[string]any{"mail": "as approved"}}}} oldNode, oldAsk := nodeName, asker t.Cleanup(func() { nodeName, asker = oldNode, oldAsk }) - nodeName = func() string { return "g14" } + nodeName = func() string { return "node-b" } asker = mesh.ask a, err := f.Derive(nil, false, false, nil, "") if err != nil { t.Fatal(err) } - if !reflect.DeepEqual(a.From, []string{"g14", "shanks"}) || len(a.NotHeard) != 0 { + if !reflect.DeepEqual(a.From, []string{"node-b", "node-d"}) || len(a.NotHeard) != 0 { t.Fatalf("derived from the nodes holding Betterbird: %v %v", a.From, a.NotHeard) } whole := mustJSON(a) @@ -134,9 +134,9 @@ func TestDeriveLaysEachEntryOutPerLayerAndProposesOneLayerAtATime(t *testing.T) } cals, _ := mw[kindCalendars].(map[string]any) if cals["home"] == nil || cals["standup"] != nil { - t.Errorf("a calendar both hold is mesh-wide, one only shanks holds is not: %v", cals) + t.Errorf("a calendar both hold is mesh-wide, one only node-d holds is not: %v", cals) } - if sh := mustJSON(a.Layers["shanks"]); !strings.Contains(sh, "https://dav.example.org/cal/standup/") { + if sh := mustJSON(a.Layers["node-d"]); !strings.Contains(sh, "https://dav.example.org/cal/standup/") { t.Errorf("an entry on one node only goes into that node's layer: %s", sh) } tags, _ := mw[kindTags].(map[string]any) @@ -147,8 +147,8 @@ func TestDeriveLaysEachEntryOutPerLayerAndProposesOneLayerAtATime(t *testing.T) for _, d := range a.Differences { diffs[d.Kind+"/"+d.Entry+"/"+d.Field] = d } - if d := diffs["identities/mail/signature"]; d.Node != "shanks" || mustJSON(d.There) != `["Cheers"]` { - t.Errorf("the answer lists the field shanks holds differently: %+v", a.Differences) + if d := diffs["identities/mail/signature"]; d.Node != "node-d" || mustJSON(d.There) != `["Cheers"]` { + t.Errorf("the answer lists the field node-d holds differently: %+v", a.Differences) } if d := diffs["tags/$label1/color"]; d.There != "#CC0000" || d.MeshWide != "#FF0000" { t.Errorf("%+v", a.Differences) @@ -195,22 +195,22 @@ func TestDeriveLaysEachEntryOutPerLayerAndProposesOneLayerAtATime(t *testing.T) if _, err := f.Derive(nil, false, true, nil, ""); err == nil || !strings.Contains(err.Error(), "sab12 (the whole mesh)") || len(mesh.proposed) != 1 { t.Fatalf("one open proposal of the module's at a time: %v", err) } - // Approved: the mesh-wide layer holds it, so the next layer is shanks's own. + // Approved: the mesh-wide layer holds it, so the next layer is node-d's own. mesh.open = false mesh.layers[""] = values a, err = f.Derive(nil, false, true, prefs, "") if err != nil { t.Fatal(err) } - if len(mesh.proposed) != 2 || mesh.proposed[1]["node"] != "shanks" || a.Next != "shanks" || + if len(mesh.proposed) != 2 || mesh.proposed[1]["node"] != "node-d" || a.Next != "node-d" || !strings.Contains(mesh.proposed[1]["values"].(string), "standup") { t.Fatalf("then the node's own layer: %v", mesh.proposed) } - var shanks map[string]any - if err := json.Unmarshal([]byte(mesh.proposed[1]["values"].(string)), &shanks); err != nil { + var nodeD map[string]any + if err := json.Unmarshal([]byte(mesh.proposed[1]["values"].(string)), &nodeD); err != nil { t.Fatal(err) } - mesh.layers["shanks"] = shanks + mesh.layers["node-d"] = nodeD if a, err = f.Derive(nil, false, true, prefs, ""); err != nil || len(mesh.proposed) != 2 || !strings.HasPrefix(a.Next, "none") { t.Fatalf("nothing left to propose: %v %s", err, a.Next) } @@ -240,10 +240,10 @@ func TestALayerThatCannotBeReadRefusesTheProposal(t *testing.T) { f2 := newFake(t) f2.profileWithAccounts() f2.declareAccounts(adoptable) - mesh := &derivingMesh{t: t, shanks: f2, garbled: true} + mesh := &derivingMesh{t: t, nodeD: f2, garbled: true} oldNode, oldAsk := nodeName, asker t.Cleanup(func() { nodeName, asker = oldNode, oldAsk }) - nodeName = func() string { return "g14" } + nodeName = func() string { return "node-b" } asker = mesh.ask if _, err := f.Derive(nil, false, true, nil, ""); err == nil || !strings.Contains(err.Error(), "cannot be read as the layer") || len(mesh.proposed) != 0 { @@ -257,7 +257,7 @@ func TestALayerThatCannotBeReadRefusesTheProposal(t *testing.T) { t.Fatal("an unreadable layer is an error, never an empty layer") } mesh.garbled = false - if got, err := readLayer("shanks"); err != nil || len(got) != 0 { + if got, err := readLayer("node-d"); err != nil || len(got) != 0 { t.Fatalf("no layer, in the controller's words, is an empty layer: %v %v", got, err) } }