Duel System — Agent Test Runbook
Duel System — Agent Test Runbook
This is a fully executable test suite for an agent with Chrome MCP connected. Read the entire Session Bootstrap section before running any test. Each test case is written as numbered steps you execute in order.
App URL: https://galraz.github.io/daumis-debt/
Chrome MCP Tool Reference
| Task | Tool |
|---|---|
| Navigate to URL | mcp__claude-in-chrome__navigate |
| Click / interact | mcp__claude-in-chrome__find + mcp__claude-in-chrome__computer |
| Run JavaScript | mcp__claude-in-chrome__javascript_tool |
| Take screenshot | mcp__claude-in-chrome__computer |
| Read page text | mcp__claude-in-chrome__get_page_text |
| Watch network | mcp__claude-in-chrome__read_network_requests |
Session Bootstrap
Run this once at the start of every testing session before any individual test. It collects the UIDs, week info, and current game — values you’ll reuse throughout.
Step B-1: Navigate and confirm login
Navigate to https://galraz.github.io/daumis-debt/. Take a screenshot. If a login screen is shown, stop and ask the user to sign in, then re-take the screenshot and confirm the main dashboard is visible.
Step B-2: Collect session state
Run in javascript_tool:
// Collect all values needed for tests
const user = firebase.auth().currentUser;
const now = new Date();
const d = new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate()));
const dayNum = d.getUTCDay() || 7;
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
const week = Math.ceil((((d - yearStart) / 86400000) + 1) / 7);
const year = now.getFullYear();
const seed = year * 100 + week;
// Get partner UID from app state
const { getPartnerUid, getUserName } = await import('./js/app.js');
const partnerUid = getPartnerUid();
// Compute this week's game
const { getWeeklyGame } = await import('./js/duel.js');
const thisWeeksGame = getWeeklyGame(seed);
console.log(JSON.stringify({
myUid: user.uid,
myEmail: user.email,
partnerUid,
year,
week,
seed,
thisWeeksGame,
isWeekday: now.getDay(), // 0=Sun, 3=Wed+
isDuelDay: now.getDay() >= 3
}));
Record the output. You will refer to MY_UID, PARTNER_UID, YEAR, WEEK, SEED, and THIS_WEEKS_GAME throughout the tests.
Step B-3: Override duel day gate (if not Wednesday or later)
If isDuelDay is false, run this once to patch the gate for this session:
// Patch isDuelDay in the loaded module — we override at the window level
// The duel module calls new Date().getDay() inline, so we mock Date
const RealDate = Date;
class MockDate extends RealDate {
getDay() { return 3; } // Wednesday
}
window.Date = MockDate;
console.log('isDuelDay override active');
Step B-4: Helper functions (paste once, reuse everywhere)
Run in javascript_tool to define helpers used in all tests:
// HELPER: delete this week's duel doc(s) — run before each test
window._cleanDuel = async () => {
const snap = await firebase.firestore().collection('duels')
.where('year', '==', YEAR).where('week', '==', WEEK).get();
const dels = snap.docs.map(d => d.ref.delete());
await Promise.all(dels);
console.log(`Deleted ${snap.docs.length} duel doc(s) for ${YEAR}W${WEEK}`);
};
// HELPER: read this week's duel doc
window._getDuel = async () => {
const snap = await firebase.firestore().collection('duels')
.where('year', '==', YEAR).where('week', '==', WEEK).get();
if (snap.empty) return null;
return { id: snap.docs[0].id, ...snap.docs[0].data() };
};
// HELPER: write a partial duel doc to simulate the other player's submission
window._seedDuel = async (data) => {
await firebase.firestore().collection('duels').add({
year: YEAR, week: WEEK, seed: SEED,
result: null, balanceAdjust: 0, favoredUser: null, playedAt: null,
...data
});
console.log('Seeded duel doc:', data);
};
// REPLACE these with values from Step B-2
const YEAR = /* from B-2 */ 2026;
const WEEK = /* from B-2 */ 14;
const SEED = /* from B-2 */ 202614;
const MY_UID = /* from B-2 */ 'paste-uid-here';
const PARTNER_UID = /* from B-2 */ 'paste-uid-here';
console.log('Helpers ready');
Important: Fill in YEAR, WEEK, SEED, MY_UID, PARTNER_UID from the B-2 output before running this block.
Multi-User Test Strategy
RPS and Lucky Number require two players. The app uses Google OAuth so you cannot be signed in as two users simultaneously in one Chrome instance.
Approach for all two-player tests: simulate the partner’s submission by writing directly to Firestore using _seedDuel() or by directly updating the duel doc. You are always signed in as User A (yourself). You inject User B’s (the partner’s) moves via Firestore, then observe what A’s UI does.
Known Issues Reference
| ID | File | Issue |
|---|---|---|
| KI-1 | coin-flip.js:18 | Uses Math.random() not the week seed — result not reproducible without override |
| KI-2 | wheel.js:64 | Same — Math.random() for spin result |
| KI-3 | wheel.js:98-100 | favoredUser falls back to string 'partner' if getPartnerUid() returns null |
| KI-4 | rps.js:97-104 | Second player uses duelDocRef.update() directly, bypassing notifyPartner |
| KI-5 | rps.js / lucky-number.js | First player (A) never sees the result if B resolved — only “Duel already played this week!” |
| KI-6 | lucky-number.js:9 | Target is computable from the public seed before either player picks |
| KI-7 | scratch-card.js:16 | Result value is in the DOM before scratching, only hidden by canvas overlay |
Game 1: Coin Flip
Mechanic: Single player. Math.random() < 0.5 → heads (you win $10) / tails (partner wins $10). Fires recordDuelResult after 1000ms animation.
CF-1: Heads — you win
- Run
await _cleanDuel(). - Inject
Math.randomoverride:Math.random = () => 0.3; // < 0.5 → heads - Navigate to the duel screen (click the duel button in the app nav).
- Confirm game title shows “Coin Flip”. Take screenshot.
- Click “Flip!”.
- Wait 1500ms:
await new Promise(r => setTimeout(r, 1500)); - Take screenshot. Confirm coin shows “H” and result text contains “Heads! You win $10!”.
- Restore
Math.random:delete Math.random; - Read Firestore:
const d = await _getDuel(); console.log(JSON.stringify(d)); - Assert:
d.favoredUser === MY_UID,d.balanceAdjust === 10,d.result.side === 'heads'. - Assert: Button text is “Done!” — run:
console.log(document.getElementById('btn-flip')?.textContent, document.getElementById('btn-flip')?.disabled);Expected:
"Done!" true.
CF-2: Tails — you lose
- Run
await _cleanDuel(). Math.random = () => 0.7; // >= 0.5 → tails- Navigate to duel screen. Click “Flip!”.
- Wait 1500ms.
- Take screenshot. Confirm “Tails! You lose $10.”
delete Math.random;- Read Firestore. Assert:
d.favoredUser === PARTNER_UID,d.balanceAdjust === 10,d.result.side === 'tails'. - Assert (KI-3):
d.favoredUseris a real UID string, not the literal string'partner'. If it equals'partner', log this as a bug.
CF-3: Double-click prevention
- Run
await _cleanDuel(). Math.random = () => 0.3;- Navigate to duel screen.
- Click “Flip!” twice in rapid succession (use
findto click, then immediately click again). - Wait 1500ms.
delete Math.random;- Query all duel docs for the week:
const snap = await firebase.firestore().collection('duels') .where('year', '==', YEAR).where('week', '==', WEEK).get(); console.log('Doc count:', snap.docs.length); - Assert: Exactly 1 document.
Game 2: Wheel of Fortune
Mechanic: Single player. Math.random() picks from 6 slices (index 0–5): -$10, -$5, $0, $0, +$5, +$10. Canvas animates for ~3000ms then fires recordDuelResult.
Slice mapping: | Math.random() value | Math.floor(v * 6) | Slice | Value | |—|—|—|—| | 0.01 | 0 | -$10 | -10 | | 0.18 | 1 | -$5 | -5 | | 0.35 | 2 | $0 | 0 | | 0.52 | 3 | $0 | 0 | | 0.69 | 4 | +$5 | 5 | | 0.9 | 5 | +$10 | 10 |
Note: Math.random() is called twice per spin — once for resultIndex, once for spins in the animation. Override must return the target value consistently or use a counter:
let _callCount = 0;
Math.random = () => { _callCount++; return _callCount === 1 ? TARGET_VALUE : 0.5; };
WH-1: Positive result (+$10)
- Run
await _cleanDuel(). let _c = 0; Math.random = () => { _c++; return _c === 1 ? 0.9 : 0.5; };- Navigate to duel screen. Confirm “Wheel of Fortune” subtitle.
- Take screenshot — confirm canvas with 6 labeled slices is visible.
- Click “Spin!”.
- Wait 3500ms:
await new Promise(r => setTimeout(r, 3500)); delete Math.random;- Take screenshot. Confirm result text contains “+$10 — you win!”.
- Read Firestore. Assert:
d.favoredUser === MY_UID,d.balanceAdjust === 10,d.result.value === 10.
WH-2: Negative result (-$10)
- Run
await _cleanDuel(). let _c = 0; Math.random = () => { _c++; return _c === 1 ? 0.01 : 0.5; };- Navigate to duel screen. Click “Spin!”. Wait 3500ms.
delete Math.random; - Take screenshot. Confirm “-$10 — you lose!”.
- Read Firestore. Assert:
d.favoredUser === PARTNER_UID,d.balanceAdjust === 10. - Assert (KI-3):
d.favoredUseris a real UID, not the string'partner'.
WH-3: Zero result ($0)
- Run
await _cleanDuel(). let _c = 0; Math.random = () => { _c++; return _c === 1 ? 0.35 : 0.5; };- Navigate to duel screen. Click “Spin!”. Wait 3500ms.
delete Math.random; - Read Firestore. Assert:
d.favoredUser === null,d.balanceAdjust === 0.
WH-4: Spin button disabled during animation
- Run
await _cleanDuel(). Math.random = () => 0.9;- Navigate to duel screen. Click “Spin!”.
- Immediately check button state (before 3s elapses):
const btn = document.getElementById('btn-spin'); console.log('disabled:', btn.disabled); - Assert:
disabled === true. - Wait 3500ms.
delete Math.random;
Game 3: Rock Paper Scissors
Mechanic: Two-player. Choices: rock ✊, paper ✋, scissors ✌️. BEATS = { rock: 'scissors', paper: 'rock', scissors: 'paper' }. $10 stake.
For all RPS tests, you are signed in as User A (yourself). User B’s moves are injected via Firestore.
RPS-1: A goes first — waiting state
- Run
await _cleanDuel(). - Navigate to duel screen. Confirm “Rock Paper Scissors”.
- Click the rock choice (
data-choice="rock"):document.querySelector('.rps-choice[data-choice="rock"]').click(); - Wait 500ms.
- Read Firestore:
const d = await _getDuel(); console.log(JSON.stringify(d)); - Assert:
d.result === null,d.submissions[MY_UID] === 'rock'. - Read page text. Assert: Page contains “Waiting” and shows a Refresh button.
RPS-2: A sees “partner has played” on refresh
Continues from RPS-1 (doc has submissions: { [MY_UID]: 'rock' }).
- Simulate B submitting (inject into existing doc):
const snap = await firebase.firestore().collection('duels') .where('year', '==', YEAR).where('week', '==', WEEK).get(); const ref = snap.docs[0].ref; await ref.update({ submissions: { [MY_UID]: 'rock', [PARTNER_UID]: 'scissors' } }); console.log('B submission injected'); - Click the Refresh button:
document.getElementById('btn-refresh').click(); - Wait 500ms. Take screenshot.
- Read page text. Assert: Page contains “[partner name] has played! Your turn.” and all 3 choice buttons are visible.
RPS-3: A resolves — A wins (rock beats scissors)
Continues from RPS-2 (doc has submissions: { [MY_UID]: 'rock', [PARTNER_UID]: 'scissors' }).
- Click rock:
document.querySelector('.rps-choice[data-choice="rock"]').click(); - Wait 500ms. Take screenshot.
- Read page text. Assert: Contains “You win!” and “✊ beats ✌️”.
- Read Firestore. Assert:
d.favoredUser === MY_UID,d.balanceAdjust === 10,d.result[MY_UID] === 'rock',d.result[PARTNER_UID] === 'scissors'. - Assert: Choice buttons are no longer clickable:
const choices = document.querySelectorAll('.rps-choice'); console.log('pointerEvents:', [...choices].map(c => c.style.pointerEvents));Expected: all
'none'.
RPS-4: Tie
- Run
await _cleanDuel(). - Inject B’s submission first:
await _seedDuel({ game: 'Rock Paper Scissors', submissions: { [PARTNER_UID]: 'scissors' } }); - Navigate to duel screen. Wait 500ms.
- Read page text. Confirm “[partner] has played! Your turn.”
- Click scissors:
document.querySelector('.rps-choice[data-choice="scissors"]').click(); - Wait 500ms.
- Read Firestore. Assert:
d.favoredUser === null,d.balanceAdjust === 0. - Read page text. Assert: Contains “Tie!” and “No change”.
RPS-5: Already played guard
- Run
await _cleanDuel(). - Inject a completed duel doc:
await _seedDuel({ game: 'Rock Paper Scissors', submissions: { [MY_UID]: 'rock', [PARTNER_UID]: 'paper' }, result: { [MY_UID]: 'rock', [PARTNER_UID]: 'paper' }, favoredUser: PARTNER_UID, balanceAdjust: 10 }); - Navigate to duel screen. Wait 500ms.
- Read page text. Assert: Contains “Duel already played this week!”.
- Assert: No
.rps-choiceelements in DOM:console.log('choices:', document.querySelectorAll('.rps-choice').length);Expected:
0.
RPS-6: Refresh re-polls (still waiting)
- Run
await _cleanDuel(). - Inject doc with only A’s submission:
await _seedDuel({ game: 'Rock Paper Scissors', submissions: { [MY_UID]: 'paper' } }); - Navigate to duel screen. Wait 500ms. Confirm waiting state shown.
- Click Refresh:
document.getElementById('btn-refresh').click(); - Wait 500ms. Read page text.
- Assert: Still shows waiting message (B still hasn’t submitted). No choice grid.
RPS-7: A sees “already played” after B resolved — UX gap (KI-5)
- Run
await _cleanDuel(). - Inject doc with A’s submission:
await _seedDuel({ game: 'Rock Paper Scissors', submissions: { [MY_UID]: 'rock' } }); - Navigate to duel screen. Confirm waiting state.
- Now simulate B resolving by updating the doc to a final state:
const snap = await firebase.firestore().collection('duels') .where('year', '==', YEAR).where('week', '==', WEEK).get(); await snap.docs[0].ref.update({ submissions: { [MY_UID]: 'rock', [PARTNER_UID]: 'paper' }, result: { [MY_UID]: 'rock', [PARTNER_UID]: 'paper' }, favoredUser: PARTNER_UID, balanceAdjust: 10 }); console.log('B resolved'); - Click Refresh. Wait 500ms. Take screenshot.
- Read page text. Assert: Shows “Duel already played this week!” — no win/loss breakdown visible.
- Log as KI-5: A played first but never gets to see the result.
RPS-8: B goes first — A resolves and wins
- Run
await _cleanDuel(). - Inject B’s submission as the first doc:
await _seedDuel({ game: 'Rock Paper Scissors', submissions: { [PARTNER_UID]: 'scissors' } }); - Navigate to duel screen. Wait 500ms.
- Read page text. Assert: “[partner] has played! Your turn.” preamble.
- Click rock:
document.querySelector('.rps-choice[data-choice="rock"]').click(); - Wait 500ms. Take screenshot.
- Read Firestore. Assert:
d.favoredUser === MY_UID,d.balanceAdjust === 10. - Assert (KI-4): Check network for EmailJS — run before clicking in a fresh test:
// Start watching network, then perform the action // After action: mcp__claude-in-chrome__read_network_requests // Assert: no request to api.emailjs.comUse
mcp__claude-in-chrome__read_network_requestsand confirm noemailjs.comrequest fires.
RPS-9: A loses to B
- Run
await _cleanDuel(). - Inject B’s submission:
await _seedDuel({ game: 'Rock Paper Scissors', submissions: { [PARTNER_UID]: 'paper' } }); - Navigate to duel screen. Click rock (paper beats rock → A loses).
- Wait 500ms. Take screenshot.
- Read Firestore. Assert:
d.favoredUser === PARTNER_UID,d.balanceAdjust === 10. - Read page text. Assert: Contains “You lose!” and “✋ beats ✊”.
Game 4: Lucky Number
Mechanic: Two-player. Target 1–10 determined by seededRandom(seed * 7 + 31). Closest pick wins $10.
Step: Pre-compute this week’s target
Run once at the start of Lucky Number tests:
function seededRandom(seed) {
return function() {
seed |= 0; seed = seed + 0x6D2B79F5 | 0;
let t = Math.imul(seed ^ seed >>> 15, 1 | seed);
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
return ((t ^ t >>> 14) >>> 0) / 4294967296;
};
}
const rng = seededRandom(SEED * 7 + 31);
const TARGET = Math.floor(rng() * 10) + 1;
console.log('Lucky Number TARGET:', TARGET);
Record TARGET. All LN tests reference it.
LN-1: A goes first — waiting state
- Run
await _cleanDuel(). - Navigate to duel screen. Confirm “Lucky Number”.
- Click the button for number 4 (arbitrary pick far from target):
document.querySelector('.number-btn[data-num="4"]').click(); - Wait 500ms.
- Read Firestore. Assert:
d.result === null,d.submissions[MY_UID] === 4. - Read page text. Assert: Contains “You picked 4. Waiting”.
LN-2: A wins (closer to target)
Choose MY_PICK to be 1 step from TARGET and B_PICK to be 4+ steps away. Example: if TARGET=7, MY_PICK=6, B_PICK=1.
- Run
await _cleanDuel(). - Compute picks based on TARGET:
const MY_PICK = TARGET === 1 ? 2 : TARGET - 1; // 1 away const B_PICK = TARGET >= 6 ? 1 : 10; // far away console.log('MY_PICK:', MY_PICK, 'B_PICK:', B_PICK); - Inject B’s submission:
await _seedDuel({ game: 'Lucky Number', submissions: { [PARTNER_UID]: B_PICK } }); - Navigate to duel screen. Confirm “[partner] has picked! Your turn.”
- Click MY_PICK:
document.querySelector(`.number-btn[data-num="${MY_PICK}"]`).click(); - Wait 500ms. Take screenshot.
- Read page text. Assert: Contains “You win!” and
Target: ${TARGET}. - Read Firestore. Assert:
d.favoredUser === MY_UID,d.balanceAdjust === 10,d.result.target === TARGET.
LN-3: A loses (B is closer)
- Run
await _cleanDuel(). const B_PICK = TARGET; // B picks exactly the target — distance 0 const MY_PICK = TARGET >= 6 ? 1 : 10; // far away- Inject B’s submission:
await _seedDuel({ game: 'Lucky Number', submissions: { [PARTNER_UID]: B_PICK } }); - Navigate to duel screen. Click MY_PICK.
- Wait 500ms. Take screenshot.
- Read Firestore. Assert:
d.favoredUser === PARTNER_UID,d.balanceAdjust === 10. - Read page text. Assert: Contains partner’s name and “wins!”.
LN-4: Tie (equal distance)
- Run
await _cleanDuel(). - Pick two numbers equidistant from TARGET:
// If TARGET=5: picks 3 and 7 (both distance 2) const T = TARGET; const B_PICK = T > 1 ? T - 2 : T + 2; const MY_PICK = T < 10 ? T + 2 : T - 2; // Ensure both are in range 1-10 console.log('T:', T, 'B_PICK:', B_PICK, 'MY_PICK:', MY_PICK);If B_PICK or MY_PICK is out of range, adjust to nearest equidistant pair.
- Inject B’s submission:
await _seedDuel({ game: 'Lucky Number', submissions: { [PARTNER_UID]: B_PICK } }); - Navigate to duel screen. Click MY_PICK.
- Wait 500ms.
- Read Firestore. Assert:
d.favoredUser === null,d.balanceAdjust === 0. - Read page text. Assert: Contains “equally close” and “No change”.
LN-5: Target button highlighted after reveal
Reuse the state after any of LN-2 or LN-3 resolving.
- After resolution, run:
const targetBtn = document.querySelector('.number-btn.target'); console.log('target btn num:', targetBtn?.dataset.num, 'expected:', TARGET); - Assert:
parseInt(targetBtn.dataset.num) === TARGET.
LN-6: A sees “already played” after B resolved — UX gap (KI-5)
- Run
await _cleanDuel(). - Inject A’s submission:
await _seedDuel({ game: 'Lucky Number', submissions: { [MY_UID]: 5 } }); - Navigate to duel screen. Confirm waiting state.
- Simulate B resolving the duel:
const snap = await firebase.firestore().collection('duels') .where('year', '==', YEAR).where('week', '==', WEEK).get(); await snap.docs[0].ref.update({ submissions: { [MY_UID]: 5, [PARTNER_UID]: TARGET }, result: { target: TARGET, [MY_UID]: 5, [PARTNER_UID]: TARGET }, favoredUser: PARTNER_UID, balanceAdjust: 10 }); - Click Refresh. Wait 500ms. Take screenshot.
- Read page text. Assert: Contains “Duel already played this week!”. No result breakdown shown.
- Log as KI-5.
LN-7: B goes first — A resolves
- Run
await _cleanDuel(). - Inject B’s submission:
const B_PICK = TARGET >= 6 ? 1 : 10; await _seedDuel({ game: 'Lucky Number', submissions: { [PARTNER_UID]: B_PICK } }); - Navigate to duel screen. Read page text. Assert: “[partner] has picked! Your turn.”
- Click
TARGET(pick the exact target to guarantee a win):document.querySelector(`.number-btn[data-num="${TARGET}"]`).click(); - Wait 500ms.
- Read Firestore. Assert:
d.favoredUser === MY_UID.
LN-8: Same pick — exact tie
- Run
await _cleanDuel(). await _seedDuel({ game: 'Lucky Number', submissions: { [PARTNER_UID]: TARGET } });- Navigate to duel screen. Click TARGET:
document.querySelector(`.number-btn[data-num="${TARGET}"]`).click(); - Wait 500ms.
- Read Firestore. Assert:
d.favoredUser === null,d.balanceAdjust === 0. - Read page text. Assert: Contains “equally close” (both distance 0).
Game 5: Scratch Card
Mechanic: Single player. netAdjust from [-10, -5, 0, 5, 10] is seeded. Value hidden under canvas. Reveal fires when > 40% of canvas pixels are cleared.
Step: Pre-compute this week’s scratch card result
function seededRandom(seed) {
return function() {
seed |= 0; seed = seed + 0x6D2B79F5 | 0;
let t = Math.imul(seed ^ seed >>> 15, 1 | seed);
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
return ((t ^ t >>> 14) >>> 0) / 4294967296;
};
}
const VALUES = [-10, -5, 0, 5, 10];
const rng = seededRandom(SEED * 13 + 7);
const SC_RESULT = VALUES[Math.floor(rng() * VALUES.length)];
console.log('Scratch Card result this week:', SC_RESULT);
Record SC_RESULT.
SC-1: Partial scratch — no trigger
- Run
await _cleanDuel(). - Navigate to duel screen. Confirm “Scratch Card”.
- Scratch a tiny area via synthetic mouse events (small circle, well under 40%):
const canvas = document.getElementById('scratch-canvas'); const rect = canvas.getBoundingClientRect(); // Scratch just 3 pixels in the top-left corner canvas.dispatchEvent(new MouseEvent('mousedown', { clientX: rect.left + 5, clientY: rect.top + 5, bubbles: true })); canvas.dispatchEvent(new MouseEvent('mousemove', { clientX: rect.left + 6, clientY: rect.top + 5, bubbles: true })); canvas.dispatchEvent(new MouseEvent('mouseup', { bubbles: true })); - Wait 300ms.
- Read Firestore. Assert:
(await _getDuel()) === null— no doc created yet. - Assert: Hint text still visible:
console.log(document.getElementById('scratch-hint')?.textContent);Expected: “Drag or tap to scratch”.
SC-2: Full scratch — reveal triggers
- Continuing from SC-1 (same session, same page).
- Scratch across the full canvas:
const canvas = document.getElementById('scratch-canvas'); const rect = canvas.getBoundingClientRect(); const ctx = canvas.getContext('2d'); // Use destination-out to clear the whole canvas directly ctx.globalCompositeOperation = 'destination-out'; ctx.fillStyle = 'rgba(0,0,0,1)'; ctx.fillRect(0, 0, 200, 140); // Manually fire the pixel check by dispatching a mousemove canvas.dispatchEvent(new MouseEvent('mousemove', { clientX: rect.left + 100, clientY: rect.top + 70, bubbles: true })); - Wait 500ms. Take screenshot.
- Read Firestore. Assert: Doc exists with correct
d.result.netAdjust === SC_RESULT. - Read page text. Assert:
- If
SC_RESULT > 0: contains “you win!” in page. - If
SC_RESULT < 0: contains “you lose!”. - If
SC_RESULT === 0: contains “no change”.
- If
SC-3: Value in DOM before scratching (KI-7)
- Run
await _cleanDuel(). - Navigate to duel screen. Before any scratching, read the hidden value:
const val = document.getElementById('scratch-value')?.textContent.trim(); console.log('Value in DOM:', val, 'Expected:', SC_RESULT >= 0 ? `+$${SC_RESULT}` : `$${SC_RESULT}`); - Take screenshot. Assert visually: The value text is NOT visible (canvas covers it).
- Log as KI-7: The result is readable from the DOM before any interaction.
SC-4: Correct favoredUser for negative result
Run this only if SC_RESULT < 0 (otherwise skip or use a seed override — see note below).
- Run
await _cleanDuel(). - Navigate to duel screen. Perform full scratch (step SC-2).
- Wait 500ms.
- Read Firestore. Assert:
d.favoredUser === PARTNER_UIDand it is a real UID, not the string'partner'(KI-3).
Note on seed override for specific SC values: To force a specific netAdjust when the week’s natural result doesn’t match, the seeded RNG runs at module load time — you cannot override it via Math.random. Instead, test whatever the week naturally produces and note it. To test a specific value in isolation, directly call recordDuelResult and inspect Firestore manually.
Game Selection Tests
GS-1: Determinism
const { getWeeklyGame } = await import('./js/duel.js');
const results = Array.from({ length: 5 }, () => getWeeklyGame(SEED));
console.log('All same?', new Set(results).size === 1, results);
Assert: All 5 results are identical.
GS-2: All games reachable across a year
const { getWeeklyGame } = await import('./js/duel.js');
const seen = new Set();
for (let w = 1; w <= 52; w++) seen.add(getWeeklyGame(2026 * 100 + w));
console.log('Games seen:', [...seen]);
console.log('All 5 covered?', seen.size === 5);
Assert: All 5 game IDs appear: coin-flip, wheel, rps, lucky-number, scratch-card.
GS-3: Both players see the same game
// Compute seed independently and confirm same game
const { getWeeklyGame, getCurrentWeekInfo } = await import('./js/duel.js');
const { seed } = getCurrentWeekInfo();
console.log('Game from seed:', getWeeklyGame(seed));
Compare this output with the thisWeeksGame from Step B-2. Assert: Identical.
Balance Impact Tests
BI-1: Duel win increases balance
- Read current balance from the page (navigate to dashboard and note the displayed balance in the consolidation currency).
- Run a solo game (Coin Flip, CF-1) and win.
- Navigate back to dashboard.
- Assert: Balance has shifted by $10 × exchange rate in the consolidation currency.
BI-2: Tie has no balance impact
- Note current balance.
- Complete RPS-4 (tie).
- Navigate to dashboard.
- Assert: Balance unchanged.
BI-3: Duel record in Insights
- Complete any duel.
- Navigate to Insights.
- Read page text. Assert: “Duel record” row shows updated win count.
Test Run Log Template
After completing each test, record:
| Test | Result | Notes |
|------|--------|-------|
| CF-1 | PASS/FAIL | |
| CF-2 | PASS/FAIL | |
| CF-3 | PASS/FAIL | |
| CF-4 | PASS/FAIL | |
| WH-1 | PASS/FAIL | |
| WH-2 | PASS/FAIL | |
| WH-3 | PASS/FAIL | |
| WH-4 | PASS/FAIL | |
| RPS-1 | PASS/FAIL | |
| RPS-2 | PASS/FAIL | |
| RPS-3 | PASS/FAIL | |
| RPS-4 | PASS/FAIL | |
| RPS-5 | PASS/FAIL | |
| RPS-6 | PASS/FAIL | |
| RPS-7 | PASS/FAIL | KI-5 expected |
| RPS-8 | PASS/FAIL | |
| RPS-9 | PASS/FAIL | |
| LN-1 | PASS/FAIL | |
| LN-2 | PASS/FAIL | |
| LN-3 | PASS/FAIL | |
| LN-4 | PASS/FAIL | |
| LN-5 | PASS/FAIL | |
| LN-6 | PASS/FAIL | KI-5 expected |
| LN-7 | PASS/FAIL | |
| LN-8 | PASS/FAIL | |
| SC-1 | PASS/FAIL | |
| SC-2 | PASS/FAIL | |
| SC-3 | PASS/FAIL | KI-7 expected |
| SC-4 | PASS/FAIL | skip if SC_RESULT >= 0 |
| GS-1 | PASS/FAIL | |
| GS-2 | PASS/FAIL | |
| GS-3 | PASS/FAIL | |
| BI-1 | PASS/FAIL | |
| BI-2 | PASS/FAIL | |
| BI-3 | PASS/FAIL | |
