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 diff --git a/.githooks/pre-push b/.githooks/pre-push new file mode 100755 index 0000000..ac0e1a3 --- /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 . --error-on-warnings + +echo "==> Running unit tests..." +npm test + +echo "==> All checks passed." diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aa1adbb..cdce97f 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,15 +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 - - - name: E2E tests - run: npm run test:e2e -w apps/web - - name: Check changeset if: ${{ !startsWith(github.head_ref, 'changeset-release/') }} run: | @@ -51,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/.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/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 dad30fd..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; } @@ -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-20-host-call-storage.spec.ts b/apps/web/e2e/sprint-20-host-call-storage.spec.ts index 7cd6e5f..14f5521 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,22 @@ 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", - ) { + // These tests step through multiple host calls to find storage — needs extra time in CI + test.slow(); + + /** 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 +52,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 +86,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 +115,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 +136,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 +158,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 +190,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 +198,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-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-23-logs.spec.ts b/apps/web/e2e/sprint-23-logs.spec.ts index df1db6c..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"); diff --git a/apps/web/e2e/sprint-24-pvm-tabs.spec.ts b/apps/web/e2e/sprint-24-pvm-tabs.spec.ts index d11efb1..20f4cce 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"); @@ -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(); @@ -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-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-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"); 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/e2e/sprint-43-fetch-host-call.spec.ts b/apps/web/e2e/sprint-43-fetch-host-call.spec.ts index ab25bf8..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,16 +53,18 @@ 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; @@ -143,15 +145,19 @@ test.describe("Sprint 43 — Fetch Host Call Handler", () => { await loadAllEcalliExample(page, "refine"); await setNeverAutoContinue(page); - // Run to generate some trace entries + // Step through several host calls to generate trace data with fetch 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; + await page.getByTestId("run-button").click(); + try { + await expect(pvmStatus(page)).toHaveText("Host Call", { + timeout: 10000, + }); + } catch { + break; + } } // Open ecalli trace tab 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", }, 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}