DEV Community

Charlie Zhu
Charlie Zhu

Posted on

The Replay Box Workshop

The projector showed a green check. A student had asked a coding assistant for a refund endpoint, pasted the function into a file, and hit the route from a laptop. The JSON came back {"ok": true, "refunded": 400}. The applause lasted until the same curl ran on the shared classroom server and returned 500. The request that had "worked" was already gone. Nobody could prove which headers had been sent, which body the model had assumed, or whether the handler had credited twice. The room did not need a sharper prompt. It needed a replay box.

A replay box is a boring object with a sharp edge. It is a directory of frozen requests, a tiny HTTP listener, and a second command that fires those requests again and compares bytes. Generated code is allowed to be wrong. It is not allowed to be unrepeatable. Public talk this week keeps treating "vibe coding" as a taste fight. This workshop stays on the floor. If a classmate cannot replay the failure, the work is not finished.

The session runs eighty minutes. It assumes Python 3.11 or newer, a terminal, and no framework religion. Students may draft the first handler with any assistant. When a laptop has no paid key, MonkeyCode's free model access and free server option are one way to keep the draft and the listener in the same place. Disclosure: This article was prepared as part of MonkeyCode's product outreach. The product is not the subject. The box is.

Minute 0 to 15: the scene, not the sermon

The facilitator retells a story like the one above, then drops a naive handler on disk. The handler refunds a cart total it trusts from the client. That is the whole bug. Students are told not to improve it yet. They must first catch it in a box.

# refund.py
from http.server import BaseHTTPRequestHandler, HTTPServer
import json

class RefundHandler(BaseHTTPRequestHandler):
    charged = 0

    def do_POST(self):
        length = int(self.headers.get("Content-Length", "0"))
        raw = self.rfile.read(length)
        try:
            body = json.loads(raw.decode("utf-8") or "{}")
        except json.JSONDecodeError:
            return self.reply(400, {"error": "bad json"})
        amount = int(body.get("amount", 0))
        RefundHandler.charged += amount
        self.reply(200, {
            "ok": True,
            "refunded": amount,
            "total": RefundHandler.charged,
        })

    def reply(self, code, payload):
        data = json.dumps(payload).encode("utf-8")
        self.send_response(code)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(data)))
        self.end_headers()
        self.wfile.write(data)

    def log_message(self, fmt, *args):
        return

if __name__ == "__main__":
    HTTPServer(("127.0.0.1", 8088), RefundHandler).serve_forever()
Enter fullscreen mode Exit fullscreen mode

The analogy is a flight recorder, not a linter. A linter argues with style. A recorder argues with time. If the second flight cannot be flown, the first flight did not happen for anyone else.

Minute 15 to 35: freeze the request

Students write a client that does two jobs: send, then save. The save is the whole point. A pretty print in the terminal is a souvenir. A file under fixtures/ is evidence.

# freeze.py
import json, pathlib, urllib.request

FIXTURES = pathlib.Path("fixtures")
FIXTURES.mkdir(exist_ok=True)

def freeze(name, url, payload):
    body = json.dumps(payload).encode("utf-8")
    req = urllib.request.Request(url, data=body, method="POST")
    req.add_header("Content-Type", "application/json")
    with urllib.request.urlopen(req) as resp:
        raw = resp.read()
        record = {
            "name": name,
            "url": url,
            "status": resp.status,
            "request": payload,
            "response": json.loads(raw.decode("utf-8")),
        }
    path = FIXTURES / f"{name}.json"
    path.write_text(json.dumps(record, indent=2), encoding="utf-8")
    print(f"wrote {path}")

if __name__ == "__main__":
    freeze("refund_once", "http://127.0.0.1:8088/", {"amount": 400})
    freeze("refund_again", "http://127.0.0.1:8088/", {"amount": 400})
Enter fullscreen mode Exit fullscreen mode

They start the server in one terminal with python3 refund.py and freeze in another with python3 freeze.py. The second fixture is the trap. The handler keeps a class variable, so a second identical refund inflates total. Students who only watched the first green check will miss it. The box does not.

A facilitator who wants the failure to land harder can open fixtures/refund_again.json on the projector and read the total field aloud. The number is not a mystery. It is yesterday's memory wearing today's clothes.

Minute 35 to 55: replay, then fail on purpose

A replay script must not trust the live process's memory. It either boots a fresh server as a child or it demands a restart so charged returns to zero. This workshop uses a restart. That constraint is pedagogical. Hidden process state is how generated services lie in public.

# replay.py
import json, pathlib, urllib.request, sys

def replay(path):
    record = json.loads(path.read_text(encoding="utf-8"))
    body = json.dumps(record["request"]).encode("utf-8")
    req = urllib.request.Request(record["url"], data=body, method="POST")
    req.add_header("Content-Type", "application/json")
    with urllib.request.urlopen(req) as resp:
        got = json.loads(resp.read().decode("utf-8"))
        status = resp.status
    ok = status == record["status"] and got == record["response"]
    print(path.name, "PASS" if ok else "FAIL")
    if not ok:
        print("  expected", record["response"])
        print("  got     ", got)
        return 1
    return 0

if __name__ == "__main__":
    failed = 0
    for path in sorted(pathlib.Path("fixtures").glob("*.json")):
        failed |= replay(path)
    sys.exit(failed)
Enter fullscreen mode Exit fullscreen mode

After a restart, refund_again.json should fail if the first freeze already mutated memory before the second freeze. That sentence is the lesson. A fixture captured against dirty state is a tainted tape. Students delete fixtures/, restart, and freeze in a known order, or they split the handler so each test process is born empty. Either fix is engineering. Pasting a new prompt is not.

The worked example the facilitator keeps off-screen until the replay is red is a third file, refund_id.py. It keys refunds on an Idempotency-Key header and refuses a second credit. Green-first teaching trains people to hide the bug. Red-first teaching makes the extra six lines feel earned.

# snippet from refund_id.py — labeled example, not a full payment system
seen = set()

def apply_refund(key, amount):
    if not key:
        raise ValueError("missing idempotency key")
    if key in seen:
        return {"ok": True, "refunded": 0, "duplicate": True}
    seen.add(key)
    return {"ok": True, "refunded": amount, "duplicate": False}
Enter fullscreen mode Exit fullscreen mode

They extend freeze.py to send the header and extend replay.py to restore it. The header is a seat belt. The fixture is the crash test. Students who skip the header and only "ask the model to make it idempotent" often get a comment and no behavior. The replay file is the only grader in the room.

Minute 55 to 75: move the box off the laptop

Laptops lie in chorus. Different Python patch levels, different meanings of localhost, a student VPN that rewrites ports. The last teaching block moves the same three files onto one shared host so every replay aims at the same listener. A classroom that already has a VM should use it. A classroom that does not can park the listener on a shared free server and keep the fixtures in git. The model is useful only for drafting apply_refund. The server is useful for making the draft answer to someone else.

Commands stay small on purpose.

git init
git add refund.py freeze.py replay.py
git commit -m "replay box v0"
python3 refund.py &
python3 freeze.py
python3 replay.py
Enter fullscreen mode Exit fullscreen mode

If replay exits non-zero, the commit is not tagged. That is the only grade. No screenshot of a chat window counts. A student who cannot explain why total changed between two identical bodies has not finished, even if an assistant later rewrites the handler in a prettier style.

One optional drill fills leftover minutes. The facilitator sabotages refund.py by sorting JSON keys differently or by adding a server_time field. Replay goes red for a reason that is not a logic bug. The class then learns to pin only the fields it owns, or to inject a clock. Non-deterministic assistants produce the same mess in the wild. The box makes that mess visible instead of mystical.

Minute 75 to 80: what the box will not do

The replay box does not prove safety. It proves that yesterday's bytes still happen. It will not catch a race, a clock skew, or a model that invents a field the fixture never named. It should not hold secrets, payment card numbers, or production URLs. Teams that already have contract tests in CI do not need this ritual; they already own a stricter box. Students who cannot restart a process should not pretend a class variable is a database.

The approach also fails when the assistant writes noisy output: timestamps, random ids, floating error text. Those belong behind a clock or an id injector, or the fixture will flap and train people to ignore red. Ignore that warning and the workshop becomes another source of flaky green. It is also the wrong tool for load, authz matrices, or anything that must survive a real payment network. Those problems need different recorders.

Who should skip the session is as important as who should run it. Facilitators teaching production finance, health data, or anything with a retention rule should not point a free shared host at real payloads. People hunting model leaderboards will be bored. People who want a screenshot of "the AI did it" will be annoyed, which is the point.

The public mood around generated code keeps asking whether the work still counts as engineering. A replay box answers a smaller claim a classroom can grade: whether a stranger can run the same request tomorrow and get the same bytes. If that claim is false, the green check was a performance. If it is true, the model was a draftsperson and the box was the editor.

Facilitators who need a shared target without standing up hardware can park the listener on MonkeyCode's free server option and keep every fixture in the repository. The useful part still fits in three files. Remove the product names and the workshop does not shrink.

Top comments (0)