Bake-off: tests in the prompt vs. a file beside it
A prompt and its evals can live in one file or two. Every number the toolchain prints says the choice does not matter. Then a typo, a rename and a directory build say otherwise.
TL;DR A .tg file and a sibling .test.tg merge into one suite, so the two layouts tie on check, score, coverage and the artifact hash, and the difference only appears when the suite breaks: a malformed case inline is a blocking structure/bad-test that refuses to build, while the same case in the sibling leaves check, score and build on the prompt file green.
The question arrives about a week after a team starts keeping prompts in git. The rules are forty lines and the eval suite is two hundred, the person who owns the rules is not the person who owns the suite, and somebody asks whether the cases should really be sitting at the bottom of the prompt. TypeGlish supports both answers: $TEST blocks inline, or a sibling file named <prompt>.test.tg that the toolchain picks up automatically. This is the bake-off, refereed the usual way, by running both.
§1The two contenders
A billing agent for a mobile network. Three rules, two cases, and nothing exotic in either: one case checks that the agent asks for an account number, and one checks that it never says a number back when asked about price.
# Role You are a billing support agent for Cobalt Mobile. # Constraints - MUST keep every reply to at most 3 sentences. - ALWAYS ask for an account number before you discuss a bill. - NEVER state a specific price. $TEST bill_query - input:: Why is my bill higher this month? - expect:: - contains "account number" $TEST no_price - input:: How much is the unlimited plan? - expect:: - matches /^(?!.*[0-9]).*$/
Contender B is the same text, cut along the dotted line. The rules stay in billing.tg; the cases move to billing.test.tg beside it. Nothing declares the relationship, and nothing needs to: the sibling is found by name.
# Role You are a billing support agent for Cobalt Mobile. # Constraints - MUST keep every reply to at most 3 sentences. - ALWAYS ask for an account number before you discuss a bill. - NEVER state a specific price.
$TEST bill_query - input:: Why is my bill higher this month? - expect:: - contains "account number" $TEST no_price - input:: How much is the unlimited plan? - expect:: - matches /^(?!.*[0-9]).*$/
.test.tg is an ordinary .tg file with an extension the tools recognise. Check it on its own and it is a prompt with no rules in it, which is worth remembering in §4.§2Round 1: nothing can tell them apart
Run everything. Both layouts check clean, both report the same rule coverage, both score identically down to the facet, and both build to the same artifact.
$ typeglish check billing.tg # A, one file ✓ 1 file - 0 error, 0 warning, 0 info $ typeglish test billing.tg --dry ✓ billing.tg coverage: 1/3 rules exercised · bill_query - "Why is my bill higher this month?" (not run) ✓ contains "account number" · no_price - "How much is the unlimited plan?" (not run) ✓ matches /^(?!.*[0-9]).*$/ ✓ 1 prompt - 0 failed $ typeglish build billing.tg ✓ built .typeglish/dist/billing.txt ← billing.tg (415e35be65a8, full) $ typeglish check billing.tg # B, two files ✓ 1 file - 0 error, 0 warning, 0 info $ typeglish test billing.tg --dry ✓ billing.tg (+ /tmp/split/billing.test.tg) coverage: 1/3 rules exercised · bill_query - "Why is my bill higher this month?" (not run) ✓ contains "account number" · no_price - "How much is the unlimited plan?" (not run) ✓ matches /^(?!.*[0-9]).*$/ ✓ 1 prompt - 0 failed $ typeglish build billing.tg ✓ built .typeglish/dist/billing.txt ← billing.tg (415e35be65a8, full)
(+ .../billing.test.tg), is the only place in the whole toolchain that mentions where the suite came from.billing.tg - B (85/100) proven errors: none tiers: base+z3 # A planes runtime 97 (what the model reads) · hygiene 50 (source only) facets enforceability 90 x.21 · hardness 100 x.12 · directness 100 x.08 · consistency 100 x.17 structure 100 x.12 (hygiene) · annotation 0 x.12 (hygiene) · style 100 x.08 · security 100 x.08 billing.tg - B (85/100) proven errors: none tiers: base+z3 # B planes runtime 97 (what the model reads) · hygiene 50 (source only) facets enforceability 90 x.21 · hardness 100 x.12 · directness 100 x.08 · consistency 100 x.17 structure 100 x.12 (hygiene) · annotation 0 x.12 (hygiene) · style 100 x.08 · security 100 x.08
Score the sibling on its own and you get the joke the layout makes possible: billing.test.tg - A (100/100), four facets, all of them full marks. A file with no rules in it has nothing to mark down. Do not put that number on a slide.
Round 1 is a tie, and a tie on every instrument is the reason people pick by taste.
§3Round 2: somebody mistypes an expectation
Here is the edit. An engineer adds an expectation the way it reads in their head, on one line, instead of as a sub-bullet under - expect::. It is the single most common malformed field in a suite, and it is the same three characters wrong in both layouts.
# Role You are a billing support agent for Cobalt Mobile. # Constraints - MUST keep every reply to at most 3 sentences. - ALWAYS ask for an account number before you discuss a bill. - NEVER state a specific price. $TEST bill_query - input:: Why is my bill higher this month? - expect:: contains "account number" $TEST no_price - input:: How much is the unlimited plan? - expect:: - matches /^(?!.*[0-9]).*$/
$ typeglish check billing.tg billing.tg:9:1 error structure/bad-test "- expect::" is a list - put each expectation on its own "- " sub-bullet beneath it. ✗ 1 file - 1 error, 0 warning, 0 info # exit 1 $ typeglish build billing.tg typeglish build: billing.tg refused - nothing written $ typeglish score billing.tg billing.tg - F (73/100) proven errors - grade capped at F tiers: base+z3 ✖ L7 structure/bad-test: "- expect::" is a list - put each expectation on its own "- " sub-bullet beneath it. planes runtime 97 (what the model reads) · hygiene 0 (source only) facets enforceability 90 x.21 · hardness 100 x.12 · directness 100 x.08 · consistency 100 x.17 structure 0 x.12 (hygiene) · annotation 0 x.12 (hygiene) · style 100 x.08 · security 100 x.08 lever structure 0/100 (up to +12 overall) - Fix the ledger rows with structure/ codes; most carry a one-click fix. L9 structure/bad-test −2 "- expect::" is a list - put each expectation on its own "- " sub-bullet benea
runtime 97 is untouched, because the defect is in the source plane and the model was never going to read it.Now the identical typo in contender B, in billing.test.tg, with the prompt file byte-for-byte the same as it was in §1.
$ typeglish check billing.tg ✓ 1 file - 0 error, 0 warning, 0 info # exit 0 $ typeglish score billing.tg billing.tg - B (85/100) proven errors: none tiers: base+z3 $ typeglish build billing.tg ✓ built .typeglish/dist/billing.txt ← billing.tg (415e35be65a8, full) $ typeglish test billing.tg --dry ✗ billing.tg (+ /tmp/r2b/billing.test.tg) does not compile - structure/bad-test: "- expect::" is a list - put each expectation on its own "- " sub-bullet beneath it. ✗ 1 prompt - 1 failed # exit 1 $ typeglish check . billing.test.tg:1:1 error structure/bad-test "- expect::" is a list - put each expectation on its own "- " sub-bullet beneath it. ✗ 2 files - 1 error, 0 warning, 0 info # exit 1 program: 2 independent files - no $IMPORT compositions
test, or a directory check.Which behaviour you want here is a genuine argument, and it is the argument. Contender A couples the two: your prompt cannot build while its suite is malformed, which is right if you believe an eval suite is part of the spec, and irritating at 6pm when a test typo is holding a rules change. Contender B decouples them: the prompt is judged on the prompt. The cost is that check and score on the file you actually deploy will tell you everything is fine while the suite beside it has not compiled since March.
A suite that fails loudly is a suite. A suite nothing runs is a directory.
§4Round 3: the rename, and the directory
The sibling relationship is by filename, and filenames move. Split the prompt for a second market, git mv billing.tg billing-uk.tg, and the suite stays where it was.
$ typeglish test billing-uk.tg --dry ✓ billing-uk.tg no $TEST cases ✓ 1 prompt - 0 failed # exit 0 $ typeglish check . ✓ 2 files - 0 error, 0 warning, 0 info # exit 0 program: 2 independent files - no $IMPORT compositions $ typeglish test *.tg --dry ✓ billing-uk.tg no $TEST cases ✗ billing.test.tg (+ /tmp/r3/billing.test.tg) testfile/missing-prompt: billing.test.tg has no sibling "billing.tg" to test - a .test.tg tests the .tg beside it. ✗ 2 prompts - 1 failed # exit 1 $ typeglish test . --dry typeglish: EISDIR: illegal operation on a directory, read
no $TEST cases with a tick and exit 0 is what a detached suite looks like from the prompt side. testfile/missing-prompt exists and is exactly right, but it only fires when the test file is named as a target, and test does not take a directory the way check does.Two more findings live in the same corner, and both come from test rather than check. Keep a case inline and add one with the same id to the sibling, and the merged suite reports testfile/duplicate-case: case "bill_query" is defined both inline in billing.tg and in billing.test.tg - ids must be unique across the merged suite at exit 1, while typeglish check . on that same pair prints 0 error, 0 warning, 0 info. The two files are not co-checked, they are co-run.
And then the directory build, which is the one that actually costs something. A .test.tg is a .tg, so typeglish build on a folder builds it.
$ typeglish build . ✓ built .typeglish/dist/billing.test.txt ← billing.test.tg (e3b0c44298fc, full) ✓ built .typeglish/dist/billing.txt ← billing.tg (415e35be65a8, full) ✓ built 2/2 $ cat .typeglish/dist/billing.test.txt $
e3b0c44298fc is the SHA-256 of the empty string. A suite-only file compiles to nothing, gets its own row in .typeglish/build-manifest.json, and lands in the directory a deploy script globs.The sharper version of the same fact: a .test.tg will hold anything a prompt holds. Write a rule in it, the way a QA team reasonably might when the rule is theirs, and it counts as a rule of the prompt for coverage while reaching no model at all.
# appended to billing.test.tg @@ qa_survey: the QA team's own rule, kept with the suite - MUST close every reply with a survey link. $ typeglish test billing.tg --dry ✓ billing.tg (+ /tmp/r4/billing.test.tg) coverage: 1/4 rules exercised $ typeglish build . ✓ built .typeglish/dist/billing.test.txt ← billing.test.tg (094e31fe192f, full) ✓ built .typeglish/dist/billing.txt ← billing.tg (415e35be65a8, full) $ cat .typeglish/dist/billing.test.txt - MUST close every reply with a survey link.
1/3 to 1/4: the denominator counts a rule that is not in the prompt, and the agent never sees the survey line. The merge is at file scope, not at $TEST scope.§5The verdict, and the gate either way
Inline wins the default. Not on any measurement in §2, since there are none, but on the two failure modes: the suite cannot silently detach from the rules it tests, and a broken case is a blocking error on the file you deploy rather than a green check with a dead suite behind it. One file also means one diff, and a rules change that arrives without a case change is visible in the same review.
The sibling wins two specific arguments. When the suite is much larger than the prompt and you want the prompt readable end to end, and when a different team owns the cases and you want their commits out of the file that ships. Both are real. Take the deal knowingly: you are trading a coupling for a filename convention, and filenames are not checked by anything.
Either way the gate has to name the tests, because no default command reaches them. This is the minimum that fails on all four defects in this post.
#!/usr/bin/env bash set -euo pipefail typeglish check prompts # the directory, so .test.tg files are checked too typeglish score prompts/billing.tg --min B typeglish test prompts/*.tg --dry # the glob, so an orphaned suite reports typeglish build prompts/billing.tg # name the prompts; never build the folder into a dist you deploy
testfile/missing-prompt and testfile/duplicate-case, neither of which any check will ever report.One thing worth saying plainly, since a bake-off invites a scoreboard: the number that decides whether your suite is worth anything is not in this post. It is coverage, and it is a rate over the rules, not over the conversations you worried about. Deterministic asserts vs. an LLM judge is the argument about what a case should assert; this one is only about which file it sits in.
§6Common questions
- Where should my agent prompt's tests live?
- Inline, in the
.tgfile, unless something concrete pushes them out. A.tgand its sibling.test.tgmerge into one suite, so the layouts tie oncheck,score, rule coverage and the artifact hash: the same three rules and two cases give0 error, 0 warning, 0 info,B (85/100)with byte-identical facets,coverage 1/3, and the artifact415e35be65a8either way. What differs is failure. A malformed test inline is a blockingstructure/bad-testthat refuses to build, so the suite cannot rot unnoticed, and the suite cannot be detached from the rules by a rename. Move to a sibling file when the suite is large or another team owns it, and then add the test files to your CI command by name. - Does a .test.tg file change what my agent receives?
- No. Tests are control plane in both layouts: the same prompt built with its cases inline and with its cases in a sibling produces the same artifact and the same short hash,
415e35be65a8. Two cautions. A.test.tgis still an ordinary.tgfile, sotypeglish buildon a directory writes an artifact for it too, and a suite-only file builds to an empty artifact ate3b0c44298fc, which is the SHA-256 of nothing. And any rule you write in the test file counts in the prompt's coverage denominator, taking three rules to1/4rather than1/3, while reaching no model at all. - Why does a broken test block my prompt from building?
- Because inline cases are part of the source, and a
$TESTblock with a malformed field is a blockingstructure/bad-testerror like any other structural defect. Writing an expectation as- expect:: contains "account number"instead of a list of sub-bullets takes the file to 1 error, exit 1,typeglish build: billing.tg refused - nothing written, andscoretoF (73/100)with the grade capped at F. The rules did not change. If you would rather a test typo never stopped a deploy, that is the argument for the sibling layout, where the prompt file still checks at0 error, 0 warning, 0 infoand builds. - Can typeglish test run over a directory of prompts?
- No.
typeglish checktakes a directory and expands it, buttypeglish testdoes not: pointing it at one printstypeglish: EISDIR: illegal operation on a directory, read. Use a glob so the test files are named as targets,typeglish test prompts/*.tg --dry, which is also the only way an orphaned suite reports. Rename a prompt and leave its sibling behind, andtypeglish teston the renamed prompt prints a tick andno $TEST casesat exit 0 while the directory check stays clean; the glob turns the same tree intotestfile/missing-promptat exit 1.
Everything above runs offline. test --dry validates the merged suite, attributes coverage and reports testfile/ defects with no model call and no API key, which is what makes it safe to put in the same CI job as check. The moment you drop --dry, every case becomes a request, and the file layout question turns into a bill. Worth deciding the layout while it is still free.