Skip to content
Open
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
46 changes: 43 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
74 changes: 74 additions & 0 deletions tests/e2e/contributor-loop.spec.ts
Original file line number Diff line number Diff line change
@@ -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<string, number>;
detail?: { run?: { workload_id?: string; duration_ns?: number } };
message?: string;
};
}

async function submitAndWait(
request: APIRequestContext,
code: string,
): Promise<ResultResponse> {
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);
});
Loading