carlos / platform Public

offline return: the door needs the record's proof the forward completed (final review F4)

offlineReturnOpen, shared by the return door and return-mode stage and
start, requires the record's last move to be return_of, done, before any
write; a matching open record alone (a forward that opened and then
aborted, or one superseded by a later move) answers the existing
"no completed move" refusal.

Witness: TestReturnNeedsTheForwardCompleted (nil, foreign and aborted last
move through return, stage and start). The return fixture now records the
forward as done, as a completed forward leaves the record.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 47567b372e8edae5674d5c5181af259491cb3d94 parent 6a2fe8b
2 files changed, +43 −0
  • internal/console/offlinereturn.go +5 −0
  • internal/console/offlinereturn_test.go +38 −0
diff --git a/internal/console/offlinereturn.go b/internal/console/offlinereturn.go
index f3587befd..5dcf11ad9 100644
--- a/internal/console/offlinereturn.go
+++ b/internal/console/offlinereturn.go
@@ -91,6 +91,9 @@ func (s *Server) readOpenRecord(ctx context.Context, host, id string) (*moves.Op
// offlineReturnOpen checks that returnOf is a completed forward move of
// this record's lifetime from the region (code) asked for into the
// record's region, with nothing in flight: its open record, or the 409.
+// Completed is the record's own word (final review F4): its last move is
+// returnOf, done; an open record alone proves only that the forward opened
+// once.
func (s *Server) offlineReturnOpen(ctx context.Context, rec *provision.Record, region, code, returnOf string) (*moves.OpenRecord, *offlineRefusal) {
none := &offlineRefusal{status: http.StatusConflict, msg: ssNoCompletedMove(returnOf, region, s.offlineName(rec.Region))}
o, err := s.readOpenRecord(ctx, rec.Host, returnOf)
@@ -104,6 +107,8 @@ func (s *Server) offlineReturnOpen(ctx context.Context, rec *provision.Record, r
return nil, &offlineRefusal{status: http.StatusConflict, msg: msgReturnRecreated}
case rec.Move != nil:
return nil, none
+ case rec.LastMove == nil || rec.LastMove.ID != returnOf || rec.LastMove.Outcome != provision.OfflineOutcomeDone:
+ return nil, none
}
return o, nil
}
diff --git a/internal/console/offlinereturn_test.go b/internal/console/offlinereturn_test.go
index 40528d0ed..0015f9415 100644
--- a/internal/console/offlinereturn_test.go
+++ b/internal/console/offlinereturn_test.go
@@ -28,6 +28,11 @@ type returnWorld struct {
func newReturnWorld(t *testing.T, edit func(o *moves.OpenRecord)) *returnWorld {
t.Helper()
w := &returnWorld{offlineWorld: newOfflineWorld(t, oneEach...)}
+ // The forward finished: the record's last move is it, done.
+ w.edit(func(r *provision.Record) error {
+ r.LastMove = &provision.MoveOutcome{ID: returnFwd, Outcome: provision.OfflineOutcomeDone, At: time.Now().Add(-time.Hour).UTC()}
+ return nil
+ })
rec := w.record()
w.open = moves.OpenRecord{MoveID: returnFwd, Host: offlineHost, Account: rec.Account, App: rec.App, Incarnation: rec.Incarnation,
From: "uk", To: "ie", Layout: moves.LayoutFlat, OpenedAt: time.Now().Add(-time.Hour).UTC(),
@@ -323,3 +328,36 @@ func TestOperatorReturnHandshakeAtTheConsole(t *testing.T) {
}
})
}
+
+// TestReturnNeedsTheForwardCompleted (final review F4): a return names a
+// forward the record says completed, its last move that forward, done.
+// With a matching open record but no last move, another move's, or the
+// forward aborted, the return door, return-mode stage and return-mode
+// start all refuse before any write.
+func TestReturnNeedsTheForwardCompleted(t *testing.T) {
+ noMove := "no completed move " + returnFwd + " from uk to ie for this instance"
+ for name, last := range map[string]*provision.MoveOutcome{
+ "no last move": nil,
+ "another move's": {ID: "mv_othermoveothermoveothermov", Outcome: provision.OfflineOutcomeDone, At: time.Now().UTC()},
+ "the forward aborted": {ID: returnFwd, Outcome: provision.OfflineOutcomeAborted, At: time.Now().UTC()},
+ } {
+ t.Run(name, func(t *testing.T) {
+ w := newReturnWorld(t, nil)
+ // A request and its report written while the forward still
+ // looked complete, so only the completion proof can refuse.
+ w.report("uk-box", "uk-box", "", w.request(), time.Now(), w.ready())
+ w.edit(func(r *provision.Record) error { r.LastMove = last; return nil })
+ requests, _ := w.fx.rel.List(context.Background(), moves.ReturnRequestPrefix(offlineHost, returnFwd))
+ body := `{"region":"uk","return_of":"` + returnFwd + `"}`
+ w.refused(http.MethodPost, "/return", body, http.StatusConflict, noMove)
+ w.refused(http.MethodPost, "/stage", body, http.StatusConflict, noMove)
+ w.refused(http.MethodPost, "", body, http.StatusConflict, noMove)
+ if rec := w.record(); rec.Move != nil {
+ t.Fatalf("a refused return wrote %+v", rec.Move)
+ }
+ if again, _ := w.fx.rel.List(context.Background(), moves.ReturnRequestPrefix(offlineHost, returnFwd)); len(again) != len(requests) {
+ t.Fatalf("the refused return door wrote a request: %v, was %v", again, requests)
+ }
+ })
+ }
+}