ff1db11db4
gates / gates (push) Successful in 29s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
174 lines
6.6 KiB
Go
174 lines
6.6 KiB
Go
package osupdates
|
|
|
|
import (
|
|
"database/sql"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
// `11` §5.3.1 (hub v0.133.0): test approvals end with the test.
|
|
|
|
// approveUnderTest makes ring 0 run a set and approves it at once under a TEST override (OS_APPROVE_AFTER=0 shape).
|
|
func approveUnderTest(t *testing.T, f *fix, override string, set ...Package) string {
|
|
t.Helper()
|
|
f.s.TestOverride = override
|
|
f.s.ApproveAfter, f.s.NightsRequired = 0, 0
|
|
f.report(t, "hp", "debug", true, set...)
|
|
f.report(t, "n100", "debug", true, set...)
|
|
if _, err := f.s.Evaluate(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rel, _ := f.s.Store.LatestOSRelease(LayerGuest)
|
|
if rel == nil {
|
|
t.Fatal("not approved")
|
|
}
|
|
return rel.ID
|
|
}
|
|
|
|
// An approval made under the override carries the mark; one made without it does not.
|
|
func TestTestApproval_IsMarked(t *testing.T) {
|
|
f := newFix(t)
|
|
approveUnderTest(t, f, "OS_APPROVE_AFTER=0s", pk("libc6", "u4"))
|
|
rel, _ := f.s.Store.LatestOSRelease(LayerGuest)
|
|
if !rel.Test {
|
|
t.Fatalf("an approval under a TEST override must be marked: %+v", rel)
|
|
}
|
|
if info := f.s.Releases(); len(info) != 1 || !info[0].Test {
|
|
t.Fatalf("the System page must see the mark: %+v", info)
|
|
}
|
|
f.s.TestOverride = ""
|
|
f.now = f.now.Add(time.Hour)
|
|
f.report(t, "hp", "debug", true, pk("libc6", "u5"))
|
|
f.report(t, "n100", "debug", true, pk("libc6", "u5"))
|
|
f.s.Evaluate()
|
|
rel, _ = f.s.Store.LatestOSRelease(LayerGuest)
|
|
if rel.Test {
|
|
t.Fatalf("an approval without the override must not be marked: %+v", rel)
|
|
}
|
|
}
|
|
|
|
// The consequence: after a restart without the override, a ring-1 box gets NO plan from the test approval; the
|
|
// cancellation is an operator event and ring-1 boxes are bumped.
|
|
func TestTestApproval_CancelledAtAStartWithoutTheOverride(t *testing.T) {
|
|
f := newFix(t)
|
|
id := approveUnderTest(t, f, "OS_APPROVE_AFTER=0s", pk("libc6", "u4"))
|
|
if b := f.s.DesiredBlock("cust1"); b.Release == nil || b.Release.ID != id {
|
|
t.Fatalf("before the restart the ring-1 box is served the release: %+v", b)
|
|
}
|
|
f.events, f.bumps = nil, nil
|
|
f.s.TestOverride = "" // the hub restarts without the override
|
|
ids, err := f.s.CancelTestReleases()
|
|
if err != nil || len(ids) != 1 || ids[0] != id {
|
|
t.Fatalf("cancelled %v, %v", ids, err)
|
|
}
|
|
if b := f.s.DesiredBlock("cust1"); b.Release != nil {
|
|
t.Fatalf("a ring-1 box must get no plan from a cancelled test approval: %+v", b.Release)
|
|
}
|
|
if len(f.events) != 1 || f.events[0] != EventCancelled {
|
|
t.Fatalf("events = %v", f.events)
|
|
}
|
|
if len(f.bumps) != 1 || f.bumps[0] != "cust1" {
|
|
t.Fatalf("ring-1 boxes must be bumped: %v", f.bumps)
|
|
}
|
|
if c := f.s.CancelledReleases(); len(c) != 1 || c[0].ID != id {
|
|
t.Fatalf("the page must list the cancellation: %+v", c)
|
|
}
|
|
// once is enough: a second start cancels nothing more
|
|
if again, _ := f.s.CancelTestReleases(); len(again) != 0 {
|
|
t.Fatalf("second start cancelled %v", again)
|
|
}
|
|
}
|
|
|
|
func TestTestApproval_StaysWhileTheOverrideIsStillOn(t *testing.T) {
|
|
f := newFix(t)
|
|
approveUnderTest(t, f, "OS_APPROVE_AFTER=0s", pk("libc6", "u4"))
|
|
if ids, _ := f.s.CancelTestReleases(); len(ids) != 0 {
|
|
t.Fatalf("the test is still running; nothing to cancel: %v", ids)
|
|
}
|
|
if b := f.s.DesiredBlock("cust1"); b.Release == nil {
|
|
t.Fatal("still served while the override is on")
|
|
}
|
|
}
|
|
|
|
// A test approval that a REAL approval has superseded is history, not cancelled; the real one stays served.
|
|
func TestTestApproval_SupersededIsLeftAlone(t *testing.T) {
|
|
f := newFix(t)
|
|
old := approveUnderTest(t, f, "OS_APPROVE_AFTER=0s", pk("libc6", "u4"))
|
|
f.s.TestOverride = ""
|
|
f.now = f.now.Add(time.Hour)
|
|
f.report(t, "hp", "debug", true, pk("libc6", "u5"))
|
|
f.report(t, "n100", "debug", true, pk("libc6", "u5"))
|
|
f.s.Evaluate()
|
|
real, _ := f.s.Store.LatestOSRelease(LayerGuest)
|
|
if real.ID == old || real.Test {
|
|
t.Fatalf("setup: %+v", real)
|
|
}
|
|
if ids, _ := f.s.CancelTestReleases(); len(ids) != 0 {
|
|
t.Fatalf("a superseded test approval must not be cancelled: %v", ids)
|
|
}
|
|
if b := f.s.DesiredBlock("cust1"); b.Release == nil || b.Release.ID != real.ID {
|
|
t.Fatalf("the real release stays served: %+v", b.Release)
|
|
}
|
|
}
|
|
|
|
// After a cancellation the SAME set is approved again by the ruled wait (a real release) — the cancel is not a ban.
|
|
func TestTestApproval_TheSetIsApprovedAgainByTheRuledWait(t *testing.T) {
|
|
f := newFix(t)
|
|
set := []Package{pk("libc6", "u4")}
|
|
approveUnderTest(t, f, "OS_APPROVE_AFTER=0s", set...)
|
|
f.s.TestOverride = ""
|
|
f.s.ApproveAfter, f.s.NightsRequired = 24*time.Hour, 1
|
|
f.s.CancelTestReleases()
|
|
f.now = f.now.Add(25 * time.Hour)
|
|
f.report(t, "hp", "night", true, set...)
|
|
f.report(t, "n100", "night", true, set...)
|
|
f.s.Evaluate()
|
|
rel, _ := f.s.Store.LatestOSRelease(LayerGuest)
|
|
if rel == nil || rel.Test {
|
|
t.Fatalf("the ruled wait must approve the set again, unmarked: %+v", rel)
|
|
}
|
|
}
|
|
|
|
// The one-time backfill: on a database from before the mark, an approval earlier than 24 h after its set was first
|
|
// seen (the 2026-10-04 shape: 1.5 h) is marked test; one after the ruled wait is not.
|
|
func TestTestApproval_BackfillMarksTheEarlyApprovals(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "hub.db")
|
|
db, err := sql.Open("sqlite", path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, q := range []string{
|
|
`CREATE TABLE os_candidates (fingerprint TEXT PRIMARY KEY, first_seen DATETIME NOT NULL, packages_json TEXT NOT NULL)`,
|
|
`CREATE TABLE os_releases (id TEXT PRIMARY KEY, fingerprint TEXT NOT NULL, approved_at DATETIME NOT NULL, approved_by TEXT NOT NULL, packages_json TEXT NOT NULL, layer TEXT NOT NULL DEFAULT 'guest')`,
|
|
`INSERT INTO os_candidates VALUES ('fpA', '2026-10-04 11:07:00', '[]'), ('fpB', '2026-10-02 08:00:00', '[]')`,
|
|
`INSERT INTO os_candidates VALUES ('fpD', '2026-10-04 14:28:00', '[]')`,
|
|
`INSERT INTO os_releases VALUES ('os-guest-early', 'fpA', '2026-10-04 12:39:33', 'auto', '[]', 'guest'),
|
|
('os-guest-ruled', 'fpB', '2026-10-03 09:00:00', 'auto', '[]', 'guest'),
|
|
('os-docker-button', 'fpD', '2026-10-04 14:28:42', 'operator', '[]', 'docker')`,
|
|
} {
|
|
if _, err := db.Exec(q); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
db.Close()
|
|
st, err := store.New(path, log.New(os.Stderr, "", 0))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer st.Close()
|
|
rels, _ := st.UnsupersededTestReleases(LayerGuest)
|
|
if len(rels) != 1 || rels[0].ID != "os-guest-early" {
|
|
t.Fatalf("backfill: unsuperseded test releases = %+v", rels)
|
|
}
|
|
// the operator's own button approval is not backfilled (a person approved it)
|
|
if d, _ := st.UnsupersededTestReleases(LayerDocker); len(d) != 0 {
|
|
t.Fatalf("backfill marked the operator's Docker approval: %+v", d)
|
|
}
|
|
}
|