Test a plugin with evals
A plugin can load cleanly and still not help. Evals check the part that matters: when someone asks a real question with your plugin loaded, does the answer contain what it should? syntic plugin eval runs a list of prompts against your plugin, scores each answer with simple checks, and writes a report you can open in a browser or read from CI.
How a run works
- The suite is read from
evals/evals.jsoninside the plugin folder, or from the file you name. - Each case’s prompt runs as its own one-shot session:
syntic -p --plugin-dir <plugin> "<prompt>". The plugin is loaded exactly as--plugin-dirwould load it, and the prompt is answered by your usual model, with your usual settings and permissions. - The answer is scored on your machine against the case’s checks.
report.jsonandreport.htmlare written, a summary is printed, and the command exits with0if every case passed or1if any failed.
Cases run one after another. Each case may take up to five minutes; a case that runs longer is stopped and marked as failed with timed out after 300s.
Every case is a real session, so it uses your plan’s usage like any other prompt. Keep suites small and focused.
Write your first suite
Create the file
release-helper/
.syntic-plugin/plugin.json
skills/...
evals/evals.jsonAdd cases
{
"cases": [
{
"name": "suggests the next version",
"prompt": "We fixed two bugs since v1.4.0. What should the next version be?",
"expect": { "contains": ["1.4.1"], "not_contains": ["2.0.0"] }
},
{
"name": "slash command explains itself",
"prompt": "/release-helper:ship --help",
"expect": { "contains": ["changelog", "tag"], "min_chars": 80 }
},
{
"name": "answers something",
"prompt": "Summarise what the release helper can do."
}
]
}Run it
syntic plugin eval ./release-helperpass suggests the next version (100%)
FAIL slash command explains itself (67%)
pass answers something (100%)
2 of 3 cases passed, score 89%. Reports: ./release-helper/evals/resultssyntic plugin test is another name for the same command.
How a case is scored
Each check in expect passes or fails on its own:
| Check | Passes when |
|---|---|
contains: [...] | Each listed text appears in the answer. One check per item. Case is ignored. |
not_contains: [...] | Each listed text is absent from the answer. One check per item. Case is ignored. |
min_chars: N | The trimmed answer has at least N characters. |
| (no checks) | The answer is not empty. |
A case’s score is the share of its checks that passed. A case passes only when all of them do. The run’s score is the average of the case scores.
If the session itself fails (for example, the prompt could not be sent), the case scores 0 and the reason is recorded instead of an answer.
Suite reference
{ "cases": [ { "name": "...", "prompt": "...", "expect": { ... } } ] }| Field | Type | Required | Notes |
|---|---|---|---|
cases | array | yes | Must hold at least one case. |
cases[].name | string | yes | Shown in the summary and reports. Must not be blank. |
cases[].prompt | string | yes | Sent as the session’s prompt. Slash commands work. Must not be blank. |
cases[].expect | object | no | The checks. Omit it to only require a non-empty answer. |
expect.contains | array of strings | no | Texts that must appear. |
expect.not_contains | array of strings | no | Texts that must not appear. |
expect.min_chars | number | no | Minimum answer length. |
Other fields are ignored.
Command options
syntic plugin eval <path> [--out <folder>] [--json]| Argument | Meaning |
|---|---|
<path> | The plugin folder (the suite is evals/evals.json inside it), or a suite file. When you name a file at <plugin>/evals/<file>.json, the plugin folder is two levels up. |
--out <folder> | Where to write the reports. Default: <plugin>/evals/results/. |
--json | Print the outcome as one JSON object (ok, code, messages) instead of lines. |
Keeping several suites is easy: put them side by side, such as evals/smoke.json and evals/full.json, and name the one you want.
Read the results
HTML report
report.html needs no other files. It shows the overall score, then one section per case with each check marked pass or fail and the answer the session gave. Open it in any browser.
JSON report
report.json has the same data for scripts:
{
"plugin": "./release-helper",
"passed": 2,
"total": 3,
"score": 0.889,
"cases": [
{
"name": "slash command explains itself",
"passed": false,
"score": 0.667,
"checks": [
{ "check": "contains \"changelog\"", "passed": true },
{ "check": "contains \"tag\"", "passed": false },
{ "check": "at least 80 characters", "passed": true }
],
"output": "…the answer, cut at 4,000 characters…",
"error": null
}
]
}Run evals in CI
The exit code is all CI needs:
- name: Plugin evals
run: syntic plugin eval ./plugins/release-helper --out eval-results
- uses: actions/upload-artifact@v4
if: always()
with:
name: plugin-eval
path: eval-resultsThe job must be signed in, like any other headless run; see Headless mode. A case that needs a tool which would normally ask for approval cannot get it in a one-shot run, so set the permission rules the cases need in the project’s settings.
Writing good cases
- Test behaviour, not wording. Check for the facts that must be there (a version number, a file name), not for whole sentences.
- Use
not_containsfor mistakes you have seen. If the agent once suggested the wrong package manager, add a check that it does not. - Cover each component. One case per command or skill is a good start.
- Expect some variation. Answers differ between runs. If a case fails now and then, loosen its checks rather than retrying until it passes.
Troubleshooting
| Message or symptom | What to do |
|---|---|
no eval suite at …/evals/evals.json | Create the file, or pass the suite file’s path. |
… is not a valid eval suite | The JSON does not match the format above. Check for a missing cases array or a typo in a field type. |
… has no cases | Add at least one case. |
every case needs a name and a prompt | One of the cases has a blank name or prompt. |
timed out after 300s | The prompt makes the session do too much. Narrow it. |
could not run: … with a sign-in message | The machine running the evals is not signed in. |
| Every case fails the same way | Load the plugin by hand with syntic --plugin-dir <plugin> and try one prompt, then check syntic plugin validate. |
| Exit code 1 though most cases pass | Any failing case makes the run fail. Look for the FAIL lines. |