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:
@@ -7,6 +7,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/hub"
|
||||
)
|
||||
@@ -62,6 +63,24 @@ func (l *Leg) SendUnsent(ctx context.Context) int {
|
||||
return l.sendUnsentLocked(ctx)
|
||||
}
|
||||
|
||||
// SendUnsentLoop runs SendUnsent now and then every `every` until ctx ends (v0.144.1). MEASURED live on demo-hp
|
||||
// 2026-10-05: after a kill -9 the daemon restarted in ~5 s, while the orphaned root wrapper was still installing — its
|
||||
// copy appeared ~7 s AFTER the start-time sender had looked. One look at start is therefore not enough. A glob of the
|
||||
// plan dir every few minutes costs nothing; the pass lock keeps it off a running pass. Pinned by
|
||||
// TestR868_ACopyWrittenAfterTheStartIsSentByTheLoop.
|
||||
func (l *Leg) SendUnsentLoop(ctx context.Context, every time.Duration, onSent func(int)) {
|
||||
for {
|
||||
if n := l.SendUnsent(ctx); n > 0 && onSent != nil {
|
||||
onSent(n)
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(every):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Leg) sendUnsentLocked(ctx context.Context) int {
|
||||
if l.Hub == nil {
|
||||
return 0 // nobody to send to: keep the copies for a process that has the hub
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/hub"
|
||||
)
|
||||
@@ -115,6 +116,29 @@ func TestR868_RunningPassKeepsTheSenderOff(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// v0.144.1 — THE LIVE SHAPE (demo-hp 2026-10-05 05:45 UTC): the restarted daemon looked at 05:45:09, the orphaned
|
||||
// wrapper wrote its copy at ~05:45:16. The loop must still send it.
|
||||
// COMPANION RED-PROOF: make SendUnsentLoop return after the first look → "the late copy was never sent".
|
||||
func TestR868_ACopyWrittenAfterTheStartIsSentByTheLoop(t *testing.T) {
|
||||
w := &fakeWrapper{t: t}
|
||||
l, h := newLeg(t, w, nil)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
sent := make(chan int, 4)
|
||||
go l.SendUnsentLoop(ctx, 20*time.Millisecond, func(n int) { sent <- n })
|
||||
time.Sleep(50 * time.Millisecond) // the start-time look found nothing
|
||||
must(t, os.WriteFile(reportFile(l.PlanDir, "late", LayerGuest, "apply"),
|
||||
[]byte(`{"mode":"apply","layer":"guest","run_id":"late","trigger":"debug","ring":0,"vmid":9201,"upgraded":[{"name":"openssl","version":"u3"}]}`), 0o600))
|
||||
select {
|
||||
case n := <-sent:
|
||||
if n != 1 || len(h.reports) != 1 || h.reports[0].RunID != "late" || h.reports[0].Outcome == "" {
|
||||
t.Fatalf("sent %d: %+v", n, h.reports)
|
||||
}
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("the late copy was never sent")
|
||||
}
|
||||
}
|
||||
|
||||
func must(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user