package release import ( "bytes" "context" "crypto/ed25519" "encoding/json" "errors" "fmt" "os" "path/filepath" "strings" "sync" "testing" "time" "github.com/carlosframework/platform/internal/registry" ) const proofHost = "console.example.com" // proofEnv is startUnitFixture (live "old", channel canary/x promoted to // "new" whose bytes are "bad build") plus a prover key, a policy path, a bake // directory and one gate shared by every pass, as the agent shares one. It // keeps the release key too, so a test can promote again. type proofEnv struct { ctx context.Context reg *registry.Registry store Store priv ed25519.PrivateKey pub ed25519.PublicKey opt, bake string policy string prover ed25519.PrivateKey proverPub string newSHA string clock time.Time gate *ProofGate } func newProofEnv(t *testing.T) *proofEnv { t.Helper() // startUnitFixture's setup, inlined so the release key is kept. s := NewDirStore(t.TempDir()) ctx := context.Background() priv, pub := testKeys(t) opt := t.TempDir() reg := testRegistry(t) shipRelease(t, s, "bab", "console", "old", adoptPlatform, []byte("good build")) shipRelease(t, s, "bab", "console", "new", adoptPlatform, []byte("bad build")) if err := StageArtifact(opt, "bab", "old", "console", []byte("good build")); err != nil { t.Fatal(err) } addRoute(t, reg, proofHost, "console", "canary/x", false) if err := PointLive(opt, proofHost, "bab", "old", "console"); err != nil { t.Fatal(err) } if err := Promote(ctx, s, priv, "bab", "console", "canary/x", "new"); err != nil { t.Fatalf("Promote new: %v", err) } prover, _, pubB64 := proverKey(t) e := &proofEnv{ctx: ctx, reg: reg, store: s, priv: priv, pub: pub, opt: opt, bake: t.TempDir(), policy: filepath.Join(t.TempDir(), "proof-policy.json"), prover: prover, proverPub: pubB64, newSHA: etagOf([]byte("bad build")), clock: time.Date(2026, 10, 6, 10, 0, 0, 0, time.UTC)} e.gate = NewProofGate(e.policy, e.bake).WithClock(func() time.Time { return e.clock }) return e } func (e *proofEnv) writePolicy(t *testing.T, body string) { t.Helper() if err := os.WriteFile(e.policy, []byte(body), 0o644); err != nil { t.Fatal(err) } } func (e *proofEnv) requireDev(t *testing.T) { e.writePolicy(t, fmt.Sprintf(`{"schema":1,"require":[{"app":"console","prover":"dev","key":%q}]}`, e.proverPub)) } func (e *proofEnv) pass(t *testing.T, s Store) string { t.Helper() return e.passWith(t, s, func(string) error { return nil }) } func (e *proofEnv) passWith(t *testing.T, s Store, restart func(string) error) string { t.Helper() var out bytes.Buffer withCapturedStdout(t, &out, func() { if err := Pass(e.ctx, e.reg, s, e.pub, e.opt, restart, BakeDir(e.bake), WithProofGate(e.gate)); err != nil { t.Fatalf("Pass: %v", err) } }) return out.String() } func (e *proofEnv) live() string { v, _ := LiveVersion(e.opt, proofHost); return v } func (e *proofEnv) signed(t *testing.T, key ed25519.PrivateKey, app, sha, prover string) *Proof { t.Helper() p, err := SignProof(key, Proof{App: app, Platform: adoptPlatform, SHA256: sha, Version: "new", Prover: prover, ActiveSince: e.clock.Add(-time.Hour), ProvenAt: e.clock}) if err != nil { t.Fatal(err) } return p } // putAt writes p at the dev key for sha, whatever p itself says. func (e *proofEnv) putAt(t *testing.T, sha string, p *Proof) { t.Helper() b, err := json.MarshalIndent(p, "", " ") if err != nil { t.Fatal(err) } if _, err := e.store.Put(e.ctx, ProofKey("bab", "console", sha, "dev"), b, ""); err != nil { t.Fatal(err) } } // manifestDownStore answers every other key from the wrapped store but fails // reads of one manifest with an error that is not a not-found, as a bucket // that is unreachable for that object would. type manifestDownStore struct { Store key string } func (m manifestDownStore) Get(ctx context.Context, key string) ([]byte, string, error) { if key == m.key { return nil, "", errors.New("bucket unreachable for the manifest") } return m.Store.Get(ctx, key) } // stageStaleCopyKept stages stale as the binary of acct/app/ver, with the // canonical manifest (the store's exact bytes) as its sibling, and returns s // wrapped so that manifest cannot be read from the bucket. // // This is how stale bytes reach the binding check since staging hashes a copy // it reuses (spec section 4.2): an unreferenced copy that differs from its // release is restaged, so the check only still sees different bytes where the // canonical manifest cannot be read, a case ensureStagedChecked leaves as // staged. The sibling lets the proof gate still learn the manifest's sha256. func stageStaleCopyKept(t *testing.T, ctx context.Context, s Store, opt, acct, app, ver string, stale []byte) Store { t.Helper() key := ManifestKey(acct, app, ver) raw, _, err := s.Get(ctx, key) if err != nil { t.Fatal(err) } if err := StageArtifact(opt, acct, ver, app, stale); err != nil { t.Fatal(err) } if err := StageManifest(opt, acct, ver, app, raw); err != nil { t.Fatal(err) } return manifestDownStore{Store: s, key: key} } type proofReadCountingStore struct { Store mu sync.Mutex gets []string } func (c *proofReadCountingStore) Get(ctx context.Context, key string) ([]byte, string, error) { c.mu.Lock() c.gets = append(c.gets, key) c.mu.Unlock() return c.Store.Get(ctx, key) } func (c *proofReadCountingStore) proofGets() int { c.mu.Lock() defer c.mu.Unlock() n := 0 for _, k := range c.gets { if strings.Contains(k, "/proofs/") { n++ } } return n } func TestProofNoPolicyMakesNoProofRead(t *testing.T) { e := newProofEnv(t) cs := &proofReadCountingStore{Store: e.store} e.pass(t, cs) if e.live() != "new" { t.Fatalf("live = %q, want new: with no policy file adoption is unchanged", e.live()) } if n := cs.proofGets(); n != 0 { t.Fatalf("%d proof GETs with no policy file, want 0 (spec ยง4.6)", n) } } func TestProofMissingStaysAndIsNotARefusal(t *testing.T) { e := newProofEnv(t) e.requireDev(t) out := e.pass(t, e.store) if e.live() != "old" { t.Fatalf("live = %q, want old", e.live()) } if !strings.Contains(out, "is not proven on dev; staying on old") { t.Fatalf("printed %q, want the not-proven line", out) } if r, _ := ReadRefusal(e.bake, proofHost); r != nil { t.Fatalf("a missing proof wrote a refusal: %+v", r) } w := e.gate.Waits() if len(w) != 1 || w[0].Host != proofHost || w[0].SHA256 != e.newSHA || w[0].Prover != "dev" { t.Fatalf("waits = %+v", w) } } func TestProofValidAdopts(t *testing.T) { e := newProofEnv(t) e.requireDev(t) e.putAt(t, e.newSHA, e.signed(t, e.prover, "console", e.newSHA, "dev")) e.pass(t, e.store) if e.live() != "new" { t.Fatalf("live = %q, want new", e.live()) } if w := e.gate.Waits(); len(w) != 0 { t.Fatalf("waits = %+v after adopting", w) } } func TestProofRejectsBadProofs(t *testing.T) { for name, mk := range map[string]func(t *testing.T, e *proofEnv) *Proof{ "bad signature": func(t *testing.T, e *proofEnv) *Proof { other, _, _ := proverKey(t) return e.signed(t, other, "console", e.newSHA, "dev") }, "wrong sha256": func(t *testing.T, e *proofEnv) *Proof { return e.signed(t, e.prover, "console", etagOf([]byte("other bytes")), "dev") }, "wrong app": func(t *testing.T, e *proofEnv) *Proof { return e.signed(t, e.prover, "other", e.newSHA, "dev") }, "wrong prover": func(t *testing.T, e *proofEnv) *Proof { return e.signed(t, e.prover, "console", e.newSHA, "staging") }, } { t.Run(name, func(t *testing.T) { e := newProofEnv(t) e.requireDev(t) e.putAt(t, e.newSHA, mk(t, e)) out := e.pass(t, e.store) if e.live() != "old" || !strings.Contains(out, "is not proven on dev") { t.Fatalf("live = %q, printed %q; want old and the not-proven line", e.live(), out) } }) } } // Review focus 3: rolling back to a build this box ran never waits on dev. func TestProofHealthyHistoryAdmitsWithoutProof(t *testing.T) { e := newProofEnv(t) e.requireDev(t) if err := RecordHealthy(e.bake, "bab", "console", HealthyBuild{SHA256: e.newSHA, Version: "new", How: "settle", At: e.clock}); err != nil { t.Fatal(err) } cs := &proofReadCountingStore{Store: e.store} e.pass(t, cs) if e.live() != "new" { t.Fatalf("live = %q, want new from the healthy history", e.live()) } if n := cs.proofGets(); n != 0 { t.Fatalf("%d proof GETs for a build in the healthy history, want 0", n) } } // Review focus 2: the manifest's sha256 is proven, but the staged file holds // other bytes. They reach the binding check through a kept stale copy: the // bucket cannot answer for the manifest, so staging leaves the copy as staged // (stageStaleCopyKept). // // Ruling 3: the record is its own kind, recovered and still on the old // build, because nothing was restarted. The next pass's gate line says the // staged bytes do not match the proof and never suggests --retry, which // would re-sign the pointer over the same stale bytes and be refused again. func TestProofBindingRefusesStagedBytesThatDiffer(t *testing.T) { e := newProofEnv(t) e.requireDev(t) stale := []byte("not the shipped bytes") down := stageStaleCopyKept(t, e.ctx, e.store, e.opt, "bab", "console", "new", stale) e.putAt(t, e.newSHA, e.signed(t, e.prover, "console", e.newSHA, "dev")) out := e.pass(t, down) if e.live() != "old" || !strings.Contains(out, "not pointed live") { t.Fatalf("live = %q, printed %q; want old and the binding refusal", e.live(), out) } r, _ := ReadRefusal(e.bake, proofHost) if r == nil || !strings.Contains(r.Reason, etagOf(stale)) || !strings.Contains(r.Reason, e.newSHA) { t.Fatalf("refusal = %+v, want one naming both hashes", r) } if r.Kind != RefusalNotProven || !r.Recovered || r.RestoredVersion != "old" || r.RecoveryError != "" { t.Fatalf("refusal = %+v, want kind %q, recovered, restored to old", r, RefusalNotProven) } out = e.pass(t, e.store) if e.live() != "old" { t.Fatalf("live = %q on the second pass, want old", e.live()) } want := "refused: new's staged bytes do not match dev's proof" if !strings.Contains(out, want) || !strings.Contains(out, "still on old") || !strings.Contains(out, "roll the exact bytes dev proved") { t.Fatalf("second pass printed %q, want the not-proven gate line", out) } if strings.Contains(out, "--retry") || strings.Contains(out, "did not come up") { t.Fatalf("second pass printed %q: a not-proven refusal is not a failed start", out) } } // Final review triage (Task 4): the healthy history admits the manifest's // sha256, but the staged file holds other bytes (a kept stale copy, as in // TestProofBindingRefusesStagedBytesThatDiffer). bind must ask the history // about the staged hash, not the manifest's, so the build is refused. func TestProofHealthyManifestWithOtherStagedBytesIsRefused(t *testing.T) { e := newProofEnv(t) e.requireDev(t) if err := RecordHealthy(e.bake, "bab", "console", HealthyBuild{SHA256: e.newSHA, Version: "new", How: "settle", At: e.clock}); err != nil { t.Fatal(err) } stale := []byte("not the shipped bytes") down := stageStaleCopyKept(t, e.ctx, e.store, e.opt, "bab", "console", "new", stale) out := e.pass(t, down) if e.live() != "old" || !strings.Contains(out, "not pointed live") { t.Fatalf("live = %q, printed %q; want old and the binding refusal", e.live(), out) } r, _ := ReadRefusal(e.bake, proofHost) if r == nil || r.Kind != RefusalNotProven || !strings.Contains(r.Reason, etagOf(stale)) || !strings.Contains(r.Reason, e.newSHA) { t.Fatalf("refusal = %+v, want a not-proven record naming the staged and manifest hashes", r) } } func TestProofMissIsCachedThirtySeconds(t *testing.T) { e := newProofEnv(t) e.requireDev(t) cs := &proofReadCountingStore{Store: e.store} e.pass(t, cs) e.clock = e.clock.Add(10 * time.Second) e.pass(t, cs) if n := cs.proofGets(); n != 1 { t.Fatalf("%d proof GETs inside 30s, want 1", n) } if e.gate.WaitingDue(proofHost) { t.Fatal("due inside its 30s miss") } e.clock = e.clock.Add(21 * time.Second) if !e.gate.WaitingDue(proofHost) { t.Fatal("not due after 31s") } e.pass(t, cs) if n := cs.proofGets(); n != 2 { t.Fatalf("%d proof GETs after 31s, want 2", n) } } // Review focus 1: a typo keeps the gate closed for a named-unit route. func TestProofPolicyUnparseableKeepsGateClosed(t *testing.T) { e := newProofEnv(t) if err := e.reg.SetUnit(proofHost, "console.service"); err != nil { t.Fatal(err) } e.writePolicy(t, `{"schema":1,"require":[`) out := e.pass(t, e.store) if e.live() != "old" { t.Fatalf("live = %q, want old: an unparseable policy must not open the gate", e.live()) } if !strings.Contains(out, "does not parse") { t.Fatalf("printed %q, want the policy error", out) } if w := e.gate.Waits(); len(w) != 1 || !strings.Contains(w[0].Reason, "does not parse") { t.Fatalf("waits = %+v", w) } } // The Task 2 carry: an error that is not a parse error (here, a policy path // that is a directory and cannot be read) is closed too, never "no policy". func TestProofPolicyUnreadableKeepsGateClosed(t *testing.T) { e := newProofEnv(t) if err := e.reg.SetUnit(proofHost, "console.service"); err != nil { t.Fatal(err) } if err := os.Mkdir(e.policy, 0o755); err != nil { t.Fatal(err) } e.pass(t, e.store) if e.live() != "old" { t.Fatalf("live = %q, want old: an unreadable policy must not open the gate", e.live()) } if w := e.gate.Waits(); len(w) != 1 || w[0].Host != proofHost { t.Fatalf("waits = %+v, want the named-unit route waiting", w) } } // The agent's gate lives across passes: a policy edit is seen on the next // pass without restarting anything. func TestProofPolicyChangeSeenNextPass(t *testing.T) { e := newProofEnv(t) e.requireDev(t) e.pass(t, e.store) if e.live() != "old" { t.Fatalf("live = %q, want old while required", e.live()) } e.writePolicy(t, `{"schema":1}`) e.pass(t, e.store) if e.live() != "new" { t.Fatalf("live = %q, want new once the require entry is gone", e.live()) } } // Ruling 5 (preflight 4.6): a host waiting on one head whose channel moves // to a build that stops before the gate (bake hold, poisoned this pass, a // refusal record) is no longer waiting. Otherwise WaitingDue stays true and // the fast loop sweeps it every tick for the whole hold. func TestProofWaitClearedWhenTheGateIsNotConsulted(t *testing.T) { assertCleared := func(t *testing.T, e *proofEnv, out string) { t.Helper() if w := e.gate.Waits(); len(w) != 0 { t.Fatalf("waits = %+v, printed %q; want none once the gate is not consulted", w, out) } if e.gate.WaitingDue(proofHost) { t.Fatal("still due once the gate is not consulted") } } waitOnNew := func(t *testing.T) *proofEnv { t.Helper() e := newProofEnv(t) e.requireDev(t) e.pass(t, e.store) if w := e.gate.Waits(); len(w) != 1 || w[0].Version != "new" { t.Fatalf("waits = %+v, want one on new", w) } e.clock = e.clock.Add(ProofMissTTL + time.Second) return e } t.Run("bake hold", func(t *testing.T) { e := waitOnNew(t) // beta has a 24h window, and a production app's windows apply. if err := SetProduction(e.ctx, e.store, e.priv, "bab", "console", true, "test"); err != nil { t.Fatal(err) } shipRelease(t, e.store, "bab", "console", "newer", adoptPlatform, []byte("newer build")) if err := Promote(e.ctx, e.store, e.priv, "bab", "console", "beta", "newer"); err != nil { t.Fatal(err) } addRoute(t, e.reg, proofHost, "console", "beta", false) out := e.pass(t, e.store) if !strings.Contains(out, "bake window") { t.Fatalf("printed %q, want the bake hold", out) } assertCleared(t, e, out) }) t.Run("poisoned", func(t *testing.T) { e := waitOnNew(t) // A second, hibernating route on the same channel goes after the // awake one, so it meets the build the awake one poisoned. const sleeper = "sleeper.example.com" addRoute(t, e.reg, sleeper, "console", "canary/x", true) if err := PointLive(e.opt, sleeper, "bab", "old", "console"); err != nil { t.Fatal(err) } e.putAt(t, e.newSHA, e.signed(t, e.prover, "console", e.newSHA, "dev")) // The sleeper is the one whose wait must go: make it wait first. e.gate.hold(registry.Route{Host: sleeper, App: "console"}, "new", e.newSHA, "dev", "no proof from dev", "old") e.clock = e.clock.Add(ProofMissTTL + time.Second) out := e.passWith(t, e.store, func(string) error { return errors.New("Job for console.service failed") }) if !strings.Contains(out, "failed on another host this pass") { t.Fatalf("printed %q, want the poisoned skip", out) } for _, w := range e.gate.Waits() { if w.Host == sleeper { t.Fatalf("waits = %+v, printed %q; the poisoned host is still waiting", e.gate.Waits(), out) } } if e.gate.WaitingDue(sleeper) { t.Fatal("poisoned host still due") } }) t.Run("refusal gate", func(t *testing.T) { e := waitOnNew(t) ptr, err := Channel(e.ctx, e.store, e.pub, "bab", "console", "canary/x") if err != nil { t.Fatal(err) } if err := WriteRefusal(e.bake, Refusal{Host: proofHost, App: "console", Version: "new", PointerPromotedAt: ptr.PromotedAt, RefusedAt: e.clock, Reason: "exited: status=2"}); err != nil { t.Fatal(err) } out := e.pass(t, e.store) if !strings.Contains(out, "refused: new") { t.Fatalf("printed %q, want the refusal gate", out) } assertCleared(t, e, out) }) } // A host found live on its head (another process, root say, moved it) is // not waiting for anything. func TestProofWaitClearedOnceLiveOnTheHead(t *testing.T) { e := newProofEnv(t) e.requireDev(t) e.pass(t, e.store) if len(e.gate.Waits()) != 1 { t.Fatalf("waits = %+v, want one", e.gate.Waits()) } if err := StageArtifact(e.opt, "bab", "new", "console", []byte("bad build")); err != nil { t.Fatal(err) } if err := PointLive(e.opt, proofHost, "bab", "new", "console"); err != nil { t.Fatal(err) } e.pass(t, e.store) if w := e.gate.Waits(); len(w) != 0 || e.gate.WaitingDue(proofHost) { t.Fatalf("waits = %+v once live on the head, want none", w) } } // A host that stops waiting forgets its cached miss too: when the channel // comes back to the same build inside 30 s, the gate looks again and the // host is listed as waiting, rather than held silently off the list. func TestProofClearDropsTheCachedMiss(t *testing.T) { e := newProofEnv(t) e.requireDev(t) cs := &proofReadCountingStore{Store: e.store} e.pass(t, cs) e.gate.clear(proofHost) e.clock = e.clock.Add(time.Second) e.pass(t, cs) if n := cs.proofGets(); n != 2 { t.Fatalf("%d proof GETs, want 2: a cleared host's miss is not cached", n) } if w := e.gate.Waits(); len(w) != 1 { t.Fatalf("waits = %+v, want the host listed again", w) } } // Every gate method is nil-safe: no WithProofGate is today's behaviour. func TestProofGateNilIsOff(t *testing.T) { var g *ProofGate g.BeginPass() g.clear("x") if g.Waits() != nil || g.WaitingDue("x") { t.Fatal("a nil gate reports a wait") } if _, ok := g.admit(context.Background(), nil, "", registry.Route{Host: "x"}, &ChannelPointer{}, ""); !ok { t.Fatal("a nil gate refused admission") } if !g.bind("", registry.Route{Host: "x"}, &ChannelPointer{}, proofVerdict{required: true}, "", time.Time{}) { t.Fatal("a nil gate refused the staged bytes") } } // bindingRefusal is TestProofBindingRefusesStagedBytesThatDiffer's first // pass: a proven manifest, other bytes staged (kept, see stageStaleCopyKept), // a RefusalNotProven written. func bindingRefusal(t *testing.T) *proofEnv { t.Helper() e := newProofEnv(t) e.requireDev(t) down := stageStaleCopyKept(t, e.ctx, e.store, e.opt, "bab", "console", "new", []byte("not the shipped bytes")) e.putAt(t, e.newSHA, e.signed(t, e.prover, "console", e.newSHA, "dev")) e.pass(t, down) if r, _ := ReadRefusal(e.bake, proofHost); r == nil || r.Kind != RefusalNotProven || e.live() != "old" { t.Fatalf("refusal = %+v, live = %q; want a not-proven refusal and old", r, e.live()) } return e } // Final review H3: removing the require entry, or the whole policy file, is // the break glass, and it works after a binding refusal as well as after a // missing-proof wait. The stored D3 record stops blocking and is cleared. func TestProofBreakGlassOverridesABindingRefusal(t *testing.T) { for name, lift := range map[string]func(t *testing.T, e *proofEnv){ "require entry removed": func(t *testing.T, e *proofEnv) { e.writePolicy(t, `{"schema":1}`) }, "policy file removed": func(t *testing.T, e *proofEnv) { if err := os.Remove(e.policy); err != nil { t.Fatal(err) } }, } { t.Run(name, func(t *testing.T) { e := bindingRefusal(t) lift(t, e) out := e.pass(t, e.store) if e.live() != "new" { t.Fatalf("live = %q, printed %q; want new once the box no longer requires a proof", e.live(), out) } if r, _ := ReadRefusal(e.bake, proofHost); r != nil { t.Fatalf("refusal = %+v, want it cleared", r) } if strings.Contains(out, "refused:") { t.Fatalf("printed %q: the D3 refusal still gated the pass", out) } }) } } // A policy that will not load is closed: the D3 record keeps blocking, even // on a route with no unit, which admit alone would let through. func TestProofBrokenPolicyKeepsABindingRefusal(t *testing.T) { e := bindingRefusal(t) e.writePolicy(t, `{"schema":1}}`) out := e.pass(t, e.store) if e.live() != "old" || !strings.Contains(out, "refused: new's staged bytes do not match dev's proof") { t.Fatalf("live = %q, printed %q; want old and the not-proven gate line", e.live(), out) } if r, _ := ReadRefusal(e.bake, proofHost); r == nil || r.Kind != RefusalNotProven { t.Fatalf("refusal = %+v, want the not-proven record kept", r) } } // D1 and D2 records never depend on the policy: lifting the requirement // leaves them blocking. func TestProofBreakGlassLeavesOtherRefusals(t *testing.T) { for _, kind := range []string{RefusalNotUp, RefusalWrongProgram} { t.Run("kind "+kind, func(t *testing.T) { e := bindingRefusal(t) r, _ := ReadRefusal(e.bake, proofHost) r.Kind = kind if err := WriteRefusal(e.bake, *r); err != nil { t.Fatal(err) } if err := os.Remove(e.policy); err != nil { t.Fatal(err) } out := e.pass(t, e.store) if e.live() != "old" || !strings.Contains(out, "refused: new") { t.Fatalf("live = %q, printed %q; want old and the refusal gate", e.live(), out) } if got, _ := ReadRefusal(e.bake, proofHost); got == nil || got.Kind != kind { t.Fatalf("refusal = %+v, want the %q record kept", got, kind) } }) } } func TestProofGateRequires(t *testing.T) { var nilGate *ProofGate if req, perr := nilGate.Requires("console"); req || perr { t.Fatalf("nil gate Requires = %v, %v; want false, false", req, perr) } e := newProofEnv(t) e.gate.BeginPass() if req, perr := e.gate.Requires("console"); req || perr { t.Fatalf("no policy: Requires = %v, %v; want false, false", req, perr) } e.requireDev(t) e.gate.BeginPass() if req, perr := e.gate.Requires("console"); !req || perr { t.Fatalf("listed: Requires = %v, %v; want true, false", req, perr) } if req, perr := e.gate.Requires("other"); req || perr { t.Fatalf("unlisted: Requires = %v, %v; want false, false", req, perr) } e.writePolicy(t, `{"schema":1,"require":[`) e.gate.BeginPass() if req, perr := e.gate.Requires("other"); !req || !perr { t.Fatalf("broken policy: Requires = %v, %v; want true, true", req, perr) } }