Merge branch 'feat/modules-name-no-node' into bulk-c
This commit is contained in:
@@ -159,11 +159,11 @@ func aMesh(t *testing.T) (*Config, *memStore) {
|
||||
"managed":{"kind":"","default":{},"why":"","type":"object","description":"managed settings","trusted":true,"predefined":"agent-managed-settings"}}}`)
|
||||
m.mirror(t, DeclaredConfigRead, "old", `{"module":"old","settings":{"workers":{"kind":"preference","default":4,"why":"two per core"}}}`)
|
||||
m.mirror(t, PredefinedRead, PredefinedMirrorKey, theList)
|
||||
for _, node := range []string{"g14", "novox"} {
|
||||
for _, node := range []string{"node-b", "node-c"} {
|
||||
m.assign(t, node, "dunst")
|
||||
}
|
||||
m.assign(t, "g14", "claude-code")
|
||||
m.assign(t, "g14", "old")
|
||||
m.assign(t, "node-b", "claude-code")
|
||||
m.assign(t, "node-b", "old")
|
||||
clock := time.Date(2026, 10, 14, 19, 0, 0, 0, time.UTC)
|
||||
c := &Config{Store: m, Now: func() time.Time { clock = clock.Add(time.Second); return clock }, Logf: t.Logf}
|
||||
if _, err := c.Reconcile(); err != nil {
|
||||
@@ -179,7 +179,7 @@ func set(t *testing.T, c *Config, call Call) *SetAnswer {
|
||||
call.Action = "set"
|
||||
}
|
||||
if call.Caller == "" {
|
||||
call.Caller = "g14.node-tools"
|
||||
call.Caller = "node-b.node-tools"
|
||||
}
|
||||
if call.Why == "" {
|
||||
call.Why = "the operator asked for it"
|
||||
@@ -204,42 +204,42 @@ func expect(t *testing.T, m *memStore, node string, value, source string) {
|
||||
// cleared falls back to the one below.
|
||||
func TestEachOfTheFourSourcesResolvesInTurnAndEachClearedFallsBack(t *testing.T) {
|
||||
c, m := aMesh(t)
|
||||
expect(t, m, "g14", "10", SourceDefault)
|
||||
expect(t, m, "novox", "10", SourceDefault)
|
||||
expect(t, m, "node-b", "10", SourceDefault)
|
||||
expect(t, m, "node-c", "10", SourceDefault)
|
||||
|
||||
a := set(t, c, Call{Key: "font-size", Value: 12.0})
|
||||
expect(t, m, "g14", "12", SourceMesh)
|
||||
expect(t, m, "novox", "12", SourceMesh)
|
||||
expect(t, m, "node-b", "12", SourceMesh)
|
||||
expect(t, m, "node-c", "12", SourceMesh)
|
||||
if len(a.Moved) != 2 || a.Level.Kind != LevelMesh {
|
||||
t.Errorf("a mesh value moved %+v", a.Moved)
|
||||
}
|
||||
|
||||
a = set(t, c, Call{Key: "font-size", Value: 14.0, Node: "g14"})
|
||||
expect(t, m, "g14", "14", SourceNode)
|
||||
expect(t, m, "novox", "12", SourceMesh)
|
||||
if len(a.Moved) != 1 || a.Moved[0].Node != "g14" || string(a.Moved[0].From) != "12" || string(a.Moved[0].To) != "14" {
|
||||
t.Errorf("g14's node value moved %+v", a.Moved)
|
||||
a = set(t, c, Call{Key: "font-size", Value: 14.0, Node: "node-b"})
|
||||
expect(t, m, "node-b", "14", SourceNode)
|
||||
expect(t, m, "node-c", "12", SourceMesh)
|
||||
if len(a.Moved) != 1 || a.Moved[0].Node != "node-b" || string(a.Moved[0].From) != "12" || string(a.Moved[0].To) != "14" {
|
||||
t.Errorf("node-b's node value moved %+v", a.Moved)
|
||||
}
|
||||
|
||||
a = set(t, c, Call{Key: "font-size", Value: 16.0, Node: "g14", Module: "dunst"})
|
||||
expect(t, m, "g14", "16", SourceModule)
|
||||
expect(t, m, "novox", "12", SourceMesh)
|
||||
a = set(t, c, Call{Key: "font-size", Value: 16.0, Node: "node-b", Module: "dunst"})
|
||||
expect(t, m, "node-b", "16", SourceModule)
|
||||
expect(t, m, "node-c", "12", SourceMesh)
|
||||
if len(a.Moved) != 1 || a.Moved[0].Source != SourceModule {
|
||||
t.Errorf("dunst's own value moved %+v", a.Moved)
|
||||
}
|
||||
|
||||
// A value set below the module's own moves nothing it overrides.
|
||||
if a = set(t, c, Call{Key: "font-size", Value: 18.0, Node: "g14"}); len(a.Moved) != 0 {
|
||||
if a = set(t, c, Call{Key: "font-size", Value: 18.0, Node: "node-b"}); len(a.Moved) != 0 {
|
||||
t.Errorf("a node value under a module's own moved %+v", a.Moved)
|
||||
}
|
||||
|
||||
set(t, c, Call{Action: "clear", Key: "font-size", Node: "g14", Module: "dunst"})
|
||||
expect(t, m, "g14", "18", SourceNode)
|
||||
set(t, c, Call{Action: "clear", Key: "font-size", Node: "g14"})
|
||||
expect(t, m, "g14", "12", SourceMesh)
|
||||
set(t, c, Call{Action: "clear", Key: "font-size", Node: "node-b", Module: "dunst"})
|
||||
expect(t, m, "node-b", "18", SourceNode)
|
||||
set(t, c, Call{Action: "clear", Key: "font-size", Node: "node-b"})
|
||||
expect(t, m, "node-b", "12", SourceMesh)
|
||||
set(t, c, Call{Action: "clear", Key: "font-size"})
|
||||
expect(t, m, "g14", "10", SourceDefault)
|
||||
expect(t, m, "novox", "10", SourceDefault)
|
||||
expect(t, m, "node-b", "10", SourceDefault)
|
||||
expect(t, m, "node-c", "10", SourceDefault)
|
||||
}
|
||||
|
||||
// **A value naming no predefined key ignores mesh and node config** (ADR 0348 §2): it resolves from its default and
|
||||
@@ -247,20 +247,20 @@ func TestEachOfTheFourSourcesResolvesInTurnAndEachClearedFallsBack(t *testing.T)
|
||||
func TestAValueNamingNoPredefinedKeyResolvesFromItsDefaultAndItsOwnValueOnly(t *testing.T) {
|
||||
c, m := aMesh(t)
|
||||
set(t, c, Call{Key: "font-size", Value: 12.0})
|
||||
set(t, c, Call{Key: "font-size", Value: 14.0, Node: "g14"})
|
||||
if r := m.resolved(t, "g14", "dunst", "width"); string(r.Value) != "300" || r.Source != SourceDefault {
|
||||
set(t, c, Call{Key: "font-size", Value: 14.0, Node: "node-b"})
|
||||
if r := m.resolved(t, "node-b", "dunst", "width"); string(r.Value) != "300" || r.Source != SourceDefault {
|
||||
t.Errorf("width is %s from %s, want its default", r.Value, r.Source)
|
||||
}
|
||||
set(t, c, Call{Key: "width", Value: 400.0, Node: "g14", Module: "dunst"})
|
||||
if r := m.resolved(t, "g14", "dunst", "width"); string(r.Value) != "400" || r.Source != SourceModule {
|
||||
set(t, c, Call{Key: "width", Value: 400.0, Node: "node-b", Module: "dunst"})
|
||||
if r := m.resolved(t, "node-b", "dunst", "width"); string(r.Value) != "400" || r.Source != SourceModule {
|
||||
t.Errorf("width is %s from %s, want dunst's own value", r.Value, r.Source)
|
||||
}
|
||||
// A module not yet moved, in the settings form: its default and its own value.
|
||||
if r := m.resolved(t, "g14", "old", "workers"); string(r.Value) != "4" || r.Source != SourceDefault {
|
||||
if r := m.resolved(t, "node-b", "old", "workers"); string(r.Value) != "4" || r.Source != SourceDefault {
|
||||
t.Errorf("old's workers is %s from %s", r.Value, r.Source)
|
||||
}
|
||||
set(t, c, Call{Key: "workers", Value: 8.0, Node: "g14", Module: "old"})
|
||||
if r := m.resolved(t, "g14", "old", "workers"); string(r.Value) != "8" || r.Source != SourceModule {
|
||||
set(t, c, Call{Key: "workers", Value: 8.0, Node: "node-b", Module: "old"})
|
||||
if r := m.resolved(t, "node-b", "old", "workers"); string(r.Value) != "8" || r.Source != SourceModule {
|
||||
t.Errorf("old's workers is %s from %s", r.Value, r.Source)
|
||||
}
|
||||
}
|
||||
@@ -268,15 +268,15 @@ func TestAValueNamingNoPredefinedKeyResolvesFromItsDefaultAndItsOwnValueOnly(t *
|
||||
// **A required value nothing sets is answered unset** (ADR 0348 §4), and once set, it is the value.
|
||||
func TestARequiredValueNothingSetsIsAnsweredUnset(t *testing.T) {
|
||||
c, m := aMesh(t)
|
||||
r := m.resolved(t, "g14", "dunst", "origin")
|
||||
r := m.resolved(t, "node-b", "dunst", "origin")
|
||||
if r.Source != SourceUnset || !r.Required || len(r.Value) != 0 {
|
||||
t.Errorf("an unset required value resolves as %+v", r)
|
||||
}
|
||||
set(t, c, Call{Key: "origin", Value: "home", Node: "g14", Module: "dunst"})
|
||||
if r = m.resolved(t, "g14", "dunst", "origin"); r.Source != SourceModule || string(r.Value) != `"home"` {
|
||||
set(t, c, Call{Key: "origin", Value: "home", Node: "node-b", Module: "dunst"})
|
||||
if r = m.resolved(t, "node-b", "dunst", "origin"); r.Source != SourceModule || string(r.Value) != `"home"` {
|
||||
t.Errorf("a set required value resolves as %+v", r)
|
||||
}
|
||||
if r = m.resolved(t, "novox", "dunst", "origin"); r.Source != SourceUnset {
|
||||
if r = m.resolved(t, "node-c", "dunst", "origin"); r.Source != SourceUnset {
|
||||
t.Errorf("another node's required value resolves as %+v", r)
|
||||
}
|
||||
}
|
||||
@@ -291,30 +291,30 @@ func TestTheVerbRefusesWhatADeclarationDoesNotAllow(t *testing.T) {
|
||||
call Call
|
||||
want string
|
||||
}{
|
||||
{"a key the module does not declare", Call{Key: "colour", Value: "red", Node: "g14", Module: "dunst"},
|
||||
{"a key the module does not declare", Call{Key: "colour", Value: "red", Node: "node-b", Module: "dunst"},
|
||||
"it declares: font-size, origin, position, width"},
|
||||
{"a key not on the predefined list, at the mesh", Call{Key: "width", Value: 400.0},
|
||||
"the predefined keys are: agent-managed-settings, font-size"},
|
||||
{"a key not on the predefined list, at a node", Call{Key: "width", Value: 400.0, Node: "g14"}, "is not one"},
|
||||
{"a key not on the predefined list, at a node", Call{Key: "width", Value: 400.0, Node: "node-b"}, "is not one"},
|
||||
{"a module value without a node", Call{Key: "width", Value: 400.0, Module: "dunst"},
|
||||
"no module value for the whole mesh"},
|
||||
{"a module on a node it is not assigned to", Call{Key: "width", Value: 400.0, Node: "ace", Module: "dunst"},
|
||||
"it is assigned to: g14, novox"},
|
||||
{"a module not registered", Call{Key: "width", Value: 400.0, Node: "g14", Module: "nobody"}, "not a registered module"},
|
||||
{"a module on a node it is not assigned to", Call{Key: "width", Value: 400.0, Node: "node-a", Module: "dunst"},
|
||||
"it is assigned to: node-b, node-c"},
|
||||
{"a module not registered", Call{Key: "width", Value: 400.0, Node: "node-b", Module: "nobody"}, "not a registered module"},
|
||||
{"a value of another type", Call{Key: "font-size", Value: "12"}, `"12" is not a number`},
|
||||
{"a value below its minimum", Call{Key: "font-size", Value: 2.0, Node: "g14"}, "below the minimum 6"},
|
||||
{"a value above its maximum", Call{Key: "font-size", Value: 99.0, Node: "g14", Module: "dunst"}, "above the maximum 72"},
|
||||
{"a value not in its enum", Call{Key: "position", Value: "middle", Node: "g14", Module: "dunst"}, "is not one of"},
|
||||
{"a value not matching its pattern", Call{Key: "origin", Value: "Home 1", Node: "g14", Module: "dunst"},
|
||||
{"a value below its minimum", Call{Key: "font-size", Value: 2.0, Node: "node-b"}, "below the minimum 6"},
|
||||
{"a value above its maximum", Call{Key: "font-size", Value: 99.0, Node: "node-b", Module: "dunst"}, "above the maximum 72"},
|
||||
{"a value not in its enum", Call{Key: "position", Value: "middle", Node: "node-b", Module: "dunst"}, "is not one of"},
|
||||
{"a value not matching its pattern", Call{Key: "origin", Value: "Home 1", Node: "node-b", Module: "dunst"},
|
||||
"does not match the pattern"},
|
||||
{"a trusted value at the mesh, not from the terminal", Call{Key: "agent-managed-settings",
|
||||
Value: map[string]any{"a": 1.0}}, "set only at the controller's terminal"},
|
||||
{"a trusted module value, not from the terminal", Call{Key: "managed", Value: map[string]any{},
|
||||
Node: "g14", Module: "claude-code"}, "set only at the controller's terminal"},
|
||||
Node: "node-b", Module: "claude-code"}, "set only at the controller's terminal"},
|
||||
{"a change without why", Call{Key: "font-size", Value: 12.0, Why: " "}, "say why"},
|
||||
{"an unproven caller", Call{Key: "font-size", Value: 12.0, Caller: Unproven}, "no proven caller"},
|
||||
{"by from anyone but the controller", Call{Key: "font-size", Value: 12.0, By: "jochen"}, "only the controller says"},
|
||||
{"a clear of nothing set", Call{Action: "clear", Key: "font-size", Node: "g14"}, "nothing is set"},
|
||||
{"a clear of nothing set", Call{Action: "clear", Key: "font-size", Node: "node-b"}, "nothing is set"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
call := tc.call
|
||||
@@ -322,7 +322,7 @@ func TestTheVerbRefusesWhatADeclarationDoesNotAllow(t *testing.T) {
|
||||
call.Action = "set"
|
||||
}
|
||||
if call.Caller == "" {
|
||||
call.Caller = "g14.node-tools"
|
||||
call.Caller = "node-b.node-tools"
|
||||
}
|
||||
if call.Why == "" {
|
||||
call.Why = "a test"
|
||||
@@ -349,7 +349,7 @@ func TestATrustedValueFromTheTerminalIsSetWithWhoGaveIt(t *testing.T) {
|
||||
if string(a.Value) != `{"a":1,"b":2}` {
|
||||
t.Errorf("the value is written as %s", a.Value)
|
||||
}
|
||||
r := m.resolved(t, "g14", "claude-code", "managed")
|
||||
r := m.resolved(t, "node-b", "claude-code", "managed")
|
||||
if r.Source != SourceMesh || !r.Trusted || string(r.Value) != `{"a":1,"b":2}` {
|
||||
t.Errorf("claude-code's managed settings resolve as %+v", r)
|
||||
}
|
||||
@@ -366,8 +366,8 @@ func TestATrustedValueFromTheTerminalIsSetWithWhoGaveIt(t *testing.T) {
|
||||
func TestEveryChangeIsKeptWithWhoAndWhyAndRaisesConfigChanged(t *testing.T) {
|
||||
c, m := aMesh(t)
|
||||
m.events = nil
|
||||
set(t, c, Call{Key: "font-size", Value: 12.0, Caller: "novox.node-tools", Why: "bigger on every desk"})
|
||||
set(t, c, Call{Action: "clear", Key: "font-size", Caller: "g14.node-tools", Why: "back to the default"})
|
||||
set(t, c, Call{Key: "font-size", Value: 12.0, Caller: "node-c.node-tools", Why: "bigger on every desk"})
|
||||
set(t, c, Call{Action: "clear", Key: "font-size", Caller: "node-b.node-tools", Why: "back to the default"})
|
||||
|
||||
history, err := c.History(Call{})
|
||||
if err != nil {
|
||||
@@ -379,22 +379,22 @@ func TestEveryChangeIsKeptWithWhoAndWhyAndRaisesConfigChanged(t *testing.T) {
|
||||
changes = append(changes, h)
|
||||
}
|
||||
}
|
||||
if len(changes) != 2 || changes[0].By != "novox.node-tools" || changes[0].Why != "bigger on every desk" ||
|
||||
changes[1].Action != "clear" || changes[1].By != "g14.node-tools" || string(changes[1].Was) != "12" {
|
||||
if len(changes) != 2 || changes[0].By != "node-c.node-tools" || changes[0].Why != "bigger on every desk" ||
|
||||
changes[1].Action != "clear" || changes[1].By != "node-b.node-tools" || string(changes[1].Was) != "12" {
|
||||
t.Fatalf("the history holds %+v", changes)
|
||||
}
|
||||
if len(m.handActs) != 2 || !strings.Contains(m.handActs[0], "config set font-size at mesh config to 12, by novox.node-tools | bigger on every desk") {
|
||||
if len(m.handActs) != 2 || !strings.Contains(m.handActs[0], "config set font-size at mesh config to 12, by node-c.node-tools | bigger on every desk") {
|
||||
t.Errorf("the hand-act log holds %q", m.handActs)
|
||||
}
|
||||
if len(m.events) != 2 || len(m.events[0].Moved) != 2 || m.events[0].Key != "font-size" || m.events[1].Action != "clear" {
|
||||
t.Errorf("config-changed was raised as %+v", m.events)
|
||||
}
|
||||
// Narrowed: g14's dunst sees both, through what they moved; a node that holds no dunst sees neither.
|
||||
if h, _ := c.History(Call{Module: "dunst", Node: "g14"}); len(h) < 2 {
|
||||
t.Errorf("dunst on g14's history holds %d changes", len(h))
|
||||
// Narrowed: node-b's dunst sees both, through what they moved; a node that holds no dunst sees neither.
|
||||
if h, _ := c.History(Call{Module: "dunst", Node: "node-b"}); len(h) < 2 {
|
||||
t.Errorf("dunst on node-b's history holds %d changes", len(h))
|
||||
}
|
||||
if h, _ := c.History(Call{Node: "ace"}); len(h) != 0 {
|
||||
t.Errorf("ace's history holds %+v", h)
|
||||
if h, _ := c.History(Call{Node: "node-a"}); len(h) != 0 {
|
||||
t.Errorf("node-a's history holds %+v", h)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -406,7 +406,7 @@ func TestAChangeTheHandActLogRefusesIsSaid(t *testing.T) {
|
||||
if len(a.Unsaid) != 1 || !strings.Contains(a.Unsaid[0], "hand-act log does not hold it") {
|
||||
t.Errorf("the answer says %q", a.Unsaid)
|
||||
}
|
||||
expect(t, m, "g14", "12", SourceMesh)
|
||||
expect(t, m, "node-b", "12", SourceMesh)
|
||||
}
|
||||
|
||||
// **The newest list of predefined keys is published, and an older one heard later is not** (ADR 0348 §7).
|
||||
@@ -447,7 +447,7 @@ func TestTheNewestListOfPredefinedKeysIsPublished(t *testing.T) {
|
||||
// listing says why until the value is cleared.
|
||||
func TestAListDroppingAKeyThatHoldsAValueIsWithheldUntilItIsCleared(t *testing.T) {
|
||||
c, m := aMesh(t)
|
||||
set(t, c, Call{Key: "font-size", Value: 12.0, Node: "novox"})
|
||||
set(t, c, Call{Key: "font-size", Value: 12.0, Node: "node-c"})
|
||||
m.mirror(t, PredefinedRead, PredefinedMirrorKey, `{"keys":{"agent-managed-settings":{"type":"object",
|
||||
"description":"managed settings","trusted":true}},"commit":"cccccccccc","generation":9}`)
|
||||
if _, err := c.Reconcile(); err != nil {
|
||||
@@ -458,11 +458,11 @@ func TestAListDroppingAKeyThatHoldsAValueIsWithheldUntilItIsCleared(t *testing.T
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, kept := listed.Predefined["font-size"]; !kept ||
|
||||
!strings.Contains(listed.Withheld, "drops font-size (still set as node config on novox)") {
|
||||
!strings.Contains(listed.Withheld, "drops font-size (still set as node config on node-c)") {
|
||||
t.Fatalf("the list was published over a value it drops: withheld %q, published %v", listed.Withheld,
|
||||
joinKeys(listed.Predefined))
|
||||
}
|
||||
set(t, c, Call{Action: "clear", Key: "font-size", Node: "novox"})
|
||||
set(t, c, Call{Action: "clear", Key: "font-size", Node: "node-c"})
|
||||
if _, err := c.Reconcile(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -481,19 +481,19 @@ func TestAnAssignmentMovesTheResolvedValuesAndAQuietPassWritesNothing(t *testing
|
||||
t.Fatalf("a pass over unchanged sources moved %+v (%v)", r.Moved, err)
|
||||
}
|
||||
m.events = nil
|
||||
m.assign(t, "ace", "dunst")
|
||||
m.assign(t, "node-a", "dunst")
|
||||
if r, _ = c.Reconcile(); len(r.Moved) != 4 {
|
||||
t.Errorf("an assignment moved %+v", r.Moved)
|
||||
}
|
||||
expect(t, m, "ace", "10", SourceDefault)
|
||||
expect(t, m, "node-a", "10", SourceDefault)
|
||||
if len(m.events) != 1 || m.events[0].Action != "resolve" {
|
||||
t.Errorf("an assignment raised %+v", m.events)
|
||||
}
|
||||
_ = m.Delete(AssignmentsRead, "ace.dunst")
|
||||
_ = m.Delete(AssignmentsRead, "node-a.dunst")
|
||||
if r, _ = c.Reconcile(); len(r.Moved) != 4 || !r.Moved[0].Gone {
|
||||
t.Errorf("an unassignment moved %+v", r.Moved)
|
||||
}
|
||||
if e, _ := m.Get(ResolvedState, ResolvedKey("ace", "dunst", "font-size")); e != nil {
|
||||
if e, _ := m.Get(ResolvedState, ResolvedKey("node-a", "dunst", "font-size")); e != nil {
|
||||
t.Error("an unassigned module keeps its resolved values")
|
||||
}
|
||||
}
|
||||
@@ -523,17 +523,17 @@ func TestTheDeclaredConfigMirrorsTwoShapesReadTheSame(t *testing.T) {
|
||||
func TestAListingNarrowsByModuleAndNode(t *testing.T) {
|
||||
c, _ := aMesh(t)
|
||||
set(t, c, Call{Key: "font-size", Value: 12.0})
|
||||
set(t, c, Call{Key: "font-size", Value: 14.0, Node: "novox"})
|
||||
l, err := c.List(Call{Module: "dunst", Node: "g14"})
|
||||
set(t, c, Call{Key: "font-size", Value: 14.0, Node: "node-c"})
|
||||
l, err := c.List(Call{Module: "dunst", Node: "node-b"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(l.Resolved) != 4 || len(l.Set) != 1 || l.Set[0].Level.Kind != LevelMesh || l.Predefined != nil {
|
||||
t.Errorf("dunst on g14 lists %d resolved and %+v", len(l.Resolved), l.Set)
|
||||
t.Errorf("dunst on node-b lists %d resolved and %+v", len(l.Resolved), l.Set)
|
||||
}
|
||||
l, _ = c.List(Call{Node: "novox"})
|
||||
l, _ = c.List(Call{Node: "node-c"})
|
||||
if len(l.Resolved) != 4 || len(l.Set) != 2 {
|
||||
t.Errorf("novox lists %d resolved and %+v", len(l.Resolved), l.Set)
|
||||
t.Errorf("node-c lists %d resolved and %+v", len(l.Resolved), l.Set)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -556,20 +556,20 @@ func TestTheVerbsArgumentsAreReadStrictly(t *testing.T) {
|
||||
// resolves nothing and takes the module's resolved values away, as a removed key does.
|
||||
func TestAnEndedAssignmentIsNotAnAssignment(t *testing.T) {
|
||||
c, m := aMesh(t)
|
||||
m.assign(t, "ace", "dunst")
|
||||
m.assign(t, "node-a", "dunst")
|
||||
if r, err := c.Reconcile(); err != nil || len(r.Moved) != 4 {
|
||||
t.Fatalf("the assignment moved %+v (%v)", r.Moved, err)
|
||||
}
|
||||
for _, ended := range []string{`"ended":"2026-10-18T01:00:00Z"`, `"state":"pending"`} {
|
||||
m.mirror(t, AssignmentsRead, "ace.dunst", `{"node":"ace","module":"dunst","assigned":"2026-10-14T00:00:00Z",`+ended+`}`)
|
||||
m.mirror(t, AssignmentsRead, "node-a.dunst", `{"node":"node-a","module":"dunst","assigned":"2026-10-14T00:00:00Z",`+ended+`}`)
|
||||
r, err := c.Reconcile()
|
||||
if err != nil || len(r.Moved) != 4 || !r.Moved[0].Gone {
|
||||
t.Errorf("%s moved %+v (%v)", ended, r.Moved, err)
|
||||
}
|
||||
if e, _ := m.Get(ResolvedState, ResolvedKey("ace", "dunst", "font-size")); e != nil {
|
||||
if e, _ := m.Get(ResolvedState, ResolvedKey("node-a", "dunst", "font-size")); e != nil {
|
||||
t.Errorf("%s: the module keeps its resolved values", ended)
|
||||
}
|
||||
m.assign(t, "ace", "dunst")
|
||||
m.assign(t, "node-a", "dunst")
|
||||
if r, _ = c.Reconcile(); len(r.Moved) != 4 {
|
||||
t.Fatalf("the assignment standing again moved %+v", r.Moved)
|
||||
}
|
||||
@@ -579,14 +579,14 @@ func TestAnEndedAssignmentIsNotAnAssignment(t *testing.T) {
|
||||
// Issue 1098: the fields the controller adds to an assignment change nothing, under the same key.
|
||||
func TestAnAssignmentCarriesBuildCommitAndRolloutWithoutChangingItsKey(t *testing.T) {
|
||||
c, m := aMesh(t)
|
||||
m.mirror(t, AssignmentsRead, "ace.dunst", `{"node":"ace","module":"dunst","assigned":"2026-10-14T00:00:00Z",`+
|
||||
m.mirror(t, AssignmentsRead, "node-a.dunst", `{"node":"node-a","module":"dunst","assigned":"2026-10-14T00:00:00Z",`+
|
||||
`"build":"b-1","artifact":{"name":"dunst","version":"1.2"},"commit":"abc123","rollout":"r-9",`+
|
||||
`"since":"2026-10-18T00:00:00Z","ended":"","state":"","future":{"x":1}}`)
|
||||
r, err := c.Reconcile()
|
||||
if err != nil || len(r.Moved) != 4 {
|
||||
t.Fatalf("an assignment with the new fields moved %+v (%v)", r.Moved, err)
|
||||
}
|
||||
expect(t, m, "ace", "10", SourceDefault)
|
||||
expect(t, m, "node-a", "10", SourceDefault)
|
||||
if r, _ = c.Reconcile(); len(r.Moved) != 0 {
|
||||
t.Errorf("a quiet pass moved %+v", r.Moved)
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ type standIn struct {
|
||||
|
||||
func newStandIn() *standIn {
|
||||
return &standIn{asks: map[string]asks.Ask{}, records: map[string][]byte{}, kinds: []stdio.KindHeld{
|
||||
{Seat: asks.ChannelSeat, Kind: "telegram", Module: "telegram", Node: "novox",
|
||||
{Seat: asks.ChannelSeat, Kind: "telegram", Module: "telegram", Node: "node-c",
|
||||
Capabilities: []string{"choice", "exact-render", "verified-sender"}}}}
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ func propose(t *testing.T, c *Config, call Call) *ProposeAnswer {
|
||||
t.Helper()
|
||||
call.Action = "propose"
|
||||
if call.Caller == "" {
|
||||
call.Caller = "g14.node-tools"
|
||||
call.Caller = "node-b.node-tools"
|
||||
}
|
||||
if call.Why == "" {
|
||||
call.Why = "the agents need the new rule"
|
||||
@@ -176,7 +176,7 @@ func TestATrustedValueIsProposedAskedUnderItsOwnNameAndSetOnTheOperatorsApprove(
|
||||
t.Fatalf("a proposal set something: %d events, %d hand-acts", len(m.events), len(m.handActs))
|
||||
}
|
||||
if !strings.Contains(a.Router, "took the ask") || a.Proposal == "" || len(a.Reaches) != 1 ||
|
||||
a.Reaches[0] != "claude-code on g14" {
|
||||
a.Reaches[0] != "claude-code on node-b" {
|
||||
t.Errorf("the proposal answered %+v", a)
|
||||
}
|
||||
// Asked under mesh-config's own name, on the operator channel.
|
||||
@@ -234,12 +234,12 @@ func TestATrustedValueIsProposedAskedUnderItsOwnNameAndSetOnTheOperatorsApprove(
|
||||
}
|
||||
v, set := storedValue(t, m, "mesh.agent-managed-settings")
|
||||
if !set || string(v.Value) != `{"permissions":{"deny":["Bash(ssh:*)"]}}` || v.By != "the operator" ||
|
||||
v.Via != "g14.node-tools" || !strings.Contains(v.Approved, "approved by the operator, as telegram identity 4242, "+
|
||||
v.Via != "node-b.node-tools" || !strings.Contains(v.Approved, "approved by the operator, as telegram identity 4242, "+
|
||||
"via telegram (user id verified)") || !strings.Contains(v.Approved, "ask "+a.Proposal) ||
|
||||
!strings.Contains(v.Approved, "proposed by g14.node-tools") {
|
||||
!strings.Contains(v.Approved, "proposed by node-b.node-tools") {
|
||||
t.Fatalf("the value is %+v", v)
|
||||
}
|
||||
if res := m.resolved(t, "g14", "claude-code", "managed"); res.Source != SourceMesh ||
|
||||
if res := m.resolved(t, "node-b", "claude-code", "managed"); res.Source != SourceMesh ||
|
||||
string(res.Value) != string(v.Value) {
|
||||
t.Errorf("claude-code's managed settings resolve as %+v", res)
|
||||
}
|
||||
@@ -285,14 +285,14 @@ func TestAnApproveIsRefusedWhenTheStoredValueChangedSince(t *testing.T) {
|
||||
}
|
||||
|
||||
// And a clear since: the value it was shown against (a value) is no longer stored.
|
||||
set(t, c, Call{Key: "font-size", Value: 12.0, Node: "g14"})
|
||||
a = propose(t, c, Call{Key: "font-size", Value: 14.0, Node: "g14"})
|
||||
set(t, c, Call{Action: "clear", Key: "font-size", Node: "g14"})
|
||||
set(t, c, Call{Key: "font-size", Value: 12.0, Node: "node-b"})
|
||||
a = propose(t, c, Call{Key: "font-size", Value: 14.0, Node: "node-b"})
|
||||
set(t, c, Call{Action: "clear", Key: "font-size", Node: "node-b"})
|
||||
r.choose(t, a.Proposal, OptionApprove, nil)
|
||||
if said, _ = c.Decide(); len(said) != 1 || !strings.Contains(said[0], "no value set") {
|
||||
t.Fatalf("the Approve after a clear decided %v", said)
|
||||
}
|
||||
if _, isSet := storedValue(t, m, "node.g14.font-size"); isSet {
|
||||
if _, isSet := storedValue(t, m, "node.node-b.font-size"); isSet {
|
||||
t.Error("an Approve against a cleared value set it")
|
||||
}
|
||||
}
|
||||
@@ -313,7 +313,7 @@ func TestAnApproveIsRefusedWhenTheProposalChangedAfterItWasAsked(t *testing.T) {
|
||||
{"the value it was shown against", func(p *Proposal) {
|
||||
p.Was, p.HadValue = json.RawMessage(`{"x":1}`), true
|
||||
}, "the value it was shown against"},
|
||||
{"its level", func(p *Proposal) { p.Level = Level{Kind: LevelNode, Node: "g14"} }, "not the ask the person was shown"},
|
||||
{"its level", func(p *Proposal) { p.Level = Level{Kind: LevelNode, Node: "node-b"} }, "not the ask the person was shown"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
c, m, r := aProposingMesh(t)
|
||||
@@ -378,7 +378,7 @@ func TestDeclineExpiryAndTheRoutersRefusalSetNothing(t *testing.T) {
|
||||
}
|
||||
|
||||
// Ended without a choice: the router's word.
|
||||
a = propose(t, c, Call{Key: "font-size", Value: 12.0, Node: "g14"})
|
||||
a = propose(t, c, Call{Key: "font-size", Value: 12.0, Node: "node-b"})
|
||||
c.HeardEnd(asks.Warrant{Ask: a.Proposal, Asker: Module, Outcome: asks.OutcomeExpired, At: time.Now()})
|
||||
if p := storedProposal(t, m, a.Proposal); p.State != ProposalEnded || !strings.Contains(p.Outcome, "expired") {
|
||||
t.Errorf("the expired proposal is %+v", p)
|
||||
@@ -386,7 +386,7 @@ func TestDeclineExpiryAndTheRoutersRefusalSetNothing(t *testing.T) {
|
||||
|
||||
// Ended, and the router never recorded it: discarded once its time is past.
|
||||
r.noRecord = true
|
||||
a = propose(t, c, Call{Key: "font-size", Value: 13.0, Node: "novox"})
|
||||
a = propose(t, c, Call{Key: "font-size", Value: 13.0, Node: "node-c"})
|
||||
if !strings.Contains(a.Router, "has not said yet") {
|
||||
t.Errorf("an ask the router has not taken answered %q", a.Router)
|
||||
}
|
||||
@@ -402,7 +402,7 @@ func TestDeclineExpiryAndTheRoutersRefusalSetNothing(t *testing.T) {
|
||||
// Refused by the router at once: said to the proposer.
|
||||
c2, m2, r2 := aProposingMesh(t)
|
||||
r2.refuseWith = "no channel can carry an approval"
|
||||
_, err := c2.Propose(Call{Action: "propose", Key: "font-size", Value: 12.0, Caller: "g14.node-tools", Why: "w"})
|
||||
_, err := c2.Propose(Call{Action: "propose", Key: "font-size", Value: 12.0, Caller: "node-b.node-tools", Why: "w"})
|
||||
if err == nil || !strings.Contains(err.Error(), "no channel can carry an approval") {
|
||||
t.Fatalf("the router's refusal answered %v", err)
|
||||
}
|
||||
@@ -426,11 +426,11 @@ func TestAProposalIsJudgedAndRefusedBeforeTheOperatorIsAsked(t *testing.T) {
|
||||
call Call
|
||||
want string
|
||||
}{
|
||||
{"a key not declared", Call{Key: "colour", Value: "red", Node: "g14", Module: "dunst"}, "it declares:"},
|
||||
{"a key not declared", Call{Key: "colour", Value: "red", Node: "node-b", Module: "dunst"}, "it declares:"},
|
||||
{"a value failing validation", Call{Key: "font-size", Value: 99.0}, "above the maximum 72"},
|
||||
{"a module value without a node", Call{Key: "width", Value: 400.0, Module: "dunst"}, "no module value for the whole mesh"},
|
||||
{"nothing that would change", Call{Key: "font-size", Value: 12.0}, "already holds exactly 12"},
|
||||
{"a clear of nothing", Call{Key: "font-size", Clear: true, Node: "g14"}, "nothing to clear"},
|
||||
{"a clear of nothing", Call{Key: "font-size", Clear: true, Node: "node-b"}, "nothing to clear"},
|
||||
{"a clear with a value", Call{Key: "font-size", Clear: true, Value: 1.0}, "a clear carries no value"},
|
||||
{"a secret's shape", Call{Key: "agent-managed-settings", Value: map[string]any{"env": map[string]any{
|
||||
"API_TOKEN": "ghp_abcdefghijklmnopqrstuvwxyz0123456789"}}}, "leaves the mesh nowhere"},
|
||||
@@ -442,7 +442,7 @@ func TestAProposalIsJudgedAndRefusedBeforeTheOperatorIsAsked(t *testing.T) {
|
||||
call := tc.call
|
||||
call.Action = "propose"
|
||||
if call.Caller == "" {
|
||||
call.Caller = "g14.node-tools"
|
||||
call.Caller = "node-b.node-tools"
|
||||
}
|
||||
if call.Why == "" {
|
||||
call.Why = "a test"
|
||||
@@ -470,20 +470,20 @@ func TestAProposalIsJudgedAndRefusedBeforeTheOperatorIsAsked(t *testing.T) {
|
||||
}
|
||||
r.refuse = nil
|
||||
|
||||
first := propose(t, c, Call{Key: "font-size", Value: 14.0, Node: "g14"})
|
||||
if _, err := c.Propose(Call{Action: "propose", Key: "font-size", Value: 14.0, Node: "g14", Caller: "g", Why: "w"}); err == nil ||
|
||||
first := propose(t, c, Call{Key: "font-size", Value: 14.0, Node: "node-b"})
|
||||
if _, err := c.Propose(Call{Action: "propose", Key: "font-size", Value: 14.0, Node: "node-b", Caller: "g", Why: "w"}); err == nil ||
|
||||
!strings.Contains(err.Error(), "proposed already, as "+first.Proposal) {
|
||||
t.Errorf("the same change twice: %v", err)
|
||||
}
|
||||
// A newer proposal for the same place replaces the older (ADR 0277 §10), and is not counted twice.
|
||||
newer := propose(t, c, Call{Key: "font-size", Value: 15.0, Node: "g14"})
|
||||
newer := propose(t, c, Call{Key: "font-size", Value: 15.0, Node: "node-b"})
|
||||
if len(newer.Replaced) != 1 || newer.Replaced[0] != first.Proposal ||
|
||||
storedProposal(t, m, first.Proposal).State != ProposalEnded {
|
||||
t.Errorf("the newer proposal replaced %v", newer.Replaced)
|
||||
}
|
||||
propose(t, c, Call{Key: "font-size", Value: 15.0, Node: "novox"})
|
||||
propose(t, c, Call{Key: "font-size", Value: 15.0, Node: "node-c"})
|
||||
propose(t, c, Call{Key: "agent-managed-settings", Value: map[string]any{"a": 1.0}})
|
||||
if _, err := c.Propose(Call{Action: "propose", Key: "width", Value: 400.0, Node: "g14", Module: "dunst",
|
||||
if _, err := c.Propose(Call{Action: "propose", Key: "width", Value: 400.0, Node: "node-b", Module: "dunst",
|
||||
Caller: "g", Why: "w"}); err == nil || !strings.Contains(err.Error(), "3 proposals already wait") {
|
||||
t.Errorf("a fourth open ask: %v", err)
|
||||
}
|
||||
@@ -496,16 +496,16 @@ func TestAProposalIsJudgedAndRefusedBeforeTheOperatorIsAsked(t *testing.T) {
|
||||
// naming the proposal as the way (ADR 0277 §2).
|
||||
func TestAClearIsProposedAndATrustedSetNamesTheProposal(t *testing.T) {
|
||||
c, m, r := aProposingMesh(t)
|
||||
_, err := c.Change(Call{Action: "set", Key: "agent-managed-settings", Value: map[string]any{}, Caller: "g14.node-tools",
|
||||
_, err := c.Change(Call{Action: "set", Key: "agent-managed-settings", Value: map[string]any{}, Caller: "node-b.node-tools",
|
||||
Why: "w"})
|
||||
if err == nil || !strings.Contains(err.Error(), "propose it instead") {
|
||||
t.Errorf("a trusted set through the verb: %v", err)
|
||||
}
|
||||
set(t, c, Call{Key: "managed", Value: map[string]any{"a": 1.0}, Node: "g14", Module: "claude-code",
|
||||
set(t, c, Call{Key: "managed", Value: map[string]any{"a": 1.0}, Node: "node-b", Module: "claude-code",
|
||||
Caller: ControllerCaller, By: "jochen"})
|
||||
a := propose(t, c, Call{Key: "managed", Clear: true, Node: "g14", Module: "claude-code"})
|
||||
a := propose(t, c, Call{Key: "managed", Clear: true, Node: "node-b", Module: "claude-code"})
|
||||
ask := r.asks[a.Proposal]
|
||||
if !strings.HasPrefix(ask.Headline, "Clear managed for claude-code on g14") ||
|
||||
if !strings.HasPrefix(ask.Headline, "Clear managed for claude-code on node-b") ||
|
||||
!strings.Contains(ask.Explanation, `- managed (was: {"a":1})`) {
|
||||
t.Errorf("the clear asks %q:\n%s", ask.Headline, ask.Explanation)
|
||||
}
|
||||
@@ -513,7 +513,7 @@ func TestAClearIsProposedAndATrustedSetNamesTheProposal(t *testing.T) {
|
||||
if said, _ := c.Decide(); len(said) != 1 || !strings.Contains(said[0], "is cleared") {
|
||||
t.Fatalf("the clear decided %v", said)
|
||||
}
|
||||
if _, isSet := storedValue(t, m, "module.g14.claude-code.managed"); isSet {
|
||||
if _, isSet := storedValue(t, m, "module.node-b.claude-code.managed"); isSet {
|
||||
t.Error("an approved clear left the value")
|
||||
}
|
||||
h, _ := c.History(Call{Key: "managed"})
|
||||
@@ -526,7 +526,7 @@ func TestAClearIsProposedAndATrustedSetNamesTheProposal(t *testing.T) {
|
||||
func TestTheProposalsAreListedAndOneIsReadWhole(t *testing.T) {
|
||||
c, m, _ := aProposingMesh(t)
|
||||
a := propose(t, c, Call{Key: "agent-managed-settings", Value: map[string]any{"a": 1.0}})
|
||||
b := propose(t, c, Call{Key: "font-size", Value: 12.0, Node: "g14"})
|
||||
b := propose(t, c, Call{Key: "font-size", Value: 12.0, Node: "node-b"})
|
||||
rows, err := c.Do(Call{Action: "proposals"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -575,7 +575,7 @@ func TestAnOwnersListOfNamesWithDigitsIsProposed(t *testing.T) {
|
||||
c, m, r := aProposingMesh(t)
|
||||
m.mirror(t, DeclaredConfigRead, "mesh-issues", `{"module":"mesh-issues","settings":{
|
||||
"owners":{"kind":"","default":[],"why":"","type":"list","description":"owners","trusted":true}}}`)
|
||||
m.assign(t, "novox", "mesh-issues")
|
||||
m.assign(t, "node-c", "mesh-issues")
|
||||
raw, err := os.ReadFile(filepath.Join("testdata", "owners-1439.json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -584,7 +584,7 @@ func TestAnOwnersListOfNamesWithDigitsIsProposed(t *testing.T) {
|
||||
if err := json.Unmarshal(raw, &owners); err != nil || len(owners) != 135 {
|
||||
t.Fatalf("testdata holds %d owners (%v), want the 134 defaults plus okioki", len(owners), err)
|
||||
}
|
||||
a := propose(t, c, Call{Key: "owners", Value: owners, Node: "novox", Module: "mesh-issues"})
|
||||
a := propose(t, c, Call{Key: "owners", Value: owners, Node: "node-c", Module: "mesh-issues"})
|
||||
if a.Proposal == "" || len(r.subjects) != 1 {
|
||||
t.Fatalf("the owners were not put to the operator: %+v, asked on %v", a, r.subjects)
|
||||
}
|
||||
|
||||
@@ -11,13 +11,13 @@ func TestAModulePulledInHasItsValuesResolvedOnItsNode(t *testing.T) {
|
||||
if _, err := c.Reconcile(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m.mirror(t, AssignmentsRead, "ace.dunst", `{"node":"ace","module":"dunst","required-by":"desk",`+
|
||||
m.mirror(t, AssignmentsRead, "node-a.dunst", `{"node":"node-a","module":"dunst","required-by":"desk",`+
|
||||
`"provisioning":{"templates":{"source":"x","digest":"y"},"files":[]}}`)
|
||||
if r, err := c.Reconcile(); err != nil || len(r.Moved) != 4 {
|
||||
t.Fatalf("a pulled-in module's value moved %+v (%v)", r.Moved, err)
|
||||
}
|
||||
expect(t, m, "ace", "10", SourceDefault)
|
||||
_ = m.Delete(AssignmentsRead, "ace.dunst")
|
||||
expect(t, m, "node-a", "10", SourceDefault)
|
||||
_ = m.Delete(AssignmentsRead, "node-a.dunst")
|
||||
if r, _ := c.Reconcile(); len(r.Moved) != 4 || !r.Moved[0].Gone {
|
||||
t.Errorf("a module no longer pulled in moved %+v", r.Moved)
|
||||
}
|
||||
|
||||
@@ -18,13 +18,13 @@ func n8nPullDeclares() map[string]any {
|
||||
"description": "the media group", "required": true, "minimum": 1, "maximum": 65535}}}
|
||||
}
|
||||
|
||||
// aHoldingMesh is a proposing mesh with n8n assigned to ace and declaring nothing on the trunk (the case of the
|
||||
// aHoldingMesh is a proposing mesh with n8n assigned to node-a and declaring nothing on the trunk (the case of the
|
||||
// issue), and a stand-in forge.
|
||||
func aHoldingMesh(t *testing.T) (*Config, *memStore, *standIn, *standForge) {
|
||||
t.Helper()
|
||||
c, m, r := aProposingMesh(t)
|
||||
m.mirror(t, DeclaredConfigRead, "n8n", `{"module":"n8n","settings":{}}`)
|
||||
m.assign(t, "ace", "n8n")
|
||||
m.assign(t, "node-a", "n8n")
|
||||
f := &standForge{state: map[string][2]bool{}}
|
||||
c.Forge = f
|
||||
if _, err := c.Reconcile(); err != nil {
|
||||
@@ -60,10 +60,10 @@ func tell(t *testing.T, c *Config, pull, head string, declares map[string]any) *
|
||||
return a.(*PullDeclaresAnswer)
|
||||
}
|
||||
|
||||
// holdIt proposes media-gid=2000 for n8n on ace held for the pull request and has the operator approve it.
|
||||
// holdIt proposes media-gid=2000 for n8n on node-a held for the pull request and has the operator approve it.
|
||||
func holdIt(t *testing.T, c *Config, m *memStore, r *standIn) string {
|
||||
t.Helper()
|
||||
a := propose(t, c, Call{Module: "n8n", Node: "ace", Key: "media-gid", Value: 2000.0, Pull: thePull,
|
||||
a := propose(t, c, Call{Module: "n8n", Node: "node-a", Key: "media-gid", Value: 2000.0, Pull: thePull,
|
||||
Why: "n8n's container joins the media group, held for #434"})
|
||||
r.choose(t, a.Proposal, OptionApprove, nil)
|
||||
said, err := c.Decide()
|
||||
@@ -78,7 +78,7 @@ func keptCount(m *memStore) int { k, _ := m.Keys(KeptState); return len(k) }
|
||||
// 1826/K1: mesh-config takes what a pull request declares from the controller alone, and keeps it per pull request.
|
||||
func TestAPullRequestsDeclarationsAreTakenFromTheControllerAlone(t *testing.T) {
|
||||
c, m, _, _ := aHoldingMesh(t)
|
||||
for _, caller := range []string{"g14.node-tools", Unproven, ""} {
|
||||
for _, caller := range []string{"node-b.node-tools", Unproven, ""} {
|
||||
if _, err := c.Do(Call{Action: "pull-declares", Pull: thePull, Head: "abc1234", Declares: n8nPullDeclares(),
|
||||
Caller: caller}); err == nil || !strings.Contains(err.Error(), "controller") {
|
||||
t.Errorf("pull-declares from %q: %v", caller, err)
|
||||
@@ -106,7 +106,7 @@ func TestAPullRequestsDeclarationsAreTakenFromTheControllerAlone(t *testing.T) {
|
||||
}())
|
||||
}
|
||||
// Nothing is resolved from a pull request's declaration.
|
||||
if r, _ := m.Get(ResolvedState, ResolvedKey("ace", "n8n", "media-gid")); r != nil {
|
||||
if r, _ := m.Get(ResolvedState, ResolvedKey("node-a", "n8n", "media-gid")); r != nil {
|
||||
t.Errorf("a pull request's declaration was resolved: %s", r.Value)
|
||||
}
|
||||
}
|
||||
@@ -117,11 +117,11 @@ func TestAPullRequestsDeclarationsAreTakenFromTheControllerAlone(t *testing.T) {
|
||||
func TestAProposalNamingAPullRequestIsJudgedAgainstItsDeclaration(t *testing.T) {
|
||||
c, m, _, _ := aHoldingMesh(t)
|
||||
try := func(call Call) error {
|
||||
call.Action, call.Caller, call.Why = "propose", "g14.node-tools", "held for 434"
|
||||
call.Action, call.Caller, call.Why = "propose", "node-b.node-tools", "held for 434"
|
||||
_, err := c.Propose(call)
|
||||
return err
|
||||
}
|
||||
base := Call{Module: "n8n", Node: "ace", Key: "media-gid", Value: 2000.0, Pull: thePull}
|
||||
base := Call{Module: "n8n", Node: "node-a", Key: "media-gid", Value: 2000.0, Pull: thePull}
|
||||
if err := try(base); err == nil || !strings.Contains(err.Error(), "holds no declaration for "+thePull) {
|
||||
t.Errorf("without the pull request's declaration: %v", err)
|
||||
}
|
||||
@@ -142,7 +142,7 @@ func TestAProposalNamingAPullRequestIsJudgedAgainstItsDeclaration(t *testing.T)
|
||||
"a key not added": {func(c *Call) { c.Key = "other" }, "does not add or change a value"},
|
||||
"a bad value": {func(c *Call) { c.Value = 0.0 }, "below the minimum"},
|
||||
"a wrong type": {func(c *Call) { c.Value = "2000" }, "is not a number"},
|
||||
"another node": {func(c *Call) { c.Node = "g14" }, "is not assigned to g14"},
|
||||
"another node": {func(c *Call) { c.Node = "node-b" }, "is not assigned to node-b"},
|
||||
"another pull": {func(c *Call) { c.Pull = "novox/mesh-catalog#435" }, "holds no declaration for"},
|
||||
"a malformed pull": {func(c *Call) { c.Pull = "434" }, "owner/repository#number"},
|
||||
} {
|
||||
@@ -153,8 +153,8 @@ func TestAProposalNamingAPullRequestIsJudgedAgainstItsDeclaration(t *testing.T)
|
||||
}
|
||||
}
|
||||
// A set or a clear through the verb takes no pull request.
|
||||
if _, err := c.Change(Call{Action: "set", Module: "n8n", Node: "ace", Key: "media-gid", Value: 2000.0, Pull: thePull,
|
||||
Caller: "g14.node-tools", Why: "w"}); err == nil || !strings.Contains(err.Error(), "only a proposal") {
|
||||
if _, err := c.Change(Call{Action: "set", Module: "n8n", Node: "node-a", Key: "media-gid", Value: 2000.0, Pull: thePull,
|
||||
Caller: "node-b.node-tools", Why: "w"}); err == nil || !strings.Contains(err.Error(), "only a proposal") {
|
||||
t.Errorf("a set naming a pull request: %v", err)
|
||||
}
|
||||
if keptCount(m) != 0 {
|
||||
@@ -167,7 +167,7 @@ func TestAProposalNamingAPullRequestIsJudgedAgainstItsDeclaration(t *testing.T)
|
||||
func TestAHeldValueIsAskedAndKeptApartOnApprove(t *testing.T) {
|
||||
c, m, r, _ := aHoldingMesh(t)
|
||||
tell(t, c, thePull, "abc1234def", n8nPullDeclares())
|
||||
a := propose(t, c, Call{Module: "n8n", Node: "ace", Key: "media-gid", Value: 2000.0, Pull: thePull, Why: "held for #434"})
|
||||
a := propose(t, c, Call{Module: "n8n", Node: "node-a", Key: "media-gid", Value: 2000.0, Pull: thePull, Why: "held for #434"})
|
||||
if len(a.Reaches) != 0 {
|
||||
t.Errorf("a held value reaches %v", a.Reaches)
|
||||
}
|
||||
@@ -175,7 +175,7 @@ func TestAHeldValueIsAskedAndKeptApartOnApprove(t *testing.T) {
|
||||
if err := ask.Check(c.now()); err != nil {
|
||||
t.Fatalf("the ask: %v", err)
|
||||
}
|
||||
for _, want := range []string{thePull, "merges", "dropped if it closes unmerged", "media-gid", "n8n", "ace"} {
|
||||
for _, want := range []string{thePull, "merges", "dropped if it closes unmerged", "media-gid", "n8n", "node-a"} {
|
||||
if !strings.Contains(ask.Explanation, want) {
|
||||
t.Errorf("the ask does not say %q:\n%s", want, ask.Explanation)
|
||||
}
|
||||
@@ -195,10 +195,10 @@ func TestAHeldValueIsAskedAndKeptApartOnApprove(t *testing.T) {
|
||||
t.Fatalf("decided %v", said)
|
||||
}
|
||||
// Kept apart: not a value, not resolved.
|
||||
if _, set := storedValue(t, m, "module.ace.n8n.media-gid"); set {
|
||||
if _, set := storedValue(t, m, "module.node-a.n8n.media-gid"); set {
|
||||
t.Error("an Approve set the value")
|
||||
}
|
||||
if e, _ := m.Get(ResolvedState, ResolvedKey("ace", "n8n", "media-gid")); e != nil {
|
||||
if e, _ := m.Get(ResolvedState, ResolvedKey("node-a", "n8n", "media-gid")); e != nil {
|
||||
t.Errorf("an Approve resolved the value: %s", e.Value)
|
||||
}
|
||||
if keptCount(m) != 1 {
|
||||
@@ -212,12 +212,12 @@ func TestAHeldValueIsAskedAndKeptApartOnApprove(t *testing.T) {
|
||||
if len(m.handActs) != 1 || !strings.Contains(m.handActs[0], thePull) {
|
||||
t.Errorf("hand-acts %q", m.handActs)
|
||||
}
|
||||
h, _ := c.History(Call{Module: "n8n", Node: "ace"})
|
||||
h, _ := c.History(Call{Module: "n8n", Node: "node-a"})
|
||||
if len(h) != 1 || h[0].Action != ActionHold || h[0].Pull != thePull {
|
||||
t.Errorf("history %+v", h)
|
||||
}
|
||||
// held answers it, at the head it is kept at, to the controller alone.
|
||||
if _, err := c.Do(Call{Action: "held", Caller: "g14.node-tools", Pulls: []PullAt{{thePull, "abc1234def"}}}); err == nil {
|
||||
if _, err := c.Do(Call{Action: "held", Caller: "node-b.node-tools", Pulls: []PullAt{{thePull, "abc1234def"}}}); err == nil {
|
||||
t.Error("held answered a caller that is not the controller")
|
||||
}
|
||||
got, err := c.Do(Call{Action: "held", Caller: ControllerCaller, Pulls: []PullAt{{thePull, "abc1234def"}}})
|
||||
@@ -225,7 +225,7 @@ func TestAHeldValueIsAskedAndKeptApartOnApprove(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ha := got.(*HeldAnswer)
|
||||
if len(ha.Held) != 1 || ha.Held[0].Module != "n8n" || ha.Held[0].Node != "ace" || ha.Held[0].Key != "media-gid" ||
|
||||
if len(ha.Held) != 1 || ha.Held[0].Module != "n8n" || ha.Held[0].Node != "node-a" || ha.Held[0].Key != "media-gid" ||
|
||||
string(ha.Held[0].Value) != "2000" || !strings.Contains(ha.Held[0].Approved, "approved by the operator") ||
|
||||
ha.Held[0].Pull != thePull || len(ha.Skipped) != 0 {
|
||||
t.Errorf("held answered %+v", ha)
|
||||
@@ -251,7 +251,7 @@ func TestAHeldValueIsAskedAndKeptApartOnApprove(t *testing.T) {
|
||||
func TestAnApproveOfAHeldValueIsRefusedWhenTheDeclarationChanged(t *testing.T) {
|
||||
c, m, r, _ := aHoldingMesh(t)
|
||||
tell(t, c, thePull, "abc1234def", n8nPullDeclares())
|
||||
a := propose(t, c, Call{Module: "n8n", Node: "ace", Key: "media-gid", Value: 2000.0, Pull: thePull, Why: "w"})
|
||||
a := propose(t, c, Call{Module: "n8n", Node: "node-a", Key: "media-gid", Value: 2000.0, Pull: thePull, Why: "w"})
|
||||
changed := n8nPullDeclares()
|
||||
changed["n8n"].(map[string]any)["media-gid"].(map[string]any)["maximum"] = 1000.0
|
||||
tell(t, c, thePull, "def5678abc", changed)
|
||||
@@ -312,7 +312,7 @@ func TestTheMergeAppliesAHeldValueAndItResolvesOnceTheTrunkDeclaresTheKey(t *tes
|
||||
tell(t, c, thePull, "abc1234def", n8nPullDeclares())
|
||||
holdIt(t, c, m, r)
|
||||
approved := func() string {
|
||||
raw, _ := m.Get(KeptState, "novox.mesh-catalog.434.ace.n8n.media-gid")
|
||||
raw, _ := m.Get(KeptState, "novox.mesh-catalog.434.node-a.n8n.media-gid")
|
||||
var k KeptValue
|
||||
_ = json.Unmarshal(raw.Value, &k)
|
||||
return k.Approved
|
||||
@@ -323,7 +323,7 @@ func TestTheMergeAppliesAHeldValueAndItResolvesOnceTheTrunkDeclaresTheKey(t *tes
|
||||
if len(said) == 0 {
|
||||
t.Fatal("the merge said nothing")
|
||||
}
|
||||
v, set := storedValue(t, m, "module.ace.n8n.media-gid")
|
||||
v, set := storedValue(t, m, "module.node-a.n8n.media-gid")
|
||||
if !set || string(v.Value) != "2000" || v.Approved != approved || v.Approved == "" || v.By != "the operator" ||
|
||||
!strings.Contains(v.Why, thePull) {
|
||||
t.Fatalf("the merge wrote %+v", v)
|
||||
@@ -339,7 +339,7 @@ func TestTheMergeAppliesAHeldValueAndItResolvesOnceTheTrunkDeclaresTheKey(t *tes
|
||||
t.Errorf("raised %+v, recorded %q", m.events, m.handActs)
|
||||
}
|
||||
// Not resolved before the trunk declares it.
|
||||
if e, _ := m.Get(ResolvedState, ResolvedKey("ace", "n8n", "media-gid")); e != nil {
|
||||
if e, _ := m.Get(ResolvedState, ResolvedKey("node-a", "n8n", "media-gid")); e != nil {
|
||||
t.Errorf("resolved before the trunk declares the key: %s", e.Value)
|
||||
}
|
||||
m.mirror(t, DeclaredConfigRead, "n8n", `{"module":"n8n","settings":{"media-gid":{"kind":"","default":null,"why":"",`+
|
||||
@@ -347,10 +347,10 @@ func TestTheMergeAppliesAHeldValueAndItResolvesOnceTheTrunkDeclaresTheKey(t *tes
|
||||
if _, err := c.Reconcile(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if res := m.resolved(t, "ace", "n8n", "media-gid"); string(res.Value) != "2000" || res.Source != SourceModule {
|
||||
if res := m.resolved(t, "node-a", "n8n", "media-gid"); string(res.Value) != "2000" || res.Source != SourceModule {
|
||||
t.Errorf("media-gid resolves as %+v", res)
|
||||
}
|
||||
l, _ := c.List(Call{Module: "n8n", Node: "ace"})
|
||||
l, _ := c.List(Call{Module: "n8n", Node: "node-a"})
|
||||
if len(l.Set) != 1 || l.Set[0].Approved != approved || len(l.Held) != 0 {
|
||||
t.Errorf("the listing: set %+v held %+v", l.Set, l.Held)
|
||||
}
|
||||
@@ -373,7 +373,7 @@ func TestAClosedPullRequestDropsItsHeldValuesAndSaysSo(t *testing.T) {
|
||||
if len(said) == 0 || !strings.Contains(strings.Join(said, "\n"), "closed unmerged: dropped, nothing was set") {
|
||||
t.Errorf("said %v", said)
|
||||
}
|
||||
if _, set := storedValue(t, m, "module.ace.n8n.media-gid"); set || keptCount(m) != 0 {
|
||||
if _, set := storedValue(t, m, "module.node-a.n8n.media-gid"); set || keptCount(m) != 0 {
|
||||
t.Error("a closed pull request left a value")
|
||||
}
|
||||
if len(m.events) != 1 || m.events[0].Action != ActionDrop || m.events[0].Pull != thePull ||
|
||||
@@ -393,7 +393,7 @@ func TestACloseEventForAMergedPullRequestIsItsMerge(t *testing.T) {
|
||||
f.state[thePull] = [2]bool{false, true}
|
||||
f.head = "abc1234def"
|
||||
c.HeardPull(PullEvent{Owner: "novox", Repo: "mesh-catalog", Number: 434}, false)
|
||||
if v, set := storedValue(t, m, "module.ace.n8n.media-gid"); !set || string(v.Value) != "2000" {
|
||||
if v, set := storedValue(t, m, "module.node-a.n8n.media-gid"); !set || string(v.Value) != "2000" {
|
||||
t.Errorf("the merge of a pull request announced closed set %+v", v)
|
||||
}
|
||||
}
|
||||
@@ -415,7 +415,7 @@ func TestTheLookAtTheForgeFindsAMissedMergeOrCloseAndLeavesAnOpenPullRequest(t *
|
||||
f.head = "abc1234def"
|
||||
f.state[thePull] = [2]bool{false, true}
|
||||
c.SweepPulls()
|
||||
if v, set := storedValue(t, m, "module.ace.n8n.media-gid"); !set || string(v.Value) != "2000" || keptCount(m) != 0 {
|
||||
if v, set := storedValue(t, m, "module.node-a.n8n.media-gid"); !set || string(v.Value) != "2000" || keptCount(m) != 0 {
|
||||
t.Errorf("a missed merge: %+v", v)
|
||||
}
|
||||
// A missed close.
|
||||
@@ -454,7 +454,7 @@ func TestTheMergeAppliesOnlyAValueHeldAtTheMergedHead(t *testing.T) {
|
||||
tell(t, c, thePull, "abc1234def", n8nPullDeclares())
|
||||
holdIt(t, c, m, r)
|
||||
said := c.HeardPull(PullEvent{Owner: "novox", Repo: "mesh-catalog", Number: 434, Merged: true, HeadSHA: head}, true)
|
||||
if _, set := storedValue(t, m, "module.ace.n8n.media-gid"); set || keptCount(m) != 0 ||
|
||||
if _, set := storedValue(t, m, "module.node-a.n8n.media-gid"); set || keptCount(m) != 0 ||
|
||||
!strings.Contains(strings.Join(said, "\n"), "dropped, nothing was set") {
|
||||
t.Errorf("%s: set=%v held=%d said %v", name, set, keptCount(m), said)
|
||||
}
|
||||
@@ -471,7 +471,7 @@ func TestALaterHeadWithoutTheDeclarationLeavesNothingToApplyAtTheMerge(t *testin
|
||||
holdIt(t, c, m, r)
|
||||
tell(t, c, thePull, "bbbbbbbbbb", map[string]any{})
|
||||
c.HeardPull(PullEvent{Owner: "novox", Repo: "mesh-catalog", Number: 434, Merged: true, HeadSHA: "bbbbbbbbbb"}, true)
|
||||
if _, set := storedValue(t, m, "module.ace.n8n.media-gid"); set {
|
||||
if _, set := storedValue(t, m, "module.node-a.n8n.media-gid"); set {
|
||||
t.Error("a value held for a declaration the merged head lacks was set")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Captured answers of the gitea module's `gitea_get_pull_request`, taken 2026-10-22 by fix-1826-catalog@shanks with
|
||||
Captured answers of the gitea module's `gitea_get_pull_request`, taken 2026-10-22 by fix-1826-catalog@node-d with
|
||||
`mesh_call gitea.gitea_get_pull_request {"owner":"novox","repo":"mesh-catalog","number":N}` through the mesh MCP server.
|
||||
|
||||
- get_pull_request_closed_merged_435.json: pull request 435, closed and merged.
|
||||
|
||||
Vendored
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"pull": {
|
||||
"base": "main",
|
||||
"body": "Part of design 47's milestone R4c-9 (ADR 0487): the image build module. A container image becomes one more artifact kind the builder builds through a build module, like Go and Node: mesh-build-image builds rootless with buildah, a candidate's check publishes nothing, a trunk build publishes exactly one digest and its build key names the reference. build-agent keeps building and delivering every image until R4g; nothing the operator sees changes. One delivery group on branch `feat/r4c9-image-build` in mesh-catalog and mesh-delivery. After the delivery the lead assigns buildah and mesh-build-image to shanks and ace.\n\nReviewed-by: reviewer-r4c9@shanks at 7b5f7f84acb73aaf8189200548ff69fd9aac04b2",
|
||||
"body": "Part of design 47's milestone R4c-9 (ADR 0487): the image build module. A container image becomes one more artifact kind the builder builds through a build module, like Go and Node: mesh-build-image builds rootless with buildah, a candidate's check publishes nothing, a trunk build publishes exactly one digest and its build key names the reference. build-agent keeps building and delivering every image until R4g; nothing the operator sees changes. One delivery group on branch `feat/r4c9-image-build` in mesh-catalog and mesh-delivery. After the delivery the lead assigns buildah and mesh-build-image to node-d and node-a.\n\nReviewed-by: reviewer-r4c9@node-d at 7b5f7f84acb73aaf8189200548ff69fd9aac04b2",
|
||||
"head": "feat/r4c9-image-build",
|
||||
"head_sha": "7b5f7f84acb73aaf8189200548ff69fd9aac04b2",
|
||||
"html_url": "http://127.0.0.1:20000/novox/mesh-catalog/pulls/435",
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"pull": {
|
||||
"base": "main",
|
||||
"body": "Part of design 47's milestone R4d: n8n and the bus run pinned upstream images (ADR 0480).\n\nReviewed-by: reviewer-r4d@shanks at c948f3a61c74257e6e3147effb396d9d6434f491\n",
|
||||
"body": "Part of design 47's milestone R4d: n8n and the bus run pinned upstream images (ADR 0480).\n\nReviewed-by: reviewer-r4d@node-d at c948f3a61c74257e6e3147effb396d9d6434f491\n",
|
||||
"head": "feat/r4d",
|
||||
"head_sha": "c948f3a61c74257e6e3147effb396d9d6434f491",
|
||||
"html_url": "http://127.0.0.1:20000/novox/mesh-catalog/pulls/434",
|
||||
|
||||
@@ -161,15 +161,15 @@ func TestMailSenderAcceptanceAnApprovedSendIsSentOnceAndSaidOnce(t *testing.T) {
|
||||
|
||||
dir := t.TempDir()
|
||||
accountsPath := filepath.Join(dir, "accounts.json")
|
||||
writeFile(t, accountsPath, `{"accounts": {"mesh": {"kind": "imap", "address": "jschoubben@novox.be",
|
||||
"incoming": {"host": "mail.novox.be", "port": 993, "security": "tls"},
|
||||
writeFile(t, accountsPath, `{"accounts": {"mesh": {"kind": "imap", "address": "jschoubben@node-c.be",
|
||||
"incoming": {"host": "mail.node-c.be", "port": 993, "security": "tls"},
|
||||
"outgoing": {"host": "127.0.0.1", "port": 1, "security": "tls"}}}}`)
|
||||
writeFile(t, filepath.Join(dir, "mail-credential-mesh.secret"), "the-password\n")
|
||||
|
||||
approvedAt := judgedAt
|
||||
req, id := queueAnApprovedSend(t, bus, "mesh", approvedAt)
|
||||
|
||||
account := process.Account{URL: url, Node: "g14", Module: Module, User: "g14." + Module + "." + SendProcessID, Password: "pw"}
|
||||
account := process.Account{URL: url, Node: "node-b", Module: Module, User: "node-b." + Module + "." + SendProcessID, Password: "pw"}
|
||||
if _, err := bus.js.CreateConsumer(context.Background(), MailSendsStream, jetstream.ConsumerConfig{
|
||||
Durable: account.QueueConsumerName(), AckPolicy: jetstream.AckExplicitPolicy, AckWait: 5 * time.Second, MaxDeliver: MostAttempts}); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -209,7 +209,7 @@ func TestMailSenderAcceptanceAnApprovedSendIsSentOnceAndSaidOnce(t *testing.T) {
|
||||
if got.to[0] != "ops@example.org" {
|
||||
t.Fatalf("received: %+v", got)
|
||||
}
|
||||
if !strings.Contains(got.data, "Message-Id: <"+req.ID+"@novox.be>") {
|
||||
if !strings.Contains(got.data, "Message-Id: <"+req.ID+"@node-c.be>") {
|
||||
t.Fatalf("the mail's Message-ID does not name the send: %s", got.data)
|
||||
}
|
||||
|
||||
@@ -238,15 +238,15 @@ func TestMailSenderAcceptanceARefusedSignInFailsAfterItsAttemptsAndNothingIsRece
|
||||
|
||||
dir := t.TempDir()
|
||||
accountsPath := filepath.Join(dir, "accounts.json")
|
||||
writeFile(t, accountsPath, `{"accounts": {"mesh": {"kind": "imap", "address": "jschoubben@novox.be",
|
||||
"incoming": {"host": "mail.novox.be", "port": 993, "security": "tls"},
|
||||
writeFile(t, accountsPath, `{"accounts": {"mesh": {"kind": "imap", "address": "jschoubben@node-c.be",
|
||||
"incoming": {"host": "mail.node-c.be", "port": 993, "security": "tls"},
|
||||
"outgoing": {"host": "127.0.0.1", "port": 1, "security": "tls"}}}}`)
|
||||
writeFile(t, filepath.Join(dir, "mail-credential-mesh.secret"), "the-wrong-password\n")
|
||||
|
||||
approvedAt := judgedAt
|
||||
_, id := queueAnApprovedSend(t, bus, "mesh", approvedAt)
|
||||
|
||||
account := process.Account{URL: url, Node: "g14", Module: Module, User: "g14." + Module + "." + SendProcessID, Password: "pw"}
|
||||
account := process.Account{URL: url, Node: "node-b", Module: Module, User: "node-b." + Module + "." + SendProcessID, Password: "pw"}
|
||||
if _, err := bus.js.CreateConsumer(context.Background(), MailSendsStream, jetstream.ConsumerConfig{
|
||||
Durable: account.QueueConsumerName(), AckPolicy: jetstream.AckExplicitPolicy, AckWait: 2 * time.Second, MaxDeliver: MostAttempts}); err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -44,16 +44,16 @@ func mustNotBeRefused(t *testing.T, a asks.Ask) {
|
||||
}
|
||||
|
||||
// (1) A recipient that is one of the operator's own mesh-mail accounts (sendTestBundle's "mesh", address
|
||||
// jschoubben@novox.be) is named by the account's name alone: the ask carries no mail address at all, and
|
||||
// jschoubben@node-c.be) is named by the account's name alone: the ask carries no mail address at all, and
|
||||
// passes the real content rule. This is the first test issue 868's decision asked for; it fails today
|
||||
// because AskMailSend puts the raw address into the ask.
|
||||
func TestMailSendAskForOwnAccountRecipientCarriesNoAddress(t *testing.T) {
|
||||
b, router, _ := sendTestBundle(t, Server{Host: "127.0.0.1", Port: 25, Security: "tls"})
|
||||
rt := startBundle(t, b.Tools())
|
||||
|
||||
args := map[string]any{"account": "mesh", "to": []any{"jschoubben@novox.be"},
|
||||
args := map[string]any{"account": "mesh", "to": []any{"jschoubben@node-c.be"},
|
||||
"subject": "Testing issue 868", "body": "Does the ask reach the operator now?"}
|
||||
text, isErr := rt.call(SendTool, "novox.triage", args)
|
||||
text, isErr := rt.call(SendTool, "node-c.triage", args)
|
||||
if isErr {
|
||||
t.Fatalf("%s", text)
|
||||
}
|
||||
@@ -81,7 +81,7 @@ func TestMailSendAskForOutsideRecipientCarriesNoAddress(t *testing.T) {
|
||||
b, router, _ := sendTestBundle(t, Server{Host: "127.0.0.1", Port: 25, Security: "tls"})
|
||||
rt := startBundle(t, b.Tools())
|
||||
|
||||
text, isErr := rt.call(SendTool, "novox.triage", sendArgs) // to: ops@example.org, lead@example.org
|
||||
text, isErr := rt.call(SendTool, "node-c.triage", sendArgs) // to: ops@example.org, lead@example.org
|
||||
if isErr {
|
||||
t.Fatalf("%s", text)
|
||||
}
|
||||
@@ -108,7 +108,7 @@ func TestMailSendAskForManyRecipientsCountsTheRest(t *testing.T) {
|
||||
args := map[string]any{"account": "mesh",
|
||||
"to": []any{"a@example.org", "b@example.org", "c@example.org", "d@example.org", "e@example.org"},
|
||||
"subject": "Many recipients", "body": "body"}
|
||||
text, isErr := rt.call(SendTool, "novox.triage", args)
|
||||
text, isErr := rt.call(SendTool, "node-c.triage", args)
|
||||
if isErr {
|
||||
t.Fatalf("%s", text)
|
||||
}
|
||||
@@ -133,7 +133,7 @@ func TestMailSendAskMasksAnAddressAndALinkInTheSubject(t *testing.T) {
|
||||
args := map[string]any{"account": "mesh", "to": []any{"ops@example.org"},
|
||||
"subject": "Contact ops@example.org or see https://example.com/issue/42",
|
||||
"body": "See https://example.com/issue/42 and write to ops@example.org for details."}
|
||||
text, isErr := rt.call(SendTool, "novox.triage", args)
|
||||
text, isErr := rt.call(SendTool, "node-c.triage", args)
|
||||
if isErr {
|
||||
t.Fatalf("%s", text)
|
||||
}
|
||||
@@ -164,7 +164,7 @@ func TestMailSendAskFallsBackToDetailsWhenTheSubjectStillRefuses(t *testing.T) {
|
||||
b, router, _ := sendTestBundle(t, Server{Host: "127.0.0.1", Port: 25, Security: "tls"})
|
||||
rt := startBundle(t, b.Tools())
|
||||
args := map[string]any{"account": "mesh", "to": []any{"ops@example.org"}, "subject": subject, "body": "body"}
|
||||
text, isErr := rt.call(SendTool, "novox.triage", args)
|
||||
text, isErr := rt.call(SendTool, "node-c.triage", args)
|
||||
if isErr {
|
||||
t.Fatalf("%s", text)
|
||||
}
|
||||
@@ -198,9 +198,9 @@ func TestMailSendAskMatchesADisplayNameRecipientByItsAddress(t *testing.T) {
|
||||
b, router, _ := sendTestBundle(t, Server{Host: "127.0.0.1", Port: 25, Security: "tls"})
|
||||
rt := startBundle(t, b.Tools())
|
||||
|
||||
args := map[string]any{"account": "mesh", "to": []any{"Jochen Schoubben <jschoubben@novox.be>"},
|
||||
args := map[string]any{"account": "mesh", "to": []any{"Jochen Schoubben <jschoubben@node-c.be>"},
|
||||
"subject": "Testing a display name", "body": "body"}
|
||||
text, isErr := rt.call(SendTool, "novox.triage", args)
|
||||
text, isErr := rt.call(SendTool, "node-c.triage", args)
|
||||
if isErr {
|
||||
t.Fatalf("%s", text)
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ func claimTestSend(t *testing.T) (id string, router *sendRouter, sends *memSends
|
||||
b.Queue = queue.publish
|
||||
rt := startBundle(t, b.Tools())
|
||||
|
||||
text, isErr := rt.call(SendTool, "novox.triage", sendArgs)
|
||||
text, isErr := rt.call(SendTool, "node-c.triage", sendArgs)
|
||||
if isErr {
|
||||
t.Fatalf("%s", text)
|
||||
}
|
||||
|
||||
@@ -12,16 +12,16 @@ import (
|
||||
|
||||
// The first stage's account, as the operator sets the setting `accounts` (ADR 0359 §5): its address and its server,
|
||||
// never a credential; its password is the own secret mail-credential-mesh.
|
||||
const stageOne = `{"accounts": {"mesh": {"kind": "imap", "address": "jschoubben@novox.be",
|
||||
"incoming": {"host": "mail.novox.be", "port": 993, "security": "tls"}}}}`
|
||||
const stageOne = `{"accounts": {"mesh": {"kind": "imap", "address": "jschoubben@node-c.be",
|
||||
"incoming": {"host": "mail.node-c.be", "port": 993, "security": "tls"}}}}`
|
||||
|
||||
func TestTheFirstStagesAccountIsReadAndItsSecretNamed(t *testing.T) {
|
||||
got, err := ParseAccounts([]byte(stageOne))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(got) != 1 || got[0].Name != "mesh" || got[0].Kind != KindIMAP || got[0].SignInAs() != "jschoubben@novox.be" ||
|
||||
got[0].Incoming.Address() != "mail.novox.be:993" || got[0].Secret() != "mail-credential-mesh" || got[0].Outgoing != nil {
|
||||
if len(got) != 1 || got[0].Name != "mesh" || got[0].Kind != KindIMAP || got[0].SignInAs() != "jschoubben@node-c.be" ||
|
||||
got[0].Incoming.Address() != "mail.node-c.be:993" || got[0].Secret() != "mail-credential-mesh" || got[0].Outgoing != nil {
|
||||
t.Fatalf("%+v", got)
|
||||
}
|
||||
if p := secret.Path("/state", got[0].Secret()); p != "/state/mail-credential-mesh.secret" {
|
||||
@@ -34,7 +34,7 @@ func TestTheFirstStagesAccountIsReadAndItsSecretNamed(t *testing.T) {
|
||||
// uses to send through it (novox/hq ADR 0361 §1).
|
||||
func TestASecondIMAPAccountIsConfigurationOnly(t *testing.T) {
|
||||
got, err := ParseAccounts([]byte(`{"accounts": {
|
||||
"mesh": {"kind": "imap", "address": "jschoubben@novox.be", "incoming": {"host": "mail.novox.be", "port": 993, "security": "tls"}},
|
||||
"mesh": {"kind": "imap", "address": "jschoubben@node-c.be", "incoming": {"host": "mail.node-c.be", "port": 993, "security": "tls"}},
|
||||
"gmail": {"kind": "imap", "address": "someone@gmail.com", "incoming": {"host": "imap.gmail.com", "port": 993, "security": "tls"},
|
||||
"outgoing": {"host": "smtp.gmail.com", "port": 465, "security": "tls"}, "folders": ["[Gmail]/Sent Mail"]}}}`))
|
||||
if err != nil {
|
||||
@@ -68,23 +68,23 @@ func TestAnAccountThatCannotBeWatchedRefusesTheSetting(t *testing.T) {
|
||||
// Grants (ADR 0359 §4): nobody by default; a module's name grants it wherever it runs; a module on one machine, or a
|
||||
// person, by its full name; never the unproven, never by a wildcard, never one account for another.
|
||||
func TestGrantsAreByAccountAndByProvenCaller(t *testing.T) {
|
||||
g := Grants{"mesh": {"triage", "novox.desk", "person.jochen", "*"}}
|
||||
g := Grants{"mesh": {"triage", "node-c.desk", "person.jochen", "*"}}
|
||||
for caller, want := range map[string]bool{
|
||||
"novox.triage": true, "shanks.triage": true, "novox.desk": true, "g14.desk": false, "person.jochen": true,
|
||||
"person.triage": false, "novox.other": false, Unproven: false, "": false, "novox.*": false,
|
||||
"node-c.triage": true, "node-d.triage": true, "node-c.desk": true, "node-b.desk": false, "person.jochen": true,
|
||||
"person.triage": false, "node-c.other": false, Unproven: false, "": false, "node-c.*": false,
|
||||
} {
|
||||
if got := g.Allows(caller, "mesh"); got != want {
|
||||
t.Errorf("%s: %v", caller, got)
|
||||
}
|
||||
}
|
||||
if g.Allows("novox.triage", "gmail") || (Grants{}).Allows("novox.triage", "mesh") {
|
||||
if g.Allows("node-c.triage", "gmail") || (Grants{}).Allows("node-c.triage", "mesh") {
|
||||
t.Fatal("a grant reached an account it does not name")
|
||||
}
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "grants.json")
|
||||
_ = os.WriteFile(path, []byte(`{"grants": {"mesh": ["triage"]}}`), 0o600)
|
||||
read, err := ReadGrants(path)
|
||||
if err != nil || !read.Allows("novox.triage", "mesh") {
|
||||
if err != nil || !read.Allows("node-c.triage", "mesh") {
|
||||
t.Fatalf("%v %v", read, err)
|
||||
}
|
||||
_ = os.WriteFile(path, []byte(`{"grants": {"mesh": "triage"}}`), 0o600)
|
||||
|
||||
@@ -145,7 +145,7 @@ func randomRun(text string) (Refusal, bool) {
|
||||
}
|
||||
|
||||
// isPath: absolute, home-relative or dot-relative, or two separators deep. A mesh address names one
|
||||
// machine and one tool (`ace/postgres.query`) and has one; a ratio ("3/4") has digits only.
|
||||
// machine and one tool (`node-a/postgres.query`) and has one; a ratio ("3/4") has digits only.
|
||||
func isPath(w string) bool {
|
||||
if w == "" {
|
||||
return false
|
||||
@@ -162,7 +162,7 @@ func isPath(w string) bool {
|
||||
}
|
||||
|
||||
// isHostName: two names or more, the last a top-level one. A condition key's last name is its kind
|
||||
// (`machine.ace.silent`), which none of these is.
|
||||
// (`machine.node-a.silent`), which none of these is.
|
||||
func isHostName(w string) bool {
|
||||
w = strings.ToLower(w)
|
||||
if w == "localhost" {
|
||||
|
||||
@@ -81,7 +81,7 @@ func TestTheManifestSaysWhatTheCodeDoes(t *testing.T) {
|
||||
t.Fatalf("module %s, requires %v, contributes %v", m.Module, m.Requires, m.Contributes)
|
||||
}
|
||||
// mesh_<node>_<slug> fits the S3 access key's 20 characters on every node.
|
||||
if len("mesh_shanks_"+m.Slug) > 20 {
|
||||
if len("mesh_node-d_"+m.Slug) > 20 {
|
||||
t.Fatalf("the slug %q overflows an S3 access key", m.Slug)
|
||||
}
|
||||
fam := m.OwnSecrets[SecretFamily+"*"]
|
||||
@@ -304,7 +304,7 @@ func TestItsManifestDeclaresTheSenderProcess(t *testing.T) {
|
||||
t.Fatalf("send's bus: %+v", bus)
|
||||
}
|
||||
|
||||
// On novox the operator account's state directory is 0700, so mesh-mail-send cannot reach its own 0600
|
||||
// On node-c the operator account's state directory is 0700, so mesh-mail-send cannot reach its own 0600
|
||||
// files inside it: the state directory becomes 0711 (passable, not listable) and accounts.json (server
|
||||
// names and addresses, no password) becomes 0644, so the sender can read the outgoing server. No password
|
||||
// file's mode widens (the lead's decision, issue 1172).
|
||||
|
||||
@@ -52,7 +52,7 @@ func (m *memMirrorState) Keys() ([]string, error) {
|
||||
// accountsAndStore is one account "mesh", granted no caller (the Mirror reads no grant), with the index given.
|
||||
func accountsAndStore(t *testing.T) ([]Account, *memStore) {
|
||||
t.Helper()
|
||||
return []Account{{Name: "mesh", Kind: KindIMAP, Address: "jschoubben@novox.be"}}, newMemStore()
|
||||
return []Account{{Name: "mesh", Kind: KindIMAP, Address: "jschoubben@node-c.be"}}, newMemStore()
|
||||
}
|
||||
|
||||
func mirrorOf(accounts []Account, st *memStore, bucket MirrorState) *Mirror {
|
||||
@@ -147,7 +147,7 @@ func TestMirrorRoundWritesNothingWhenUnchanged(t *testing.T) {
|
||||
func TestMirrorRoundWritesOnlyTheChangedAccountsKey(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
accounts, st := accountsAndStore(t)
|
||||
accounts = append(accounts, Account{Name: "other", Kind: KindIMAP, Address: "other@novox.be"})
|
||||
accounts = append(accounts, Account{Name: "other", Kind: KindIMAP, Address: "other@node-c.be"})
|
||||
_ = st.SetState(ctx, AccountState{Account: "mesh", State: AccountCurrent})
|
||||
_ = st.SetState(ctx, AccountState{Account: "other", State: AccountCurrent})
|
||||
addMail(t, st, "mesh", "INBOX", 1, []string{})
|
||||
|
||||
@@ -31,7 +31,7 @@ func TestAnAttachmentIsKeptOnceByItsHash(t *testing.T) {
|
||||
kept := map[string][]byte{}
|
||||
puts := 0
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !strings.HasPrefix(r.Header.Get("Authorization"), "AWS4-HMAC-SHA256 Credential=mesh_novox_mail/") {
|
||||
if !strings.HasPrefix(r.Header.Get("Authorization"), "AWS4-HMAC-SHA256 Credential=mesh_node-c_mail/") {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
@@ -59,8 +59,8 @@ func TestAnAttachmentIsKeptOnceByItsHash(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "store.json")
|
||||
host, port, _ := strings.Cut(strings.TrimPrefix(srv.URL, "http://"), ":")
|
||||
writeFile(t, path, `{"scheme": "http", "at": "`+host+`", "port": "`+port+`", "bucket": "mesh-novox-mail", "region": "eu-west",
|
||||
"key": "mesh_novox_mail", "secret": "s3cr3t"}`)
|
||||
writeFile(t, path, `{"scheme": "http", "at": "`+host+`", "port": "`+port+`", "bucket": "mesh-node-c-mail", "region": "eu-west",
|
||||
"key": "mesh_node-c_mail", "secret": "s3cr3t"}`)
|
||||
facts, err := ReadStoreFacts(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -75,10 +75,10 @@ func TestAnAttachmentIsKeptOnceByItsHash(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if a.Key != b.Key || a.Bucket != "mesh-novox-mail" || puts != 1 || len(a.Key) != 64 {
|
||||
if a.Key != b.Key || a.Bucket != "mesh-node-c-mail" || puts != 1 || len(a.Key) != 64 {
|
||||
t.Fatalf("%+v %+v, %d put(s)", a, b, puts)
|
||||
}
|
||||
if _, ok := kept["/mesh-novox-mail/"+a.Key]; !ok {
|
||||
if _, ok := kept["/mesh-node-c-mail/"+a.Key]; !ok {
|
||||
t.Fatalf("kept %v", kept)
|
||||
}
|
||||
got, err := s3.Get(ctx, a.Key)
|
||||
|
||||
@@ -29,66 +29,66 @@ func searchFixtures() []Message {
|
||||
return []Message{
|
||||
// mesh/INBOX ------------------------------------------------------------------------------------------------
|
||||
{Account: "mesh", Folder: "INBOX", UID: 1, MessageID: "<invoice1@mesh>", Subject: "October invoice",
|
||||
Date: at(9, 0), From: []Person{{Name: "Bob", Address: "bob@example.org"}}, To: []Person{{Address: "jschoubben@novox.be"}},
|
||||
Date: at(9, 0), From: []Person{{Name: "Bob", Address: "bob@example.org"}}, To: []Person{{Address: "jschoubben@node-c.be"}},
|
||||
Flags: []string{}, Attachments: []AttachmentRef{{Bucket: "mail", Key: "a1", Size: 1200, Type: "application/pdf", Name: "invoice.pdf"}},
|
||||
Body: "please settle the invoice, due at month end", Size: 1500},
|
||||
{Account: "mesh", Folder: "INBOX", UID: 2, MessageID: "<renov1@mesh>", Subject: "renovatie plannen",
|
||||
Date: time.Date(2026, 9, 1, 8, 0, 0, 0, time.UTC), From: []Person{{Name: "Alice", Address: "alice@example.org"}},
|
||||
To: []Person{{Address: "bob@example.org"}}, Cc: []Person{{Address: "jschoubben@novox.be"}},
|
||||
To: []Person{{Address: "bob@example.org"}}, Cc: []Person{{Address: "jschoubben@node-c.be"}},
|
||||
Flags: []string{"\\Seen"}, Attachments: []AttachmentRef{}, Body: "over de renovatie van de keuken", Size: 900},
|
||||
{Account: "mesh", Folder: "INBOX", UID: 3, MessageID: "<renov2@mesh>", Subject: "renovatie herinnering",
|
||||
Date: time.Date(2026, 9, 2, 8, 0, 0, 0, time.UTC), From: []Person{{Name: "Alice", Address: "alice@example.org"}},
|
||||
To: []Person{{Address: "jschoubben@novox.be"}}, Flags: []string{"\\Seen"}, Attachments: []AttachmentRef{},
|
||||
To: []Person{{Address: "jschoubben@node-c.be"}}, Flags: []string{"\\Seen"}, Attachments: []AttachmentRef{},
|
||||
Body: "nog een vraag over de renovatie", Size: 800},
|
||||
{Account: "mesh", Folder: "INBOX", UID: 4, MessageID: "<sec1@mesh>", Subject: "security alert",
|
||||
Date: time.Date(2026, 10, 15, 7, 0, 0, 0, time.UTC), From: []Person{{Name: "Carol", Address: "carol@google.com"}},
|
||||
To: []Person{{Address: "jschoubben@novox.be"}}, Flags: []string{"\\Seen"}, Attachments: []AttachmentRef{},
|
||||
To: []Person{{Address: "jschoubben@node-c.be"}}, Flags: []string{"\\Seen"}, Attachments: []AttachmentRef{},
|
||||
Body: "someone logged into your account from a new device", Size: 600},
|
||||
{Account: "mesh", Folder: "INBOX", UID: 5, MessageID: "<cal1@mesh>", Subject: "team meeting invite",
|
||||
Date: at(8, 0), From: []Person{{Name: "Dave", Address: "dave@example.org"}}, To: []Person{{Address: "jschoubben@novox.be"}},
|
||||
Date: at(8, 0), From: []Person{{Name: "Dave", Address: "dave@example.org"}}, To: []Person{{Address: "jschoubben@node-c.be"}},
|
||||
Flags: []string{"\\Seen"}, Attachments: []AttachmentRef{{Bucket: "mail", Key: "a2", Size: 300, Type: "text/calendar", Name: "meeting.ics"}},
|
||||
Body: "please join the planning meeting", Size: 700},
|
||||
{Account: "mesh", Folder: "INBOX", UID: 6, MessageID: "<reply1@mesh>", InReplyTo: "<sent1@mesh>", Subject: "Re: quick note",
|
||||
Date: at(12, 30), From: []Person{{Name: "Bob", Address: "bob@example.org"}}, To: []Person{{Address: "jschoubben@novox.be"}},
|
||||
Date: at(12, 30), From: []Person{{Name: "Bob", Address: "bob@example.org"}}, To: []Person{{Address: "jschoubben@node-c.be"}},
|
||||
Flags: []string{}, Attachments: []AttachmentRef{}, Body: "sounds good, thanks for the note", Size: 400},
|
||||
// 1229/AC5: from a domain (google.com), has-attachment, name *.pdf and the word "factuur" — in the
|
||||
// subject only (review round 1 finding 3), so Words's "subject or body" is genuinely exercised, not
|
||||
// merely a body match that would pass even with a subject-only bug.
|
||||
{Account: "mesh", Folder: "INBOX", UID: 7, MessageID: "<factuur1@mesh>", Subject: "the factuur for Q3",
|
||||
Date: at(13, 0), From: []Person{{Name: "Erik", Address: "erik@google.com"}}, To: []Person{{Address: "jschoubben@novox.be"}},
|
||||
Date: at(13, 0), From: []Person{{Name: "Erik", Address: "erik@google.com"}}, To: []Person{{Address: "jschoubben@node-c.be"}},
|
||||
Flags: []string{}, Attachments: []AttachmentRef{{Bucket: "mail", Key: "a4", Size: 200, Type: "application/pdf", Name: "factuur.pdf"}},
|
||||
Body: "see the attached file, due on receipt", Size: 850},
|
||||
// 1229/AC12: a word with digits and a dash ("4711-882"), matched whole, in the subject only (review
|
||||
// round 1 finding 3).
|
||||
{Account: "mesh", Folder: "INBOX", UID: 8, MessageID: "<order1@mesh>", Subject: "order 4711-882 confirmed",
|
||||
Date: at(14, 0), From: []Person{{Address: "ops@example.org"}}, To: []Person{{Address: "jschoubben@novox.be"}},
|
||||
Date: at(14, 0), From: []Person{{Address: "ops@example.org"}}, To: []Person{{Address: "jschoubben@node-c.be"}},
|
||||
Flags: []string{}, Attachments: []AttachmentRef{}, Body: "your order has shipped", Size: 300},
|
||||
// Control for 1229/AC12: "4711" and "882" appear as separate tokens, never hyphenated together.
|
||||
{Account: "mesh", Folder: "INBOX", UID: 9, MessageID: "<order2@mesh>", Subject: "order 4711 shipped, item 882 noted",
|
||||
Date: at(14, 5), From: []Person{{Address: "ops@example.org"}}, To: []Person{{Address: "jschoubben@novox.be"}},
|
||||
Date: at(14, 5), From: []Person{{Address: "ops@example.org"}}, To: []Person{{Address: "jschoubben@node-c.be"}},
|
||||
Flags: []string{}, Attachments: []AttachmentRef{}, Body: "item 4711 and item 882 are unrelated", Size: 300},
|
||||
// 1229/AC15 (the "any" word list): only "verzekering" is present, not "schade" — in the subject only
|
||||
// (review round 1 finding 3) — with an attachment.
|
||||
{Account: "mesh", Folder: "INBOX", UID: 10, MessageID: "<polis1@mesh>", Subject: "verzekering aanvraag",
|
||||
Date: at(14, 10), From: []Person{{Address: "broker@example.org"}}, To: []Person{{Address: "jschoubben@novox.be"}},
|
||||
Date: at(14, 10), From: []Person{{Address: "broker@example.org"}}, To: []Person{{Address: "jschoubben@node-c.be"}},
|
||||
Flags: []string{}, Attachments: []AttachmentRef{{Bucket: "mail", Key: "a5", Size: 150, Type: "application/pdf", Name: "polis.pdf"}},
|
||||
Body: "nieuwe polis aanvraag", Size: 450},
|
||||
|
||||
// mesh/Sent ---------------------------------------------------------------------------------------------------
|
||||
{Account: "mesh", Folder: "Sent", UID: 1, MessageID: "<sentreply1@mesh>", InReplyTo: "<invoice1@mesh>", Subject: "Re: October invoice",
|
||||
Date: at(10, 0), From: []Person{{Address: "jschoubben@novox.be"}}, To: []Person{{Address: "bob@example.org"}},
|
||||
Date: at(10, 0), From: []Person{{Address: "jschoubben@node-c.be"}}, To: []Person{{Address: "bob@example.org"}},
|
||||
Flags: []string{"\\Seen"}, Attachments: []AttachmentRef{}, Body: "paid, thank you", Size: 300},
|
||||
{Account: "mesh", Folder: "Sent", UID: 2, MessageID: "<sent1@mesh>", Subject: "quick note",
|
||||
Date: time.Date(2026, 10, 18, 9, 0, 0, 0, time.UTC), From: []Person{{Address: "jschoubben@novox.be"}},
|
||||
Date: time.Date(2026, 10, 18, 9, 0, 0, 0, time.UTC), From: []Person{{Address: "jschoubben@node-c.be"}},
|
||||
To: []Person{{Address: "bob@example.org"}}, Flags: []string{"\\Seen"}, Attachments: []AttachmentRef{}, Body: "just checking in", Size: 250},
|
||||
|
||||
// mesh/Junk ---------------------------------------------------------------------------------------------------
|
||||
{Account: "mesh", Folder: "Junk", UID: 1, MessageID: "<junk1@mesh>", Subject: "special offer just for you",
|
||||
Date: time.Date(2026, 10, 10, 6, 0, 0, 0, time.UTC), From: []Person{{Address: "bob@example.org"}},
|
||||
To: []Person{{Address: "jschoubben@novox.be"}}, Flags: []string{}, Attachments: []AttachmentRef{}, Body: "buy now and save big", Size: 500},
|
||||
To: []Person{{Address: "jschoubben@node-c.be"}}, Flags: []string{}, Attachments: []AttachmentRef{}, Body: "buy now and save big", Size: 500},
|
||||
{Account: "mesh", Folder: "Junk", UID: 2, MessageID: "<junk2@mesh>", Subject: "you won the lottery",
|
||||
Date: time.Date(2026, 10, 10, 6, 5, 0, 0, time.UTC), From: []Person{{Address: "spam@unknown.com"}},
|
||||
To: []Person{{Address: "jschoubben@novox.be"}}, Flags: []string{}, Attachments: []AttachmentRef{}, Body: "claim your prize now", Size: 500},
|
||||
To: []Person{{Address: "jschoubben@node-c.be"}}, Flags: []string{}, Attachments: []AttachmentRef{}, Body: "claim your prize now", Size: 500},
|
||||
|
||||
// gmail/INBOX -------------------------------------------------------------------------------------------------
|
||||
{Account: "gmail", Folder: "INBOX", UID: 1, MessageID: "<gsec1@gmail>", Subject: "security alert for your account",
|
||||
@@ -134,7 +134,7 @@ func searchFixtures() []Message {
|
||||
// mesh/Junk (review round 1 finding 2): someone@example.org, written to only by gmail's Sent, not mesh's.
|
||||
{Account: "mesh", Folder: "Junk", UID: 3, MessageID: "<junk3@mesh>", Subject: "a gmail contact's junk mail",
|
||||
Date: time.Date(2026, 10, 11, 6, 0, 0, 0, time.UTC), From: []Person{{Address: "someone@example.org"}},
|
||||
To: []Person{{Address: "jschoubben@novox.be"}}, Flags: []string{}, Attachments: []AttachmentRef{}, Body: "unrelated offer", Size: 500},
|
||||
To: []Person{{Address: "jschoubben@node-c.be"}}, Flags: []string{}, Attachments: []AttachmentRef{}, Body: "unrelated offer", Size: 500},
|
||||
|
||||
// gmail/Junk (review round 1 finding 1): the same someone@example.org, but here it is gmail's own
|
||||
// account, whose "[Gmail]/Sent Mail" did write to this address — so this one must match.
|
||||
@@ -242,7 +242,7 @@ func TestSearchFiltersByCcNotTo(t *testing.T) {
|
||||
eachStore(t, func(t *testing.T, st Store) {
|
||||
ctx := context.Background()
|
||||
addFixtures(t, ctx, st)
|
||||
got, err := st.Search(ctx, Query{Accounts: []string{"mesh"}, Cc: "jschoubben@novox.be", Limit: 10})
|
||||
got, err := st.Search(ctx, Query{Accounts: []string{"mesh"}, Cc: "jschoubben@node-c.be", Limit: 10})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -280,7 +280,7 @@ func TestSearchFiltersByCcAndSubjectOrBodyWord(t *testing.T) {
|
||||
eachStore(t, func(t *testing.T, st Store) {
|
||||
ctx := context.Background()
|
||||
addFixtures(t, ctx, st)
|
||||
got, err := st.Search(ctx, Query{Accounts: []string{"mesh"}, Cc: "jschoubben@novox.be", Words: []string{"renovatie"}, Limit: 10})
|
||||
got, err := st.Search(ctx, Query{Accounts: []string{"mesh"}, Cc: "jschoubben@node-c.be", Words: []string{"renovatie"}, Limit: 10})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -502,7 +502,7 @@ func TestSearchOrdersBySenderAndSubject(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Both Sent messages share the same sender (jschoubben@novox.be): the tie-break (account, folder, uid)
|
||||
// Both Sent messages share the same sender (jschoubben@node-c.be): the tie-break (account, folder, uid)
|
||||
// orders them by uid ascending.
|
||||
sameOrder(t, bySender, "<sentreply1@mesh>", "<sent1@mesh>")
|
||||
|
||||
@@ -755,10 +755,10 @@ func TestSearchWithNoFolderAnswersTheSameMessageIDOncePerAccount(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
for _, m := range []Message{
|
||||
{Account: "mesh", Folder: "Sent", UID: 50, MessageID: "<cross1@shared>", Subject: "cross-account",
|
||||
Date: at1248(9, 0), From: []Person{{Address: "jschoubben@novox.be"}}, To: []Person{{Address: "me@gmail.example"}},
|
||||
Date: at1248(9, 0), From: []Person{{Address: "jschoubben@node-c.be"}}, To: []Person{{Address: "me@gmail.example"}},
|
||||
Flags: []string{}, Attachments: []AttachmentRef{}},
|
||||
{Account: "gmail", Folder: "INBOX", UID: 50, MessageID: "<cross1@shared>", Subject: "cross-account",
|
||||
Date: at1248(9, 1), From: []Person{{Address: "jschoubben@novox.be"}}, To: []Person{{Address: "me@gmail.example"}},
|
||||
Date: at1248(9, 1), From: []Person{{Address: "jschoubben@node-c.be"}}, To: []Person{{Address: "me@gmail.example"}},
|
||||
Flags: []string{}, Attachments: []AttachmentRef{}},
|
||||
} {
|
||||
if err := st.Add(ctx, m, nil); err != nil {
|
||||
|
||||
@@ -85,15 +85,15 @@ func TestTheToolPathSucceedsWithTheSecretsDirectoryAbsent(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
missing := filepath.Join(dir, "never-created")
|
||||
env := Env{Accounts: filepath.Join(dir, "accounts.json"), Grants: filepath.Join(dir, "grants.json"), Secrets: missing}
|
||||
writeFile(t, env.Accounts, `{"accounts": {"mesh": {"kind": "imap", "address": "jschoubben@novox.be",
|
||||
"incoming": {"host": "mail.novox.be", "port": 993, "security": "tls"},
|
||||
writeFile(t, env.Accounts, `{"accounts": {"mesh": {"kind": "imap", "address": "jschoubben@node-c.be",
|
||||
"incoming": {"host": "mail.node-c.be", "port": 993, "security": "tls"},
|
||||
"outgoing": {"host": "h", "port": 465, "security": "tls"}}}}`)
|
||||
writeFile(t, env.Grants, `{"grants": {"mesh": ["triage"]}}`)
|
||||
router, sends, queue := newSendRouter(), newMemSends(), newMemQueue()
|
||||
b := &Bundle{Env: env, Now: func() time.Time { return judgedAt }, Asks: router, Sends: sends, Queue: queue.publish}
|
||||
rt := startBundle(t, b.Tools())
|
||||
|
||||
text, isErr := rt.call(SendTool, "novox.triage", sendArgs)
|
||||
text, isErr := rt.call(SendTool, "node-c.triage", sendArgs)
|
||||
if isErr {
|
||||
t.Fatalf("mail_send: %s", text)
|
||||
}
|
||||
@@ -114,7 +114,7 @@ func TestTheToolPathSucceedsWithTheSecretsDirectoryAbsent(t *testing.T) {
|
||||
t.Fatalf("the decision names a secret file: %q", d.Outcome)
|
||||
}
|
||||
|
||||
text, isErr = rt.call(SendStatusTool, "novox.triage", map[string]any{"id": id})
|
||||
text, isErr = rt.call(SendStatusTool, "node-c.triage", map[string]any{"id": id})
|
||||
if isErr {
|
||||
t.Fatalf("mail_send_status: %s", text)
|
||||
}
|
||||
|
||||
@@ -349,8 +349,8 @@ func sendTestBundle(t *testing.T, outgoing Server) (*Bundle, *sendRouter, *memSe
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
env := Env{Accounts: filepath.Join(dir, "accounts.json"), Grants: filepath.Join(dir, "grants.json"), Secrets: dir}
|
||||
writeFile(t, env.Accounts, fmt.Sprintf(`{"accounts": {"mesh": {"kind": "imap", "address": "jschoubben@novox.be",
|
||||
"incoming": {"host": "mail.novox.be", "port": 993, "security": "tls"},
|
||||
writeFile(t, env.Accounts, fmt.Sprintf(`{"accounts": {"mesh": {"kind": "imap", "address": "jschoubben@node-c.be",
|
||||
"incoming": {"host": "mail.node-c.be", "port": 993, "security": "tls"},
|
||||
"outgoing": {"host": %q, "port": %d, "security": %q}}}}`, outgoing.Host, outgoing.Port, outgoing.Security))
|
||||
writeFile(t, env.Grants, `{"grants": {"mesh": ["triage"]}}`)
|
||||
giveSecret(t, dir, Account{Name: "mesh"}, "the-password")
|
||||
@@ -366,7 +366,7 @@ var sendArgs = map[string]any{"account": "mesh", "to": []any{"ops@example.org",
|
||||
func TestMailSendRefusesAnUngrantedCallerAndRaisesNoAsk(t *testing.T) {
|
||||
b, router, _ := sendTestBundle(t, Server{Host: "127.0.0.1", Port: 25, Security: "tls"})
|
||||
rt := startBundle(t, b.Tools())
|
||||
text, isErr := rt.call(SendTool, "novox.other", sendArgs)
|
||||
text, isErr := rt.call(SendTool, "node-c.other", sendArgs)
|
||||
if !isErr || !strings.Contains(text, "refused") || !strings.Contains(text, "mesh") {
|
||||
t.Fatalf("%v %s", isErr, text)
|
||||
}
|
||||
@@ -381,7 +381,7 @@ func TestMailSendRefusesAnUngrantedCallerAndRaisesNoAsk(t *testing.T) {
|
||||
func TestMailSendAsksWithTheAccountRecipientsSubjectAndFirstLines(t *testing.T) {
|
||||
b, router, _ := sendTestBundle(t, Server{Host: "127.0.0.1", Port: 25, Security: "tls"})
|
||||
rt := startBundle(t, b.Tools())
|
||||
text, isErr := rt.call(SendTool, "novox.triage", sendArgs)
|
||||
text, isErr := rt.call(SendTool, "node-c.triage", sendArgs)
|
||||
if isErr {
|
||||
t.Fatalf("%s", text)
|
||||
}
|
||||
@@ -425,7 +425,7 @@ func TestOnApproveTheSendIsQueuedExactlyOnceEvenDeliveredTwice(t *testing.T) {
|
||||
b.Queue = queue.publish
|
||||
rt := startBundle(t, b.Tools())
|
||||
|
||||
text, isErr := rt.call(SendTool, "novox.triage", sendArgs)
|
||||
text, isErr := rt.call(SendTool, "node-c.triage", sendArgs)
|
||||
if isErr {
|
||||
t.Fatalf("%s", text)
|
||||
}
|
||||
@@ -457,7 +457,7 @@ func TestOnApproveTheSendIsQueuedExactlyOnceEvenDeliveredTwice(t *testing.T) {
|
||||
|
||||
// mail_send_status reports it waiting on the sender, and reports the same thing again without queueing a
|
||||
// third time.
|
||||
text, isErr = rt.call(SendStatusTool, "novox.triage", map[string]any{"id": id})
|
||||
text, isErr = rt.call(SendStatusTool, "node-c.triage", map[string]any{"id": id})
|
||||
if isErr || !strings.Contains(text, `"status":"waiting"`) {
|
||||
t.Fatalf("%v %s", isErr, text)
|
||||
}
|
||||
@@ -473,7 +473,7 @@ func TestOnDeclineNothingIsQueuedAndTheStatusSaysDeclined(t *testing.T) {
|
||||
b.Queue = queue.publish
|
||||
rt := startBundle(t, b.Tools())
|
||||
|
||||
text, isErr := rt.call(SendTool, "novox.triage", sendArgs)
|
||||
text, isErr := rt.call(SendTool, "node-c.triage", sendArgs)
|
||||
if isErr {
|
||||
t.Fatalf("%s", text)
|
||||
}
|
||||
@@ -490,7 +490,7 @@ func TestOnDeclineNothingIsQueuedAndTheStatusSaysDeclined(t *testing.T) {
|
||||
t.Fatalf("the queue received %d message(s) on Decline", n)
|
||||
}
|
||||
|
||||
text, isErr = rt.call(SendStatusTool, "novox.triage", map[string]any{"id": id})
|
||||
text, isErr = rt.call(SendStatusTool, "node-c.triage", map[string]any{"id": id})
|
||||
if isErr || !strings.Contains(text, `"status":"declined"`) {
|
||||
t.Fatalf("%v %s", isErr, text)
|
||||
}
|
||||
@@ -505,7 +505,7 @@ func TestMailSendRefusesAnAccountWithNoOutgoingServer(t *testing.T) {
|
||||
router := newSendRouter()
|
||||
b := &Bundle{Env: env, Now: func() time.Time { return judgedAt }, Asks: router, Sends: newMemSends()}
|
||||
rt := startBundle(t, b.Tools())
|
||||
text, isErr := rt.call(SendTool, "novox.triage", sendArgs)
|
||||
text, isErr := rt.call(SendTool, "node-c.triage", sendArgs)
|
||||
if !isErr || !strings.Contains(text, "no outgoing server") {
|
||||
t.Fatalf("%v %s", isErr, text)
|
||||
}
|
||||
@@ -522,7 +522,7 @@ func TestAnApprovedSendIsQueuedOnceWithNoStatusCall(t *testing.T) {
|
||||
b.Queue = queue.publish
|
||||
rt := startBundle(t, b.Tools())
|
||||
|
||||
text, isErr := rt.call(SendTool, "novox.triage", sendArgs)
|
||||
text, isErr := rt.call(SendTool, "node-c.triage", sendArgs)
|
||||
if isErr {
|
||||
t.Fatalf("%s", text)
|
||||
}
|
||||
@@ -551,7 +551,7 @@ func TestSweepActsOnAnApprovedSendItWasNotToldAboutDirectly(t *testing.T) {
|
||||
b.Queue = queue.publish
|
||||
rt := startBundle(t, b.Tools())
|
||||
|
||||
text, isErr := rt.call(SendTool, "novox.triage", sendArgs)
|
||||
text, isErr := rt.call(SendTool, "node-c.triage", sendArgs)
|
||||
if isErr {
|
||||
t.Fatalf("%s", text)
|
||||
}
|
||||
@@ -585,7 +585,7 @@ func TestSweepPublishesAgainAQueuedRecordPastItsGrace(t *testing.T) {
|
||||
b.Queue = queue.publish
|
||||
rt := startBundle(t, b.Tools())
|
||||
|
||||
text, isErr := rt.call(SendTool, "novox.triage", sendArgs)
|
||||
text, isErr := rt.call(SendTool, "node-c.triage", sendArgs)
|
||||
if isErr {
|
||||
t.Fatalf("%s", text)
|
||||
}
|
||||
@@ -632,7 +632,7 @@ func TestAStaleSendingClaimIsNotProvenNotWaiting(t *testing.T) {
|
||||
b, _, sends := sendTestBundle(t, Server{Host: "127.0.0.1", Port: 25, Security: "tls"})
|
||||
rt := startBundle(t, b.Tools())
|
||||
|
||||
text, isErr := rt.call(SendTool, "novox.triage", sendArgs)
|
||||
text, isErr := rt.call(SendTool, "node-c.triage", sendArgs)
|
||||
if isErr {
|
||||
t.Fatalf("%s", text)
|
||||
}
|
||||
@@ -650,7 +650,7 @@ func TestAStaleSendingClaimIsNotProvenNotWaiting(t *testing.T) {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
|
||||
text, isErr = rt.call(SendStatusTool, "novox.triage", map[string]any{"id": id})
|
||||
text, isErr = rt.call(SendStatusTool, "node-c.triage", map[string]any{"id": id})
|
||||
if isErr {
|
||||
t.Fatalf("%s", text)
|
||||
}
|
||||
@@ -670,14 +670,14 @@ func TestMailSendRefusesAControlCharacterInTheSubjectOrAnAddress(t *testing.T) {
|
||||
|
||||
bySubject := map[string]any{"account": "mesh", "to": []any{"ops@example.org"},
|
||||
"subject": "Report\r\nBcc: attacker@example.org", "body": "body"}
|
||||
text, isErr := rt.call(SendTool, "novox.triage", bySubject)
|
||||
text, isErr := rt.call(SendTool, "node-c.triage", bySubject)
|
||||
if !isErr || !strings.Contains(text, "control character") {
|
||||
t.Fatalf("a subject with a control character was not refused: %v %s", isErr, text)
|
||||
}
|
||||
|
||||
byAddress := map[string]any{"account": "mesh", "to": []any{"ops@example.org\r\nBcc: attacker@example.org"},
|
||||
"subject": "Report", "body": "body"}
|
||||
text, isErr = rt.call(SendTool, "novox.triage", byAddress)
|
||||
text, isErr = rt.call(SendTool, "node-c.triage", byAddress)
|
||||
if !isErr || !strings.Contains(text, "control character") {
|
||||
t.Fatalf("an address with a control character was not refused: %v %s", isErr, text)
|
||||
}
|
||||
@@ -689,7 +689,7 @@ func TestMailSendRefusesAControlCharacterInTheSubjectOrAnAddress(t *testing.T) {
|
||||
// is where SendMail is actually called from.
|
||||
func TestSendUsesTheParsedAddressForRcptAndTheFormattedAddressForTheToHeader(t *testing.T) {
|
||||
fake := newFakeSMTPServer(t)
|
||||
acc := Account{Name: "mesh", Address: "jschoubben@novox.be", Outgoing: &Server{Host: "127.0.0.1", Port: 25, Security: "tls"}}
|
||||
acc := Account{Name: "mesh", Address: "jschoubben@node-c.be", Outgoing: &Server{Host: "127.0.0.1", Port: 25, Security: "tls"}}
|
||||
if err := SendMail(fake.dialer(), acc, "the-password", []string{"Ops Team <ops@example.org>"}, "Status", "body", judgedAt, "send-abc123"); err != nil {
|
||||
t.Fatalf("SendMail: %v", err)
|
||||
}
|
||||
@@ -710,7 +710,7 @@ func TestASendWhoseAskNeverPublishedEndsAfterTheAskWouldHaveLasted(t *testing.T)
|
||||
router.publishErr = errors.New("the bus is down")
|
||||
rt := startBundle(t, b.Tools())
|
||||
|
||||
_, isErr := rt.call(SendTool, "novox.triage", sendArgs)
|
||||
_, isErr := rt.call(SendTool, "node-c.triage", sendArgs)
|
||||
if !isErr {
|
||||
t.Fatalf("mail_send did not report the publish failure")
|
||||
}
|
||||
@@ -731,7 +731,7 @@ func TestASendWhoseAskNeverPublishedEndsAfterTheAskWouldHaveLasted(t *testing.T)
|
||||
}
|
||||
|
||||
// Before the ask would have lasted, it still reads waiting: the router may yet come up and record it.
|
||||
text, isErr := rt.call(SendStatusTool, "novox.triage", map[string]any{"id": id})
|
||||
text, isErr := rt.call(SendStatusTool, "node-c.triage", map[string]any{"id": id})
|
||||
if isErr || !strings.Contains(text, `"status":"waiting"`) {
|
||||
t.Fatalf("before expiry: %v %s", isErr, text)
|
||||
}
|
||||
@@ -739,7 +739,7 @@ func TestASendWhoseAskNeverPublishedEndsAfterTheAskWouldHaveLasted(t *testing.T)
|
||||
// Once a sweep runs after the ask would have lasted, it settles as ended, and nothing was sent.
|
||||
b.Sweep(judgedAt.Add(SendAskLasts + time.Minute))
|
||||
|
||||
text, isErr = rt.call(SendStatusTool, "novox.triage", map[string]any{"id": id})
|
||||
text, isErr = rt.call(SendStatusTool, "node-c.triage", map[string]any{"id": id})
|
||||
if isErr {
|
||||
t.Fatalf("%s", text)
|
||||
}
|
||||
@@ -758,7 +758,7 @@ func TestSweepReadsOnlyUndecidedSends(t *testing.T) {
|
||||
rt := startBundle(t, b.Tools())
|
||||
|
||||
// One send, declined right away: decided, so it leaves the undecided index.
|
||||
text, isErr := rt.call(SendTool, "novox.triage", sendArgs)
|
||||
text, isErr := rt.call(SendTool, "node-c.triage", sendArgs)
|
||||
if isErr {
|
||||
t.Fatalf("%s", text)
|
||||
}
|
||||
@@ -772,7 +772,7 @@ func TestSweepReadsOnlyUndecidedSends(t *testing.T) {
|
||||
}
|
||||
|
||||
// A second send, left undecided: still waiting on the operator.
|
||||
text, isErr = rt.call(SendTool, "novox.triage", sendArgs)
|
||||
text, isErr = rt.call(SendTool, "node-c.triage", sendArgs)
|
||||
if isErr {
|
||||
t.Fatalf("%s", text)
|
||||
}
|
||||
@@ -798,7 +798,7 @@ func TestMailSendRefusesWhenTheUndecidedIndexCannotBeWritten(t *testing.T) {
|
||||
sends.failKey, sends.failErr = undecidedIndexKey, errors.New("the store is down")
|
||||
rt := startBundle(t, b.Tools())
|
||||
|
||||
text, isErr := rt.call(SendTool, "novox.triage", sendArgs)
|
||||
text, isErr := rt.call(SendTool, "node-c.triage", sendArgs)
|
||||
if !isErr {
|
||||
t.Fatalf("mail_send did not report the undecided index failing to write: %s", text)
|
||||
}
|
||||
@@ -838,7 +838,7 @@ func TestADigestMismatchIsRefusedAndNothingIsQueued(t *testing.T) {
|
||||
b.Queue = queue.publish
|
||||
rt := startBundle(t, b.Tools())
|
||||
|
||||
text, isErr := rt.call(SendTool, "novox.triage", sendArgs)
|
||||
text, isErr := rt.call(SendTool, "node-c.triage", sendArgs)
|
||||
if isErr {
|
||||
t.Fatalf("%s", text)
|
||||
}
|
||||
@@ -870,7 +870,7 @@ func TestSendStatusAnswersTheCallersOwnSendInFull(t *testing.T) {
|
||||
b, _, _ := sendTestBundle(t, Server{Host: "127.0.0.1", Port: 25, Security: "tls"})
|
||||
rt := startBundle(t, b.Tools())
|
||||
|
||||
text, isErr := rt.call(SendTool, "novox.triage", sendArgs)
|
||||
text, isErr := rt.call(SendTool, "node-c.triage", sendArgs)
|
||||
if isErr {
|
||||
t.Fatalf("%s", text)
|
||||
}
|
||||
@@ -878,7 +878,7 @@ func TestSendStatusAnswersTheCallersOwnSendInFull(t *testing.T) {
|
||||
_ = json.Unmarshal([]byte(text), &out)
|
||||
id := out["ask"].(string)
|
||||
|
||||
text, isErr = rt.call(SendStatusTool, "novox.triage", map[string]any{"id": id})
|
||||
text, isErr = rt.call(SendStatusTool, "node-c.triage", map[string]any{"id": id})
|
||||
if isErr {
|
||||
t.Fatalf("%s", text)
|
||||
}
|
||||
@@ -901,7 +901,7 @@ func TestSendStatusAnswersUnknownAndNothingElseToAnotherCaller(t *testing.T) {
|
||||
b, _, _ := sendTestBundle(t, Server{Host: "127.0.0.1", Port: 25, Security: "tls"})
|
||||
rt := startBundle(t, b.Tools())
|
||||
|
||||
text, isErr := rt.call(SendTool, "novox.triage", sendArgs)
|
||||
text, isErr := rt.call(SendTool, "node-c.triage", sendArgs)
|
||||
if isErr {
|
||||
t.Fatalf("%s", text)
|
||||
}
|
||||
@@ -909,7 +909,7 @@ func TestSendStatusAnswersUnknownAndNothingElseToAnotherCaller(t *testing.T) {
|
||||
_ = json.Unmarshal([]byte(text), &out)
|
||||
id := out["ask"].(string)
|
||||
|
||||
text, isErr = rt.call(SendStatusTool, "novox.someone-else", map[string]any{"id": id})
|
||||
text, isErr = rt.call(SendStatusTool, "node-c.someone-else", map[string]any{"id": id})
|
||||
if isErr {
|
||||
t.Fatalf("another caller's read of the send was an error: %s", text)
|
||||
}
|
||||
@@ -927,7 +927,7 @@ func TestSendStatusAnswersUnknownForAnUnknownIDWithoutAnError(t *testing.T) {
|
||||
b, _, _ := sendTestBundle(t, Server{Host: "127.0.0.1", Port: 25, Security: "tls"})
|
||||
rt := startBundle(t, b.Tools())
|
||||
|
||||
text, isErr := rt.call(SendStatusTool, "novox.triage", map[string]any{"id": "send-0000000000000000"})
|
||||
text, isErr := rt.call(SendStatusTool, "node-c.triage", map[string]any{"id": "send-0000000000000000"})
|
||||
if isErr {
|
||||
t.Fatalf("an unknown id was an error: %s", text)
|
||||
}
|
||||
@@ -944,7 +944,7 @@ func TestSendStatusAnswersUnknownForAnUnknownIDWithoutAnError(t *testing.T) {
|
||||
// not proven, declined, expired), answered to the send's own caller.
|
||||
func TestSendStatusWordsAreTheFixedVocabularyForTheCallersOwnSend(t *testing.T) {
|
||||
sends := newMemSends()
|
||||
const caller = "novox.triage"
|
||||
const caller = "node-c.triage"
|
||||
cases := []struct {
|
||||
name, outcome, want string
|
||||
}{
|
||||
@@ -981,7 +981,7 @@ func TestSeatSendStatusAnswersExactlyAsMailSendStatus(t *testing.T) {
|
||||
b, _, _ := sendTestBundle(t, Server{Host: "127.0.0.1", Port: 25, Security: "tls"})
|
||||
rt := startBundle(t, b.Tools())
|
||||
|
||||
text, isErr := rt.call(SendTool, "novox.triage", sendArgs)
|
||||
text, isErr := rt.call(SendTool, "node-c.triage", sendArgs)
|
||||
if isErr {
|
||||
t.Fatalf("%s", text)
|
||||
}
|
||||
@@ -989,8 +989,8 @@ func TestSeatSendStatusAnswersExactlyAsMailSendStatus(t *testing.T) {
|
||||
_ = json.Unmarshal([]byte(text), &out)
|
||||
id := out["ask"].(string)
|
||||
|
||||
toolText, toolErr := rt.call(SendStatusTool, "novox.triage", map[string]any{"id": id})
|
||||
seatText, seatErr := rt.call(SeatSendStatusTool, "novox.triage", map[string]any{"id": id})
|
||||
toolText, toolErr := rt.call(SendStatusTool, "node-c.triage", map[string]any{"id": id})
|
||||
seatText, seatErr := rt.call(SeatSendStatusTool, "node-c.triage", map[string]any{"id": id})
|
||||
if toolErr != seatErr || toolText != seatText {
|
||||
t.Fatalf("mail_send_status: %v %s; mail-store.send-status: %v %s", toolErr, toolText, seatErr, seatText)
|
||||
}
|
||||
@@ -1004,7 +1004,7 @@ func TestSweepSettlesASendRegardlessOfCaller(t *testing.T) {
|
||||
b.Queue = queue.publish
|
||||
rt := startBundle(t, b.Tools())
|
||||
|
||||
text, isErr := rt.call(SendTool, "novox.triage", sendArgs)
|
||||
text, isErr := rt.call(SendTool, "node-c.triage", sendArgs)
|
||||
if isErr {
|
||||
t.Fatalf("%s", text)
|
||||
}
|
||||
@@ -1015,7 +1015,7 @@ func TestSweepSettlesASendRegardlessOfCaller(t *testing.T) {
|
||||
w := router.chose(id, OptionSend, judgedAt.Add(time.Minute))
|
||||
router.close(w) // only the router's record holds the answer
|
||||
|
||||
// The sweep itself names no caller at all; it must still settle "novox.triage"'s send.
|
||||
// The sweep itself names no caller at all; it must still settle "node-c.triage"'s send.
|
||||
b.Sweep(judgedAt.Add(2 * time.Minute))
|
||||
if n := queue.count(); n != 1 {
|
||||
t.Fatalf("the sweep did not settle the send regardless of caller: %d message(s)", n)
|
||||
|
||||
@@ -90,7 +90,7 @@ func TestSendConsumesMailSendsAndStopsCleanlyOnSignal(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
account := process.Account{URL: url, Node: "g14", Module: Module, User: "g14." + Module + "." + SendProcessID, Password: "pw"}
|
||||
account := process.Account{URL: url, Node: "node-b", Module: Module, User: "node-b." + Module + "." + SendProcessID, Password: "pw"}
|
||||
if _, err := js.CreateConsumer(ctx, MailSendsStream, mailSendsConsumerConfig(account.QueueConsumerName())); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ import (
|
||||
// form, when it starts, then it reads the account and the accounts file, and refuses to start naming what
|
||||
// is missing when either is absent.
|
||||
func TestSendRefusesToStartNamingWhatIsMissing(t *testing.T) {
|
||||
validAccount := `{"url": "nats://127.0.0.1:4222", "node": "g14", "module": "mesh-mail", ` +
|
||||
`"user": "g14.mesh-mail.send", "password": "pw"}`
|
||||
validAccount := `{"url": "nats://127.0.0.1:4222", "node": "node-b", "module": "mesh-mail", ` +
|
||||
`"user": "node-b.mesh-mail.send", "password": "pw"}`
|
||||
|
||||
t.Run("no secrets directory", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
@@ -57,7 +57,7 @@ func TestSendRefusesToStartNamingWhatIsMissing(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
env := Env{Accounts: filepath.Join(dir, "accounts.json"), Secrets: dir}
|
||||
writeFile(t, env.Accounts, stageOne)
|
||||
writeFile(t, filepath.Join(dir, "broker-send.secret"), `{"url": "nats://127.0.0.1:4222", "node": "g14", "module": "mesh-mail"}`)
|
||||
writeFile(t, filepath.Join(dir, "broker-send.secret"), `{"url": "nats://127.0.0.1:4222", "node": "node-b", "module": "mesh-mail"}`)
|
||||
code := send(env)
|
||||
if code == 0 {
|
||||
t.Fatal("send started with an own account missing its user and password")
|
||||
|
||||
@@ -139,7 +139,7 @@ func classifyTestAccount(addr string) Account {
|
||||
host, portStr, _ := net.SplitHostPort(addr)
|
||||
var port int
|
||||
_, _ = fmt.Sscanf(portStr, "%d", &port)
|
||||
return Account{Name: "mesh", Address: "jschoubben@novox.be", Outgoing: &Server{Host: host, Port: port, Security: "tls"}}
|
||||
return Account{Name: "mesh", Address: "jschoubben@node-c.be", Outgoing: &Server{Host: host, Port: port, Security: "tls"}}
|
||||
}
|
||||
|
||||
func outcomeOf(t *testing.T, err error) SendOutcome {
|
||||
@@ -234,8 +234,8 @@ func TestSendMailSaysNotProvenWhenTheConnectionIsLostAfterTheDataWasWritten(t *t
|
||||
// (<send id>@<the account's domain>). Proved both in isolation (composeMessage) and over the wire
|
||||
// (SendMail, with the existing fakeSMTPServer, which keeps the body it received).
|
||||
func TestComposeMessageCarriesTheSendIDInItsMessageID(t *testing.T) {
|
||||
raw := composeMessage("jschoubben@novox.be", []string{"a@b.c"}, "s", "body", judgedAt, "send-abc123")
|
||||
if !strings.Contains(string(raw), "Message-Id: <send-abc123@novox.be>") {
|
||||
raw := composeMessage("jschoubben@node-c.be", []string{"a@b.c"}, "s", "body", judgedAt, "send-abc123")
|
||||
if !strings.Contains(string(raw), "Message-Id: <send-abc123@node-c.be>") {
|
||||
t.Fatalf("the message does not carry the send id's Message-ID:\n%s", raw)
|
||||
}
|
||||
}
|
||||
@@ -256,7 +256,7 @@ func TestSendMailCarriesTheMessageIDOverTheWire(t *testing.T) {
|
||||
t.Fatalf("SendMail: %v", err)
|
||||
}
|
||||
got := fake.last()
|
||||
if !strings.Contains(got.data, "Message-Id: <send-xyz@novox.be>") {
|
||||
if !strings.Contains(got.data, "Message-Id: <send-xyz@node-c.be>") {
|
||||
t.Fatalf("the message sent does not carry the send id's Message-ID:\n%s", got.data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ func aBundle(t *testing.T) (*Bundle, *memStore, *memObjects) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m := Message{Account: "mesh", Folder: "INBOX", UID: 7, UIDValidity: 1, MessageID: "att@example.org", Subject: "the invoice",
|
||||
Date: judgedAt, From: []Person{{Name: "Bob", Address: "bob@example.org"}}, To: []Person{{Address: "jschoubben@novox.be"}},
|
||||
Date: judgedAt, From: []Person{{Name: "Bob", Address: "bob@example.org"}}, To: []Person{{Address: "jschoubben@node-c.be"}},
|
||||
Flags: []string{}, Attachments: []AttachmentRef{ref}, Body: "Please find the invoice attached."}
|
||||
if err := st.Add(ctx, m, addedEvent(m)); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -151,11 +151,11 @@ func TestTheBodyToolsAnswerAGrantedCallerAndRefuseAnyOther(t *testing.T) {
|
||||
rt := startBundle(t, b.Tools())
|
||||
read := map[string]any{"account": "mesh", "folder": "INBOX", "uid": 7}
|
||||
|
||||
text, isErr := rt.call(ReadTool, "novox.triage", read)
|
||||
text, isErr := rt.call(ReadTool, "node-c.triage", read)
|
||||
if isErr || !strings.Contains(text, "Please find the invoice attached.") || !strings.Contains(text, `"subject":"the invoice"`) {
|
||||
t.Fatalf("granted: %v %s", isErr, text)
|
||||
}
|
||||
for _, caller := range []string{"novox.other", "person.someone", Unproven} {
|
||||
for _, caller := range []string{"node-c.other", "person.someone", Unproven} {
|
||||
text, isErr := rt.call(ReadTool, caller, read)
|
||||
if !isErr || !strings.Contains(text, "refused") || strings.Contains(text, "Please find") {
|
||||
t.Fatalf("%s: %v %s", caller, isErr, text)
|
||||
@@ -166,11 +166,11 @@ func TestTheBodyToolsAnswerAGrantedCallerAndRefuseAnyOther(t *testing.T) {
|
||||
}
|
||||
}
|
||||
// An argument cannot name the caller.
|
||||
if text, isErr := rt.call(ReadTool, "novox.other", map[string]any{"account": "mesh", "folder": "INBOX", "uid": 7, "caller": "novox.triage"}); !isErr || strings.Contains(text, "Please find") {
|
||||
if text, isErr := rt.call(ReadTool, "node-c.other", map[string]any{"account": "mesh", "folder": "INBOX", "uid": 7, "caller": "node-c.triage"}); !isErr || strings.Contains(text, "Please find") {
|
||||
t.Fatalf("an argument named the caller: %s", text)
|
||||
}
|
||||
|
||||
text, isErr = rt.call(SearchTool, "shanks.triage", map[string]any{"query": "invoice"})
|
||||
text, isErr = rt.call(SearchTool, "node-d.triage", map[string]any{"query": "invoice"})
|
||||
if isErr || !strings.Contains(text, `"message-id":"att@example.org"`) || strings.Contains(text, "Please find") {
|
||||
t.Fatalf("search: %v %s", isErr, text)
|
||||
}
|
||||
@@ -180,7 +180,7 @@ func TestTheBodyToolsAnswerAGrantedCallerAndRefuseAnyOther(t *testing.T) {
|
||||
got, _, _ := m.Get(context.Background(), "mesh", "INBOX", 7)
|
||||
sum = got.Attachments[0].Key
|
||||
}
|
||||
text, isErr = rt.call(ReadTool, "novox.triage", map[string]any{"account": "mesh", "folder": "INBOX", "uid": 7, "attachment": sum})
|
||||
text, isErr = rt.call(ReadTool, "node-c.triage", map[string]any{"account": "mesh", "folder": "INBOX", "uid": 7, "attachment": sum})
|
||||
var att struct {
|
||||
Base64 string `json:"base64"`
|
||||
}
|
||||
@@ -200,7 +200,7 @@ func TestTheAccountsToolListsOnlyTheAccountsTheCallerIsGranted(t *testing.T) {
|
||||
st, _ := b.Index(context.Background())
|
||||
_ = st.SetFolder(context.Background(), Folder{Account: "mesh", Name: "Secret-Project", Messages: 41, Synced: judgedAt})
|
||||
_ = st.SetState(context.Background(), AccountState{Account: "mesh", State: AccountFailing, Why: "the server said NO [AUTHENTICATIONFAILED]", Said: judgedAt})
|
||||
text, isErr := rt.call(AccountsTool, "novox.other", map[string]any{})
|
||||
text, isErr := rt.call(AccountsTool, "node-c.other", map[string]any{})
|
||||
if isErr || !strings.Contains(text, `"accounts":[]`) {
|
||||
t.Fatalf("ungranted: %s", text)
|
||||
}
|
||||
@@ -209,8 +209,8 @@ func TestTheAccountsToolListsOnlyTheAccountsTheCallerIsGranted(t *testing.T) {
|
||||
t.Fatalf("an ungranted caller learns %q: %s", leak, text)
|
||||
}
|
||||
}
|
||||
text, _ = rt.call(AccountsTool, "novox.triage", map[string]any{})
|
||||
if !strings.Contains(text, `"address":"jschoubben@novox.be"`) || !strings.Contains(text, "Secret-Project") || !strings.Contains(text, "AUTHENTICATIONFAILED") {
|
||||
text, _ = rt.call(AccountsTool, "node-c.triage", map[string]any{})
|
||||
if !strings.Contains(text, `"address":"jschoubben@node-c.be"`) || !strings.Contains(text, "Secret-Project") || !strings.Contains(text, "AUTHENTICATIONFAILED") {
|
||||
t.Fatalf("granted: %s", text)
|
||||
}
|
||||
}
|
||||
@@ -260,15 +260,15 @@ func TestARefusalSaysWhyTheGrantsCouldNotBeRead(t *testing.T) {
|
||||
b, _, _ := aBundle(t)
|
||||
writeFile(t, b.Env.Grants, `{"grants": {"mesh": "triage"}}`)
|
||||
rt := startBundle(t, b.Tools())
|
||||
text, isErr := rt.call(ReadTool, "novox.triage", map[string]any{"account": "mesh", "folder": "INBOX", "uid": 7})
|
||||
text, isErr := rt.call(ReadTool, "node-c.triage", map[string]any{"account": "mesh", "folder": "INBOX", "uid": 7})
|
||||
if !isErr || !strings.Contains(text, "the setting grants cannot be read") || !strings.Contains(text, "not the module's") || strings.Contains(text, "Please find") {
|
||||
t.Fatalf("read: %v %s", isErr, text)
|
||||
}
|
||||
text, isErr = rt.call(SearchTool, "novox.triage", map[string]any{"query": "invoice"})
|
||||
text, isErr = rt.call(SearchTool, "node-c.triage", map[string]any{"query": "invoice"})
|
||||
if !isErr || !strings.Contains(text, "the setting grants cannot be read") {
|
||||
t.Fatalf("search: %v %s", isErr, text)
|
||||
}
|
||||
text, _ = rt.call(AccountsTool, "novox.triage", map[string]any{})
|
||||
text, _ = rt.call(AccountsTool, "node-c.triage", map[string]any{})
|
||||
if !strings.Contains(text, "the setting grants cannot be read") || !strings.Contains(text, `"accounts":[]`) {
|
||||
t.Fatalf("accounts: %s", text)
|
||||
}
|
||||
@@ -284,7 +284,7 @@ func TestMailSearchWithNoFilterAnswersTheNewestGrantedMail(t *testing.T) {
|
||||
env := Env{Accounts: filepath.Join(dir, "accounts.json"), Grants: filepath.Join(dir, "grants.json"), Secrets: dir,
|
||||
Status: filepath.Join(dir, "health.json")}
|
||||
writeFile(t, env.Accounts, `{"accounts": {
|
||||
"mesh": {"kind": "imap", "address": "jschoubben@novox.be", "incoming": {"host": "mail.novox.be", "port": 993, "security": "tls"}},
|
||||
"mesh": {"kind": "imap", "address": "jschoubben@node-c.be", "incoming": {"host": "mail.node-c.be", "port": 993, "security": "tls"}},
|
||||
"gmail": {"kind": "imap", "address": "me@gmail.example", "incoming": {"host": "imap.gmail.com", "port": 993, "security": "tls"}},
|
||||
"secret": {"kind": "imap", "address": "other@example.org", "incoming": {"host": "mail.example.org", "port": 993, "security": "tls"}}}}`)
|
||||
writeFile(t, env.Grants, `{"grants": {"mesh": ["triage"], "gmail": ["triage"], "secret": ["other"]}}`)
|
||||
@@ -314,7 +314,7 @@ func TestMailSearchWithNoFilterAnswersTheNewestGrantedMail(t *testing.T) {
|
||||
}
|
||||
b := &Bundle{Env: env, Index: func(context.Context) (Store, error) { return st, nil }, Now: func() time.Time { return judgedAt }}
|
||||
rt := startBundle(t, b.Tools())
|
||||
text, isErr := rt.call(SearchTool, "novox.triage", map[string]any{"limit": 3})
|
||||
text, isErr := rt.call(SearchTool, "node-c.triage", map[string]any{"limit": 3})
|
||||
if isErr {
|
||||
t.Fatalf("%v %s", isErr, text)
|
||||
}
|
||||
@@ -350,7 +350,7 @@ func TestMailSearchValidatesBeforeAskingTheIndex(t *testing.T) {
|
||||
{"account", map[string]any{"account": "no-such-account"}, "no-such-account"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
text, isErr := rt.call(SearchTool, "novox.triage", c.args)
|
||||
text, isErr := rt.call(SearchTool, "node-c.triage", c.args)
|
||||
if !isErr || !strings.Contains(text, c.want) {
|
||||
t.Fatalf("%s: %v %s", c.name, isErr, text)
|
||||
}
|
||||
@@ -412,12 +412,12 @@ func TestMailSearchEndToEndFiltersAndGroupBySender(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
older := judgedAt.Add(-time.Hour)
|
||||
if err := st.Add(ctx, Message{Account: "mesh", Folder: "INBOX", UID: 1, MessageID: "<a@mesh>", Subject: "from alice",
|
||||
Date: older, From: []Person{{Address: "alice@example.org"}}, To: []Person{{Address: "jschoubben@novox.be"}},
|
||||
Date: older, From: []Person{{Address: "alice@example.org"}}, To: []Person{{Address: "jschoubben@node-c.be"}},
|
||||
Flags: []string{}, Attachments: []AttachmentRef{}, Body: "hi"}, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := st.Add(ctx, Message{Account: "mesh", Folder: "INBOX", UID: 2, MessageID: "<b@mesh>", Subject: "from alice again",
|
||||
Date: judgedAt, From: []Person{{Address: "alice@example.org"}}, To: []Person{{Address: "jschoubben@novox.be"}},
|
||||
Date: judgedAt, From: []Person{{Address: "alice@example.org"}}, To: []Person{{Address: "jschoubben@node-c.be"}},
|
||||
Flags: []string{"\\Seen"}, Attachments: []AttachmentRef{}, Body: "hi again"}, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -425,13 +425,13 @@ func TestMailSearchEndToEndFiltersAndGroupBySender(t *testing.T) {
|
||||
rt := startBundle(t, b.Tools())
|
||||
|
||||
unread := true
|
||||
text, isErr := rt.call(SearchTool, "novox.triage", map[string]any{"from": []any{"example.org"}, "unread": unread,
|
||||
text, isErr := rt.call(SearchTool, "node-c.triage", map[string]any{"from": []any{"example.org"}, "unread": unread,
|
||||
"order-by": "date", "order": "asc"})
|
||||
if isErr || !strings.Contains(text, "a@mesh") || strings.Contains(text, "b@mesh") {
|
||||
t.Fatalf("%v %s", isErr, text)
|
||||
}
|
||||
|
||||
text, isErr = rt.call(SearchTool, "novox.triage", map[string]any{"group-by-sender": true})
|
||||
text, isErr = rt.call(SearchTool, "node-c.triage", map[string]any{"group-by-sender": true})
|
||||
var grouped struct {
|
||||
Senders []SenderGroup `json:"senders"`
|
||||
}
|
||||
@@ -443,7 +443,7 @@ func TestMailSearchEndToEndFiltersAndGroupBySender(t *testing.T) {
|
||||
// 1232/AC17 (review round 1 finding 5): order-by given, order left out, defaults to desc, as the schema's
|
||||
// own description promises — not asc. "from alice" is a prefix of "from alice again", so ascending would put
|
||||
// <a@mesh> first; descending puts <b@mesh> first.
|
||||
text, isErr = rt.call(SearchTool, "novox.triage", map[string]any{"order-by": "subject"})
|
||||
text, isErr = rt.call(SearchTool, "node-c.triage", map[string]any{"order-by": "subject"})
|
||||
if isErr {
|
||||
t.Fatalf("%v %s", isErr, text)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user