fix(rust): hold the reconcile until the world is loaded, and never read a refusal as a revert
All checks were successful
PR Checks / client-build (pull_request) Successful in 20s
PR Checks / server-tests (pull_request) Successful in 25s
PR Checks / frozen-manifest (pull_request) Successful in -1m10s

Two defects the phase 13a walk found by restarting the rig mid-run:

- The watch asked core to reconcile the moment a new boot id appeared, which
  is before the game has loaded its save — every crate looked gone and was
  orphaned. It now waits for the plugin's hello to say `worldReady`; an older
  plugin that never says is taken as ready.
- revert() read any 200 as success. On this bridge a refusal is a 200
  carrying world.error (`not-ready` while loading), so every row would have
  been marked reverted with the game still holding every crate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-09-24 02:16:03 -05:00
parent fab31f23e8
commit ba636c8939
3 changed files with 59 additions and 3 deletions

View File

@@ -329,3 +329,39 @@ test('only a restart or a wipe asks core to reconcile — never a first sighting
assert.strictEqual(world.observeServer('alt', { bootId: 'x', wipeId: 'y' }), false) // another server's baseline
assert.strictEqual(asked, 2)
})
test('a refusal that arrives as a 200 is not a revert (§28.6)', async (t) => {
// The plugin answers `world.error` with a 200 — `not-ready` while the world is
// still loading. Read as success, every row would be marked reverted while the
// game still held every crate.
stub(t, {
revert: async () => ({ ok: true, data: { kind: 'world.error', reason: 'not-ready', message: 'still loading' } }),
})
const result = await world.revert({ runId: 8, resources: [{ kind: 'world', ref: 'main:101' }] })
assert.deepStrictEqual(result, { ok: false, error: 'still loading' })
const lost = await world.revert({ runId: 8, resources: [], idempotencyKey: 'k' })
assert.strictEqual(lost.ok, false)
})
test('a boot id seen before the world has loaded is not a restart yet (§28.6)', (t) => {
world.resetWatch()
let asked = 0
const saved = core.reconcileEvents
core.reconcileEvents = () => {
asked += 1
return Promise.resolve({})
}
t.after(() => {
core.reconcileEvents = saved
world.resetWatch()
})
world.observeServer('main', { bootId: 'b1', wipeId: 'w1', worldReady: true })
// The plugin reconnects on the new boot BEFORE the save loads.
assert.strictEqual(world.observeServer('main', { bootId: 'b2', wipeId: 'w1', worldReady: false }), false)
assert.strictEqual(asked, 0)
// Its next hello says the world is there, and that is when the change counts.
assert.strictEqual(world.observeServer('main', { bootId: 'b2', wipeId: 'w1', worldReady: true }), true)
assert.strictEqual(asked, 1)
})