The extension question was hidden behind the results screen (Gitea#11)

Jesse, playing v0.7.8: "Solitaire game ended. I did not have an option
to extend the game by a day."

The engine and the Frame were right — checked before changing anything.
A solitaire game at the end of its timetable reaches awaitingExtension
with extensionVotes [null], and renderEnding writes "play one more Day"
into #actions. It then opens #resultsdlg, which is MODAL, so those
buttons were directly underneath a dialog whose only control was Close.

The results dialog now carries the question itself, hidden unless a vote
is pending: Play One More Day / End the Game Here, casting the same
game.extend intent. The #actions buttons stay as the fallback once it is
closed.

TODO.md #35 recorded extended play as verified on phoenix.local — over
the HTTP API, which renders no dialog. What was proven was that the
server supports it, not that a player can reach it. Noted there.

Rides along in the unshipped v0.7.9. 870 tests pass, one new.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AdG46Ja2PEDBkpqiDazMoX
This commit is contained in:
Jesse.Markowitz
2026-08-30 01:26:03 -04:00
co-authored by Claude Sonnet 5
parent f2c87b6871
commit e035dda5a3
5 changed files with 101 additions and 2 deletions
+30
View File
@@ -3362,6 +3362,36 @@ describe('the end-of-game results screen (Gitea#16)', () => {
return snapshot(s, [], null);
};
it('asks the extension question ON the results dialog, not only behind it (Gitea#11)', () => {
/**
* REPORTED BY JESSE 2026-08-30: "Solitaire game ended. I did not have an option to extend the
* game by a day."
*
* The engine and the Frame were right all along — a solitaire game at the end of its timetable
* reaches `awaitingExtension` with an uncast vote, asserted below. What went wrong is that
* `renderEnding` writes its two buttons into `#actions` and then opens `#resultsdlg`, which is
* MODAL: the question was underneath a dialog whose only control was Close. Gitea#11 was
* verified over the HTTP API, which renders no dialog, so the browser path never was.
*/
const f = finished();
assert.equal(f.status, 'awaitingExtension', 'a solitaire game no longer pauses to ask');
assert.equal(f.extensionVotes[f.viewer], null, 'the viewer has somehow already voted');
const html = readFileSync(join(dist, 'play.html'), 'utf8');
for (const id of ['rs-extend-yes', 'rs-extend-no']) {
assert.ok(html.includes(`id="${id}"`), `the results dialog cannot ask: no #${id}`);
}
// And they are wired: shown only while a vote is pending, and casting the same intent the
// `#actions` buttons do. Read from the source, since the dialog needs a DOM to drive.
const src = readFileSync(join(root, 'src/web/main.ts'), 'utf8');
const fn = /function showResults\(f: Frame\): void \{[\s\S]*?\n\}/.exec(src)?.[0] ?? '';
assert.ok(fn !== '', 'showResults moved and this test cannot see it');
assert.match(fn, /awaitingExtension/, 'the dialog does not know the game is asking');
assert.match(fn, /'game\.extend'/, 'the dialog offers no way to answer');
assert.match(fn, /yes\.hidden = !asking/, 'the buttons are not hidden on a game that is over');
});
it('never prints a raw enum at the player', () => {
// The bug the issue opens on: `GAME OVER — revenueFloor` is an internal identifier, shown at
// the one moment the game has the player's whole attention.