You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[FEA]: Executed unit coverage for the interrupt implementations (NodeRestart, ServiceRestart, RestartAllServices) #678
Give the Go agent's three interrupt implementations executed unit coverage. Today NodeRestart.Run, ServiceRestart.RunPending, and RestartAllServices.run sit at 0–17% because each constructs its own command.Runner internally (command.NewRunner(command.WithChroot(config.RootMount()))), so the only way to execute them is with a real reboot or systemctl inside a real chroot. Nothing does, so nothing has.
Motivation
These are the highest-consequence paths in the agent and the most novel code in the rewrite. NodeRestart carries the .pending / boot-ID reboot confirmation that has no Python equivalent, and the SIGTERM-as-success path that #673 just changed. None of it has ever executed: unit tests cannot run the commands, and the only interrupt type the operator-agent e2e suite runs against a real agent is noop. The main chainsaw suite's ten type: reboot cases run against the agentless image and cover the operator's side only. #222 ships this code as the production agent.
Proposed direction
Inject the runner through execution.Config, which is already the injection point for the execution environment (WithRootMount, WithRunOutput). Add a factory rather than a runner, because steps need two (chroot for on_host: true, plain otherwise):
The five call sites (step/shared.go ×2, service_restart.go, node_restart.go, restart_all_services.go) call config.NewRunner(...) instead. Production behaviour is unchanged. command.Runner is already the interface NewRunner returns, so make generate-mocks provides the fake once command is added to .mockery.yaml.
Specs, table-driven per the package convention:
ServiceRestart with [a, b]: exactly systemctl daemon-reload, systemctl restart a, systemctl restart b, in order, each with the chroot root and SkyhookDir as working directory; markCompleted(i) after each success and never after a failure; a failure on a stops before b; completed=[true,true,false] runs only b. The resume path a retry depends on has never executed.
RestartAllServices: service procps force-reload, same marker discipline.
NodeRestart, all four outcomes: Signal: SIGTERM → success; exit 0 → waitForNodeRestart (millisecond timeout, assert the "did not restart" error); non-zero → failed; runner error → error. Plus Signal: SIGKILL must not be success, pinning the race fix(agent): let the running step finish on SIGTERM so gracefulShutdown holds #673 removed.
runInterrupt with the real NodeRestart against a temp root and a fake proc/sys/kernel/random/boot_id: pending→complete promotion when the ID changes; stale pending rerun when it does not; and a pre-existing Python-style node_restart_0.complete being honoured without running anything — the Python→Go handoff as a test.
A stability spec for step.Fingerprint, which is the Go agent's on-disk flag filename and currently has no direct test.
The factory that records the options it was asked for also lets a step spec assert that on_host: false selected the non-chroot runner, closing that gap as a side effect.
Acceptance criteria
command.Runner is injectable through execution.Config; default behaviour unchanged.
internal/interrupts and the interrupt orchestration in internal/agent are above 85% with the specs above present.
Summary
Give the Go agent's three interrupt implementations executed unit coverage. Today
NodeRestart.Run,ServiceRestart.RunPending, andRestartAllServices.runsit at 0–17% because each constructs its owncommand.Runnerinternally (command.NewRunner(command.WithChroot(config.RootMount()))), so the only way to execute them is with a realrebootorsystemctlinside a real chroot. Nothing does, so nothing has.Motivation
These are the highest-consequence paths in the agent and the most novel code in the rewrite.
NodeRestartcarries the.pending/ boot-ID reboot confirmation that has no Python equivalent, and the SIGTERM-as-success path that #673 just changed. None of it has ever executed: unit tests cannot run the commands, and the only interrupt type the operator-agent e2e suite runs against a real agent isnoop. The main chainsaw suite's tentype: rebootcases run against the agentless image and cover the operator's side only. #222 ships this code as the production agent.Proposed direction
Inject the runner through
execution.Config, which is already the injection point for the execution environment (WithRootMount,WithRunOutput). Add a factory rather than a runner, because steps need two (chroot foron_host: true, plain otherwise):The five call sites (
step/shared.go×2,service_restart.go,node_restart.go,restart_all_services.go) callconfig.NewRunner(...)instead. Production behaviour is unchanged.command.Runneris already the interfaceNewRunnerreturns, somake generate-mocksprovides the fake oncecommandis added to.mockery.yaml.Specs, table-driven per the package convention:
ServiceRestartwith[a, b]: exactlysystemctl daemon-reload,systemctl restart a,systemctl restart b, in order, each with the chroot root andSkyhookDiras working directory;markCompleted(i)after each success and never after a failure; a failure onastops beforeb;completed=[true,true,false]runs onlyb. The resume path a retry depends on has never executed.RestartAllServices:service procps force-reload, same marker discipline.NodeRestart, all four outcomes:Signal: SIGTERM→ success; exit 0 →waitForNodeRestart(millisecond timeout, assert the "did not restart" error); non-zero → failed; runner error → error. PlusSignal: SIGKILLmust not be success, pinning the race fix(agent): let the running step finish on SIGTERM so gracefulShutdown holds #673 removed.runInterruptwith the realNodeRestartagainst a temp root and a fakeproc/sys/kernel/random/boot_id: pending→complete promotion when the ID changes; stale pending rerun when it does not; and a pre-existing Python-stylenode_restart_0.completebeing honoured without running anything — the Python→Go handoff as a test.step.Fingerprint, which is the Go agent's on-disk flag filename and currently has no direct test.The factory that records the options it was asked for also lets a step spec assert that
on_host: falseselected the non-chroot runner, closing that gap as a side effect.Acceptance criteria
command.Runneris injectable throughexecution.Config; default behaviour unchanged.internal/interruptsand the interrupt orchestration ininternal/agentare above 85% with the specs above present.make testandmake lintinagent/goare clean.Code of Conduct