test(provision): pin manifest-read-failure seed withhold + removal skip
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 file changed,
+52
−0
internal/provision/reconcile_test.go+52 −0
diff --git a/internal/provision/reconcile_test.go b/internal/provision/reconcile_test.go| index c6891234d..9212d2ea6 100644 |
| --- a/internal/provision/reconcile_test.go |
| +++ b/internal/provision/reconcile_test.go |
| @@ -313,6 +313,58 @@ func TestReconcileFailSafeOnBrokenAccountManifest(t *testing.T) { |
| } |
| } |
| +// A manifest READ FAILURE (as opposed to "no manifest") must not wipe an |
| +// already-provisioned route's seed: seedFor's "" fallback is only safe to |
| +// hand a brand-new mint (nothing existing to protect), never the converge |
| +// branch. This pins the self-review fix in reconcile.go's seedFor/seedIsKnown |
| +// split — the first passing implementation called SetSeed(host, "") here, |
| +// which would erase a working seed on a transient blip. |
| +// |
| +// It also confirms readFailed from a manifest read failure disables removal |
| +// for the WHOLE pass, not only the account-listing step the brief's step 1 |
| +// names explicitly: a second, in-scope provisioned route with no matching |
| +// record this pass (the removal phase's only candidate) must survive too. |
| +func TestReconcileWithholdsSeedOnManifestReadFailure(t *testing.T) { |
| + s, sqid := setup(t) |
| + if err := CreateRecord(ctx, s, acmeRecord(sqid)); err != nil { |
| + t.Fatal(err) |
| + } |
| + reg := regWith(t) |
| + cfg := ReconcileConfig{Region: "ie", Pools: []string{"tito"}, DataDir: "/data"} |
| + if err := Reconcile(ctx, s, reg, cfg); err != nil { |
| + t.Fatal(err) |
| + } |
| + before := *mustGet(t, reg, "acme.go.tito.io") |
| + if before.Seed != `["seed"]` { |
| + t.Fatalf("fixture's route didn't seed as expected: %+v", before) |
| + } |
| + |
| + // A second provisioned, in-scope route with no matching record at all — |
| + // the removal phase's only candidate this pass. |
| + if err := reg.Upsert(registry.Route{Host: "ghost.go.tito.io", Kind: "instance", App: "tito", |
| + Status: "ready", Provisioned: true, Region: "ie", Pool: "tito", Account: sqid}); err != nil { |
| + t.Fatal(err) |
| + } |
| + |
| + // Break the app's instances.json: GetManifest now fails non-ENOENT. |
| + putReplace(t, s, ManifestKey(sqid, "tito"), []byte("not json")) |
| + |
| + if err := Reconcile(ctx, s, reg, cfg); err != nil { |
| + t.Fatal(err) |
| + } |
| + |
| + after := *mustGet(t, reg, "acme.go.tito.io") |
| + if after.Seed != before.Seed { |
| + t.Errorf("manifest read failure wiped an existing route's seed: before %q after %q", before.Seed, after.Seed) |
| + } |
| + after.Seed = before.Seed |
| + if after != before { |
| + t.Errorf("manifest read failure touched more than Seed: before %+v after %+v", before, after) |
| + } |
| + |
| + mustGet(t, reg, "ghost.go.tito.io") // removal skipped for the whole pass |
| +} |
| + |
| // 8. Malformed record. |
| func TestReconcileSkipsMalformedRecord(t *testing.T) { |
| s, sqid := setup(t) |