fix(rust): hold the reconcile until the world is loaded, and never read a refusal as a revert
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:
@@ -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)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user