provision: the queue's receipt is the request's nonce alone
OfflineQueue kept a last_request the caller left in ss when the receipt was empty. It now sets self_serve.last_request to the receipt's nonce whatever ss carried, so an empty receipt leaves none and an operator-shaped call can never carry a stale confirmation. Pinned in TestOfflineQueueThenStaging. Census: the internal/provision envelope re-pinned (PROPOSED-self-serve-moves-2).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
3 files changed,
+20
−6
internal/provision/offline_move.go+5 −5internal/provision/offline_queue_test.go+14 −0internal/provision/testdata/plan14-root-reads-self-serve-moves-2.tsv+1 −1
diff --git a/internal/provision/offline_move.go b/internal/provision/offline_move.go| index 5cc11fa99..9530e1e72 100644 |
| --- a/internal/provision/offline_move.go |
| +++ b/internal/provision/offline_move.go |
| @@ -330,8 +330,8 @@ func offlineBegin(id, from, to, by string, now time.Time, state, returnOf string |
| // st's bucket and region, and carries p and ss. requested_at, queued_at, |
| // staged_at and precheck_at are all `at`, the caller's proposed stamp, so |
| // StartedOffline is false until the start; move.seq is the record's |
| -// move_seq, which the queue does not advance. A non-empty receipt is ss's |
| -// last_request. |
| +// move_seq, which the queue does not advance. ss's last_request is the |
| +// receipt's nonce, "" under an empty receipt, whatever ss carried. |
| func OfflineQueue(id, from, to, by string, st StageTarget, p Participants, ss SelfServeMove, at time.Time, req Receipt) func(*Record) error { |
| begin := offlineBegin(id, from, to, by, at, OfflineQueued, "") |
| return func(rec *Record) error { |
| @@ -350,9 +350,9 @@ func OfflineQueue(id, from, to, by string, st StageTarget, p Participants, ss Se |
| t := *ss.OfflineAt |
| self.OfflineAt = &t |
| } |
| - if req.Nonce != "" { |
| - self.LastRequest = req.Nonce |
| - } |
| + // The receipt is the request's nonce alone: whatever ss carried is |
| + // replaced, and an empty receipt leaves none. |
| + self.LastRequest = req.Nonce |
| m := rec.Move |
| m.Stager, m.StageBucket, m.StageRegion = p.Source[0].Label, st.Bucket, st.Region |
| m.Participants, m.SelfServe = &pin, &self |
diff --git a/internal/provision/offline_queue_test.go b/internal/provision/offline_queue_test.go| index 936751573..c9df9e79d 100644 |
| --- a/internal/provision/offline_queue_test.go |
| +++ b/internal/provision/offline_queue_test.go |
| @@ -121,6 +121,20 @@ func TestOfflineQueueThenStaging(t *testing.T) { |
| if got.MoveSeq != 5 || m.Seq != 5 { |
| t.Fatalf("staged start: move_seq %d move.seq %d, want 5", got.MoveSeq, m.Seq) |
| } |
| + |
| + // The queue's receipt is the request's nonce and nothing else: a |
| + // last_request the caller left in ss is replaced by the nonce, and |
| + // cleared by an empty receipt. |
| + for nonce, want := range map[string]string{"": "", "n-q": "n-q"} { |
| + s := release.NewDirStore(t.TempDir()) |
| + rec := ssRecord(t, s) |
| + ss := ssMove(2) |
| + ss.LastRequest = "left-by-the-caller" |
| + got := apply(t, s, rec, OfflineQueue(offID, "", "uk", "owner@example.com", ssStage, ssPin, ss, at, Receipt{Nonce: nonce})) |
| + if got.Move.SelfServe.LastRequest != want || offlineGet(t, s, rec).Move.SelfServe.LastRequest != want { |
| + t.Fatalf("receipt %q: last_request %q, want %q", nonce, got.Move.SelfServe.LastRequest, want) |
| + } |
| + } |
| } |
| func TestOfflineQueueRefusesARecreatedLifetime(t *testing.T) { |
diff --git a/internal/provision/testdata/plan14-root-reads-self-serve-moves-2.tsv b/internal/provision/testdata/plan14-root-reads-self-serve-moves-2.tsv| index 4a5d1f8fc..c82a4e4ca 100644 |
| --- a/internal/provision/testdata/plan14-root-reads-self-serve-moves-2.tsv |
| +++ b/internal/provision/testdata/plan14-root-reads-self-serve-moves-2.tsv |
| @@ -23,4 +23,4 @@ internal/console/templates/settings.html external legacy accepted-legacy accepte |
| internal/provision/offline_move.go offlineBegin legacy accepted-legacy accepted-legacy;rev3=PROPOSED-self-serve-moves-2;basis=root-input (J4, as OfflineStart); the console (cmd/console serve under console.service, user carlos-console): self-serve moves 1 Task 7's body (OfflineStart's, the state a parameter; a direct start advances move_seq and stamps move.seq, refusing ErrLifetimeExhausted at MaxInt64 before any write), and it now also creates the self-serve move in offline-queued for OfflineQueue (queued_at, staged_at and precheck_at at the caller's stamp, move.seq set to the record's move_seq, no advance) and records return_of; a stage and a queue hold nothing; its one site is still rec.ActiveRun.Open(), a method on the record's run state (RunState.Open), not a file read; self-serve moves 2 Task 2 file:Open@208 0a9b432ddf2f39a977c0651e377258b35c1d05635e9979583eeac6253d6c0ec8 |
| @coverage internal/console 29ae289dc256408f6debca9150987ba5ce7a2e80a0bbf5fc3064ead950d16bd1 |
| @coverage internal/moves 51465341e311dc007d75ed4f68474fe4cec05d51c6ec07ee5c64653936faa410 |
| -@coverage internal/provision 6e75b92e716fd61f0f70e3071e36dc2be240fb419b70845b3429e8c9990a018a |
| +@coverage internal/provision 177bbc7f94d9ba7e4d999f832920e8552a0af4315d7ce2445572efb21fada342 |