review fix S2: activation also refuses a ready --from seed and a refused import
Ruled after the first fix: neither can be finished once claims are on. A --from seed at ready cannot be placed (the mint and the seed fallback refuse under claims), and a refused import cannot be deleted on a claim console. Both now count as unfinished in the activation check, with the same seedUnfinished sentence and no new string. SeedSource.Unfinished is now every seed but a placed --from copy and a committed import, so a state this build does not know refuses too. Tests: a case for each in the hostops activation table and in TestSeedUnfinished.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4 files changed,
+25
−14
internal/hostops/backfill_seeds.go+2 −1internal/hostops/backfill_seeds_test.go+9 −4internal/provision/seed.go+11 −7internal/provision/seed_test.go+3 −2
diff --git a/internal/hostops/backfill_seeds.go b/internal/hostops/backfill_seeds.go| index 3a26b8492..543eb7f44 100644 |
| --- a/internal/hostops/backfill_seeds.go |
| +++ b/internal/hostops/backfill_seeds.go |
| @@ -23,7 +23,8 @@ import ( |
| // activation already authenticates: |
| // |
| // - an unfinished seed on a record, from the census's own record read |
| -// (provision.SeedSource.Unfinished); |
| +// (provision.SeedSource.Unfinished): every seed but a placed --from |
| +// copy and a committed import; |
| // - an import journal row on a box; |
| // - a gated route on a box. |
| // |
diff --git a/internal/hostops/backfill_seeds_test.go b/internal/hostops/backfill_seeds_test.go| index dff0d637d..f3f6e4596 100644 |
| --- a/internal/hostops/backfill_seeds_test.go |
| +++ b/internal/hostops/backfill_seeds_test.go |
| @@ -158,8 +158,11 @@ func TestActivationPublishesWithNoUnfinishedSeed(t *testing.T) { |
| } |
| // Activation refuses over each unfinished seed a record can carry, under |
| -// the collecting freeze, and publishes nothing. The states outside the |
| -// ruling's list do not refuse. |
| +// the collecting freeze, and publishes nothing. That includes the two |
| +// states ruled in after the first fix: a --from seed at ready, which |
| +// nothing can place once claims are on, and an import at refused, which a |
| +// claim console cannot delete. Only a placed copy and a committed import |
| +// publish. |
| func TestActivationRefusesAnUnfinishedSeedRecord(t *testing.T) { |
| for _, c := range []struct { |
| kind, state string |
| @@ -175,8 +178,10 @@ func TestActivationRefusesAnUnfinishedSeedRecord(t *testing.T) { |
| {seeds.KindReplica, seeds.StateAborted, true}, |
| {seeds.KindImport, seeds.StateRolledBack, true}, |
| {seeds.KindReplica, seeds.StateRolledBack, true}, |
| - {seeds.KindReplica, seeds.StateReady, false}, |
| - {seeds.KindImport, seeds.StateRefused, false}, |
| + {seeds.KindReplica, seeds.StateReady, true}, |
| + {seeds.KindImport, seeds.StateRefused, true}, |
| + {seeds.KindReplica, seeds.StatePlaced, false}, |
| + {seeds.KindImport, seeds.StateCommitted, false}, |
| } { |
| t.Run(c.kind+" "+c.state, func(t *testing.T) { |
| a := newActivation(t) |
diff --git a/internal/provision/seed.go b/internal/provision/seed.go| index 7638466a5..cb78fb041 100644 |
| --- a/internal/provision/seed.go |
| +++ b/internal/provision/seed.go |
| @@ -53,22 +53,26 @@ func (s *SeedSource) Abandoned() bool { |
| // console or a claim-armed box. |
| // |
| // - reserving, awaiting-upload, uploaded: a door is still writing it; |
| -// - an import at ready, swapped or placed: not committed; |
| +// - ready: a --from seed waits for its first wake to place it, and an |
| +// import for its swap; |
| +// - an import at swapped or placed: not committed; |
| +// - refused: an import the box would not swap, which only the delete |
| +// door's import rule can remove; |
| // - aborted, rolled-back: its rollback or its finish is still owed. |
| // |
| -// Not in the list: a --from seed at ready (it waits only for its first |
| -// wake) or placed, an import at committed, and refused. |
| +// Finished, and the only two: a --from seed at placed and an import at |
| +// committed. A state this build does not know is unfinished too. |
| func (s *SeedSource) Unfinished() bool { |
| if s == nil { |
| return false |
| } |
| switch s.State { |
| - case seeds.StateReserving, seeds.StateAwaitingUpload, seeds.StateUploaded, seeds.StateAborted, seeds.StateRolledBack: |
| - return true |
| - case seeds.StateReady, seeds.StateSwapped, seeds.StatePlaced: |
| + case seeds.StatePlaced: |
| return s.Import() |
| + case seeds.StateCommitted: |
| + return false |
| } |
| - return false |
| + return true |
| } |
| // BlocksRun is run admission's predicate (run spec §5.1 step 9, codex |
diff --git a/internal/provision/seed_test.go b/internal/provision/seed_test.go| index bf1242225..5bd6ae605 100644 |
| --- a/internal/provision/seed_test.go |
| +++ b/internal/provision/seed_test.go |
| @@ -599,7 +599,7 @@ func TestSeedUnfinished(t *testing.T) { |
| want bool |
| }{ |
| {seeds.KindReplica, seeds.StateReserving, true}, |
| - {seeds.KindReplica, seeds.StateReady, false}, |
| + {seeds.KindReplica, seeds.StateReady, true}, |
| {seeds.KindReplica, seeds.StatePlaced, false}, |
| {seeds.KindReplica, seeds.StateAborted, true}, |
| {seeds.KindReplica, seeds.StateRolledBack, true}, |
| @@ -609,7 +609,8 @@ func TestSeedUnfinished(t *testing.T) { |
| {seeds.KindImport, seeds.StateSwapped, true}, |
| {seeds.KindImport, seeds.StatePlaced, true}, |
| {seeds.KindImport, seeds.StateCommitted, false}, |
| - {seeds.KindImport, seeds.StateRefused, false}, |
| + {seeds.KindImport, seeds.StateRefused, true}, |
| + {seeds.KindImport, "a state from a later build", true}, |
| {seeds.KindImport, seeds.StateAborted, true}, |
| {seeds.KindImport, seeds.StateRolledBack, true}, |
| } { |