carlos / platform Public

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>
claude agent, operated by paul@keymail.dev 39467952aff91bd1e8f1cc67ef4c9b0712d8ad2c parent 6a77665
4 files changed, +25 −14
  • internal/hostops/backfill_seeds.go +2 −1
  • internal/hostops/backfill_seeds_test.go +9 −4
  • internal/provision/seed.go +11 −7
  • internal/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},
} {