| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "crypto/ed25519" |
| 6 | "crypto/sha256" |
| 7 | "encoding/hex" |
| 8 | "fmt" |
| 9 | "os" |
| 10 | "path/filepath" |
| 11 | "strings" |
| 12 | "testing" |
| 13 | "time" |
| 14 | |
| 15 | "github.com/carlosframework/platform/internal/registry" |
| 16 | "github.com/carlosframework/platform/internal/release" |
| 17 | ) |
| 18 | |
| 19 | type proofRootEnv struct { |
| 20 | root proofRoot |
| 21 | store release.Store |
| 22 | bake string |
| 23 | keyFile string |
| 24 | pub ed25519.PublicKey |
| 25 | pubB64 string |
| 26 | sha string |
| 27 | now time.Time |
| 28 | logs []string |
| 29 | } |
| 30 | |
| 31 | func provePolicy(keyFile string) string { |
| 32 | return fmt.Sprintf(`{"schema":1,"prove":{"apps":["console"],"prover":"dev","after":"15m","key_file":%q}}`, keyFile) |
| 33 | } |
| 34 | |
| 35 | // newProofRootEnv: one named-unit console route, bound, running its live |
| 36 | // build for 20 minutes, ready. policy builds the policy file from the key |
| 37 | // file's path; nil writes no policy file. |
| 38 | func newProofRootEnv(t *testing.T, policy func(keyFile string) string) *proofRootEnv { |
| 39 | t.Helper() |
| 40 | opt, bake, etc := t.TempDir(), t.TempDir(), t.TempDir() |
| 41 | liveTo(t, opt, observedHost, "v1") |
| 42 | reg := restartTestRegistry(t) |
| 43 | rt := registry.Route{Host: observedHost, Kind: "instance", App: "console", |
| 44 | Channel: "canary/console", Unit: "console.service", Status: "ready"} |
| 45 | if err := reg.Upsert(rt); err != nil { |
| 46 | t.Fatal(err) |
| 47 | } |
| 48 | seed, pubB64, err := release.GenerateKey() |
| 49 | if err != nil { |
| 50 | t.Fatal(err) |
| 51 | } |
| 52 | pub, err := release.ParsePublicKey(pubB64, "test") |
| 53 | if err != nil { |
| 54 | t.Fatal(err) |
| 55 | } |
| 56 | keyFile := filepath.Join(etc, "proving.key") |
| 57 | if err := os.WriteFile(keyFile, []byte(seed+"\n"), 0o600); err != nil { |
| 58 | t.Fatal(err) |
| 59 | } |
| 60 | policyPath := filepath.Join(etc, "proof-policy.json") |
| 61 | if policy != nil { |
| 62 | if err := os.WriteFile(policyPath, []byte(policy(keyFile)), 0o644); err != nil { |
| 63 | t.Fatal(err) |
| 64 | } |
| 65 | } |
| 66 | staged := filepath.Join(opt, "versions", "v1", "console") |
| 67 | sha, err := release.FileSHA256(staged) |
| 68 | if err != nil { |
| 69 | t.Fatal(err) |
| 70 | } |
| 71 | e := &proofRootEnv{store: release.NewDirStore(t.TempDir()), bake: bake, keyFile: keyFile, pub: pub, pubB64: pubB64, |
| 72 | sha: sha, now: time.Date(2026, 10, 6, 10, 0, 0, 0, time.UTC)} |
| 73 | units := &fakeUnits{shows: []unitProps{{ActiveState: "active", InvocationID: "i1", MainPID: 7, |
| 74 | ExecPath: release.LiveLink(opt, observedHost), ActiveEnterTimestamp: e.now.Add(-20 * time.Minute)}}} |
| 75 | e.root = proofRoot{ |
| 76 | mintCensus: func() (bool, error) { return true, nil }, |
| 77 | obs: proofObserver{sys: units, procExe: func(int) string { return staged }, optDir: opt, |
| 78 | now: func() time.Time { return e.now }, resolve: proofFixtureResolver(rt), ready: func(registry.Route) func() error { return func() error { return nil } }}, |
| 79 | reg: reg, store: e.store, bakeDir: bake, policyPath: policyPath, readKey: readProvingKey, |
| 80 | logf: func(f string, a ...any) { e.logs = append(e.logs, fmt.Sprintf(f, a...)) }, |
| 81 | } |
| 82 | return e |
| 83 | } |
| 84 | |
| 85 | func (e *proofRootEnv) proof(t *testing.T) *release.Proof { |
| 86 | t.Helper() |
| 87 | p, err := release.GetProof(context.Background(), e.store, "bab", "console", e.sha, "dev") |
| 88 | if err != nil { |
| 89 | return nil |
| 90 | } |
| 91 | return p |
| 92 | } |
| 93 | |
| 94 | // signed is a proof for the env's live build, signed with its proving key. |
| 95 | func (e *proofRootEnv) signed(t *testing.T, provenAt time.Time) *release.Proof { |
| 96 | t.Helper() |
| 97 | seed, err := os.ReadFile(e.keyFile) |
| 98 | if err != nil { |
| 99 | t.Fatal(err) |
| 100 | } |
| 101 | key, err := release.ParseSigningKey(string(seed), "test") |
| 102 | if err != nil { |
| 103 | t.Fatal(err) |
| 104 | } |
| 105 | p, err := release.SignProof(key, release.Proof{App: "console", Platform: release.AdoptPlatform(), SHA256: e.sha, |
| 106 | Version: "v1", Prover: "dev", ActiveSince: e.now.Add(-time.Hour), ProvenAt: provenAt}) |
| 107 | if err != nil { |
| 108 | t.Fatal(err) |
| 109 | } |
| 110 | return p |
| 111 | } |
| 112 | |
| 113 | func TestProofRootMintsWhenEveryConditionHolds(t *testing.T) { |
| 114 | t.Parallel() |
| 115 | e := newProofRootEnv(t, provePolicy) |
| 116 | e.root.run(context.Background()) |
| 117 | p := e.proof(t) |
| 118 | if p == nil { |
| 119 | t.Fatalf("no proof minted; logs: %v", e.logs) |
| 120 | } |
| 121 | if err := p.Check(e.pub, "console", release.AdoptPlatform(), e.sha, "dev"); err != nil { |
| 122 | t.Fatalf("minted proof: %v", err) |
| 123 | } |
| 124 | if p.Version != "v1" || !p.ActiveSince.Equal(e.now.Add(-20*time.Minute)) || !p.ProvenAt.Equal(e.now) { |
| 125 | t.Fatalf("proof = %+v", p) |
| 126 | } |
| 127 | } |
| 128 | |
| 129 | func TestProofRootDoesNotMintARefusedBuild(t *testing.T) { |
| 130 | e := newProofRootEnv(t, provePolicy) |
| 131 | if err := release.WriteRefusal(e.bake, release.Refusal{Host: observedHost, App: "console", Version: "v1", SHA256: e.sha}); err != nil { |
| 132 | t.Fatal(err) |
| 133 | } |
| 134 | e.root.run(context.Background()) |
| 135 | if e.proof(t) != nil { |
| 136 | t.Fatal("minted a proof for a build a refusal record names") |
| 137 | } |
| 138 | } |
| 139 | |
| 140 | // D3-held fix round 1: a move's held refusal (MoveOp set) is scoped to |
| 141 | // that move's held stage and never stops this box minting a proof for a |
| 142 | // build it runs. Since final review finding 3 it lives in the held |
| 143 | // namespace, which minting never reads. |
| 144 | func TestProofRootMintsPastAMovesHeldRefusal(t *testing.T) { |
| 145 | e := newProofRootEnv(t, provePolicy) |
| 146 | if err := release.WriteRefusal(release.HeldRefusalBase(e.bake), release.Refusal{Host: "held.example.com", App: "console", Version: "v1", SHA256: e.sha, |
| 147 | Kind: release.RefusalNotProven, MoveOp: "mv_abcdefghijklmnopqrstuvwxyz"}); err != nil { |
| 148 | t.Fatal(err) |
| 149 | } |
| 150 | e.root.run(context.Background()) |
| 151 | if e.proof(t) == nil { |
| 152 | t.Fatalf("a move's held refusal stopped the mint; logs: %v", e.logs) |
| 153 | } |
| 154 | } |
| 155 | |
| 156 | func TestProofRootNeverOverwritesAProof(t *testing.T) { |
| 157 | t.Parallel() |
| 158 | e := newProofRootEnv(t, provePolicy) |
| 159 | first := e.signed(t, e.now.Add(-time.Minute)) |
| 160 | if err := release.PutProof(context.Background(), e.store, "bab", first); err != nil { |
| 161 | t.Fatal(err) |
| 162 | } |
| 163 | e.root.run(context.Background()) |
| 164 | if p := e.proof(t); p == nil || !p.ProvenAt.Equal(first.ProvenAt) { |
| 165 | t.Fatalf("proof = %+v, want the first one untouched", p) |
| 166 | } |
| 167 | } |
| 168 | |
| 169 | // racedStore hides one key from the first Get, as if another pass minted it |
| 170 | // between this pass's existence check and its write. |
| 171 | type racedStore struct { |
| 172 | release.Store |
| 173 | hidden string |
| 174 | gets int |
| 175 | } |
| 176 | |
| 177 | func (s *racedStore) Get(ctx context.Context, key string) ([]byte, string, error) { |
| 178 | if key == s.hidden { |
| 179 | s.gets++ |
| 180 | if s.gets == 1 { |
| 181 | return nil, "", fmt.Errorf("raced: %s: %w", key, os.ErrNotExist) |
| 182 | } |
| 183 | } |
| 184 | return s.Store.Get(ctx, key) |
| 185 | } |
| 186 | |
| 187 | // PutProof is create-only: a second mint of one build carries a different |
| 188 | // proven_at and conflicts. That conflict means "already minted", not an error. |
| 189 | func TestProofRootTreatsAMintRaceAsAlreadyMinted(t *testing.T) { |
| 190 | e := newProofRootEnv(t, provePolicy) |
| 191 | first := e.signed(t, e.now.Add(-time.Minute)) |
| 192 | if err := release.PutProof(context.Background(), e.store, "bab", first); err != nil { |
| 193 | t.Fatal(err) |
| 194 | } |
| 195 | e.root.store = &racedStore{Store: e.store, hidden: release.ProofKey("bab", "console", e.sha, "dev")} |
| 196 | e.root.run(context.Background()) |
| 197 | logs := strings.Join(e.logs, "\n") |
| 198 | if strings.Contains(logs, "writing the proof") { |
| 199 | t.Fatalf("a mint race was logged as a write failure: %v", e.logs) |
| 200 | } |
| 201 | if strings.Contains(logs, "proof: minted") { |
| 202 | t.Fatalf("a mint race was logged as this pass's mint: %v", e.logs) |
| 203 | } |
| 204 | if p := e.proof(t); p == nil || !p.ProvenAt.Equal(first.ProvenAt) { |
| 205 | t.Fatalf("proof = %+v, want the first one untouched", p) |
| 206 | } |
| 207 | } |
| 208 | |
| 209 | func TestProofRootRefusesAKeyOthersCanRead(t *testing.T) { |
| 210 | e := newProofRootEnv(t, provePolicy) |
| 211 | if err := os.Chmod(e.keyFile, 0o644); err != nil { |
| 212 | t.Fatal(err) |
| 213 | } |
| 214 | e.root.run(context.Background()) |
| 215 | if e.proof(t) != nil { |
| 216 | t.Fatal("minted with a world-readable key") |
| 217 | } |
| 218 | if !strings.Contains(strings.Join(e.logs, "\n"), "readable by root only") { |
| 219 | t.Fatalf("logs = %v, want the key-mode refusal", e.logs) |
| 220 | } |
| 221 | } |
| 222 | |
| 223 | // Task 2's carry: a policy file that does not parse is closed. A proving box |
| 224 | // with a typo mints nothing, and says so on every pass. |
| 225 | func TestProofRootBrokenPolicyMintsNothingAndSaysSoEveryPass(t *testing.T) { |
| 226 | e := newProofRootEnv(t, func(keyFile string) string { |
| 227 | return strings.Replace(provePolicy(keyFile), `"schema":1`, `"schema":1,"requier":[]`, 1) |
| 228 | }) |
| 229 | for pass := 0; pass < 2; pass++ { |
| 230 | e.logs = nil |
| 231 | e.root.hourly = pass == 1 |
| 232 | e.root.run(context.Background()) |
| 233 | if e.proof(t) != nil { |
| 234 | t.Fatalf("pass %d minted under a policy that does not parse", pass) |
| 235 | } |
| 236 | if !strings.Contains(strings.Join(e.logs, "\n"), "nothing is minted until it parses") { |
| 237 | t.Fatalf("pass %d logs = %v, want the policy error", pass, e.logs) |
| 238 | } |
| 239 | } |
| 240 | } |
| 241 | |
| 242 | type noReadStore struct { |
| 243 | release.Store |
| 244 | t *testing.T |
| 245 | } |
| 246 | |
| 247 | func (s noReadStore) Get(ctx context.Context, key string) ([]byte, string, error) { |
| 248 | s.t.Errorf("bucket GET %s with no policy file", key) |
| 249 | return s.Store.Get(ctx, key) |
| 250 | } |
| 251 | |
| 252 | // Review focus 3's source: with no policy file at all, the build already |
| 253 | // live enters the healthy history, and no proof is read or minted. |
| 254 | func TestProofRootRecordsTheLiveBuildAsHealthy(t *testing.T) { |
| 255 | e := newProofRootEnv(t, nil) |
| 256 | e.root.store = noReadStore{Store: e.store, t: t} |
| 257 | e.root.run(context.Background()) |
| 258 | h := release.ReadHealthy(e.bake, "bab", "console") |
| 259 | if len(h) != 1 || h[0].SHA256 != e.sha || h[0].How != "live" || h[0].Version != "v1" { |
| 260 | t.Fatalf("history = %+v, want the live build", h) |
| 261 | } |
| 262 | if e.proof(t) != nil { |
| 263 | t.Fatal("a proof was minted with no policy file") |
| 264 | } |
| 265 | } |
| 266 | |
| 267 | // A box that never had the requirement (Tito, a self-hoster) has nothing |
| 268 | // switched off and must stay quiet; one that had it and lost it logs hourly. |
| 269 | func TestProofRootHourlyLineOnlyWhereTheRequirementWasOnce(t *testing.T) { |
| 270 | e := newProofRootEnv(t, func(string) string { return `{"schema":1}` }) |
| 271 | e.root.hourly = true |
| 272 | e.root.run(context.Background()) |
| 273 | if strings.Contains(strings.Join(e.logs, "\n"), "requirement is off") { |
| 274 | t.Fatalf("a box that never required a proof logged that the requirement is off: %v", e.logs) |
| 275 | } |
| 276 | } |
| 277 | |
| 278 | func TestProofRootHourlyLineWhenRequirementIsOff(t *testing.T) { |
| 279 | e := newProofRootEnv(t, func(string) string { return `{"schema":1}` }) |
| 280 | marker := filepath.Join(e.root.bakeDir, "proof-required", "console") |
| 281 | if err := os.MkdirAll(filepath.Dir(marker), 0o755); err != nil { |
| 282 | t.Fatal(err) |
| 283 | } |
| 284 | if err := os.WriteFile(marker, nil, 0o644); err != nil { |
| 285 | t.Fatal(err) |
| 286 | } |
| 287 | e.root.hourly = true |
| 288 | e.root.run(context.Background()) |
| 289 | if !strings.Contains(strings.Join(e.logs, "\n"), "requirement is off for console") { |
| 290 | t.Fatalf("logs = %v, want the hourly requirement-off line", e.logs) |
| 291 | } |
| 292 | e.logs = nil |
| 293 | e.root.hourly = false |
| 294 | e.root.run(context.Background()) |
| 295 | if strings.Contains(strings.Join(e.logs, "\n"), "requirement is off") { |
| 296 | t.Fatalf("a stage-only pass printed the hourly line: %v", e.logs) |
| 297 | } |
| 298 | } |
| 299 | |
| 300 | // The marker is written by any root pass that reads a require entry, not |
| 301 | // only the hourly one, so a requirement switched on and off between two |
| 302 | // hourly passes still leaves its trail. Deleting the whole file (the other |
| 303 | // break glass) is "off" too. |
| 304 | func TestProofRootRequireEntryLeavesTheMarker(t *testing.T) { |
| 305 | t.Parallel() |
| 306 | e := newProofRootEnv(t, nil) |
| 307 | if err := os.WriteFile(e.root.policyPath, []byte(fmt.Sprintf( |
| 308 | `{"schema":1,"require":[{"app":"console","prover":"dev","key":%q}]}`, e.pubB64)), 0o644); err != nil { |
| 309 | t.Fatal(err) |
| 310 | } |
| 311 | marker := filepath.Join(e.bake, "proof-required", "console") |
| 312 | e.root.run(context.Background()) |
| 313 | if _, err := os.Stat(marker); err != nil { |
| 314 | t.Fatalf("a stage-only pass that read a require entry left no marker: %v", err) |
| 315 | } |
| 316 | if strings.Contains(strings.Join(e.logs, "\n"), "requirement is off") { |
| 317 | t.Fatalf("logged the requirement off while it is on: %v", e.logs) |
| 318 | } |
| 319 | if err := os.Remove(e.root.policyPath); err != nil { |
| 320 | t.Fatal(err) |
| 321 | } |
| 322 | e.logs = nil |
| 323 | e.root.hourly = true |
| 324 | e.root.run(context.Background()) |
| 325 | if !strings.Contains(strings.Join(e.logs, "\n"), "requirement is off for console") { |
| 326 | t.Fatalf("logs = %v, want the hourly requirement-off line once the file is gone", e.logs) |
| 327 | } |
| 328 | } |
| 329 | |
| 330 | // Final review H4: the first adoption after a requirement is switched on. |
| 331 | // A has run well for two hours with an empty healthy history; B is promoted |
| 332 | // with a valid dev proof. One RunAdopt-shaped run (proofRoot.around a real |
| 333 | // release.Pass) adopts B, and A, the build B replaced, is in the history |
| 334 | // too, so a later rollback to A does not wait for a dev proof. B enters it |
| 335 | // through the settle hook. |
| 336 | func TestProofRootRecordsTheReplacedBuildBeforeAdoption(t *testing.T) { |
| 337 | ctx := context.Background() |
| 338 | opt, bake, etc := t.TempDir(), t.TempDir(), t.TempDir() |
| 339 | s := release.NewDirStore(t.TempDir()) |
| 340 | plat := release.AdoptPlatform() |
| 341 | pub, priv, err := ed25519.GenerateKey(nil) |
| 342 | if err != nil { |
| 343 | t.Fatal(err) |
| 344 | } |
| 345 | for ver, body := range map[string]string{"a": "build a", "b": "build b"} { |
| 346 | m := release.Manifest{App: "console", Version: ver, Kind: "binary", Artifacts: map[string]release.Artifact{plat: {}}} |
| 347 | if err := release.Ship(ctx, s, "bab", m, map[string][]byte{plat: []byte(body)}); err != nil { |
| 348 | t.Fatalf("Ship %s: %v", ver, err) |
| 349 | } |
| 350 | } |
| 351 | if err := release.StageArtifact(opt, "bab", "a", "console", []byte("build a")); err != nil { |
| 352 | t.Fatal(err) |
| 353 | } |
| 354 | if err := release.PointLive(opt, observedHost, "bab", "a", "console"); err != nil { |
| 355 | t.Fatal(err) |
| 356 | } |
| 357 | if err := release.Promote(ctx, s, priv, "bab", "console", "canary/console", "b"); err != nil { |
| 358 | t.Fatal(err) |
| 359 | } |
| 360 | reg := restartTestRegistry(t) |
| 361 | rt := registry.Route{Host: observedHost, Kind: "instance", App: "console", |
| 362 | Channel: "canary/console", Unit: "console.service", Status: "ready"} |
| 363 | if err := reg.Upsert(rt); err != nil { |
| 364 | t.Fatal(err) |
| 365 | } |
| 366 | shaA := sha256Hex("build a") |
| 367 | shaB := sha256Hex("build b") |
| 368 | |
| 369 | seed, proverPub, err := release.GenerateKey() |
| 370 | if err != nil { |
| 371 | t.Fatal(err) |
| 372 | } |
| 373 | proverKey, err := release.ParseSigningKey(seed, "test") |
| 374 | if err != nil { |
| 375 | t.Fatal(err) |
| 376 | } |
| 377 | now := time.Date(2026, 10, 6, 10, 0, 0, 0, time.UTC) |
| 378 | p, err := release.SignProof(proverKey, release.Proof{App: "console", Platform: plat, SHA256: shaB, |
| 379 | Version: "b", Prover: "dev", ActiveSince: now.Add(-time.Hour), ProvenAt: now}) |
| 380 | if err != nil { |
| 381 | t.Fatal(err) |
| 382 | } |
| 383 | if err := release.PutProof(ctx, s, "bab", p); err != nil { |
| 384 | t.Fatal(err) |
| 385 | } |
| 386 | policyPath := filepath.Join(etc, "proof-policy.json") |
| 387 | if err := os.WriteFile(policyPath, []byte(fmt.Sprintf( |
| 388 | `{"schema":1,"require":[{"app":"console","prover":"dev","key":%q}]}`, proverPub)), 0o644); err != nil { |
| 389 | t.Fatal(err) |
| 390 | } |
| 391 | |
| 392 | link := release.LiveLink(opt, observedHost) |
| 393 | units := &fakeUnits{shows: []unitProps{{ActiveState: "active", InvocationID: "a1", MainPID: 7, |
| 394 | ExecPath: link, ActiveEnterTimestamp: now.Add(-2 * time.Hour)}}} |
| 395 | procExe := func(int) string { return link } // the process runs whatever the link names |
| 396 | clock := func() time.Time { return now } |
| 397 | root := proofRoot{ |
| 398 | obs: proofObserver{sys: units, procExe: procExe, optDir: opt, now: clock, |
| 399 | resolve: proofFixtureResolver(rt), ready: func(registry.Route) func() error { return func() error { return nil } }}, |
| 400 | reg: reg, store: s, bakeDir: bake, policyPath: policyPath, readKey: readProvingKey, |
| 401 | logf: func(f string, a ...any) { t.Logf(f, a...) }, |
| 402 | } |
| 403 | // The restart starts B's process and settles, as checkedUnitRestart |
| 404 | // would, and its settled hook records what it runs. |
| 405 | restart := func(host string) error { |
| 406 | units.shows, units.n = []unitProps{{ActiveState: "active", InvocationID: "b1", MainPID: 8, |
| 407 | ExecPath: link, ActiveEnterTimestamp: now}}, 0 |
| 408 | if err := root.obs.recordSettled(bake)(rt); err != nil { |
| 409 | return err |
| 410 | } |
| 411 | return nil |
| 412 | } |
| 413 | if err := root.around(ctx, func() error { |
| 414 | return release.Pass(ctx, reg, s, pub, opt, restart, release.BakeDir(bake), |
| 415 | release.WithProofGate(release.NewProofGate(policyPath, bake))) |
| 416 | }); err != nil { |
| 417 | t.Fatal(err) |
| 418 | } |
| 419 | if v, _ := release.LiveVersion(opt, observedHost); v != "b" { |
| 420 | t.Fatalf("live = %q, want b adopted on its proof", v) |
| 421 | } |
| 422 | got := map[string]string{} |
| 423 | for _, h := range release.ReadHealthy(bake, "bab", "console") { |
| 424 | got[h.SHA256] = h.How |
| 425 | } |
| 426 | if got[shaA] != "live" { |
| 427 | t.Fatalf("history = %+v, want a (sha256 %s) recorded live before the pass replaced it", got, shaA) |
| 428 | } |
| 429 | if got[shaB] != "settle" { |
| 430 | t.Fatalf("history = %+v, want b (sha256 %s) recorded by the settle hook", got, shaB) |
| 431 | } |
| 432 | } |
| 433 | |
| 434 | // A route the pass did not touch is observed once: the second half reuses |
| 435 | // the first half's look when the link and the process are unchanged, and |
| 436 | // looks again when the process was restarted under the same link. |
| 437 | func TestProofRootReusesAnUnchangedObservation(t *testing.T) { |
| 438 | e := newProofRootEnv(t, provePolicy) |
| 439 | units := e.root.obs.sys.(*fakeUnits) |
| 440 | obs := e.root.recordLive(context.Background()) |
| 441 | if o := obs.byHost[observedHost]; o.err != nil { |
| 442 | t.Fatalf("first half: %v", o.err) |
| 443 | } |
| 444 | rt := registry.Route{Host: observedHost, App: "console", Unit: "console.service"} |
| 445 | if !e.root.stillLive(rt, obs.byHost[observedHost]) { |
| 446 | t.Fatal("an untouched route is not still live") |
| 447 | } |
| 448 | units.shows = []unitProps{{ActiveState: "active", InvocationID: "i2", MainPID: 9, |
| 449 | ExecPath: units.shows[0].ExecPath, ActiveEnterTimestamp: e.now}} |
| 450 | units.n = 0 |
| 451 | if e.root.stillLive(rt, obs.byHost[observedHost]) { |
| 452 | t.Fatal("a restarted process under the same link counts as still live") |
| 453 | } |
| 454 | e.root.mintAndLog(context.Background(), obs) |
| 455 | if e.proof(t) != nil { |
| 456 | t.Fatal("minted from the first half's look at a process that has since restarted") |
| 457 | } |
| 458 | } |
| 459 | |
| 460 | func sha256Hex(s string) string { |
| 461 | h := sha256.Sum256([]byte(s)) |
| 462 | return hex.EncodeToString(h[:]) |
| 463 | } |
| 464 | |
| 465 | func TestPlan14NoCensusProofKeepsMain(t *testing.T) { |
| 466 | for _, flag := range []string{"", "1"} { |
| 467 | t.Run("flag="+flag, func(t *testing.T) { |
| 468 | t.Setenv("CARLOS_HOST_CLAIM", flag) |
| 469 | e := newProofRootEnv(t, provePolicy) |
| 470 | e.root.mintCensus = nil |
| 471 | before, _ := os.ReadDir(e.bake) |
| 472 | history, err := configureRootProof(&e.root, rootRestartChecks{legacy: func() (bool, error) { return true, nil }}, func() (*release.RootHistoryPass, error) { |
| 473 | t.Fatal("no-census history creation") |
| 474 | return nil, os.ErrPermission |
| 475 | }) |
| 476 | if err != nil || history != nil || e.root.recordProgram != nil || e.root.refusals != nil || e.root.mintCensus != nil { |
| 477 | t.Fatal("new root policy on legacy box", err) |
| 478 | } |
| 479 | after, _ := os.ReadDir(e.bake) |
| 480 | if len(before) != len(after) { |
| 481 | t.Fatal("configuration wrote state") |
| 482 | } |
| 483 | e.root.run(t.Context()) |
| 484 | if e.proof(t) == nil { |
| 485 | t.Fatal("legacy proving box stopped minting", e.logs) |
| 486 | } |
| 487 | }) |
| 488 | } |
| 489 | } |
| 490 | |