From 63e5f22607848c967b0fbb70876e1ba1dc496a92 Mon Sep 17 00:00:00 2001 From: Quintine <15965533+Quintine@users.noreply.github.com> Date: Mon, 12 Oct 2026 14:16:01 +1100 Subject: [PATCH] Repair stale Semuta battle-frame recovery expectation --- docs/CURRENT_STATUS.md | 13 +++++++++++++ tests/semuta-recovery.test.ts | 22 ++++++++++++++++++---- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md index eeaeaefb..c6df3f99 100644 --- a/docs/CURRENT_STATUS.md +++ b/docs/CURRENT_STATUS.md @@ -29,6 +29,19 @@ optional skill bands, combinations and public expansion starts remain open. Rules implementation continues; comprehensive assurance and administration remain deferred until all rules are implemented. +**Stale battle-frame recovery expectation repaired — 11 October.** The +`authenticated clean Semuta producers claim once through restart and room CAS` +case asserted the winner's cheap-hero frame as the first post-battle +continuation. Actual native order is the mandatory battle batch +(`battleResolved`/`battle:mandatory`, loser's used defense) followed by the +winner's compulsory discard (`winnerMandatoryDiscard`/`battle:winnerMandatory`, +the hero). The case now walks that real order, keeps its restart/room-CAS and +single-claim assertions on the winner frame, and asserts the loser frame +separately. Verified pre-existing: the same assertion failed at `5b3729e` in a +clean worktree, so it was not a regression from this session's slices. +`tests/semuta-recovery.test.ts` passes15/15; the affected offline rule union +passes650/650 across60 files; types/lint pass. + **Semuta / printed native CHOAM cancellation cost — 11 October.** `karamaChoamWorthlessDiscard` reuses the original response-cost path and Worthless cancellation quote for five ordinary printed/Cunning powers. diff --git a/tests/semuta-recovery.test.ts b/tests/semuta-recovery.test.ts index 499aa0c2..2d323b6e 100644 --- a/tests/semuta-recovery.test.ts +++ b/tests/semuta-recovery.test.ts @@ -448,14 +448,28 @@ void test('authenticated clean Semuta producers claim once through restart and r defense: nextDefense.id }); mandatoryBattle = await act(1, { type: 'traitorCall', call: false }); const heroOffer = await act(2, { type: 'traitorCall', call: false }); - assert.equal(heroOffer.pendingTreacheryDiscard?.continuation.kind, 'winnerMandatoryDiscard'); + // The winner's cheap hero and the loser's used defense form separate frames: + // the mandatory battle batch settles first, then the winner's own compulsory + // discard. The Semuta claim under test targets that second frame. + assert.equal(heroOffer.pendingTreacheryDiscard?.continuation.kind, 'battleResolved'); + assert.equal(heroOffer.pendingTreacheryDiscard?.batch.cause, 'battle:mandatory'); assert.equal(heroOffer.pendingTreacheryDiscard?.reaction?.stage, 'offer'); - assert.equal(heroOffer.discard.filter(card => card.id === hero.id).length, 1); + assert.equal(heroOffer.discard.filter(card => card.id === nextDefense.id).length, 1); + let heroFrame = heroOffer; + { + const mandatoryEvent = heroOffer.pendingTreacheryDiscard!.batch.event; + for (const index of [2, 1, 0]) + heroFrame = await act(index, { type: 'semutaPass', event: mandatoryEvent }); + } + assert.equal(heroFrame.pendingTreacheryDiscard?.continuation.kind, 'winnerMandatoryDiscard'); + assert.equal(heroFrame.pendingTreacheryDiscard?.batch.cause, 'battle:winnerMandatory'); + assert.equal(heroFrame.pendingTreacheryDiscard?.reaction?.stage, 'offer'); + assert.equal(heroFrame.discard.filter(card => card.id === hero.id).length, 1); await f.restart().continueRoomAutomatic(code, clock); - assert.equal((await f.restart().readRoom(code)).version, heroOffer.version); + assert.equal((await f.restart().readRoom(code)).version, heroFrame.version); assert.equal((await f.restart().readSeatView(code, auths[0])).semutaReaction?.canCommit, true); const heroClaim = await act(0, { type: 'semutaCommit', - event: heroOffer.pendingTreacheryDiscard!.batch.event }); + event: heroFrame.pendingTreacheryDiscard!.batch.event }); assert.equal(heroClaim.pendingTreacheryDiscard, null); assert.equal(heroClaim.decision?.kind, 'battleCards'); assert.equal(seat(heroClaim, ids[1]).hand.some(card => card.id === nextWeapon.id), true);