Chapter 7 of 14

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.

Habit from here on: AGENTS.md already says new features need a test. Now the workflow is concrete: build the feature, 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_tests reads the code. Real assertions from real behavior — not assertion-free stubs.
  • /goal makes "make tests pass" unattended. The agent iterates until the command exits 0; --budget caps 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.
Next: in Chapter 8 Freddy's code gets reviewed — by a model from a different vendor than the one that wrote it. Same-family review shares the author's blind spots; /review --cross doesn't.