docs: update-v86.js rewrite + SMB/v86/testing knowledge (#348)

* Rewrite update-v86.js for the current build pipeline

Both v86 fixes we've been carrying are now real branches on the fork
(PR #1540 electron-renderer-fs-loader, PR #1541 ide-shared-registers)
combined as felixrieseberg/v86:windows95-base. update-v86.js no longer
needs to patch sources at build time — it just builds whatever's checked
out and copies the result.

Gone: the fallback-to-copy.sh path, the skew-day check, the structural
regex patches for load_file/exportSymbol/fetch-bind, the phantom-slave
guard (both are in the branch), the --js-only flag. If you don't have
cargo/clang/java/closure, the script fails loudly — no silent fallbacks.

Added: sanity checks against the installed libv86.js for the invariants
our SMB integration and parcel-build shim depend on, so if upstream
changes something load-bearing we see it as a WARN at update time
instead of a runtime failure.

Tested end-to-end: 5/5 sanity checks, fresh boot SUCCESS in 32s.

* docs and skills: capture SMB/v86/testing knowledge from the session

- docs/smb-share.md: user-facing SMB integration overview (how to mount in
  Win95, what's implemented, what's not). Points at the protocol-level
  README inside src/renderer/smb/ for wire-level gotchas.
- .claude/skills/probe-win95: how to boot and test the VM without a human.
  Env vars, file locations, failure modes, the XT scancode keyboard trick,
  bisect rules of thumb.
- .claude/skills/update-v86: how to pull upstream v86 changes, what the
  five sanity-check WARNs mean, how to retire the fork branches when the
  PRs merge upstream.

.gitignore narrowed to exclude only the runtime dirs (scheduled_tasks.lock,
worktrees) instead of the whole .claude/ tree, so skills can be committed.
This commit is contained in:
Felix Rieseberg
2026-04-11 09:33:51 -07:00
committed by GitHub
parent 2ef06eb412
commit 89c0a8575d
6 changed files with 433 additions and 168 deletions

View File

@@ -0,0 +1,113 @@
---
name: probe-win95
description: Boot Windows 95 in Electron under Claude's control, without a human clicking anything. Use when testing v86 updates, SMB changes, keyboard input, boot stability, or bisecting regressions.
---
# Probing Windows 95 autonomously
You can run and test the Win95 VM yourself. The harness is already wired
up — three pieces:
| File | Role |
|---|---|
| `src/renderer/debug-harness.ts` | Activated by `WIN95_PROBE=1`. Boots fresh automatically, samples CPU + VGA + text screen every 5s, writes `/tmp/win95-probe.json` + `/tmp/win95-screen.png`, detects SUCCESS vs FAIL modes, optionally drives keyboard input. |
| `src/renderer/smb/index.ts` | Wraps `console.log` so `[smb]` and `[nbns]` lines tee to `$TMPDIR/windows95-smb.log` (outside Electron, readable by any polling script — no CDP needed). |
| `tools/probe-boot.sh` | One-shot: kill leftovers → parcel build → launch Electron → poll `/tmp/win95-probe.done` → report → kill. |
## One-shot boot test
```sh
tools/probe-boot.sh
```
Prints SUCCESS or a FAIL verdict. ~40s on a clean run.
## Boot + type into Run
```sh
pkill -9 -f "windows95.*electron"; sleep 2
rm -f "$HOME/Library/Application Support/windows95/state-v4.bin"
rm -f /tmp/win95-probe.json /tmp/win95-probe.done \
"$TMPDIR/windows95-smb.log"
WIN95_PROBE=1 \
WIN95_PROBE_SCRIPT='HOST/HOST' \
WIN95_SMB_SHARE="$HOME/Downloads" \
./node_modules/.bin/electron . > /tmp/win95-electron.log 2>&1 &
```
`WIN95_PROBE_SCRIPT='HOST/HOST'` types `\\HOST\HOST` into Start → Run on
desktop. `/``\` substitution (env var / shell quoting, pragmatism). The
harness drives it via XT scancodes — Win95 doesn't have Win+R (Win98+
only), so the sequence is Esc, Esc, Ctrl+Esc, R, backslashes + text,
Enter.
## Reading results
| File | What |
|---|---|
| `/tmp/win95-probe.json` | Live status: `phase` (`init`/`text-mode`/`splash`/`desktop`), `gfxW/H`, `textScreen`, `instructionDelta`, `verdict` |
| `/tmp/win95-probe.done` | Written once when verdict is decided |
| `/tmp/win95-screen.png` | Canvas screenshot, refreshed each tick |
| `$TMPDIR/windows95-smb.log` | SMB/NBNS protocol trace |
| `/tmp/win95-electron.log` | Electron stderr |
## Verdicts
| Verdict | Meaning | Action |
|---|---|---|
| `SUCCESS` | Canvas ≥640×480, CPU active, uptime >30s | desktop reached |
| `FAIL_VXDLINK` | "Invalid VxD dynamic link call" | flaky — retry |
| `FAIL_IOS` / `FAIL_PROTECTION` | IOS subsystem protection error | usually driver/BIOS mismatch |
| `FAIL_KRNL386` | "Cannot find KRNL386.EXE" in safe mode | disk reads returning garbage — wasm/BIOS drift |
| `FAIL_SPLASH_HANG` | Canvas stuck 320×400 for >70s | IRQ starvation — if you're on v86 master, check the IDE register fix |
| `FAIL_HUNG` | CPU stopped advancing or text screen frozen 40s | hard hang |
## Rules of the road
- **Sporadic bluescreens are normal** on all v86 versions. One FAIL_VXDLINK
or FAIL_HUNG doesn't prove anything — retry up to 3×.
- **Always clean state** (`state-v4.bin`) before a probe. `pkill` on a
wedged Electron triggers `onbeforeunload`, saving the *corrupted* state.
Deleting it forces fallback to `images/default-state.bin`.
- **Don't trust the text buffer in graphics mode.** After desktop (≥640×480)
the stale BIOS text lingers in the buffer. The harness's `phase` field
accounts for this; don't re-read `textScreen` in a `desktop` phase and
think you hit a BSOD.
- **Kill Electron when done.** Background processes pile up, each holding
the disk image lock. `pkill -f "windows95.*electron"` on every path out.
## Bisecting v86
`tools/bisect-v86.sh <commit>` handles one step. The harness retries 3×
per commit. Hard-won lessons:
1. **Validate bounds against a known-good binary.** Source-built wasm can
drift from prod due to cargo/rustc version differences. We hit this:
the "GOOD" bound produced a wasm that couldn't read the disk at all.
2. **JS-only when toolchain drifts.** Keep the prod wasm, rebuild only
libv86.js at each commit. Closure is deterministic enough; cargo
isn't always. Works until you cross a commit that changes the JS↔wasm
ABI (for v86, the APIC→Rust port in Aug 2025).
3. **Retry on FAIL, never on SUCCESS.** One SUCCESS = commit is good.
Three different FAILs at the same commit = commit is bad.
4. **State cleanup between runs** (see above). Skipping this is the #1
cause of spurious "bad" verdicts during bisect.
## Extending the harness
- New verdicts: add to the chain in `collectStatus` in `debug-harness.ts`
- New keyboard actions: extend `runScript` (current types: `keys`, `chord`,
`text`, `wait`)
- New probe signals: add to `ProbeStatus` interface
Gate everything new on `process.env.WIN95_PROBE === "1"` so it stays out
of the normal app.
## Common failure diagnostics
| Symptom | Check |
|---|---|
| No SMB traffic at all | `$TMPDIR/windows95-smb.log` should have `hooked adapter` line. If absent, v86 API changed — see `src/renderer/smb/README.md` |
| SMB hooks fire, no connection | Win95's "NetBIOS over TCP/IP" checkbox — bake into default-state.bin |
| Boot hangs on `2996c087` or older v86 | You probably have a ABI-mismatched wasm/JS pair. Prod wasm is the ground truth; rebuild JS against it. |

View File

@@ -0,0 +1,127 @@
---
name: update-v86
description: Build and install v86 (wasm + libv86.js + BIOS) into windows95. Use when pulling upstream v86 changes, fixing a broken build, verifying the fork branches are still in sync, or setting up a fresh v86 checkout.
---
# Updating v86
windows95 builds v86 from source — not from copy.sh. Two small bugfix
patches ride along on a fork branch until the upstream PRs land.
## Sources
| File | Built from |
|---|---|
| `src/renderer/lib/libv86.js` | `make build/libv86.js` in `../v86` |
| `src/renderer/lib/build/v86.wasm` | `make build/v86.wasm` |
| `bios/seabios.bin`, `bios/vgabios.bin` | copied from `../v86/bios/` |
`tools/update-v86.js` runs those targets, copies the artifacts, runs 5
sanity checks, and fails loudly if any prerequisite is missing. No
fallbacks, no fetching from copy.sh.
## The fork branch
v86 should be checked out on **`felixrieseberg/v86:windows95-base`**.
That branch merges two bugfix branches, each with an open upstream PR:
- **`electron-renderer-fs-loader`** (PR #1540) — `src/lib.js` uses
`require("fs")` instead of `await import("node:fs/promises")`. Dynamic
import of `node:` URLs doesn't work in an Electron renderer.
- **`ide-shared-registers`** (PR #1541) — `src/ide.js` writes ATA Command
Block registers (Features, Sector Count, LBA Low/Mid/High) to both
master and slave. Without this, Win95/98 hang at the splash screen on
any disk >~535MiB. Root cause: v86 commit `1b90d2e7` changed those
writes to target only `current_interface`, but per ATA spec they're
channel-shared (one register file on the IDE cable; both drives latch
the same value).
## Prerequisites
```sh
rustup target add wasm32-unknown-unknown
brew install openjdk
# one-time: fetch the Closure compiler v86's Makefile pins to
curl -sL https://repo1.maven.org/maven2/com/google/javascript/closure-compiler/v20210601/closure-compiler-v20210601.jar \
-o ../v86/closure-compiler/compiler.jar
```
Closure **must** be v20210601 — newer versions hit
[closure-compiler#3972](https://github.com/google/closure-compiler/issues/3972)
on v86's source. The pin is in v86's Makefile.
## Steps
```sh
cd ../v86
git fetch fork origin
git checkout windows95-base
git rebase fork/windows95-base # in case fork was updated elsewhere
cd ../windows95
node tools/update-v86.js
```
That's it. Script runs both `make` targets, copies, verifies.
## Sanity-check WARNs
The 5 checks assert invariants `src/renderer/smb/index.ts` and
`tools/parcel-build.js` depend on. A WARN means upstream changed
something load-bearing — don't ignore it:
1. **`await import("node:...")` still present** → PR #1540 was reverted
or the pattern moved. Electron renderer will fail to load disk images.
2. **`master.features_reg=` missing in minified** → PR #1541 was reverted
or `windows95-base` lost the commit. Win95 will hang at splash on
disks >535MiB. Check `cd ../v86 && git log --oneline windows95-base`.
3. **Export pattern changed**`tools/parcel-build.js` shim needs
updating. Look for `module.exports.V86=` and `window.V86=`.
4. **`tcp-connection` event gone** → SMB falls back to the old-API theft
hack in `src/renderer/smb/index.ts` — still works, but surprising.
5. **`on_tcp_connection` gone** → old-API fallback is dead. SMB integration
only works via the `tcp-connection` bus event now. Harmless; update
the comment in `index.ts` and retire the theft code.
## After updating, probe-test
```sh
node tools/update-v86.js && tools/probe-boot.sh
```
Should land SUCCESS in ~40s. If FAIL_SPLASH_HANG, the IDE fix didn't
take — check `grep master.features_reg src/renderer/lib/libv86.js`. If
FAIL_VXDLINK, retry — sporadic bluescreens are normal (see the
`probe-win95` skill).
## When a PR merges upstream
Rebase `windows95-base` to drop the now-redundant commit:
```sh
cd ../v86
git fetch origin
git checkout windows95-base
git rebase origin/master # drops the merged commit cleanly
git push fork windows95-base --force-with-lease
```
If **both** PRs are upstream, retire the fork branch entirely:
1. Point `tools/update-v86.js` default at `origin/master` (it already
uses `../v86`, so just `git checkout master` there)
2. Delete `fork/windows95-base`
3. Remove this skill's "The fork branch" section
4. Confirm the 5 sanity checks still pass — they're version-agnostic
## Integration contract with SMB
The SMB server sits on top of v86's network adapter. Details in
`src/renderer/smb/README.md`. Short version: the new path uses the
`tcp-connection` bus event; the fallback path uses
`adapter.on_tcp_connection` callback + connection-theft (stealing a
`TCPConnection` the HTTP probe builds for us). Both use `.on_data` on
the conn, not `.on("data")`, because Closure dead-code-eliminates the
event emitter plumbing.
If any v86 update breaks these assumptions, `src/renderer/smb/index.ts`
needs updating, not just `tools/update-v86.js`.