v0.144.1 code: the wrapper survives a dead reader (BrokenPipe) so a killed pass still keeps its report; the agent looks for kept copies every 5 min (R-868, measured live)
gates / gates (push) Successful in 20s
gates / gates (push) Successful in 20s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -1077,3 +1077,49 @@ class SaveReportOnDisk(unittest.TestCase):
|
||||
with self.assertRaises(OSError):
|
||||
self.r.save_report(os.path.join(link, "plan-r3-guest-apply.json"), {"mode": "apply"})
|
||||
self.assertEqual(os.listdir(real), [])
|
||||
|
||||
|
||||
class AgentDiesMidPass(unittest.TestCase):
|
||||
"""R-868, MEASURED LIVE 2026-10-05 05:45 UTC on demo-hp (agent v0.144.0): the agent was kill -9-ed while apt-get
|
||||
ran; apt finished all 13 packages, but the wrapper's next log line went to a stderr pipe nobody reads any more ->
|
||||
BrokenPipeError -> the wrapper died before save_report: no DONE in the journal, no kept copy, no report.
|
||||
COMPANION RED-PROOF: let Runner.log write to stderr unguarded -> this test fails (BrokenPipeError)."""
|
||||
|
||||
def test_a_dead_reader_does_not_stop_the_report_copy(self):
|
||||
import io, sys, contextlib
|
||||
|
||||
class DeadPipe(io.TextIOBase):
|
||||
dead = False
|
||||
def write(self, s):
|
||||
if DeadPipe.dead:
|
||||
raise BrokenPipeError(32, "Broken pipe")
|
||||
return len(s)
|
||||
def flush(self):
|
||||
if DeadPipe.dead:
|
||||
raise BrokenPipeError(32, "Broken pipe")
|
||||
|
||||
class DyingFake(Fake):
|
||||
def emulate(self, argv):
|
||||
a = [x for x in argv if not re.match(r"^[A-Z_]+=", x) and x != "env"]
|
||||
if a and a[0] == "apt-get" and "install" in a and "-s" not in a and "--print-uris" not in a:
|
||||
DeadPipe.dead = True # the agent is killed while apt-get runs
|
||||
return super().emulate(argv)
|
||||
|
||||
f = DyingFake()
|
||||
journal = []
|
||||
f.log = lambda line: osapply.Runner.log(f, line) # the REAL log(): stderr, then the journal
|
||||
prev_run = osapply.subprocess.run
|
||||
osapply.subprocess.run = lambda argv, *a, **kw: (journal.append(argv[-1]) if argv[0] == "logger"
|
||||
else prev_run(argv, *a, **kw)) # never the real `logger`
|
||||
prev_err, prev_out = sys.stderr, sys.stdout
|
||||
sys.stderr, sys.stdout = DeadPipe(), DeadPipe()
|
||||
try:
|
||||
rc = osapply.main(["felhom-os-apply", "--plan", PLAN], runner=f)
|
||||
finally:
|
||||
sys.stderr, sys.stdout = prev_err, prev_out
|
||||
osapply.subprocess.run = prev_run
|
||||
DeadPipe.dead = False
|
||||
self.assertEqual(rc, 0)
|
||||
self.assertEqual(len(f.saved_reports), 1, "the kept copy is the only way this report reaches the hub")
|
||||
self.assertEqual(len(f.saved_reports[0][1]["upgraded"]), 2)
|
||||
self.assertTrue(any(l.startswith("os-apply: DONE") for l in journal), "the journal must still get the DONE line")
|
||||
|
||||
Reference in New Issue
Block a user