package main // The after-upgrade check, its first part (novox/hq ADR 0508 ยง4; acceptance 1841/AC5): .pacnew and .pacsave // files from the transaction's log lines, the news since the previous upgrade, units newly failed against the // record, and a restart needed. import ( "errors" "strings" "testing" "time" ) const checkLog = "[2026-10-04T12:00:01+0000] [PACMAN] Running 'pacman -Syu --noconfirm'\n" + "[2026-10-04T12:00:02+0000] [PACMAN] starting full system upgrade\n" + "[2026-10-04T12:00:09+0000] [ALPM] transaction started\n" + "[2026-10-04T12:00:10+0000] [ALPM] upgraded pacman-mirrorlist (20260901-1 -> 20261001-1)\n" + "[2026-10-04T12:00:10+0000] [ALPM] warning: /etc/pacman.d/mirrorlist installed as /etc/pacman.d/mirrorlist.pacnew\n" + "[2026-10-04T12:00:10+0000] [ALPM] warning: /etc/lemurs/config.toml saved as /etc/lemurs/config.toml.pacsave\n" + "[2026-10-04T12:00:10+0000] [ALPM] removed lemurs-git (0.4.0.r17.g7151336-2)\n" + "[2026-10-04T12:00:11+0000] [ALPM] transaction completed\n" + "[2026-10-04T12:05:00+0000] [PACMAN] Running 'pacman -S --noconfirm made-up'\n" + "[2026-10-04T12:05:01+0000] [ALPM] transaction started\n" + "[2026-10-04T12:05:01+0000] [ALPM] warning: /etc/made-up.conf installed as /etc/made-up.conf.pacnew\n" + "[2026-10-04T12:05:02+0000] [ALPM] transaction completed\n" const newsFeed = ` A manual interventionhttps://example.org/news/2Fri, 02 Oct 2026 10:00:00 +0000x An older posthttps://example.org/news/1Mon, 21 Sep 2026 10:00:00 +0000y ` // checkMachine is a machine after an upgrade: these units failed now, this kernel running, and the linux // package now installing linuxNow. func checkMachine(failed, linuxNow string) *Machine { m := machine(byLine(map[string]Ran{ "systemctl list-units --state=failed --plain --no-legend --no-pager": {Stdout: failed}, "pacman -Qq": {Stdout: "bash\nlinux\nlinux-firmware\nlinux-lts\n"}, "dkms status": {Status: 127, Err: "ENOENT"}, "pacman -Qlq -- linux": {Stdout: "/usr/lib/modules/" + linuxNow + "/vmlinuz\n"}, "pacman -Qlq -- linux-firmware": {Stdout: "/usr/lib/firmware/\n"}, "pacman -Qlq -- linux-lts": {Stdout: "/usr/lib/modules/6.18.54-2-lts/vmlinuz\n"}, }, nil), 1000) m.ReadFile = func(path string) ([]byte, error) { switch path { case OSRelease: return []byte("7.2.8-arch1-2\n"), nil case PacmanLog: return []byte(checkLog), nil } return nil, errNoFile } return m } func checkedRun(steps ...string) Run { r := Run{Node: "node-a", Run: "20261004T120000Z", Kind: RunUpgrade, From: FromAsk} at := time.Date(2026, 10, 4, 12, 0, 0, 0, time.UTC) for _, s := range steps { r.Steps = append(r.Steps, Step{Step: s, At: at}) } return r } func checkUpgrades(t *testing.T, m *Machine) *Upgrades { runs := newBucket() previous := Run{Node: "node-a", Run: "20260926T040000Z", Kind: RunUpgrade, From: FromRound, Steps: []Step{{Step: StepStarted, At: time.Date(2026, 9, 26, 4, 0, 0, 0, time.UTC)}}} if _, err := runs.Create(NodeKey("node-a", previous.Run), previous); err != nil { t.Fatal(err) } saved := fetch fetch = func(string) ([]byte, error) { return []byte(newsFeed), nil } t.Cleanup(func() { fetch = saved }) return &Upgrades{Node: "node-a", Machine: m, Runs: runs} } func TestTheCheckNamesTheFilesTheTransactionLeftAndTheNewsSinceThePreviousUpgrade(t *testing.T) { u := checkUpgrades(t, checkMachine("", "7.2.8-arch1-2")) c := u.AfterCheck(checkedRun(StepStarted, StepFinished), &Record{}) if strings.Join(c.Pacnew, " ") != "/etc/pacman.d/mirrorlist.pacnew" || strings.Join(c.Pacsave, " ") != "/etc/lemurs/config.toml.pacsave" { t.Errorf("only this transaction's files: %v %v", c.Pacnew, c.Pacsave) } if len(c.News) != 1 || c.News[0].Title != "A manual intervention" || c.NewsError != "" { t.Errorf("the news since the previous upgrade (26 September): %+v %q", c.News, c.NewsError) } if !c.Passed || c.RestartNeeded || c.RunningKernel != "7.2.8-arch1-2" || c.InstalledKernel != "7.2.8-arch1-2" { t.Errorf("%+v", c) } } func TestNewsThatCannotBeReadIsSaidNotShownAsNone(t *testing.T) { u := checkUpgrades(t, checkMachine("", "7.2.8-arch1-2")) fetch = func(string) ([]byte, error) { return nil, errors.New("no route to host") } c := u.AfterCheck(checkedRun(StepStarted, StepFinished), &Record{}) if !strings.Contains(c.NewsError, "no route to host") || !c.Passed { t.Errorf("%+v", c) } } func TestAUnitNewlyFailedFailsTheCheckAndOneFailedBeforeDoesNot(t *testing.T) { u := checkUpgrades(t, checkMachine("backup.service loaded failed failed The backup\nweb.service loaded failed failed The web\n", "7.2.8-arch1-2")) c := u.AfterCheck(checkedRun(StepStarted, StepFinished), &Record{FailedUnits: []string{"backup.service"}}) if strings.Join(c.NewlyFailed, " ") != "web.service" || c.Passed { t.Errorf("%+v", c) } } func TestARestartIsNeededWhenTheRunningKernelIsNoLongerTheInstalledOne(t *testing.T) { u := checkUpgrades(t, checkMachine("", "7.2.9-arch1-1")) c := u.AfterCheck(checkedRun(StepStarted, StepFinished), &Record{}) if !c.RestartNeeded || c.InstalledKernel != "7.2.9-arch1-1" || !c.Passed { t.Errorf("a restart is said and does not fail the check: %+v", c) } } func TestTheCheckPassesOnlyWhenTheTransactionSucceeded(t *testing.T) { u := checkUpgrades(t, checkMachine("", "7.2.8-arch1-2")) if c := u.AfterCheck(checkedRun(StepStarted, StepFailed), &Record{}); c.Passed { t.Errorf("%+v", c) } }