Compare commits

...

2 Commits

Author SHA1 Message Date
funman300 5b2b234c54 fix(web-e2e): expose serialize() on classic bridge; use real clock API
Web WASM Freshness / freshness (pull_request) Failing after 7m8s
Two latent test bugs surfaced once the e2e suite actually ran (it had been
red on the webServer startup timeout, so these never executed): 14 passed,
4 failed.

- game.js `__FERROUS_DEBUG__` was missing `serialize()` — play.html's bridge
  has it but the /play-classic bridge (which the resume/move-history tests
  use) drifted. Added it (the wasm SolitaireGame already exposes serialize()).
- game_behaviors.spec.js called `page.clock.tick()`, which is the sinon name;
  Playwright's Clock API method is `page.clock.runFor()`. Replaced all 6 calls.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 12:46:12 -07:00
funman300 9c473d6a51 fix(web): make wasm builds reproducible so the freshness gate passes
The web-wasm-freshness gate (#93) false-failed because the Bevy wasm build
baked in machine-specific absolute source paths — the cargo registry
(/home/<user>/.cargo/registry/...), the rustup std sources (~/.rustup/...),
and this checkout — so CI's rebuild (under /workspace, /root) never matched the
committed bytes even with identical pinned tool versions.

build_wasm.sh now exports CARGO_ENCODED_RUSTFLAGS with three --remap-path-prefix
rules (cargo home -> /cargo, rustup home -> /rustup, repo -> /build) so the
embedded paths are identical on any machine. It re-states the
getrandom_backend cfg because a *_RUSTFLAGS env var replaces (not merges with)
.cargo/config.toml's target rustflags.

Regenerated all four artifacts with the remap applied — verified zero
machine-specific paths remain (only canonical /cargo + /rustup prefixes), which
is the precondition for the gate's byte-for-byte rebuild-and-diff to match
across machines.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 12:46:12 -07:00
5 changed files with 26 additions and 7 deletions
+16
View File
@@ -22,6 +22,22 @@ set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
OUT_DIR="$REPO_ROOT/solitaire_server/web/pkg"
# Reproducible builds. The wasm artifacts otherwise bake in machine-specific
# absolute source paths (the cargo registry, the rustup std sources, and this
# checkout), so a rebuild on a different machine produces different bytes and
# the CI freshness gate (rebuild-and-diff) false-positives. Remap all three
# prefixes to fixed names so the output is byte-identical anywhere.
#
# We must use CARGO_ENCODED_RUSTFLAGS (not RUSTFLAGS) and re-state the
# getrandom backend cfg here: a `*_RUSTFLAGS` env var *replaces* — does not
# merge with — the `[target.wasm32-unknown-unknown] rustflags` in
# .cargo/config.toml, so dropping that cfg would break the wasm getrandom build.
# Keep this `--cfg` in sync with .cargo/config.toml.
CARGO_HOME_DIR="${CARGO_HOME:-$HOME/.cargo}"
RUSTUP_HOME_DIR="${RUSTUP_HOME:-$HOME/.rustup}"
US=$'\x1f' # unit separator: CARGO_ENCODED_RUSTFLAGS arg delimiter
export CARGO_ENCODED_RUSTFLAGS="--cfg${US}getrandom_backend=\"wasm_js\"${US}--remap-path-prefix=${CARGO_HOME_DIR}=/cargo${US}--remap-path-prefix=${RUSTUP_HOME_DIR}=/rustup${US}--remap-path-prefix=${REPO_ROOT}=/build"
if ! command -v wasm-pack &> /dev/null; then
echo "error: wasm-pack not found." >&2
echo " Install with: cargo install wasm-pack" >&2
@@ -136,7 +136,7 @@ test("new game button resets move history and score", async ({ page }) => {
test("timer stops accumulating while tab is hidden", async ({ page }) => {
// Install the fake clock before navigation so the game's setInterval is
// controlled by page.clock.tick() and won't fire on real wall-clock time.
// controlled by page.clock.runFor() and won't fire on real wall-clock time.
await page.clock.install();
await page.goto("/play-classic?seed=42");
@@ -144,7 +144,7 @@ test("timer stops accumulating while tab is hidden", async ({ page }) => {
await waitForBridge(page);
// Advance 3 fake seconds to get a non-zero timer reading.
await page.clock.tick(3_000);
await page.clock.runFor(3_000);
const timerAfter3s = await page.locator("#hud-timer").textContent();
expect(timerAfter3s).toBe("0:03");
@@ -152,7 +152,7 @@ test("timer stops accumulating while tab is hidden", async ({ page }) => {
await setTabHidden(page, true);
// Advance 10 fake seconds while hidden.
await page.clock.tick(10_000);
await page.clock.runFor(10_000);
const timerWhileHidden = await page.locator("#hud-timer").textContent();
expect(timerWhileHidden).toBe("0:03"); // must not have advanced
@@ -160,7 +160,7 @@ test("timer stops accumulating while tab is hidden", async ({ page }) => {
await setTabHidden(page, false);
// Advance 2 more fake seconds.
await page.clock.tick(2_000);
await page.clock.runFor(2_000);
const timerAfterResume = await page.locator("#hud-timer").textContent();
expect(timerAfterResume).toBe("0:05"); // only 3 + 2 visible seconds counted
});
@@ -182,11 +182,11 @@ test("timer does not restart while tab is visible during an auto-complete or won
// the snap state correctly gates the restart.
//
// Advance 2 s, then hide+show — timer should continue normally.
await page.clock.tick(2_000);
await page.clock.runFor(2_000);
await setTabHidden(page, true);
await page.clock.tick(5_000);
await page.clock.runFor(5_000);
await setTabHidden(page, false);
await page.clock.tick(2_000);
await page.clock.runFor(2_000);
const timerText = await page.locator("#hud-timer").textContent();
// 2 visible + 0 hidden + 2 visible = 4 total
+3
View File
@@ -986,6 +986,9 @@ window.__FERROUS_DEBUG__ = {
snapshot() {
return game ? game.debug_snapshot() : null;
},
serialize() {
return game ? game.serialize() : null;
},
applyLegalMove(index) {
if (!game) return { ok: false, error: "game_not_ready" };
const result = game.debug_apply_legal_move(index);
Binary file not shown.
Binary file not shown.