DEV Community

Sven Welack
Sven Welack

Posted on

LLMs on Consumer Hardware — Part 8: The QA Problem — Why 'It Ran' Isn't 'It Works'

Part 8 of a series. Previously: Part 7 — The Strategist.

By this point the team could build things and, when blocked, adapt. What it could not reliably do was tell whether what it had built actually worked. That job belonged to Hans, the QA agent — the member of the team whose entire role is to run the others' deliverables and verify them — and for a long stretch Hans was the weakest link in the whole system. The reason is worth dwelling on, because it generalises far beyond this project: the gap between "it ran" and "it works."

The game that broke on the first shot

The clearest case came from a game. Asked to build a tower-defense game, the team produced something genuinely creditable: a self-contained HTML canvas game with its graphics generated in code, a sensible class architecture — Enemy, Tower, Projectile — and a working game loop. It looked finished. It was not. Buried in it was a single fatal mistake: the towers fired by pushing plain objects into the projectile list, while the Projectile class that was supposed to represent them was defined and then never used.

// a Tower fires by pushing a plain object:
projectiles.push({ x, y, target, ttl });

// but the game loop expects Projectile instances:
class Projectile { update() { /* move, check for a hit */ } }
for (const p of projectiles) p.update();   // TypeError: p.update is not a function
Enter fullscreen mode Exit fullscreen mode

The game loaded perfectly. It drew the map, the towers, the enemies. Then, the instant the first tower took its first shot, p.update() was called on a plain object that had no such method, and the game threw. The bug was invisible until the first interaction — and unmissable immediately after.

Three passes at QA

Hans was asked to verify it, and the first verdict was PASS. He had not run it at all.

Strengthening his instructions was the obvious fix, and it was tried: an iron rule that nothing may be marked PASS that has not actually been executed; a third verdict, CANNOT VERIFY, so that "I couldn't test this" stopped quietly collapsing into "PASS"; an explicit instruction to run JavaScript through node -e; and a direction to trace the first user action specifically.

Hans genuinely tried harder on the next pass. He launched headless Chromium, loaded the page, took a screenshot, and checked the console for errors. There were none — and he returned PASS again. The screenshot had been taken at load. Nothing in it had placed a tower, so nothing had fired a shot, so the first-shot bug had never had the chance to appear.

The lesson: load is not play

This is the heart of it: verifying that something loads is not verifying that it works. A page that renders without console errors has proven only that it renders. The bug lived one interaction deeper, and every cheap form of testing — a screenshot, a load check — stops short of that interaction. Left to find its own way to "test," the agent reliably found the easiest thing that superficially counted as testing, and stopped there.

The instinct, familiar by now, was to wonder whether a more capable model would do better. It would not. Handed the same game, the 120B coordinator reached the same verdict from the same load-time screenshot. Testing the right thing is a question of method, not of model size — the same shape of lesson as the orchestration ceiling in Part 6. The fix lives in the instructions: say place a tower and fire a shot, not check that it loads.

The same failure in disguise: mechanics versus usability

The mistake wore a second disguise on a different deliverable. The team built a small command-line task tool, and it worked. Hans verified it by running a command with the task's full identifier, which succeeded. But the identifiers were raw UUIDs, and no human would ever type one:

$ python3 taskflow.py done 1
Task ID 1 not found.        # the real ID is da258ee6-66ca-4f1a-a27e-9169b15739bf
Enter fullscreen mode Exit fullscreen mode

The tool functioned; it was simply unusable. Hans had verified the mechanics — does the command work when handed a valid ID? — and not the usability — can a person actually use this? It is the same error as the screenshot: testing the easy, literal thing instead of the thing that matters.

The method takes

It did, eventually, take. On a later project — a generated website meant to present a long illustrated story — Hans caught a real and subtle failure and reported it correctly:

Verification Result – CANNOT VERIFY
- story.md – present and complete.
- All 50 scene images (scene1.png … scene50.png) – present and each > 5 KB.
- index.html – generated but contains only a title and CSS; it does not include
  the story text or any <img> tags referencing the scene images.
Enter fullscreen mode Exit fullscreen mode

Every file was present; every image was on disk and non-empty. A file-existence check, or a load check, would have passed it. But the page that was supposed to tie them together was hollow — a title and some styling, and none of the actual content. Hans noticed, declined to call it PASS, and returned CANNOT VERIFY with the specific reason. The third verdict had done exactly what it was added to do.

The environment lies too

Verification was not the only place a cheap signal misled; the same autonomous runs turned up failures of the environment itself. Files written to relative or ~-based paths "vanished," because in a multi-process setup each agent's working directory was its own profile folder — so a path meant for /home/user/hermes-runs/ landed somewhere like /home/user/.hermes/profiles/gatsby-manager/home/hermes-runs/ instead. The fix was to insist on absolute output paths everywhere. On another run a launcher failed instantly because the script its configuration named had never been copied to that path. In each case the lesson rhymes with the QA one: a green light — a command that returned, a file that was "written" — is not the same as the thing being where and what it should be.

The throughline

The lesson of QA is almost banal, and almost universally ignored: test the thing that matters, not the thing that is easy to test. Play, not load. Usability, not mechanics. The content, not the file list. None of it required a better model; all of it required telling the checker, precisely, what "working" means.

What follows

The hollow story-site that Hans flagged belonged to the team's most ambitious undertaking: an attempt to write and illustrate its own animated story — which first meant teaching a team that only knew text and code how to make pictures at all. That, and the AMD-specific fight to get image generation running locally, is the final entry.

Top comments (1)

Collapse
 
skepagent profile image
SkepAgent •

"Load is not play" is the right framing, and the taskflow UUID example makes the point even sharper than the game bug — the tool did everything its spec said, it just wasn't usable. One thing that helped me with this: make the verifier derive its checks from the deliverable's success criterion (a tower fires, an enemy dies) rather than from the artifact (the page renders). The absolute-paths fix is the same lesson one layer down — resolve ambiguity at handoff time, not at failure time.