63dcccfeec
gates / gates (push) Successful in 1m29s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
109 lines
5.1 KiB
Go
109 lines
5.1 KiB
Go
package stacks
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// v0.341.0 — D39 = B (`09` §3 decision 255): the night leg takes a `security_fix` step like a proven one, and every
|
|
// other guard holds for it unchanged and in the same order. Same real leg over the real guarded update as
|
|
// unattended_test.go (legManager: nextcloud pinned at A, the catalog at C, ladder A→B→C).
|
|
//
|
|
// COMPANION RED-PROOF (REPORT.md): against the OLD verdict line (`e.Verdict != "proven"`) the first two tests fail
|
|
// (the step is skipped as not_proven). The held and stopped-by-household guards run BEFORE the verdict line, so the old
|
|
// line cannot turn them red; their red-proof is the mutation "a security_fix step returns before the guards" (both
|
|
// fail). The unknown-verdict test's red-proof is the mutation "any verdict but unrecorded is taken" (it fails).
|
|
|
|
const secMarks = `{"files_may_change": false, "needs_person": null, "memory_tight": false}`
|
|
|
|
// securityLine is a security_fix ladder entry A→B with its public notice, optionally with other marks.
|
|
func securityLine(from, to, marks string) string {
|
|
return ` - {"from": {"web": "` + from + `"}, "to": {"web": "` + to + `"}, "digest": {"web": "sha256:` +
|
|
strings.Repeat("a", 64) + `"}, "verdict": "security_fix", "marks": ` + marks +
|
|
`, "security": {"ids": ["CVE-2026-96652", "CVE-2026-96654"], "advisory": "https://www.cisa.gov/kev", "fixed_in": "1.43.3.10861"}, "seed": "none"}` + "\n"
|
|
}
|
|
|
|
func TestD39_SecurityFixStepIsTakenAndNamed(t *testing.T) {
|
|
m, dir, _, ups, logBuf := legManager(t)
|
|
writeLadder(t, m, securityLine(ladderA, ladderB, secMarks), ladderLine(ladderB, ladderC))
|
|
legOpts(m, nil)
|
|
s := mustLeg(t, m)
|
|
if len(*ups) != 1 || pinOf(t, dir) != ladderB {
|
|
t.Fatalf("the security_fix step was not taken: ups=%v pin=%s summary=%+v", *ups, pinOf(t, dir), s)
|
|
}
|
|
st := legStepFor(s, "nextcloud")
|
|
if st.Outcome != LegOutcomeDone || strings.Join(st.Security, ",") != "CVE-2026-96652,CVE-2026-96654" {
|
|
t.Fatalf("the summary step must be done and name the ids, got %+v", st)
|
|
}
|
|
want := "[update-leg] nextcloud: security fix CVE-2026-96652,CVE-2026-96654 taken (" + summarisePin(map[string]string{"web": ladderA}) + " → " + summarisePin(map[string]string{"web": ladderB}) + ")"
|
|
if !strings.Contains(logBuf.String(), want) {
|
|
t.Fatalf("the log must carry %q; log:\n%s", want, logBuf.String())
|
|
}
|
|
if !strings.Contains(s.Line(), "[security fix nextcloud: CVE-2026-96652,CVE-2026-96654 done]") {
|
|
t.Fatalf("the operator's summary line must name the security fix: %s", s.Line())
|
|
}
|
|
}
|
|
|
|
func TestD39_SecurityFixWithNeedsPersonIsSkipped(t *testing.T) {
|
|
m, dir, _, ups, _ := legManager(t)
|
|
writeLadder(t, m, securityLine(ladderA, ladderB, `{"files_may_change": false, "needs_person": "the admin must sign in again", "memory_tight": false}`),
|
|
ladderLine(ladderB, ladderC))
|
|
legOpts(m, nil)
|
|
s := mustLeg(t, m)
|
|
if len(*ups) != 0 || pinOf(t, dir) != ladderA || legStepFor(s, "nextcloud").Reason != LegSkipNeedsPerson {
|
|
t.Fatalf("security_fix + needs_person: ups=%v pin=%s step=%+v, want skipped %q", *ups, pinOf(t, dir), legStepFor(s, "nextcloud"), LegSkipNeedsPerson)
|
|
}
|
|
}
|
|
|
|
func TestD39_SecurityFixOnAHeldAppIsSkipped(t *testing.T) {
|
|
m, _, g, ups, _ := legManager(t)
|
|
g.held, g.holdWhy = true, "HELD"
|
|
writeLadder(t, m, securityLine(ladderA, ladderB, secMarks), ladderLine(ladderB, ladderC))
|
|
legOpts(m, nil)
|
|
s := mustLeg(t, m)
|
|
if len(*ups) != 0 || legStepFor(s, "nextcloud").Reason != LegSkipHeld {
|
|
t.Fatalf("security_fix on a held app: ups=%v step=%+v, want skipped %q", *ups, legStepFor(s, "nextcloud"), LegSkipHeld)
|
|
}
|
|
}
|
|
|
|
func TestD39_SecurityFixOnAnAppTheHouseholdStoppedIsSkipped(t *testing.T) {
|
|
m, _, _, ups, _ := legManager(t)
|
|
writeLadder(t, m, securityLine(ladderA, ladderB, secMarks), ladderLine(ladderB, ladderC))
|
|
legOpts(m, nil)
|
|
if err := m.ScanStacks(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := m.SetDesiredState("nextcloud", DesiredStateStopped); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s := mustLeg(t, m)
|
|
if len(*ups) != 0 || legStepFor(s, "nextcloud").Reason != LegSkipStoppedByHousehold {
|
|
t.Fatalf("security_fix on a stopped app: ups=%v step=%+v, want skipped %q", *ups, legStepFor(s, "nextcloud"), LegSkipStoppedByHousehold)
|
|
}
|
|
}
|
|
|
|
func TestD39_AnUnknownVerdictIsStillNotProven(t *testing.T) {
|
|
for _, v := range []string{"security-fix", "failed", "inconclusive", ""} {
|
|
m, _, _, ups, _ := legManager(t)
|
|
writeLadder(t, m, ladderLineMarks(ladderA, ladderB, v, secMarks), ladderLine(ladderB, ladderC))
|
|
legOpts(m, nil)
|
|
s := mustLeg(t, m)
|
|
if len(*ups) != 0 || legStepFor(s, "nextcloud").Reason != LegSkipNotProven {
|
|
t.Fatalf("verdict %q: ups=%v step=%+v, want skipped %q", v, *ups, legStepFor(s, "nextcloud"), LegSkipNotProven)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestD39_ProvenIsUnchangedAndCarriesNoSecurityNames(t *testing.T) {
|
|
m, dir, _, _, logBuf := legManager(t)
|
|
legOpts(m, nil)
|
|
s := mustLeg(t, m)
|
|
st := legStepFor(s, "nextcloud")
|
|
if pinOf(t, dir) != ladderB || st.Outcome != LegOutcomeDone || st.Security != nil {
|
|
t.Fatalf("a proven step: pin=%s step=%+v, want B done with no security names", pinOf(t, dir), st)
|
|
}
|
|
if strings.Contains(logBuf.String(), "security fix") || strings.Contains(s.Line(), "security fix") {
|
|
t.Fatalf("a proven step must not be logged as a security fix")
|
|
}
|
|
}
|