diff --git a/system/tests/oskiewar-practice.browser.mjs b/system/tests/oskiewar-practice.browser.mjs
new file mode 100644
index 0000000000..2ecf416bab
--- /dev/null
+++ b/system/tests/oskiewar-practice.browser.mjs
@@ -0,0 +1,38 @@
+// Full game renderer regression on Poorslice. Local changed assets over the
+// deployed shell; synthetic appearance, no model call or account mutation.
+import assert from 'node:assert/strict';
+import { readFile, mkdir } from 'node:fs/promises';
+import puppeteer from 'puppeteer';
+const dir = process.env.OSKIEWAR_SHOTS || '/tmp/oskiewar-practice-browser';
+await mkdir(dir, {recursive:true});
+const browser = await puppeteer.launch({headless:true, executablePath:process.env.CHROME_BIN || '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'});
+const page = await browser.newPage(), errors = [], sockets = [], replays = [];
+page.on('pageerror', e => errors.push(e.message));
+const cdp = await page.createCDPSession(); await cdp.send('Network.enable');
+cdp.on('Network.webSocketCreated', e => sockets.push(e.url));
+await page.setRequestInterception(true);
+page.on('request', async request => {
+ const url = new URL(request.url());
+ if (url.pathname.includes('oskiewar-replays') && request.method() === 'POST') replays.push(url.pathname);
+ let file;
+ if (url.origin === 'https://oskiewar.com') {
+ if (url.pathname === '/') file = 'mac-test.html';
+ else if (['/oskiewar.js','/oskiewar-fighter.mjs','/oskiewar-wizard.mjs'].includes(url.pathname)) file = url.pathname.slice(1);
+ }
+ if (file) await request.respond({status:200,contentType:file.endsWith('html')?'text/html':'text/javascript',body:await readFile(new URL('../../xbox/live/'+file,import.meta.url))});
+ else await request.continue();
+});
+try {
+ await page.setViewport({width:1200,height:900});
+ await page.goto('https://oskiewar.com/?practice',{waitUntil:'domcontentloaded'});
+ await page.waitForFunction(()=>globalThis.__oskiewarTouch?.screen,{timeout:60000});
+ await page.evaluate(()=>{globalThis.__oskiewarFighterAppearance={handle:'@fixture',validUntil:Date.now()+600000,appearance:{skin:[217,164,126],hair:[54,42,34],shirt:[54,95,144],pants:[24,55,104],shoes:[235,235,235],hairStyle:'short',beard:false,glasses:false,sleeves:'long'}};});
+ await page.keyboard.down('Enter'); await new Promise(r=>setTimeout(r,300)); await page.keyboard.up('Enter');
+ await page.waitForFunction(()=>globalThis.__oskiewarTouch?.practiceFighter==='@fixture',{timeout:30000});
+ await page.screenshot({path:dir+'/practice.png'});
+ await page.evaluate(()=>{globalThis.__oskiewarFighterAppearance=null;});
+ await page.waitForFunction(()=>!globalThis.__oskiewarTouch?.practiceFighter);
+ assert.deepEqual(errors,[]); assert.deepEqual(sockets,[]); assert.deepEqual(replays,[]);
+ console.log('PASS: actual game renders generated appearance, clears it on withdrawal, and opens no game sockets or replay uploads.');
+} catch(error) { await page.screenshot({path:dir+'/failure.png'}); throw error; }
+finally { await browser.close(); }
diff --git a/system/tests/oskiewar-turnstile-live.mjs b/system/tests/oskiewar-turnstile-live.mjs
index 2d179082cb..8be052a146 100644
--- a/system/tests/oskiewar-turnstile-live.mjs
+++ b/system/tests/oskiewar-turnstile-live.mjs
@@ -71,11 +71,16 @@ try {
assert.equal(await page.$eval('#wizard-go',el=>el.textContent),'Accept & use in practice',await page.$eval('#wizard-note',el=>el.textContent));
assert.equal(await page.evaluate(()=>!!globalThis.__oskiewarFighterAppearance),false,'review cannot equip before acceptance');
await page.screenshot({path:dir+'/02-generated.png'});
- await page.click('#wizard-go');await wait(()=>!!globalThis.__oskiewarFighterAppearance);
+ assert.equal(await page.$eval('#wizard-card canvas',el=>getComputedStyle(el).position),'static');
+ await Promise.all([page.waitForNavigation({waitUntil:'domcontentloaded'}),page.click('#wizard-go')]);
+ await wait(()=>!!globalThis.__oskiewarFighterAppearance);
+ assert.ok(new URL(page.url()).searchParams.has('practice'));
const account=await post({action:'account'});assert.equal(account.status,200);assert.equal(account.body.status,'accepted');
assert.equal(account.body.handle.toLowerCase(),handle.toLowerCase());acceptedHash=account.body.fighter.hash;
await page.click('#wizard-back');await page.keyboard.press('Space');
await wait(()=>globalThis.__oskiewarTouch?.screen==='game');
+ await wait(()=>!!globalThis.__oskiewarTouch?.practiceFighter);
+ assert.equal(await page.evaluate(()=>globalThis.__oskiewarTouch.practiceFighter.toLowerCase()),handle.toLowerCase());
await page.keyboard.down('ArrowRight');await new Promise(r=>setTimeout(r,1000));await page.keyboard.up('ArrowRight');
assert.ok(await page.evaluate(()=>Array.isArray(globalThis.__oskiewarFighterAppearance?.appearance.skin)));
await page.screenshot({path:dir+'/03-local-practice.png'});
@@ -87,6 +92,7 @@ try {
assert.equal((await post({action:'status',hash:source,capability:grant.slice(0,-4)+'AAAA'})).status,403,'tampered capability refused');
await page.click('button::-p-text(Withdraw my material)');await wait(()=>document.querySelector('#wizard-note').textContent.startsWith('Withdrawn.'));
assert.equal(await page.evaluate(()=>!!globalThis.__oskiewarFighterAppearance),false);
+ await wait(()=>!globalThis.__oskiewarTouch?.practiceFighter);
assert.equal((await post({action:'account'})).body.status,'empty');
assert.equal((await post({action:'status',hash:source,capability:grant})).status,403,'withdrawn grant refuses old preview');
await page.screenshot({path:dir+'/05-withdrawn.png'});
diff --git a/system/tests/oskiewar-turnstile.browser.mjs b/system/tests/oskiewar-turnstile.browser.mjs
index 67cdd3050a..7a8b5c6987 100644
--- a/system/tests/oskiewar-turnstile.browser.mjs
+++ b/system/tests/oskiewar-turnstile.browser.mjs
@@ -13,7 +13,7 @@ let saved = false, denied = false, missingHandle = false, modelCalls = 0, accept
const result = status => ({status,handle:'@fixture',fighter,validUntil:Date.now()+600000});
const server = createServer(async (req,res) => {
const send = (body,status=200,type='application/json') => {res.writeHead(status,{'content-type':type});res.end(type==='application/json'?JSON.stringify(body):body);};
- if(req.url === '/') return send(``,200,'text/html');
@@ -47,6 +47,7 @@ try {
const upload=await page.$('input[type=file]');await upload.uploadFile(new URL('./oskiewar-generation.test.mjs',import.meta.url).pathname);
await page.click('#wizard-go');await wait(()=>document.querySelector('#wizard-go').textContent==='Accept & use in practice');
assert.equal(saved,false);assert.equal(await page.evaluate(()=>!!globalThis.__oskiewarFighterAppearance),false);
+ assert.equal(await page.$eval('#wizard-card canvas',el=>getComputedStyle(el).position),'static');
await page.screenshot({path:shotDir+'/01-review.png'});
await page.click('#wizard-go');await wait(()=>!!globalThis.__oskiewarFighterAppearance);
assert.equal(acceptedCalls,1);assert.equal(saved,true);assert.match(await note(),/@fixture/);
diff --git a/xbox/OSKIEWAR-RELEASE.md b/xbox/OSKIEWAR-RELEASE.md
index f3969a005c..f0e7319ddf 100644
--- a/xbox/OSKIEWAR-RELEASE.md
+++ b/xbox/OSKIEWAR-RELEASE.md
@@ -9,13 +9,16 @@ yourself restores it without another model call. Handle changes retain ownership
Withdrawal removes the account's saved fighter and source material. While equipped,
authority is checked every 15 seconds; a failed check clears the selection,
and its current expiry is also enforced at render time. Presentation is
-limited to local practice; online play uses the standard fighter. The REGARDE
+limited to local practice: acceptance opens `/?practice`, restores the accepted
+fighter against its live grant, and disables game broadcasts and replay uploads.
+Online play uses the standard fighter. The REGARDE
gate still signs with its documented demo key.
Run browser checks on Poorslice under Node 24:
```sh
node system/tests/oskiewar-turnstile.browser.mjs
+node system/tests/oskiewar-practice.browser.mjs
```
That browser regression uses synthetic API fixtures. A live test must also
diff --git a/xbox/live/mac-test.html b/xbox/live/mac-test.html
index 9e8f1e3001..c4e0db9e0b 100644
--- a/xbox/live/mac-test.html
+++ b/xbox/live/mac-test.html
@@ -41,8 +41,9 @@
// self-play casts all want the fronts they were built against.
{
const harness = new URLSearchParams(location.search);
+ globalThis.__oskiewarLocalPractice = harness.has("practice");
globalThis.__oskiewarVersusCapable = !harness.has("social-preview") &&
- !harness.has("replay-oven") && !harness.has("self-play") && !harness.has("workshop") && !harness.has("map");
+ !harness.has("practice") && !harness.has("replay-oven") && !harness.has("self-play") && !harness.has("workshop") && !harness.has("map");
}