7581f8140a
gates / gates (push) Successful in 7s
All three are the reporting and release path misreporting its own work. No
customer machine, no backup, no restore, no data. The restore-test itself and
when it runs are unchanged.
R-189 — a passing restore-test no longer vanishes on a restart. restore_tests[]
came only from the in-memory store, whose comment ("lost on restart; the cadence
re-populates") was true under a timer and stopped being true when R-86 made the
agent refuse to re-test a proven archive: the proof is then not repeated for a
whole archive generation. Observed live — a 14.5 GB offsite PASS reached no
host-report because the agent was restarted 2m43s later. RestoreTestState now
carries tier + verified beside the archive and renders reportable entries; the
collector merges them, one per tier, newest by TestedAt. It refuses to lie: a
record missing archive-or-tier produces no entry, and run mechanics are not
re-invented. Only successes are persisted, and the asymmetry is now written where
it will be read.
R-188 — a correct release stops emailing a failure. Only the tag PUSH moved
(build -> tag locally -> publish -> push tag): the push wakes CI, and a tag
visible before its package made the gate correctly fail a correct release about
half the time. The old order's invariant is asserted directly instead — the gate
now refuses a published version with no tag, as a bounded probe that prints its
own coverage, because the package listing api is still 401 without a token.
R-186 — a released binary can be verified by rebuilding it. -trimpath
-buildvcs=false: same source, same bytes, tag or no tag. Measured. publish-agent's
fallback also forced CGO_ENABLED=0 and produced a 74 KB different binary for the
same version; both paths now build identically. CLAUDE.md records the command.
157 lines
5.3 KiB
Go
157 lines
5.3 KiB
Go
package main
|
|
|
|
import (
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"testing"
|
|
)
|
|
|
|
// R-86 Scenario I — the seam-discipline test for the due-check.
|
|
//
|
|
// A due-check is worth nothing if the daemon still wires the OLD picker: every unit test in
|
|
// internal/backup would stay green (they inject the seam directly), the scheduler would ask for the
|
|
// newest archive with no settle cutoff, and the per-archive rule would run against a candidate that
|
|
// changes every time a backup lands. That is the same shape as the v0.91.0 inert seam — built,
|
|
// tested, never called — and this repo has shipped it four times.
|
|
//
|
|
// It walks main.go's AST rather than grepping: a commented-out call still satisfies a substring
|
|
// match, and a comment is not a caller.
|
|
func TestMainWiresTheSettleAwareTierPicker(t *testing.T) {
|
|
f := parseMainForWiring(t)
|
|
|
|
var settlePicker, oldPicker, settleWired, evalInterval bool
|
|
ast.Inspect(f, func(n ast.Node) bool {
|
|
switch node := n.(type) {
|
|
case *ast.SelectorExpr:
|
|
// runner.PickSettledRestoreCandidateOn passed as a value (not called).
|
|
switch node.Sel.Name {
|
|
case "PickSettledRestoreCandidateOn":
|
|
settlePicker = true
|
|
case "PickRestoreCandidateOn":
|
|
oldPicker = true
|
|
}
|
|
case *ast.KeyValueExpr:
|
|
key, ok := node.Key.(*ast.Ident)
|
|
if !ok {
|
|
return true
|
|
}
|
|
if key.Name == "Settle" {
|
|
settleWired = true
|
|
}
|
|
case *ast.CallExpr:
|
|
if sel, ok := node.Fun.(*ast.SelectorExpr); ok && sel.Sel.Name == "RestoreTestEvalInterval" {
|
|
evalInterval = true
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
|
|
if !settlePicker {
|
|
t.Error("main.go never passes runner.PickSettledRestoreCandidateOn as the scheduler's TierPick — " +
|
|
"the due-check would run without a settle cutoff, i.e. against an archive that may still be being written")
|
|
}
|
|
if oldPicker {
|
|
t.Error("main.go still wires the pre-R-86 PickRestoreCandidateOn as a tier picker — " +
|
|
"two pickers means the one under test is not the one running")
|
|
}
|
|
if !settleWired {
|
|
t.Error("main.go never sets SchedulerOptions.Settle — the settle lag would default to 0 in the daemon " +
|
|
"and every freshly-landed archive would be an immediate candidate")
|
|
}
|
|
if !evalInterval {
|
|
t.Error("main.go never calls cfg.Backup.RestoreTestEvalInterval() — the scheduler would be driven by " +
|
|
"the retired cadence knob")
|
|
}
|
|
}
|
|
|
|
// The two R-85 guarantees the due-check must not have quietly dropped: the spec is still built PER
|
|
// RUN, and the shared heavy-operation gate is still handed to the scheduler.
|
|
func TestMainStillWiresTheHeavyOperationGateAndPerRunSpec(t *testing.T) {
|
|
f := parseMainForWiring(t)
|
|
|
|
var inFlightWired, specIsAFunc bool
|
|
ast.Inspect(f, func(n ast.Node) bool {
|
|
kv, ok := n.(*ast.KeyValueExpr)
|
|
if !ok {
|
|
return true
|
|
}
|
|
key, ok := kv.Key.(*ast.Ident)
|
|
if !ok {
|
|
return true
|
|
}
|
|
switch key.Name {
|
|
case "InFlight":
|
|
inFlightWired = true
|
|
case "Spec":
|
|
// A FuncLit means it is evaluated per run; anything else is a frozen value.
|
|
if _, isFunc := kv.Value.(*ast.FuncLit); isFunc {
|
|
specIsAFunc = true
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
|
|
if !inFlightWired {
|
|
t.Error("main.go no longer hands the scheduler the shared InFlight gate — a restore-test could pull a " +
|
|
"multi-GB archive over the same tunnel an offsite backup is pushing one over (Scenario F)")
|
|
}
|
|
if !specIsAFunc {
|
|
t.Error("SchedulerOptions.Spec is no longer a function literal — a frozen spec is the R-85 defect " +
|
|
"(the tier and its timeout evaluated once at daemon start, forever)")
|
|
}
|
|
}
|
|
|
|
func parseMainForWiring(t *testing.T) *ast.File {
|
|
t.Helper()
|
|
fset := token.NewFileSet()
|
|
f, err := parser.ParseFile(fset, "main.go", nil, 0)
|
|
if err != nil {
|
|
t.Fatalf("parse main.go: %v", err)
|
|
}
|
|
return f
|
|
}
|
|
|
|
// R-189 Scenario I — the DURABLE proof source must actually be wired into the collector.
|
|
//
|
|
// This test exists because the method it feeds is the project's own cautionary tale:
|
|
// `RestoreTestState.Snapshot` carried the doc comment "for the host-report gauge" from the day it
|
|
// was written and **had no caller at all** — a seam built, documented and never connected, found
|
|
// only when a live restore-test's PASS reached no host-report. The fix must not become the next
|
|
// instance, so the wiring is asserted rather than trusted.
|
|
//
|
|
// AST, not grep: a commented-out call still contains the string (proven yesterday, when commenting
|
|
// out the tier-picker line failed this test while a `strings.Contains` check would have passed).
|
|
func TestMainWiresTheDurableRestoreTestProof(t *testing.T) {
|
|
f := parseMainForWiring(t)
|
|
|
|
var wired, feedsState bool
|
|
ast.Inspect(f, func(n ast.Node) bool {
|
|
call, ok := n.(*ast.CallExpr)
|
|
if !ok {
|
|
return true
|
|
}
|
|
sel, ok := call.Fun.(*ast.SelectorExpr)
|
|
if !ok || sel.Sel.Name != "SetProvenRestoreTests" {
|
|
return true
|
|
}
|
|
wired = true
|
|
// ...and it must be fed the PERSISTED state, not the in-memory store.
|
|
if len(call.Args) == 1 {
|
|
if id, ok := call.Args[0].(*ast.Ident); ok && id.Name == "rtState" {
|
|
feedsState = true
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
|
|
if !wired {
|
|
t.Error("main.go never calls collector.SetProvenRestoreTests — the persisted proof would never " +
|
|
"reach the hub, which is the R-189 defect exactly: a passing restore-test that vanishes on restart")
|
|
}
|
|
if wired && !feedsState {
|
|
t.Error("collector.SetProvenRestoreTests is not fed rtState — the in-memory store is the thing " +
|
|
"that does NOT survive a restart, so wiring it here would fix nothing")
|
|
}
|
|
}
|