Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .changeset/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Changesets

This project uses [Changesets](https://github.com/changesets/changesets) for version management.

## Adding a changeset

When you make a change that should result in a package version bump, run:

```bash
npx changeset
```

Follow the prompts to select the affected packages and the type of version bump (patch, minor, major). This creates a `.md` file in this directory describing the change.

## How it works

- Every PR to `main` must include at least one changeset file (enforced by CI)
- On merge to `main`, a "Version Packages" PR is automatically created/updated
- Merging the "Version Packages" PR publishes all bumped packages to npm
11 changes: 11 additions & 0 deletions .changeset/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
"changelog": ["@changesets/changelog-github", { "repo": "FluffyLabs/pvm-debugger" }],
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["@pvmdbg/web"]
}
53 changes: 53 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: CI

on:
pull_request:
branches: [main]

jobs:
ci:
name: Lint, Test & Build
runs-on: ubuntu-latest

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 packages
run: |
for pkg in packages/*/; do
npm run build -w "$pkg"
done

- name: Lint
run: npx biome check .

- 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: |
changeset_files=$(find .changeset -name '*.md' ! -name 'README.md' | head -1)
if [ -z "$changeset_files" ]; then
echo "::error::No changeset file found. Run 'npx changeset' to add one."
exit 1
fi
72 changes: 72 additions & 0 deletions .github/workflows/publish-next.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
name: Publish Next

on:
push:
branches: [main]

permissions:
id-token: write
contents: read
pages: write

jobs:
publish-next:
name: Publish next & Deploy Pages
runs-on: ubuntu-latest

environment:
name: github-pages
url: ${{ steps.deploy.outputs.page_url }}

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
registry-url: "https://registry.npmjs.org"
cache: npm

- name: Install dependencies
run: npm ci

- name: Build packages
run: npm run build

- name: Patch versions with next tag
run: |
SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7)
for pkg in packages/*/package.json; do
current=$(node -p "require('./$pkg').version")
next_version="${current}-next.${SHORT_SHA}"
node -e "
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('$pkg', 'utf8'));
pkg.version = '$next_version';
fs.writeFileSync('$pkg', JSON.stringify(pkg, null, 2) + '\n');
"
echo "Patched $(dirname $pkg) to $next_version"
done

- name: Publish all packages with next tag
run: |
for pkg_dir in packages/*/; do
echo "Publishing $pkg_dir..."
(cd "$pkg_dir" && npm publish --tag next --provenance --access public) || echo "Failed to publish $pkg_dir (may already exist)"
done
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Build web app
run: npm run build -w apps/web

- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3
with:
path: apps/web/dist/

- name: Deploy to GitHub Pages
id: deploy
uses: actions/deploy-pages@v4
51 changes: 51 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Release

on:
push:
branches: [main]

permissions:
id-token: write
contents: write
pull-requests: write

jobs:
release:
name: Changesets Release
runs-on: ubuntu-latest

steps:
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}

- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
registry-url: "https://registry.npmjs.org"
cache: npm

- name: Install dependencies
run: npm ci

- name: Build packages
run: npm run build

- name: Create Release Pull Request or Publish
uses: changesets/action@v1
with:
version: npx changeset version
publish: npx changeset publish --access public
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
79 changes: 58 additions & 21 deletions apps/web/e2e/integration-smoke.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { test, expect } from "@playwright/test";
import { expect, test } from "@playwright/test";
import path from "path";
import { fileURLToPath } from "url";

Expand All @@ -14,10 +14,13 @@ const FIXTURES_DIR = path.resolve(__dirname, "../../../fixtures");
const COLLAPSED_CATEGORY: Record<string, string> = {
"inst-add-32": "json-test-vectors",
"inst-add-64": "json-test-vectors",
"io-trace": "traces", // traces is expanded, but kept for safety
"io-trace": "traces", // traces is expanded, but kept for safety
};

async function loadExample(page: import("@playwright/test").Page, exampleId: string) {
async function loadExample(
page: import("@playwright/test").Page,
exampleId: string,
) {
await page.goto("/#/load");
// Expand collapsed category if needed
const categoryId = COLLAPSED_CATEGORY[exampleId];
Expand All @@ -34,21 +37,28 @@ async function loadExample(page: import("@playwright/test").Page, exampleId: str
await expect(card).toBeVisible({ timeout: 15000 });
await card.click();
// Non-SPI programs skip config step and go directly to debugger
await expect(page.getByTestId("debugger-page")).toBeVisible({ timeout: 15000 });
await expect(page.getByTestId("debugger-page")).toBeVisible({
timeout: 15000,
});
}

/**
* Helper: upload a local fixture file, confirm detection, and load into debugger.
*/
async function loadFile(page: import("@playwright/test").Page, fixturePath: string) {
async function loadFile(
page: import("@playwright/test").Page,
fixturePath: string,
) {
await page.goto("/#/load");
await expect(page.getByTestId("load-page")).toBeVisible();
const fileInput = page.getByTestId("file-upload-input");
await fileInput.setInputFiles(path.join(FIXTURES_DIR, fixturePath));
await expect(page.getByTestId("file-upload-selected")).toBeVisible();
await page.getByTestId("source-step-continue").click();
// Non-SPI programs skip config step and go directly to debugger
await expect(page.getByTestId("debugger-page")).toBeVisible({ timeout: 15000 });
await expect(page.getByTestId("debugger-page")).toBeVisible({
timeout: 15000,
});
}

/** Open the settings tab in the bottom drawer. */
Expand All @@ -71,7 +81,9 @@ test.describe("Sprint 36 — Integration Smoke Test", () => {
// Increase timeout for the whole suite — up to 120s per test
test.setTimeout(60_000);

test("JAM SPI example loads with correct format summary", async ({ page }) => {
test("JAM SPI example loads with correct format summary", async ({
page,
}) => {
// Load a JAM SPI example (add-jam is in wat, collapsed by default)
await page.goto("/#/load");
await page.getByTestId("category-toggle-wat").click();
Expand All @@ -80,17 +92,23 @@ test.describe("Sprint 36 — Integration Smoke Test", () => {
await card.click();

// Verify step 2 shows detection summary with SPI format
await expect(page.getByTestId("config-step")).toBeVisible({ timeout: 15000 });
await expect(page.getByTestId("config-step")).toBeVisible({
timeout: 15000,
});
await expect(page.getByTestId("detection-summary")).toBeVisible();
await expect(page.getByTestId("detection-summary-format")).toHaveText(/JAM SPI/);
await expect(page.getByTestId("detection-summary-format")).toHaveText(
/JAM SPI/,
);

// SPI structural details should be present
await expect(page.getByTestId("detection-summary-spi")).toBeVisible();
await expect(page.getByTestId("summary-code-size")).toBeVisible();

// Load and verify the debugger renders
await page.getByTestId("config-step-load").click();
await expect(page.getByTestId("debugger-page")).toBeVisible({ timeout: 15000 });
await expect(page.getByTestId("debugger-page")).toBeVisible({
timeout: 15000,
});

// Verify a real state exists (gas should be a non-zero number)
const gasValue = page.getByTestId("gas-value");
Expand All @@ -112,8 +130,12 @@ test.describe("Sprint 36 — Integration Smoke Test", () => {

// Run to completion
await page.getByTestId("run-button").click();
await expect(page.getByTestId("execution-complete-badge")).toBeVisible({ timeout: 15000 });
await expect(page.getByTestId("execution-complete-badge")).toHaveText("Execution Complete");
await expect(page.getByTestId("execution-complete-badge")).toBeVisible({
timeout: 15000,
});
await expect(page.getByTestId("execution-complete-badge")).toHaveText(
"Execution Complete",
);

// Gas should have decreased (real state transition)
const finalGas = await gasValue.textContent();
Expand All @@ -138,7 +160,9 @@ test.describe("Sprint 36 — Integration Smoke Test", () => {

// Run to completion
await page.getByTestId("run-button").click();
await expect(page.getByTestId("execution-complete-badge")).toBeVisible({ timeout: 15000 });
await expect(page.getByTestId("execution-complete-badge")).toBeVisible({
timeout: 15000,
});
});

test("register edit changes execution result", async ({ page }) => {
Expand All @@ -162,7 +186,9 @@ test.describe("Sprint 36 — Integration Smoke Test", () => {

// Step once — add_64 executes: r9 = r7(100) + r8(2) = 102 (0x66)
await page.getByTestId("next-button").click();
await expect(page.getByTestId("pc-value")).not.toHaveText("0x0000", { timeout: 5000 });
await expect(page.getByTestId("pc-value")).not.toHaveText("0x0000", {
timeout: 5000,
});

// r9 should be 102 (0x66) — proving editing ω7 changed the downstream result
const regHex9 = page.getByTestId("register-hex-9");
Expand Down Expand Up @@ -211,13 +237,18 @@ test.describe("Sprint 36 — Integration Smoke Test", () => {

// Run — should stop at the first host call
await page.getByTestId("run-button").click();
await expect(page.getByTestId("pvm-status-typeberry")).toHaveText("Host Call", {
timeout: 15000,
});
await expect(page.getByTestId("pvm-status-typeberry")).toHaveText(
"Host Call",
{
timeout: 15000,
},
);

// The host call tab should be visible (drawer auto-opens on host call)
// Per spec pitfall: assert the rendered panel directly, don't click the tab
await expect(page.getByTestId("host-call-tab")).toBeVisible({ timeout: 5000 });
await expect(page.getByTestId("host-call-tab")).toBeVisible({
timeout: 5000,
});
await expect(page.getByTestId("host-call-header")).toBeVisible();

// Verify host call hint text is present
Expand All @@ -227,8 +258,12 @@ test.describe("Sprint 36 — Integration Smoke Test", () => {
await page.getByTestId("drawer-tab-ecalli_trace").click();
await expect(page.getByTestId("ecalli-trace-tab")).toBeVisible();
// Default view is "formatted" with two trace columns
await expect(page.getByTestId("trace-column-execution-trace")).toBeVisible();
await expect(page.getByTestId("trace-column-reference-trace")).toBeVisible();
await expect(
page.getByTestId("trace-column-execution-trace"),
).toBeVisible();
await expect(
page.getByTestId("trace-column-reference-trace"),
).toBeVisible();
});

test("JSON vector reaches expected terminal status", async ({ page }) => {
Expand All @@ -237,7 +272,9 @@ test.describe("Sprint 36 — Integration Smoke Test", () => {

// Run to completion
await page.getByTestId("run-button").click();
await expect(page.getByTestId("execution-complete-badge")).toBeVisible({ timeout: 15000 });
await expect(page.getByTestId("execution-complete-badge")).toBeVisible({
timeout: 15000,
});

// The PVM should be in a terminal state
const statusBadge = page.getByTestId("status-badge");
Expand Down
2 changes: 1 addition & 1 deletion apps/web/e2e/smoke.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { test, expect } from "@playwright/test";
import { expect, test } from "@playwright/test";

test("redirects / to /load and shows header", async ({ page }) => {
await page.goto("/");
Expand Down
Loading
Loading