From 83fdcc3b680d218c24fe348c9e1197f301865417 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Tue, 7 Apr 2026 22:43:46 +0200 Subject: [PATCH 01/10] fix: build packages in dependency order and add pre-push hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The workspace build ran packages in parallel/alphabetical order, causing CI failures because downstream packages couldn't find type declarations from their dependencies. Build now runs in topological order: types → trace → runtime-worker → content → orchestrator → cli → web. Also adds a .githooks/pre-push hook mirroring CI checks (build, lint, test) and a prepare script to auto-configure the hooks path. Co-Authored-By: Claude Opus 4.6 (1M context) --- .githooks/pre-push | 16 ++++++++++++++++ .github/workflows/ci.yml | 10 ++-------- package.json | 6 ++++-- spec/ui/sprint-46-github-actions-ci-cd.md | 21 ++++++++++++++------- 4 files changed, 36 insertions(+), 17 deletions(-) create mode 100755 .githooks/pre-push diff --git a/.githooks/pre-push b/.githooks/pre-push new file mode 100755 index 0000000..fb85589 --- /dev/null +++ b/.githooks/pre-push @@ -0,0 +1,16 @@ +#!/bin/sh +# Pre-push hook: runs the same checks as CI to prevent failures. +# Install: git config core.hooksPath .githooks + +set -e + +echo "==> Building all packages..." +npm run build + +echo "==> Running lint..." +npx biome check . + +echo "==> Running unit tests..." +npm test + +echo "==> All checks passed." diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aa1adbb..71ce83f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,11 +22,8 @@ jobs: - name: Install dependencies run: npm ci - - name: Build packages - run: | - for pkg in packages/*/; do - npm run build -w "$pkg" - done + - name: Build all packages and web app + run: npm run build - name: Lint run: npx biome check . @@ -34,9 +31,6 @@ jobs: - name: Unit tests run: npm test - - name: Build web app - run: npm run build -w apps/web - - name: Install Playwright Chromium run: npx playwright install chromium --with-deps diff --git a/package.json b/package.json index 52fca8e..987f668 100644 --- a/package.json +++ b/package.json @@ -8,13 +8,15 @@ "apps/*" ], "scripts": { - "build": "npm run build --workspaces --if-present", + "build": "npm run build -w packages/types && npm run build -w packages/trace && npm run build -w packages/runtime-worker && npm run build -w packages/content && npm run build -w packages/orchestrator && npm run build -w packages/cli && npm run build -w apps/web", + "build:packages": "npm run build -w packages/types && npm run build -w packages/trace && npm run build -w packages/runtime-worker && npm run build -w packages/content && npm run build -w packages/orchestrator && npm run build -w packages/cli", "test": "vitest run", "lint": "biome check .", "lint:fix": "biome check --write .", "dev": "npm run dev --workspace=apps/web", "test:e2e": "npm run test:e2e --workspace=apps/web", - "cli": "node packages/cli/dist/index.js" + "cli": "node packages/cli/dist/index.js", + "prepare": "git config core.hooksPath .githooks" }, "devDependencies": { "@biomejs/biome": "^2.0.0", diff --git a/spec/ui/sprint-46-github-actions-ci-cd.md b/spec/ui/sprint-46-github-actions-ci-cd.md index e4c2404..2f440b4 100644 --- a/spec/ui/sprint-46-github-actions-ci-cd.md +++ b/spec/ui/sprint-46-github-actions-ci-cd.md @@ -25,13 +25,14 @@ Add full GitHub Actions CI/CD: PR checks (lint, type-check, test, e2e, changeset .github/workflows/publish-next.yml .github/workflows/release.yml .changeset/config.json +.githooks/pre-push biome.json ``` ## Modified Files ``` -package.json (add biome, changesets deps; add lint/lint:fix scripts) +package.json (add biome, changesets deps; add lint/lint:fix/prepare scripts; fix build order) apps/web/vite.config.ts (set base path for GitHub Pages) ``` @@ -56,12 +57,11 @@ Steps (sequential — project is small enough that parallelism adds complexity w 1. Checkout code 2. Setup Node.js 22.x with npm cache 3. `npm ci` -4. **Build packages** — `npm run build` across all `packages/*` (produces `.js` + `.d.ts` needed by downstream packages and the web app) +4. **Build all packages and web app** — `npm run build` (builds packages in dependency order: types → trace → runtime-worker → content → orchestrator → cli → web) 5. **Lint** — `npx biome check .` 6. **Unit tests** — `npm test` -7. **Build web app** — `npm run build -w apps/web` -8. **E2E tests** — Install Playwright Chromium, then `npm run test:e2e -w apps/web` -9. **Changeset check** — Verify a `.changeset/*.md` file is present in the PR (skip for PRs from the changesets bot) +7. **E2E tests** — Install Playwright Chromium, then `npm run test:e2e -w apps/web` +8. **Changeset check** — Verify a `.changeset/*.md` file is present in the PR (skip for PRs from the changesets bot) ### Publish Next Workflow (`publish-next.yml`) @@ -171,8 +171,15 @@ Added to root `package.json` as `devDependencies`: Added to root `package.json`: +- `"build"` — builds all packages in dependency order (types → trace → runtime-worker → content → orchestrator → cli) then web app +- `"build:packages"` — builds only the library packages in dependency order (excludes web app) - `"lint": "biome check ."` - `"lint:fix": "biome check --write ."` +- `"prepare"` — configures git to use `.githooks/` as the hooks directory + +## Pre-push Git Hook + +A `.githooks/pre-push` hook runs the same checks as CI locally before each push: build, lint, and unit tests. The `prepare` npm script automatically configures `git config core.hooksPath .githooks` on `npm install`, so the hook activates for all contributors without manual setup. ## Manual Setup Required (Outside of Code) @@ -219,6 +226,6 @@ npx biome check . # Validate changesets config npx changeset status -# Test the build pipeline locally -npm ci && npm run build && npm test && npm run build -w apps/web +# Test the full build pipeline locally (same as CI) +npm ci && npm run build && npx biome check . && npm test ``` From 2dc3a47d2560153ef9bd4d5f24c4a63941e62932 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Wed, 8 Apr 2026 15:27:26 +0200 Subject: [PATCH 02/10] fix: split E2E tests into separate CI job E2E tests now run in a dedicated job that depends on the main ci job passing first. This prevents slow/flaky E2E tests from blocking the core lint, build, and unit test feedback loop. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/ci.yml | 33 ++++++++++++++++++----- spec/ui/sprint-46-github-actions-ci-cd.md | 13 ++++++--- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 71ce83f..cdce97f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,12 +31,6 @@ jobs: - name: Unit tests run: npm test - - name: Install Playwright Chromium - run: npx playwright install chromium --with-deps - - - name: E2E tests - run: npm run test:e2e -w apps/web - - name: Check changeset if: ${{ !startsWith(github.head_ref, 'changeset-release/') }} run: | @@ -45,3 +39,30 @@ jobs: echo "::error::No changeset file found. Run 'npx changeset' to add one." exit 1 fi + + e2e: + name: E2E Tests + runs-on: ubuntu-latest + needs: ci + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Build all packages and web app + run: npm run build + + - name: Install Playwright Chromium + run: npx playwright install chromium --with-deps + + - name: E2E tests + run: npm run test:e2e -w apps/web diff --git a/spec/ui/sprint-46-github-actions-ci-cd.md b/spec/ui/sprint-46-github-actions-ci-cd.md index 2f440b4..53ce615 100644 --- a/spec/ui/sprint-46-github-actions-ci-cd.md +++ b/spec/ui/sprint-46-github-actions-ci-cd.md @@ -52,7 +52,9 @@ Each workflow has a single trigger and a clear purpose: Trigger: `on: pull_request` targeting `main`. -Steps (sequential — project is small enough that parallelism adds complexity without meaningful speed gain): +Two jobs: `ci` (required) and `e2e` (runs after `ci` passes). + +**Job: `ci` — Lint, Test & Build** 1. Checkout code 2. Setup Node.js 22.x with npm cache @@ -60,8 +62,13 @@ Steps (sequential — project is small enough that parallelism adds complexity w 4. **Build all packages and web app** — `npm run build` (builds packages in dependency order: types → trace → runtime-worker → content → orchestrator → cli → web) 5. **Lint** — `npx biome check .` 6. **Unit tests** — `npm test` -7. **E2E tests** — Install Playwright Chromium, then `npm run test:e2e -w apps/web` -8. **Changeset check** — Verify a `.changeset/*.md` file is present in the PR (skip for PRs from the changesets bot) +7. **Changeset check** — Verify a `.changeset/*.md` file is present in the PR (skip for PRs from the changesets bot) + +**Job: `e2e` — E2E Tests** (needs: `ci`) + +1. Checkout, setup Node.js, `npm ci`, build (same as above) +2. Install Playwright Chromium +3. `npm run test:e2e -w apps/web` ### Publish Next Workflow (`publish-next.yml`) From 9332e1c263f5508c44957af8d8efad6595043ac7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Wed, 8 Apr 2026 15:52:31 +0200 Subject: [PATCH 03/10] fix: fix E2E tests by using configurable vite base path Root cause: vite.config.ts hardcoded base: "/pvm-debugger/" for GitHub Pages, but Playwright tests navigate to http://localhost:4199/ (root). This caused 193/251 E2E failures because vite preview served the app under /pvm-debugger/ while tests expected it at /. Changes: - Make vite base path configurable via VITE_BASE_PATH env var (default /) - Set VITE_BASE_PATH=/pvm-debugger/ only in publish-next workflow for GitHub Pages deployment - Add fetch-host-call to sprint-19 test's host call visibility check - Skip 6 tests with broken stepToFetchHostCall logic (pre-existing) - Migrate biome.json schema to 2.4.10 and exclude test-results/ Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/publish-next.yml | 4 +- apps/web/e2e/sprint-19-host-call-tab.spec.ts | 14 +- apps/web/e2e/sprint-23-logs.spec.ts | 2 +- .../web/e2e/sprint-43-fetch-host-call.spec.ts | 192 +++++++++--------- apps/web/vite.config.ts | 2 +- biome.json | 10 +- 6 files changed, 123 insertions(+), 101 deletions(-) diff --git a/.github/workflows/publish-next.yml b/.github/workflows/publish-next.yml index bc10cfd..861fdee 100644 --- a/.github/workflows/publish-next.yml +++ b/.github/workflows/publish-next.yml @@ -59,8 +59,10 @@ jobs: env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - name: Build web app + - name: Build web app for GitHub Pages run: npm run build -w apps/web + env: + VITE_BASE_PATH: /pvm-debugger/ - name: Upload Pages artifact uses: actions/upload-pages-artifact@v3 diff --git a/apps/web/e2e/sprint-19-host-call-tab.spec.ts b/apps/web/e2e/sprint-19-host-call-tab.spec.ts index dad30fd..94a86d5 100644 --- a/apps/web/e2e/sprint-19-host-call-tab.spec.ts +++ b/apps/web/e2e/sprint-19-host-call-tab.spec.ts @@ -173,15 +173,23 @@ test.describe("Sprint 19 — Host Call Drawer Tab", () => { .getByTestId("storage-host-call") .isVisible() .catch(() => false); + const fetchVisible = await page + .getByTestId("fetch-host-call") + .isVisible() + .catch(() => false); const genericVisible = await page .getByTestId("generic-host-call") .isVisible() .catch(() => false); // Exactly one should be visible - expect(gasVisible || logVisible || storageVisible || genericVisible).toBe( - true, - ); + expect( + gasVisible || + logVisible || + storageVisible || + fetchVisible || + genericVisible, + ).toBe(true); }); test("no resume button is present in the tab", async ({ page }) => { diff --git a/apps/web/e2e/sprint-23-logs.spec.ts b/apps/web/e2e/sprint-23-logs.spec.ts index df1db6c..603bd62 100644 --- a/apps/web/e2e/sprint-23-logs.spec.ts +++ b/apps/web/e2e/sprint-23-logs.spec.ts @@ -67,7 +67,7 @@ test.describe("Sprint 23 — Logs Tab", () => { ); }); - test("after a log host call, decoded text appears with step number", async ({ + test.skip("after a log host call, decoded text appears with step number", async ({ page, }) => { await loadTraceProgram(page); diff --git a/apps/web/e2e/sprint-43-fetch-host-call.spec.ts b/apps/web/e2e/sprint-43-fetch-host-call.spec.ts index ab25bf8..6fe2f34 100644 --- a/apps/web/e2e/sprint-43-fetch-host-call.spec.ts +++ b/apps/web/e2e/sprint-43-fetch-host-call.spec.ts @@ -68,111 +68,117 @@ test.describe("Sprint 43 — Fetch Host Call Handler", () => { return false; } - test.describe("Refine context", () => { - test("fetch handler UI renders for all-ecalli-refine", async ({ page }) => { - await loadAllEcalliExample(page, "refine"); - await setNeverAutoContinue(page); - await runToHostCall(page); - - await expect(page.getByTestId("host-call-tab")).toBeVisible({ - timeout: 5000, + // TODO: stepToFetchHostCall never finds a fetch host call (index 1) within 30 steps. + // The all-ecalli program may need more steps or different SPI parameters to reach fetch. + test.describe + .skip("Refine context", () => { + test("fetch handler UI renders for all-ecalli-refine", async ({ + page, + }) => { + await loadAllEcalliExample(page, "refine"); + await setNeverAutoContinue(page); + await runToHostCall(page); + + await expect(page.getByTestId("host-call-tab")).toBeVisible({ + timeout: 5000, + }); + + // Step to a fetch host call + const found = await stepToFetchHostCall(page); + expect(found).toBe(true); + + // Verify fetch handler is shown and generic empty is NOT + await expect(page.getByTestId("fetch-host-call")).toBeVisible(); + await expect(page.getByTestId("host-call-empty")).not.toBeVisible(); }); - // Step to a fetch host call - const found = await stepToFetchHostCall(page); - expect(found).toBe(true); - - // Verify fetch handler is shown and generic empty is NOT - await expect(page.getByTestId("fetch-host-call")).toBeVisible(); - await expect(page.getByTestId("host-call-empty")).not.toBeVisible(); - }); - - test("struct mode shows editor and encoded output", async ({ page }) => { - await loadAllEcalliExample(page, "refine"); - await setNeverAutoContinue(page); - await runToHostCall(page); + test("struct mode shows editor and encoded output", async ({ page }) => { + await loadAllEcalliExample(page, "refine"); + await setNeverAutoContinue(page); + await runToHostCall(page); - await expect(page.getByTestId("host-call-tab")).toBeVisible({ - timeout: 5000, - }); - const found = await stepToFetchHostCall(page); - expect(found).toBe(true); + await expect(page.getByTestId("host-call-tab")).toBeVisible({ + timeout: 5000, + }); + const found = await stepToFetchHostCall(page); + expect(found).toBe(true); - // Switch to Struct mode - const structBtn = page.getByTestId("fetch-mode-struct"); - await expect(structBtn).toBeVisible(); - await structBtn.click(); + // Switch to Struct mode + const structBtn = page.getByTestId("fetch-mode-struct"); + await expect(structBtn).toBeVisible(); + await structBtn.click(); - // Verify struct editor is shown - await expect(page.getByTestId("struct-editor")).toBeVisible(); + // Verify struct editor is shown + await expect(page.getByTestId("struct-editor")).toBeVisible(); - // Verify encoded output preview is shown - await expect(page.getByTestId("struct-encoded-output")).toBeVisible(); - }); - - test("NONE toggle hides mode tabs", async ({ page }) => { - await loadAllEcalliExample(page, "refine"); - await setNeverAutoContinue(page); - await runToHostCall(page); - - await expect(page.getByTestId("host-call-tab")).toBeVisible({ - timeout: 5000, + // Verify encoded output preview is shown + await expect(page.getByTestId("struct-encoded-output")).toBeVisible(); }); - const found = await stepToFetchHostCall(page); - expect(found).toBe(true); - - // Verify Struct mode button is visible before NONE - await expect(page.getByTestId("fetch-mode-struct")).toBeVisible(); - - // Toggle NONE - const noneToggle = page.getByTestId("none-toggle"); - await expect(noneToggle).toBeVisible(); - await noneToggle.check(); - - // Struct mode button should be hidden - await expect(page.getByTestId("fetch-mode-struct")).not.toBeVisible(); - - // Output preview should show NONE sentinel - const preview = page.getByTestId("output-preview"); - await expect(preview).toBeVisible(); - const text = await preview.textContent(); - expect(text).toContain("18446744073709551615"); - }); - test("fetch-specific badges in trace (count > 0)", async ({ page }) => { - await loadAllEcalliExample(page, "refine"); - await setNeverAutoContinue(page); - - // Run to generate some trace entries - await runToHostCall(page); - - // Step through a few host calls to generate trace data - for (let i = 0; i < 5; i++) { - await page.getByTestId("next-button").click(); - await page.waitForTimeout(200); - const status = await pvmStatus(page).textContent(); - if (!status?.includes("Host Call")) break; - } - - // Open ecalli trace tab - await page.getByTestId("drawer-tab-ecalli_trace").click(); - await expect(page.getByTestId("ecalli-trace-tab")).toBeVisible(); - - // Look for trace badges - const badges = page.locator("[data-testid='trace-entry-badge']"); - await expect(badges.first()).toBeVisible({ timeout: 5000 }); + test("NONE toggle hides mode tabs", async ({ page }) => { + await loadAllEcalliExample(page, "refine"); + await setNeverAutoContinue(page); + await runToHostCall(page); + + await expect(page.getByTestId("host-call-tab")).toBeVisible({ + timeout: 5000, + }); + const found = await stepToFetchHostCall(page); + expect(found).toBe(true); + + // Verify Struct mode button is visible before NONE + await expect(page.getByTestId("fetch-mode-struct")).toBeVisible(); + + // Toggle NONE + const noneToggle = page.getByTestId("none-toggle"); + await expect(noneToggle).toBeVisible(); + await noneToggle.check(); + + // Struct mode button should be hidden + await expect(page.getByTestId("fetch-mode-struct")).not.toBeVisible(); + + // Output preview should show NONE sentinel + const preview = page.getByTestId("output-preview"); + await expect(preview).toBeVisible(); + const text = await preview.textContent(); + expect(text).toContain("18446744073709551615"); + }); - // Count fetch-specific badges (those containing "fetch") - const allBadges = await badges.allTextContents(); - const fetchCount = allBadges.filter((b) => - b.toLowerCase().includes("fetch"), - ).length; - expect(fetchCount).toBeGreaterThan(0); + test("fetch-specific badges in trace (count > 0)", async ({ page }) => { + await loadAllEcalliExample(page, "refine"); + await setNeverAutoContinue(page); + + // Run to generate some trace entries + await runToHostCall(page); + + // Step through a few host calls to generate trace data + for (let i = 0; i < 5; i++) { + await page.getByTestId("next-button").click(); + await page.waitForTimeout(200); + const status = await pvmStatus(page).textContent(); + if (!status?.includes("Host Call")) break; + } + + // Open ecalli trace tab + await page.getByTestId("drawer-tab-ecalli_trace").click(); + await expect(page.getByTestId("ecalli-trace-tab")).toBeVisible(); + + // Look for trace badges + const badges = page.locator("[data-testid='trace-entry-badge']"); + await expect(badges.first()).toBeVisible({ timeout: 5000 }); + + // Count fetch-specific badges (those containing "fetch") + const allBadges = await badges.allTextContents(); + const fetchCount = allBadges.filter((b) => + b.toLowerCase().includes("fetch"), + ).length; + expect(fetchCount).toBeGreaterThan(0); + }); }); - }); test.describe("Accumulate context", () => { - test("all-ecalli-accumulate loads and works", async ({ page }) => { + // TODO: stepToFetchHostCall never reaches a fetch host call within 30 steps + test.skip("all-ecalli-accumulate loads and works", async ({ page }) => { await loadAllEcalliExample(page, "accumulate"); await setNeverAutoContinue(page); await runToHostCall(page); diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 58c03c9..7490d91 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -4,7 +4,7 @@ import react from "@vitejs/plugin-react-swc"; import { defineConfig } from "vite"; export default defineConfig({ - base: "/pvm-debugger/", + base: process.env.VITE_BASE_PATH || "/", plugins: [react(), tailwindcss()], worker: { format: "es", diff --git a/biome.json b/biome.json index e2c31ee..2b2481d 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.0/schema.json", + "$schema": "https://biomejs.dev/schemas/2.4.10/schema.json", "linter": { "enabled": true, "rules": { @@ -19,6 +19,12 @@ "indentWidth": 2 }, "files": { - "includes": ["**", "!**/dist", "!**/node_modules", "!**/.changeset"] + "includes": [ + "**", + "!**/dist", + "!**/node_modules", + "!**/.changeset", + "!**/test-results" + ] } } From 76b9b55fab15f4935ebb7c4449fc4fe0f477264f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Wed, 8 Apr 2026 19:02:02 +0200 Subject: [PATCH 04/10] fix: fix E2E test stepping logic and add CI retries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - sprint-23: add parallel mode to avoid worker contamination from heavy trace replays between sequential tests - sprint-43: fix stepToFetchHostCall to use run-button (run to next host call) instead of next-button (single step) after resuming from a host call — the old logic only advanced one instruction and never reached the next host call - sprint-43: fix trace badge test to step through host calls properly - playwright: add retries: 2 in CI for flaky test resilience Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/web/e2e/sprint-23-logs.spec.ts | 3 +- .../web/e2e/sprint-43-fetch-host-call.spec.ts | 194 +++++++++--------- apps/web/playwright.config.ts | 1 + 3 files changed, 100 insertions(+), 98 deletions(-) diff --git a/apps/web/e2e/sprint-23-logs.spec.ts b/apps/web/e2e/sprint-23-logs.spec.ts index 603bd62..f52eba5 100644 --- a/apps/web/e2e/sprint-23-logs.spec.ts +++ b/apps/web/e2e/sprint-23-logs.spec.ts @@ -1,6 +1,7 @@ import { expect, test } from "@playwright/test"; test.describe("Sprint 23 — Logs Tab", () => { + test.describe.configure({ mode: "parallel" }); /** Load a simple (non-trace) program. */ async function loadSimpleProgram(page: import("@playwright/test").Page) { await page.goto("/#/load"); @@ -67,7 +68,7 @@ test.describe("Sprint 23 — Logs Tab", () => { ); }); - test.skip("after a log host call, decoded text appears with step number", async ({ + test("after a log host call, decoded text appears with step number", async ({ page, }) => { await loadTraceProgram(page); diff --git a/apps/web/e2e/sprint-43-fetch-host-call.spec.ts b/apps/web/e2e/sprint-43-fetch-host-call.spec.ts index 6fe2f34..105e178 100644 --- a/apps/web/e2e/sprint-43-fetch-host-call.spec.ts +++ b/apps/web/e2e/sprint-43-fetch-host-call.spec.ts @@ -53,132 +53,132 @@ test.describe("Sprint 43 — Fetch Host Call Handler", () => { const isVisible = await fetchHandler.isVisible().catch(() => false); if (isVisible) return true; - // Continue to next host call + // Resume past this host call (single step) await page.getByTestId("next-button").click(); - // Wait a moment for the PVM to process - const status = await pvmStatus(page).textContent(); - if (!status?.includes("Host Call")) { - await expect(pvmStatus(page)) - .toHaveText("Host Call", { timeout: 10000 }) - .catch(() => {}); - const newStatus = await pvmStatus(page).textContent(); - if (!newStatus?.includes("Host Call")) return false; + await page.waitForTimeout(200); + + // Run to the next host call + await page.getByTestId("run-button").click(); + try { + await expect(pvmStatus(page)).toHaveText("Host Call", { + timeout: 10000, + }); + } catch { + return false; } } return false; } - // TODO: stepToFetchHostCall never finds a fetch host call (index 1) within 30 steps. - // The all-ecalli program may need more steps or different SPI parameters to reach fetch. - test.describe - .skip("Refine context", () => { - test("fetch handler UI renders for all-ecalli-refine", async ({ - page, - }) => { - await loadAllEcalliExample(page, "refine"); - await setNeverAutoContinue(page); - await runToHostCall(page); - - await expect(page.getByTestId("host-call-tab")).toBeVisible({ - timeout: 5000, - }); - - // Step to a fetch host call - const found = await stepToFetchHostCall(page); - expect(found).toBe(true); + test.describe("Refine context", () => { + test("fetch handler UI renders for all-ecalli-refine", async ({ page }) => { + await loadAllEcalliExample(page, "refine"); + await setNeverAutoContinue(page); + await runToHostCall(page); - // Verify fetch handler is shown and generic empty is NOT - await expect(page.getByTestId("fetch-host-call")).toBeVisible(); - await expect(page.getByTestId("host-call-empty")).not.toBeVisible(); + await expect(page.getByTestId("host-call-tab")).toBeVisible({ + timeout: 5000, }); - test("struct mode shows editor and encoded output", async ({ page }) => { - await loadAllEcalliExample(page, "refine"); - await setNeverAutoContinue(page); - await runToHostCall(page); - - await expect(page.getByTestId("host-call-tab")).toBeVisible({ - timeout: 5000, - }); - const found = await stepToFetchHostCall(page); - expect(found).toBe(true); + // Step to a fetch host call + const found = await stepToFetchHostCall(page); + expect(found).toBe(true); - // Switch to Struct mode - const structBtn = page.getByTestId("fetch-mode-struct"); - await expect(structBtn).toBeVisible(); - await structBtn.click(); + // Verify fetch handler is shown and generic empty is NOT + await expect(page.getByTestId("fetch-host-call")).toBeVisible(); + await expect(page.getByTestId("host-call-empty")).not.toBeVisible(); + }); - // Verify struct editor is shown - await expect(page.getByTestId("struct-editor")).toBeVisible(); + test("struct mode shows editor and encoded output", async ({ page }) => { + await loadAllEcalliExample(page, "refine"); + await setNeverAutoContinue(page); + await runToHostCall(page); - // Verify encoded output preview is shown - await expect(page.getByTestId("struct-encoded-output")).toBeVisible(); + await expect(page.getByTestId("host-call-tab")).toBeVisible({ + timeout: 5000, }); + const found = await stepToFetchHostCall(page); + expect(found).toBe(true); - test("NONE toggle hides mode tabs", async ({ page }) => { - await loadAllEcalliExample(page, "refine"); - await setNeverAutoContinue(page); - await runToHostCall(page); + // Switch to Struct mode + const structBtn = page.getByTestId("fetch-mode-struct"); + await expect(structBtn).toBeVisible(); + await structBtn.click(); - await expect(page.getByTestId("host-call-tab")).toBeVisible({ - timeout: 5000, - }); - const found = await stepToFetchHostCall(page); - expect(found).toBe(true); + // Verify struct editor is shown + await expect(page.getByTestId("struct-editor")).toBeVisible(); - // Verify Struct mode button is visible before NONE - await expect(page.getByTestId("fetch-mode-struct")).toBeVisible(); - - // Toggle NONE - const noneToggle = page.getByTestId("none-toggle"); - await expect(noneToggle).toBeVisible(); - await noneToggle.check(); + // Verify encoded output preview is shown + await expect(page.getByTestId("struct-encoded-output")).toBeVisible(); + }); - // Struct mode button should be hidden - await expect(page.getByTestId("fetch-mode-struct")).not.toBeVisible(); + test("NONE toggle hides mode tabs", async ({ page }) => { + await loadAllEcalliExample(page, "refine"); + await setNeverAutoContinue(page); + await runToHostCall(page); - // Output preview should show NONE sentinel - const preview = page.getByTestId("output-preview"); - await expect(preview).toBeVisible(); - const text = await preview.textContent(); - expect(text).toContain("18446744073709551615"); + await expect(page.getByTestId("host-call-tab")).toBeVisible({ + timeout: 5000, }); + const found = await stepToFetchHostCall(page); + expect(found).toBe(true); + + // Verify Struct mode button is visible before NONE + await expect(page.getByTestId("fetch-mode-struct")).toBeVisible(); + + // Toggle NONE + const noneToggle = page.getByTestId("none-toggle"); + await expect(noneToggle).toBeVisible(); + await noneToggle.check(); - test("fetch-specific badges in trace (count > 0)", async ({ page }) => { - await loadAllEcalliExample(page, "refine"); - await setNeverAutoContinue(page); + // Struct mode button should be hidden + await expect(page.getByTestId("fetch-mode-struct")).not.toBeVisible(); + + // Output preview should show NONE sentinel + const preview = page.getByTestId("output-preview"); + await expect(preview).toBeVisible(); + const text = await preview.textContent(); + expect(text).toContain("18446744073709551615"); + }); - // Run to generate some trace entries - await runToHostCall(page); + test("fetch-specific badges in trace (count > 0)", async ({ page }) => { + await loadAllEcalliExample(page, "refine"); + await setNeverAutoContinue(page); - // Step through a few host calls to generate trace data - for (let i = 0; i < 5; i++) { - await page.getByTestId("next-button").click(); - await page.waitForTimeout(200); - const status = await pvmStatus(page).textContent(); - if (!status?.includes("Host Call")) break; + // Step through several host calls to generate trace data with fetch entries + await runToHostCall(page); + for (let i = 0; i < 5; i++) { + await page.getByTestId("next-button").click(); + await page.waitForTimeout(200); + await page.getByTestId("run-button").click(); + try { + await expect(pvmStatus(page)).toHaveText("Host Call", { + timeout: 10000, + }); + } catch { + break; } + } - // Open ecalli trace tab - await page.getByTestId("drawer-tab-ecalli_trace").click(); - await expect(page.getByTestId("ecalli-trace-tab")).toBeVisible(); + // Open ecalli trace tab + await page.getByTestId("drawer-tab-ecalli_trace").click(); + await expect(page.getByTestId("ecalli-trace-tab")).toBeVisible(); - // Look for trace badges - const badges = page.locator("[data-testid='trace-entry-badge']"); - await expect(badges.first()).toBeVisible({ timeout: 5000 }); + // Look for trace badges + const badges = page.locator("[data-testid='trace-entry-badge']"); + await expect(badges.first()).toBeVisible({ timeout: 5000 }); - // Count fetch-specific badges (those containing "fetch") - const allBadges = await badges.allTextContents(); - const fetchCount = allBadges.filter((b) => - b.toLowerCase().includes("fetch"), - ).length; - expect(fetchCount).toBeGreaterThan(0); - }); + // Count fetch-specific badges (those containing "fetch") + const allBadges = await badges.allTextContents(); + const fetchCount = allBadges.filter((b) => + b.toLowerCase().includes("fetch"), + ).length; + expect(fetchCount).toBeGreaterThan(0); }); + }); test.describe("Accumulate context", () => { - // TODO: stepToFetchHostCall never reaches a fetch host call within 30 steps - test.skip("all-ecalli-accumulate loads and works", async ({ page }) => { + test("all-ecalli-accumulate loads and works", async ({ page }) => { await loadAllEcalliExample(page, "accumulate"); await setNeverAutoContinue(page); await runToHostCall(page); diff --git a/apps/web/playwright.config.ts b/apps/web/playwright.config.ts index f685562..407fd11 100644 --- a/apps/web/playwright.config.ts +++ b/apps/web/playwright.config.ts @@ -3,6 +3,7 @@ import { defineConfig } from "@playwright/test"; export default defineConfig({ testDir: "./e2e", timeout: 30_000, + retries: process.env.CI ? 2 : 0, use: { baseURL: "http://localhost:4199", }, From 15395153f7cb0f69958c13adf7ce01705d98ec6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Wed, 8 Apr 2026 21:35:54 +0200 Subject: [PATCH 05/10] changeset --- .changeset/calm-forks-smash.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .changeset/calm-forks-smash.md diff --git a/.changeset/calm-forks-smash.md b/.changeset/calm-forks-smash.md new file mode 100644 index 0000000..e344a9c --- /dev/null +++ b/.changeset/calm-forks-smash.md @@ -0,0 +1,10 @@ +--- +"@pvmdbg/cli": patch +"@pvmdbg/content": patch +"@pvmdbg/orchestrator": patch +"@pvmdbg/runtime-worker": patch +"@pvmdbg/trace": patch +"@pvmdbg/types": patch +--- + +Initial From fc2089d505d40fffd86ed6c39d4c81699928aaad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Thu, 9 Apr 2026 00:18:35 +0200 Subject: [PATCH 06/10] =?UTF-8?q?fix:=20unskip=20all=20E2E=20tests=20?= =?UTF-8?q?=E2=80=94=20fix=20fixtures=20and=20remove=20skip=20guards?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - sprint-20: switch from io-trace (trace replay only had 2 accessible host calls) to all-ecalli-accumulate which actually produces storage host calls. Replace graceful test.skip() with expect().toBe(true). - sprint-24/25/30: PVM switching timing issue was caused by the base URL bug (app not loading properly). Now that the app loads correctly, tryEnableAnanas() succeeds within timeout. Replace test.skip guards with expect(enabled).toBe(true). Result: 249 E2E tests pass, 0 skipped, 0 failed. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../e2e/sprint-20-host-call-storage.spec.ts | 93 ++++++++----------- apps/web/e2e/sprint-24-pvm-tabs.spec.ts | 6 +- apps/web/e2e/sprint-25-divergence.spec.ts | 15 +-- .../e2e/sprint-30-change-highlighting.spec.ts | 5 +- 4 files changed, 44 insertions(+), 75 deletions(-) diff --git a/apps/web/e2e/sprint-20-host-call-storage.spec.ts b/apps/web/e2e/sprint-20-host-call-storage.spec.ts index 7cd6e5f..c098ded 100644 --- a/apps/web/e2e/sprint-20-host-call-storage.spec.ts +++ b/apps/web/e2e/sprint-20-host-call-storage.spec.ts @@ -1,15 +1,19 @@ import { expect, test } from "@playwright/test"; test.describe("Sprint 20 — Host Call Storage Table", () => { - /** Load a trace-backed program and wait for the debugger page. */ - async function loadTraceProgram( - page: import("@playwright/test").Page, - exampleId = "io-trace", - ) { + /** Load the all-ecalli-refine SPI program (has storage host calls). */ + async function loadProgram(page: import("@playwright/test").Page) { await page.goto("/#/load"); - const card = page.getByTestId(`example-card-${exampleId}`); + const card = page.getByTestId("example-card-all-ecalli-accumulate"); await expect(card).toBeVisible({ timeout: 15000 }); await card.click(); + + // SPI programs show a config step — click Load to proceed + await expect(page.getByTestId("config-step")).toBeVisible({ + timeout: 15000, + }); + await page.getByTestId("config-step-load").click(); + await expect(page.getByTestId("debugger-page")).toBeVisible({ timeout: 15000, }); @@ -45,37 +49,32 @@ test.describe("Sprint 20 — Host Call Storage Table", () => { page: import("@playwright/test").Page, ): Promise { for (let attempt = 0; attempt < 30; attempt++) { - // Check if we're on a host call - const status = await pvmStatus(page).textContent(); - if (status !== "Host Call") { - // Try running to find the next host call - await page.getByTestId("run-button").click(); - try { - await expect(pvmStatus(page)).toHaveText("Host Call", { - timeout: 5000, - }); - } catch { - // May have terminated - return false; - } - } - - // Check if the storage host call view rendered (only for read/write indices) - const storageView = page.getByTestId("storage-host-call"); - const isStorage = await storageView.isVisible().catch(() => false); + // Check if the storage host call view rendered (indices 3=read, 4=write) + const isStorage = await page + .getByTestId("storage-host-call") + .isVisible() + .catch(() => false); if (isStorage) { return true; } - // Not a storage host call — click Next to skip + // Not a storage host call — skip and run to the next one await page.getByTestId("next-button").click(); - await page.waitForTimeout(300); + await page.waitForTimeout(200); + await page.getByTestId("run-button").click(); + try { + await expect(pvmStatus(page)).toHaveText("Host Call", { + timeout: 5000, + }); + } catch { + return false; + } } return false; } test("storage host call renders the dedicated view", async ({ page }) => { - await loadTraceProgram(page); + await loadProgram(page); await setAutoContinuePolicy(page, "never"); // Run to first host call @@ -84,27 +83,21 @@ test.describe("Sprint 20 — Host Call Storage Table", () => { // The first host call in io-trace is ecalli=1 (fetch), which is a storage type const found = await stepToStorageHostCall(page); - if (!found) { - test.skip(); - return; - } + expect(found).toBe(true); // Should render the storage host call view await expect(page.getByTestId("storage-host-call")).toBeVisible(); }); test("the storage table shows entries", async ({ page }) => { - await loadTraceProgram(page); + await loadProgram(page); await setAutoContinuePolicy(page, "never"); await page.getByTestId("run-button").click(); await expect(pvmStatus(page)).toHaveText("Host Call", { timeout: 15000 }); const found = await stepToStorageHostCall(page); - if (!found) { - test.skip(); - return; - } + expect(found).toBe(true); // The storage table component should be visible await expect(page.getByTestId("storage-table")).toBeVisible(); @@ -119,17 +112,14 @@ test.describe("Sprint 20 — Host Call Storage Table", () => { }); test("adding a new key/value entry works", async ({ page }) => { - await loadTraceProgram(page); + await loadProgram(page); await setAutoContinuePolicy(page, "never"); await page.getByTestId("run-button").click(); await expect(pvmStatus(page)).toHaveText("Host Call", { timeout: 15000 }); const found = await stepToStorageHostCall(page); - if (!found) { - test.skip(); - return; - } + expect(found).toBe(true); // Add a new entry via the form await page.getByTestId("storage-new-key").fill("0x1234"); @@ -143,17 +133,14 @@ test.describe("Sprint 20 — Host Call Storage Table", () => { }); test("editing an existing value works", async ({ page }) => { - await loadTraceProgram(page); + await loadProgram(page); await setAutoContinuePolicy(page, "never"); await page.getByTestId("run-button").click(); await expect(pvmStatus(page)).toHaveText("Host Call", { timeout: 15000 }); const found = await stepToStorageHostCall(page); - if (!found) { - test.skip(); - return; - } + expect(found).toBe(true); // Add an entry await page.getByTestId("storage-new-key").fill("0xaa"); @@ -168,17 +155,14 @@ test.describe("Sprint 20 — Host Call Storage Table", () => { }); test("the active key is highlighted", async ({ page }) => { - await loadTraceProgram(page); + await loadProgram(page); await setAutoContinuePolicy(page, "never"); await page.getByTestId("run-button").click(); await expect(pvmStatus(page)).toHaveText("Host Call", { timeout: 15000 }); const found = await stepToStorageHostCall(page); - if (!found) { - test.skip(); - return; - } + expect(found).toBe(true); // Check if an active indicator is present. This depends on whether // the trace data includes key info that matches a table entry. @@ -203,7 +187,7 @@ test.describe("Sprint 20 — Host Call Storage Table", () => { test("storage entries persist across multiple host calls in the same session", async ({ page, }) => { - await loadTraceProgram(page); + await loadProgram(page); await setAutoContinuePolicy(page, "never"); await page.getByTestId("run-button").click(); @@ -211,10 +195,7 @@ test.describe("Sprint 20 — Host Call Storage Table", () => { // Find first storage host call const found1 = await stepToStorageHostCall(page); - if (!found1) { - test.skip(); - return; - } + expect(found1).toBe(true); // Add an entry await page.getByTestId("storage-new-key").fill("0xpersist"); diff --git a/apps/web/e2e/sprint-24-pvm-tabs.spec.ts b/apps/web/e2e/sprint-24-pvm-tabs.spec.ts index d11efb1..a6cc0fa 100644 --- a/apps/web/e2e/sprint-24-pvm-tabs.spec.ts +++ b/apps/web/e2e/sprint-24-pvm-tabs.spec.ts @@ -65,7 +65,7 @@ test.describe("Sprint 24 — Multi-PVM Tabs", () => { test("both PVM tabs render when both are enabled", async ({ page }) => { await loadProgram(page); const enabled = await tryEnableAnanas(page); - test.skip(!enabled, "PVM switching did not stabilize (timing issue)"); + expect(enabled).toBe(true); await expect(page.getByTestId("pvm-tab-typeberry")).toBeVisible(); await expect(page.getByTestId("pvm-tab-ananas")).toBeVisible(); @@ -78,7 +78,7 @@ test.describe("Sprint 24 — Multi-PVM Tabs", () => { // Enable ananas (this resets the session — both PVMs start at initial state) const enabled = await tryEnableAnanas(page); - test.skip(!enabled, "PVM switching did not stabilize (timing issue)"); + expect(enabled).toBe(true); // Step once so typeberry registers diverge const nextBtn = page.getByTestId("next-button"); @@ -116,7 +116,7 @@ test.describe("Sprint 24 — Multi-PVM Tabs", () => { test("removed PVM disappears from tab bar", async ({ page }) => { await loadProgram(page); const enabled = await tryEnableAnanas(page); - test.skip(!enabled, "PVM switching did not stabilize (timing issue)"); + expect(enabled).toBe(true); // Both tabs should be active buttons await expect(page.getByTestId("pvm-tab-typeberry")).toHaveAttribute( diff --git a/apps/web/e2e/sprint-25-divergence.spec.ts b/apps/web/e2e/sprint-25-divergence.spec.ts index 9ca2262..f34e7e9 100644 --- a/apps/web/e2e/sprint-25-divergence.spec.ts +++ b/apps/web/e2e/sprint-25-divergence.spec.ts @@ -60,10 +60,7 @@ test.describe("Sprint 25 — Divergence Detection", () => { test("both PVMs agree after running to completion", async ({ page }) => { await loadProgram(page); const enabled = await tryEnableAnanas(page); - test.skip( - !enabled, - "PVM switching did not stabilize (pre-existing sprint-24 issue)", - ); + expect(enabled).toBe(true); // Run to completion const runBtn = page.getByTestId("run-button"); @@ -87,10 +84,7 @@ test.describe("Sprint 25 — Divergence Detection", () => { }) => { await loadProgram(page); const enabled = await tryEnableAnanas(page); - test.skip( - !enabled, - "PVM switching did not stabilize (pre-existing sprint-24 issue)", - ); + expect(enabled).toBe(true); // Run to completion const runBtn = page.getByTestId("run-button"); @@ -113,10 +107,7 @@ test.describe("Sprint 25 — Divergence Detection", () => { test("divergence clears after reset", async ({ page }) => { await loadProgram(page); const enabled = await tryEnableAnanas(page); - test.skip( - !enabled, - "PVM switching did not stabilize (pre-existing sprint-24 issue)", - ); + expect(enabled).toBe(true); // Run to completion const runBtn = page.getByTestId("run-button"); diff --git a/apps/web/e2e/sprint-30-change-highlighting.spec.ts b/apps/web/e2e/sprint-30-change-highlighting.spec.ts index 4c56595..a088a76 100644 --- a/apps/web/e2e/sprint-30-change-highlighting.spec.ts +++ b/apps/web/e2e/sprint-30-change-highlighting.spec.ts @@ -164,10 +164,7 @@ test.describe("Sprint 30 — Registers Change Highlighting", () => { }) => { await loadProgram(page); const enabled = await tryEnableAnanas(page); - test.skip( - !enabled, - "PVM switching did not stabilize (pre-existing sprint-24 issue)", - ); + expect(enabled).toBe(true); // Run to completion — PVMs may diverge const runBtn = page.getByTestId("run-button"); From ad7b0be55de0b3fb2e5d8b730bb2f143d9e1cc76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Thu, 9 Apr 2026 10:06:53 +0200 Subject: [PATCH 07/10] fix: resolve all biome lint warnings and enforce strict checking - Fix 411 biome lint warnings across 87 files - Add type="button" to all 33 button elements missing it - Replace non-null assertions with proper null handling in source code - Fix unused function parameters (prefix with _) - Replace explicit any with unknown or biome-ignore where generic constraints require it - Add keyboard handlers and ARIA roles to interactive elements - Add biome-ignore comments for array-index keys where no stable key exists - Promote all biome rules from "warn" to "error" severity - Suppress noNonNullAssertion and noExplicitAny in test files via overrides - Disable noLabelWithoutControl (false positives on custom form components) - Update pre-push hook to use --error-on-warnings flag Co-Authored-By: Claude Opus 4.6 (1M context) --- .githooks/pre-push | 2 +- apps/web/e2e/integration-smoke.spec.ts | 6 +- apps/web/e2e/sprint-03-instructions.spec.ts | 2 +- apps/web/e2e/sprint-07-layout.spec.ts | 10 +- apps/web/e2e/sprint-10-file-upload.spec.ts | 6 +- apps/web/e2e/sprint-11-url-and-hex.spec.ts | 4 +- .../e2e/sprint-12-detection-summary.spec.ts | 4 +- apps/web/e2e/sprint-13-spi-config.spec.ts | 4 +- apps/web/e2e/sprint-14-drawer.spec.ts | 20 ++-- apps/web/e2e/sprint-19-host-call-tab.spec.ts | 2 +- apps/web/e2e/sprint-21-ecalli-trace.spec.ts | 2 +- apps/web/e2e/sprint-24-pvm-tabs.spec.ts | 4 +- apps/web/e2e/sprint-26-breakpoints.spec.ts | 8 +- apps/web/e2e/sprint-27-blocks-virtual.spec.ts | 2 +- .../web/e2e/sprint-28-asm-raw-popover.spec.ts | 4 +- .../e2e/sprint-29-register-editing.spec.ts | 4 +- apps/web/e2e/sprint-33-block-stepping.spec.ts | 8 +- apps/web/e2e/sprint-35-responsive.spec.ts | 6 +- apps/web/src/App.test.tsx | 1 - .../src/components/debugger/BlockHeader.tsx | 8 +- .../src/components/debugger/BottomDrawer.tsx | 3 + .../components/debugger/DebuggerLayout.tsx | 1 + apps/web/src/components/debugger/HexDump.tsx | 36 ++++++-- .../components/debugger/InstructionRow.tsx | 8 +- .../components/debugger/InstructionsPanel.tsx | 2 + .../src/components/debugger/MemoryPanel.tsx | 4 +- .../components/debugger/PendingChanges.tsx | 5 +- apps/web/src/components/debugger/PvmTabs.tsx | 1 + .../src/components/debugger/RegisterRow.tsx | 13 +++ .../components/debugger/RegistersPanel.tsx | 13 ++- .../src/components/debugger/StatusHeader.tsx | 13 +++ .../src/components/debugger/value-format.ts | 4 +- .../src/components/drawer/EcalliTraceTab.tsx | 9 +- .../web/src/components/drawer/HostCallTab.tsx | 1 + apps/web/src/components/drawer/LogsTab.tsx | 6 +- .../drawer/hostcalls/FetchHostCall.tsx | 7 +- .../drawer/hostcalls/GenericHostCall.tsx | 5 +- .../drawer/hostcalls/StorageHostCall.tsx | 21 ++--- .../hostcalls/fetch/AllTransfersEditor.tsx | 3 + .../hostcalls/fetch/AllWorkItemsEditor.tsx | 3 + .../hostcalls/fetch/BytesBlobEditor.tsx | 6 +- .../fetch/ProtocolConstantsEditor.tsx | 2 +- .../drawer/hostcalls/fetch/RawEditor.tsx | 2 +- .../fetch/RefinementContextEditor.tsx | 5 +- .../drawer/hostcalls/fetch/StructEditor.tsx | 1 - .../fetch/TransferOrOperandEditor.tsx | 6 +- .../hostcalls/fetch/WorkItemInfoEditor.tsx | 10 +- .../hostcalls/fetch/WorkPackageEditor.tsx | 9 +- .../src/components/load/DetectionSummary.tsx | 2 +- apps/web/src/components/load/ExampleList.tsx | 2 + apps/web/src/components/load/FileUpload.tsx | 10 ++ .../components/load/SpiEntrypointConfig.tsx | 10 +- apps/web/src/components/load/UrlInput.tsx | 1 + apps/web/src/hooks/useDebuggerActions.test.ts | 6 +- apps/web/src/hooks/useDebuggerActions.ts | 12 +-- apps/web/src/hooks/useDivergenceCheck.ts | 4 +- apps/web/src/hooks/useLogMessages.ts | 6 +- apps/web/src/hooks/useMemoryReader.ts | 6 +- apps/web/src/hooks/usePendingChanges.test.tsx | 46 +++++----- apps/web/src/hooks/useStableCallback.ts | 8 +- apps/web/src/hooks/useStorageTable.ts | 5 +- apps/web/src/lib/debugger-settings.ts | 8 +- apps/web/src/lib/fetch-codec.test.ts | 45 +++++---- apps/web/src/lib/fetch-codec.ts | 2 - apps/web/src/lib/fetch-defaults.ts | 2 +- apps/web/src/lib/fetch-utils.ts | 6 +- apps/web/src/lib/storage-utils.test.ts | 6 +- apps/web/src/main.tsx | 2 +- apps/web/src/pages/DebuggerPage.tsx | 8 +- apps/web/src/pages/LoadPage.tsx | 2 +- apps/web/src/workers/ananas.worker.ts | 2 +- biome.json | 31 ++++++- packages/cli/src/replay.test.ts | 14 +-- packages/cli/src/replay.ts | 2 +- packages/content/src/decode-trace.ts | 2 +- packages/content/src/detect.ts | 2 +- packages/content/src/index.test.ts | 51 +++++----- .../orchestrator/src/orchestrator.test.ts | 92 +++++++++---------- packages/orchestrator/src/orchestrator.ts | 8 +- .../orchestrator/src/typed-event-emitter.ts | 4 +- packages/runtime-worker/src/index.test.ts | 6 +- packages/runtime-worker/src/utils.ts | 6 +- packages/runtime-worker/src/worker-bridge.ts | 1 - packages/trace/src/index.test.ts | 36 ++++---- packages/trace/src/parser.ts | 4 +- packages/trace/src/serializer.ts | 10 +- packages/types/src/jam-codec.test.ts | 2 +- 87 files changed, 433 insertions(+), 345 deletions(-) diff --git a/.githooks/pre-push b/.githooks/pre-push index fb85589..ac0e1a3 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -8,7 +8,7 @@ echo "==> Building all packages..." npm run build echo "==> Running lint..." -npx biome check . +npx biome check . --error-on-warnings echo "==> Running unit tests..." npm test diff --git a/apps/web/e2e/integration-smoke.spec.ts b/apps/web/e2e/integration-smoke.spec.ts index 56585d4..99a722c 100644 --- a/apps/web/e2e/integration-smoke.spec.ts +++ b/apps/web/e2e/integration-smoke.spec.ts @@ -1,6 +1,6 @@ +import path from "node:path"; +import { fileURLToPath } from "node:url"; import { expect, test } from "@playwright/test"; -import path from "path"; -import { fileURLToPath } from "url"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -116,7 +116,7 @@ test.describe("Sprint 36 — Integration Smoke Test", () => { const gasText = await gasValue.textContent(); expect(gasText).toBeTruthy(); // Gas should be parseable as a number (may have thousands separators) - const gasNum = Number(gasText!.replace(/,/g, "")); + const gasNum = Number(gasText?.replace(/,/g, "")); expect(gasNum).toBeGreaterThan(0); }); diff --git a/apps/web/e2e/sprint-03-instructions.spec.ts b/apps/web/e2e/sprint-03-instructions.spec.ts index 7fbc72c..2537ed4 100644 --- a/apps/web/e2e/sprint-03-instructions.spec.ts +++ b/apps/web/e2e/sprint-03-instructions.spec.ts @@ -78,7 +78,7 @@ test.describe("Sprint 03 — Flat Instruction List", () => { let foundOmega = false; for (let i = 0; i < count; i++) { const text = await argsElements.nth(i).textContent(); - if (text && text.includes("ω")) { + if (text?.includes("ω")) { foundOmega = true; break; } diff --git a/apps/web/e2e/sprint-07-layout.spec.ts b/apps/web/e2e/sprint-07-layout.spec.ts index 2fff946..4dfc0f4 100644 --- a/apps/web/e2e/sprint-07-layout.spec.ts +++ b/apps/web/e2e/sprint-07-layout.spec.ts @@ -33,9 +33,9 @@ test.describe("Sprint 07 — 3-Column Debugger Layout", () => { expect(mBox).toBeTruthy(); // Instructions is to the left of Registers - expect(iBox!.x + iBox!.width).toBeLessThanOrEqual(rBox!.x + 2); + expect(iBox?.x + iBox?.width).toBeLessThanOrEqual(rBox?.x + 2); // Registers is to the left of Memory - expect(rBox!.x + rBox!.width).toBeLessThanOrEqual(mBox!.x + 2); + expect(rBox?.x + rBox?.width).toBeLessThanOrEqual(mBox?.x + 2); }); test("panel headers align at the same height", async ({ page }) => { @@ -54,8 +54,8 @@ test.describe("Sprint 07 — 3-Column Debugger Layout", () => { expect(mBox).toBeTruthy(); // All panels start at the same Y coordinate (header alignment) - expect(Math.abs(iBox!.y - rBox!.y)).toBeLessThan(2); - expect(Math.abs(rBox!.y - mBox!.y)).toBeLessThan(2); + expect(Math.abs(iBox?.y - rBox?.y)).toBeLessThan(2); + expect(Math.abs(rBox?.y - mBox?.y)).toBeLessThan(2); }); test("toolbar row is visible above the panels", async ({ page }) => { @@ -74,7 +74,7 @@ test.describe("Sprint 07 — 3-Column Debugger Layout", () => { expect(pBox).toBeTruthy(); // Toolbar is above the panel area - expect(tBox!.y + tBox!.height).toBeLessThanOrEqual(pBox!.y + 2); + expect(tBox?.y + tBox?.height).toBeLessThanOrEqual(pBox?.y + 2); }); test("each panel scrolls independently", async ({ page }) => { diff --git a/apps/web/e2e/sprint-10-file-upload.spec.ts b/apps/web/e2e/sprint-10-file-upload.spec.ts index 69b962d..b5154bb 100644 --- a/apps/web/e2e/sprint-10-file-upload.spec.ts +++ b/apps/web/e2e/sprint-10-file-upload.spec.ts @@ -1,6 +1,6 @@ +import path from "node:path"; +import { fileURLToPath } from "node:url"; import { expect, test } from "@playwright/test"; -import path from "path"; -import { fileURLToPath } from "url"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -122,7 +122,7 @@ test.describe("Sprint 10 — File Upload Source", () => { expect(leftBox).toBeTruthy(); expect(rightBox).toBeTruthy(); // In stacked layout, the left column top should be above right column top - expect(leftBox!.y).toBeLessThan(rightBox!.y); + expect(leftBox?.y).toBeLessThan(rightBox?.y); }); test("example cards still work in two-column layout", async ({ page }) => { diff --git a/apps/web/e2e/sprint-11-url-and-hex.spec.ts b/apps/web/e2e/sprint-11-url-and-hex.spec.ts index d7cf73f..192b856 100644 --- a/apps/web/e2e/sprint-11-url-and-hex.spec.ts +++ b/apps/web/e2e/sprint-11-url-and-hex.spec.ts @@ -160,8 +160,8 @@ test.describe("Sprint 11 — URL + Manual Hex Sources", () => { // Upload a file — should clear hex result const fileInput = page.getByTestId("file-upload-input"); - const path = await import("path"); - const url = await import("url"); + const path = await import("node:path"); + const url = await import("node:url"); const __filename = url.fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const fixturesDir = path.resolve(__dirname, "../../../fixtures"); diff --git a/apps/web/e2e/sprint-12-detection-summary.spec.ts b/apps/web/e2e/sprint-12-detection-summary.spec.ts index aecc067..6526e41 100644 --- a/apps/web/e2e/sprint-12-detection-summary.spec.ts +++ b/apps/web/e2e/sprint-12-detection-summary.spec.ts @@ -1,6 +1,6 @@ +import * as path from "node:path"; +import * as url from "node:url"; import { expect, test } from "@playwright/test"; -import * as path from "path"; -import * as url from "url"; const __filename = url.fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); diff --git a/apps/web/e2e/sprint-13-spi-config.spec.ts b/apps/web/e2e/sprint-13-spi-config.spec.ts index c13a29e..f820cea 100644 --- a/apps/web/e2e/sprint-13-spi-config.spec.ts +++ b/apps/web/e2e/sprint-13-spi-config.spec.ts @@ -1,6 +1,6 @@ +import * as path from "node:path"; +import * as url from "node:url"; import { expect, test } from "@playwright/test"; -import * as path from "path"; -import * as url from "url"; const __filename = url.fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); diff --git a/apps/web/e2e/sprint-14-drawer.spec.ts b/apps/web/e2e/sprint-14-drawer.spec.ts index 6c49969..789e658 100644 --- a/apps/web/e2e/sprint-14-drawer.spec.ts +++ b/apps/web/e2e/sprint-14-drawer.spec.ts @@ -60,7 +60,7 @@ test.describe("Sprint 14 — Bottom Drawer Shell", () => { // Drawer should be taller than just the tab bar const box = await drawer.boundingBox(); expect(box).toBeTruthy(); - expect(box!.height).toBeGreaterThan(40); + expect(box?.height).toBeGreaterThan(40); }); test("clicking the active tab collapses the drawer", async ({ page }) => { @@ -101,15 +101,15 @@ test.describe("Sprint 14 — Bottom Drawer Shell", () => { const drawer = page.getByTestId("bottom-drawer"); const initialBox = await drawer.boundingBox(); expect(initialBox).toBeTruthy(); - const initialHeight = initialBox!.height; + const initialHeight = initialBox?.height; // Drag the handle upward to expand const handle = page.getByTestId("drawer-drag-handle"); const handleBox = await handle.boundingBox(); expect(handleBox).toBeTruthy(); - const startX = handleBox!.x + handleBox!.width / 2; - const startY = handleBox!.y + handleBox!.height / 2; + const startX = handleBox?.x + handleBox?.width / 2; + const startY = handleBox?.y + handleBox?.height / 2; await page.mouse.move(startX, startY); await page.mouse.down(); @@ -119,7 +119,7 @@ test.describe("Sprint 14 — Bottom Drawer Shell", () => { const afterBox = await drawer.boundingBox(); expect(afterBox).toBeTruthy(); // Drawer should be taller after dragging up - expect(afterBox!.height).toBeGreaterThan(initialHeight + 50); + expect(afterBox?.height).toBeGreaterThan(initialHeight + 50); }); test("drawer height clamps to 60% of viewport maximum", async ({ page }) => { @@ -133,8 +133,8 @@ test.describe("Sprint 14 — Bottom Drawer Shell", () => { const handleBox = await handle.boundingBox(); expect(handleBox).toBeTruthy(); - const startX = handleBox!.x + handleBox!.width / 2; - const startY = handleBox!.y + handleBox!.height / 2; + const startX = handleBox?.x + handleBox?.width / 2; + const startY = handleBox?.y + handleBox?.height / 2; // Drag far upward — well beyond 60% of viewport await page.mouse.move(startX, startY); @@ -148,7 +148,7 @@ test.describe("Sprint 14 — Bottom Drawer Shell", () => { const drawer = page.getByTestId("bottom-drawer"); const box = await drawer.boundingBox(); expect(box).toBeTruthy(); - expect(box!.height).toBeLessThanOrEqual(maxAllowed); + expect(box?.height).toBeLessThanOrEqual(maxAllowed); }); test("drawer is positioned below the panel area", async ({ page }) => { @@ -164,8 +164,8 @@ test.describe("Sprint 14 — Bottom Drawer Shell", () => { expect(drawerBox).toBeTruthy(); // Drawer top should be at or below panels bottom - expect(drawerBox!.y).toBeGreaterThanOrEqual( - panelsBox!.y + panelsBox!.height - 2, + expect(drawerBox?.y).toBeGreaterThanOrEqual( + panelsBox?.y + panelsBox?.height - 2, ); }); }); diff --git a/apps/web/e2e/sprint-19-host-call-tab.spec.ts b/apps/web/e2e/sprint-19-host-call-tab.spec.ts index 94a86d5..98562f9 100644 --- a/apps/web/e2e/sprint-19-host-call-tab.spec.ts +++ b/apps/web/e2e/sprint-19-host-call-tab.spec.ts @@ -119,7 +119,7 @@ test.describe("Sprint 19 — Host Call Drawer Tab", () => { const headerText = await page .getByTestId("host-call-header") .textContent(); - if (headerText && headerText.includes("log")) { + if (headerText?.includes("log")) { foundLog = true; break; } diff --git a/apps/web/e2e/sprint-21-ecalli-trace.spec.ts b/apps/web/e2e/sprint-21-ecalli-trace.spec.ts index eb4368b..33951b8 100644 --- a/apps/web/e2e/sprint-21-ecalli-trace.spec.ts +++ b/apps/web/e2e/sprint-21-ecalli-trace.spec.ts @@ -45,7 +45,7 @@ test.describe("Sprint 21 — Ecalli Trace Tab", () => { ).toBeChecked(); } - function pvmStatus(page: import("@playwright/test").Page) { + function _pvmStatus(page: import("@playwright/test").Page) { return page.getByTestId("pvm-status-typeberry"); } diff --git a/apps/web/e2e/sprint-24-pvm-tabs.spec.ts b/apps/web/e2e/sprint-24-pvm-tabs.spec.ts index a6cc0fa..20f4cce 100644 --- a/apps/web/e2e/sprint-24-pvm-tabs.spec.ts +++ b/apps/web/e2e/sprint-24-pvm-tabs.spec.ts @@ -88,7 +88,7 @@ test.describe("Sprint 24 — Multi-PVM Tabs", () => { // Capture register state for typeberry (after stepping) const regPanel = page.getByTestId("panel-registers"); await expect(regPanel).toBeVisible(); - const typeberryText = await regPanel.innerText(); + const _typeberryText = await regPanel.innerText(); // Click ananas tab await page.getByTestId("pvm-tab-ananas").click(); @@ -98,7 +98,7 @@ test.describe("Sprint 24 — Multi-PVM Tabs", () => { ); // Register panel should now show ananas state (same step applied to both PVMs) - const ananasText = await regPanel.innerText(); + const _ananasText = await regPanel.innerText(); // Click back to typeberry await page.getByTestId("pvm-tab-typeberry").click(); diff --git a/apps/web/e2e/sprint-26-breakpoints.spec.ts b/apps/web/e2e/sprint-26-breakpoints.spec.ts index 806a824..198c53b 100644 --- a/apps/web/e2e/sprint-26-breakpoints.spec.ts +++ b/apps/web/e2e/sprint-26-breakpoints.spec.ts @@ -28,7 +28,7 @@ test.describe("Sprint 26 — Instructions Breakpoints", () => { // Get the second row's data-testid to extract its PC const secondRowTestId = await rows.nth(1).getAttribute("data-testid"); - const pc = secondRowTestId!.replace("instruction-row-", ""); + const pc = secondRowTestId?.replace("instruction-row-", ""); // Click the gutter const gutter = page.getByTestId(`breakpoint-gutter-${pc}`); @@ -46,7 +46,7 @@ test.describe("Sprint 26 — Instructions Breakpoints", () => { const panel = page.getByTestId("instructions-panel"); const rows = panel.locator("[data-testid^='instruction-row-']"); const secondRowTestId = await rows.nth(1).getAttribute("data-testid"); - const pc = secondRowTestId!.replace("instruction-row-", ""); + const pc = secondRowTestId?.replace("instruction-row-", ""); const gutter = page.getByTestId(`breakpoint-gutter-${pc}`); @@ -75,7 +75,7 @@ test.describe("Sprint 26 — Instructions Breakpoints", () => { // Record the new PC — we'll use it as a breakpoint target const targetPcText = await pcValue.textContent(); - const targetPc = parseInt(targetPcText!.replace("0x", ""), 16); + const targetPc = parseInt(targetPcText?.replace("0x", ""), 16); // Reset to go back to start await page.getByTestId("reset-button").click(); @@ -104,7 +104,7 @@ test.describe("Sprint 26 — Instructions Breakpoints", () => { const panel = page.getByTestId("instructions-panel"); const rows = panel.locator("[data-testid^='instruction-row-']"); const secondRowTestId = await rows.nth(1).getAttribute("data-testid"); - const pc = secondRowTestId!.replace("instruction-row-", ""); + const pc = secondRowTestId?.replace("instruction-row-", ""); // Set breakpoint const gutter = page.getByTestId(`breakpoint-gutter-${pc}`); diff --git a/apps/web/e2e/sprint-27-blocks-virtual.spec.ts b/apps/web/e2e/sprint-27-blocks-virtual.spec.ts index a8b1db4..6d9d606 100644 --- a/apps/web/e2e/sprint-27-blocks-virtual.spec.ts +++ b/apps/web/e2e/sprint-27-blocks-virtual.spec.ts @@ -164,7 +164,7 @@ test.describe("Sprint 27 — Block Folding + Virtualization", () => { expect(rowCount).toBeGreaterThan(1); const secondRowTestId = await rows.nth(1).getAttribute("data-testid"); - const pc = secondRowTestId!.replace("instruction-row-", ""); + const pc = secondRowTestId?.replace("instruction-row-", ""); // Click gutter to set breakpoint const gutter = page.getByTestId(`breakpoint-gutter-${pc}`); diff --git a/apps/web/e2e/sprint-28-asm-raw-popover.spec.ts b/apps/web/e2e/sprint-28-asm-raw-popover.spec.ts index 9880667..d1f8c26 100644 --- a/apps/web/e2e/sprint-28-asm-raw-popover.spec.ts +++ b/apps/web/e2e/sprint-28-asm-raw-popover.spec.ts @@ -49,7 +49,7 @@ test.describe("Sprint 28 — ASM/Raw Toggle + Binary Popover", () => { let hasOmega = false; for (let i = 0; i < count; i++) { const text = await argsElements.nth(i).textContent(); - if (text && text.includes("ω")) { + if (text?.includes("ω")) { hasOmega = true; break; } @@ -112,7 +112,7 @@ test.describe("Sprint 28 — ASM/Raw Toggle + Binary Popover", () => { let hasOmega = false; for (let i = 0; i < count; i++) { const text = await argsElements.nth(i).textContent(); - if (text && text.includes("ω")) { + if (text?.includes("ω")) { hasOmega = true; break; } diff --git a/apps/web/e2e/sprint-29-register-editing.spec.ts b/apps/web/e2e/sprint-29-register-editing.spec.ts index 0567ca5..a0b18ab 100644 --- a/apps/web/e2e/sprint-29-register-editing.spec.ts +++ b/apps/web/e2e/sprint-29-register-editing.spec.ts @@ -100,7 +100,7 @@ test.describe("Sprint 29 — Registers Inline Editing", () => { await loadProgram(page); const gasValue = page.getByTestId("gas-value"); - const originalGas = await gasValue.textContent(); + const _originalGas = await gasValue.textContent(); await gasValue.click(); const gasEdit = page.getByTestId("gas-value-edit"); @@ -118,7 +118,7 @@ test.describe("Sprint 29 — Registers Inline Editing", () => { await loadProgram(page); const pcValue = page.getByTestId("pc-value"); - const originalPc = await pcValue.textContent(); + const _originalPc = await pcValue.textContent(); await pcValue.click(); const pcEdit = page.getByTestId("pc-value-edit"); diff --git a/apps/web/e2e/sprint-33-block-stepping.spec.ts b/apps/web/e2e/sprint-33-block-stepping.spec.ts index d56878e..2b84b0a 100644 --- a/apps/web/e2e/sprint-33-block-stepping.spec.ts +++ b/apps/web/e2e/sprint-33-block-stepping.spec.ts @@ -28,18 +28,18 @@ test.describe("Sprint 33 — Block Stepping (Real)", () => { page: import("@playwright/test").Page, ): Promise { const text = await page.getByTestId("pc-value").textContent(); - return parseInt(text!.replace("0x", ""), 16); + return parseInt(text?.replace("0x", ""), 16); } /** Get the block header PCs visible in the instructions panel. */ - async function getBlockHeaderPcs( + async function _getBlockHeaderPcs( page: import("@playwright/test").Page, ): Promise { const headers = page.locator("[data-testid^='block-header-']"); const count = await headers.count(); const pcs: number[] = []; for (let i = 0; i < count; i++) { - const testId = await headers.nth(i).getAttribute("data-testid"); + const _testId = await headers.nth(i).getAttribute("data-testid"); // block-header-N where N is block index — extract startPc from first instruction in block // Instead, look at the block header content which typically shows the start PC pcs.push(i); // placeholder — we use a different approach below @@ -142,7 +142,7 @@ test.describe("Sprint 33 — Block Stepping (Real)", () => { await nextBtn.click(); await expect(pcValue).not.toHaveText("0x0000", { timeout: 5000 }); const targetPcText = await pcValue.textContent(); - const targetPc = parseInt(targetPcText!.replace("0x", ""), 16); + const targetPc = parseInt(targetPcText?.replace("0x", ""), 16); // Reset and set breakpoint at that PC await page.getByTestId("reset-button").click(); diff --git a/apps/web/e2e/sprint-35-responsive.spec.ts b/apps/web/e2e/sprint-35-responsive.spec.ts index 5a0f6ed..a9f7221 100644 --- a/apps/web/e2e/sprint-35-responsive.spec.ts +++ b/apps/web/e2e/sprint-35-responsive.spec.ts @@ -73,8 +73,8 @@ test.describe("Sprint 35 — Mobile / Responsive Layout", () => { expect(panelsBox).toBeTruthy(); expect(drawerBox).toBeTruthy(); - expect(panelsBox!.y + panelsBox!.height).toBeLessThanOrEqual( - drawerBox!.y + 2, + expect(panelsBox?.y + panelsBox?.height).toBeLessThanOrEqual( + drawerBox?.y + 2, ); }); @@ -132,6 +132,6 @@ test.describe("Sprint 35 — Mobile / Responsive Layout", () => { const rightBox = await right.boundingBox(); expect(leftBox).toBeTruthy(); expect(rightBox).toBeTruthy(); - expect(leftBox!.y).toBeLessThan(rightBox!.y); + expect(leftBox?.y).toBeLessThan(rightBox?.y); }); }); diff --git a/apps/web/src/App.test.tsx b/apps/web/src/App.test.tsx index 9e3afb4..f1acac4 100644 --- a/apps/web/src/App.test.tsx +++ b/apps/web/src/App.test.tsx @@ -1,5 +1,4 @@ import { render, screen } from "@testing-library/react"; -import React from "react"; import { MemoryRouter } from "react-router"; import { describe, expect, it } from "vitest"; import App from "./App"; diff --git a/apps/web/src/components/debugger/BlockHeader.tsx b/apps/web/src/components/debugger/BlockHeader.tsx index 4b75baf..a131345 100644 --- a/apps/web/src/components/debugger/BlockHeader.tsx +++ b/apps/web/src/components/debugger/BlockHeader.tsx @@ -12,10 +12,10 @@ export const BlockHeader = memo(function BlockHeader({ onToggle, }: BlockHeaderProps) { return ( -
onToggle(block.index)} @@ -29,6 +29,6 @@ export const BlockHeader = memo(function BlockHeader({ ({block.instructions.length} instr) -
+ ); }); diff --git a/apps/web/src/components/debugger/BottomDrawer.tsx b/apps/web/src/components/debugger/BottomDrawer.tsx index 7ad73da..ba8b5a6 100644 --- a/apps/web/src/components/debugger/BottomDrawer.tsx +++ b/apps/web/src/components/debugger/BottomDrawer.tsx @@ -128,6 +128,7 @@ export function BottomDrawer({ > {TABS.map(({ id, label }) => ( )} {value.map((item, idx) => ( + // biome-ignore lint/suspicious/noArrayIndexKey: index is the only stable key
Input #{idx}
{value.map((item, idx) => ( + // biome-ignore lint/suspicious/noArrayIndexKey: index is the only stable key
Item #{idx}
@@ -132,6 +132,7 @@ export function RefinementContextEditor({ Prerequisites ({value.prerequisites.length})
{value.prerequisites.map((prereq, idx) => ( + // biome-ignore lint/suspicious/noArrayIndexKey: index is the only stable key
@@ -158,7 +158,7 @@ function TransferEditor({ value={value.dest} onChange={(e) => { const n = parseInt(e.target.value, 10); - if (!isNaN(n)) onChange({ ...value, dest: n }); + if (!Number.isNaN(n)) onChange({ ...value, dest: n }); }} />
@@ -232,12 +232,14 @@ export function TransferOrOperandEditor({
Type:
@@ -97,7 +97,7 @@ export function WorkItemInfoEditor({ value={value.exportcount} onChange={(e) => { const n = parseInt(e.target.value, 10); - if (!isNaN(n)) setField("exportcount", n); + if (!Number.isNaN(n)) setField("exportcount", n); }} /> @@ -110,7 +110,7 @@ export function WorkItemInfoEditor({ value={value.importsegmentsCount} onChange={(e) => { const n = parseInt(e.target.value, 10); - if (!isNaN(n)) setField("importsegmentsCount", n); + if (!Number.isNaN(n)) setField("importsegmentsCount", n); }} /> @@ -123,7 +123,7 @@ export function WorkItemInfoEditor({ value={value.extrinsicsCount} onChange={(e) => { const n = parseInt(e.target.value, 10); - if (!isNaN(n)) setField("extrinsicsCount", n); + if (!Number.isNaN(n)) setField("extrinsicsCount", n); }} /> @@ -136,7 +136,7 @@ export function WorkItemInfoEditor({ value={value.payloadLength} onChange={(e) => { const n = parseInt(e.target.value, 10); - if (!isNaN(n)) setField("payloadLength", n); + if (!Number.isNaN(n)) setField("payloadLength", n); }} /> diff --git a/apps/web/src/components/drawer/hostcalls/fetch/WorkPackageEditor.tsx b/apps/web/src/components/drawer/hostcalls/fetch/WorkPackageEditor.tsx index dcabbc3..1546e91 100644 --- a/apps/web/src/components/drawer/hostcalls/fetch/WorkPackageEditor.tsx +++ b/apps/web/src/components/drawer/hostcalls/fetch/WorkPackageEditor.tsx @@ -28,7 +28,7 @@ function WorkItemEditor({ value={value.serviceindex} onChange={(e) => { const n = parseInt(e.target.value, 10); - if (!isNaN(n)) onChange({ ...value, serviceindex: n }); + if (!Number.isNaN(n)) onChange({ ...value, serviceindex: n }); }} /> @@ -92,7 +92,7 @@ function WorkItemEditor({ value={value.exportcount} onChange={(e) => { const n = parseInt(e.target.value, 10); - if (!isNaN(n)) onChange({ ...value, exportcount: n }); + if (!Number.isNaN(n)) onChange({ ...value, exportcount: n }); }} /> @@ -148,7 +148,7 @@ export function WorkPackageEditor({ value, onChange }: WorkPackageEditorProps) { value={value.authcodehost} onChange={(e) => { const n = parseInt(e.target.value, 10); - if (!isNaN(n)) onChange({ ...value, authcodehost: n }); + if (!Number.isNaN(n)) onChange({ ...value, authcodehost: n }); }} /> @@ -210,6 +210,7 @@ export function WorkPackageEditor({ value, onChange }: WorkPackageEditorProps) { {value.workitems.length} work item(s)
{value.workitems.map((item, idx) => ( + // biome-ignore lint/suspicious/noArrayIndexKey: index is the only stable key
Work Item #{idx}