Drop the keeper's and config's take-over verbs with the controller's sources
mesh-sdk drops compare, take-over and hand-back from the keeper's and config's seats (ADR 0590): there is no controller to compare with or hand back to. mesh-conditions loses those verbs, its take-over key and asks; mesh-config reads its own sources always and pull-declares is gone; and mesh-hand-acts no longer names the controller's log (issue 1126). Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
@@ -66,14 +66,9 @@ Each directory or archive this module declares says how it treats a file it did
|
||||
|
||||
- `mesh-config_declared`: each placed module's declarations at the trunk, with the manifest and commit they come
|
||||
from, written only when they differ.
|
||||
- `mesh-config_takeover`: the key `switch` names the source that holds: none (declarations from the modules' own
|
||||
manifests, the list from the indexer's `predefined`, pull requests' config from the controller's `pull-declares`),
|
||||
or `indexer` after the operator's take-over (pull requests' config from the indexer's `config-candidates`; the
|
||||
controller's `pull-declares` refused). A switch still naming `controller` (the old hand-back) is read as `indexer` and
|
||||
said gone. Each take-over ask is kept beside it.
|
||||
- `config_compare` lists every difference between each open pull request's candidate at the indexer and what the
|
||||
controller told by `pull-declares`; the controller's mirrors are no longer read (ADR 0565 §4, issue 774).
|
||||
`config_take_over` asks the operator's Approve, refused while any difference stands and switched on the Approve only
|
||||
if none stands then. `config_hand_back` is gone: it answers so and changes nothing. `config_declared` and `config_resync` read and write again what it keeps.
|
||||
- Its own sources hold always (novox/hq ADR 0590): declarations from the modules' own manifests, the list from the
|
||||
indexer's `predefined`, pull requests' config from the indexer's `config-candidates`. The controller's
|
||||
`pull-declares`, the take-over, compare and hand-back went with the controller. `config_declared` and
|
||||
`config_resync` read and write again what it keeps.
|
||||
- A trusted value is set only through propose and the operator's Approve: no controller relay is trusted (issue 1126).
|
||||
- Its sets, clears and warrants are recorded in the hand-act log of the seat `mesh-hand-acts` alone.
|
||||
|
||||
@@ -124,7 +124,7 @@ func resolving(c *Config) {
|
||||
// A build's manifest is written before a want names the build, so a want, a trunk module or a placement moving is
|
||||
// every change of what a module declares where (issue 1953); the builds themselves are read, not watched.
|
||||
for _, read := range []string{PlacementRead, WantsRead, ModulesRead, IndexerPredefinedRead,
|
||||
CandidatesRead, TakeoverState} {
|
||||
CandidatesRead} {
|
||||
// Asked again until the runtime takes it, each give-up said (mesh-sdk's Follow).
|
||||
go stdio.State(read).Follow("", poke)
|
||||
}
|
||||
|
||||
@@ -31,10 +31,10 @@ func TestEveryStateAndActHasItsTool(t *testing.T) {
|
||||
via := map[string]string{
|
||||
ValuesState: "config list", ResolvedState: "config list", PredefinedState: "config list",
|
||||
ProposalsState: "config proposals", PullsState: "config list", KeptState: "config list", HistoryLog: "config history",
|
||||
DeclaredState: DeclaredTool, TakeoverState: DeclaredTool,
|
||||
PlacementRead: CompareTool, WantsRead: CompareTool, BuildsRead: CompareTool, ModulesRead: DeclaredTool,
|
||||
ManifestsRead: DeclaredTool, IndexerPredefinedRead: CompareTool, CandidatesRead: CompareTool,
|
||||
"a resolution": ResyncTool, "the indexer's candidates taken": ResyncTool, "a take-over decided": TakeOverTool,
|
||||
DeclaredState: DeclaredTool,
|
||||
PlacementRead: "config list", WantsRead: "config list", BuildsRead: "config list", ModulesRead: DeclaredTool,
|
||||
ManifestsRead: DeclaredTool, IndexerPredefinedRead: "config list", CandidatesRead: ResyncTool,
|
||||
"a resolution": ResyncTool, "the indexer's candidates taken": ResyncTool,
|
||||
}
|
||||
served := map[string]bool{}
|
||||
for _, tool := range Tools(&Config{}) {
|
||||
|
||||
@@ -758,7 +758,6 @@ func (c *Config) Decide() ([]string, error) {
|
||||
for _, r := range ready {
|
||||
said = append(said, c.decide(r.p, r.w))
|
||||
}
|
||||
said = append(said, c.decideTakeovers()...)
|
||||
c.recordWarrants(now)
|
||||
return said, nil
|
||||
}
|
||||
|
||||
@@ -136,59 +136,15 @@ type PullDeclaresAnswer struct {
|
||||
Unsaid []string `json:"unsaid,omitempty"`
|
||||
}
|
||||
|
||||
// PullDeclares keeps what a pull request's head declares, from the controller alone, and judges again the values
|
||||
// held for it: a value whose key is declared the same (by digest) and still passes is kept at the new head; any
|
||||
// other is dropped, recorded and raised, and must be proposed again (ADR 0502 clause 6).
|
||||
func (c *Config) PullDeclares(call Call) (*PullDeclaresAnswer, error) {
|
||||
if call.Caller != ControllerCaller {
|
||||
return nil, fmt.Errorf("pull-declares is the controller's: it reads a pull request's manifests and tells "+
|
||||
"mesh-config what the head declares; %s is not the controller", call.Caller)
|
||||
}
|
||||
if sw, err := c.source(); err != nil {
|
||||
return nil, err
|
||||
} else if sw.Source == SourceIndexer {
|
||||
return nil, errors.New("pull-declares is refused: since the take-over mesh-config reads a pull request's config " +
|
||||
"declarations from the indexer's config-candidates (novox/hq ADR 0565 §4)")
|
||||
}
|
||||
if _, _, _, err := parsePull(call.Pull); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
head := strings.TrimSpace(call.Head)
|
||||
if head == "" {
|
||||
return nil, errors.New("name the head the declarations are read at")
|
||||
}
|
||||
raw, err := json.Marshal(call.Declares)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the declarations cannot be read: %v", err)
|
||||
}
|
||||
declares := map[string]map[string]Declaration{}
|
||||
if call.Declares != nil {
|
||||
if err := json.Unmarshal(raw, &declares); err != nil {
|
||||
return nil, fmt.Errorf("declares is the modules' declarations, a key to a declaration for each module: %v", err)
|
||||
}
|
||||
}
|
||||
for m, keys := range declares {
|
||||
if !nodeOrModule.MatchString(m) {
|
||||
return nil, fmt.Errorf("%q is not a module's name", m)
|
||||
}
|
||||
for k, d := range keys {
|
||||
if !keyName.MatchString(k) {
|
||||
return nil, fmt.Errorf("%q is not a config key", k)
|
||||
}
|
||||
if !d.HasDefault() {
|
||||
d.Default = nil
|
||||
}
|
||||
keys[k] = d
|
||||
}
|
||||
}
|
||||
pull := strings.TrimSpace(call.Pull)
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.declarePull(pull, head, declares)
|
||||
// PullDeclares is gone: a pull request's config declarations are read from the indexer's config-candidates
|
||||
// (takeCandidates), and the controller that told them is not placed (novox/hq ADR 0565 §4, ADR 0590).
|
||||
func (c *Config) PullDeclares(Call) (*PullDeclaresAnswer, error) {
|
||||
return nil, errors.New("pull-declares is gone: mesh-config reads a pull request's config declarations from the " +
|
||||
"indexer's config-candidates (novox/hq ADR 0565 §4, ADR 0590)")
|
||||
}
|
||||
|
||||
// declarePull keeps what a pull request's head declares and judges again the values held for it; from the
|
||||
// controller's pull-declares, or after the take-over from the indexer's config candidates. The caller holds the lock.
|
||||
// declarePull keeps what a pull request's head declares and judges again the values held for it, from the indexer's
|
||||
// config candidates. The caller holds the lock.
|
||||
func (c *Config) declarePull(pull, head string, declares map[string]map[string]Declaration) (*PullDeclaresAnswer, error) {
|
||||
if err := c.Store.Put(PullsState, pullKey(pull), PullDecl{Pull: pull, Head: head, Declares: declares,
|
||||
At: c.now()}); err != nil {
|
||||
|
||||
@@ -51,13 +51,21 @@ func (f *standForge) Pull(owner, repo string, n int) (bool, bool, string, error)
|
||||
return st[0], st[1], f.head, nil
|
||||
}
|
||||
|
||||
// tell keeps what a pull request's head declares as the indexer's candidate is taken (takeCandidates).
|
||||
func tell(t *testing.T, c *Config, pull, head string, declares map[string]any) *PullDeclaresAnswer {
|
||||
t.Helper()
|
||||
a, err := c.Do(Call{Action: "pull-declares", Pull: pull, Head: head, Declares: declares, Caller: ControllerCaller})
|
||||
if err != nil {
|
||||
t.Fatalf("pull-declares: %v", err)
|
||||
raw, _ := json.Marshal(declares)
|
||||
d := map[string]map[string]Declaration{}
|
||||
if err := json.Unmarshal(raw, &d); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return a.(*PullDeclaresAnswer)
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
a, err := c.declarePull(pull, head, d)
|
||||
if err != nil {
|
||||
t.Fatalf("declare: %v", err)
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// holdIt proposes media-gid=2000 for n8n on ace held for the pull request and has the operator approve it.
|
||||
@@ -75,21 +83,15 @@ func holdIt(t *testing.T, c *Config, m *memStore, r *standIn) string {
|
||||
|
||||
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) {
|
||||
// 1826/K1: mesh-config keeps what a pull request declares per pull request; pull-declares is gone (ADR 0590).
|
||||
func TestAPullRequestsDeclarationsAreKeptPerPullRequest(t *testing.T) {
|
||||
c, m, _, _ := aHoldingMesh(t)
|
||||
for _, caller := range []string{"g14.node-tools", Unproven, ""} {
|
||||
for _, caller := range []string{"g14.node-tools", Unproven, "", ControllerCaller} {
|
||||
if _, err := c.Do(Call{Action: "pull-declares", Pull: thePull, Head: "abc1234", Declares: n8nPullDeclares(),
|
||||
Caller: caller}); err == nil || !strings.Contains(err.Error(), "controller") {
|
||||
Caller: caller}); err == nil || !strings.Contains(err.Error(), "config-candidates") {
|
||||
t.Errorf("pull-declares from %q: %v", caller, err)
|
||||
}
|
||||
}
|
||||
for _, bad := range []Call{{Pull: "mesh-catalog#434", Head: "a"}, {Pull: thePull}, {Pull: "a/b#0", Head: "a"}} {
|
||||
bad.Action, bad.Caller = "pull-declares", ControllerCaller
|
||||
if _, err := c.Do(bad); err == nil {
|
||||
t.Errorf("pull-declares %+v was taken", bad)
|
||||
}
|
||||
}
|
||||
a := tell(t, c, thePull, "abc1234def", n8nPullDeclares())
|
||||
if len(a.Modules) != 1 || a.Modules[0] != "n8n" {
|
||||
t.Errorf("answered %+v", a)
|
||||
|
||||
@@ -365,10 +365,6 @@ func (c *Config) Reconcile() (Resolution, error) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
var r Resolution
|
||||
if sw, serr := c.source(); serr == nil && sw.Gone != "" {
|
||||
r.SourceGone = sw.Gone
|
||||
c.logf("[mesh-config] %s", sw.Gone)
|
||||
}
|
||||
published, err := c.publish()
|
||||
if err != nil {
|
||||
c.logf("[mesh-config] the list of predefined keys is not published: %v", err)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package main
|
||||
|
||||
// The tools of the switch (novox/hq ADR 0565 §4, §15): compare, take-over and hand-back, served as the seat's verbs
|
||||
// and as the module's own tools beside them, the read of `declared` and resync.
|
||||
// The module's own tools beside the seat's verb (novox/hq ADR 0565 §15): the read of `declared` and resync. compare,
|
||||
// take-over and hand-back went with the controller (ADR 0590).
|
||||
|
||||
import (
|
||||
"strings"
|
||||
@@ -11,56 +11,17 @@ import (
|
||||
|
||||
// The switch's tools.
|
||||
const (
|
||||
CompareTool = "config_compare"
|
||||
TakeOverTool = "config_take_over"
|
||||
HandBackTool = "config_hand_back"
|
||||
DeclaredTool = "config_declared"
|
||||
ResyncTool = "config_resync"
|
||||
)
|
||||
|
||||
// SeatVerbs are the seat's verbs, as the manifest claims them.
|
||||
var SeatVerbs = []string{Verb, CompareVerb, TakeOverVerb, HandBackVerb}
|
||||
var SeatVerbs = []string{Verb}
|
||||
|
||||
func switchTools(c *Config) []stdio.Tool {
|
||||
own := ownSwitchTools(c)
|
||||
byName := map[string]stdio.Tool{}
|
||||
for _, t := range own {
|
||||
byName[t.Name] = t
|
||||
}
|
||||
for verb, tool := range map[string]string{CompareVerb: CompareTool, TakeOverVerb: TakeOverTool, HandBackVerb: HandBackTool} {
|
||||
t := byName[tool]
|
||||
t.Name = Seat + "." + verb
|
||||
own = append(own, t)
|
||||
}
|
||||
return own
|
||||
}
|
||||
func switchTools(c *Config) []stdio.Tool { return ownSwitchTools(c) }
|
||||
|
||||
func ownSwitchTools(c *Config) []stdio.Tool {
|
||||
why := map[string]any{"type": "object", "properties": map[string]any{
|
||||
"why": map[string]any{"type": "string", "description": "why, in a sentence"}}, "required": []string{"why"}}
|
||||
whyOf := func(a map[string]any) string { s, _ := a["why"].(string); return strings.TrimSpace(s) }
|
||||
return []stdio.Tool{
|
||||
{Name: CompareTool,
|
||||
Description: "Every difference between the indexer's records and the controller's pull-declares (novox/hq ADR 0565 §4): " +
|
||||
"each open pull request's config (the indexer's candidate against the controller's pull-declares), each " +
|
||||
"with its reason. The controller's mirrors are no longer read, so nothing else is compared. The " +
|
||||
"take-over is refused while any stands.",
|
||||
Run: func(map[string]any) (any, error) { return c.Compare() }},
|
||||
{Name: TakeOverTool,
|
||||
Description: "Ask the operator's Approve, under mesh-config's own name, to switch config to its own sources: " +
|
||||
"the indexer's list of predefined keys and pull request config, refusing the controller's pull-declares. " +
|
||||
"Refused while compare shows a difference; on Approve switched if nothing differs then.",
|
||||
Input: why,
|
||||
Run: func(a map[string]any) (any, error) {
|
||||
return c.TakeOver(Call{Why: whyOf(a), Caller: stdio.CallerOf(a)})
|
||||
}},
|
||||
{Name: HandBackTool,
|
||||
Description: "Gone: the controller's records end with its lease and config no longer reads them (novox/hq ADR 0565 §4, " +
|
||||
"issue 774); answers that, and changes nothing.",
|
||||
Input: why,
|
||||
Run: func(a map[string]any) (any, error) {
|
||||
return c.HandBack(Call{Why: whyOf(a), Caller: stdio.CallerOf(a)})
|
||||
}},
|
||||
{Name: DeclaredTool,
|
||||
Description: "Each placed module's declarations as config keeps them (its state declared): the manifest and " +
|
||||
"commit they come from and how many keys, or one module whole; and which source holds now.",
|
||||
@@ -72,7 +33,7 @@ func ownSwitchTools(c *Config) []stdio.Tool {
|
||||
}},
|
||||
{Name: ResyncTool,
|
||||
Description: "Read every source again and write what differs: the published list of predefined keys, the " +
|
||||
"resolved values, declared, and after the take-over the indexer's config candidates.",
|
||||
"resolved values, declared, and the indexer's config candidates.",
|
||||
Run: func(map[string]any) (any, error) { return c.Resync() }},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,95 +1,44 @@
|
||||
package main
|
||||
|
||||
// Config's records out of the controller (novox/hq ADR 0565 §3, §4, §13, §15, and the progressive insight of
|
||||
// 2026-10-29): mesh-config keeps each placed module's declarations in its own state `declared`; `compare` lists every
|
||||
// difference between each open pull request's candidate at the indexer and what the controller told by pull-declares;
|
||||
// `take-over` asks the operator's Approve under mesh-config's own name and, refused while anything differs, writes the
|
||||
// key `takeover.switch` naming the indexer.
|
||||
// Config's records out of the controller (novox/hq ADR 0565 §3, §15, and the progressive insight of 2026-10-29):
|
||||
// mesh-config keeps each placed module's declarations in its own state `declared`, reads the list of predefined keys
|
||||
// from the indexer's `predefined` and a pull request's config from the indexer's `config-candidates`.
|
||||
//
|
||||
// **The controller's records are no longer read** (issue 774, ADR 0571 §13): its mirrors `declared-config` and
|
||||
// `predefined` are gone from the reads, so declarations come from each placed module's own manifests and the list of
|
||||
// predefined keys from the indexer's `predefined`, whatever the switch says. `hand-back` is answered as gone (ADR 0565
|
||||
// §4: it ends with the lease), and a switch that still names the controller is read as the indexer, said so.
|
||||
//
|
||||
// **Which source holds** is the key `switch` alone, read by mesh-config alone:
|
||||
// - none written: a pull request's declarations from the controller's pull-declares;
|
||||
// - `indexer` (taken over): a pull request's from the indexer's `config-candidates`; pull-declares refused;
|
||||
// - `controller` (the old hand-back): gone, read as `indexer`.
|
||||
// **Its own sources hold always** (ADR 0590, issue 1126): the controller ended by its unassignment, so its pull-declares,
|
||||
// the take-over that switched away from them, compare and hand-back are gone, and the switch is the indexer's whatever
|
||||
// a key once said.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// The states and reads of the switch.
|
||||
// The states and reads.
|
||||
const (
|
||||
// DeclaredState is each placed module's declarations at the trunk, one key per module (ADR 0565 §3).
|
||||
DeclaredState = "declared"
|
||||
// TakeoverState holds the key SwitchKey and each take-over ask, `ask.<id>`.
|
||||
TakeoverState = "takeover"
|
||||
SwitchKey = "switch"
|
||||
|
||||
// IndexerPredefinedRead and CandidatesRead are the indexer's states (ADR 0565 §2).
|
||||
IndexerPredefinedRead = delivery.IndexerModule + "." + delivery.IndexerPredefinedState
|
||||
CandidatesRead = delivery.IndexerModule + "." + delivery.IndexerConfigCandidatesState
|
||||
)
|
||||
|
||||
// The sources the switch names.
|
||||
const (
|
||||
SourceInterim = "interim"
|
||||
SourceIndexer = "indexer"
|
||||
// SourceController is a switch the old hand-back wrote; the controller's records are gone, so it is read as the indexer.
|
||||
SourceController = "controller"
|
||||
)
|
||||
// SourceIndexer is the one source of a pull request's config and of the predefined list: the indexer's.
|
||||
const SourceIndexer = "indexer"
|
||||
|
||||
// TakeOverVerb and HandBackVerb are the seat's verbs.
|
||||
const (
|
||||
TakeOverVerb = "take-over"
|
||||
HandBackVerb = "hand-back"
|
||||
CompareVerb = "compare"
|
||||
)
|
||||
|
||||
// Switch is the key `takeover.switch`: the source that holds, who chose it, why and on which ask.
|
||||
// Switch is which source holds: the indexer's, always, since the controller ended (ADR 0590).
|
||||
type Switch struct {
|
||||
Source string `json:"source"`
|
||||
At time.Time `json:"at"`
|
||||
By string `json:"by"`
|
||||
Why string `json:"why"`
|
||||
Ask string `json:"ask,omitempty"`
|
||||
// Gone says the stored switch named the controller, whose records config no longer reads (ADR 0565 §4); the switch
|
||||
// returned is then the indexer's. Never stored.
|
||||
Gone string `json:"-"`
|
||||
Source string `json:"source"`
|
||||
Why string `json:"why"`
|
||||
}
|
||||
|
||||
// goneSource is what is said of a switch that still names the controller.
|
||||
const goneSource = "the switch names the controller, which is gone: config no longer reads its records (novox/hq ADR 0565 §4, " +
|
||||
"issue 774) and reads the indexer's"
|
||||
|
||||
// source is the source that holds now.
|
||||
// source is the source that holds: the indexer's.
|
||||
func (c *Config) source() (Switch, error) {
|
||||
e, err := c.Store.Get(TakeoverState, SwitchKey)
|
||||
if err != nil {
|
||||
return Switch{}, fmt.Errorf("the take-over's switch cannot be read: %w", err)
|
||||
}
|
||||
if e == nil {
|
||||
return Switch{Source: SourceInterim}, nil
|
||||
}
|
||||
var s Switch
|
||||
if err := json.Unmarshal(e.Value, &s); err != nil || (s.Source != SourceIndexer && s.Source != SourceController) {
|
||||
return Switch{}, fmt.Errorf("the take-over's switch is not one: %s", e.Value)
|
||||
}
|
||||
if s.Source == SourceController {
|
||||
s.Source, s.Gone = SourceIndexer, goneSource
|
||||
}
|
||||
return s, nil
|
||||
return Switch{Source: SourceIndexer, Why: "config reads its own sources since the controller ended by its unassignment (novox/hq ADR 0590)"}, nil
|
||||
}
|
||||
|
||||
// ---- declared --------------------------------------------------------------------------------------------
|
||||
@@ -254,368 +203,6 @@ func declaresDigest(d map[string]map[string]Declaration) string {
|
||||
return string(raw)
|
||||
}
|
||||
|
||||
// ---- compare ----------------------------------------------------------------------------------------------
|
||||
|
||||
// Difference is one thing the indexer's records and the controller's pull-declares say differently, and why.
|
||||
type Difference struct {
|
||||
What string `json:"what"`
|
||||
Module string `json:"module,omitempty"`
|
||||
Node string `json:"node,omitempty"`
|
||||
Key string `json:"key,omitempty"`
|
||||
Pull string `json:"pull,omitempty"`
|
||||
Own json.RawMessage `json:"own,omitempty"`
|
||||
Why string `json:"why"`
|
||||
}
|
||||
|
||||
// The kinds of difference.
|
||||
const (
|
||||
DiffPull = "pull"
|
||||
)
|
||||
|
||||
// Comparison is what compare answers.
|
||||
type Comparison struct {
|
||||
Source string `json:"source"`
|
||||
Differences []Difference `json:"differences"`
|
||||
Count int `json:"count"`
|
||||
Said string `json:"said"`
|
||||
// Gone is said when the stored switch still names the controller.
|
||||
Gone string `json:"gone,omitempty"`
|
||||
}
|
||||
|
||||
// Compare lists every difference (ADR 0565 §4 and its insight). The caller holds no lock.
|
||||
func (c *Config) Compare() (*Comparison, error) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.compare()
|
||||
}
|
||||
|
||||
// compare is Compare; the caller holds the lock. The controller's mirrors are no longer read (issue 774, ADR 0565 §4),
|
||||
// so the declarations, the list and the resolved values have no second side: what remains to compare is each open pull
|
||||
// request's candidate at the indexer against what the controller told by pull-declares.
|
||||
func (c *Config) compare() (*Comparison, error) {
|
||||
sw, err := c.source()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var own Sources
|
||||
if err := c.readPulls(&own); err != nil {
|
||||
return nil, fmt.Errorf("mesh-config's own pull request records cannot be read: %w", err)
|
||||
}
|
||||
var diffs []Difference
|
||||
|
||||
// Each open pull request: the indexer's candidate against what the controller told by pull-declares.
|
||||
cands, cerr := c.candidates()
|
||||
if cerr != nil {
|
||||
diffs = append(diffs, Difference{What: DiffPull, Why: "the indexer's config candidates cannot be read: " + cerr.Error()})
|
||||
}
|
||||
settled := map[string]string{} // a pull request the forge says is no longer open → how it ended
|
||||
for _, k := range unionKeys(cands, own.Pulls) {
|
||||
a, aok := cands[k]
|
||||
b, bok := own.Pulls[k]
|
||||
isSettled, forgeNote := c.pullSettled(firstNonEmpty(a.Pull, b.Pull), settled)
|
||||
if isSettled {
|
||||
continue // a merged or closed pull request is no candidate to agree on (the operator, 2026-11-01)
|
||||
}
|
||||
before := len(diffs)
|
||||
switch {
|
||||
case !aok:
|
||||
diffs = append(diffs, Difference{What: DiffPull, Pull: b.Pull, Why: "the controller told its declarations and the indexer holds no candidate"})
|
||||
case !bok:
|
||||
diffs = append(diffs, Difference{What: DiffPull, Pull: a.Pull, Why: "the indexer holds a candidate and the controller told nothing"})
|
||||
case a.Head != b.Head:
|
||||
diffs = append(diffs, Difference{What: DiffPull, Pull: a.Pull, Why: fmt.Sprintf("the indexer's candidate is at %s, "+
|
||||
"the controller told %s", short(a.Head), short(b.Head))})
|
||||
case declaresDigest(a.Declares) != declaresDigest(b.Declares):
|
||||
diffs = append(diffs, Difference{What: DiffPull, Pull: a.Pull, Why: "the two declare its config otherwise"})
|
||||
}
|
||||
if forgeNote != "" && len(diffs) > before {
|
||||
diffs[len(diffs)-1].Why += "; " + forgeNote // the row stays because the forge was not heard: say so in the row
|
||||
}
|
||||
}
|
||||
|
||||
sort.SliceStable(diffs, func(i, j int) bool {
|
||||
a, b := diffs[i], diffs[j]
|
||||
return a.What+a.Module+a.Node+a.Key+a.Pull < b.What+b.Module+b.Node+b.Key+b.Pull
|
||||
})
|
||||
if diffs == nil {
|
||||
diffs = []Difference{}
|
||||
}
|
||||
cmp := &Comparison{Source: sw.Source, Differences: diffs, Count: len(diffs), Gone: sw.Gone}
|
||||
if len(diffs) == 0 {
|
||||
cmp.Said = "nothing differs: the take-over may be asked (the controller's mirrors are no longer read, so only the pull requests are compared)"
|
||||
} else {
|
||||
cmp.Said = fmt.Sprintf("%d difference(s): the take-over is refused while any stands", len(diffs))
|
||||
}
|
||||
return cmp, nil
|
||||
}
|
||||
|
||||
func firstNonEmpty(a, b string) string {
|
||||
if a != "" {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// pullSettled is whether the forge says a pull request merged or closed. Without a forge, or when it does not say, the
|
||||
// pull request counts as open, so its row stays (failure is loud).
|
||||
func (c *Config) pullSettled(pull string, memo map[string]string) (bool, string) {
|
||||
if c.Forge == nil || pull == "" {
|
||||
return false, ""
|
||||
}
|
||||
if v, ok := memo[pull]; ok {
|
||||
return v != "" && !strings.HasPrefix(v, forgeSilent), forgeNoteOf(v)
|
||||
}
|
||||
owner, repo, n, err := parsePull(pull)
|
||||
if err != nil {
|
||||
return false, ""
|
||||
}
|
||||
open, merged, _, err := c.Forge.Pull(owner, repo, n)
|
||||
switch {
|
||||
case err != nil:
|
||||
c.logf("[mesh-config] compare: the forge does not say whether %s is open (%v); its row stays", pull, err)
|
||||
memo[pull] = forgeSilent + err.Error()
|
||||
case open:
|
||||
memo[pull] = ""
|
||||
case merged:
|
||||
memo[pull] = "merged"
|
||||
default:
|
||||
memo[pull] = "closed"
|
||||
}
|
||||
v := memo[pull]
|
||||
return v != "" && !strings.HasPrefix(v, forgeSilent), forgeNoteOf(v)
|
||||
}
|
||||
|
||||
// forgeSilent marks a memo entry for a pull request the forge could not be asked about; the rest of the entry is why.
|
||||
const forgeSilent = "forge-silent: "
|
||||
|
||||
// forgeNoteOf is the words a row carries when the forge was not heard on its pull request, or "".
|
||||
func forgeNoteOf(v string) string {
|
||||
if why, ok := strings.CutPrefix(v, forgeSilent); ok {
|
||||
return "the forge does not say whether the pull request is open (" + why + ")"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func unionKeys[A, B any](a map[string]A, b map[string]B) []string {
|
||||
seen := map[string]bool{}
|
||||
for k := range a {
|
||||
seen[k] = true
|
||||
}
|
||||
for k := range b {
|
||||
seen[k] = true
|
||||
}
|
||||
return sortedKeys(seen)
|
||||
}
|
||||
|
||||
// ---- take-over and hand-back ---------------------------------------------------------------------------------
|
||||
|
||||
// TakeoverAsk is one take-over asked of the operator, `takeover.ask.<id>`: what the ask is composed from again, and
|
||||
// where it stands.
|
||||
type TakeoverAsk struct {
|
||||
ID string `json:"id"`
|
||||
From string `json:"from"`
|
||||
Why string `json:"why"`
|
||||
At time.Time `json:"at"`
|
||||
State string `json:"state"`
|
||||
Outcome string `json:"outcome,omitempty"`
|
||||
Decided time.Time `json:"decided,omitzero"`
|
||||
Answered string `json:"answered,omitempty"`
|
||||
Recorded bool `json:"recorded,omitempty"`
|
||||
}
|
||||
|
||||
func askKey(id string) string { return "ask." + id }
|
||||
|
||||
// act is what an answer to the take-over binds.
|
||||
func (t TakeoverAsk) act(option string) asks.Act {
|
||||
return asks.Act{"verb": Seat + "." + TakeOverVerb, "option": option, "ask": t.ID, "from": t.From,
|
||||
"at": t.At.UTC().Format(time.RFC3339Nano)}
|
||||
}
|
||||
|
||||
// ask is the take-over's ask, composed from it alone.
|
||||
func (t TakeoverAsk) ask() asks.Ask {
|
||||
why, _ := sayable("Why: " + t.Why)
|
||||
a := asks.Ask{ID: t.ID, Headline: "Switch config to its own sources",
|
||||
Explanation: strings.Join([]string{
|
||||
"mesh-config would read each pull request's config from the delivery's " +
|
||||
"indexer, no longer from the controller. Nothing any node resolves changes: compare showed no difference.",
|
||||
point(why)}, "\n"),
|
||||
Who: asks.Operator, Expires: t.At.Add(ProposalLasts),
|
||||
OnExpiry: "nothing changes; a pull request's config keeps coming from the controller's pull-declares", About: "config.takeover",
|
||||
Details: "Read what was compared with mesh-config's compare verb, through the mesh MCP server. hand-back is gone."}
|
||||
for _, o := range []struct{ id, label, does string }{
|
||||
{OptionApprove, "Approve", "config switches to its own sources at once, if nothing differs then"},
|
||||
{OptionDecline, "Decline", "nothing changes"},
|
||||
} {
|
||||
binds, _ := asks.ActDigest(t.act(o.id))
|
||||
a.Options = append(a.Options, asks.Option{ID: o.id, Label: o.label, Does: o.does, Level: asks.Approve, Binds: binds})
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// TakeoverAnswer is what take-over and hand-back answer.
|
||||
type TakeoverAnswer struct {
|
||||
Said string `json:"said"`
|
||||
Ask string `json:"ask,omitempty"`
|
||||
Source string `json:"source"`
|
||||
}
|
||||
|
||||
// TakeOver asks the operator's Approve to switch to the own sources, refused while compare shows a difference.
|
||||
func (c *Config) TakeOver(call Call) (*TakeoverAnswer, error) {
|
||||
if strings.TrimSpace(call.Why) == "" {
|
||||
return nil, errors.New("say why the take-over is asked now: it is shown to the operator")
|
||||
}
|
||||
if call.Caller == "" || call.Caller == Unproven {
|
||||
return nil, errors.New("the call names no proven caller")
|
||||
}
|
||||
if c.Asks == nil {
|
||||
return nil, errors.New("the operator cannot be asked from here, so nothing is switched")
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
sw, err := c.source()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if sw.Source == SourceIndexer {
|
||||
return &TakeoverAnswer{Source: sw.Source, Said: "config reads its own sources already, since " +
|
||||
sw.At.UTC().Format(time.RFC3339) + " (" + sw.By + ")"}, nil
|
||||
}
|
||||
cmp, err := c.compare()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cmp.Count > 0 {
|
||||
first := cmp.Differences[0]
|
||||
return nil, fmt.Errorf("the take-over is refused: compare shows %d difference(s), the first %s %s%s%s%s: %s. "+
|
||||
"Nothing was asked", cmp.Count, first.What, first.Module, first.Node, first.Key, first.Pull, first.Why)
|
||||
}
|
||||
now := c.now()
|
||||
var open []string
|
||||
_ = c.each(TakeoverState, func(key string, raw []byte) error {
|
||||
var t TakeoverAsk
|
||||
if strings.HasPrefix(key, "ask.") && json.Unmarshal(raw, &t) == nil && t.State == ProposalWaiting &&
|
||||
now.Before(t.At.Add(ProposalLasts)) {
|
||||
open = append(open, t.ID)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if len(open) > 0 {
|
||||
return nil, fmt.Errorf("the take-over is asked already, as %s, and waits for the operator's answer", open[0])
|
||||
}
|
||||
t := TakeoverAsk{ID: "takeover-" + newProposalID(), From: call.Caller, Why: strings.TrimSpace(call.Why), At: now.UTC(),
|
||||
State: ProposalWaiting}
|
||||
ask := t.ask()
|
||||
if err := ask.Check(now); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := c.Store.Put(TakeoverState, askKey(t.ID), t); err != nil {
|
||||
return nil, fmt.Errorf("the take-over could not be kept, so the operator was not asked: %w", err)
|
||||
}
|
||||
if err := c.Asks.Publish(asks.AskSubject(Module), ask, ask.ID); err != nil {
|
||||
t.State, t.Outcome = ProposalUnasked, "the ask could not be published: "+err.Error()
|
||||
_ = c.Store.Put(TakeoverState, askKey(t.ID), t)
|
||||
return nil, fmt.Errorf("the operator could not be asked: %w", err)
|
||||
}
|
||||
return &TakeoverAnswer{Ask: t.ID, Source: sw.Source, Said: fmt.Sprintf("the operator is asked %q under "+
|
||||
"mesh-config's own name; nothing changes until they approve, and on Approve config switches if nothing differs "+
|
||||
"then", ask.Headline)}, nil
|
||||
}
|
||||
|
||||
// decideTakeovers acts on each take-over ask the router has closed. The caller holds the lock.
|
||||
func (c *Config) decideTakeovers() []string {
|
||||
var said []string
|
||||
var waiting []TakeoverAsk
|
||||
_ = c.each(TakeoverState, func(key string, raw []byte) error {
|
||||
var t TakeoverAsk
|
||||
if strings.HasPrefix(key, "ask.") && json.Unmarshal(raw, &t) == nil && t.State == ProposalWaiting {
|
||||
waiting = append(waiting, t)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
now := c.now()
|
||||
for _, t := range waiting {
|
||||
keep := func(state, outcome string) string {
|
||||
t.State, t.Outcome, t.Decided = state, outcome, now
|
||||
_ = c.Store.Put(TakeoverState, askKey(t.ID), t)
|
||||
return "take-over " + t.ID + ": " + outcome
|
||||
}
|
||||
e, err := c.Asks.Record(t.ID)
|
||||
if err != nil || e == nil {
|
||||
if err == nil && now.After(t.At.Add(ProposalLasts+time.Hour)) {
|
||||
said = append(said, keep(ProposalEnded, "the router keeps no record of its ask; nothing was switched"))
|
||||
}
|
||||
continue
|
||||
}
|
||||
var rec routerRecord
|
||||
if json.Unmarshal(e.Value, &rec) != nil || rec.State == "open" || rec.Warrant == nil {
|
||||
continue
|
||||
}
|
||||
w := *rec.Warrant
|
||||
if w.Asker != Module || w.Ask != t.ID {
|
||||
said = append(said, keep(ProposalRefused, "the router's record is for another ask; nothing was switched"))
|
||||
continue
|
||||
}
|
||||
if w.Outcome != asks.OutcomeChosen {
|
||||
said = append(said, keep(ProposalEnded, "the ask "+string(w.Outcome)+"; nothing was switched"))
|
||||
continue
|
||||
}
|
||||
option, err := w.For(Module, t.ask())
|
||||
if err == nil {
|
||||
err = option.Performs(t.act(option.ID))
|
||||
}
|
||||
if err != nil {
|
||||
said = append(said, keep(ProposalRefused, "the answer is refused: "+err.Error()))
|
||||
continue
|
||||
}
|
||||
t.Answered = w.Says()
|
||||
if option.ID != OptionApprove {
|
||||
said = append(said, keep(ProposalDeclined, "declined by the operator; nothing was switched"))
|
||||
continue
|
||||
}
|
||||
cmp, err := c.compare()
|
||||
switch {
|
||||
case err != nil:
|
||||
said = append(said, keep(ProposalRefused, "approved, and compare could not be read, so nothing was switched: "+err.Error()))
|
||||
continue
|
||||
case cmp.Count > 0:
|
||||
said = append(said, keep(ProposalRefused, fmt.Sprintf("approved, and refused: compare shows %d difference(s) "+
|
||||
"now; nothing was switched, ask again once it is empty", cmp.Count)))
|
||||
continue
|
||||
}
|
||||
sw := Switch{Source: SourceIndexer, At: now.UTC(), By: approvedTakeover(w), Why: t.Why, Ask: t.ID}
|
||||
if err := c.Store.Put(TakeoverState, SwitchKey, sw); err != nil {
|
||||
said = append(said, fmt.Sprintf("take-over %s: approved, and the switch could not be written (%v); read again "+
|
||||
"on the next look", t.ID, err))
|
||||
continue
|
||||
}
|
||||
if err := c.Asks.Warranted(t.ID); err == nil {
|
||||
t.Recorded = true
|
||||
}
|
||||
said = append(said, keep(ProposalApproved, "approved by the operator: config reads its own sources from now"))
|
||||
}
|
||||
return said
|
||||
}
|
||||
|
||||
func approvedTakeover(w asks.Warrant) string {
|
||||
if w.By == nil {
|
||||
return "the operator"
|
||||
}
|
||||
return fmt.Sprintf("the %s, as %s identity %s", w.By.Who, w.By.Kind, w.By.Identity)
|
||||
}
|
||||
|
||||
// HandBack is answered as gone: the controller's records end with its lease, and config no longer reads them (novox/hq
|
||||
// ADR 0565 §4, issue 774). Nothing is written.
|
||||
func (c *Config) HandBack(call Call) (*TakeoverAnswer, error) {
|
||||
if strings.TrimSpace(call.Why) == "" {
|
||||
return nil, errors.New("say why config hands back: it is kept with the switch")
|
||||
}
|
||||
if call.Caller == "" || call.Caller == Unproven {
|
||||
return nil, errors.New("the call names no proven caller")
|
||||
}
|
||||
return nil, errors.New("hand-back is gone: the controller's records end with its lease and config no longer reads " +
|
||||
"them, so there is nothing to hand back to (novox/hq ADR 0565 §4); nothing was changed")
|
||||
}
|
||||
|
||||
// Declared is the read of `declared`: every module's key, or one whole.
|
||||
func (c *Config) Declared(module string) (any, error) {
|
||||
if module != "" {
|
||||
@@ -641,7 +228,7 @@ func (c *Config) Declared(module string) (any, error) {
|
||||
}
|
||||
|
||||
// Resync reads every source again and writes what differs: the published list, the resolved values, `declared`, and
|
||||
// after the take-over the indexer's config candidates (ADR 0565 §15).
|
||||
// the indexer's config candidates (ADR 0565 §15).
|
||||
func (c *Config) Resync() (any, error) {
|
||||
r, err := c.Reconcile()
|
||||
if err != nil {
|
||||
|
||||
@@ -2,11 +2,8 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
@@ -18,7 +15,7 @@ const (
|
||||
oldDeclared = `{"module":"old","settings":{"workers":{"kind":"preference","default":4,"why":"two per core"}}}`
|
||||
)
|
||||
|
||||
// anAgreeingMesh is aProposingMesh with the indexer's predefined list in place and no open pull request: compare is empty.
|
||||
// anAgreeingMesh is aProposingMesh with the indexer's predefined list in place and no open pull request.
|
||||
func anAgreeingMesh(t *testing.T) (*Config, *memStore, *standIn) {
|
||||
t.Helper()
|
||||
c, m, r := aProposingMesh(t)
|
||||
@@ -47,80 +44,11 @@ func candidate(t *testing.T, m *memStore, pull, head, declares string) {
|
||||
m.mirror(t, CandidatesRead, key, `{"v":1,"pull":"`+pull+`","head":"`+head+`","declares":`+declares+`}`)
|
||||
}
|
||||
|
||||
func kinds(cmp *Comparison) []string {
|
||||
var out []string
|
||||
for _, d := range cmp.Differences {
|
||||
k := d.What + ":" + d.Module + d.Pull + d.Key
|
||||
if d.Node != "" {
|
||||
k += "@" + d.Node
|
||||
}
|
||||
if !slices.Contains(out, k) {
|
||||
out = append(out, k)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// compare lists every difference with its reason — a pull request's config at the indexer against what the controller
|
||||
// told — and the take-over is refused while one stands (ADR 0565 §4 and its insight of 2026-10-29; issue 1953 L5-C4).
|
||||
// The controller's mirrors are no longer read, so no declaration, list or value is compared (issue 774, S9).
|
||||
func TestCompareListsEveryDifferenceAndTheTakeOverWaitsForNone(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
change func(t *testing.T, m *memStore)
|
||||
want []string
|
||||
}{
|
||||
{"all agree", func(*testing.T, *memStore) {}, nil},
|
||||
{"a pull request only the indexer holds", func(t *testing.T, m *memStore) {
|
||||
candidate(t, m, "novox/mesh-catalog#9", "h1", `{"dunst":{"gap":{"kind":"","type":"number","default":2}}}`)
|
||||
}, []string{"pull:novox/mesh-catalog#9"}},
|
||||
{"a candidate of a merged pull request is no difference", func(t *testing.T, m *memStore) {
|
||||
candidate(t, m, "novox/mesh-catalog#11", "h1", `{"dunst":{"gap":{"kind":"","type":"number","default":2}}}`)
|
||||
candidate(t, m, "novox/mesh-catalog#10", "h2", `{"dunst":{"gap":{"kind":"","type":"number","default":2}}}`)
|
||||
}, []string{"pull:novox/mesh-catalog#10"}},
|
||||
{"a forge that cannot be asked keeps the row and says why in it", func(t *testing.T, m *memStore) {
|
||||
candidate(t, m, "novox/mesh-catalog#12", "h1", `{"dunst":{"gap":{"kind":"","type":"number","default":2}}}`)
|
||||
}, []string{"pull:novox/mesh-catalog#12"}},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
c, m, r := anAgreeingMesh(t)
|
||||
c.Forge = &standForge{state: map[string][2]bool{"novox/mesh-catalog#11": {false, true}}}
|
||||
if strings.Contains(tc.name, "forge that cannot be asked") {
|
||||
c.Forge = &standForge{err: errors.New("forge down")}
|
||||
}
|
||||
tc.change(t, m)
|
||||
cmp, err := c.Compare()
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", tc.name, err)
|
||||
}
|
||||
if got := kinds(cmp); !slices.Equal(got, tc.want) {
|
||||
t.Errorf("%s: compare lists %v, want %v (%+v)", tc.name, got, tc.want, cmp.Differences)
|
||||
}
|
||||
for _, d := range cmp.Differences {
|
||||
if d.Why == "" {
|
||||
t.Errorf("%s: a difference without its reason: %+v", tc.name, d)
|
||||
}
|
||||
if strings.Contains(tc.name, "forge that cannot be asked") && !strings.Contains(d.Why, "forge down") {
|
||||
t.Errorf("%s: the row does not say the forge was not heard: %q", tc.name, d.Why)
|
||||
}
|
||||
}
|
||||
a, err := c.TakeOver(Call{Why: "compare is empty", Caller: "person.jochen"})
|
||||
if len(tc.want) > 0 {
|
||||
if err == nil || !strings.Contains(err.Error(), "refused") || len(r.subjects) != 0 {
|
||||
t.Errorf("%s: the take-over was asked while compare differs: %v", tc.name, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err != nil || a.Ask == "" || len(r.subjects) != 1 {
|
||||
t.Errorf("%s: the take-over was not asked: %v", tc.name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The operator's Approve switches config to the indexer: pull-declares is refused, the list and the pull requests'
|
||||
// config come from the indexer; hand-back is gone, and a switch naming the controller is read as the indexer; declared is written once per manifest (ADR 0565 §3, §4, §13).
|
||||
func TestTheTakeOverAndTheHandBack(t *testing.T) {
|
||||
c, m, r := anAgreeingMesh(t)
|
||||
// ADR 0590 (issue 1126): config reads its own sources always, with no take-over: declared is written once per
|
||||
// manifest, pull-declares is refused naming the indexer's candidates, and the list and a pull request's config come from
|
||||
// the indexer. compare, take-over and hand-back went with the controller.
|
||||
func TestConfigReadsItsOwnSourcesAlways(t *testing.T) {
|
||||
c, m, _ := anAgreeingMesh(t)
|
||||
steps := []struct {
|
||||
name string
|
||||
do func() (string, error)
|
||||
@@ -132,22 +60,7 @@ func TestTheTakeOverAndTheHandBack(t *testing.T) {
|
||||
again, _ := c.Reconcile()
|
||||
return strings.Join(first.Declared, ",") + "|" + strings.Join(again.Declared, ","), err
|
||||
}, "dunst|"},
|
||||
{"pull-declares taken before the take-over", func() (string, error) {
|
||||
a, err := c.PullDeclares(Call{Caller: ControllerCaller, Pull: "novox/mesh-catalog#7", Head: "h0",
|
||||
Declares: map[string]any{}})
|
||||
return answered(a), err
|
||||
}, "keeps what"},
|
||||
{"the take-over is asked", func() (string, error) {
|
||||
_ = m.Delete(PullsState, "novox.mesh-catalog.7") // compare holds the told pull request against no candidate
|
||||
a, err := c.TakeOver(Call{Why: "the release is delivered", Caller: "person.jochen"})
|
||||
return answered(a), err
|
||||
}, "is asked"},
|
||||
{"the operator approves: switched to the indexer", func() (string, error) {
|
||||
r.choose(t, r.lastAsk(t), OptionApprove, nil)
|
||||
out, err := c.Decide()
|
||||
return strings.Join(out, "; "), err
|
||||
}, "reads its own sources from now"},
|
||||
{"pull-declares refused after it, naming the indexer", func() (string, error) {
|
||||
{"pull-declares refused, naming the indexer", func() (string, error) {
|
||||
_, err := c.PullDeclares(Call{Caller: ControllerCaller, Pull: "novox/mesh-catalog#8", Head: "h1",
|
||||
Declares: map[string]any{}})
|
||||
return "", err
|
||||
@@ -164,15 +77,6 @@ func TestTheTakeOverAndTheHandBack(t *testing.T) {
|
||||
res, err := c.Reconcile()
|
||||
return strings.Join(res.Candidates, "; "), err
|
||||
}, "from the indexer"},
|
||||
{"hand-back is gone, naming the record (774/S9/AC1)", func() (string, error) {
|
||||
_, err := c.HandBack(Call{Why: "a difference was found after the switch", Caller: "person.jochen"})
|
||||
return "", err
|
||||
}, "ADR 0565 §4"},
|
||||
{"a switch naming the controller is read as gone, and the indexer holds (774/S9/AC2)", func() (string, error) {
|
||||
m.mirror(t, TakeoverState, SwitchKey, `{"source":"controller","by":"person.jochen","why":"old hand-back"}`)
|
||||
res, err := c.Reconcile()
|
||||
return res.SourceGone + "|" + string(m.resolved(t, "g14", "dunst", "width").Value), err
|
||||
}, "is gone: config no longer reads its records"},
|
||||
}
|
||||
for _, s := range steps {
|
||||
got, err := s.do()
|
||||
@@ -184,44 +88,7 @@ func TestTheTakeOverAndTheHandBack(t *testing.T) {
|
||||
t.Errorf("%s: said %q, want %q", s.name, text, s.want)
|
||||
}
|
||||
}
|
||||
if len(m.handActs) != 0 {
|
||||
t.Errorf("a hand-back that is gone recorded an act: %v", m.handActs)
|
||||
}
|
||||
if e, _ := m.Get(DeclaredState, "dunst"); e == nil || !strings.Contains(string(e.Value), `"manifest":"`) || !strings.Contains(string(e.Value), `"commit":"c1"`) {
|
||||
t.Errorf("declared holds no dunst with its manifest: %v", e)
|
||||
}
|
||||
// An Approve that finds compare differing at the answer switches nothing.
|
||||
c2, m2, r2 := anAgreeingMesh(t)
|
||||
if _, err := c2.TakeOver(Call{Why: "now", Caller: "person.jochen"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
candidate(t, m2, "novox/mesh-catalog#9", "h9", `{"dunst":{"gap":{"kind":"","type":"number","default":2}}}`)
|
||||
r2.choose(t, r2.lastAsk(t), OptionApprove, nil)
|
||||
out, _ := c2.Decide()
|
||||
if sw, _ := c2.source(); sw.Source != SourceInterim || !strings.Contains(strings.Join(out, ""), "difference") {
|
||||
t.Errorf("an Approve while compare differs switched: %s, %v", sw.Source, out)
|
||||
if sw, _ := c.source(); sw.Source != SourceIndexer {
|
||||
t.Errorf("the source is %q", sw.Source)
|
||||
}
|
||||
}
|
||||
|
||||
// lastAsk is the newest ask the router took.
|
||||
func (r *standIn) lastAsk(t *testing.T) string {
|
||||
t.Helper()
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
var newest string
|
||||
var at time.Time
|
||||
for id, a := range r.asks {
|
||||
if newest == "" || a.Expires.After(at) {
|
||||
newest, at = id, a.Expires
|
||||
}
|
||||
}
|
||||
if newest == "" {
|
||||
t.Fatal("the router took no ask")
|
||||
}
|
||||
return newest
|
||||
}
|
||||
|
||||
func answered(a any) string {
|
||||
raw, _ := json.Marshal(a)
|
||||
return string(raw)
|
||||
}
|
||||
|
||||
@@ -7,10 +7,7 @@
|
||||
"name": "mesh-config",
|
||||
"scope": "mesh",
|
||||
"serves": [
|
||||
"config",
|
||||
"compare",
|
||||
"take-over",
|
||||
"hand-back"
|
||||
"config"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -40,9 +37,6 @@
|
||||
"mesh-delivery-indexer.config-candidates"
|
||||
],
|
||||
"tools": [
|
||||
"config_compare",
|
||||
"config_take_over",
|
||||
"config_hand_back",
|
||||
"config_declared",
|
||||
"config_resync"
|
||||
],
|
||||
@@ -53,8 +47,7 @@
|
||||
"proposals",
|
||||
"pulls",
|
||||
"kept",
|
||||
"declared",
|
||||
"takeover"
|
||||
"declared"
|
||||
],
|
||||
"logs": [
|
||||
"history"
|
||||
|
||||
Reference in New Issue
Block a user