Reconcile superseded Boat stop operations

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* commit 'ff26b3af9':
  Reconcile superseded Boat stop operations
This commit is contained in:
DottaandPaperclip committed 2026-10-10 10:51:43 -05:00
commit 38fee2e4f1
2 files changed
+50 -1

No files matched your search

@@ -191,6 +191,41 @@ describe("computer ownership", () => {
(await f.repository.get(f.scope)).ledger.action?.providerStopId,
).toBe("stop_1");
});
it("replaces a superseded stop while preserving the admission fence", async () => {
const f = fixture();
await f.attach();
await f.service.reconcile();
vi.mocked(f.backend.stopStatus).mockResolvedValueOnce({ id: "stop_1", status: "superseded" });
vi.mocked(f.backend.stop).mockResolvedValueOnce({ id: "stop_2", status: "pending" });
await f.service.reconcile();
expect((await f.repository.get(f.scope)).ledger.action?.providerStopId).toBe("stop_2");
await expect(f.admit()).rejects.toMatchObject({ code: "conflict" });
vi.mocked(f.backend.stopStatus).mockResolvedValueOnce({ id: "stop_2", status: "completed" });
await f.service.reconcile();
expect(f.backend.stopStatus).toHaveBeenLastCalledWith(expect.anything(), "stop_2");
await expect(f.admit()).resolves.toHaveProperty("owner");
});
it("adopts a newer pending stop without issuing a duplicate operation", async () => {
const f = fixture();
await f.attach();
await f.service.reconcile();
vi.mocked(f.backend.stopStatus).mockResolvedValueOnce({ id: "stop_1", status: "superseded" });
vi.mocked(f.backend.inspect).mockResolvedValueOnce({ state: "ready", snapshots: true, stop: { id: "stop_2", status: "failing" } });
await f.service.reconcile();
expect((await f.repository.get(f.scope)).ledger.action?.providerStopId).toBe("stop_2");
expect(f.backend.stop).toHaveBeenCalledOnce();
await expect(f.admit()).rejects.toMatchObject({ code: "conflict" });
});
it("settles a superseded stop only after physical archival proof", async () => {
const f = fixture();
await f.attach();
await f.service.detach(f.scope);
vi.mocked(f.backend.stopStatus).mockResolvedValueOnce({ id: "stop_1", status: "superseded" });
vi.mocked(f.backend.inspect).mockResolvedValueOnce({ state: "archived", snapshots: true, stop: null });
await f.service.reconcile();
expect((await f.repository.get(f.scope)).ledger).toMatchObject({ status: "detached", action: null });
expect(f.backend.stop).toHaveBeenCalledOnce();
});
it("viewer renewals cannot extend the absolute warm deadline or use another user", async () => {
const f = fixture();
await f.attach();
@@ -885,9 +885,23 @@ finally:
if (record.ledger.action) {
const action = record.ledger.action;
// A lost stop response is retried only while admission is fenced. Boat returns its same pending ID.
const stop = action.providerStopId
let stop = action.providerStopId
? await backend.stopStatus(record, action.providerStopId)
: await backend.stop(record);
if (stop.status === "superseded") {
// A provider-side resume can supersede the recorded stop. Keep admission
// fenced until current physical state proves stopped or a replacement
// normal snapshot operation completes; never force or delete the host.
const current = await backend.inspect(record);
if (["stopped", "archived"].includes(current.state)) {
stop = { id: stop.id, status: "completed" };
} else if (current.stop && current.stop.id !== stop.id &&
["pending", "failing"].includes(current.stop.status)) {
stop = current.stop;
} else {
stop = await backend.stop(record);
}
}
await repository.update(record, (current) => {
if (current.ledger.action?.id !== action.id) return;
current.ledger.action.providerStopId = stop.id;