Name no node in dnsmasq's, mesh-mail-reader's and influxdb's fixtures (issue 1838)

This commit is contained in:
jochen
2026-10-22 21:44:22 +02:00
parent a8e62b52fd
commit c09b7c0513
5 changed files with 53 additions and 53 deletions
@@ -134,8 +134,8 @@ func aResolverMachine(t *testing.T, port string, now func() time.Time) *Machine
}
}
write(ConfigFile, "bind-interfaces\nlisten-address=127.0.0.2\nlisten-address=127.0.0.1\nno-resolv\n")
write(NodesFile, "local=/internal/\naddress=/novox.internal/127.0.0.2\nhost-record=novox.internal,127.0.0.2\n"+
"address=/ace.internal/10.77.0.2\nhost-record=ace.internal,10.77.0.2\n")
write(NodesFile, "local=/internal/\naddress=/node-c.internal/127.0.0.2\nhost-record=node-c.internal,127.0.0.2\n"+
"address=/node-a.internal/10.77.0.2\nhost-record=node-a.internal,10.77.0.2\n")
if err := os.MkdirAll(filepath.Join(root, filepath.Dir(StateFile)), 0o755); err != nil {
t.Fatal(err)
}
@@ -147,13 +147,13 @@ func TestTheConfigurationAndTheRosterSayWhereAndWhatToAsk(t *testing.T) {
if strings.Join(listen, " ") != "10.77.0.1 127.0.0.1" {
t.Fatalf("listen addresses %v", listen)
}
names := AnsweredNames("local=/internal/\naddress=/ace.internal/10.77.0.2\nhost-record=ace.internal,10.77.0.2\n" +
"host-record=novox.internal,10.77.0.1\n")
names := AnsweredNames("local=/internal/\naddress=/node-a.internal/10.77.0.2\nhost-record=node-a.internal,10.77.0.2\n" +
"host-record=node-c.internal,10.77.0.1\n")
q, ok := Question(listen, names)
if !ok || q.Name != "novox.internal" || q.Address != "10.77.0.1" {
if !ok || q.Name != "node-c.internal" || q.Address != "10.77.0.1" {
t.Fatalf("asks %+v", q)
}
if q, _ := Question([]string{"127.0.0.1"}, names); q.Name != "ace.internal" {
if q, _ := Question([]string{"127.0.0.1"}, names); q.Name != "node-a.internal" {
t.Fatalf("with no own name in the roster, asks %+v", q)
}
}
@@ -165,7 +165,7 @@ func TestALookFindsAResolverThatStoppedAnsweringOnOneAddress(t *testing.T) {
m := aResolverMachine(t, port, time.Now)
private := startStandIn(t, "127.0.0.2", port, "127.0.0.2")
startStandIn(t, "127.0.0.1", port, "127.0.0.2")
if l := m.Look(t.Context()); !l.Healthy || len(l.Answers) != 4 || l.Asked.Name != "novox.internal" {
if l := m.Look(t.Context()); !l.Healthy || len(l.Answers) != 4 || l.Asked.Name != "node-c.internal" {
t.Fatalf("a resolver answering everywhere: %+v", l)
}
private.stop()
@@ -173,14 +173,14 @@ func TestALookFindsAResolverThatStoppedAnsweringOnOneAddress(t *testing.T) {
if l.Healthy || len(l.Wrong) != 2 {
t.Fatalf("a resolver that stopped answering on its private address: %+v", l)
}
if why := l.Why(); !strings.Contains(why, "127.0.0.2:"+port+" over udp did not answer novox.internal: refused") ||
if why := l.Why(); !strings.Contains(why, "127.0.0.2:"+port+" over udp did not answer node-c.internal: refused") ||
!strings.Contains(why, "over tcp") || strings.Contains(why, "127.0.0.1:") {
t.Fatalf("said as %q", why)
}
}
func TestAWrongAnswerIsNotAnAnswer(t *testing.T) {
q := Named{Name: "novox.internal", Address: "10.77.0.1"}
q := Named{Name: "node-c.internal", Address: "10.77.0.1"}
for _, c := range []struct {
a Answer
want string
@@ -198,7 +198,7 @@ func TestAWrongAnswerIsNotAnAnswer(t *testing.T) {
func failing(at time.Time) Look {
return Look{At: at, Answers: []Answer{{Server: "10.77.0.1:53", Over: "udp", Failed: "refused: nothing listens there"}},
Wrong: []string{"10.77.0.1:53 over udp did not answer novox.internal: refused: nothing listens there"}}
Wrong: []string{"10.77.0.1:53 over udp did not answer node-c.internal: refused: nothing listens there"}}
}
func healthy(at time.Time) Look { return Look{At: at, Healthy: true} }
@@ -323,7 +323,7 @@ func TestTheWatchRestartsAResolverThatStoppedAnsweringAndHealthSaysWhetherItHelp
look()
}
h := m.Health(t.Context())
if h.Healthy || !strings.Contains(h.Why, "did not answer novox.internal: refused") ||
if h.Healthy || !strings.Contains(h.Why, "did not answer node-c.internal: refused") ||
!strings.Contains(h.Why, "which did not help") {
t.Fatalf("a restart that did not help: %q", h.Why)
}
@@ -482,10 +482,10 @@ func TestResolveAsksOnlyAListenAddress(t *testing.T) {
m := aResolverMachine(t, port, time.Now)
startStandIn(t, "127.0.0.2", port, "127.0.0.2")
startStandIn(t, "127.0.0.1", port, "127.0.0.2")
if _, err := m.Resolve(t.Context(), "novox.internal", "8.8.8.8", ""); err == nil || !strings.Contains(err.Error(), "not one the resolver listens on") {
if _, err := m.Resolve(t.Context(), "node-c.internal", "8.8.8.8", ""); err == nil || !strings.Contains(err.Error(), "not one the resolver listens on") {
t.Fatalf("another address was asked: %v", err)
}
got, err := m.Resolve(t.Context(), "novox.internal", "127.0.0.2", "tcp")
got, err := m.Resolve(t.Context(), "node-c.internal", "127.0.0.2", "tcp")
if err != nil {
t.Fatal(err)
}
@@ -493,10 +493,10 @@ func TestResolveAsksOnlyAListenAddress(t *testing.T) {
if len(answers) != 1 || answers[0].Over != "tcp" || answers[0].Code != "NOERROR" || answers[0].Addresses[0] != "127.0.0.2" {
t.Fatalf("%+v", answers)
}
if got, _ := m.Resolve(t.Context(), "novox.internal", "", ""); len(got.(map[string]any)["answers"].([]Answer)) != 4 {
if got, _ := m.Resolve(t.Context(), "node-c.internal", "", ""); len(got.(map[string]any)["answers"].([]Answer)) != 4 {
t.Fatalf("every listen address over both: %+v", got)
}
if _, err := m.Resolve(t.Context(), "novox.internal", "", "quic"); err == nil {
if _, err := m.Resolve(t.Context(), "node-c.internal", "", "quic"); err == nil {
t.Fatal("over quic was accepted")
}
if _, err := m.Resolve(t.Context(), "", "", ""); err == nil {
@@ -540,12 +540,12 @@ func TestTheAnnouncerSaysWhatChangedAndRemembersOnlyWhatWasSaid(t *testing.T) {
}
// The roster changes: one machine added, one gone. An unreadable roster says nothing.
nodes := filepath.Join(m.Root, NodesFile)
if err := os.WriteFile(nodes, []byte("host-record=novox.internal,127.0.0.2\nhost-record=shanks.internal,10.77.0.4\n"), 0o644); err != nil {
if err := os.WriteFile(nodes, []byte("host-record=node-c.internal,127.0.0.2\nhost-record=node-d.internal,10.77.0.4\n"), 0o644); err != nil {
t.Fatal(err)
}
Tell(m, &a, emit)
if got := strings.Join(said, "|"); !strings.Contains(got, "name.added shanks.internal") ||
!strings.Contains(got, "name.removed ace.internal") || len(said) != 2 {
if got := strings.Join(said, "|"); !strings.Contains(got, "name.added node-d.internal") ||
!strings.Contains(got, "name.removed node-a.internal") || len(said) != 2 {
t.Fatalf("the roster's change: %v", said)
}
said = nil
+17 -17
View File
@@ -108,13 +108,13 @@ const grants = new ApiGrants(new InfluxDBClient(`http://127.0.0.1:${port}`, ADMI
const PW = "mesh-minted-password-of-forty-characters";
/** Grafana on ace, as the mesh hands it to the provisioner. */
/** Grafana on node-a, as the mesh hands it to the provisioner. */
function grafana(password = PW, values: Record<string, unknown> = { access: "read" }) {
return { as: "mesh_ace_grafana", password, consumer: "ace", values };
return { as: "mesh_node-a_grafana", password, consumer: "node-a", values };
}
/** Node-RED on ace: writes one bucket. */
/** Node-RED on node-a: writes one bucket. */
function nodered(password = PW, values: Record<string, unknown> = { access: "write", buckets: ["zurag"] }) {
return { as: "mesh_ace_nodered", password, consumer: "ace", values };
return { as: "mesh_node-a_nodered", password, consumer: "node-a", values };
}
function only(token: string): Rec {
@@ -144,7 +144,7 @@ test("what a contribution may ask for, and what is refused", () => {
test("a reader is given one authorization, reading every bucket of the org, under the mesh's password", async () => {
assert.equal(await grants.ensure(grafana()), "created");
const a = only("mesh_ace_grafana");
const a = only("mesh_node-a_grafana");
assert.equal(a.orgID, "org1");
assert.equal(a.status, "active");
assert.ok(a.description.startsWith(MARK));
@@ -158,8 +158,8 @@ test("a writer is allowed its own buckets only, and a missing one is made — ne
const made = buckets.find((b) => b.name === "printer");
assert.ok(made, "the missing bucket was created");
assert.deepEqual(made!.retentionRules, [], "kept for ever: retention is the operator's choice");
assert.deepEqual(perms(only("mesh_ace_nodered")), [`write:buckets:${made!.id}`, "write:buckets:zb"]);
assert.equal(await grants.remove("mesh_ace_nodered"), "removed");
assert.deepEqual(perms(only("mesh_node-a_nodered")), [`write:buckets:${made!.id}`, "write:buckets:zb"]);
assert.equal(await grants.remove("mesh_node-a_nodered"), "removed");
assert.equal(buckets.length, 2, "withdrawing the consumer leaves every bucket and its data");
});
@@ -168,30 +168,30 @@ test("applying the same grant again writes nothing", async () => {
calls = [];
assert.equal(await grants.ensure(grafana()), "unchanged");
assert.ok(calls.every((c) => c.startsWith("GET")), `only reads: ${calls.join(", ")}`);
only("mesh_ace_grafana");
only("mesh_node-a_grafana");
});
test("a rotated password is set in place; a changed access remakes only the mesh's own", async () => {
await grants.ensure(nodered());
const id = only("mesh_ace_nodered").id;
const id = only("mesh_node-a_nodered").id;
assert.equal(await grants.holds(nodered("rotated-password-0123456789")), false);
assert.equal(await grants.ensure(nodered("rotated-password-0123456789")), "updated");
assert.equal(only("mesh_ace_nodered").id, id, "updated, not replaced");
assert.equal(only("mesh_node-a_nodered").id, id, "updated, not replaced");
assert.equal(await grants.holds(nodered("rotated-password-0123456789")), true);
await grants.ensure(nodered(PW, { access: "read-write", buckets: ["zurag"] }));
assert.deepEqual(perms(only("mesh_ace_nodered")), ["read:buckets:zb", "write:buckets:zb"]);
assert.deepEqual(perms(only("mesh_node-a_nodered")), ["read:buckets:zb", "write:buckets:zb"]);
assert.equal(await grants.holds(nodered(PW, { access: "read-write", buckets: ["zurag"] })), true);
});
test("an authorization disabled, re-passworded or deleted behind the mesh's back is not held, and is made whole", async () => {
await grants.ensure(grafana());
only("mesh_ace_grafana").status = "inactive";
only("mesh_node-a_grafana").status = "inactive";
assert.equal(await grants.holds(grafana()), false);
assert.equal(await grants.ensure(grafana()), "updated");
assert.equal(await grants.holds(grafana()), true);
only("mesh_ace_grafana").password = "somebody-else-set-this";
only("mesh_node-a_grafana").password = "somebody-else-set-this";
assert.equal(await grants.holds(grafana()), false);
await grants.ensure(grafana());
assert.equal(await grants.holds(grafana()), true);
@@ -211,14 +211,14 @@ test("holds only reads, and a bucket gone missing is not held rather than made",
});
test("an authorization of the same name the mesh did not make is refused, and left exactly as it was", async () => {
auths = [{ id: "theirs", token: "mesh_ace_grafana", orgID: "org1", status: "active", description: "hand-made",
auths = [{ id: "theirs", token: "mesh_node-a_grafana", orgID: "org1", status: "active", description: "hand-made",
permissions: [{ action: "write", resource: { type: "buckets", orgID: "org1" } }], password: "their-password" }];
const before = JSON.stringify(auths);
await assert.rejects(grants.ensure(grafana()), /did not make/);
assert.equal(JSON.stringify(auths), before);
assert.ok(calls.every((c) => c.startsWith("GET")), `only reads: ${calls.join(", ")}`);
assert.equal(await grants.holds(grafana()), false);
assert.equal(await grants.remove("mesh_ace_grafana"), "not ours");
assert.equal(await grants.remove("mesh_node-a_grafana"), "not ours");
assert.equal(auths.length, 1, "never deleted");
});
@@ -240,7 +240,7 @@ test("an org the instance does not have, or a non-mesh name, makes nothing", asy
test("a withdrawn consumer's authorization is removed, and an absent one is not an error", async () => {
await grants.ensure(grafana());
assert.equal(await grants.remove("mesh_ace_grafana"), "removed");
assert.equal(await grants.remove("mesh_node-a_grafana"), "removed");
assert.equal(auths.length, 0);
assert.equal(await grants.remove("mesh_ace_grafana"), "absent");
assert.equal(await grants.remove("mesh_node-a_grafana"), "absent");
});
@@ -151,22 +151,22 @@ func TestNewJudgeCollectsAPendingAnswer(t *testing.T) {
var sawTicket string
collected := 0
ask := func(key string, body map[string]any) (json.RawMessage, error) {
if key == "claude-code.claude_code_judge@novox" {
if key == "claude-code.claude_code_judge@node-c" {
sawWithin = body["within"].(int)
return json.Marshal(map[string]any{"pending": true, "ticket": "tk-1", "node": "novox"})
return json.Marshal(map[string]any{"pending": true, "ticket": "tk-1", "node": "node-c"})
}
if key == "claude-code.claude_code_judged@novox" {
if key == "claude-code.claude_code_judged@node-c" {
sawTicket = body["ticket"].(string)
collected++
if collected < 2 {
return json.Marshal(map[string]any{"pending": true, "ticket": "tk-1", "node": "novox"})
return json.Marshal(map[string]any{"pending": true, "ticket": "tk-1", "node": "node-c"})
}
return json.Marshal(map[string]any{"answer": json.RawMessage(`{"mails":[{"id":1,"mark":"neither"}]}`)})
}
return nil, fmt.Errorf("unexpected ask %s", key)
}
now := time.Now()
judge := NewJudge(ask, "novox", func() time.Time { return now })
judge := NewJudge(ask, "node-c", func() time.Time { return now })
raw, outcome, why := judge("the question", AnswerSchema())
if outcome != OutcomeOK {
t.Fatalf("outcome = %q, why = %q, want OutcomeOK", outcome, why)
@@ -198,9 +198,9 @@ func TestNewJudgeTimesOut(t *testing.T) {
} else {
now = now.Add(JudgeWithin + 2*time.Minute) // collecting again takes "too long"
}
return json.Marshal(map[string]any{"pending": true, "ticket": "tk-1", "node": "novox"})
return json.Marshal(map[string]any{"pending": true, "ticket": "tk-1", "node": "node-c"})
}
judge := NewJudge(ask, "novox", func() time.Time { return now })
judge := NewJudge(ask, "node-c", func() time.Time { return now })
_, outcome, why := judge("the question", AnswerSchema())
if outcome != OutcomeTimedOut {
t.Fatalf("outcome = %q, why = %q, want OutcomeTimedOut", outcome, why)
@@ -212,7 +212,7 @@ func TestNewJudgeRefused(t *testing.T) {
ask := func(string, map[string]any) (json.RawMessage, error) {
return nil, fmt.Errorf("holds no licence")
}
judge := NewJudge(ask, "novox", time.Now)
judge := NewJudge(ask, "node-c", time.Now)
_, outcome, why := judge("the question", AnswerSchema())
if outcome != OutcomeRefused || !strings.Contains(why, "licence") {
t.Fatalf("outcome = %q, why = %q, want OutcomeRefused naming the licence", outcome, why)
@@ -228,14 +228,14 @@ func TestNewJudgeSortsItsCauseByTheRealJudgesWords(t *testing.T) {
err string
want HealthOutcome
}{
{"timed out", "the agent on novox did not answer within 40s; the judgement was ended", OutcomeTimedOut},
{"out of shape", `the agent on novox answered, and not in the shape asked: "mails" is not an array`, OutcomeOutOfShape},
{"refused", "the agent on novox could not judge: holds no licence", OutcomeRefused},
{"timed out", "the agent on node-c did not answer within 40s; the judgement was ended", OutcomeTimedOut},
{"out of shape", `the agent on node-c answered, and not in the shape asked: "mails" is not an array`, OutcomeOutOfShape},
{"refused", "the agent on node-c could not judge: holds no licence", OutcomeRefused},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
ask := func(string, map[string]any) (json.RawMessage, error) { return nil, fmt.Errorf("%s", c.err) }
judge := NewJudge(ask, "novox", time.Now)
judge := NewJudge(ask, "node-c", time.Now)
_, outcome, why := judge("the question", AnswerSchema())
if outcome != c.want {
t.Fatalf("outcome = %q, want %q", outcome, c.want)
@@ -355,7 +355,7 @@ func TestRunOneBatchNeverAsksToSendMoveFlagOrDelete(t *testing.T) {
return json.Marshal(map[string]any{"text": "hi"})
}
readText := NewReadText(recordingAsk)
judge := NewJudge(recordingAsk, "novox", time.Now)
judge := NewJudge(recordingAsk, "node-c", time.Now)
if err := RunOneBatch(ctx, store, readText, judge, waiting, time.Now, noopLogf); err != nil {
t.Fatal(err)
}
@@ -1,6 +1,6 @@
package main
// Callers is the trusted setting `callers` (lead-m2@g14's decision at this milestone's prepare step, said to
// Callers is the trusted setting `callers` (lead-m2@node-b's decision at this milestone's prepare step, said to
// the operator; novox/hq ADR 0459 §2: "each answers only a caller the operator grants it"): who may call one
// of mail-reading's six verbs. An entry is a bare module name (granted wherever it runs, on any machine),
// exactly <machine>.<module>, or exactly person.<name> — the same shapes mesh-mail's own trusted setting
@@ -5,15 +5,15 @@ package main
import "testing"
func TestCallersAllowsEachShape(t *testing.T) {
c := Callers{"mesh-mail-web", "novox.mesh-knowledge", "person.jochen"}
c := Callers{"mesh-mail-web", "node-c.mesh-knowledge", "person.jochen"}
cases := []struct {
caller string
want bool
}{
{"mesh-mail-web", true}, // a bare module name
{"shanks.mesh-mail-web", true}, // ... on any machine
{"novox.mesh-knowledge", true}, // exactly <machine>.<module>
{"shanks.mesh-knowledge", false}, // ... but not on another machine
{"node-d.mesh-mail-web", true}, // ... on any machine
{"node-c.mesh-knowledge", true}, // exactly <machine>.<module>
{"node-d.mesh-knowledge", false}, // ... but not on another machine
{"person.jochen", true}, // exactly person.<name>
{"person.someoneelse", false}, // ... but not another person
{"mesh-mail-notifier", false}, // not named at all