Tests That Keep Freddy Honest
Six chapters in, most of Freddy's code was written by an
agent. This chapter is about the only verification that
doesn't depend on the agent that did the writing: tests.
And about /goal, which turns "make the tests
pass" into a loop you don't have to supervise.
The problem
Agent-written code needs agent-independent verification. "It looked right in the diff" is the agent grading its own homework; "the diagnostics were clean" only proves it compiles. Sessions end, contexts compact, and next month a different model — maybe a different vendor — will edit this code. Tests are the only contract that survives all of that. They don't care which model wrote the code or which one is about to change it.
The hard way
Two classic failure modes. The first:
> write some tests for the notes feature
Created tests/Feature/NoteTest.php with 6 tests.
Open the file and you find assertion-free stubs:
expect(true)->toBeTrue() with an
optimistic comment, or tests that call the endpoint and
assert only the status code. Green checkmarks, zero
protection. The second failure mode is worse because it
feels responsible: you skip tests entirely and manually
click through Capture, Browse, and Archive after every
change. Forever. That's not verification, that's a
part-time job.
The Elyra way: install test-gen
/ext
Select @elyracode/test-gen (space,
enter), then /reload. Two tools arrive:
-
find_untested— scans the project for source files without corresponding tests and returns a prioritized list. -
generate_tests— reads the actual source and generates real tests: happy path, edge cases, mocks where needed. It auto-detects the framework — Pest for Freddy's PHP, Vitest if you point it at TypeScript.
Generating real tests
Start with the gap analysis:
> What's untested in Freddy? Prioritize.
find_untested
Untested source files (highest priority first):
1. app/Http/Controllers/NoteController.php
- store, update, archive; core write paths
2. app/Models/Note.php
- tag attachment logic, archived scope
3. app/Http/Controllers/TagController.php
The write paths first — that's where regressions hurt. Then:
> Generate tests for NoteController and the Note model.
generate_tests app/Http/Controllers/NoteController.php
generate_tests app/Models/Note.php
Created tests/Feature/NoteControllerTest.php (7 tests)
Created tests/Unit/NoteTest.php (4 tests)
The difference from "write some tests": these are built from what the code actually does, not from what a test file usually looks like. A sample of what lands:
it('rejects a note without content', ...)
// store() validates content as required
it('creates missing tags and attaches existing ones', ...)
// the create-or-attach path from Chapter 5
it('archive sets archived_at without deleting', ...)
// soft-delete semantics per SPEC.md
it('inserts an FTS row when a note is created', ...)
// the notes_fts trigger from Chapter 5, verified
That last one matters: the FTS5 sync triggers are exactly the kind of invisible machinery that breaks silently six weeks from now. Now they can't.
The /goal loop
Generated tests need to actually pass — and this is
where the chapter's star command comes in. AGENTS.md
already says tests run through composer test,
so:
/goal composer test
/goal gives the agent a command and one
instruction: keep working until it exits 0. Watch the
first run:
[Goal: composer test] Run 1
FAILED Tests\Feature\NoteControllerTest
> creates missing tags and attaches existing ones
Expected 1 row in note_tag for tag 'ideas', found 2.
FAILED Tests\Unit\NoteTest
> archived scope excludes archived notes
Undefined scope 'active' on App\Models\Note.
Two failures. The second is a straightforward miss — the scope was referenced but never written. The first is the interesting one: the test revealed a real bug. Re-attaching a tag that's already on the note creates a duplicate pivot row. The tag feature from Chapter 5 has been quietly wrong this whole time; nothing surfaced it because nothing ever attached the same tag twice.
Here's the moment that separates a good loop from a useless one. The lazy fix is to bend the test — assert 2 rows and move on. The agent doesn't, and it says why:
The test expectation is correct: attaching an existing tag
must be idempotent. Fixing the implementation, not the test
- per AGENTS.md, tests define behavior. Switching the pivot
attach to syncWithoutDetaching.
[Goal: composer test] Run 2
PASSED Tests\Feature\NoteControllerTest (7 tests)
PASSED Tests\Unit\NoteTest (4 tests)
Goal completed: composer test exited 0.
Two runs, one real bug fixed, zero supervision. For bigger goals where the loop might run long, cap the spend:
/goal composer test --budget 2
The agent stops when the goal is met or when $2 is spent, whichever comes first. Cheap insurance for unattended loops.
generate_tests
if the agent didn't write them alongside, then
/goal composer test before every commit.
What you learned
- Tests are the definition of done. The only contract that survives sessions, compaction, and model switches.
-
generate_testsreads the code. Real assertions from real behavior — not assertion-free stubs. -
/goalmakes "make tests pass" unattended. The agent iterates until the command exits 0;--budgetcaps the spend. - Fix the implementation, never bend the test. When a test fails, first ask which one is wrong — the duplicate-pivot bug was caught because the test held its ground.
/review --cross
doesn't.