diff --git a/README.md b/README.md index a591f32..f911603 100644 --- a/README.md +++ b/README.md @@ -77,23 +77,63 @@ tracks the migration from in-process execution to a durable, isolated worker. Requires Docker, Node.js 22, and npm. Go is required only when running the simulator tests directly on the host. +For the newer public challenges, clone the independently maintained challenge +repository beside this one. The current local runner reads those challenge +packages from disk; a registry file alone cannot execute them. This is an +explicit development setup, not yet a downloadable challenge-bundle release. + +```text +roughstack/ + challenges/ + web-platform/ +``` + +From a new `roughstack` directory: + ```bash +git clone https://github.com/roughstack/challenges.git +git clone https://github.com/roughstack/web-platform.git +cd web-platform npm ci cp .env.example .env.local +BYTEARENA_ARENA_ROOTS=../challenges/challenges npm run arena:validate -- --all docker build -t roughstack-execution-runtime:latest ./sprite npm run docker:up npm run db:push npm run db:seed ``` -The app is served at `http://localhost:3100`. +Open `http://localhost:3100/challenges/cache-pressure-easy`. The editor must +show an incomplete scaffold, not the passing public baseline. Complete it and +submit to see a real local run. `EXECUTION_MODE=local` in `.env.local` is required; +`stub` emits synthetic UI-development results and must not be used to verify a +challenge. The Docker Compose file mounts `../challenges` read-only into the web +container, so a differently placed checkout needs an adjusted mount and +`BYTEARENA_ARENA_ROOTS` path. + +To run the automated baseline-versus-scaffold check against the running stack: + +```bash +ROUGHSTACK_REAL_ARENA_E2E=1 npx playwright test tests/e2e/contributor-loop.spec.ts +``` + +This check runs submitted code in Docker and asserts that the public baseline +passes while the learner scaffold fails with a useful diagnostic. It is opt-in +because it requires the separate challenge checkout, Docker access, and a live +database. Use it only on a trusted local machine; the current Docker-backed +development server is not a public untrusted-code service. The extracted +`execution-runtime` repository owns SDK and protocol development. This local +setup still builds its compatibility image from `web-platform/sprite`; moving +that image to a versioned runtime release is separate work. ```bash npm run docker:logs # tail the web container -npm run docker:reset # wipe volumes and rebuild from scratch -npm run docker:down # stop everything +npm run docker:down # stop everything without deleting data ``` +`npm run docker:reset` deletes the local Compose database volume; do not use it +to troubleshoot an existing checkout without backing up data first. + --- ## Verification diff --git a/tests/e2e/contributor-loop.spec.ts b/tests/e2e/contributor-loop.spec.ts new file mode 100644 index 0000000..fae0337 --- /dev/null +++ b/tests/e2e/contributor-loop.spec.ts @@ -0,0 +1,74 @@ +import { readFile } from "node:fs/promises"; +import path from "node:path"; + +import { expect, test, type APIRequestContext } from "@playwright/test"; + +const enabled = process.env.ROUGHSTACK_REAL_ARENA_E2E === "1"; +const challengeRoot = path.resolve( + process.env.ROUGHSTACK_CHALLENGES_ROOT ?? "../challenges", +); +const arenaRoot = path.join(challengeRoot, "challenges/cache-pressure-easy"); + +interface SubmissionResponse { + id: string; +} + +interface ResultResponse { + status: string; + error?: string; + result?: { + status: string; + metrics: Record; + detail?: { run?: { workload_id?: string; duration_ns?: number } }; + message?: string; + }; +} + +async function submitAndWait( + request: APIRequestContext, + code: string, +): Promise { + const submission = await request.post("/api/submissions", { + data: { slug: "cache-pressure-easy", language: "GO", code }, + }); + expect(submission.ok(), await submission.text()).toBe(true); + const { id } = (await submission.json()) as SubmissionResponse; + expect(id).toBeTruthy(); + + const deadline = Date.now() + 180_000; + while (Date.now() < deadline) { + const response = await request.get(`/api/submissions/${id}`); + expect(response.ok(), await response.text()).toBe(true); + const body = (await response.json()) as ResultResponse; + if (body.status === "done" || body.status === "error") return body; + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + throw new Error(`Submission ${id} did not finish within 180 seconds`); +} + +test("a contributor can run a public baseline and reject the learner scaffold", async ({ + request, +}) => { + test.skip(!enabled, "Set ROUGHSTACK_REAL_ARENA_E2E=1 for the local Docker acceptance check"); + test.setTimeout(420_000); + + const page = await request.get("/challenges/cache-pressure-easy"); + expect(page.ok(), await page.text()).toBe(true); + + const baseline = await readFile(path.join(arenaRoot, "starter/cache.go"), "utf8"); + const scaffold = await readFile(path.join(arenaRoot, "learner/starter.go"), "utf8"); + expect(scaffold).not.toBe(baseline); + + const passing = await submitAndWait(request, baseline); + expect(passing, JSON.stringify(passing)).toMatchObject({ + status: "done", + result: { status: "passed" }, + }); + expect(passing.result?.detail?.run?.workload_id).toBeTruthy(); + expect(Object.keys(passing.result?.metrics ?? {}).length).toBeGreaterThan(0); + + const failing = await submitAndWait(request, scaffold); + expect(failing.status, JSON.stringify(failing)).toBe("done"); + expect(failing.result?.status).toBe("failed"); + expect(failing.result?.message).toMatch(/TODO|build|test|failed/i); +});