Repair stale Semuta battle-frame recovery expectation

This commit is contained in:
Quintine committed 2026-10-12 14:16:01 +11:00
1 parent 7ae0a9f7ae
commit 63e5f22607
2 files changed
+31 -4

No files matched your search

+13
View File
@@ -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.
+18 -4
View File
@@ -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);