quc v0 (4/4): tests, dev server, deploy, pipeline

15 tests (the run end to end against a fake GitHub tarball, safeword, the three phrases, veto revert, owner-only API, cross-site refusal, daily cap). dev.mjs takes POST bodies and a memory store. deploy.py creates the time-quc KV namespace and binds the Venice key as a Worker secret. quc.yml tests and deploys on main and skips the deploy until CLOUDFLARE_API_TOKEN is staged; ci.yml runs the quc tests on PRs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EVg99kQMY2Tumhobuyg7Fm
This commit is contained in:
teahubxyzandClaude Opus 5.5 committed 2026-10-22 18:15:15 +00:00
1 parent e7c0ec1188
commit 84bf73bc83
8 files changed
+282 -61

No files matched your search

+1
View File
@@ -20,3 +20,4 @@ jobs:
- run: python3 .github/time/test_time.py
- run: python3 .github/time/test_seed.py
- run: python3 .github/time/test_housekeeping.py
- run: npm test --prefix quc
+31
View File
@@ -0,0 +1,31 @@
name: quc
# Test and deploy quc (quc/, live at quc.<venture domain>) when it changes on main. Skips the deploy until the Cloudflare
# token is staged as a repo secret (see .github/time/SECRETS.md); deploys from hb-5 work meanwhile.
on:
push:
branches: [main]
paths: ["quc/**"]
workflow_dispatch:
permissions:
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 6
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID || 'dac3b11890319628eb5cd761445b63ee' }}
VENICE_API_KEY: ${{ secrets.VENICE_API_KEY }}
TIME_VENTURE_DOMAIN: ${{ vars.TIME_VENTURE_DOMAIN || 'yolopreneur.lol' }}
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm test --prefix quc
- name: deploy
if: env.CLOUDFLARE_API_TOKEN != ''
run: python3 quc/deploy.py
- name: no token yet
if: env.CLOUDFLARE_API_TOKEN == ''
run: echo "CLOUDFLARE_API_TOKEN is not staged; tests passed, deploy skipped" >> "$GITHUB_STEP_SUMMARY"
+27 -26
View File
@@ -1,33 +1,34 @@
# quc
App shell + API in one Cloudflare Worker (`src/worker.js`), live at https://quc.yolopreneur.lol/. No dependencies, no build step.
Hand an agent your repo, sit in the chair, watch. Live at https://quc.yolopreneur.lol/ (spec: teahubxyz/time#220).
- `npm start` serves it on :8787; `npm test` runs the tests; `npm run deploy` (needs `CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ACCOUNT_ID`) deploys and waits for `/api/health`.
- `GET /api/health` → `{ok, rev}`. `GET /chair.svg` is the chair-in-the-corner motif (also inlined in the shell).
One Cloudflare Worker, no dependencies, no build step. `npm start` serves it on :8787 (offline answers unless `VENICE_API_KEY`
is set), `npm test` runs the tests, `npm run deploy` deploys (needs `CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ACCOUNT_ID`, and
`VENICE_API_KEY` for real models) and waits for `/api/health`.
## Pipeline (needs a human: workers can't edit `.github/workflows`)
## How a run works
Add `.github/workflows/quc.yml`:
1. **Start**: a public GitHub URL and one line of task. One run at a time per visitor (a cookie), 5 runs a day each, 300 a
day in all (`RUNS_PER_DAY`, `RUNS_PER_DAY_ALL`).
2. **The cage**: quc downloads the repo's tarball from GitHub and keeps the readable text files (caps: 15 MB download, 2,000
files, 40 KB a file, 4 files changed, 10 minutes). Nothing from the repo is executed, and nothing is pushed anywhere.
3. **Steps**: the chair page calls `POST /api/runs/:id/step` again and again; each call does one thing (clone, plan, rewrite
one file, wrap up), so every request stays short. A coding model (`CODER_MODEL`, default Qwen3 Coder via Venice) plans and
rewrites whole files; Grok (`PERSONA_MODEL`, default `grok-4-3` via Venice) narrates. Diffs, commits and commentary are
appended to the run's event log.
4. **The chair**: the page shows the commentary and the diffs, read-only. Only the owner sees the safeword and the box.
5. **Safeword**: ends the run at once and throws the edits away (the replay still shows what happened).
6. **Magic words** (typed in the box, matched loosely; the persona drops hints, so keep them out of marketing copy):
1. "may I say something": notes. Anything typed after this is passed to the agent as a note.
2. "not that one": vetoes. Type a file's path and the agent reverts and skips it.
3. "my repo, my rules": takes the wheel. The run ends as `unlocked` and the patch is yours.
Anything else gets mocked.
7. **Replay**: the same URL, for anyone: a scrubber over every event, plus the aftermath card and the patch download.
```yaml
name: quc
on:
push: { branches: [main], paths: ["quc/**"] }
workflow_dispatch:
permissions: { contents: read }
jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 6
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with: { node-version: 22 }
- run: npm test --prefix quc
- run: python3 quc/deploy.py
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} # still "missing" in SECRETS.md
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
```
Storage is a KV namespace (`time-quc`): `run:<id>`, `events:<id>`, `work:<id>` (repo text and edits), `active:<visitor>`,
`cap:<day>:*`, `lock:<id>`. KV is eventually consistent and has no compare-and-set; a Durable Object per run is the upgrade
when that matters.
and, in `ci.yml`, `- run: npm test --prefix quc` so PRs run the quc tests.
## Pipeline
`.github/workflows/quc.yml` deploys on pushes to `quc/**` on main, and `ci.yml` runs `npm test --prefix quc` on PRs.
+25 -9
View File
@@ -1,8 +1,9 @@
"""Deploy quc to quc.<TIME_VENTURE_DOMAIN> (default yolopreneur.lol) as the Worker `time-quc`, then wait for /api/health.
CLOUDFLARE_API_TOKEN=... CLOUDFLARE_ACCOUNT_ID=... python3 quc/deploy.py
CLOUDFLARE_API_TOKEN=... CLOUDFLARE_ACCOUNT_ID=... VENICE_API_KEY=... python3 quc/deploy.py
Reuses .github/time/cloudflare.py. Idempotent: the script is replaced in place, the custom domain re-attached.
Reuses .github/time/cloudflare.py. Idempotent: the KV namespace `time-quc` is created once and reused, the script is replaced
in place, the custom domain re-attached. VENICE_API_KEY is bound as a Worker secret (without it the agent answers offline).
"""
from __future__ import annotations
@@ -19,7 +20,7 @@ sys.path.insert(0, str(HERE.parent / ".github" / "time"))
import cloudflare as cf # noqa: E402
NAME = "time-quc"
SRC = HERE / "src" / "worker.js"
MODULES = ["worker.js", "page.js", "runs.js", "engine.js", "agent.js", "diff.js", "validate.js"]
def healthy(url: str, tries: int = 18) -> bool:
@@ -34,16 +35,31 @@ def healthy(url: str, tries: int = 18) -> bool:
return False
def kv_namespace(title: str) -> str:
for ns in cf.cf("GET", f"{cf.account()}/storage/kv/namespaces?per_page=100") or []:
if ns.get("title") == title:
return ns["id"]
return cf.cf("POST", f"{cf.account()}/storage/kv/namespaces", {"title": title})["id"]
def deploy() -> dict:
if not cf.enabled():
sys.exit("CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID are needed")
os.environ.setdefault("TIME_VENTURE_DOMAIN", "yolopreneur.lol")
code = SRC.read_bytes()
rev = hashlib.sha256(code).hexdigest()[:12]
meta = {"main_module": "worker.js", "compatibility_date": cf.COMPAT,
"bindings": [{"type": "plain_text", "name": "REV", "text": rev}]}
data, ctype = cf.multipart([("metadata", "application/json", json.dumps(meta).encode()),
("worker.js", "application/javascript+module", code)])
code = {m: (HERE / "src" / m).read_bytes() for m in MODULES}
rev = hashlib.sha256(b"".join(code[m] for m in MODULES)).hexdigest()[:12]
bindings = [{"type": "kv_namespace", "name": "KV", "namespace_id": kv_namespace(NAME)},
{"type": "plain_text", "name": "REV", "text": rev}]
for k in ("CODER_MODEL", "PERSONA_MODEL", "RUNS_PER_DAY", "RUNS_PER_DAY_ALL"):
if os.environ.get(k):
bindings.append({"type": "plain_text", "name": k, "text": os.environ[k]})
if os.environ.get("VENICE_API_KEY"):
bindings.append({"type": "secret_text", "name": "VENICE_API_KEY", "text": os.environ["VENICE_API_KEY"]})
else:
print("warning: no VENICE_API_KEY, so the agent gives offline answers", file=sys.stderr)
meta = {"main_module": "worker.js", "compatibility_date": cf.COMPAT, "bindings": bindings}
data, ctype = cf.multipart([("metadata", "application/json", json.dumps(meta).encode())]
+ [(m, "application/javascript+module", code[m]) for m in MODULES])
cf.cf("PUT", f"{cf.account()}/workers/scripts/{NAME}", data=data, ctype=ctype)
return {"url": cf.attach(NAME, cf.host("quc")), "worker": NAME, "rev": rev}
+9 -2
View File
@@ -1,9 +1,16 @@
// Run the Worker locally: `npm start` in quc/, then http://localhost:8787/
// Without VENICE_API_KEY in the environment the agent gives canned offline answers; with it, it uses the real models.
import { createServer } from "node:http";
import worker from "./src/worker.js";
import { memoryStore } from "./src/runs.js";
const env = { REV: "dev", KV: memoryStore(), VENICE_API_KEY: process.env.VENICE_API_KEY || "" };
createServer(async (q, s) => {
const r = await worker.fetch(new Request(`http://${q.headers.host}${q.url}`, { method: q.method }), { REV: "dev" });
const chunks = [];
for await (const c of q) chunks.push(c);
const body = chunks.length && q.method !== "GET" && q.method !== "HEAD" ? Buffer.concat(chunks) : undefined;
const r = await worker.fetch(new Request(`http://${q.headers.host}${q.url}`, { method: q.method, headers: q.headers, body }), env);
s.writeHead(r.status, Object.fromEntries(r.headers));
s.end(Buffer.from(await r.arrayBuffer()));
}).listen(process.env.PORT || 8787);
}).listen(process.env.PORT || 8787, () => console.log(`quc on http://localhost:${process.env.PORT || 8787}/`));
+2 -2
View File
@@ -1,9 +1,9 @@
{
"name": "quc",
"version": "0.0.0",
"version": "0.1.0",
"private": true,
"type": "module",
"description": "quc.yolopreneur.lol: app shell + API in one Cloudflare Worker. No dependencies, no build step.",
"description": "quc.yolopreneur.lol: hand an agent your repo, sit in the chair, watch. One Cloudflare Worker, no dependencies, no build step.",
"scripts": {
"start": "node dev.mjs",
"test": "node --test",
+172
View File
@@ -0,0 +1,172 @@
import { test, beforeEach } from "node:test";
import assert from "node:assert/strict";
import worker, { validate } from "../src/worker.js";
import { memoryStore, createRun, getRun, getEvents } from "../src/runs.js";
import { step, say, safeword, patch } from "../src/engine.js";
import { untar, parseRepo } from "../src/agent.js";
import { unified } from "../src/diff.js";
// ---------- a fake GitHub: a gzipped tarball built in memory ----------
function tar(files) {
const enc = new TextEncoder(), blocks = [];
for (const [path, body] of Object.entries(files)) {
const data = enc.encode(body), h = new Uint8Array(512);
h.set(enc.encode("repo-abc123/" + path).subarray(0, 100), 0);
h.set(enc.encode("0000644\0"), 100);
h.set(enc.encode(data.length.toString(8).padStart(11, "0") + "\0"), 124);
h[156] = 48;
h.set(enc.encode("ustar\0"), 257);
blocks.push(h, data, new Uint8Array((512 - (data.length % 512)) % 512));
}
blocks.push(new Uint8Array(1024));
const out = new Uint8Array(blocks.reduce((n, b) => n + b.length, 0));
let i = 0; for (const b of blocks) { out.set(b, i); i += b.length; }
return out;
}
const gzip = async (bytes) => new Uint8Array(await new Response(new Blob([bytes]).stream().pipeThrough(new CompressionStream("gzip"))).arrayBuffer());
const REPO = { "README.md": "# demo\n\nA tiny repo.\n", "src/index.js": "export const x = 1;\n", "logo.png": "\0\0binary" };
let tgz;
const github = async (url) => url.startsWith("https://codeload.github.com/") ? new Response(tgz, { headers: { "content-length": String(tgz.length) } }) : new Response("no", { status: 404 });
beforeEach(async () => { tgz = await gzip(tar(REPO)); });
async function drive(store, id, n = 20) {
for (let i = 0; i < n; i++) { const r = await step(store, id, {}, github); if (r.status === "ended") return r; }
return getRun(store, id);
}
// ---------- unit ----------
test("validate accepts a GitHub URL and a one-line task", () => {
assert.deepEqual(validate("https://github.com/a/b", "make it better please"), {});
assert.ok(validate("https://gitlab.com/a/b", "make it better please").repo);
assert.ok(validate("https://github.com/a/b", "short").task);
});
test("parseRepo and untar read GitHub's tarball shape", async () => {
assert.deepEqual(parseRepo("https://github.com/o/n.git"), { owner: "o", name: "n" });
const files = untar(tar({ "a.txt": "hi", "d/b.txt": "yo" })).map((f) => f.path);
assert.deepEqual(files, ["a.txt", "d/b.txt"]);
});
test("unified diff counts and marks lines", () => {
const d = unified("f", "a\nb\nc\n", "a\nB\nc\nd\n");
assert.equal(d.added, 2); assert.equal(d.removed, 1);
assert.match(d.text, /^--- a\/f\n\+\+\+ b\/f\n@@ -1,3 \+1,4 @@/);
assert.equal(unified("f", "same\n", "same\n").text, "");
});
// ---------- the run ----------
test("a run clones, plans, rewrites and finishes, and the patch has the change", async () => {
const store = memoryStore();
const run = await createRun(store, { repo: "https://github.com/o/demo", task: "make the readme nicer", visitor: "v1" });
const done = await drive(store, run.id);
assert.equal(done.status, "ended"); assert.equal(done.outcome, "finished");
const kinds = (await getEvents(store, run.id)).map((e) => e.kind);
assert.ok(kinds.includes("diff") && kinds.includes("commit") && kinds.includes("commentary"));
assert.equal(done.stats.files, 1);
assert.match(await patch(store, run.id), /\+quc was here\./);
assert.equal(await store.get("active:v1"), null, "the visitor's slot is free again");
});
test("one active run per visitor", async () => {
const store = memoryStore();
await createRun(store, { repo: "https://github.com/o/demo", task: "make the readme nicer", visitor: "v1" });
await assert.rejects(createRun(store, { repo: "https://github.com/o/demo", task: "another one please", visitor: "v1" }), { code: "active" });
});
test("the safeword stops the run and throws the work away", async () => {
const store = memoryStore();
const run = await createRun(store, { repo: "https://github.com/o/demo", task: "make the readme nicer", visitor: "v1" });
await step(store, run.id, {}, github);
const r = await safeword(store, run.id);
assert.equal(r.outcome, "safeworded");
assert.equal(await patch(store, run.id), "");
assert.equal((await step(store, run.id, {}, github)).status, "ended", "a stopped run never moves again");
});
test("talking from the chair: mocked until the phrases unlock notes, a veto, then the wheel", async () => {
const store = memoryStore();
const run = await createRun(store, { repo: "https://github.com/o/demo", task: "make the readme nicer", visitor: "v1" });
await step(store, run.id, {}, github); // cloned
await step(store, run.id, {}, github); // planned: README.md
await step(store, run.id, {}, github); // README.md rewritten (and finished, one file planned)
const r1 = await getRun(store, run.id);
assert.equal(r1.status, "ended");
const store2 = memoryStore();
const run2 = await createRun(store2, { repo: "https://github.com/o/demo", task: "make the readme nicer", visitor: "v2" });
await step(store2, run2.id, {}, github); await step(store2, run2.id, {}, github);
assert.equal((await say(store2, run2.id, "hey stop")).kind, "commentary");
assert.equal((await getRun(store2, run2.id)).level, 0);
await say(store2, run2.id, "May I say something?");
assert.equal((await getRun(store2, run2.id)).level, 1);
await say(store2, run2.id, "please keep it short");
assert.deepEqual((await getRun(store2, run2.id)).notes, ["please keep it short"]);
await say(store2, run2.id, "not that one!");
await say(store2, run2.id, "README.md");
const vetoed = await getRun(store2, run2.id);
assert.deepEqual(vetoed.vetoed, ["README.md"]); assert.deepEqual(vetoed.queue, []);
await say(store2, run2.id, "My repo, my rules.");
const end = await getRun(store2, run2.id);
assert.equal(end.status, "ended"); assert.equal(end.outcome, "unlocked");
});
test("a veto after the edit reverts it", async () => {
const store = memoryStore();
const run = await createRun(store, { repo: "https://github.com/o/demo", task: "make the readme nicer", visitor: "v1" });
const r = await getRun(store, run.id);
await step(store, run.id, {}, github); await step(store, run.id, {}, github);
const live = await getRun(store, run.id); live.queue.push("src/index.js"); await store.put(`run:${run.id}`, JSON.stringify(live));
await step(store, run.id, {}, github); // README.md edited, src/index.js still queued
assert.match(await patch(store, run.id), /README/);
await say(store, run.id, "may i say something"); await say(store, run.id, "not that one"); await say(store, run.id, "README.md");
assert.equal(await patch(store, run.id), "");
const ev = (await getEvents(store, run.id)).filter((e) => e.kind === "diff");
assert.ok(ev.at(-1).data.reverted);
assert.ok(r);
});
// ---------- the Worker ----------
const call = (path, { method = "GET", body, cookie, origin } = {}) => worker.fetch(new Request("https://quc.test" + path, {
method, body: body ? JSON.stringify(body) : undefined,
headers: { "content-type": "application/json", ...(cookie ? { cookie } : {}), ...(origin ? { origin } : {}) } }), env);
let env;
beforeEach(() => { env = { KV: memoryStore(), REV: "t" }; globalThis.fetch = github; });
test("health, page and chair render", async () => {
assert.deepEqual(await (await call("/api/health")).json(), { ok: true, rev: "t", live: false });
const page = await (await call("/")).text();
assert.match(page, /Hand over your repo/); assert.match(page, /function validate/);
assert.equal((await call("/run/abc123def0")).status, 200);
assert.match(await (await call("/chair.svg")).text(), /<svg/);
});
test("API: owner drives the run, others can only watch, the patch downloads", async () => {
const res = await call("/api/runs", { method: "POST", body: { repo: "https://github.com/o/demo", task: "make the readme nicer" } });
assert.equal(res.status, 201);
const cookie = res.headers.get("set-cookie").split(";")[0];
const { id } = await res.json();
assert.equal((await call(`/api/runs/${id}/step`, { method: "POST", body: {} })).status, 403, "a stranger can't drive");
assert.equal((await call(`/api/runs/${id}/step`, { method: "POST", body: {}, cookie, origin: "https://evil.test" })).status, 403, "no cross-site");
for (let i = 0; i < 6; i++) await call(`/api/runs/${id}/step`, { method: "POST", body: {}, cookie });
const view = await (await call(`/api/runs/${id}`)).json();
assert.equal(view.owner, false); assert.equal(view.run.status, "ended"); assert.equal(view.run.visitor, undefined);
assert.ok(view.events.length >= 4);
const p = await call(`/api/runs/${id}/patch`);
assert.match(await p.text(), /quc was here/);
const again = await (await call(`/api/runs/${id}?after=${view.events.length}`)).json();
assert.equal(again.events.length, 0);
});
test("API: daily cap per visitor", async () => {
env.RUNS_PER_DAY = 1;
const first = await call("/api/runs", { method: "POST", body: { repo: "https://github.com/o/demo", task: "make the readme nicer" } });
const cookie = first.headers.get("set-cookie").split(";")[0];
const { id } = await first.json();
await call(`/api/runs/${id}/safeword`, { method: "POST", body: {}, cookie });
const second = await call("/api/runs", { method: "POST", body: { repo: "https://github.com/o/demo", task: "make the readme nicer" }, cookie });
assert.equal(second.status, 429);
});
+15 -22
View File
@@ -1,33 +1,32 @@
import test from "node:test";
import assert from "node:assert/strict";
import worker from "../src/worker.js";
import worker, { validate } from "../src/worker.js";
const get = (path, env, method = "GET") => worker.fetch(new Request("http://x" + path, { method }), env);
const get = (path, env = {}, method = "GET") => worker.fetch(new Request("http://x" + path, { method }), env);
test("health reports ok and the revision", async () => {
test("health reports ok, the revision, and whether real models are bound", async () => {
const r = await get("/api/health", { REV: "abc" });
assert.equal(r.status, 200);
assert.deepEqual(await r.json(), { ok: true, rev: "abc" });
assert.deepEqual(await r.json(), { ok: true, rev: "abc", live: false });
assert.equal((await (await get("/api/health", { VENICE_API_KEY: "k" })).json()).live, true);
});
test("shell is responsive html that loads the motif", async () => {
test("the page is responsive html, the motif is an svg, unknown paths 404", async () => {
const r = await get("/");
const html = await r.text();
assert.match(r.headers.get("content-type"), /text\/html/);
const html = await r.text();
assert.match(html, /name="viewport" content="width=device-width/);
assert.match(html, /chair\.svg/);
});
test("motif is an svg, unknown paths 404, writes are refused", async () => {
assert.match(html, /<form id="form"/);
assert.match(html, /Replays are public/);
const svg = await get("/chair.svg");
assert.equal(svg.headers.get("content-type"), "image/svg+xml");
assert.match(await svg.text(), /^<svg/);
assert.equal((await get("/nope")).status, 404);
assert.equal((await get("/api/health", {}, "POST")).status, 405);
assert.equal((await get("/run/a/b")).status, 404);
assert.equal((await get("/", {}, "POST")).status, 405);
});
test("validate accepts a repo + task and rejects bad shapes", async () => {
const { validate } = await import("../src/worker.js");
test("validate accepts a repo + task and rejects bad shapes", () => {
const task = "Fix the flaky date test";
assert.deepEqual(validate("https://github.com/a/b", task), {});
assert.deepEqual(validate("https://github.com/a/b.git/", task), {});
@@ -37,13 +36,7 @@ test("validate accepts a repo + task and rejects bad shapes", async () => {
assert.ok(validate("https://github.com/a/b", "x".repeat(201)).task);
});
test("start form, public-replay notice and run page are served; the inlined validator parses", async () => {
const html = await (await get("/")).text();
assert.match(html, /<form id="form"/);
assert.match(html, /Replays are public/);
const run = await get("/run/abc123");
assert.equal(run.status, 200);
assert.match(await run.text(), /id="run-id"/);
assert.equal((await get("/run/a/b")).status, 404);
new Function(html.match(/<script>([\s\S]*)<\/script>/)[1]); // syntax check of the shipped script
test("the shipped script parses", async () => {
const html = await (await get("/run/abc123def0")).text();
new Function(html.match(/<script type="module">([\s\S]*)<\/script>/)[1]);
});