The payment service sat on a green pipeline at noon. Forty new tests had landed from an overnight agent. Every assertion matched a fixture the agent also wrote. Staging accepted the card charge while production rejected it. The mock had never seen the real processor's 409 body. A free lane had authored both the patch and the proof.
This guide treats that pattern as a refuse case. Generated tests may explore a change under review. They must not certify that change for merge.
The shared-bias failure
An agent on a cheap lane writes product code. The same agent then writes the blessing tests. Continuous integration reports only a clean green run. Reviewers often trust that badge without reading asserts. The oracle and the implementation share one bias.
Free-tier endpoints drop context without a warning. They retry later with shorter and weaker prompts. They skip edge cases that look expensive to sample. None of that drift shows up in a passing badge.
The merge queue then ships a self-checked change. On-call inherits the miss during the next incident.
What counts as a test oracle
A test file is not an oracle by itself. An oracle is the bit that decides pass or fail. That bit must come from a pinned external source.
Treat the following artifacts as real oracles:
- OpenAPI or protobuf snapshots already checked into git
- HTTP fixtures recorded from a named staging host
- Golden JSON with a human
Signed-off-bytrailer - Contract tests compiled from a locked schema file
- Manual acceptance checks owned by a named reviewer
Treat the following artifacts as untrusted input:
- Asserts an agent invented inside the same product diff
- Mocks the agent derived from its own new patch
- Coverage totals produced only on a free runner
- Review comments from a free model with merge rights
- Snapshots regenerated without a human fixture diff
Red flags: when not to use a free lane
Teams refuse a free lane when any rule below matches.
- The agent authors tests and production code together.
- The suite has no fixture sourced outside the model.
- Failures retry until the model rewrites the assert.
- The runner can reach deploy keys or release signing.
- The change touches money, identity, or deletion paths.
- Coverage is the only merge requirement on the branch.
- No human is named as oracle owner on the pull request.
- The free server also stores secrets or recorded payloads.
A free model can draft a test sketch. A free server can run that sketch in isolation. Neither should own the bit that opens the merge queue.
Teams sometimes park exploratory agents on MonkeyCode. The open-source project offers free model access and a free server option for disposable scratch work.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
Those options help when the task is throwaway exploration. They do not help when the task is certification. Keep any free lane, including that one, off the oracle path.
Decision table
Teams apply this table before generated tests touch main.
| Signal | Free-lane tests allowed | Required action |
|---|---|---|
| Draft unit tests on a feature branch | Yes, behind a scratch/ prefix |
Delete or rewrite before review |
| Contract tests from a locked schema | No authoring; compile only | Human pin on the schema file |
| Snapshot refresh in the same product diff | No | Split the snapshot into a second PR |
| Payment, auth, or purge behavior | No | Paid or pinned model plus human oracle |
| Coverage gate as the only merge check | No | Add a pinned contract or recorded fixture |
Runner holds DEPLOY_KEY or sigstore material |
No | Move tests to a no-secret worker |
| Agent retries until the assert goes green | No | Fail the job on first oracle mismatch |
Human oracle-owner trailer present |
Conditional | Owner must diff fixtures, not code only |
The table is a refuse map, not a permission map. Ambiguous rows stay in the No column.
Artifact: a fail-closed oracle gate
The workflow below is a labeled, unexecuted example. Operators should try it on a throwaway branch first. It does not claim production metrics.
Pin owners beside the fixtures
# .oracle-owners
# format: glob<TAB>owner<TAB>source
contracts/*.openapi.json payments-oncall git-pin
fixtures/http/*.json payments-oncall staging-record
tests/oracle/*.py qa-lead human-assert
Oracle files stay small and boring. Product tests may import them. Product tests may not rewrite them in the same commit.
Reject mixed authorship
# oracle_gate.py — proposal / unexecuted example
from __future__ import annotations
import os
import subprocess
import sys
from pathlib import Path
ORACLE_OWNERS = Path(".oracle-owners")
SCRATCH_PREFIX = "tests/scratch/"
MIXED_EXIT = 12
def git_lines(diff_range: str) -> list[str]:
out = subprocess.check_output(
["git", "diff", "--name-only", diff_range],
text=True,
)
return [line.strip() for line in out.splitlines() if line.strip()]
def load_oracles() -> list[str]:
globs: list[str] = []
for raw in ORACLE_OWNERS.read_text().splitlines():
line = raw.strip()
if not line or line.startswith("#"):
continue
globs.append(line.split()[0])
return globs
def matches(path: str, glob_pat: str) -> bool:
return Path(path).match(glob_pat)
def main() -> int:
lane = os.environ.get("AGENT_LANE", "unknown")
diff_range = os.environ.get("ORACLE_DIFF", "origin/main...HEAD")
changed = git_lines(diff_range)
oracles = load_oracles()
oracle_hits = [
path for path in changed
if any(matches(path, g) for g in oracles)
]
product_hits = [
path for path in changed
if path.endswith((".py", ".ts", ".go"))
and path not in oracle_hits
and not path.startswith(SCRATCH_PREFIX)
]
print(f"lane={lane} oracles={oracle_hits} product={product_hits}")
if lane == "free" and oracle_hits:
print("refuse: free lane touched an oracle file")
return MIXED_EXIT
if oracle_hits and product_hits:
print("refuse: mixed oracle and product authorship")
return MIXED_EXIT
if lane == "free" and product_hits:
unpinned_tests = [
path for path in product_hits
if "/test" in path or path.startswith("tests/")
]
if unpinned_tests:
print(f"refuse: free lane wrote tests {unpinned_tests}")
return MIXED_EXIT
return 0
if __name__ == "__main__":
sys.exit(main())
The script fails closed on mixed diffs. A free lane may still write files under tests/scratch/. Those files never gate merge.
Wire the gate in CI
# .github/workflows/oracle-gate.yml — proposal
name: oracle-gate
on:
pull_request:
types: [opened, synchronize, reopened]
jobs:
refuse-mixed-oracle:
runs-on: ubuntu-latest
env:
AGENT_LANE: ${{ vars.AGENT_LANE }}
ORACLE_DIFF: origin/${{ github.base_ref }}...HEAD
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Fail if a free lane owns the oracle
run: python3 oracle_gate.py
vars.AGENT_LANE stays a human-set variable. An agent must not write that variable. A missing value should be treated as unknown and refused on oracle files.
Record fixtures outside the model
# record_fixture.sh — proposal / unexecuted example
set -euo pipefail
HOST="${STAGING_HOST:?set STAGING_HOST}"
OUT="fixtures/http/charge-409.json"
curl -sS -D - -o /tmp/body.json \
-H "Authorization: Bearer ${STAGING_TOKEN}" \
-H "Content-Type: application/json" \
"${HOST}/v1/charges" \
-d '{"amount":0,"currency":"usd"}' \
> /tmp/headers.txt
python3 - <<'PY'
from pathlib import Path
import json
headers = Path("/tmp/headers.txt").read_text().splitlines()
status = headers[0]
body = json.loads(Path("/tmp/body.json").read_text())
Path("fixtures/http/charge-409.json").write_text(
json.dumps({"status": status, "body": body}, indent=2) + "\n"
)
PY
git add fixtures/http/charge-409.json
echo "Recorded ${OUT}. A human must commit this file."
The recorder talks to staging, not to a model. The agent may later write a test that reads the file. The agent may not edit the file in that same change.
Keep scratch tests quarantined
# tests/oracle/test_charge_conflict.py — human-owned oracle import
import json
from pathlib import Path
FIXTURE = Path("fixtures/http/charge-409.json")
def test_zero_amount_matches_recorded_409():
recorded = json.loads(FIXTURE.read_text())
assert "409" in recorded["status"]
assert recorded["body"].get("code") == "amount_zero"
# tests/scratch/test_agent_sketch.py — never a merge gate
def test_sketch_only():
# Agent sandbox. CI must not require this file.
assert True
Scratch files stay optional. Oracle files stay required. Mixing them is the bug.
Better alternatives
Teams that still want agent help keep a split workflow.
- A free lane drafts tests under
tests/scratch/only. - A human promotes one sketch into
tests/oracle/after reading it. - A recorded fixture or locked schema remains the pass/fail bit.
- A second, pinned model may review prose, never fixtures.
- Property tests use explicit generators checked in by a person.
- Merge queues require the oracle gate, not coverage alone.
The split costs a review cycle. That cycle is cheaper than a bad charge path.
Exit criteria
Teams leave free-lane test generation when any exit fires.
- An incident traces to an agent-written assert on
main. - Oracle files and product files share more than one commit.
- The free runner has ever seen a deploy key or customer payload.
- Retry loops have rewritten asserts to match broken code.
- Reviewers can no longer name the human oracle owner.
- Coverage rises while contract fixtures stay stale for a week.
After an exit, scratch tests remain allowed. Merge tests do not return to the free lane without a new pin.
Limitations of this gate
The script only sees git paths. It cannot see prompt injection inside a fixture. It cannot prove a staging record is current. It cannot stop a human from rubber-stamping a bad golden file.
Lane labels are only as honest as CI variables. A mis-set AGENT_LANE defeats the refuse path. Teams still need a CODEOWNERS file on contracts/ and fixtures/.
The gate also ignores test quality. A human-owned assert can still be wrong. Pinning authorship is necessary. It is not sufficient.
Who should not use this approach
This field guide is a refuse map for agent-authored tests. It is not a license to skip tests.
Skip a free-lane test workflow entirely when:
- The repo ships payments, health records, or identity changes
- Compliance needs a named evaluator for every release artifact
- The team cannot isolate a no-secret CI worker
- Staging cannot record fixtures without production data
- No person will own
.oracle-ownersweek after week
Those teams keep agents off tests. They write oracles first. They generate code against the oracle, never the reverse.
What this guide is not claiming
This article does not rank vendors. It does not publish latency numbers. It does not claim a free server is safe for secrets. It does not treat generated coverage as evidence.
Vibe-written tests can still teach a design. Calling those tests a merge gate is the error. Engineering keeps the oracle in a pinned file with a named owner.
Scratch agents can stay on a free model and a free server. Certification stays on recorded fixtures, locked schemas, and a fail-closed gate.
Top comments (0)