pacman reads the node-engine's packages first and the controller's mirror per node until the lease write; compare counts only an indexer that was read, and a forge not heard is said in the row (review round 1)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@@ -260,16 +261,25 @@ func TestAHomeItemIsImportedAndTheStaleOnesAreNamed(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The manifest lists exactly the tools the bundle serves: a tool missing from it is never announced.
|
// The manifest lists exactly the tools the bundle serves: a tool missing from it is never announced. It invokes the
|
||||||
|
// hand-act seat's warranted and no verb of the controller, and the warrant is asked of that seat (ADR 0565 clause 11).
|
||||||
func TestTheManifestListsEveryToolServed(t *testing.T) {
|
func TestTheManifestListsEveryToolServed(t *testing.T) {
|
||||||
raw, err := os.ReadFile("../../module.json")
|
raw, err := os.ReadFile("../../module.json")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
var m struct {
|
var m struct {
|
||||||
Tools []string `json:"tools"`
|
Tools []string `json:"tools"`
|
||||||
|
Invokes []string `json:"invokes"`
|
||||||
}
|
}
|
||||||
_ = json.Unmarshal(raw, &m)
|
_ = json.Unmarshal(raw, &m)
|
||||||
|
if !slices.Equal(m.Invokes, []string{"seat:mesh-hand-acts.warranted"}) {
|
||||||
|
t.Errorf("invokes = %v, want the hand-act seat's warranted alone", m.Invokes)
|
||||||
|
}
|
||||||
|
if src, _ := os.ReadFile("proposals.go"); !strings.Contains(string(src), `"seat:mesh-hand-acts.warranted"`) ||
|
||||||
|
strings.Contains(string(src), "seat:mesh-controller.warranted") {
|
||||||
|
t.Error("recordWarrant does not ask the hand-act seat alone")
|
||||||
|
}
|
||||||
listed := map[string]bool{}
|
listed := map[string]bool{}
|
||||||
for _, n := range m.Tools {
|
for _, n := range m.Tools {
|
||||||
listed[n] = true
|
listed[n] = true
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"os"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// The warrant is recorded through the seat mesh-hand-acts, which the manifest invokes, and no verb of the controller
|
|
||||||
// is invoked (ADR 0565 clause 11, issue 1953).
|
|
||||||
func TestTheWarrantIsRecordedThroughTheHandActSeat(t *testing.T) {
|
|
||||||
raw, err := os.ReadFile("../../module.json")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
var m struct {
|
|
||||||
Invokes []string `json:"invokes"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(raw, &m); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if !slices.Equal(m.Invokes, []string{"seat:mesh-hand-acts.warranted"}) {
|
|
||||||
t.Errorf("invokes = %v", m.Invokes)
|
|
||||||
}
|
|
||||||
src, _ := os.ReadFile("proposals.go")
|
|
||||||
if !strings.Contains(string(src), `"seat:mesh-hand-acts.warranted"`) || strings.Contains(string(src), "seat:mesh-controller.warranted") {
|
|
||||||
t.Error("recordWarrant does not ask the hand-act seat alone")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -75,9 +75,8 @@ func (runtimeStore) Read(log string) ([]LogEntry, error) {
|
|||||||
|
|
||||||
func (runtimeStore) Emit(event string, body any) error { return stdio.Emit(event, body) }
|
func (runtimeStore) Emit(event string, body any) error { return stdio.Emit(event, body) }
|
||||||
|
|
||||||
// recorder records into the hand-act log of the seat mesh-hand-acts (novox/hq ADR 0565 §11). The manifest invokes no verb
|
// recorder records into the hand-act log of the seat mesh-hand-acts alone (novox/hq ADR 0565 §11, issue 1953): the manifest
|
||||||
// of the controller (issue 1953), so the SDK recorder's fall back to the controller's log, while the seat has no holder,
|
// invokes no verb of the controller and the SDK recorder has no fall back to its log.
|
||||||
// is refused by the bus and said in the answer: nothing is recorded in two logs.
|
|
||||||
var recorder = handacts.Recorder{Ask: stdio.Ask, LeaseAway: func() bool {
|
var recorder = handacts.Recorder{Ask: stdio.Ask, LeaseAway: func() bool {
|
||||||
e, err := stdio.State(LeaseRead).Get(buscompose.ControllerLeaseKey)
|
e, err := stdio.State(LeaseRead).Get(buscompose.ControllerLeaseKey)
|
||||||
var raw []byte
|
var raw []byte
|
||||||
|
|||||||
@@ -355,8 +355,11 @@ func (c *Config) compare() (*Comparison, error) {
|
|||||||
declaresNothing := map[string]bool{}
|
declaresNothing := map[string]bool{}
|
||||||
for module, d := range mirror.Declared {
|
for module, d := range mirror.Declared {
|
||||||
if len(d.Config) == 0 {
|
if len(d.Config) == 0 {
|
||||||
if _, err := c.trunkDeclarations(module); err != nil {
|
// Only an indexer that was read and holds no modules.<module>: a failed read says nothing, and the row stays.
|
||||||
declaresNothing[module] = true
|
if key, kerr := delivery.ModulesKey(module); kerr == nil {
|
||||||
|
if e, err := c.Store.Get(ModulesRead, key); err == nil && e == nil {
|
||||||
|
declaresNothing[module] = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -428,9 +431,11 @@ func (c *Config) compare() (*Comparison, error) {
|
|||||||
for _, k := range unionKeys(cands, own.Pulls) {
|
for _, k := range unionKeys(cands, own.Pulls) {
|
||||||
a, aok := cands[k]
|
a, aok := cands[k]
|
||||||
b, bok := own.Pulls[k]
|
b, bok := own.Pulls[k]
|
||||||
if c.pullSettled(firstNonEmpty(a.Pull, b.Pull), settled) {
|
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)
|
continue // a merged or closed pull request is no candidate to agree on (the operator, 2026-11-01)
|
||||||
}
|
}
|
||||||
|
before := len(diffs)
|
||||||
switch {
|
switch {
|
||||||
case !aok:
|
case !aok:
|
||||||
diffs = append(diffs, Difference{What: DiffPull, Pull: b.Pull, Why: "the controller told its declarations and the indexer holds no candidate"})
|
diffs = append(diffs, Difference{What: DiffPull, Pull: b.Pull, Why: "the controller told its declarations and the indexer holds no candidate"})
|
||||||
@@ -442,6 +447,9 @@ func (c *Config) compare() (*Comparison, error) {
|
|||||||
case declaresDigest(a.Declares) != declaresDigest(b.Declares):
|
case declaresDigest(a.Declares) != declaresDigest(b.Declares):
|
||||||
diffs = append(diffs, Difference{What: DiffPull, Pull: a.Pull, Why: "the two declare its config otherwise"})
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Every placed module's value on every node, resolved both ways.
|
// Every placed module's value on every node, resolved both ways.
|
||||||
@@ -502,22 +510,22 @@ func firstNonEmpty(a, b string) string {
|
|||||||
|
|
||||||
// pullSettled is whether the forge says a pull request merged or closed. Without a forge, or when it does not say, the
|
// 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).
|
// pull request counts as open, so its row stays (failure is loud).
|
||||||
func (c *Config) pullSettled(pull string, memo map[string]string) bool {
|
func (c *Config) pullSettled(pull string, memo map[string]string) (bool, string) {
|
||||||
if c.Forge == nil || pull == "" {
|
if c.Forge == nil || pull == "" {
|
||||||
return false
|
return false, ""
|
||||||
}
|
}
|
||||||
if v, ok := memo[pull]; ok {
|
if v, ok := memo[pull]; ok {
|
||||||
return v != ""
|
return v != "" && !strings.HasPrefix(v, forgeSilent), forgeNoteOf(v)
|
||||||
}
|
}
|
||||||
owner, repo, n, err := parsePull(pull)
|
owner, repo, n, err := parsePull(pull)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false, ""
|
||||||
}
|
}
|
||||||
open, merged, _, err := c.Forge.Pull(owner, repo, n)
|
open, merged, _, err := c.Forge.Pull(owner, repo, n)
|
||||||
switch {
|
switch {
|
||||||
case err != nil:
|
case err != nil:
|
||||||
c.logf("[mesh-config] compare: the forge does not say whether %s is open (%v); its row stays", pull, err)
|
c.logf("[mesh-config] compare: the forge does not say whether %s is open (%v); its row stays", pull, err)
|
||||||
memo[pull] = ""
|
memo[pull] = forgeSilent + err.Error()
|
||||||
case open:
|
case open:
|
||||||
memo[pull] = ""
|
memo[pull] = ""
|
||||||
case merged:
|
case merged:
|
||||||
@@ -525,7 +533,19 @@ func (c *Config) pullSettled(pull string, memo map[string]string) bool {
|
|||||||
default:
|
default:
|
||||||
memo[pull] = "closed"
|
memo[pull] = "closed"
|
||||||
}
|
}
|
||||||
return memo[pull] != ""
|
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 ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// normal is declarations in one encoding: a null default as none.
|
// normal is declarations in one encoding: a null default as none.
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -92,6 +93,9 @@ func TestCompareListsEveryDifferenceAndTheTakeOverWaitsForNone(t *testing.T) {
|
|||||||
candidate(t, m, "novox/mesh-catalog#11", "h1", `{"dunst":{"gap":{"kind":"","type":"number","default":2}}}`)
|
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}}}`)
|
candidate(t, m, "novox/mesh-catalog#10", "h2", `{"dunst":{"gap":{"kind":"","type":"number","default":2}}}`)
|
||||||
}, []string{"pull:novox/mesh-catalog#10"}},
|
}, []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"}},
|
||||||
{"a module no manifest names and the mirror declares nothing of", func(t *testing.T, m *memStore) {
|
{"a module no manifest names and the mirror declares nothing of", func(t *testing.T, m *memStore) {
|
||||||
m.assign(t, "g14", "mesh-wireguard")
|
m.assign(t, "g14", "mesh-wireguard")
|
||||||
m.mirror(t, MirrorDeclaredRead, "mesh-wireguard", `{"module":"mesh-wireguard","settings":{}}`)
|
m.mirror(t, MirrorDeclaredRead, "mesh-wireguard", `{"module":"mesh-wireguard","settings":{}}`)
|
||||||
@@ -100,6 +104,9 @@ func TestCompareListsEveryDifferenceAndTheTakeOverWaitsForNone(t *testing.T) {
|
|||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
c, m, r := anAgreeingMesh(t)
|
c, m, r := anAgreeingMesh(t)
|
||||||
c.Forge = &standForge{state: map[string][2]bool{"novox/mesh-catalog#11": {false, true}}}
|
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)
|
tc.change(t, m)
|
||||||
cmp, err := c.Compare()
|
cmp, err := c.Compare()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -112,6 +119,9 @@ func TestCompareListsEveryDifferenceAndTheTakeOverWaitsForNone(t *testing.T) {
|
|||||||
if d.Why == "" {
|
if d.Why == "" {
|
||||||
t.Errorf("%s: a difference without its reason: %+v", tc.name, d)
|
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"})
|
a, err := c.TakeOver(Call{Why: "compare is empty", Caller: "person.jochen"})
|
||||||
if len(tc.want) > 0 {
|
if len(tc.want) > 0 {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ that seat (ADR 0207).
|
|||||||
`package.changed`. A list over the bus's cap on a value is not written: the key keeps its last value and
|
`package.changed`. A list over the bus's cap on a value is not written: the key keeps its last value and
|
||||||
`<node>.not-current` says the size and since when, which the health says too. `pacman_packages`,
|
`<node>.not-current` says the size and since when, which the health says too. `pacman_packages`,
|
||||||
`pacman_updates`, `pacman_installed` and `pacman_foreign` answer every node from these states, and what no
|
`pacman_updates`, `pacman_installed` and `pacman_foreign` answer every node from these states, and what no
|
||||||
module declares from the node-engine's `packages`, without asking a machine.
|
module declares from the node-engine's `packages`, else the controller's `declared-packages` until its lease is written, without asking a machine.
|
||||||
- **Upgrades as runs, one node at a time (ADR 0508).** Every full upgrade and every rollback is a run in the
|
- **Upgrades as runs, one node at a time (ADR 0508).** Every full upgrade and every rollback is a run in the
|
||||||
state `runs` at `<node>.<run>`, written at each step and emitted as it: the package record taken and read back
|
state `runs` at `<node>.<run>`, written at each step and emitted as it: the package record taken and read back
|
||||||
(`records`, kept `record-days`), the DKMS gate before the transaction (a kernel an installed DKMS module's own
|
(`records`, kept `record-days`), the DKMS gate before the transaction (a kernel an installed DKMS module's own
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ func TestTheKeeperDeclaresTheModulesStatesAndEvents(t *testing.T) {
|
|||||||
}
|
}
|
||||||
// The verbs read which packages each node's modules declare; the process reads no other module's state
|
// The verbs read which packages each node's modules declare; the process reads no other module's state
|
||||||
// (ADR 0507 §5, ADR 0436 §1).
|
// (ADR 0507 §5, ADR 0436 §1).
|
||||||
if got := strs(raw["reads"]); !reflect.DeepEqual(got, []string{"node-engine.packages"}) {
|
if got := strs(raw["reads"]); !reflect.DeepEqual(got, []string{"node-engine.packages", "mesh-controller.declared-packages"}) {
|
||||||
t.Errorf("reads: %v", got)
|
t.Errorf("reads: %v", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,17 @@ import (
|
|||||||
// DeclaredPackagesState is the node-engine's state of the packages its node's modules declare, one key per node,
|
// DeclaredPackagesState is the node-engine's state of the packages its node's modules declare, one key per node,
|
||||||
// written by that node's engine alone (ADR 0565 clause 6, which moved ADR 0507 §5's mirror off the controller), read
|
// written by that node's engine alone (ADR 0565 clause 6, which moved ADR 0507 §5's mirror off the controller), read
|
||||||
// through the manifest's `reads`.
|
// through the manifest's `reads`.
|
||||||
const DeclaredPackagesState = "node-engine." + delivery.EnginePackagesState
|
const DeclaredPackagesState = SourceEngine
|
||||||
|
|
||||||
|
// ControllerDeclaredState is the controller's mirror of the same packages (ADR 0507 §5), the per-node fallback for a node
|
||||||
|
// whose engine has written no key yet, until the controller's lease is written away (ADR 0565 clause 6).
|
||||||
|
const ControllerDeclaredState = "mesh-controller.declared-packages"
|
||||||
|
|
||||||
|
// The sources a node's declaration is read from, said in the answer.
|
||||||
|
const (
|
||||||
|
SourceEngine = "node-engine.packages"
|
||||||
|
SourceController = "mesh-controller.declared-packages"
|
||||||
|
)
|
||||||
|
|
||||||
// declaredValue is a node's key of `packages`, in mesh-sdk's layout (the controller's mirror unchanged).
|
// declaredValue is a node's key of `packages`, in mesh-sdk's layout (the controller's mirror unchanged).
|
||||||
type declaredValue = delivery.DeclaredPackages
|
type declaredValue = delivery.DeclaredPackages
|
||||||
@@ -107,28 +117,35 @@ func (m *Machine) installedOn(node string) ([]nodeInstalled, error) {
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// declaredOn is what each node's modules declare present, by node and package, with the declaring modules; a
|
// declaredOn is what each node's modules declare present, by node and package, with the declaring modules, and the source
|
||||||
// node whose engine has written no key is missing.
|
// that answered for each node: its engine's key, else the controller's mirror; a node neither holds is missing.
|
||||||
func (m *Machine) declaredOn(nodes []string) (map[string]map[string][]string, error) {
|
func (m *Machine) declaredOn(nodes []string) (map[string]map[string][]string, map[string]string, error) {
|
||||||
r := m.States(DeclaredPackagesState)
|
|
||||||
out := map[string]map[string][]string{}
|
out := map[string]map[string][]string{}
|
||||||
for _, n := range nodes {
|
from := map[string]string{}
|
||||||
var v declaredValue
|
for _, source := range []string{SourceEngine, SourceController} {
|
||||||
found, err := readValue(r, n, &v)
|
r := m.States(source)
|
||||||
if err != nil {
|
for _, n := range nodes {
|
||||||
return nil, err
|
if from[n] != "" {
|
||||||
}
|
continue
|
||||||
if !found {
|
}
|
||||||
continue
|
var v declaredValue
|
||||||
}
|
found, err := readValue(r, n, &v)
|
||||||
out[n] = map[string][]string{}
|
if err != nil {
|
||||||
for _, p := range v.Packages {
|
return nil, nil, err
|
||||||
if !p.Absent {
|
}
|
||||||
out[n][p.Package] = p.Modules
|
if !found {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
from[n] = source
|
||||||
|
out[n] = map[string][]string{}
|
||||||
|
for _, p := range v.Packages {
|
||||||
|
if !p.Absent {
|
||||||
|
out[n][p.Package] = p.Modules
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out, nil
|
return out, from, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NodeView is one node in the view.
|
// NodeView is one node in the view.
|
||||||
@@ -142,9 +159,11 @@ type NodeView struct {
|
|||||||
Current bool `json:"current"`
|
Current bool `json:"current"`
|
||||||
NotCurrent *NotCurrent `json:"not_current,omitempty"`
|
NotCurrent *NotCurrent `json:"not_current,omitempty"`
|
||||||
// Undeclared is the packages installed explicitly from a repository that no module assigned to the node
|
// Undeclared is the packages installed explicitly from a repository that no module assigned to the node
|
||||||
// declares; null when the node-engine has published no declaration of the node.
|
// declares; null when neither the node-engine nor the controller's mirror holds a declaration of the node.
|
||||||
Undeclared []PackageVersion `json:"undeclared"`
|
Undeclared []PackageVersion `json:"undeclared"`
|
||||||
Foreign []PackageVersion `json:"foreign"`
|
Foreign []PackageVersion `json:"foreign"`
|
||||||
|
// DeclaredFrom is the source that answered for the node's declaration; empty when neither holds one.
|
||||||
|
DeclaredFrom string `json:"declared_from,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spread is one package across the nodes: its version on each node that has it.
|
// Spread is one package across the nodes: its version on each node that has it.
|
||||||
@@ -163,6 +182,8 @@ type PackageOnNode struct {
|
|||||||
Foreign bool `json:"foreign,omitempty"`
|
Foreign bool `json:"foreign,omitempty"`
|
||||||
DeclaredBy []string `json:"declared_by"`
|
DeclaredBy []string `json:"declared_by"`
|
||||||
Current bool `json:"current"`
|
Current bool `json:"current"`
|
||||||
|
// DeclaredFrom is the source that answered for the node's declaration; empty when neither holds one.
|
||||||
|
DeclaredFrom string `json:"declared_from,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Packages is pacman_packages: one package on every node, or the whole view: per node its counts, its explicit
|
// Packages is pacman_packages: one package on every node, or the whole view: per node its counts, its explicit
|
||||||
@@ -177,14 +198,14 @@ func (m *Machine) Packages(name string, limit int) (map[string]any, error) {
|
|||||||
for _, ni := range all {
|
for _, ni := range all {
|
||||||
nodes = append(nodes, ni.Node)
|
nodes = append(nodes, ni.Node)
|
||||||
}
|
}
|
||||||
declared, err := m.declaredOn(nodes)
|
declared, sources, err := m.declaredOn(nodes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if name != "" {
|
if name != "" {
|
||||||
on := []PackageOnNode{}
|
on := []PackageOnNode{}
|
||||||
for _, ni := range all {
|
for _, ni := range all {
|
||||||
p := PackageOnNode{Node: ni.Node, Current: ni.NotCurrent == nil, DeclaredBy: declared[ni.Node][name]}
|
p := PackageOnNode{Node: ni.Node, Current: ni.NotCurrent == nil, DeclaredBy: declared[ni.Node][name], DeclaredFrom: sources[ni.Node]}
|
||||||
if p.DeclaredBy == nil {
|
if p.DeclaredBy == nil {
|
||||||
p.DeclaredBy = []string{}
|
p.DeclaredBy = []string{}
|
||||||
}
|
}
|
||||||
@@ -205,7 +226,7 @@ func (m *Machine) Packages(name string, limit int) (map[string]any, error) {
|
|||||||
versions := map[string]map[string]string{}
|
versions := map[string]map[string]string{}
|
||||||
for _, ni := range all {
|
for _, ni := range all {
|
||||||
v := NodeView{Node: ni.Node, At: ni.Value.At, Packages: len(ni.Value.Packages), Current: ni.NotCurrent == nil,
|
v := NodeView{Node: ni.Node, At: ni.Value.At, Packages: len(ni.Value.Packages), Current: ni.NotCurrent == nil,
|
||||||
NotCurrent: ni.NotCurrent, Foreign: []PackageVersion{}}
|
NotCurrent: ni.NotCurrent, Foreign: []PackageVersion{}, DeclaredFrom: sources[ni.Node]}
|
||||||
d, known := declared[ni.Node]
|
d, known := declared[ni.Node]
|
||||||
if known {
|
if known {
|
||||||
v.Undeclared = []PackageVersion{}
|
v.Undeclared = []PackageVersion{}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ func viewStates(t *testing.T) (*fakeStates, *Machine) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestOnePackageOnEveryNodeWithWhoDeclaresIt(t *testing.T) {
|
func TestOnePackageOnEveryNodeWithWhoDeclaresIt(t *testing.T) {
|
||||||
_, m := viewStates(t)
|
states, m := viewStates(t)
|
||||||
r, err := m.Packages("zsh", 500)
|
r, err := m.Packages("zsh", 500)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -50,6 +50,22 @@ func TestOnePackageOnEveryNodeWithWhoDeclaresIt(t *testing.T) {
|
|||||||
t.Fatalf("%+v", on)
|
t.Fatalf("%+v", on)
|
||||||
}
|
}
|
||||||
a, b := on[0], on[1]
|
a, b := on[0], on[1]
|
||||||
|
if a.DeclaredFrom != SourceEngine || b.DeclaredFrom != "" {
|
||||||
|
t.Errorf("sources: %q, %q", a.DeclaredFrom, b.DeclaredFrom)
|
||||||
|
}
|
||||||
|
// A node whose engine has written no key reads the controller's mirror, said so; a node with both reads its engine's.
|
||||||
|
var legacy, stale declaredValue
|
||||||
|
json.Unmarshal([]byte(`{"node":"node-b","packages":[{"package":"zsh","modules":["legacy"],"absent":false}]}`), &legacy)
|
||||||
|
json.Unmarshal([]byte(`{"node":"node-a","packages":[{"package":"zsh","modules":["stale"],"absent":false}]}`), &stale)
|
||||||
|
mirror := states.store(ControllerDeclaredState)
|
||||||
|
mirror.Put("node-b", legacy)
|
||||||
|
mirror.Put("node-a", stale)
|
||||||
|
r2, _ := m.Packages("zsh", 500)
|
||||||
|
on2 := r2["nodes"].([]PackageOnNode)
|
||||||
|
if on2[0].DeclaredFrom != SourceEngine || strings.Join(on2[0].DeclaredBy, ",") != "login,shell" ||
|
||||||
|
on2[1].DeclaredFrom != SourceController || strings.Join(on2[1].DeclaredBy, ",") != "legacy" {
|
||||||
|
t.Errorf("fallback: %+v", on2)
|
||||||
|
}
|
||||||
if a.Node != "node-a" || !a.Installed || a.Version != "5.9-2" || !a.Explicit || a.Repository != "extra" ||
|
if a.Node != "node-a" || !a.Installed || a.Version != "5.9-2" || !a.Explicit || a.Repository != "extra" ||
|
||||||
strings.Join(a.DeclaredBy, ",") != "login,shell" || !a.Current {
|
strings.Join(a.DeclaredBy, ",") != "login,shell" || !a.Current {
|
||||||
t.Errorf("node-a: %+v", a)
|
t.Errorf("node-a: %+v", a)
|
||||||
|
|||||||
@@ -12,7 +12,8 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"reads": [
|
"reads": [
|
||||||
"node-engine.packages"
|
"node-engine.packages",
|
||||||
|
"mesh-controller.declared-packages"
|
||||||
],
|
],
|
||||||
"uses": [
|
"uses": [
|
||||||
"operator-channel"
|
"operator-channel"
|
||||||
|
|||||||
Reference in New Issue
Block a user