mesh/merge-gate pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery-group group fix/233-a-failed-probe-fails-only-what-needs-the-tool-2 ready: every member ready, and composed together they pass
mesh/delivery superseded: a newer head of the same pull request
110 lines
4.4 KiB
Go
110 lines
4.4 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-host/internal/declaration"
|
|
"github.com/novox/mesh-host/internal/store"
|
|
)
|
|
|
|
// unansweringPacman puts a pacman on PATH that answers as one whose configuration file is gone, as the
|
|
// workstation's did on 2026-10-04 (novox/hq issue 233), and makes this an arch host.
|
|
func unansweringPacman(t *testing.T) {
|
|
t.Helper()
|
|
bin := t.TempDir()
|
|
script := "#!/bin/sh\necho 'error: config file /etc/pacman.conf could not be read: No such file or directory' >&2\nexit 1\n"
|
|
if err := os.WriteFile(filepath.Join(bin, "pacman"), []byte(script), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
|
|
was := builtFor
|
|
builtFor = "arch"
|
|
t.Cleanup(func() { builtFor = was })
|
|
}
|
|
|
|
// TestReplay233 replays novox/hq issue 233. The package manager's module wrote pacman's configuration
|
|
// file; a declaration that left the module out removed it; from then on pacman did not answer, and the
|
|
// node-engine refused every declaration whole — the one that named the module again and would have
|
|
// written the file back included. A person restored the file by hand an hour and a half later.
|
|
//
|
|
// Held to: a declaration that restores the file is applied while pacman does not answer. Only what needs
|
|
// pacman fails, with the probe's words; the file is written and recorded. Written only with what the
|
|
// node-engine had before the fix, so it lays over the commit before it, where it fails.
|
|
func TestReplay233(t *testing.T) {
|
|
unansweringPacman(t)
|
|
dir := t.TempDir()
|
|
opts := options{state: filepath.Join(dir, "state.json")}
|
|
conf := filepath.Join(dir, "pacman.conf")
|
|
raw := []byte(`{"declaration":1,"resources":[` +
|
|
`{"id":"pacman.conf","type":"file","path":"` + conf + `","content":"[options]\n"},` +
|
|
`{"id":"pacman.base","type":"package","package":"base"}]}`)
|
|
|
|
report := applyAndKeep(context.Background(), opts, raw, nil, nil, nil)
|
|
|
|
if report.Refused != "" {
|
|
t.Fatalf("the declaration that restores the package manager's configuration was refused whole: %s", report.Refused)
|
|
}
|
|
if got, err := os.ReadFile(conf); err != nil || string(got) != "[options]\n" {
|
|
t.Fatalf("the file that restores the package manager was not written: %q, %v", got, err)
|
|
}
|
|
known, err := store.Load(opts.state)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := known.Find("pacman.conf"); !ok {
|
|
t.Error("the file written was not recorded, so the next apply would not know it is the mesh's")
|
|
}
|
|
words := strings.Join(failedWords(report.Failed), "\n")
|
|
if !strings.Contains(words, "pacman does not answer") || !strings.Contains(words, "could not be read") {
|
|
t.Errorf("the report does not say the package manager did not answer, in the probe's words: %q", words)
|
|
}
|
|
if !strings.Contains(words, "pacman.base") {
|
|
t.Errorf("the report does not name the package that was not applied: %q", words)
|
|
}
|
|
}
|
|
|
|
func failedWords(failed map[string]string) []string {
|
|
var out []string
|
|
for id, why := range failed {
|
|
out = append(out, id+": "+why)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Defends novox/hq issue 233 at the machine: `mesh-host apply` applies what does not need the package
|
|
// manager, and says the apply was partial with the probe's words — a non-zero exit, never "applied".
|
|
func TestApplyByHandAppliesWhatDoesNotNeedAPackageManagerThatDoesNotAnswer(t *testing.T) {
|
|
unansweringPacman(t)
|
|
dir := t.TempDir()
|
|
out := &bytes.Buffer{}
|
|
opts := options{state: filepath.Join(dir, "state.json"), out: out}
|
|
conf := filepath.Join(dir, "pacman.conf")
|
|
raw := []byte(`{"declaration":1,"resources":[` +
|
|
`{"id":"pacman.conf","type":"file","path":"` + conf + `","content":"[options]\n"},` +
|
|
`{"id":"pacman.base","type":"package","package":"base"}]}`)
|
|
d, err := declaration.ParseFileTrusted(raw)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = runApply(context.Background(), opts, d, raw, fromFile)
|
|
|
|
if _, statErr := os.Stat(conf); statErr != nil {
|
|
t.Fatalf("the file was not written: %v (the apply said %v)", statErr, err)
|
|
}
|
|
if err == nil {
|
|
t.Fatal("an apply whose package manager did not answer said it applied")
|
|
}
|
|
if !strings.Contains(err.Error(), "pacman does not answer") || !strings.Contains(err.Error(), "pacman.base") {
|
|
t.Errorf("the failure does not name the package or the probe's words: %v", err)
|
|
}
|
|
if !strings.Contains(out.String(), "pacman does not answer") {
|
|
t.Errorf("what was said before applying does not say the package manager does not answer:\n%s", out.String())
|
|
}
|
|
}
|