|
| 1 | +# Testing the fz skill with shell commands only |
| 2 | + |
| 3 | +This is the shell-only equivalent of `tests/test_skill_e2e.py`: it drives a headless |
| 4 | +Claude Code session against the fz skill and a tiny deterministic simulation (perfect gas |
| 5 | +pressure), then asserts on the **artifacts** the agent leaves behind — never on its prose. |
| 6 | +Paste the blocks in order, or save them as a script. Every block ends in a checkable exit |
| 7 | +status, so `bash -e` (or `&&`-chaining) aborts at the first failure. The `< /dev/null` on each |
| 8 | +`claude` call matters when running this as a script: headless claude reads piped stdin as |
| 9 | +extra input and would otherwise swallow the rest of the script. |
| 10 | + |
| 11 | +Requirements: `claude` CLI (logged in, or `ANTHROPIC_API_KEY` set), `fz` on PATH |
| 12 | +(`pip install funz-fz`), `jq`. |
| 13 | + |
| 14 | +```bash |
| 15 | +# Model used by the agent session — haiku keeps the cost negligible |
| 16 | +MODEL=${FZ_SKILL_TEST_MODEL:-claude-haiku-4-5} |
| 17 | +``` |
| 18 | + |
| 19 | +## 0. Probe: does claude actually work here? |
| 20 | + |
| 21 | +One tiny one-turn call. If this fails (not installed, not logged in, no key), there is no |
| 22 | +point running the rest — this is exactly the check the pytest version uses to self-skip. |
| 23 | + |
| 24 | +```bash |
| 25 | +claude -p "Reply with exactly: OK" --model "$MODEL" --max-turns 1 < /dev/null \ |
| 26 | + || { echo "claude CLI not functional — stopping"; exit 1; } |
| 27 | +``` |
| 28 | + |
| 29 | +## 1. Build a sandbox project |
| 30 | + |
| 31 | +The sandbox must live **outside any git repository** (in particular outside the fz repo): |
| 32 | +Claude Code resolves its project at the enclosing repo root, and would otherwise ignore |
| 33 | +the sandbox's `.claude/skills/`. |
| 34 | + |
| 35 | +```bash |
| 36 | +FZ_REPO=~/Sync/Open/Funz/github/fz # adjust to your fz checkout |
| 37 | +SANDBOX=$(mktemp -d) |
| 38 | +cd "$SANDBOX" |
| 39 | + |
| 40 | +# the skill, project-level |
| 41 | +mkdir -p .claude/skills |
| 42 | +cp -r "$FZ_REPO/skills/fz" .claude/skills/ |
| 43 | +``` |
| 44 | + |
| 45 | +The simulation: an input file parameterized with fz syntax (`$var` variables, `@{...}` |
| 46 | +formulas), a solver script, and a model alias telling fz how to parse the output. |
| 47 | + |
| 48 | +```bash |
| 49 | +# input file: 3 variables, 2 compile-time formulas |
| 50 | +cat > input.txt <<'EOF' |
| 51 | +# perfect gas pressure input |
| 52 | +n_mol=$n_mol |
| 53 | +T_kelvin=@{$T_celsius + 273.15} |
| 54 | +V_m3=@{$V_L / 1000} |
| 55 | +EOF |
| 56 | + |
| 57 | +# the "simulation": computes P = nRT/V into output.txt |
| 58 | +cat > calc.sh <<'EOF' |
| 59 | +#!/bin/bash |
| 60 | +source $1 |
| 61 | +python3 -c "print('pressure =', $n_mol*8.314*$T_kelvin/$V_m3)" > output.txt |
| 62 | +EOF |
| 63 | + |
| 64 | +# model alias: variable syntax + how to extract the pressure |
| 65 | +mkdir -p .fz/models |
| 66 | +cat > .fz/models/perfectgas.json <<'EOF' |
| 67 | +{ |
| 68 | + "varprefix": "$", |
| 69 | + "formulaprefix": "@", |
| 70 | + "delim": "{}", |
| 71 | + "commentline": "#", |
| 72 | + "output": {"pressure": "grep 'pressure = ' output.txt | awk '{print $3}'"}, |
| 73 | + "id": "perfectgas" |
| 74 | +} |
| 75 | +EOF |
| 76 | +``` |
| 77 | + |
| 78 | +## 2. Level 1 — skill activation |
| 79 | + |
| 80 | +Ask a question whose answer requires the skill, and check the transcript shows the skill |
| 81 | +was actually loaded. `--output-format stream-json --verbose` makes every tool call visible |
| 82 | +as a JSON line; `--allowedTools` pre-authorizes the tools so the headless session never |
| 83 | +blocks on a permission prompt. |
| 84 | + |
| 85 | +```bash |
| 86 | +claude -p "Using the fz skill, find which input variables input.txt contains |
| 87 | +(the fz model alias is 'perfectgas') and list their names." \ |
| 88 | + --output-format stream-json --verbose \ |
| 89 | + --max-turns 10 --model "$MODEL" \ |
| 90 | + --allowedTools "Bash,Read,Write,Edit,Glob,Grep,Skill" < /dev/null > transcript.jsonl |
| 91 | + |
| 92 | +# evidence the skill was used: a Skill tool call on "fz", or SKILL.md being read |
| 93 | +grep -q -e '"Skill"' -e 'SKILL.md' transcript.jsonl && echo "PASS: skill used" |
| 94 | + |
| 95 | +# the three variables must appear in the transcript (final answer) |
| 96 | +for var in n_mol T_celsius V_L; do |
| 97 | + grep -q "$var" transcript.jsonl || { echo "FAIL: $var not found"; exit 1; } |
| 98 | +done |
| 99 | +echo "PASS: all variables identified" |
| 100 | +``` |
| 101 | + |
| 102 | +## 3. Level 2 — end-to-end parametric study |
| 103 | + |
| 104 | +Give the agent the full task and let it choose its path; we only check the artifact it |
| 105 | +must produce. 2×2 = 4 cases, physics known exactly, so the assertions are deterministic |
| 106 | +even though the agent's reasoning is not. |
| 107 | + |
| 108 | +```bash |
| 109 | +claude -p "Using the fz skill, run a parametric study of the simulation launched by |
| 110 | +'bash calc.sh' (fz model alias: 'perfectgas', input file: input.txt) over |
| 111 | +T_celsius in [10, 20] and V_L in [1, 2] with n_mol fixed to 1. |
| 112 | +Then write the results to a file named results.json as a JSON list of |
| 113 | +records, one per case, each with keys T_celsius, V_L, n_mol, pressure, status." \ |
| 114 | + --output-format stream-json --verbose \ |
| 115 | + --max-turns 40 --model "$MODEL" \ |
| 116 | + --allowedTools "Bash,Read,Write,Edit,Glob,Grep,Skill" < /dev/null > transcript2.jsonl |
| 117 | +``` |
| 118 | + |
| 119 | +Now the artifact checks — the same three as the pytest version: |
| 120 | + |
| 121 | +```bash |
| 122 | +# 1. the file exists and is valid JSON with 4 case records, all done |
| 123 | +jq -e 'length == 4 and all(.[]; .status == "done")' results.json \ |
| 124 | + && echo "PASS: 4 cases, all done" |
| 125 | + |
| 126 | +# 2. every pressure matches P = nRT/V within 0.1% |
| 127 | +jq -e ' |
| 128 | + all(.[]; |
| 129 | + (8.314 * (.T_celsius + 273.15) / (.V_L / 1000)) as $expected |
| 130 | + | ((.pressure - $expected) / $expected | fabs) < 0.001 |
| 131 | + )' results.json && echo "PASS: pressures physically correct" |
| 132 | +``` |
| 133 | + |
| 134 | +(No `jq`? The same check in python: |
| 135 | +`python3 -c "import json; rs=json.load(open('results.json')); assert len(rs)==4 and all(r['status']=='done' and abs(r['pressure']-8.314*(r['T_celsius']+273.15)/(r['V_L']/1000))/ (8.314*(r['T_celsius']+273.15)/(r['V_L']/1000)) < 1e-3 for r in rs); print('PASS')"`) |
| 136 | + |
| 137 | +## 4. Clean up |
| 138 | + |
| 139 | +```bash |
| 140 | +cd / && rm -rf "$SANDBOX" |
| 141 | +``` |
| 142 | + |
| 143 | +## Notes |
| 144 | + |
| 145 | +- **Assert on artifacts, not prose**: the agent's wording varies between runs; the |
| 146 | + existence and numerical content of `results.json` (and the case directories fz creates |
| 147 | + under `results/`) do not. |
| 148 | +- Expect ~10 s for Level 1 and ~40 s for Level 2 with haiku; a few cents of tokens. |
| 149 | +- If Level 2 fails, look inside the sandbox before deleting it: `results/*/out.txt`, |
| 150 | + `err.txt` and `log.txt` show what each fz case actually did, and `transcript2.jsonl` |
| 151 | + shows what the agent tried. |
| 152 | +- The pytest version (`tests/test_skill_e2e.py`) automates exactly this, probe included: |
| 153 | + `venv/bin/python -m pytest tests/test_skill_e2e.py -v`. |
0 commit comments