stacks: R-687 — an empty leg reports steps [], and a taken files_may_change step names its whole copy
gates / gates (push) Successful in 26s
gates / gates (push) Successful in 26s
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:
@@ -186,7 +186,7 @@ func (m *Manager) LastUpdateLegSummary() *UpdateLegSummary {
|
||||
return nil
|
||||
}
|
||||
c := *m.leg.last
|
||||
c.Steps = append([]LegStep(nil), m.leg.last.Steps...)
|
||||
c.Steps = append([]LegStep{}, m.leg.last.Steps...) // R-687: `[]`, never `null`, in the hub report
|
||||
return &c
|
||||
}
|
||||
|
||||
@@ -437,6 +437,8 @@ func (m *Manager) legCandidate(ctx context.Context, name string, o *UpdateLegOpt
|
||||
m.logger.Printf("[INFO] [update-leg] %s: the step may change the app's files and no fresh whole copy exists (%s)", name, why)
|
||||
return e, LegSkipFilesNoCopy
|
||||
}
|
||||
// R-687: the TAKEN case names the copy that allowed it, as the skip names why not.
|
||||
m.logger.Printf("[INFO] [update-leg] %s: the step may change the app's files — taken, a fresh whole copy exists: %s", name, why)
|
||||
}
|
||||
return e, ""
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package stacks
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -227,13 +228,13 @@ func TestLeg_NeedsPersonIsNeverTaken(t *testing.T) {
|
||||
// TestLeg_FilesMayChangeNeedsAWholeCopy — decision 13's other mark: taken only when a fresh WHOLE copy
|
||||
// exists (the backup side's truth table, asked through FreshWholeCopy).
|
||||
func TestLeg_FilesMayChangeNeedsAWholeCopy(t *testing.T) {
|
||||
m, dir, _, ups, _ := legManager(t)
|
||||
m, dir, _, ups, logBuf := legManager(t)
|
||||
writeLadder(t, m,
|
||||
ladderLineMarks(ladderA, ladderB, "proven", `{"files_may_change": true, "needs_person": null, "memory_tight": false}`),
|
||||
ladderLine(ladderB, ladderC))
|
||||
whole := false
|
||||
legOpts(m, func(o *UpdateLegOptions) {
|
||||
o.FreshWholeCopy = func(context.Context, string) (bool, string) { return whole, "fake" }
|
||||
o.FreshWholeCopy = func(context.Context, string) (bool, string) { return whole, "tier 2 copy from FAKE-TIME" }
|
||||
})
|
||||
s := mustLeg(t, m)
|
||||
if len(*ups) != 0 || legStepFor(s, "nextcloud").Reason != LegSkipFilesNoCopy {
|
||||
@@ -244,6 +245,10 @@ func TestLeg_FilesMayChangeNeedsAWholeCopy(t *testing.T) {
|
||||
if len(*ups) != 1 || pinOf(t, dir) != ladderB || s.Done != 1 {
|
||||
t.Fatalf("with a fresh whole copy the step must be taken: ups=%v pin=%s", *ups, pinOf(t, dir))
|
||||
}
|
||||
// R-687 (v0.273.0): the taken step names the copy that allowed it. COMPANION RED-PROOF: drop the log line.
|
||||
if !strings.Contains(logBuf.String(), "taken, a fresh whole copy exists: tier 2 copy from FAKE-TIME") {
|
||||
t.Fatal("a taken files_may_change step must log which whole copy allowed it")
|
||||
}
|
||||
// unwired check = no whole copy (fail closed)
|
||||
m2, _, _, ups2, _ := legManager(t)
|
||||
writeLadder(t, m2,
|
||||
@@ -452,3 +457,19 @@ func TestLegDeadline(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestR687_EmptyLegReportsEmptySteps — a leg that pressed nothing reports `"steps": []` to the hub, not
|
||||
// `null` (read live 2026-09-25 from demo-hp's report). COMPANION RED-PROOF: copy with append(nil, …).
|
||||
func TestR687_EmptyLegReportsEmptySteps(t *testing.T) {
|
||||
m, _, _, _, _ := legManager(t)
|
||||
m.leg.mu.Lock()
|
||||
m.leg.last = &UpdateLegSummary{Trigger: "after-offsite", Steps: []LegStep{}}
|
||||
m.leg.mu.Unlock()
|
||||
b, err := json.Marshal(m.LastUpdateLegSummary())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(string(b), `"steps":[]`) {
|
||||
t.Fatalf("an empty leg must report steps as [], got %s", b)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user