// Round forty-two: turned over, for real. Round twenty-seven gave the phone // a landscape layout and proved it in headless Chrome behind a viewport // override; no real device has ever been rotated, and the phone walk refuses // a `{ view: [...] }` step on purpose — a device's orientation is not the // step list's to set. That refusal is right as a default and wrong as a // permanent state, because the one thing the mock cannot tell you is what // Android itself does: the angle (an override without `screenOrientation` // leaves screen.orientation.angle at 0, so 90 is a fact the mock physically // cannot fake), the real safe-area insets around the cutout and the gesture // pill, whether the activity survived the configuration change instead of // being recreated, and whether the audio kept going through it. So: a // rotation block in the phone walk's TAIL rather than in its step list — // because the two hard requirements, "restore even if an assertion fails" // and "skip cleanly rather than fail", are exactly what the tail's phases // already do and what a step array cannot — reading both rotation settings // before writing either, and putting them back from four places. Nothing in // it downloads, joins, sends, signs in, or writes to the config or the // library. No src/, no CSS, no Rust, no Kotlin: the layout was already // written, this round only proves it on the device. // // It also makes the `{ adb }` step keep its stdout, which it has always // discarded: `am start` prints "Error: Activity not started, unable to // resolve Intent" on STDOUT and exits 0, so `share-import` — the one step // that exists to prove an intent resolves — could not fail for its own // reason. Those three edits (4/5/6 below) are separable from the rotation // block and can be dropped whole if a sibling round gets there first. const fs = require('fs'); const root = process.env.SS_ROOT || 'F:/Coding/SoulStream/'; const scratch = 'C:/Users/Kyle/AppData/Local/Temp/claude/F--Coding-SoulStream/5488514a-11e8-482a-b0a0-20cb511ee110/scratchpad/'; const ins = (name) => fs.readFileSync(scratch + name, 'utf8').replace(/\s+$/, ''); let p, s; const load = (file) => { p = file; s = fs.readFileSync(root + p, 'utf8'); }; const save = () => fs.writeFileSync(root + p, s); const once = (a) => { const i = s.indexOf(a); if (i < 0) throw new Error('missing in ' + p + ': ' + a.slice(0, 90)); if (s.indexOf(a, i + 1) >= 0) throw new Error('ambiguous in ' + p + ': ' + a.slice(0, 90)); return i; }; const rep = (a, b) => { once(a); s = s.replace(a, () => b); }; const NL = String.fromCharCode(10); /* Two of the inserts below re-emit their own anchor, so a second run would double-write where `once` cannot see it coming and only throw further down, leaving half a tree. Refuse one here, before anything is touched: `rotationRestore` appears nowhere in the repo today and is the first name this round writes. */ if (fs.readFileSync(root + 'tests/mobile/smoke.mjs', 'utf8').includes('rotationRestore')) { throw new Error('round 42 is already applied in ' + root + ' — reset the tree rather than re-running'); } /* The phase carries its own reading as a template literal (PLAYER, STREAM, HERO, CAT, NET and BRW are all local literals — PROBE is deliberately left alone, because sibling rounds append their fields to it). A raw backtick inside that literal would end it and what follows becomes a tagged template: the file still parses, `node --check` says nothing at all, and smoke.mjs throws the moment it is imported. `${` is the same trap the other way. Both checked here, across the ROT body only — the rest of the phase is ordinary code and uses both legitimately. */ { const phase = ins('ins-r42-phase.txt'); const TICK = String.fromCharCode(96); const open = phase.indexOf('const ROT = ' + TICK); if (open < 0) throw new Error('ins-r42-phase.txt no longer opens the ROT literal the guard knows about'); const close = phase.indexOf(TICK, open + 13); if (close < 0) throw new Error('ins-r42-phase.txt never closes the ROT literal'); const body = phase.slice(open + 13, close); if (body.includes('${')) throw new Error('the ROT body carries a ${ — it lives inside a template literal'); if (!/\)\(\)$/.test(body.trim())) throw new Error('the ROT body does not end in an invoked function — the literal closed early'); } // ── the walk ────────────────────────────────────────────────────────── load('tests/mobile/smoke.mjs'); /* EDIT 1 — the helpers, beside shadeNotice / mediaSessionState / audioUnderruns, which are the precedent for shelling out and parsing. They are at module scope because the restore latch has to be reachable from main's finally and from a process handler, neither of which can see inside main. */ rep(`/* The audio mixer's underrun count for the app's fast track: a dropout the`, ins('ins-r42-helpers.txt') + NL + NL + `/* The audio mixer's underrun count for the app's fast track: a dropout the`); /* EDIT 2 — the block itself, in the phone-only tail, between the `background` check (which has just proved playback is live with the app off screen) and the network block (which is the skip idiom this one copies). Not a step: a step's adb array runs unconditionally and an expectation cannot skip itself, and this round needs both. */ rep(` /* ── the network: a search, then a peer's copy played as it`, ins('ins-r42-phase.txt') + NL + NL + ` /* ── the network: a search, then a peer's copy played as it`); /* EDIT 3 — main's own finally. A throw out of the step loop or out of the tail lands here, and today it restores nothing; this is the path that would otherwise leave the phone sideways. The latch clears itself before it writes, so the block's own finally having already restored makes this a no-op. */ rep(` if (cdp) cdp.close();`, ` const rotLeft = rotationRestore(); if (rotLeft !== true && rotLeft !== null) problems.push("ROTATION could not put the phone's rotation settings back: " + rotLeft); if (cdp) cdp.close();`); /* EDIT 4 — the `{ adb }` step keeps its stdout. `adb()` already returns it and throws only on a non-zero status, and `am start` answers "Error: Activity not started, unable to resolve Intent" on stdout with status 0 — so a discarded stdout is exactly why the share-sheet step cannot fail for the one reason it exists. */ rep(` if (s.adb) { try { adb(...s.adb); } catch (e) { problems.push("ADB " + (s.name || s.adb.join(" ")) + ": " + e.message); } }`, ` let adbOut = ""; if (s.adb) { /* kept, not discarded: \`am start\` prints "Error: Activity not started, unable to resolve Intent" on STDOUT and still exits 0, so a step's own expectation is the only thing that can tell a resolved intent from an unresolved one. It reaches the expectation as \`adbOut\` below. */ try { adbOut = adb(...s.adb).toString().trim(); } catch (e) { problems.push("ADB " + (s.name || s.adb.join(" ")) + ": " + e.message); } }`); /* EDIT 5 — and reaches the expectation, before the `probe` merge so a probe could still override it */ rep(` const info = await cdp.evaluate(PROBE);`, ` const info = await cdp.evaluate(PROBE); if (adbOut) info.adbOut = adbOut;`); /* EDIT 6 — the one place it bites today. This is PHONE_EXPECT's share-import (the mock's own, five lines up the file, is a different sentence and is left alone): the intent has to have RESOLVED, not merely have been sent to an activity that is not there. */ rep(` "share-import": (i) => i.title.startsWith("Import") && (i.imp > 0 || i.impErr),`, ` "share-import": (i) => i.title.startsWith("Import") && (i.imp > 0 || i.impErr) && !/^Error/m.test(i.adbOut || ""),`); save(); // ── the doc ─────────────────────────────────────────────────────────── load('ANDROID.md'); /* the refusal sentence is WRAPPED across two lines — a one-line grep for it finds nothing, which is the trap a careless patch falls into. Kept verbatim; the phone's own block is described after it. */ rep(`the phone walk refuses one, because a real device's orientation is not the harness's to set.`, ins('ins-r42-doc-walk.txt')); /* the paragraph at the top of the doc that has asserted, in prose, exactly what no one had tested. The anchor runs to the end of the sheet sentence and the insert re-emits all of it: a shorter anchor leaves the paragraph wrapped around a 60-character orphan where the new sentence ends. */ rep(`layout is the only thing that has to answer. Two media queries do that. The player sheet is the one surface whose portrait shape cannot survive a wide viewport: its artwork is a square sized by WIDTH, so at 915×411 the cover came out 879px tall and the title, the lane, the transport and the face buttons sat`, ins('ins-r42-doc-layout.txt')); /* "Not done yet" is left exactly as it is: it has no rotation entry to close, and two rounds have already collided on the step COUNTS in the sentences above it ("Fourteen mock steps", "Fifteen more mock steps", "Nine mock steps"). This round changes none of them — it adds no step to either list. */ save(); console.log('round 42 wired');