carlos / platform Public

host: establish a unit's running build from its bytes, and record settled builds as healthy

observe checks the mint conditions against /proc/<MainPID>/exe; recordSettled is the settled hook on checkedUnitRestart, run only when the settle check passed. Agent loops pass nil: only root records healthy builds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev a45afef89ad6b69f33e9709b33bc91d07db25e5b parent 8c0d720
6 files changed, +340 −13
  • internal/host/agent.go +14 −4
  • internal/host/agent_test.go +2 −2
  • internal/host/proofobserve.go +113 −0
  • internal/host/proofobserve_test.go +189 −0
  • internal/host/unitsettle.go +16 −1
  • internal/host/unitsettle_test.go +6 −6
diff --git a/internal/host/agent.go b/internal/host/agent.go
index db6bc1eff..036fd7b46 100644
--- a/internal/host/agent.go
+++ b/internal/host/agent.go
@@ -817,7 +817,7 @@ func adoptLoop(reg *registry.Registry, svc *activator.Service, sd *scheduleDeps)
return
}
optDir := release.OptDir()
- restart := makeRestartRoute(reg, systemctlTryRestart, checkedUnitRestart(systemdUnits{}, optDir, os.Getenv), adoptBounceFn(svc))
+ restart := makeRestartRoute(reg, systemctlTryRestart, checkedUnitRestart(systemdUnits{}, optDir, os.Getenv, nil), adoptBounceFn(svc))
log.Printf("agent: adopt loop armed (hourly + jitter)")
// First pass shortly after boot (Eleven's OnBootSec=5min, compressed —
// the edge is already up by the time this fires), then hourly + jitter.
@@ -1929,7 +1929,7 @@ func fastConvergeLoop(reg *registry.Registry, svc *activator.Service, sd *schedu
// sweep instead of the sweep re-cycling the service every tick. The
// remaining fields are filled in right below.
d := &fastDeps{reg: reg, store: store, pub: pub, optDir: optDir}
- restart := d.observeRestart(makeRestartRoute(reg, systemctlTryRestart, checkedUnitRestart(systemdUnits{}, optDir, os.Getenv), adoptBounceFn(svc)))
+ restart := d.observeRestart(makeRestartRoute(reg, systemctlTryRestart, checkedUnitRestart(systemdUnits{}, optDir, os.Getenv, nil), adoptBounceFn(svc)))
// Built once, used by both pass shapes below — see fastPassOpts for the
// DO-NOT list and why the stage requester is per-loop, not per-tick.
opts := fastPassOpts(dd, stageRequester(dd), restart, makeStartRoute(reg, systemctlStartUnit))
@@ -3218,7 +3218,12 @@ func unitOnly(f func(unit string) error) func(rt registry.Route) error {
// when the unit's ExecStart runs THIS route's live link. Anything it cannot
// establish (show denied, wrong binding, unit stopped) falls back to today's
// plain try-restart and returns that command's own error.
-func checkedUnitRestart(sys unitSystem, optDir string, getenv func(string) string) func(rt registry.Route) error {
+//
+// settled runs only when the settle check passed (never for a unit stopped
+// during it, a failed settle or an unchecked fallback). The agent loops pass
+// nil: the sandboxed agent cannot hash another unit's executable, and spec
+// §4.5 has root record the healthy history.
+func checkedUnitRestart(sys unitSystem, optDir string, getenv func(string) string, settled func(rt registry.Route)) func(rt registry.Route) error {
return func(rt registry.Route) error {
before, err := sys.Show(rt.Unit)
if err != nil {
@@ -3254,6 +3259,11 @@ func checkedUnitRestart(sys unitSystem, optDir string, getenv func(string) strin
}
if err == nil {
log.Printf("adopt: %s: %s settled on %s", rt.Host, rt.Unit, filepath.Dir(want))
+ // D3 (proofobserve.go): a build that passed the settle check is a
+ // build this box has run well.
+ if settled != nil {
+ settled(rt)
+ }
}
return err
}
@@ -3336,7 +3346,7 @@ func systemctlStartUnit(unit string) error {
// wired only into the agent would therefore be a restart arm the consoles
// never reach.
func rootRestartRoute(reg *registry.Registry, optDir string) func(host string) error {
- return makeRestartRoute(reg, systemctlTryRestart, checkedUnitRestart(systemdUnits{}, optDir, os.Getenv), nil)
+ return makeRestartRoute(reg, systemctlTryRestart, checkedUnitRestart(systemdUnits{}, optDir, os.Getenv, nil), nil)
}
// bounceFn adapts an activator to makeRestartRoute's bounce parameter,
diff --git a/internal/host/agent_test.go b/internal/host/agent_test.go
index ff39be92e..5ffdde92b 100644
--- a/internal/host/agent_test.go
+++ b/internal/host/agent_test.go
@@ -725,7 +725,7 @@ func TestRootRestartRouteCarriesTheNamedUnitArm(t *testing.T) {
// The checked arm reads the unit first (show), finds no binding it can
// verify from the fake's empty answer, and falls back to the plain
// try-restart: the argv this test pins is the last call.
- if want := "show console.service -p ActiveState -p SubState -p InvocationID -p MainPID -p NRestarts -p ExecStart\ntry-restart console.service\n"; string(got) != want {
+ if want := "show console.service -p ActiveState -p SubState -p InvocationID -p MainPID -p NRestarts -p ExecStart -p ActiveEnterTimestamp\ntry-restart console.service\n"; string(got) != want {
t.Fatalf("systemctl calls = %q, want %q", got, want)
}
}
@@ -974,7 +974,7 @@ func TestAdoptPassStartsTheNamedUnitAgainAfterAFailedRestart(t *testing.T) {
}
// The checked arm's show comes first (its empty fake answer verifies no
// binding, so the restart falls back to the plain try-restart).
- if want := "show console.service -p ActiveState -p SubState -p InvocationID -p MainPID -p NRestarts -p ExecStart\ntry-restart console.service\nstart console.service\n"; string(got) != want {
+ if want := "show console.service -p ActiveState -p SubState -p InvocationID -p MainPID -p NRestarts -p ExecStart -p ActiveEnterTimestamp\ntry-restart console.service\nstart console.service\n"; string(got) != want {
t.Fatalf("systemctl calls = %q, want %q — the rolled-back unit must be STARTED (try-restart would no-op on it)", got, want)
}
if v, err := release.LiveVersion(optDir, "console.test"); err != nil || v != "old" {
diff --git a/internal/host/proofobserve.go b/internal/host/proofobserve.go
new file mode 100644
index 000000000..fdad3fbcf
--- /dev/null
+++ b/internal/host/proofobserve.go
@@ -0,0 +1,113 @@
+// What a named unit is running, established from the bytes — D3 of the
+// console release defences (spec 2026-10-06-console-release-defences-design.md
+// §4.2, §4.3, §4.5).
+//
+// observe checks the first three of spec §4.3's mint conditions: the binding
+// is verified, the unit is active in one invocation for at least `after`, the
+// sha256 of /proc/<MainPID>/exe equals the live link's bytes, and the route is
+// ready now. The root pass mints on it (proofroot.go) and records the build
+// in the healthy history on it. recordSettled is the other way into that
+// history: the hook D1's checkedUnitRestart runs when the settle check passes.
+//
+// Both paths hash the running executable's bytes. A sha256 is never taken
+// from a manifest or a version directory's name: the only build a box may
+// vouch for is the one it is running.
+package host
+
+import (
+ "fmt"
+ "log"
+ "time"
+
+ "github.com/carlosframework/platform/internal/registry"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+type liveBuild struct {
+ SHA256 string
+ Version string
+ ActiveSince time.Time
+}
+
+type proofObserver struct {
+ sys unitSystem
+ procExe func(pid int) string // "/proc/<pid>/exe" in production
+ optDir string
+ now func() time.Time
+ ready func(rt registry.Route) func() error // routeReady in production; nil func = no upstream
+}
+
+func productionProofObserver(optDir string) proofObserver {
+ return proofObserver{sys: systemdUnits{}, procExe: procExePath, optDir: optDir, now: time.Now,
+ ready: func(rt registry.Route) func() error { return routeReady(rt, 2*time.Second) }}
+}
+
+func procExePath(pid int) string { return fmt.Sprintf("/proc/%d/exe", pid) }
+
+func (o proofObserver) observe(rt registry.Route, after time.Duration) (liveBuild, error) {
+ link := release.LiveLink(o.optDir, rt.Host)
+ before, err := o.sys.Show(rt.Unit)
+ if err != nil {
+ return liveBuild{}, fmt.Errorf("cannot read %s: %v", rt.Unit, err)
+ }
+ if before.ExecPath != link {
+ return liveBuild{}, fmt.Errorf("binding: %s runs %s, not %s", rt.Unit, before.ExecPath, link)
+ }
+ if before.ActiveState != "active" || before.MainPID <= 0 {
+ return liveBuild{}, fmt.Errorf("%s is %s", rt.Unit, before.ActiveState)
+ }
+ if before.ActiveEnterTimestamp.IsZero() {
+ return liveBuild{}, fmt.Errorf("%s reports no ActiveEnterTimestamp", rt.Unit)
+ }
+ if up := o.now().Sub(before.ActiveEnterTimestamp); up < after {
+ return liveBuild{}, fmt.Errorf("%s has been active for %s, needs %s", rt.Unit, up.Round(time.Second), after)
+ }
+ exe, err := release.FileSHA256(o.procExe(before.MainPID))
+ if err != nil {
+ return liveBuild{}, fmt.Errorf("hashing the running executable: %v", err)
+ }
+ live, err := release.FileSHA256(link)
+ if err != nil {
+ return liveBuild{}, fmt.Errorf("hashing the live link's build: %v", err)
+ }
+ if exe != live {
+ return liveBuild{}, fmt.Errorf("the running executable (sha256 %s) is not the live link's build (sha256 %s)", exe, live)
+ }
+ if o.ready != nil {
+ if ready := o.ready(rt); ready != nil {
+ if err := ready(); err != nil {
+ return liveBuild{}, fmt.Errorf("not ready: %v", err)
+ }
+ }
+ }
+ again, err := o.sys.Show(rt.Unit)
+ if err != nil || again.ActiveState != "active" || again.InvocationID != before.InvocationID || again.MainPID != before.MainPID {
+ return liveBuild{}, fmt.Errorf("%s restarted during the check", rt.Unit)
+ }
+ ver, _ := release.LiveVersion(o.optDir, rt.Host)
+ return liveBuild{SHA256: exe, Version: ver, ActiveSince: before.ActiveEnterTimestamp}, nil
+}
+
+// recordSettled is checkedUnitRestart's settled hook on the root pass: hash
+// the executable that just passed the settle check and add it to the app's
+// healthy history. Every failure is a log line; a missed record costs only a
+// proof wait later.
+func recordSettled(sys unitSystem, procExe func(pid int) string, optDir, bakeDir string, now func() time.Time) func(rt registry.Route) {
+ return func(rt registry.Route) {
+ p, err := sys.Show(rt.Unit)
+ if err != nil || p.ActiveState != "active" || p.MainPID <= 0 {
+ log.Printf("adopt: %s: settled, but cannot read %s's process for the healthy history (%v)", rt.Host, rt.Unit, err)
+ return
+ }
+ sha, err := release.FileSHA256(procExe(p.MainPID))
+ if err != nil {
+ log.Printf("adopt: %s: settled, but hashing its executable failed: %v", rt.Host, err)
+ return
+ }
+ ver, _ := release.LiveVersion(optDir, rt.Host)
+ if err := release.RecordHealthy(bakeDir, release.RouteAccount(rt), rt.App,
+ release.HealthyBuild{SHA256: sha, Version: ver, How: "settle", At: now().UTC()}); err != nil {
+ log.Printf("adopt: %s: recording the healthy build: %v", rt.Host, err)
+ }
+ }
+}
diff --git a/internal/host/proofobserve_test.go b/internal/host/proofobserve_test.go
new file mode 100644
index 000000000..2b7bce228
--- /dev/null
+++ b/internal/host/proofobserve_test.go
@@ -0,0 +1,189 @@
+package host
+
+import (
+ "errors"
+ "net"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/registry"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+func TestParseShowReadsActiveEnterTimestamp(t *testing.T) {
+ p := parseShow("ActiveState=active\nActiveEnterTimestamp=Mon 2026-10-06 09:30:30 UTC\n")
+ if want := time.Date(2026, 10, 6, 9, 30, 30, 0, time.UTC); !p.ActiveEnterTimestamp.Equal(want) {
+ t.Fatalf("ActiveEnterTimestamp = %v, want %v", p.ActiveEnterTimestamp, want)
+ }
+ if !parseShow("ActiveEnterTimestamp=\n").ActiveEnterTimestamp.IsZero() {
+ t.Fatal("an empty timestamp is not zero")
+ }
+ if !parseShow("ActiveEnterTimestamp=n/a\n").ActiveEnterTimestamp.IsZero() {
+ t.Fatal("n/a is not zero")
+ }
+}
+
+const observedHost = "platform.example.com"
+
+// observeFixture: the route's live link names versions/v1/console, and by
+// default the unit runs that very file, bound, active for 20 minutes, ready.
+func observeFixture(t *testing.T) (proofObserver, registry.Route, string, time.Time) {
+ t.Helper()
+ opt := t.TempDir()
+ liveTo(t, opt, observedHost, "v1")
+ staged := filepath.Join(opt, "versions", "v1", "console")
+ now := time.Date(2026, 10, 6, 10, 0, 0, 0, time.UTC)
+ o := proofObserver{
+ sys: &fakeUnits{shows: []unitProps{{ActiveState: "active", InvocationID: "i1", MainPID: 7,
+ ExecPath: release.LiveLink(opt, observedHost), ActiveEnterTimestamp: now.Add(-20 * time.Minute)}}},
+ procExe: func(int) string { return staged },
+ optDir: opt,
+ now: func() time.Time { return now },
+ ready: func(registry.Route) func() error { return nil },
+ }
+ rt := registry.Route{Host: observedHost, App: "console", Unit: "console.service"}
+ return o, rt, staged, now
+}
+
+func TestProofObserveAcceptsABakedBuild(t *testing.T) {
+ o, rt, staged, now := observeFixture(t)
+ b, err := o.observe(rt, 15*time.Minute)
+ if err != nil {
+ t.Fatalf("observe: %v", err)
+ }
+ want, _ := release.FileSHA256(staged)
+ if b.SHA256 != want || b.Version != "v1" || !b.ActiveSince.Equal(now.Add(-20*time.Minute)) {
+ t.Fatalf("build = %+v", b)
+ }
+}
+
+// Spec §7: the mint conditions, each false in turn.
+func TestProofObserveConditionsEachFalse(t *testing.T) {
+ for name, c := range map[string]struct {
+ edit func(t *testing.T, o *proofObserver, now time.Time)
+ want string
+ }{
+ "binding": {func(t *testing.T, o *proofObserver, now time.Time) {
+ o.sys.(*fakeUnits).shows[0].ExecPath = release.LiveLink(o.optDir, "console.example.com")
+ }, "binding"},
+ "not active": {func(t *testing.T, o *proofObserver, now time.Time) {
+ o.sys.(*fakeUnits).shows[0].ActiveState = "failed"
+ }, "failed"},
+ "too young": {func(t *testing.T, o *proofObserver, now time.Time) {
+ o.sys.(*fakeUnits).shows[0].ActiveEnterTimestamp = now.Add(-5 * time.Minute)
+ }, "needs 15m"},
+ "restarted during the check": {func(t *testing.T, o *proofObserver, now time.Time) {
+ f := o.sys.(*fakeUnits)
+ second := f.shows[0]
+ second.InvocationID = "i2"
+ f.shows = append(f.shows, second)
+ }, "restarted"},
+ "exe and live link differ": {func(t *testing.T, o *proofObserver, now time.Time) {
+ other := filepath.Join(t.TempDir(), "console")
+ if err := os.WriteFile(other, []byte("a different build"), 0o755); err != nil {
+ t.Fatal(err)
+ }
+ o.procExe = func(int) string { return other }
+ }, "is not the live link's build"},
+ "not ready": {func(t *testing.T, o *proofObserver, now time.Time) {
+ o.ready = func(registry.Route) func() error { return func() error { return errors.New("status 503") } }
+ }, "not ready"},
+ } {
+ t.Run(name, func(t *testing.T) {
+ o, rt, _, now := observeFixture(t)
+ c.edit(t, &o, now)
+ if _, err := o.observe(rt, 15*time.Minute); err == nil || !strings.Contains(err.Error(), c.want) {
+ t.Fatalf("observe = %v, want an error containing %q", err, c.want)
+ }
+ })
+ }
+}
+
+// D1's settle passing is the hook that records a healthy build, hashed from
+// the executable that passed.
+func TestSettledRestartRecordsTheRunningExe(t *testing.T) {
+ opt, bake := t.TempDir(), t.TempDir()
+ liveTo(t, opt, observedHost, "new")
+ staged, err := filepath.EvalSymlinks(filepath.Join(opt, "versions", "new", "console"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ ln, err := net.Listen("tcp", "127.0.0.1:0")
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { ln.Close() })
+ rt := registry.Route{Host: observedHost, App: "console", Unit: "console.service", Addr: ln.Addr().String()}
+ f := &fakeUnits{shows: []unitProps{
+ {ActiveState: "active", InvocationID: "old", ExecPath: filepath.Join(opt, "live", observedHost)},
+ {ActiveState: "active", InvocationID: "new", MainPID: 7},
+ }, exe: map[int]string{7: staged}}
+ settled := recordSettled(f, func(int) string { return staged }, opt, bake, time.Now)
+ if err := checkedUnitRestart(f, opt, func(string) string { return "" }, settled)(rt); err != nil {
+ t.Fatalf("restart: %v", err)
+ }
+ sha, _ := release.FileSHA256(staged)
+ h := release.ReadHealthy(bake, "bab", "console")
+ if len(h) != 1 || h[0].SHA256 != sha || h[0].How != "settle" || h[0].Version != "new" {
+ t.Fatalf("history = %+v, want the settled build", h)
+ }
+}
+
+func TestUnsettledRestartRecordsNothing(t *testing.T) {
+ opt, bake := t.TempDir(), t.TempDir()
+ liveTo(t, opt, observedHost, "new")
+ rt := registry.Route{Host: observedHost, App: "console", Unit: "console.service"}
+ f := &fakeUnits{shows: []unitProps{{ActiveState: "active", InvocationID: "old",
+ ExecPath: filepath.Join(opt, "live", "console.example.com")}}}
+ settled := recordSettled(f, func(int) string { return "/nonexistent" }, opt, bake, time.Now)
+ if err := checkedUnitRestart(f, opt, func(string) string { return "" }, settled)(rt); err != nil {
+ t.Fatalf("restart: %v", err)
+ }
+ if h := release.ReadHealthy(bake, "bab", "console"); len(h) != 0 {
+ t.Fatalf("a restart that was not checked recorded %+v", h)
+ }
+}
+
+// F2: a unit an operator stopped during the settle is not judged, and its
+// build is not recorded as healthy, even though its executable is readable.
+func TestStoppedDuringSettleRecordsNothing(t *testing.T) {
+ opt, bake := t.TempDir(), t.TempDir()
+ liveTo(t, opt, observedHost, "new")
+ staged := filepath.Join(opt, "versions", "new", "console")
+ rt := registry.Route{Host: observedHost, App: "console", Unit: "console.service"}
+ f := &fakeUnits{shows: []unitProps{
+ {ActiveState: "active", SubState: "running", InvocationID: "old", MainPID: 6,
+ ExecPath: filepath.Join(opt, "live", observedHost)},
+ {ActiveState: "inactive", SubState: "dead", InvocationID: "old"},
+ }}
+ settled := recordSettled(f, func(int) string { return staged }, opt, bake, time.Now)
+ if err := checkedUnitRestart(f, opt, func(string) string { return "" }, settled)(rt); err != nil {
+ t.Fatalf("restart: %v", err)
+ }
+ if h := release.ReadHealthy(bake, "bab", "console"); len(h) != 0 {
+ t.Fatalf("a stopped unit recorded %+v", h)
+ }
+}
+
+// A settle that fails records nothing either.
+func TestFailedSettleRecordsNothing(t *testing.T) {
+ opt, bake := t.TempDir(), t.TempDir()
+ liveTo(t, opt, observedHost, "new")
+ staged := filepath.Join(opt, "versions", "new", "console")
+ rt := registry.Route{Host: observedHost, App: "console", Unit: "console.service"}
+ f := &fakeUnits{shows: []unitProps{
+ {ActiveState: "active", SubState: "running", InvocationID: "old", MainPID: 6,
+ ExecPath: filepath.Join(opt, "live", observedHost)},
+ {ActiveState: "failed", SubState: "failed", InvocationID: "new"},
+ }}
+ settled := recordSettled(f, func(int) string { return staged }, opt, bake, time.Now)
+ if err := checkedUnitRestart(f, opt, func(string) string { return "" }, settled)(rt); err == nil {
+ t.Fatal("a failed unit settled")
+ }
+ if h := release.ReadHealthy(bake, "bab", "console"); len(h) != 0 {
+ t.Fatalf("a failed build recorded %+v", h)
+ }
+}
diff --git a/internal/host/unitsettle.go b/internal/host/unitsettle.go
index 15e15d842..918ec0bae 100644
--- a/internal/host/unitsettle.go
+++ b/internal/host/unitsettle.go
@@ -38,6 +38,8 @@ type unitProps struct {
MainPID int
NRestarts int
ExecPath string // ExecStart's executable path
+
+ ActiveEnterTimestamp time.Time // when the unit last entered active; zero when unknown
}
type unitSystem interface {
@@ -53,7 +55,7 @@ type systemdUnits struct{}
func (systemdUnits) Show(unit string) (unitProps, error) {
out, err := exec.Command("systemctl", "show", unit,
- "-p", "ActiveState", "-p", "SubState", "-p", "InvocationID", "-p", "MainPID", "-p", "NRestarts", "-p", "ExecStart").Output()
+ "-p", "ActiveState", "-p", "SubState", "-p", "InvocationID", "-p", "MainPID", "-p", "NRestarts", "-p", "ExecStart", "-p", "ActiveEnterTimestamp").Output()
if err != nil {
return unitProps{}, fmt.Errorf("systemctl show %s: %w", unit, err)
}
@@ -105,11 +107,24 @@ func parseShow(out string) unitProps {
if m := execPathRE.FindStringSubmatch(v); m != nil {
p.ExecPath = m[1]
}
+ case "ActiveEnterTimestamp":
+ p.ActiveEnterTimestamp = parseSystemdTime(v)
}
}
return p
}
+// parseSystemdTime reads systemctl show's default timestamp form,
+// "Mon 2026-10-06 09:30:30 UTC". Boxes run in UTC. "" and "n/a" are zero,
+// which every caller reads as "unknown", never as "long ago".
+func parseSystemdTime(v string) time.Time {
+ t, err := time.Parse("Mon 2006-01-02 15:04:05 MST", strings.TrimSpace(v))
+ if err != nil {
+ return time.Time{}
+ }
+ return t
+}
+
type settleOpts struct {
Timeout time.Duration // the whole window (CARLOS_UNIT_SETTLE)
Poll time.Duration // 500ms
diff --git a/internal/host/unitsettle_test.go b/internal/host/unitsettle_test.go
index c249b9060..a949ee2ef 100644
--- a/internal/host/unitsettle_test.go
+++ b/internal/host/unitsettle_test.go
@@ -386,7 +386,7 @@ func TestNamedRestartBindingMismatchKeepsTheError(t *testing.T) {
f := &fakeUnits{shows: []unitProps{{ActiveState: "active", InvocationID: "old",
ExecPath: filepath.Join(opt, "live", "console.example.com")}}}
failing := &failingTry{fakeUnits: f}
- err := checkedUnitRestart(failing, opt, func(string) string { return "" })(boundRoute())
+ err := checkedUnitRestart(failing, opt, func(string) string { return "" }, nil)(boundRoute())
if err == nil || !strings.Contains(err.Error(), "try-restart failed") {
t.Fatalf("err = %v, want the try-restart error propagated (codex 12)", err)
}
@@ -399,7 +399,7 @@ func (failingTry) TryRestart(string) error { return errors.New("try-restart fail
func TestNamedRestartShowFailsFallsBack(t *testing.T) {
opt := t.TempDir()
sys := &showFails{fakeUnits: &fakeUnits{}}
- if err := checkedUnitRestart(sys, opt, func(string) string { return "" })(boundRoute()); err != nil {
+ if err := checkedUnitRestart(sys, opt, func(string) string { return "" }, nil)(boundRoute()); err != nil {
t.Fatalf("err = %v, want today's behaviour (plain try-restart, its nil)", err)
}
if len(sys.restarts) != 1 || sys.restarts[0] != "try-restart console.service" {
@@ -415,7 +415,7 @@ func TestNamedRestartStoppedUnitIsNotChecked(t *testing.T) {
opt := t.TempDir()
liveTo(t, opt, "platform.example.com", "new")
f := &fakeUnits{shows: []unitProps{{ActiveState: "inactive", ExecPath: filepath.Join(opt, "live", "platform.example.com")}}}
- if err := checkedUnitRestart(f, opt, func(string) string { return "" })(boundRoute()); err != nil {
+ if err := checkedUnitRestart(f, opt, func(string) string { return "" }, nil)(boundRoute()); err != nil {
t.Fatalf("err = %v, want nil for an operator-stopped unit", err)
}
}
@@ -472,7 +472,7 @@ func TestNamedRestartSettlesAHealthyForwardRestart(t *testing.T) {
ExecPath: filepath.Join(opt, "live", "platform.example.com")},
{ActiveState: "active", SubState: "running", InvocationID: "new", MainPID: 7},
}, exe: map[int]string{7: filepath.Join(newBuildDir(t, opt, "platform.example.com"), "console")}}
- if err := checkedUnitRestart(f, opt, func(string) string { return "" })(rt); err != nil {
+ if err := checkedUnitRestart(f, opt, func(string) string { return "" }, nil)(rt); err != nil {
t.Fatalf("forward restart = %v, want settled", err)
}
if strings.Join(f.restarts, ",") != "try-restart console.service" {
@@ -492,7 +492,7 @@ func TestNamedRestartStopDuringSettleIsNotJudged(t *testing.T) {
ExecPath: filepath.Join(opt, "live", "platform.example.com")},
{ActiveState: "inactive", SubState: "dead", InvocationID: "old"},
}}
- if err := checkedUnitRestart(f, opt, func(string) string { return "" })(boundRoute()); err != nil {
+ if err := checkedUnitRestart(f, opt, func(string) string { return "" }, nil)(boundRoute()); err != nil {
t.Fatalf("err = %v, want nil: a stop is not a verdict on the build", err)
}
if strings.Join(f.restarts, ",") != "try-restart console.service" {
@@ -510,7 +510,7 @@ func TestNamedRestartFailedStillFails(t *testing.T) {
{ActiveState: "failed", SubState: "failed", InvocationID: "new"},
}}
var nu *release.UnitNotUpError
- if err := checkedUnitRestart(f, opt, func(string) string { return "" })(boundRoute()); !errors.As(err, &nu) {
+ if err := checkedUnitRestart(f, opt, func(string) string { return "" }, nil)(boundRoute()); !errors.As(err, &nu) {
t.Fatalf("err = %v, want UnitNotUpError for a failed unit", err)
}
}