mesh-config stops reading the controller's predefined list and declared-config mirror (ADR 0565 §4)
Declarations come from the modules' own manifests and the list of predefined keys from the indexer's, whatever the switch says; compare keeps only the pull request rows; hand-back is answered as gone; a switch that still names the controller is read as the indexer and said gone. The read-only mirrors, mirrorDeclarations/mirrorList and the value and list comparisons are deleted with their test cases (two table tests lose their mirror rows, one block of the newest-list test goes; two table rows added). (issue 774, 774/S9) Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
@@ -12,8 +12,8 @@ account alone writes its buckets.
|
||||
- `mesh-config_resolved`: each assigned module's value for each key it declares on its node, keyed
|
||||
`<node>.<module>.<key>`, with its source: `default`, `mesh`, `node`, `module`, or `unset` for a required value
|
||||
nothing sets.
|
||||
- `mesh-config_predefined`: the list of predefined keys, published from the list the controller kept (its mirror
|
||||
`mesh-controller.predefined`), the newest trunk build winning. A newer list dropping a key that still holds a
|
||||
- `mesh-config_predefined`: the list of predefined keys, published from the indexer's list (the controller's mirror is no longer read, issue 774), the newest trunk
|
||||
build winning. A newer list dropping a key that still holds a
|
||||
value at the mesh or a node is withheld, and every listing says why, until that value is cleared.
|
||||
- `mesh-config_proposals`: each value proposed for the operator's Approve, whole, with the value stored at its level
|
||||
when it was proposed (what the operator is shown the change against), who proposed it, why, and what came of it.
|
||||
@@ -66,16 +66,15 @@ 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 (the interim since mesh-catalog #558:
|
||||
declarations from the modules' own manifests, the list of predefined keys and pull requests' config from the
|
||||
controller), `indexer` after the operator's take-over (the list and pull requests' config from the indexer's
|
||||
`predefined` and `config-candidates`; the controller's `pull-declares` refused), `controller` after a hand-back
|
||||
(declarations, list and pull requests' config from the controller's records). Each take-over ask is kept beside it.
|
||||
- `config_compare` lists every difference between the own sources and the controller's records: each module's
|
||||
declarations, the list, each open pull request's config, and every placed module's value on every node resolved
|
||||
both ways. `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` returns to the controller's records until its lease is handed
|
||||
away. `config_declared` and `config_resync` read and write again what it keeps.
|
||||
- `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.
|
||||
- From the controller's lease write a trusted value is set only through propose and the operator's Approve.
|
||||
- Its sets, clears and warrants are recorded in the hand-act log of the seat `mesh-hand-acts`, falling back to the
|
||||
controller's while that seat has no holder and the controller holds its lease.
|
||||
|
||||
@@ -244,10 +244,9 @@ const dunstDeclared = `{"module":"dunst","settings":{
|
||||
"position":{"kind":"","default":"top-right","why":"","type":"string","description":"where notifications show","enum":["top-right","top-left"]},
|
||||
"origin":{"kind":"","default":null,"why":"","type":"string","description":"the operator's own origin","required":true,"pattern":"[a-z]+"}}}`
|
||||
|
||||
// theList is the controller's predefined mirror, as it writes it.
|
||||
const theList = `{"keys":{"agent-managed-settings":{"type":"object","description":"managed settings","trusted":true},
|
||||
"font-size":{"type":"number","description":"text size, in points","minimum":6,"maximum":72}},
|
||||
"repository":"mesh-catalog","commit":"aaaaaaaaaaaa","generation":5,"build":"b5"}`
|
||||
// theList is the indexer's list of predefined keys, as predefined.json holds it.
|
||||
const theList = `{"agent-managed-settings":{"type":"object","description":"managed settings","trusted":true},
|
||||
"font-size":{"type":"number","description":"text size, in points","minimum":6,"maximum":72}}`
|
||||
|
||||
func aMesh(t *testing.T) (*Config, *memStore) {
|
||||
t.Helper()
|
||||
@@ -256,7 +255,7 @@ func aMesh(t *testing.T) (*Config, *memStore) {
|
||||
m.declare(t, "claude-code", `{"module":"claude-code","settings":{
|
||||
"managed":{"kind":"","default":{},"why":"","type":"object","description":"managed settings","trusted":true,"predefined":"agent-managed-settings"}}}`)
|
||||
m.declare(t, "old", `{"module":"old","settings":{"workers":{"kind":"preference","default":4,"why":"two per core"}}}`)
|
||||
m.mirror(t, PredefinedRead, PredefinedMirrorKey, theList)
|
||||
indexerList(t, m, "aaaaaaaaaaaa", theList)
|
||||
for _, node := range []string{"g14", "novox"} {
|
||||
m.assign(t, node, "dunst")
|
||||
}
|
||||
@@ -507,7 +506,7 @@ func TestAChangeTheHandActLogRefusesIsSaid(t *testing.T) {
|
||||
expect(t, m, "g14", "12", SourceMesh)
|
||||
}
|
||||
|
||||
// **The newest list of predefined keys is published, and an older one heard later is not** (ADR 0348 §7).
|
||||
// **The newest list of predefined keys is published** (ADR 0348 §7; the indexer never keeps an older merge's).
|
||||
func TestTheNewestListOfPredefinedKeysIsPublished(t *testing.T) {
|
||||
c, m := aMesh(t)
|
||||
published := func() map[string]Published {
|
||||
@@ -521,24 +520,17 @@ func TestTheNewestListOfPredefinedKeysIsPublished(t *testing.T) {
|
||||
}
|
||||
return out
|
||||
}
|
||||
if p := published(); len(p) != 2 || p["font-size"].Generation != 5 || !p["agent-managed-settings"].Trusted {
|
||||
if p := published(); len(p) != 2 || p["font-size"].Commit != "aaaaaaaaaaaa" || !p["agent-managed-settings"].Trusted {
|
||||
t.Fatalf("published %+v", p)
|
||||
}
|
||||
m.mirror(t, PredefinedRead, PredefinedMirrorKey, `{"keys":{"font-size":{"type":"number","description":"x"},
|
||||
"theme":{"type":"string","description":"the theme"}},"commit":"bbbb","generation":7}`)
|
||||
indexerList(t, m, "bbbb", `{"font-size":{"type":"number","description":"x"},
|
||||
"theme":{"type":"string","description":"the theme"}}`)
|
||||
if _, err := c.Reconcile(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if p := published(); len(p) != 2 || p["theme"].Generation != 7 || p["agent-managed-settings"].Type != "" {
|
||||
if p := published(); len(p) != 2 || p["theme"].Commit != "bbbb" || p["agent-managed-settings"].Type != "" {
|
||||
t.Fatalf("a newer list published %+v", p)
|
||||
}
|
||||
m.mirror(t, PredefinedRead, PredefinedMirrorKey, theList) // generation 5, heard later
|
||||
if _, err := c.Reconcile(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if p := published(); p["theme"].Generation != 7 {
|
||||
t.Fatalf("an older list replaced the newer one: %+v", p)
|
||||
}
|
||||
}
|
||||
|
||||
// **A list dropping a key that still holds a value is withheld** (ADR 0348 §7): the list published stands, and the
|
||||
@@ -546,8 +538,7 @@ func TestTheNewestListOfPredefinedKeysIsPublished(t *testing.T) {
|
||||
func TestAListDroppingAKeyThatHoldsAValueIsWithheldUntilItIsCleared(t *testing.T) {
|
||||
c, m := aMesh(t)
|
||||
set(t, c, Call{Key: "font-size", Value: 12.0, Node: "novox"})
|
||||
m.mirror(t, PredefinedRead, PredefinedMirrorKey, `{"keys":{"agent-managed-settings":{"type":"object",
|
||||
"description":"managed settings","trusted":true}},"commit":"cccccccccc","generation":9}`)
|
||||
indexerList(t, m, "cccccccccc", `{"agent-managed-settings":{"type":"object","description":"managed settings","trusted":true}}`)
|
||||
if _, err := c.Reconcile(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ type PredefinedKey struct {
|
||||
Trusted bool `json:"trusted,omitempty"`
|
||||
}
|
||||
|
||||
// PredefinedList is the list the controller kept, as its predefined mirror holds it under PredefinedMirrorKey.
|
||||
// PredefinedList is the list of predefined keys with the commit it came from, as the indexer keeps it.
|
||||
type PredefinedList struct {
|
||||
Keys map[string]PredefinedKey `json:"keys"`
|
||||
Repository string `json:"repository"`
|
||||
@@ -138,9 +138,6 @@ type PredefinedList struct {
|
||||
Build string `json:"build"`
|
||||
}
|
||||
|
||||
// PredefinedMirrorKey is the controller's predefined mirror's one key.
|
||||
const PredefinedMirrorKey = "list"
|
||||
|
||||
// Published is one key of mesh-config_predefined: the key's declaration, and the build of the list it came from.
|
||||
type Published struct {
|
||||
PredefinedKey
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// asked under this module's own name, design 57 K4).
|
||||
// It resolves from where the seat mesh-placement places each module, and each module's declarations as its own
|
||||
// manifests declare them, the trunk's and its wanted build's (the indexer's and the builder's state, issue 1953); the
|
||||
// list of predefined keys still comes from the controller's mirror until the indexer keeps it (ADR 0565 §2). stdout is the MCP channel; what this module says, it says on stderr.
|
||||
// list of predefined keys comes from the indexer's (ADR 0565 §2); the controller's records are no longer read (issue 774). stdout is the MCP channel; what this module says, it says on stderr.
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -132,8 +132,8 @@ 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, PredefinedRead, IndexerPredefinedRead,
|
||||
CandidatesRead, MirrorDeclaredRead, TakeoverState} {
|
||||
for _, read := range []string{PlacementRead, WantsRead, ModulesRead, IndexerPredefinedRead,
|
||||
CandidatesRead, TakeoverState} {
|
||||
// Asked again until the runtime takes it, each give-up said (mesh-sdk's Follow).
|
||||
go stdio.State(read).Follow("", poke)
|
||||
}
|
||||
|
||||
@@ -33,8 +33,7 @@ func TestEveryStateAndActHasItsTool(t *testing.T) {
|
||||
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, PredefinedRead: CompareTool, MirrorDeclaredRead: CompareTool,
|
||||
IndexerPredefinedRead: CompareTool, CandidatesRead: CompareTool, LeaseRead: HandBackTool,
|
||||
ManifestsRead: DeclaredTool, IndexerPredefinedRead: CompareTool, CandidatesRead: CompareTool, LeaseRead: HandBackTool,
|
||||
"a resolution": ResyncTool, "the indexer's candidates taken": ResyncTool, "a take-over decided": TakeOverTool,
|
||||
}
|
||||
served := map[string]bool{}
|
||||
|
||||
@@ -34,9 +34,6 @@ const (
|
||||
// repository holds it (ADR 0437 §5, ADR 0478 §9); ManifestsRead its manifests by digest (ADR 0429 §16).
|
||||
ModulesRead = delivery.IndexerModule + ".modules"
|
||||
ManifestsRead = delivery.IndexerModule + ".manifests"
|
||||
// PredefinedRead is the controller's list of predefined keys, which ADR 0565 §2 moves to the indexer's
|
||||
// `predefined`: read here until that state exists.
|
||||
PredefinedRead = "mesh-controller.predefined"
|
||||
|
||||
// HistoryLog is the log of every set and clear, with the proven caller and why (ADR 0348 §9).
|
||||
HistoryLog = "history"
|
||||
|
||||
@@ -14,17 +14,8 @@ import (
|
||||
|
||||
// sources reads everything a resolution reads, from the bus as it is now, from the source the switch names
|
||||
// (takeover.go): which modules are placed where from the seat mesh-placement, and each placed module's declarations
|
||||
// from its own manifests as built (issue 1953, ADR 0565 §3), or after a hand-back from the controller's mirror.
|
||||
// from its own manifests as built (issue 1953, ADR 0565 §3); the controller's records are no longer read.
|
||||
func (c *Config) sources() (Sources, error) {
|
||||
sw, err := c.source()
|
||||
if err != nil {
|
||||
return Sources{}, err
|
||||
}
|
||||
return c.sourcesFrom(sw.Source)
|
||||
}
|
||||
|
||||
// sourcesFrom reads the sources with declarations from one source: the own (interim and indexer) or the mirror.
|
||||
func (c *Config) sourcesFrom(source string) (Sources, error) {
|
||||
s := Sources{Declared: map[string]ModuleDeclarations{}, OnNode: map[string]ModuleDeclarations{},
|
||||
Predefined: map[string]Published{}, Values: map[string]Value{}}
|
||||
var problems []string
|
||||
@@ -34,9 +25,6 @@ func (c *Config) sourcesFrom(source string) (Sources, error) {
|
||||
if err := c.readDeclarations(&s); err != nil {
|
||||
return s, fmt.Errorf("the modules' declarations cannot be read: %w", err)
|
||||
}
|
||||
if source == SourceController {
|
||||
c.mirrorDeclarations(&s)
|
||||
}
|
||||
for _, u := range s.Unresolved {
|
||||
problems = append(problems, fmt.Sprintf("%s on %s: %s", u.Module, u.Node, u.Why))
|
||||
}
|
||||
@@ -366,6 +354,8 @@ type Resolution struct {
|
||||
// did after the take-over.
|
||||
Declared []string `json:"declared,omitempty"`
|
||||
Candidates []string `json:"candidates,omitempty"`
|
||||
// SourceGone is said while the stored switch still names the controller, whose records are no longer read.
|
||||
SourceGone string `json:"source_gone,omitempty"`
|
||||
}
|
||||
|
||||
// Reconcile publishes the newest list of predefined keys and brings mesh-config_resolved to what the sources
|
||||
@@ -375,6 +365,10 @@ 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)
|
||||
@@ -401,24 +395,16 @@ func (c *Config) Reconcile() (Resolution, error) {
|
||||
return r, err
|
||||
}
|
||||
|
||||
// publish makes mesh-config_predefined hold the list its source keeps — the indexer's after the take-over, the
|
||||
// controller's mirror otherwise — when it is newer than the one published (ADR 0348 §7: the newest trunk build wins;
|
||||
// publish makes mesh-config_predefined hold the indexer's list (the controller's mirror is no longer read, issue 774)
|
||||
// when it is newer than the one published (ADR 0348 §7: the newest trunk build wins;
|
||||
// the indexer never keeps an older merge's, so its list is newer when its commit differs). **A list dropping a key that still holds a value is withheld**: the
|
||||
// list published stays, and why is said in every listing and in the log, because a value for a key no longer on
|
||||
// the list could neither be resolved nor cleared by name (§7: removing a key still holding a value is refused).
|
||||
// It answers what it published, empty when nothing changed.
|
||||
func (c *Config) publish() (string, error) {
|
||||
sw, err := c.source()
|
||||
list, found, err := c.indexerList()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
read, whose := c.mirrorList, "the controller's list"
|
||||
if sw.Source == SourceIndexer {
|
||||
read, whose = c.indexerList, "the indexer's list"
|
||||
}
|
||||
list, found, err := read()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%s cannot be read: %w", whose, err)
|
||||
return "", fmt.Errorf("the indexer's list cannot be read: %w", err)
|
||||
}
|
||||
if !found {
|
||||
return "", nil // no list kept yet
|
||||
|
||||
@@ -41,11 +41,10 @@ func ownSwitchTools(c *Config) []stdio.Tool {
|
||||
whyOf := func(a map[string]any) string { s, _ := a["why"].(string); return strings.TrimSpace(s) }
|
||||
return []stdio.Tool{
|
||||
{Name: CompareTool,
|
||||
Description: "Every difference between config's own sources and the controller's records (novox/hq ADR 0565 §4): " +
|
||||
"each placed module's declarations (declared against the mirror declared-config), the list of predefined " +
|
||||
"keys (the indexer's against the mirror's), each open pull request's config (the indexer's candidate " +
|
||||
"against the controller's pull-declares), and every placed module's value on every node resolved both " +
|
||||
"ways; each with its reason. The take-over is refused while any stands.",
|
||||
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: " +
|
||||
@@ -56,8 +55,8 @@ func ownSwitchTools(c *Config) []stdio.Tool {
|
||||
return c.TakeOver(Call{Why: whyOf(a), Caller: stdio.CallerOf(a)})
|
||||
}},
|
||||
{Name: HandBackTool,
|
||||
Description: "Return config to the controller's records (its mirrors declared-config and predefined, and " +
|
||||
"pull-declares) until the controller hands its lease away; gone after it. Recorded as a hand act.",
|
||||
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)})
|
||||
|
||||
@@ -2,19 +2,19 @@ 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 its own sources and the controller's mirrors — each module's declarations, the predefined list,
|
||||
// each open pull request's config declarations, and every placed module's resolved value on every node both ways;
|
||||
// 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; `hand-back` returns to the mirrors until the lease write.
|
||||
// key `takeover.switch` naming the indexer.
|
||||
//
|
||||
// **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 (the interim of mesh-catalog #558): declarations from each placed module's own manifests, the list
|
||||
// of predefined keys from the controller's mirror, a pull request's declarations from the controller's
|
||||
// pull-declares;
|
||||
// - `indexer` (taken over): declarations as before, the list from the indexer's `predefined`, a pull request's from
|
||||
// the indexer's `config-candidates`; pull-declares refused;
|
||||
// - `controller` (handed back): declarations from the mirror `declared-config`, the list from the mirror,
|
||||
// pull-declares taken.
|
||||
// - 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`.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -38,9 +38,6 @@ const (
|
||||
TakeoverState = "takeover"
|
||||
SwitchKey = "switch"
|
||||
|
||||
// MirrorDeclaredRead is the controller's mirror of every registered module's declarations, one key per module:
|
||||
// what `compare` holds the own source to, and the source after a hand-back.
|
||||
MirrorDeclaredRead = "mesh-controller.declared-config"
|
||||
// IndexerPredefinedRead and CandidatesRead are the indexer's states (ADR 0565 §2).
|
||||
IndexerPredefinedRead = delivery.IndexerModule + "." + delivery.IndexerPredefinedState
|
||||
CandidatesRead = delivery.IndexerModule + "." + delivery.IndexerConfigCandidatesState
|
||||
@@ -50,8 +47,9 @@ const (
|
||||
|
||||
// The sources the switch names.
|
||||
const (
|
||||
SourceInterim = "interim"
|
||||
SourceIndexer = "indexer"
|
||||
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"
|
||||
)
|
||||
|
||||
@@ -69,8 +67,15 @@ type Switch struct {
|
||||
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:"-"`
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (c *Config) source() (Switch, error) {
|
||||
e, err := c.Store.Get(TakeoverState, SwitchKey)
|
||||
@@ -84,6 +89,9 @@ func (c *Config) source() (Switch, error) {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -172,31 +180,6 @@ func (c *Config) writeDeclared(s Sources) ([]string, error) {
|
||||
return written, errors.Join(problems...)
|
||||
}
|
||||
|
||||
// mirrorDeclarations makes the sources' declarations the controller's mirror's, for every placed module: the source
|
||||
// after a hand-back, and the other side of compare.
|
||||
func (c *Config) mirrorDeclarations(s *Sources) {
|
||||
s.Declared, s.OnNode, s.Unresolved = map[string]ModuleDeclarations{}, map[string]ModuleDeclarations{}, nil
|
||||
for _, module := range modulesOf(s.Assignments) {
|
||||
e, err := c.Store.Get(MirrorDeclaredRead, module)
|
||||
var d ModuleDeclarations
|
||||
if err == nil && e != nil {
|
||||
d, err = ParseModuleDeclarations(e.Value)
|
||||
} else if err == nil {
|
||||
err = errors.New("the mirror holds no key for it")
|
||||
}
|
||||
if err != nil {
|
||||
for _, a := range s.Assignments {
|
||||
if a.Module == module {
|
||||
s.Unresolved = append(s.Unresolved, Unresolved{Node: a.Node, Module: module,
|
||||
Why: "the controller's mirror declared-config: " + err.Error()})
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
s.Declared[module] = d
|
||||
}
|
||||
}
|
||||
|
||||
// ---- the predefined list and the pull requests from the indexer ------------------------------------------
|
||||
|
||||
// indexerList is the indexer's predefined list as the mirror's list holds one: the keys, and the commit.
|
||||
@@ -216,19 +199,6 @@ func (c *Config) indexerList() (PredefinedList, bool, error) {
|
||||
return list, true, nil
|
||||
}
|
||||
|
||||
// mirrorList is the controller's mirror's list.
|
||||
func (c *Config) mirrorList() (PredefinedList, bool, error) {
|
||||
e, err := c.Store.Get(PredefinedRead, PredefinedMirrorKey)
|
||||
if err != nil || e == nil {
|
||||
return PredefinedList{}, false, err
|
||||
}
|
||||
var list PredefinedList
|
||||
if err := json.Unmarshal(e.Value, &list); err != nil || list.Keys == nil {
|
||||
return PredefinedList{}, false, fmt.Errorf("the controller's list is not one: %s", e.Value)
|
||||
}
|
||||
return list, true, nil
|
||||
}
|
||||
|
||||
// candidates is every open pull request's config declarations as the indexer keeps them, by pull key.
|
||||
func (c *Config) candidates() (map[string]PullDecl, error) {
|
||||
out := map[string]PullDecl{}
|
||||
@@ -300,7 +270,7 @@ func declaresDigest(d map[string]map[string]Declaration) string {
|
||||
|
||||
// ---- compare ----------------------------------------------------------------------------------------------
|
||||
|
||||
// Difference is one thing the own sources and the controller's mirrors say differently, and why.
|
||||
// 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"`
|
||||
@@ -308,16 +278,12 @@ type Difference struct {
|
||||
Key string `json:"key,omitempty"`
|
||||
Pull string `json:"pull,omitempty"`
|
||||
Own json.RawMessage `json:"own,omitempty"`
|
||||
Mirror json.RawMessage `json:"mirror,omitempty"`
|
||||
Why string `json:"why"`
|
||||
}
|
||||
|
||||
// The kinds of difference.
|
||||
const (
|
||||
DiffDeclared = "declared"
|
||||
DiffPredefined = "predefined"
|
||||
DiffPull = "pull"
|
||||
DiffValue = "value"
|
||||
DiffPull = "pull"
|
||||
)
|
||||
|
||||
// Comparison is what compare answers.
|
||||
@@ -326,6 +292,8 @@ type Comparison struct {
|
||||
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.
|
||||
@@ -335,93 +303,20 @@ func (c *Config) Compare() (*Comparison, error) {
|
||||
return c.compare()
|
||||
}
|
||||
|
||||
// compare is Compare; the caller holds the lock.
|
||||
// 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
|
||||
}
|
||||
own, err := c.sourcesFrom(SourceInterim)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("mesh-config's own sources cannot be read: %w", 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)
|
||||
}
|
||||
mirror := own
|
||||
mirror.Declared, mirror.OnNode, mirror.Unresolved = nil, nil, nil
|
||||
c.mirrorDeclarations(&mirror)
|
||||
var diffs []Difference
|
||||
|
||||
// A placed module no catalog manifest names (mesh-wireguard, which the mesh ships computed) whose mirror key holds
|
||||
// no config key declares nothing on both sides: nothing resolves from it, so nothing differs (issue 1953, L5d).
|
||||
declaresNothing := map[string]bool{}
|
||||
for module, d := range mirror.Declared {
|
||||
if len(d.Config) == 0 {
|
||||
// Only an indexer that was read and holds no modules.<module>: a failed read says nothing, and the row stays.
|
||||
if key, kerr := delivery.ModulesKey(module); kerr == nil {
|
||||
if e, err := c.Store.Get(ModulesRead, key); err == nil && e == nil {
|
||||
declaresNothing[module] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Each placed module's declarations: `declared` against the mirror's key for it.
|
||||
for _, module := range modulesOf(own.Assignments) {
|
||||
var ownRaw []byte
|
||||
if e, err := c.Store.Get(DeclaredState, module); err == nil && e != nil {
|
||||
var k DeclaredKept
|
||||
if json.Unmarshal(e.Value, &k) == nil {
|
||||
ownRaw, _ = json.Marshal(normal(k.Config))
|
||||
}
|
||||
}
|
||||
var mirrorRaw []byte
|
||||
if d, ok := mirror.Declared[module]; ok {
|
||||
mirrorRaw, _ = json.Marshal(normal(d.Config))
|
||||
}
|
||||
switch {
|
||||
case ownRaw == nil && declaresNothing[module]:
|
||||
case ownRaw == nil && mirrorRaw == nil:
|
||||
diffs = append(diffs, Difference{What: DiffDeclared, Module: module, Why: "neither side holds its declarations"})
|
||||
case ownRaw == nil:
|
||||
diffs = append(diffs, Difference{What: DiffDeclared, Module: module, Mirror: mirrorRaw,
|
||||
Why: "the mirror holds its declarations and declared does not"})
|
||||
case mirrorRaw == nil:
|
||||
diffs = append(diffs, Difference{What: DiffDeclared, Module: module, Own: ownRaw,
|
||||
Why: "declared holds its declarations and the mirror does not"})
|
||||
case !bytes.Equal(ownRaw, mirrorRaw):
|
||||
diffs = append(diffs, Difference{What: DiffDeclared, Module: module, Own: ownRaw, Mirror: mirrorRaw,
|
||||
Why: "the trunk's manifest declares it otherwise than the mirror"})
|
||||
}
|
||||
}
|
||||
|
||||
// The predefined list: the indexer's against the mirror's.
|
||||
il, iok, ierr := c.indexerList()
|
||||
ml, mok, merr := c.mirrorList()
|
||||
switch {
|
||||
case ierr != nil || merr != nil:
|
||||
diffs = append(diffs, Difference{What: DiffPredefined, Why: "a list cannot be read: " + errors.Join(ierr, merr).Error()})
|
||||
case !iok || !mok:
|
||||
if iok != mok {
|
||||
diffs = append(diffs, Difference{What: DiffPredefined, Why: fmt.Sprintf("the indexer holds a list: %v; the "+
|
||||
"mirror holds one: %v", iok, mok)})
|
||||
}
|
||||
default:
|
||||
for _, k := range unionKeys(il.Keys, ml.Keys) {
|
||||
a, aok := il.Keys[k]
|
||||
b, bok := ml.Keys[k]
|
||||
ar, _ := json.Marshal(a)
|
||||
br, _ := json.Marshal(b)
|
||||
switch {
|
||||
case !aok:
|
||||
diffs = append(diffs, Difference{What: DiffPredefined, Key: k, Mirror: br, Why: "only the mirror's list holds it"})
|
||||
case !bok:
|
||||
diffs = append(diffs, Difference{What: DiffPredefined, Key: k, Own: ar, Why: "only the indexer's list holds it"})
|
||||
case !bytes.Equal(ar, br):
|
||||
diffs = append(diffs, Difference{What: DiffPredefined, Key: k, Own: ar, Mirror: br,
|
||||
Why: "the two lists declare it otherwise"})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Each open pull request: the indexer's candidate against what the controller told by pull-declares.
|
||||
cands, cerr := c.candidates()
|
||||
if cerr != nil {
|
||||
@@ -452,39 +347,6 @@ func (c *Config) compare() (*Comparison, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Every placed module's value on every node, resolved both ways.
|
||||
ownValues, mirrorValues := Resolve(own), Resolve(mirror)
|
||||
for _, k := range unionKeys(ownValues, mirrorValues) {
|
||||
a, aok := ownValues[k]
|
||||
b, bok := mirrorValues[k]
|
||||
ar, _ := json.Marshal(a)
|
||||
br, _ := json.Marshal(b)
|
||||
node, module, key := a.Node, a.Module, a.Key
|
||||
if !aok {
|
||||
node, module, key = b.Node, b.Module, b.Key
|
||||
}
|
||||
d := Difference{What: DiffValue, Node: node, Module: module, Key: key}
|
||||
switch {
|
||||
case !aok:
|
||||
d.Mirror, d.Why = br, "only the mirror's declarations resolve it on this node"
|
||||
case !bok:
|
||||
d.Own, d.Why = ar, "only mesh-config's own declarations (with this node's build) resolve it"
|
||||
case !bytes.Equal(ar, br):
|
||||
d.Own, d.Mirror, d.Why = ar, br, "it resolves to another value or source"
|
||||
default:
|
||||
continue
|
||||
}
|
||||
diffs = append(diffs, d)
|
||||
}
|
||||
for _, u := range mirror.Unresolved {
|
||||
diffs = append(diffs, Difference{What: DiffValue, Node: u.Node, Module: u.Module, Why: "not resolved from the mirror: " + u.Why})
|
||||
}
|
||||
for _, u := range own.Unresolved {
|
||||
if declaresNothing[u.Module] {
|
||||
continue
|
||||
}
|
||||
diffs = append(diffs, Difference{What: DiffValue, Node: u.Node, Module: u.Module, Why: "not resolved from its own source: " + u.Why})
|
||||
}
|
||||
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
|
||||
@@ -492,9 +354,9 @@ func (c *Config) compare() (*Comparison, error) {
|
||||
if diffs == nil {
|
||||
diffs = []Difference{}
|
||||
}
|
||||
cmp := &Comparison{Source: sw.Source, Differences: diffs, Count: len(diffs)}
|
||||
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"
|
||||
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))
|
||||
}
|
||||
@@ -548,20 +410,6 @@ func forgeNoteOf(v string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// normal is declarations in one encoding: a null default as none.
|
||||
func normal(d map[string]Declaration) map[string]Declaration {
|
||||
out := map[string]Declaration{}
|
||||
for k, decl := range d {
|
||||
if !decl.HasDefault() {
|
||||
decl.Default = nil
|
||||
} else {
|
||||
decl.Default = compact(decl.Default)
|
||||
}
|
||||
out[k] = decl
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func unionKeys[A, B any](a map[string]A, b map[string]B) []string {
|
||||
seen := map[string]bool{}
|
||||
for k := range a {
|
||||
@@ -602,13 +450,12 @@ 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 the list of predefined keys and each pull request's config from the delivery's " +
|
||||
"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; config keeps reading the controller's records", About: "config.takeover",
|
||||
Details: "Read what was compared with mesh-config's compare verb, through the mesh MCP server. hand-back returns " +
|
||||
"to the controller's records until the controller's lease is handed away."}
|
||||
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"},
|
||||
@@ -770,7 +617,8 @@ func approvedTakeover(w asks.Warrant) string {
|
||||
return fmt.Sprintf("the %s, as %s identity %s", w.By.Who, w.By.Kind, w.By.Identity)
|
||||
}
|
||||
|
||||
// HandBack returns to the controller's records until the controller's lease is handed away.
|
||||
// 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")
|
||||
@@ -778,30 +626,8 @@ func (c *Config) HandBack(call Call) (*TakeoverAnswer, error) {
|
||||
if call.Caller == "" || call.Caller == Unproven {
|
||||
return nil, errors.New("the call names no proven caller")
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.leaseAway() {
|
||||
return nil, errors.New("hand-back is gone: the controller handed its lease away, and its records end with it " +
|
||||
"(novox/hq ADR 0565 §4)")
|
||||
}
|
||||
sw, err := c.source()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if sw.Source == SourceController {
|
||||
return &TakeoverAnswer{Source: sw.Source, Said: "config reads the controller's records already"}, nil
|
||||
}
|
||||
now := c.now()
|
||||
back := Switch{Source: SourceController, At: now.UTC(), By: call.Caller, Why: strings.TrimSpace(call.Why)}
|
||||
if err := c.Store.Put(TakeoverState, SwitchKey, back); err != nil {
|
||||
return nil, fmt.Errorf("the switch could not be written: %w", err)
|
||||
}
|
||||
answer := &TakeoverAnswer{Source: back.Source, Said: "config reads the controller's records again: its mirrors " +
|
||||
"declared-config and predefined, and pull-declares"}
|
||||
if err := c.Store.HandAct("handed config back to the controller's records", back.Why, "config-hand-back"); err != nil {
|
||||
answer.Said += "; the hand-act log does not hold it: " + err.Error()
|
||||
}
|
||||
return answer, nil
|
||||
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.
|
||||
|
||||
@@ -11,21 +11,17 @@ import (
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// The trunk's manifests aMesh declares, as the controller's mirror declared-config holds the same declarations.
|
||||
// The trunk's manifests aMesh declares.
|
||||
const (
|
||||
claudeDeclared = `{"module":"claude-code","settings":{
|
||||
"managed":{"kind":"","default":{},"why":"","type":"object","description":"managed settings","trusted":true,"predefined":"agent-managed-settings"}}}`
|
||||
oldDeclared = `{"module":"old","settings":{"workers":{"kind":"preference","default":4,"why":"two per core"}}}`
|
||||
)
|
||||
|
||||
// anAgreeingMesh is aProposingMesh whose controller mirrors say what mesh-config's own sources say: the mirror's
|
||||
// declarations of every placed module, and the indexer's predefined list equal to the mirror's.
|
||||
// anAgreeingMesh is aProposingMesh with the indexer's predefined list in place and no open pull request: compare is empty.
|
||||
func anAgreeingMesh(t *testing.T) (*Config, *memStore, *standIn) {
|
||||
t.Helper()
|
||||
c, m, r := aProposingMesh(t)
|
||||
m.mirror(t, MirrorDeclaredRead, "dunst", dunstDeclared)
|
||||
m.mirror(t, MirrorDeclaredRead, "claude-code", claudeDeclared)
|
||||
m.mirror(t, MirrorDeclaredRead, "old", oldDeclared)
|
||||
indexerList(t, m, "aaaaaaaaaaaa", `{"agent-managed-settings":{"type":"object","description":"managed settings","trusted":true},
|
||||
"font-size":{"type":"number","description":"text size, in points","minimum":6,"maximum":72}}`)
|
||||
if _, err := c.Reconcile(); err != nil {
|
||||
@@ -65,9 +61,9 @@ func kinds(cmp *Comparison) []string {
|
||||
return out
|
||||
}
|
||||
|
||||
// compare lists every difference with its reason — a module's declarations, the predefined list, a pull request's
|
||||
// config, and each value a node resolves both ways — and the take-over is refused while one stands (ADR 0565 §4 and
|
||||
// its insight of 2026-10-29; issue 1953 L5-C4).
|
||||
// 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
|
||||
@@ -75,17 +71,6 @@ func TestCompareListsEveryDifferenceAndTheTakeOverWaitsForNone(t *testing.T) {
|
||||
want []string
|
||||
}{
|
||||
{"all agree", func(*testing.T, *memStore) {}, nil},
|
||||
{"the mirror declares a default otherwise", func(t *testing.T, m *memStore) {
|
||||
m.mirror(t, MirrorDeclaredRead, "dunst", strings.Replace(dunstDeclared, `"default":300`, `"default":400`, 1))
|
||||
}, []string{"declared:dunst", "value:dunstwidth@g14", "value:dunstwidth@novox"}},
|
||||
{"the mirror lacks a placed module", func(t *testing.T, m *memStore) { _ = m.Delete(MirrorDeclaredRead, "old") },
|
||||
[]string{"declared:old", "value:old@g14", "value:oldworkers@g14"}},
|
||||
{"a node's build adds a key the mirror lacks", func(t *testing.T, m *memStore) {
|
||||
m.want(t, "g14", "dunst", "src2:"+strings.Repeat("ab", 32), strings.Replace(dunstDeclared, `"width"`, `"height":{"type":"number","default":5},"width"`, 1))
|
||||
}, []string{"value:dunstheight@g14", "value:dunstheight@novox"}}, // a key only a build declares resolves on every node (#558)
|
||||
{"the lists differ", func(t *testing.T, m *memStore) {
|
||||
indexerList(t, m, "bbbbbbbbbbbb", `{"font-size":{"type":"number","description":"text size, in points","minimum":6,"maximum":72}}`)
|
||||
}, []string{"predefined:agent-managed-settings"}},
|
||||
{"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"}},
|
||||
@@ -96,10 +81,6 @@ func TestCompareListsEveryDifferenceAndTheTakeOverWaitsForNone(t *testing.T) {
|
||||
{"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"}},
|
||||
{"a module no manifest names and the mirror declares nothing of", func(t *testing.T, m *memStore) {
|
||||
m.assign(t, "g14", "mesh-wireguard")
|
||||
m.mirror(t, MirrorDeclaredRead, "mesh-wireguard", `{"module":"mesh-wireguard","settings":{}}`)
|
||||
}, nil},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
c, m, r := anAgreeingMesh(t)
|
||||
@@ -137,7 +118,7 @@ func TestCompareListsEveryDifferenceAndTheTakeOverWaitsForNone(t *testing.T) {
|
||||
}
|
||||
|
||||
// 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 returns to the mirrors until the lease write and is gone after it; from the
|
||||
// config come from the indexer; hand-back is gone, and a switch naming the controller is read as the indexer; from the
|
||||
// lease write a trusted value is set only on an Approve; declared is written once per manifest (ADR 0565 §3, §4, §13).
|
||||
func TestTheTakeOverAndTheHandBack(t *testing.T) {
|
||||
c, m, r := anAgreeingMesh(t)
|
||||
@@ -184,20 +165,19 @@ func TestTheTakeOverAndTheHandBack(t *testing.T) {
|
||||
res, err := c.Reconcile()
|
||||
return strings.Join(res.Candidates, "; "), err
|
||||
}, "from the indexer"},
|
||||
{"hand-back returns to the mirrors", func() (string, error) {
|
||||
a, err := c.HandBack(Call{Why: "a difference was found after the switch", Caller: "person.jochen"})
|
||||
return answered(a), err
|
||||
}, "controller's records again"},
|
||||
{"after it, declarations resolve from the mirror", func() (string, error) {
|
||||
m.mirror(t, MirrorDeclaredRead, "dunst", strings.Replace(dunstDeclared, `"default":300`, `"default":400`, 1))
|
||||
_, err := c.Reconcile()
|
||||
return string(m.resolved(t, "g14", "dunst", "width").Value), err
|
||||
}, "400"},
|
||||
{"hand-back is gone after the lease write", func() (string, error) {
|
||||
m.mirror(t, LeaseRead, "holder", `{"handed-to":"the operator's hand-over"}`)
|
||||
_, err := c.HandBack(Call{Why: "again", Caller: "person.jochen"})
|
||||
{"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
|
||||
}, "gone"},
|
||||
}, "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"},
|
||||
{"the lease is written", func() (string, error) {
|
||||
m.mirror(t, LeaseRead, "holder", `{"handed-to":"the operator's hand-over"}`)
|
||||
return "lease", nil
|
||||
}, "lease"},
|
||||
{"the terminal sets no trusted value after the lease write", func() (string, error) {
|
||||
_, err := c.Change(Call{Action: "set", Key: "managed", Module: "claude-code", Node: "g14", Value: map[string]any{},
|
||||
Why: "x", Caller: ControllerCaller, By: "person.jochen"})
|
||||
@@ -214,8 +194,8 @@ func TestTheTakeOverAndTheHandBack(t *testing.T) {
|
||||
t.Errorf("%s: said %q, want %q", s.name, text, s.want)
|
||||
}
|
||||
}
|
||||
if !slices.Contains(m.handActs, "handed config back to the controller's records | a difference was found after the switch | config-hand-back") {
|
||||
t.Errorf("the hand-back is not recorded: %v", m.handActs)
|
||||
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)
|
||||
@@ -225,7 +205,7 @@ func TestTheTakeOverAndTheHandBack(t *testing.T) {
|
||||
if _, err := c2.TakeOver(Call{Why: "now", Caller: "person.jochen"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m2.mirror(t, MirrorDeclaredRead, "dunst", strings.Replace(dunstDeclared, `"default":300`, `"default":400`, 1))
|
||||
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") {
|
||||
|
||||
@@ -36,8 +36,6 @@
|
||||
"mesh-delivery-builder.builds",
|
||||
"mesh-delivery-indexer.modules",
|
||||
"mesh-delivery-indexer.manifests",
|
||||
"mesh-controller.predefined",
|
||||
"mesh-controller.declared-config",
|
||||
"mesh-delivery-indexer.predefined",
|
||||
"mesh-delivery-indexer.config-candidates",
|
||||
"mesh-controller.lease"
|
||||
|
||||
Reference in New Issue
Block a user