PromptForge CLI ships ten assertion types. Every assertion returns
{ passed: boolean, message?: string, details?: Record<string, unknown> }
at runtime. The details shape for each type is documented below — the
dashboard renders these fields in the failure view.
Substring match against the model's output.
- type: contains
value: "billing"
caseSensitive: false # optional; default falsedetails on failure: { expected, caseSensitive, received }.
ECMAScript regular expression match.
- type: regex
pattern: "^\\{.*\\}$"
flags: "s" # optional; JS regex flags (g, i, m, s, u, y)Use the flags field — ECMAScript regex doesn't support inline (?i).
details on failure: { pattern, flags, received }.
Validate the output as JSON against a JSON Schema. Uses Ajv
under the hood. If the output isn't pure JSON, PromptForge CLI tries to extract
the first balanced {…} or […] block.
- type: jsonSchema
schema:
type: object
required: [category, urgency]
properties:
category: { enum: [billing, technical, account, other] }
urgency: { enum: [low, medium, high] }
additionalProperties: falsedetails on failure: { errors: AjvError[], received: any }.
Cosine similarity between the output and a reference string, computed via
local embeddings (Xenova/all-MiniLM-L6-v2, ~25 MB, cached after first run).
- type: semanticSimilarity
expected: "Please provide more details about your issue."
threshold: 0.75 # optional; default 0.75Thresholds by intent:
| Threshold | Interpretation |
|---|---|
0.9+ |
Near-exact paraphrase |
0.7–0.9 |
Same meaning, different words |
0.5–0.7 |
Related topic, looser fit |
< 0.5 |
Unrelated — threshold too low to be meaningful |
details on failure: { similarity, threshold, expected, received }.
Have an LLM grade the output against plain-English criteria. Returns a
score 1–5 with a one-sentence reasoning. Fails if score < threshold.
- type: llmJudge
criteria: "Response is empathetic and offers next steps"
judgeModel: anthropic/claude-haiku-4-5 # optional
threshold: 4 # optional; default 4If judgeModel is omitted, PromptForge CLI picks the cheapest non-mock
provider listed in the suite (Ollama ranks above paid providers because
it's free). Mock is never auto-selected — a mock-only suite with
llmJudge and no explicit judgeModel fails with a clear error telling
you to set judgeModel or add a real provider.
The judge call is accounted for per-assertion, not per-run: the run's
overall providers list and top-level total_cost only reflect the
suite's primary provider completions. A failing llmJudge assertion
records the judge's own cost and latency inside its details object
(judgeCost, judgeLatencyMs) so you can see what the grader actually
spent without inflating the test's primary metrics.
details on failure: { judgeModel, score, reasoning, threshold, judgeCost, judgeLatencyMs }.
Record the output on the first run; compare subsequent runs via embedding similarity. Fails if the output drifts below the threshold.
- type: snapshot
similarity: 0.9 # optional; default 0.9Snapshots persist in .promptforge/db.sqlite (SQLite snapshots table).
Update a snapshot after an intentional change:
promptforge-cli snapshot --update --filter "some test name"Clear one:
promptforge-cli snapshot --clear "some pattern"details on failure: { similarity, threshold, stored, received }.
USD budget guardrail. Reads directly from the provider's reported cost (computed from the pricing table and the response's token usage).
- type: cost
max: 0.002 # dollarsdetails on failure: { cost, max }.
Latency budget in milliseconds. Reads the provider's measured end-to-end time (including retries).
- type: latency
maxMs: 3000details on failure: { latencyMs, maxMs }.
Function-valued assertion. Only works in .test.ts files — YAML can't
serialize functions.
import { defineTestSuite } from 'promptforge-cli';
export default defineTestSuite({
prompt: './triage.md',
providers: ['mock'],
tests: [
{
name: 'output is valid JSON with exactly the expected keys',
vars: { message: 'hi' },
mockOutput: '{"category":"other","urgency":"low"}',
assert: [
{
type: 'custom',
fn: async (output, ctx) => {
const parsed = JSON.parse(output);
const expectedKeys = ['category', 'urgency'];
return {
passed: expectedKeys.every((k) => k in parsed),
message: `missing keys: ${expectedKeys.filter((k) => !(k in parsed)).join(', ')}`,
};
},
},
],
},
],
});The function may return boolean, { passed, message?, details? }, or a
Promise of either.
Every assertion type takes the same shape: an async function that reads
an AssertionContext and returns an AssertionResult. Three files to
touch:
src/core/assertions/<yourType>.ts— the executor.src/core/assertions/index.ts— one case in the dispatcher switch.src/dashboard/components/renderers/<YourType>.tsx— the dashboard rendering (unknown types fall back to JSON, so this is optional but nice-to-have).
See CONTRIBUTING.md for the PR checklist.