package main // A run read from pacman's log, or a run whose transaction ended, never holds a new run back (issue 1841, the // refusal of 2026-10-22 22:00 on the control-node: "run 20220123T194900Z is still going (its last step is finished): one // run at a time"); and pacman_upgrades reads one run whole at an address that names the machine. import ( "strings" "testing" "time" ) // installLogHead is the first twelve lines of the control-node's /var/log/pacman.log, captured 2026-10-22 22:10 // with node-reads.read: the install's `pacman -Syu` with nothing to do, which became run 20220123T194900Z, its // last step finished at the moment it began (pacman_upgrades there, 2026-10-22: from log, outcome finished, // began 2022-01-23T19:49:00Z, ended 2022-01-23T20:49:00+01:00). const installLogHead = "[2022-01-23T20:49:00+0100] [PACMAN] Running 'pacman -Syu'\n" + "[2022-01-23T20:49:00+0100] [PACMAN] synchronizing package lists\n" + "[2022-01-23T20:49:00+0100] [PACMAN] starting full system upgrade\n" + "[2022-01-23T20:50:45+0100] [PACMAN] Running 'pacman -S nano'\n" + "[2022-01-23T20:50:49+0100] [ALPM] transaction started\n" + "[2022-01-23T20:50:50+0100] [ALPM] installed nano (6.0-1)\n" + "[2022-01-23T20:50:50+0100] [ALPM] transaction completed\n" + "[2022-01-23T20:50:50+0100] [ALPM] running '30-systemd-update.hook'...\n" + "[2022-01-23T20:55:31+0100] [PACMAN] Running 'pacman -S docker'\n" + "[2022-01-23T20:55:36+0100] [ALPM] transaction started\n" + "[2022-01-23T20:55:36+0100] [ALPM] installed bridge-utils (1.7.1-1)\n" + "[2022-01-23T20:55:36+0100] [ALPM] installed libtool (2.4.6+42+gb88cebd5-16)\n" // 1841/AC20: given the run read from the control-node's log, when an upgrade is asked, then it starts. func TestARunReadFromTheLogIsHistoryAndNeverHoldsAnAsk(t *testing.T) { k := newKeeperBed(t) past := PastUpgrades("node-a", installLogHead) if len(past) != 1 || past[0].Run != "20220123T194900Z" || steps(past[0]) != "started finished" || past[0].From != FromLog { t.Fatalf("the captured log reads as the record the control-node holds: %+v", past) } if _, err := k.runs.Create(NodeKey("node-a", past[0].Run), past[0]); err != nil { t.Fatal(err) } if _, err := k.u.StartUpgrade(FromAsk, "", "a1"); err != nil { t.Fatal(err) } var r Run k.runs.value("node-a.20261004T120000Z", &r) if lastStep(r) == StepRefused || len(k.root.started) != 1 { t.Errorf("a run read from the log held the ask back: %s %+v", steps(r), r.Steps) } } // 1841/AC21: given a run the mesh ran whose last step is finished, failed or refused (no after-upgrade check // yet, no end time of the run itself), when another run would start, then the earlier one does not hold it; // a run whose transaction may still go on (no step, recorded, checked before, started) does. func TestARunWhoseTransactionEndedNeverHoldsTheNext(t *testing.T) { at := time.Date(2026, 10, 22, 20, 0, 0, 0, time.UTC) for last, holds := range map[string]bool{ StepFinished: false, StepFailed: false, StepRefused: false, StepCheckedAfter: false, "": true, StepRecorded: true, StepCheckedBefore: true, StepStarted: true, } { r := Run{Node: "node-a", Run: "20261022T200000Z", Kind: RunUpgrade, From: FromAsk} if last != "" { r.Steps = []Step{{Step: last, At: at}} } if got := holdsTheDatabase(r); got != holds { t.Errorf("last step %q: holds %v, want %v", last, got, holds) } r.From = FromLog if holdsTheDatabase(r) { t.Errorf("a run read from the log, last step %q, holds the next", last) } } k := newKeeperBed(t) done := Run{Node: "node-a", Run: "20261004T110000Z", Kind: RunUpgrade, From: FromAsk, Steps: []Step{{Step: StepStarted, At: k.now}, {Step: StepFinished, At: k.now}}} if _, err := k.runs.Create(NodeKey("node-a", done.Run), done); err != nil { t.Fatal(err) } if _, err := k.u.StartUpgrade(FromAsk, "", "a1"); err != nil { t.Fatal(err) } var r Run k.runs.value("node-a.20261004T120000Z", &r) if lastStep(r) == StepRefused { t.Errorf("a finished run held the next: %+v", r.Steps) } } // 1841/AC22: given an address that names the machine (the console takes `node` out of the arguments), when // pacman_upgrades is asked for one run, then that machine's run is answered whole. func TestPacmanUpgradesReadsOneRunOfThisNodeWithoutANode(t *testing.T) { runs := newBucket() past := PastUpgrades("node-a", installLogHead)[0] if _, err := runs.Create(NodeKey("node-a", past.Run), past); err != nil { t.Fatal(err) } savedState, savedNode := upgradeState, thisNode upgradeState = func(string) Bucket { return runs } thisNode = func() string { return "node-a" } defer func() { upgradeState, thisNode = savedState, savedNode }() for _, tool := range tools(machine(nil, 1000)) { if tool.Name != "pacman_upgrades" { continue } out, err := tool.Run(map[string]any{"run": past.Run}) if err != nil { t.Fatalf("one run without a node: %v", err) } whole := out.(map[string]any)["run"].(Run) if whole.Node != "node-a" || steps(whole) != "started finished" || !strings.Contains(whole.Steps[1].Why, "no transaction") { t.Errorf("one run whole: %+v", whole) } return } t.Fatal("no pacman_upgrades") }