| index 3587ce194..826263ece 100644 |
| --- a/internal/hostbox/reconcile_relocation_test.go |
| +++ b/internal/hostbox/reconcile_relocation_test.go |
| @@ -71,8 +71,9 @@ func relocate(t *testing.T, r Reconciler, host, phase string) hostclaim.Observed |
| // finishes both, so neither reacquires the host lock. |
| // |
| // Claim Plan 11's policy gate admits no "reconcile" action under a |
| -// relocating claim (R1: only op.exec's probe), so the activation path's |
| -// guarded write is refused there; the hold-only path, which takes no |
| +// relocating claim (R1: only op.exec's probe), so the activation path is |
| +// refused by the first check, before the guard is activated or the |
| +// inventory written, dry run or not; the hold-only path, which takes no |
| // policy, still keeps the fenced source's row held. |
| func TestReconcileRelocationSourceReadyThenHeldWithTheRealFenceObserver(t *testing.T) { |
| r, selected, holds, guard := relocationReconciler(t) |
| @@ -80,14 +81,24 @@ func TestReconcileRelocationSourceReadyThenHeldWithTheRealFenceObserver(t *testi |
| ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) |
| defer cancel() |
| writes := 0 |
| - if err := r.Install(ctx, selected, func() error { writes++; return nil }); !errors.Is(err, ErrChanged) { |
| - t.Fatalf("the source before its fence closed: %v (claim Plan 11 refuses a relocating reconcile)", err) |
| + before := snapshotDir(t, guard.DataDir) |
| + for _, dry := range []bool{true, false} { |
| + sel := selected |
| + sel.DryRun = dry |
| + if err := r.Install(ctx, sel, func() error { writes++; return nil }); !errors.Is(err, ErrChanged) { |
| + t.Fatalf("the source before its fence closed (dry run %v): %v (claim Plan 11 refuses a relocating reconcile)", dry, err) |
| + } |
| } |
| if writes != 0 || len(*holds) != 0 { |
| t.Fatalf("writes %d holds %v", writes, *holds) |
| } |
| + if after := snapshotDir(t, guard.DataDir); !maps.Equal(before, after) { |
| + t.Fatal("a refused relocating reconcile changed the guard directory") |
| + } |
| + if now, err := hostclaim.Read(ctx, r.Claims, selected.Host); err != nil || now.ETag != obs.ETag { |
| + t.Fatalf("a refused relocating reconcile wrote the claim: %v", err) |
| + } |
| // Root begins the source's stop: the durable admission fence closes. |
| - // (The inventory write above moved the claim: read it fresh.) |
| captured, err := hostguard.Capture(guard.DataDir, selected.Host) |
| if err != nil { |
| t.Fatal(err) |
| @@ -329,13 +340,13 @@ func targetReconciler(t *testing.T, r Reconciler) (Reconciler, hostguard.Guard, |
| } |
| |
| // Final review Important 2: with region HA on, a relocation target that |
| -// has no local guard record yet mints its row through Install. Its row |
| -// case (held, as op.exec and a participant) does not depend on the fence, |
| -// so the observer, which errors without a record, is never asked; the |
| -// guarded path activates the installation. Claim Plan 11's policy gate |
| -// then refuses the write itself: it admits no "reconcile" action under a |
| -// relocating claim (R1). |
| -func TestReconcileRelocationTargetWithNoLocalRecordMintsItsRow(t *testing.T) { |
| +// has no local guard record yet decides its row (held, as op.exec and a |
| +// participant) without the fence observer, which errors without a record |
| +// and is never asked. Claim Plan 11's policy gate admits no "reconcile" |
| +// action under a relocating claim (R1), so the activation path is refused |
| +// by the first check, dry run or not: nothing is activated or written, |
| +// and no row is minted (stage B settles how one is). |
| +func TestReconcileRelocationTargetWithNoLocalRecordIsRefusedBeforeActivation(t *testing.T) { |
| r, selected, holds, _ := relocationReconciler(t) |
| r, guard, calls := targetReconciler(t, r) |
| ctx := context.Background() |
| @@ -357,15 +368,25 @@ func TestReconcileRelocationTargetWithNoLocalRecordMintsItsRow(t *testing.T) { |
| } |
| *calls = 0 |
| writes := 0 |
| - if err := r.Install(ctx, selected, func() error { writes++; return nil }); !errors.Is(err, ErrChanged) { |
| - t.Fatalf("the target's first install: %v (claim Plan 11 refuses a relocating reconcile)", err) |
| + before := snapshotDir(t, guard.DataDir) |
| + for _, dry := range []bool{true, false} { |
| + sel := selected |
| + sel.DryRun = dry |
| + if err := r.Install(ctx, sel, func() error { writes++; return nil }); !errors.Is(err, ErrChanged) { |
| + t.Fatalf("the target's first install (dry run %v): %v (claim Plan 11 refuses a relocating reconcile)", dry, err) |
| + } |
| } |
| if writes != 0 || len(*holds) != 0 || *calls != 0 { |
| t.Fatalf("writes %d holds %v observer calls %d", writes, *holds, *calls) |
| } |
| - captured, err := hostguard.Capture(guard.DataDir, selected.Host) |
| - if err != nil || captured.Stamp != obs.Claim.Holder.Stamp { |
| - t.Fatalf("the target's installation was not activated: %+v %v", captured, err) |
| + if after := snapshotDir(t, guard.DataDir); !maps.Equal(before, after) { |
| + t.Fatal("a refused relocating reconcile changed the target's guard directory") |
| + } |
| + if _, err := hostguard.Capture(guard.DataDir, selected.Host); err == nil { |
| + t.Fatal("the target's installation was activated") |
| + } |
| + if now, err := hostclaim.Read(ctx, r.Claims, selected.Host); err != nil || now.ETag != obs.ETag { |
| + t.Fatalf("a refused relocating reconcile wrote the claim: %v", err) |
| } |
| } |
| |