BXUP Track progress & test yourself — free
QA & Testing · Module · ~12 min read
QA & TESTING · MODULE

Testing AI Systems I — Non-Determinism, Assertions & Prompt Regression

The day your product ships an LLM feature, expect(output).toBe(x) dies — and with it, most of what your test suite assumed about the world. This module rebuilds the foundations: statistical pass criteria for non-deterministic output, the layered assertion hierarchy, golden datasets as the new regression pack, and prompt regression gates in CI.

This is where the genuinely new discipline begins — and where the career premium lives. Job boards now carry titles that did not exist three years ago: AI QA engineer, AI evaluation engineer, LLM test engineer, with postings naming "LLM tooling in CI/CD" and "prompt regression" verbatim, and AI-skill salary premiums running 28–43% (Lightcast). The skills are not exotic: they are QA instincts — regression packs, assertions, CI gates — re-derived for systems that answer differently every time you ask. An SDET who internalizes this module is not learning a new job; they are extending the one they have into the decade where every product has an AI feature.

The scenario — the test that passed on retry

  1. DAY 1

    Sara — SDET, five years, at a US SaaS company — is assigned quality for the new support copilot: an LLM feature that summarizes ticket threads for agents. She does the obvious thing: capture a good summary, assert equality. expect(summary).toBe(golden). It fails on the second run. Same input, same model, different words — same meaning.

  2. DAY 2

    The team's reflex diagnosis: flaky test. The retry culture from Module 4 kicks in — run it until it passes. It passes on attempt three. Everyone moves on. Sara stares at the green check with a bad feeling: the test now passes when the output happens to match, and tells you nothing when it doesn't.

  3. WEEK 3

    A prompt "improvement" ships — someone tightened the wording to reduce token cost. Nothing failed, because nothing could fail: the equality asserts had been retried into meaninglessness. In production, summaries of refund-request threads quietly stop mentioning the refund amount. Support agents start missing commitments made to customers. The bug is discovered by an angry escalation, eleven days after the prompt change.

  4. WEEK 4

    Sara rebuilds on eval foundations: a golden dataset of 60 real ticket threads with labeled expectations; layered assertions — schema and must-mention checks first, semantic similarity second, an LLM judge only for tone; every check run 5 times with statistical thresholds; the whole thing wired into CI so a prompt change that drops refund-amount mentions blocks the PR that caused it.

  5. DAY 40

    The next prompt tweak fails the gate in CI with a readable diff: "refund_amount mention-rate fell 100% → 40% on refund-thread slice." Fix, rerun, merge. Eleven days of production exposure becomes eleven minutes in a pull request.

Everything in Sara's rebuild is a concept this module teaches, and none of it required a data-science degree — it required recognizing that non-determinism is a property to be measured, not a flake to be retried.

What You'll Learn

  • Non-determinism fundamentalswhy exact-match dies, what temperature and sampling actually do, and the flakiness-vs-variance distinction that changes your whole triage instinct (UC-38).
  • Statistical pass criteriarun-N-times thresholds, pass@k and pass^k, and choosing sample sizes that make results meaningful rather than theatrical (UC-38).
  • The layered assertion hierarchydeterministic checks, then semantic similarity, then LLM-judge, always the cheapest sufficient layer (UC-39).
  • Golden dataset constructionmining production traces and failure reports into a versioned, stratified eval set: QA's new regression pack (UC-40).
  • Prompt regression in CIPromptfoo configs that matrix prompts against models and fail the PR on regression (UC-41).
  • Pytest-native LLM testingDeepEval metrics as ordinary test asserts with JUnit output into the dashboards you already own (UC-42).
  • Structured-output contract testingschema-validity rates under repeated sampling, and malformed-output injection against the consumer (UC-53).

Non-determinism — variance is not flakiness

An LLM generates text by sampling from a probability distribution over next tokens. At temperature 0 the sampling is greedy and mostly stable — but not contractually deterministic across infrastructure, batching and model updates. At production temperatures, the same input legitimately yields different phrasings, orderings and word choices on every call. This forces the distinction that Module 4's triage instincts must now absorb:

  • Flakiness is your test infrastructure failing intermittently — a wait race, a data collision. The output should be identical; the test environment broke. Fix the test.
  • Variance is the system under test behaving as designed — different tokens, same (or different!) meaning. The output should not be expected identical; the assertion was wrong. Fix the assertion.

Treating variance as flakiness leads exactly where it led Sara's team: retry-until-green, which converts your suite into a coin-flip detector that passes when the coin lands right. The correct response to variance is statistical pass criteria — make the sample size and the threshold explicit:

  • Run-N thresholdrun each eval case N times; pass if at least K of N succeed. "9 of 10 runs must include the refund amount" is a meaningful, monitorable contract; a single run is an anecdote.
  • pass@k vs pass^kpass@k asks "did at least one of k attempts succeed?" (fits retry-loop UX where one good answer is enough); pass^k asks "did every one of k attempts succeed?" (fits agent steps, where one bad call in a chain sinks the task). Same feature, different criterion per surface — choosing which is a test-design decision, and it appears in this track's exam.
  • Honest sample sizesN=3 distinguishes nothing; N=1,000 costs real money. In practice N=5–10 per case across a dataset of dozens-to-hundreds of cases detects meaningful regressions; what matters is that N and the threshold are written down, so a pass means the same thing next month.
Exam Tip

Given a scenario where an LLM test "fails sometimes," the scored move is to classify before fixing: infrastructure flake (fix the test), legitimate variance (replace exact-match with a statistical criterion), or genuine quality instability (file it — a feature that is right only 60% of the time has a product problem, not a test problem).

The layered assertion hierarchy — cheapest sufficient layer first

If exact equality is dead, what asserts? A hierarchy, ordered by cost and applied bottom-up — the single most load-bearing design pattern in LLM testing:

LAYER 1 · DETERMINISTIC

Format, schema, facts

Valid JSON against the Pydantic schema; length bounds; regex on required tokens; must-mention checks (the refund amount, the order ID); must-NOT checks (no PII, no competitor names, no apology spirals). Runs in microseconds, costs nothing, zero ambiguity.

LAYER 2 · SEMANTIC

Meaning, not words

Embedding similarity between output and reference answers a question Layer 1 cannot: "does this say approximately the right thing?" Tolerant of legitimate paraphrase, catches drift in substance. Costs an embedding call; thresholds need tuning per task against labeled examples.

LAYER 3 · LLM JUDGE

Graded qualities

A model grades the output against a rubric: professional tone, faithful to source, appropriately hedged. Most expressive, most expensive, and least trustworthy by default — a judge is itself an LLM with biases, which is why Module 7 requires calibration before any judge gates a release.

The discipline is economic and epistemic at once: never buy an expensive, fuzzy layer for a property a cheap, exact layer can check. Whether output is valid JSON is not a judge question. Whether the summary mentions the refund amount is a regex, not an embedding. Sara's suite runs forty deterministic checks, six semantic ones, and exactly one judged property — and the deterministic layer catches the majority of real regressions, for free.

LayerChecksCost per caseFailure ambiguityExample
DeterministicSchema, regex, must/must-not mention, length, PII-absence~0None — binarySummary contains the ticket's refund amount
SemanticEmbedding similarity vs references, threshold1 embedding callLow — tunableSummary meaning ≈ labeled reference, ≥0.83
LLM judgeRubric-graded qualities1+ LLM callsHigh — needs calibration"Tone professional and non-committal: yes/no + reason"

Golden datasets — QA's new regression pack

Every layer needs something to run against, and that something is the golden dataset: a versioned, labeled collection of real inputs with expectations — the direct descendant of the regression pack, rebuilt for AI features. Construction is curation, not generation:

  • Mine production tracesreal ticket threads, real queries, sampled from observability (Langfuse-class tracing makes this a query, not an expedition). Real inputs are messier and more valuable than anything invented: the code-switching customer, the thread with three intertwined issues, the furious all-caps message.
  • Harvest every failureeach production incident and every bug report becomes a dataset case. This is the regression-pack instinct verbatim: the bug that happened once must be provably unable to recur silently. Sara's refund-amount incident became six cases the same week.
  • Stratify deliberatelyhappy-path majority, labeled edge strata (long threads, multilingual, ambiguous asks), and an adversarial slice (hostile input, prompt-injection-shaped text — the robustness lens; security-class injection testing belongs to the security team). Coverage thinking, applied to input space instead of code paths.
  • Label with intent, version in gitper case: the input, the must/must-not facts, reference outputs for similarity, and why this case exists. Then treat the dataset like code — versioned, reviewed, diffable — because changing the dataset changes what "passing" means, and that must never happen silently.
Remember:

The golden dataset is a living asset with a feedback loop — production failures flow in as new cases, stale cases get re-labeled when the product changes, and dataset diffs get reviewed like code diffs. A frozen dataset decays into testing last year's product; an ungoverned one silently redefines quality.

The CI gate — prompt regression testing that blocks merges

Prompts are code: they change behavior, they regress, and they deserve the same merge gate as everything else QA protects. Promptfoo is the declarative backbone — 350k+ developers, free and local-first — and the mental model is a test matrix: prompts × providers × dataset cases × assertions.

# promptfooconfig.yaml — the regression gate for the ticket summarizer
prompts:
  - file://prompts/summarize_v3.txt
  - file://prompts/summarize_v4_candidate.txt
providers:
  - anthropic:claude-sonnet-4-5
tests:
  - file://golden/refund_threads.yaml   # per-case asserts ride with the data:
      # assert:
      #   - type: is-json                     # Layer 1
      #   - type: icontains
      #     value: "{{expected_refund_amount}}"
      #   - type: similar                     # Layer 2
      #     value: "{{reference_summary}}"
      #     threshold: 0.83

Run locally while iterating; run in CI on every PR that touches a prompt, a model version or the dataset; fail the pipeline when pass rates drop against thresholds. The matrix answers the two questions that recur for the rest of your career: did the new prompt regress anything? (v3 vs v4 across the whole dataset, per-slice diffs in the output) and does the cheaper/newer model hold up? (same prompt, providers compared on identical cases — the evidence for every "can we switch models?" meeting).

For teams that live in Python and pytest, DeepEval provides the same layers with QA-native ergonomics — tests are just pytest, metrics (G-Eval rubrics, answer relevancy, faithfulness) are asserts, and output is JUnit XML that lands in the CI dashboards your org already reads:

def test_summary_keeps_refund_commitment(refund_case):
    out = summarize(refund_case.thread)
    assert refund_case.amount in out.text                     # Layer 1
    assert_test(LLMTestCase(input=refund_case.thread, actual_output=out.text),
                [AnswerRelevancyMetric(threshold=0.8)])       # Layer 2/3

The strategic point outranks the tools: the decision — no prompt change merges without the eval suite passing — is what saved Sara's team, and it will survive any individual framework. Tools churn (Promptfoo's acquisition by OpenAI is a live governance watch-item; DeepEval iterates monthly); the gate is forever.

Structured outputs — contract testing under sampling

Most LLM features ship structured output — JSON that downstream code parses — which makes this the most classically QA-shaped corner of the new discipline: it is contract testing with a probability distribution attached (UC-53).

  • Schema-validity rate, not schema validityvalidate against the Pydantic/JSON Schema contract across N sampled runs and report a rate. 100% valid over 200 samples is a contract; "it parsed when I tried it" is not. Track the rate per model version — it moves when providers update models, and that movement is your early-warning signal.
  • Retry and fallback behavior under testproduction code retries invalid output, re-prompts with the error, or falls back to a template. Those paths are code; force them (mock an invalid response) and assert recovery — the retry path that has never executed in a test is the one that fails during the incident.
  • Malformed-output injection against the consumerfeed the downstream consumer truncated JSON, wrong enum values, missing fields, absurd-but-valid numbers. The parser's error handling is a deterministic component you can test the old way — so do; when the model misbehaves at 2 a.m., the blast radius is decided by the consumer's error handling, not by the model.
CI · PR #217 "tighten summarizer prompt"

prompt-regression gate

BLOCKED— pass rate 71% on refund-thread slice (threshold 95%)golden set v14 · 60 cases × 5 runs · prompts v3 vs v4-candidate
  • Layer 1 schema validity: 100% (300/300 runs)
  • must-mention refund_amount: v3 100% → v4 40% on refund slice
  • Layer 2 similarity ≥0.83: v4 median 0.88
  • Token cost per case: v4 −18% — the change's goal, achieved at the cost above

The regression is visible in the diff, attributed to the slice, and stopped in the PR — not discovered in an escalation eleven days later.

Where this lands in your week

  • In feature QA for anything LLM-backedyour acceptance criteria become dataset cases with layered asserts and explicit N/threshold; "works when I tried it" leaves your vocabulary.
  • In PR reviewprompt changes arrive with eval runs attached, and you read the per-slice diff the way you read a coverage report today.
  • In incident responseevery AI-feature incident ends with new golden cases, so the class of failure is permanently gated.
  • In your careerthese are the artifacts (dataset, config, gate) that turn "SDET" into "AI evaluation engineer" in an interview: bring the caught-regression run, not a claim.
Key Takeaway:

Testing LLM features is QA's own instincts under a probability distribution: regression packs become golden datasets, asserts become layered hierarchies with the cheapest sufficient layer first, and the merge gate becomes a statistical pass criterion with N and threshold written down. Variance is not flakiness — measure it, gate it, and let the PR that regresses an edge case fail in CI instead of in production.

Try it now

  1. Demonstrate the death of exact-match. Call any LLM API 10 times with an identical summarization prompt at temperature 0.7; diff the outputs, then write the three deterministic checks (schema, must-mention, must-not) that all 10 runs should pass.
  2. Build a 20-case golden set. From real inputs in your product's domain (masked per Module 5), label each with must/must-not facts and one reference output; commit it to git with a README explaining each stratum.
  3. Wire the smallest gate. Point a Promptfoo config (or a DeepEval pytest file) at your 20 cases with Layer-1 asserts and N=5; make one deliberately bad prompt edit and watch the gate catch it.

Take this into Lab 11 (first LLM-feature test suite) — where run-to-run variance is the seeded lesson — and Lab 12 (prompt regression gate in CI), where a provided prompt "improvement" secretly regresses an edge case and your gate must block the merge. Companion playbooks: pb-34 through pb-39.

This is the free reading view. Open BXUP to track your progress, run the hands-on labs, and prove it with original, exam-pattern assessments.
© Anvi Innovate · BXUP — Be Exponentially Upskilled · BXUP is an independent learning platform, not affiliated with or endorsed by AWS, Microsoft, Google, NVIDIA, IBM, Salesforce, Databricks, Oracle or Anthropic. All trademarks belong to their respective owners. · bxup.in