Skip to content

Fix sandbox review findings - #7128

Merged
matthewevans merged 3 commits into
mainfrom
fix/sandbox-review-followup
Aug 9, 2026
Merged

Fix sandbox review findings#7128
matthewevans merged 3 commits into
mainfrom
fix/sandbox-review-followup

Conversation

@matthewevans

@matthewevans matthewevans commented Aug 9, 2026

Copy link
Copy Markdown
Member

Addresses the follow-up review findings from #7123 after that PR merged.\n\n- completes debug create count, authorization, and zero-no-op handling across engine, WASM, WebSocket, and P2P\n- fixes scry overlay delivery, counter adjustment UI, and deck-sync merge validation\n- includes targeted regression coverage and protocol version updates

Summary by CodeRabbit

  • New Features
    • Added a “Keep Both Decks” option when resolving deck conflicts.
    • Debug actions that create zero items now complete cleanly without changing game state.
    • Scry results are now surfaced in the game interface and cleared appropriately between sessions.
  • Bug Fixes
    • Improved validation when merging backup data, preventing malformed entries from overwriting valid local data.
    • Enhanced multiplayer handling for actions that produce no state changes.
    • Improved debug action validation and audit reporting.
  • Localization
    • Added localized “Copies” labels to debug creation controls.

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5c5a4f09-69bf-43a7-80bf-ddbcfc05f49d

📥 Commits

Reviewing files that changed from the base of the PR and between f2dcbe8 and 062cd5f.

📒 Files selected for processing (3)
  • crates/engine-wasm/src/lib.rs
  • crates/engine/src/game/engine.rs
  • crates/engine/src/game/engine_debug.rs
💤 Files with no reviewable changes (2)
  • crates/engine-wasm/src/lib.rs
  • crates/engine/src/game/engine_debug.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/engine/src/game/engine.rs

📝 Walkthrough

Walkthrough

The change centralizes debug-action validation, supports validated zero-count no-ops, adds protocol acknowledgements, updates client handling, improves debug UI localization, surfaces scry results, and validates backup metadata before merging.

Changes

Zero-count debug actions

Layer / File(s) Summary
Protocol and action contracts
client/src/adapter/types.ts, client/src/network/*, crates/server-core/src/protocol.rs, crates/lobby-broker/src/protocol.rs, scripts/check-protocol-version.mjs
Adds PlayerActionKind and ActionNoOp. Protocol versions advance to 26 for WebSocket/server communication and 19 for P2P communication.
Shared debug preflight and creation
crates/engine/src/game/*
Centralizes debug authorization, count, owner, and priority validation. Debug card creation accepts an actor and emits engine-owned audit results.
WASM debug submission behavior
crates/engine-wasm/src/lib.rs, client/src/adapter/wasm-adapter.ts, client/src/adapter/engine-worker.ts, client/src/adapter/__tests__/wasm-adapter.test.ts
Preflights debug card creation before database access. Nonzero creation retries only after the matching missing-database error.
Server no-op acknowledgement flow
crates/server-core/src/session.rs, crates/phase-server/src/main.rs
Returns ActionNoOp for zero-count debug creations without revision changes, persistence, AI processing, or state broadcasts.
Client no-op handling
client/src/adapter/p2p-adapter.ts, client/src/adapter/ws-adapter.ts, client/src/adapter/__tests__/*
Resolves no-op submissions with empty results, clears pending state, preserves cached snapshots, and suppresses state-change events.

Client UI and session state

Layer / File(s) Summary
Debug controls and localization
client/src/components/chrome/*, client/src/i18n/locales/*/game.json
Localizes debug copy-count labels and displays raw counter values.
Scry state and cloud conflict handling
client/src/game/*, client/src/components/settings/PreferencesModal.tsx
Dispatches completed scry outcomes, clears them during session cleanup, adds integration coverage, and provides a “Keep Both Decks” merge action.

Backup record validation

Layer / File(s) Summary
Validated backup merging
client/src/services/backup.ts, client/src/services/__tests__/backup.test.ts
Validates deck metadata and folder records before merging. Invalid cloud records do not replace local collections.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ServerSession
  participant Engine
  participant CardDatabase
  Client->>ServerSession: submit debug action
  ServerSession->>Engine: preflight_debug_action
  alt zero-count creation
    Engine-->>ServerSession: validated empty result
    ServerSession-->>Client: ActionNoOp
  else nonzero CreateCard
    ServerSession->>CardDatabase: resolve card source
    CardDatabase-->>ServerSession: card source
    ServerSession->>Engine: create_debug_cards
    Engine-->>ServerSession: state transition result
    ServerSession-->>Client: action result and state update
  end
Loading

Possibly related PRs

  • phase-rs/phase#6515: Both PRs modify P2P host/action transport paths and server protocol handling.
  • phase-rs/phase#6635: Both PRs modify engine and session action validation before mutation.
  • phase-rs/phase#7123: Both PRs modify debug creation count handling across the engine, adapters, server, and protocol.

Suggested labels: bug

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately identifies the pull request as fixes for sandbox review findings and concisely reflects its follow-up purpose.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/sandbox-review-followup

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@matthewevans
matthewevans enabled auto-merge August 9, 2026 09:31
@matthewevans
matthewevans force-pushed the fix/sandbox-review-followup branch 2 times, most recently from 7833638 to 5f4ca7a Compare August 9, 2026 09:38
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown

Generated for head 062cd5f7c640bba008cc09f03b60fc130db99b6e.

Parse changes introduced by this PR

✓ No card-parse changes detected.

@matthewevans
matthewevans force-pushed the fix/sandbox-review-followup branch from 5f4ca7a to f2dcbe8 Compare August 9, 2026 10:04

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@client/src/adapter/p2p-adapter.ts`:
- Around line 570-580: Replace action-shape-based no-op detection with a typed
engine outcome such as Applied or NoOp, and propagate it without erasing NoOp.
In client/src/adapter/p2p-adapter.ts at lines 570-580 remove
isZeroCountDebugCreate; update lines 1706-1710 to return the typed outcome,
lines 2165-2169 to emit action_noop only for NoOp, and lines 3058-3065 to
resolve pending requests with that outcome. In client/src/adapter/ws-adapter.ts
lines 1453-1461 preserve NoOp rather than converting it to an empty normal
result. Update client/src/game/dispatch.ts to return before getSnapshot() when
the resolved outcome is NoOp.

In `@client/src/game/dispatch.ts`:
- Line 508: Ensure completed-scry events received through
processRemoteUpdateInner trigger flashCompletedScry just like processAction
events, preferably from the shared event boundary to avoid duplicate
notifications. Preserve existing local behavior and add WebSocket/P2P coverage
verifying the public scry overlay appears for the affected seat.

In `@crates/phase-server/src/main.rs`:
- Around line 3865-3869: Update handle_full_game_submission to accept the
per-connection mpsc sender used by connections, and in the
is_zero_count_debug_create branch enqueue ServerMessage::ActionNoOp through that
sender instead of writing directly to socket. Ensure callers pass the matching
connection sender so acknowledgements preserve ordering with queued StateUpdate
messages.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 42dbc2cb-e6a9-41dd-92e1-f053a1f965fe

📥 Commits

Reviewing files that changed from the base of the PR and between 36cd2ee and f2dcbe8.

📒 Files selected for processing (36)
  • client/src/adapter/__tests__/p2p-adapter-multiplayer.test.ts
  • client/src/adapter/__tests__/wasm-adapter.test.ts
  • client/src/adapter/__tests__/ws-adapter.test.ts
  • client/src/adapter/engine-worker.ts
  • client/src/adapter/p2p-adapter.ts
  • client/src/adapter/types.ts
  • client/src/adapter/wasm-adapter.ts
  • client/src/adapter/ws-adapter.ts
  • client/src/components/chrome/DebugCardContextMenu.tsx
  • client/src/components/chrome/DebugCreateActions.tsx
  • client/src/components/chrome/DebugObjectActions.tsx
  • client/src/components/settings/PreferencesModal.tsx
  • client/src/game/__tests__/diceContest.test.ts
  • client/src/game/__tests__/sessionCleanup.test.ts
  • client/src/game/dispatch.ts
  • client/src/game/sessionCleanup.ts
  • client/src/i18n/locales/de/game.json
  • client/src/i18n/locales/en/game.json
  • client/src/i18n/locales/es/game.json
  • client/src/i18n/locales/fr/game.json
  • client/src/i18n/locales/it/game.json
  • client/src/i18n/locales/pl/game.json
  • client/src/i18n/locales/pt/game.json
  • client/src/network/__tests__/protocol.test.ts
  • client/src/network/protocol.ts
  • client/src/services/__tests__/backup.test.ts
  • client/src/services/backup.ts
  • crates/engine-wasm/src/lib.rs
  • crates/engine/src/game/engine.rs
  • crates/engine/src/game/engine_debug.rs
  • crates/engine/src/game/mod.rs
  • crates/lobby-broker/src/protocol.rs
  • crates/phase-server/src/main.rs
  • crates/server-core/src/protocol.rs
  • crates/server-core/src/session.rs
  • scripts/check-protocol-version.mjs
💤 Files with no reviewable changes (1)
  • client/src/adapter/engine-worker.ts

Comment on lines +570 to +580
function isZeroCountDebugCreate(action: GameAction): boolean {
if (action.type !== "Debug") return false;
switch (action.data.type) {
case "CreateCard":
case "CreateToken":
case "CreateTokenCopy":
return action.data.data.count === 0;
default:
return false;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Keep the no-op outcome engine-owned and typed.

The adapters derive no-op semantics from GameAction and then resolve ActionNoOp as a normal SubmitResult. client/src/game/dispatch.ts calls adapter.getSnapshot() after every resolved action. Therefore, the host path at Line 1709 still requests a worker snapshot for a zero-count action.

Expose a typed engine result such as Applied or NoOp. Propagate it through both adapters. Update client/src/game/dispatch.ts to return before snapshot retrieval for NoOp.

  • client/src/adapter/p2p-adapter.ts#L570-L580: remove the local action-shape classifier.
  • client/src/adapter/p2p-adapter.ts#L1706-L1710: return the typed engine outcome without erasing NoOp.
  • client/src/adapter/p2p-adapter.ts#L2165-L2169: send action_noop only from the typed engine outcome.
  • client/src/adapter/p2p-adapter.ts#L3058-L3065: resolve the pending request with the typed NoOp outcome.
  • client/src/adapter/ws-adapter.ts#L1453-L1461: preserve the typed NoOp outcome instead of returning a normal empty result.
📍 Affects 2 files
  • client/src/adapter/p2p-adapter.ts#L570-L580 (this comment)
  • client/src/adapter/p2p-adapter.ts#L1706-L1710
  • client/src/adapter/p2p-adapter.ts#L2165-L2169
  • client/src/adapter/p2p-adapter.ts#L3058-L3065
  • client/src/adapter/ws-adapter.ts#L1453-L1461
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/adapter/p2p-adapter.ts` around lines 570 - 580, Replace
action-shape-based no-op detection with a typed engine outcome such as Applied
or NoOp, and propagate it without erasing NoOp. In
client/src/adapter/p2p-adapter.ts at lines 570-580 remove
isZeroCountDebugCreate; update lines 1706-1710 to return the typed outcome,
lines 2165-2169 to emit action_noop only for NoOp, and lines 3058-3065 to
resolve pending requests with that outcome. In client/src/adapter/ws-adapter.ts
lines 1453-1461 preserve NoOp rather than converting it to an empty normal
result. Update client/src/game/dispatch.ts to return before getSnapshot() when
the resolved outcome is NoOp.

Source: Path instructions

// way the turn banner bypasses the animation queue. These events are marked
// NON_VISUAL so normalizeEvents skips them below.
flashInGameRolls(events);
flashCompletedScry(events);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 12 'processRemoteUpdateInner|flashInGameRolls|flashCompletedScry' \
  client/src/game/dispatch.ts

rg -n -C 8 'flashCompletedScry|scry_top_count|scry_bottom_count' \
  client/src/game/__tests__ client/src/adapter/__tests__

Repository: phase-rs/phase

Length of output: 12193


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== dispatch.ts remote path =="
sed -n '790,900p' client/src/game/dispatch.ts

echo "== diceContest.ts implementation =="
fd -a diceContest.ts client/src/game/dispatch.ts client/src | head -20
rg -n -C 12 'export function flashCompletedScry|function flashCompletedScry|export const flashCompletedScry|flashCompletedScry' client/src/game/dispatch.ts client/src/game/*.ts

echo "== dispatch tests around remote/update =="
wc -l client/src/game/__tests__/dispatch.test.ts client/src/game/__tests__/diceContest.test.ts
sed -n '160,240p' client/src/game/__tests__/diceContest.test.ts
rg -n -C 8 'processRemote|remote update|flash.*Scry|scryOutcome' client/src/game __tests__ client/src/adapter client/src -g '*.test.ts' -g '*.test.js' -g '*.ts'

Repository: phase-rs/phase

Length of output: 14886


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== diceContest.ts =="
sed -n '86,112p' client/src/game/diceContest.ts

echo "== scry tests =="
sed -n '142,220p' client/src/game/__tests__/diceContest.test.ts

echo "== all scry mentions in tests =="
rg -n -C 12 'flashCompletedScry|scryOutcome|scry_top_count|scry_bottom_count|playerPerformedAction|PlayerPerformedAction' client/src ** -g '*.test.ts' -g '*.test.js' -g '*.ts' | head -n 240

Repository: phase-rs/phase

Length of output: 23367


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== completed scry event type definitions =="
rg -n -C 8 'CompletedScryEvent|ProcessCompletedScry|PlayerPerformedAction' . -g '*.ts' -g '*.tsx' -g '*.rs'

echo "== scry engine/rust occurrences =="
rg -n -C 8 'Scry|scry_top_count|scry_bottom_count|ProcessCompletedScry|completed_scry|CompletedScry' . -g '*.rs' | head -n 260

echo "== adapter snapshots/events in ts =="
rg -n 'submitAction|processRemoteUpdate|processSnapshot|GameEvent' client/src/adapter client/src/game/dispatch.ts -g '*.ts'

Repository: phase-rs/phase

Length of output: 50371


Route Scry counts through the remote update path.

processAction calls flashCompletedScry(events), but processRemoteUpdateInner handles remote snapshots separately and never calls this helper. Multiplayer clients can receive a completed-scry event without seeing the public scry-overlay result for that seat. Move the notification into the shared event boundary or add it to the remote path, then add WebSocket/P2P coverage.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/game/dispatch.ts` at line 508, Ensure completed-scry events
received through processRemoteUpdateInner trigger flashCompletedScry just like
processAction events, preferably from the shared event boundary to avoid
duplicate notifications. Preserve existing local behavior and add WebSocket/P2P
coverage verifying the public scry overlay appears for the affected seat.

Source: Path instructions

Comment on lines +3865 to +3869
if is_zero_count_debug_create {
drop(mgr);
if let Ok(json) = serde_json::to_string(&ServerMessage::ActionNoOp) {
let _ = socket.send(Message::text(json)).await;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Queue ActionNoOp through the per-connection sender.

Normal StateUpdate messages use the connection mpsc queue. This branch writes ActionNoOp directly to socket. If an earlier state update is queued, the acknowledgement can overtake that update and expose messages out of order.

Pass the same sender used by connections into handle_full_game_submission and enqueue ServerMessage::ActionNoOp through it.

Based on the transport-layer async-ordering requirement, acknowledgements and state updates must preserve per-connection ordering.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/phase-server/src/main.rs` around lines 3865 - 3869, Update
handle_full_game_submission to accept the per-connection mpsc sender used by
connections, and in the is_zero_count_debug_create branch enqueue
ServerMessage::ActionNoOp through that sender instead of writing directly to
socket. Ensure callers pass the matching connection sender so acknowledgements
preserve ordering with queued StateUpdate messages.

Source: Path instructions

@matthewevans
matthewevans added this pull request to the merge queue Aug 9, 2026
Merged via the queue into main with commit 7b17f21 Aug 9, 2026
13 checks passed
@matthewevans
matthewevans deleted the fix/sandbox-review-followup branch August 9, 2026 12:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant