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
|
return nil
|
||||||
}
|
}
|
||||||
c := *m.leg.last
|
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
|
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)
|
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
|
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, ""
|
return e, ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package stacks
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"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
|
// TestLeg_FilesMayChangeNeedsAWholeCopy — decision 13's other mark: taken only when a fresh WHOLE copy
|
||||||
// exists (the backup side's truth table, asked through FreshWholeCopy).
|
// exists (the backup side's truth table, asked through FreshWholeCopy).
|
||||||
func TestLeg_FilesMayChangeNeedsAWholeCopy(t *testing.T) {
|
func TestLeg_FilesMayChangeNeedsAWholeCopy(t *testing.T) {
|
||||||
m, dir, _, ups, _ := legManager(t)
|
m, dir, _, ups, logBuf := legManager(t)
|
||||||
writeLadder(t, m,
|
writeLadder(t, m,
|
||||||
ladderLineMarks(ladderA, ladderB, "proven", `{"files_may_change": true, "needs_person": null, "memory_tight": false}`),
|
ladderLineMarks(ladderA, ladderB, "proven", `{"files_may_change": true, "needs_person": null, "memory_tight": false}`),
|
||||||
ladderLine(ladderB, ladderC))
|
ladderLine(ladderB, ladderC))
|
||||||
whole := false
|
whole := false
|
||||||
legOpts(m, func(o *UpdateLegOptions) {
|
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)
|
s := mustLeg(t, m)
|
||||||
if len(*ups) != 0 || legStepFor(s, "nextcloud").Reason != LegSkipFilesNoCopy {
|
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 {
|
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))
|
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)
|
// unwired check = no whole copy (fail closed)
|
||||||
m2, _, _, ups2, _ := legManager(t)
|
m2, _, _, ups2, _ := legManager(t)
|
||||||
writeLadder(t, m2,
|
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