Judge a module the agent has not taken yet by the composition its have names, and knock the filling at the start and whenever a module is taken as running (issue 924)
A held module on a node composing its own never gets a composition, so the agent never took it and the filling judged its files by the kept declaration, older than power's three new drop-ins: refused from every engine start on ace, g14 and shanks until some module settled (2026-10-29 00:09 UTC). The second meeting of issue 924. Quick fix: knock the filling once the haves are read at the start. Thorough fix (chosen, the kind recurs for every file added since the switch): what runs of such a module is the composition its have names, from the bucket or its revisions, ahead of the kept declaration; the filling is knocked on every change of what runs, not only on a settle. One unit test replaced by a table of four cases; the count stays.
This commit is contained in:
@@ -30,9 +30,10 @@ func serveFilling(ctx context.Context, opts options, signer ed25519.PublicKey, s
|
|||||||
var fillingNow atomic.Pointer[filling.Engine]
|
var fillingNow atomic.Pointer[filling.Engine]
|
||||||
|
|
||||||
// knockFilling tells the provisioner's client to fill again what the filling refused or failed and the declaration
|
// knockFilling tells the provisioner's client to fill again what the filling refused or failed and the declaration
|
||||||
// applied now may let through (filling.Engine.Knock), once a module's apply or removal settled (agent.Env.Settled).
|
// applied now may let through (filling.Engine.Knock), after what runs changed: a module's apply or removal settled
|
||||||
// What it told, or why it could not, is said.
|
// (agent.Env.Settled, "<module> settled"), the haves were read at the start, or a module was taken as running
|
||||||
func knockFilling(module string, say func(string)) {
|
// (agent.Env.RunsChanged, issue 924). What it told, or why it could not, is said.
|
||||||
|
func knockFilling(after string, say func(string)) {
|
||||||
e := fillingNow.Load()
|
e := fillingNow.Load()
|
||||||
if e == nil {
|
if e == nil {
|
||||||
return
|
return
|
||||||
@@ -40,9 +41,9 @@ func knockFilling(module string, say func(string)) {
|
|||||||
told, err := e.Knock(filling.Socket)
|
told, err := e.Knock(filling.Socket)
|
||||||
switch {
|
switch {
|
||||||
case err != nil:
|
case err != nil:
|
||||||
say(fmt.Sprintf("after %s settled: %v", module, err))
|
say(fmt.Sprintf("after %s: %v", after, err))
|
||||||
case told != "":
|
case told != "":
|
||||||
say(fmt.Sprintf("after %s settled, the provisioner's client is told to fill again: %s", module, told))
|
say(fmt.Sprintf("after %s, the provisioner's client is told to fill again: %s", after, told))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -499,7 +499,10 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, sess
|
|||||||
Backup: backupNode(session),
|
Backup: backupNode(session),
|
||||||
// What the filling refused before this apply may be named now: the provisioner's client is told to fill
|
// What the filling refused before this apply may be named now: the provisioner's client is told to fill
|
||||||
// again (ace, 2026-10-25 21:17).
|
// again (ace, 2026-10-25 21:17).
|
||||||
Settled: func(module string) { knockFilling(module, say) },
|
Settled: func(module string) { knockFilling(module+" settled", say) },
|
||||||
|
// And once the haves are read at the start, and whenever a module is taken as running without an apply, so
|
||||||
|
// a refusal from the engine's start does not wait for some module to settle (issue 924).
|
||||||
|
RunsChanged: func(why string) { knockFilling(why, say) },
|
||||||
Say: say,
|
Say: say,
|
||||||
// A failure of the bus not answering for a moment is tried again: 2 s, 5 s, 15 s, 60 s, then every 5 min.
|
// A failure of the bus not answering for a moment is tried again: 2 s, 5 s, 15 s, 60 s, then every 5 min.
|
||||||
Backoff: link.Backoff,
|
Backoff: link.Backoff,
|
||||||
|
|||||||
+117
-6
@@ -109,7 +109,9 @@ type Env struct {
|
|||||||
Kept func() map[string][]json.RawMessage
|
Kept func() map[string][]json.RawMessage
|
||||||
// KeptFilled is the paths the kept declaration names as filled by the provisioner, by the module whose file is at
|
// KeptFilled is the paths the kept declaration names as filled by the provisioner, by the module whose file is at
|
||||||
// each (the node's own under NodeModule). A module that still runs from the kept declaration keeps its filled files
|
// each (the node's own under NodeModule). A module that still runs from the kept declaration keeps its filled files
|
||||||
// filled: without them the filling refused each one as "not named as filled" (issue 1945). Nil names none.
|
// filled: without them the filling refused each one as "not named as filled" (issue 1945). Nil names none. A module
|
||||||
|
// whose have names a composition the bucket keeps runs from that composition, never from the kept declaration
|
||||||
|
// (issue 924): the kept declaration is older than every composition applied since the switch.
|
||||||
KeptFilled func() map[string][]string
|
KeptFilled func() map[string][]string
|
||||||
// Trusted is the public keys a composition may be signed by: the controller's and the operator's (ADR 0539 §3). A
|
// Trusted is the public keys a composition may be signed by: the controller's and the operator's (ADR 0539 §3). A
|
||||||
// composition unsigned, signed by another key, or changed after it was signed is refused and nothing of it is
|
// composition unsigned, signed by another key, or changed after it was signed is refused and nothing of it is
|
||||||
@@ -131,6 +133,12 @@ type Env struct {
|
|||||||
// them. The node-engine's filling tells the provisioner's client then to fill again what it refused (mesh-host
|
// them. The node-engine's filling tells the provisioner's client then to fill again what it refused (mesh-host
|
||||||
// internal/filling Knock; ace, 2026-10-25 21:17). Nil tells nothing.
|
// internal/filling Knock; ace, 2026-10-25 21:17). Nil tells nothing.
|
||||||
Settled func(module string)
|
Settled func(module string)
|
||||||
|
// RunsChanged is told, outside the agent's lock, that what runs (Everything) changed other than by an apply or a
|
||||||
|
// removal: once at the start, when the haves and the compositions they name are read, and each time a module is
|
||||||
|
// taken as running from its have or the kept declaration. Like Settled, the filling then tells the provisioner's
|
||||||
|
// client to fill again what it refused (issue 924, the second meeting: ace, g14 and shanks on 2026-10-29 at 00:09
|
||||||
|
// UTC refused power's three new drop-ins from an engine start on, as no module settled after it). Nil tells nothing.
|
||||||
|
RunsChanged func(why string)
|
||||||
// Backoff is how long to wait before the try-th retry (1, 2, …) of an apply that failed transiently. Nil retries a
|
// Backoff is how long to wait before the try-th retry (1, 2, …) of an apply that failed transiently. Nil retries a
|
||||||
// transient failure only on RetryTransient or a change of either key.
|
// transient failure only on RetryTransient or a change of either key.
|
||||||
Backoff func(try int) time.Duration
|
Backoff func(try int) time.Duration
|
||||||
@@ -160,6 +168,10 @@ type Agent struct {
|
|||||||
// applied is the composition each module was last applied from: a revision a want named from the key's history is
|
// applied is the composition each module was last applied from: a revision a want named from the key's history is
|
||||||
// not the key's latest value, and what it holds is still held out of the others' applies.
|
// not the key's latest value, and what it holds is still held out of the others' applies.
|
||||||
applied map[string]compNow
|
applied map[string]compNow
|
||||||
|
// had is, for each module whose have names a composition the bucket keeps (its latest value or a revision), that
|
||||||
|
// composition, read once at the start (loadHad): what runs of a module the agent has not taken yet, ahead of the kept
|
||||||
|
// declaration (issue 924).
|
||||||
|
had map[string]compNow
|
||||||
// attempt is the last apply of a module that failed or is held, with the revisions it was made from.
|
// attempt is the last apply of a module that failed or is held, with the revisions it was made from.
|
||||||
attempt map[string]attempt
|
attempt map[string]attempt
|
||||||
checked map[string]bool
|
checked map[string]bool
|
||||||
@@ -218,7 +230,7 @@ func New(env Env) *Agent {
|
|||||||
if env.Say == nil {
|
if env.Say == nil {
|
||||||
env.Say = func(string) {}
|
env.Say = func(string) {}
|
||||||
}
|
}
|
||||||
return &Agent{env: env, wants: map[string]wantNow{}, comps: map[string]compNow{}, running: map[string]delivery.Have{}, applied: map[string]compNow{},
|
return &Agent{env: env, wants: map[string]wantNow{}, comps: map[string]compNow{}, running: map[string]delivery.Have{}, applied: map[string]compNow{}, had: map[string]compNow{},
|
||||||
attempt: map[string]attempt{}, checked: map[string]bool{}, prior: map[string]bool{}, said: map[string]string{}, notComposed: map[string]notComposedMark{},
|
attempt: map[string]attempt{}, checked: map[string]bool{}, prior: map[string]bool{}, said: map[string]string{}, notComposed: map[string]notComposedMark{},
|
||||||
retry: make(chan []string, 8), reconcile: make(chan []string, 64), tries: map[string]int{}, again: make(chan againAsk, 64),
|
retry: make(chan []string, 8), reconcile: make(chan []string, 64), tries: map[string]int{}, again: make(chan againAsk, 64),
|
||||||
transient: make(chan struct{}, 1), stopped: make(chan struct{})}
|
transient: make(chan struct{}, 1), stopped: make(chan struct{})}
|
||||||
@@ -236,7 +248,7 @@ func (a *Agent) Run(ctx context.Context) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("the agent cannot watch its compositions: %w", err)
|
return fmt.Errorf("the agent cannot watch its compositions: %w", err)
|
||||||
}
|
}
|
||||||
var wantsCaught, compsCaught bool
|
var wantsCaught, compsCaught, hadRead bool
|
||||||
dirty := map[string]bool{}
|
dirty := map[string]bool{}
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@@ -303,7 +315,17 @@ func (a *Agent) Run(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
// Nothing is applied until what was there at the start is all in: a module's composition may arrive after its
|
// Nothing is applied until what was there at the start is all in: a module's composition may arrive after its
|
||||||
// want, and the node's own composition comes before any module's.
|
// want, and the node's own composition comes before any module's.
|
||||||
if !(wantsCaught && compsCaught) || len(dirty) == 0 {
|
if !(wantsCaught && compsCaught) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !hadRead {
|
||||||
|
// Before the first settle: what runs of each module with a have, from the composition it names, so the
|
||||||
|
// filling judges by it at once and not by the kept declaration until some module settles (issue 924).
|
||||||
|
hadRead = true
|
||||||
|
a.loadHad(ctx)
|
||||||
|
a.runsChanged("the haves were read at the start")
|
||||||
|
}
|
||||||
|
if len(dirty) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
a.settle(ctx, dirty)
|
a.settle(ctx, dirty)
|
||||||
@@ -726,7 +748,12 @@ func (a *Agent) revisionNamed(ctx context.Context, module, fp, digest string) (c
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
dig, err := delivery.VerifyComposed(c, trusted)
|
var dig string
|
||||||
|
if a.env.SelfComposed {
|
||||||
|
dig, err = delivery.ComposedDigestOf(c) // the node's own: unsigned (ADR 0547 §2)
|
||||||
|
} else {
|
||||||
|
dig, err = delivery.VerifyComposed(c, trusted)
|
||||||
|
}
|
||||||
if err != nil || dig != digest {
|
if err != nil || dig != digest {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -755,13 +782,27 @@ func (a *Agent) others(except string) []delivery.Composed {
|
|||||||
out = append(out, c.C)
|
out = append(out, c.C)
|
||||||
} else if c, ok := a.applied[m]; ok && c.Dig == h.Composed {
|
} else if c, ok := a.applied[m]; ok && c.Dig == h.Composed {
|
||||||
out = append(out, c.C)
|
out = append(out, c.C)
|
||||||
|
} else if c, ok := a.had[m]; ok && c.Dig == h.Composed {
|
||||||
|
out = append(out, c.C) // a revision its have named, replaced since under the same key
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
hadNames := make([]string, 0, len(a.had))
|
||||||
|
for m := range a.had {
|
||||||
|
if _, held := a.running[m]; !held && m != except {
|
||||||
|
hadNames = append(hadNames, m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(hadNames)
|
||||||
|
for _, m := range hadNames {
|
||||||
|
out = append(out, a.had[m].C)
|
||||||
|
}
|
||||||
if a.env.Kept != nil {
|
if a.env.Kept != nil {
|
||||||
kept := a.env.Kept()
|
kept := a.env.Kept()
|
||||||
names := make([]string, 0, len(kept))
|
names := make([]string, 0, len(kept))
|
||||||
for m := range kept {
|
for m := range kept {
|
||||||
if _, held := a.running[m]; !held && m != except {
|
_, held := a.running[m]
|
||||||
|
_, had := a.had[m]
|
||||||
|
if !held && !had && m != except {
|
||||||
names = append(names, m)
|
names = append(names, m)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -780,6 +821,73 @@ func (a *Agent) others(except string) []delivery.Composed {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loadHad reads, once at the start, the have of every module with a want or in the kept declaration and, where it names
|
||||||
|
// a composition, that composition: the bucket's value at its key when its digest is the have's, else the revision the
|
||||||
|
// bucket keeps with that digest. A have naming none, or one the bucket no longer keeps, leaves the module to the kept
|
||||||
|
// declaration, said once (issue 924).
|
||||||
|
func (a *Agent) loadHad(ctx context.Context) {
|
||||||
|
a.mu.Lock()
|
||||||
|
seen := map[string]bool{}
|
||||||
|
for m, w := range a.wants {
|
||||||
|
if m != NodeModule && w.Present && !w.Want.Removed {
|
||||||
|
seen[m] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if a.env.Kept != nil {
|
||||||
|
for m := range a.env.Kept() {
|
||||||
|
if m != NodeModule {
|
||||||
|
seen[m] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a.mu.Unlock()
|
||||||
|
names := make([]string, 0, len(seen))
|
||||||
|
for m := range seen {
|
||||||
|
names = append(names, m)
|
||||||
|
}
|
||||||
|
sort.Strings(names)
|
||||||
|
for _, m := range names {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h, ok := a.env.Haves.Read(ctx, m)
|
||||||
|
if !ok || h.Composed == "" || h.Phase == string(delivery.PhaseRemoved) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
a.mu.Lock()
|
||||||
|
c, found := a.compositionFor(m, h.FP)
|
||||||
|
a.mu.Unlock()
|
||||||
|
if !found || c.Err != nil || c.Dig != h.Composed {
|
||||||
|
c, found = a.revisionNamed(ctx, m, h.FP, h.Composed)
|
||||||
|
}
|
||||||
|
a.mu.Lock()
|
||||||
|
if found {
|
||||||
|
a.had[m] = c
|
||||||
|
} else if _, kept := a.keptHas(m); kept {
|
||||||
|
a.sayOnce("had:"+m, fmt.Sprintf("%s's have names the composition %s, which the bucket no longer keeps: it runs "+
|
||||||
|
"from the kept declaration until it is applied or taken", m, short(h.Composed)))
|
||||||
|
}
|
||||||
|
a.mu.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// keptHas says whether the kept declaration has resources for the module. Called holding a.mu or not: Kept takes no lock
|
||||||
|
// of the agent's.
|
||||||
|
func (a *Agent) keptHas(module string) ([]json.RawMessage, bool) {
|
||||||
|
if a.env.Kept == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
rs, ok := a.env.Kept()[module]
|
||||||
|
return rs, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// runsChanged tells RunsChanged, outside the agent's lock.
|
||||||
|
func (a *Agent) runsChanged(why string) {
|
||||||
|
if a.env.RunsChanged != nil {
|
||||||
|
a.env.RunsChanged(why)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// unreadableModules are the keys of the module compositions in the node's bucket that cannot be read or are refused, of a
|
// unreadableModules are the keys of the module compositions in the node's bucket that cannot be read or are refused, of a
|
||||||
// module the assigner has not removed: for each module only the composition its want names, else its newest, since a
|
// module the assigner has not removed: for each module only the composition its want names, else its newest, since a
|
||||||
// stale one nobody applies (written before a rule the SDK now holds) must not hold the node's own back. Called holding
|
// stale one nobody applies (written before a rule the SDK now holds) must not hold the node's own back. Called holding
|
||||||
@@ -1048,6 +1156,7 @@ func (a *Agent) baseline(ctx context.Context, module string, w wantNow, comp com
|
|||||||
a.running[module] = delivery.Have{FP: h.FP, Composed: h.Composed, Phase: delivery.PhaseRunning}
|
a.running[module] = delivery.Have{FP: h.FP, Composed: h.Composed, Phase: delivery.PhaseRunning}
|
||||||
}
|
}
|
||||||
a.mu.Unlock()
|
a.mu.Unlock()
|
||||||
|
a.runsChanged(module + " was taken as held")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if h.Composed != "" {
|
if h.Composed != "" {
|
||||||
@@ -1068,6 +1177,7 @@ func (a *Agent) baseline(ctx context.Context, module string, w wantNow, comp com
|
|||||||
a.mu.Lock()
|
a.mu.Lock()
|
||||||
a.running[module] = delivery.Have{FP: h.FP, Composed: h.Composed, Phase: delivery.PhaseRunning}
|
a.running[module] = delivery.Have{FP: h.FP, Composed: h.Composed, Phase: delivery.PhaseRunning}
|
||||||
a.mu.Unlock()
|
a.mu.Unlock()
|
||||||
|
a.runsChanged(module + " was taken from its have")
|
||||||
// Said again, so the session holds the module and its key carries the health this engine judges,
|
// Said again, so the session holds the module and its key carries the health this engine judges,
|
||||||
// not the one an older engine left there (xorg's unknown on g14 and shanks, 2026-10-25).
|
// not the one an older engine left there (xorg's unknown on g14 and shanks, 2026-10-25).
|
||||||
a.writeHave(ctx, Have{Module: module, FP: h.FP, Composed: h.Composed,
|
a.writeHave(ctx, Have{Module: module, FP: h.FP, Composed: h.Composed,
|
||||||
@@ -1096,6 +1206,7 @@ func (a *Agent) baseline(ctx context.Context, module string, w wantNow, comp com
|
|||||||
a.mu.Lock()
|
a.mu.Lock()
|
||||||
a.running[module] = delivery.Have{FP: w.Want.FP, Composed: w.Want.Composed, Phase: delivery.PhaseRunning}
|
a.running[module] = delivery.Have{FP: w.Want.FP, Composed: w.Want.Composed, Phase: delivery.PhaseRunning}
|
||||||
a.mu.Unlock()
|
a.mu.Unlock()
|
||||||
|
a.runsChanged(module + " was taken from the kept declaration")
|
||||||
a.writeHave(ctx, Have{Module: module, FP: w.Want.FP, Composed: w.Want.Composed, Phase: string(delivery.PhaseRunning)})
|
a.writeHave(ctx, Have{Module: module, FP: w.Want.FP, Composed: w.Want.Composed, Phase: string(delivery.PhaseRunning)})
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1026,18 +1026,87 @@ func TestTheRevisionAWantNamesIsAppliedFromHistory(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1945/AC18 (unit): a module that still runs from the kept declaration carries the kept declaration's filled paths of
|
// 1945/AC18 and issue 924 (unit): what runs of a module the agent has not taken yet carries the filled paths the
|
||||||
// its files, so the filling writes them ("not named as filled" for claude-code and screen-lock, 2026-10-25).
|
// filling judges by. From the kept declaration while the module has no have naming a composition ("not named as filled"
|
||||||
func TestAModuleRunningFromTheKeptDeclarationKeepsItsFilledPaths(t *testing.T) {
|
// for claude-code and screen-lock, 2026-10-25); from the composition its have names once it has one, the kept
|
||||||
b := newBed(t)
|
// declaration being older, read at the start before any module settles and told to the filling then, so a file the
|
||||||
b.kept = map[string][]json.RawMessage{"claude-code": {file("claude-code", "settings", "")}}
|
// newer composition fills is accepted on the first fill (power's three drop-ins on ace, g14 and shanks, 2026-10-29).
|
||||||
b.env.KeptFilled = func() map[string][]string {
|
func TestWhatRunsBeforeTheAgentTakesAModuleNamesItsFilledPaths(t *testing.T) {
|
||||||
return map[string][]string{"claude-code": {"/tmp/r6/claude-code/settings"}}
|
const old, added = "/tmp/r6/power/settings", "/tmp/r6/power/bound"
|
||||||
|
filledFile := func(id string) json.RawMessage {
|
||||||
|
return json.RawMessage(`{"id":"power.` + id + `","type":"file","module":"power","path":"/tmp/r6/power/` + id +
|
||||||
|
`","mode":"0644","owner-at-runtime":"owned"}`)
|
||||||
}
|
}
|
||||||
|
fp := fpOf("a")
|
||||||
|
ck, _ := delivery.ComposedKey(node, "power", fp)
|
||||||
|
applied := delivery.Composed{Node: node, Module: "power", FP: fp, Filled: []string{old, added},
|
||||||
|
Resources: []json.RawMessage{filledFile("settings"), filledFile("bound")}}
|
||||||
|
_, appliedRaw, appliedDig := signedBy(t, applied, ck, controllerKey)
|
||||||
|
newer := applied
|
||||||
|
newer.Resources = append([]json.RawMessage{}, applied.Resources...)
|
||||||
|
newer.Resources = append(newer.Resources, file("power", "later", "1"))
|
||||||
|
_, newerRaw, _ := signedBy(t, newer, ck, controllerKey)
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
// have is the module's have; bucket the value its composition key holds now, history the revisions kept.
|
||||||
|
have *Have
|
||||||
|
bucket []byte
|
||||||
|
history []Update
|
||||||
|
filled, told string
|
||||||
|
}{
|
||||||
|
{name: "no have: the kept declaration's", filled: old},
|
||||||
|
{name: "a have naming the bucket's value", have: &Have{Module: "power", FP: fp, Composed: appliedDig},
|
||||||
|
bucket: appliedRaw, filled: old + "," + added, told: "the haves were read at the start"},
|
||||||
|
{name: "a have naming a revision replaced since", have: &Have{Module: "power", FP: fp, Composed: appliedDig},
|
||||||
|
bucket: newerRaw, history: []Update{{Key: ck, Value: appliedRaw, Revision: 3}, {Key: ck, Value: newerRaw, Revision: 7}},
|
||||||
|
filled: old + "," + added, told: "the haves were read at the start"},
|
||||||
|
{name: "a have naming a composition the bucket no longer keeps", have: &Have{Module: "power", FP: fp, Composed: appliedDig},
|
||||||
|
bucket: newerRaw, filled: old, told: "the haves were read at the start"},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
b := newBed(t)
|
||||||
|
b.kept = map[string][]json.RawMessage{"power": {filledFile("settings")}}
|
||||||
|
b.env.KeptFilled = func() map[string][]string { return map[string][]string{"power": {old}} }
|
||||||
|
if tc.have != nil {
|
||||||
|
b.haves.stored = map[string]Have{"power": *tc.have}
|
||||||
|
}
|
||||||
|
if tc.bucket != nil {
|
||||||
|
b.comps.before(ck, tc.bucket)
|
||||||
|
}
|
||||||
|
b.env.Revisions = func(_ context.Context, key string) ([]Update, error) { return tc.history, nil }
|
||||||
|
var told []string
|
||||||
|
var mu sync.Mutex
|
||||||
|
b.env.RunsChanged = func(why string) { mu.Lock(); told = append(told, why); mu.Unlock() }
|
||||||
|
filledNow := func() string {
|
||||||
|
for _, c := range b.ag.Everything() {
|
||||||
|
if c.Module == "power" {
|
||||||
|
return strings.Join(c.Filled, ",")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if tc.told == "" { // no want, no have: nothing to start, and the kept declaration is what runs
|
||||||
a := New(b.env)
|
a := New(b.env)
|
||||||
got := a.Everything()
|
b.ag = a
|
||||||
if len(got) != 1 || got[0].Module != "claude-code" || strings.Join(got[0].Filled, ",") != "/tmp/r6/claude-code/settings" {
|
if got := filledNow(); got != tc.filled {
|
||||||
t.Fatalf("what runs from the kept declaration: %+v", got)
|
t.Fatalf("filled %q, want %q", got, tc.filled)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stop := b.run()
|
||||||
|
defer stop()
|
||||||
|
eventually(t, "the filling was not told what runs at the start", func() bool {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
return len(told) > 0 && told[0] == tc.told
|
||||||
|
})
|
||||||
|
if got := filledNow(); got != tc.filled {
|
||||||
|
t.Fatalf("filled %q, want %q", got, tc.filled)
|
||||||
|
}
|
||||||
|
if got := b.appliedList(); len(got) != 0 {
|
||||||
|
t.Fatalf("applied %v: nothing has a want", got)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user