a491abef6c
THE FACT WAS COMPUTED EVERY CYCLE AND KEPT NOWHERE. EscrowAutoConfirmer.Reconcile
has compared the hub's restic_pw_sha256 against the local key on every ACK since
SLICE 3. On the final-walk venue it logged, at 03:28:03Z and thirty-five minutes
before the customer looked, "the hub's escrow blob does not cover the CURRENT repo
password (hub hash 30ef574f != local 9b4a9a9d)" - and dropped it. The recovery
screen, evaluating in the same process, went on asking a question that could not
see it.
Now persisted: settings.HubEscrowKeySHA256 + HubEscrowKeyCheckedAt, recorded
UNCONDITIONALLY in Reconcile beside RecordPresence and RecordSuperseded - same
place, same reason: the box that needs it most is the rebuilt one with no target,
on which every gate below returns early.
OffsiteRecoveryOffer gains SHAPE (c): the hub holds a package for a key OTHER than
the one we are using. (a) and (b) are both proxies for that question and both have
now been wrong in opposite directions - (a) goes false the moment anything mints,
(b) is unreachable while the escrow is pending.
SEC 7.2, decided deliberately and stated in the code:
- a KNOWN DIFFERENCE offers, however old the reading. Age is not gated on. Both
sides are local; only the hub's half can be stale, and what the hub holds does
not change without a ceremony THIS box runs, which refreshes the hash on the
next ACK. Gating on age would make a box offline from the hub silently stop
offering - the exact failure this session removes. CheckedAt is persisted for
diagnosis, not as a gate.
- an ABSENT hash falls back to (a)/(b) and does NOT offer. "" is the hub
positively saying its package seals no repository password (legacy hash-less
escrow). Nothing to compare, and offering would put a permanent screen in
front of every legacy box.
The write damper: CheckedAt refreshes on every ack carrying a hash, but a save is
skipped when both the hash and the UTC day are unchanged, so an idle box does not
rewrite settings.json every fifteen minutes. It records WHEN WE LAST HEARD, not
when it last changed - the R-100 distinction.
Tests: Scenario C (a differing key offers, with both proxies asserted false first),
Scenario D (a matching key offers nothing), fact 1 still required, shape (a) still
works, and both SEC 7.2 halves.
RED-PROOFS, each with the mutation confirmed present in the file first:
D) hubHash != localHash conjunct dropped -> Scenario D FAILS (a healthy box
offered recovery forever); Scenario C still passes
WIRING) RecordEscrowKeyHash removed from the EscrowAutoConfirmer literal in
main.go -> TestMainWiresRecordEscrowKeyHash FAILS. This is the ships-inert
shape: unwired, everything compiles, every test in the package passes, the
auto-confirm still works, and shape (c) reads an empty hash forever.
Green: go build, go vet, go test ./... all pass.
127 lines
4.4 KiB
Go
127 lines
4.4 KiB
Go
package report
|
|
|
|
import (
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"testing"
|
|
)
|
|
|
|
// TestMainWiresRecordPresence — the seam-discipline test (§9 rule 6).
|
|
//
|
|
// `RecordPresence` is a nil-able field: an unwired confirmer compiles, every test in this package
|
|
// passes, the fleet reports nothing new, and the whole of R-204 item 4 is inert. That is this
|
|
// project's most-repeated failure shape — six features built and never wired, one of them an off-site
|
|
// restage event that existed and never fired once.
|
|
//
|
|
// It walks the AST of main.go rather than grepping the file, because a commented-out field still
|
|
// contains the string (the lesson from the lifecycle-gate wiring test next door), and it parses with
|
|
// comments DROPPED so a commented assignment cannot satisfy it.
|
|
func TestMainWiresRecordPresence(t *testing.T) {
|
|
const mainPath = "../../cmd/controller/main.go"
|
|
fset := token.NewFileSet()
|
|
f, err := parser.ParseFile(fset, mainPath, nil, 0) // comments dropped on purpose
|
|
if err != nil {
|
|
t.Fatalf("parse %s: %v — the wiring of RecordPresence is now unasserted", mainPath, err)
|
|
}
|
|
|
|
found := false
|
|
sawConfirmerLiteral := false
|
|
ast.Inspect(f, func(n ast.Node) bool {
|
|
lit, ok := n.(*ast.CompositeLit)
|
|
if !ok {
|
|
return true
|
|
}
|
|
// Match `report.EscrowAutoConfirmer{...}` (and a bare `EscrowAutoConfirmer{...}`).
|
|
name := ""
|
|
switch t := lit.Type.(type) {
|
|
case *ast.SelectorExpr:
|
|
name = t.Sel.Name
|
|
case *ast.Ident:
|
|
name = t.Name
|
|
}
|
|
if name != "EscrowAutoConfirmer" {
|
|
return true
|
|
}
|
|
sawConfirmerLiteral = true
|
|
for _, el := range lit.Elts {
|
|
kv, ok := el.(*ast.KeyValueExpr)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if k, ok := kv.Key.(*ast.Ident); ok && k.Name == "RecordPresence" {
|
|
found = true
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
|
|
// Distinguish "the literal moved" from "the field was dropped" — otherwise a refactor that
|
|
// relocated the confirmer would read as a passing test over nothing (the §12 rule: an absent
|
|
// thing is not evidence).
|
|
if !sawConfirmerLiteral {
|
|
t.Fatalf("no EscrowAutoConfirmer composite literal found in %s — did the wiring move? This test can no longer see it", mainPath)
|
|
}
|
|
if !found {
|
|
t.Fatal("EscrowAutoConfirmer is constructed WITHOUT RecordPresence — the box will never learn the hub holds its recovery package, and R-204 item 4 ships inert")
|
|
}
|
|
}
|
|
|
|
// confirmerFieldIsWired is the generalised form of the walk above: it reports whether
|
|
// `EscrowAutoConfirmer{...}` in main.go assigns `field`, and whether the literal was found at all.
|
|
// Comments are dropped on purpose, so a commented-out assignment cannot satisfy it.
|
|
func confirmerFieldIsWired(t *testing.T, field string) (found, sawLiteral bool) {
|
|
t.Helper()
|
|
const mainPath = "../../cmd/controller/main.go"
|
|
fset := token.NewFileSet()
|
|
f, err := parser.ParseFile(fset, mainPath, nil, 0)
|
|
if err != nil {
|
|
t.Fatalf("parse %s: %v — the wiring of %s is now unasserted", mainPath, err, field)
|
|
}
|
|
ast.Inspect(f, func(n ast.Node) bool {
|
|
lit, ok := n.(*ast.CompositeLit)
|
|
if !ok {
|
|
return true
|
|
}
|
|
name := ""
|
|
switch tt := lit.Type.(type) {
|
|
case *ast.SelectorExpr:
|
|
name = tt.Sel.Name
|
|
case *ast.Ident:
|
|
name = tt.Name
|
|
}
|
|
if name != "EscrowAutoConfirmer" {
|
|
return true
|
|
}
|
|
sawLiteral = true
|
|
for _, el := range lit.Elts {
|
|
kv, ok := el.(*ast.KeyValueExpr)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if k, ok := kv.Key.(*ast.Ident); ok && k.Name == field {
|
|
found = true
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
return found, sawLiteral
|
|
}
|
|
|
|
// TestMainWiresRecordEscrowKeyHash — R-241's seam-discipline test, and it matters more than most.
|
|
//
|
|
// `RecordEscrowKeyHash` is nil-able exactly like `RecordPresence`. Unwired, the confirmer still
|
|
// compiles, every test in this package still passes, the auto-confirm still works — and
|
|
// `OffsiteRecoveryOffer`'s shape (c) reads an empty hash forever, silently falling back to the two
|
|
// proxies that R-241 proved insufficient. **The fix would ship inert, in precisely the shape the
|
|
// spike found: a correct answer computed and kept nowhere.**
|
|
func TestMainWiresRecordEscrowKeyHash(t *testing.T) {
|
|
found, sawLiteral := confirmerFieldIsWired(t, "RecordEscrowKeyHash")
|
|
if !sawLiteral {
|
|
t.Fatal("no EscrowAutoConfirmer composite literal found in main.go — did the wiring move? This test can no longer see it")
|
|
}
|
|
if !found {
|
|
t.Fatal("EscrowAutoConfirmer is constructed WITHOUT RecordEscrowKeyHash — the hub's escrowed-key hash is never persisted, so the recovery screen's shape (c) can never fire and R-241 ships inert")
|
|
}
|
|
}
|