package system import ( "context" "errors" "strings" "testing" "time" ) // pacman's own words from the control node on 2026-10-02 (novox/hq 04-ISSUES/205): every mirror // 404s the versioned file a ten-week-old database names, and the one copy that arrives fails its // signature. Errors are pacman's stderr, which the runner folds into the error, not the output. const staleStderr = `error: failed retrieving file 'nodejs-26.5.0-1-x86_64.pkg.tar.zst' from mirror.hetzner.com : The requested URL returned error: 404 error: failed retrieving file 'nodejs-26.5.0-1-x86_64.pkg.tar.zst' from mirror.rackspace.com : The requested URL returned error: 404 error: failed retrieving file 'nodejs-26.5.0-1-x86_64.pkg.tar.zst' from arch.lucassymons.net : Could not resolve host: arch.lucassymons.net warning: fatal error from arch.lucassymons.net, skipping for the remainder of this transaction error: failed retrieving file 'nodejs-26.5.0-1-x86_64.pkg.tar.zst' from mirrors.cqu.edu.cn : Connection timed out after 10002 milliseconds error: nodejs: signature from "Bert Peters (packager key) " is invalid error: failed to commit transaction (invalid or corrupted package (PGP signature))` const staleStdout = `resolving dependencies... looking for conflicting packages... Packages (4) ada-3.4.4-1 c-ares-1.34.8-1 simdjson-1:4.6.4-1 nodejs-26.5.0-1 :: Retrieving packages... nodejs-26.5.0-1-x86_64 downloading... checking keyring... checking package integrity... :: File /var/cache/pacman/pkg/nodejs-26.5.0-1-x86_64.pkg.tar.zst is corrupted (invalid or corrupted package (PGP signature)). Errors occurred, no packages were upgraded.` // A runner that behaves as ExecRunner does on failure: stdout as the output, stderr in the error. func pacmanFailing(stdout, stderr string) Runner { return func(_ context.Context, name string, args ...string) (string, error) { return stdout, errors.New(name + " exited 1: " + stderr) } } func TestAStaleDatabaseIsSaidAsOneWithItsAgeAndTheRemedy(t *testing.T) { was := syncDatabaseAge defer func() { syncDatabaseAge = was }() syncDatabaseAge = func() string { return describeAge(time.Date(2026, 7, 24, 16, 56, 0, 0, time.UTC), time.Date(2026, 10, 3, 0, 0, 0, 0, time.UTC)) } err := arch{}.InstallPackage(context.Background(), pacmanFailing(staleStdout, staleStderr), "nodejs") if err == nil { t.Fatal("a failed install must fail") } for _, want := range []string{ "the package database on this machine is from 2026-07-24, 10 weeks old", "stale package index", "upgrading the machine — a full upgrade (`pacman -Syu`) by its operator — before the mesh can install nodejs", "partial upgrade", "returned error: 404", // pacman's own words follow } { if !strings.Contains(err.Error(), want) { t.Errorf("the error does not say %q:\n%s", want, err) } } if strings.Contains(err.Error(), "mirrors or the network") { t.Errorf("a stale database must not be read as a mirror outage:\n%s", err) } } // The same 404s read from stderr alone — the half this classifier used to be blind to. func TestTheClassifierReadsWhatPacmanWroteToStderr(t *testing.T) { if classifyInstallFailure(staleStderr) != installStale { t.Fatal("every mirror 404ing the named file is a stale database") } if classifyInstallFailure(staleStdout) != installStale { t.Fatal("a corrupted-signature line alone is a stale keyring") } if classifyInstallFailure("") != installOther { t.Fatal("nothing said is nothing classified") } } func TestAnUnreachableMirrorWithAFreshDatabaseIsAMirrorProblem(t *testing.T) { was := syncDatabaseAge defer func() { syncDatabaseAge = was }() syncDatabaseAge = func() string { return "from 2026-10-02, less than a day old" } outage := `error: failed retrieving file 'core.db' from mirror.hetzner.com : Could not resolve host: mirror.hetzner.com error: failed retrieving file 'core.db' from mirror.rackspace.com : Connection timed out after 10001 milliseconds error: failed to synchronize all databases (failed to retrieve some files)` if classifyInstallFailure(outage) != installMirrors { t.Fatal("no mirror answering, no 404, no signature fault: the mirrors, not the machine") } err := arch{}.InstallPackage(context.Background(), pacmanFailing("", outage), "nodejs") if err == nil || !strings.Contains(err.Error(), "mirrors or the network") || !strings.Contains(err.Error(), "less than a day old") { t.Errorf("a mirror outage is said as one, with the database's age beside it:\n%v", err) } } func TestAFailureThatIsNeitherKeepsPacmansWords(t *testing.T) { err := arch{}.InstallPackage(context.Background(), pacmanFailing("", "error: target not found: nodejsx"), "nodejsx") if err == nil || !strings.Contains(err.Error(), "target not found") || strings.Contains(err.Error(), "package database on this machine") { t.Errorf("an unknown package is pacman's own error, not a stale database:\n%v", err) } } func TestDescribeAge(t *testing.T) { now := time.Date(2026, 10, 3, 0, 0, 0, 0, time.UTC) for when, want := range map[time.Time]string{ now.Add(-2 * time.Hour): "less than a day old", now.Add(-5 * 24 * time.Hour): "5 days old", now.Add(-71 * 24 * time.Hour): "10 weeks old", } { if got := describeAge(when, now); !strings.HasSuffix(got, want) { t.Errorf("%s: got %q, want suffix %q", when, got, want) } } }