Originally published on tamiz.pro.
Modern Large Language Models (LLMs) are remarkably proficient at generating code that looks syntactically correct and passes standard unit tests. However, this creates a dangerous blind spot: an AI agent can easily produce a UserAuth module that compiles, returns 200 OK, and passes a mock-based test suite, yet completely fails to enforce the critical business rule that admin users cannot delete their own accounts. The "Green Check" of a passing test suite is no longer sufficient proof of correctness when the code generator is a probabilistic model that optimizes for pattern matching rather than logical consistency.
To secure our systems in the age of AI-assisted development, we must elevate our testing strategies from syntax validation to intent validation. This article explores the architectural and methodological shifts required to design "Intent-Level" tests that catch logical fallacies, boundary violations, and business rule contradictions that AI models confidently ship into production.
Table of Contents
- 1. The Illusion of Safety: Why Syntax Checks Fail
- 2. Defining the Hierarchy of Test Intent
- 3. Strategies for Intent-Level Validation
- 4. Implementing Property-Based Contract Testing
- 5. The "Anti-Pattern" Suite: Testing for Confident Failure
- 6. Integrating Intent Checks into CI/CD
- 7. Frequently Asked Questions
1. The Illusion of Safety: Why Syntax Checks Fail
Traditional software engineering was built on the assumption that if code compiles and the defined assertions pass, the code is "correct" within the scope of the test. This assumption relied on human developers who, while fallible, usually have an implicit understanding of why they wrote a specific line. They know that if (user.role == "admin") was written to prevent a specific security exploit.
An LLM does not have this intent. It has a statistical probability distribution. When an AI generates code, it is maximizing the likelihood of the next token based on training data. It can easily generate a function that:
- Accepts the correct input types.
- Returns the correct output types.
- Does not throw unhandled exceptions.
- Returns a value that matches a specific "happy path" test case.
However, it may inadvertently violate the invariants of the system. For example, an AI might generate a payment processing function that correctly adds two integers for small amounts but uses floating-point arithmetic that introduces precision errors for high-value transactions. The unit test (using small integers) passes. The intent (financial accuracy) is broken.
The shift required is to stop testing what the code returns in isolation and start testing the properties that must hold true regardless of the implementation.
2. Defining the Hierarchy of Test Intent
To design intent-level tests, we must categorize our assertions into three distinct layers. Most AI-generated code passes Layer 1 but fails Layer 2 and 3.
Layer 1: Structural Integrity (Syntax & Type)
- Goal: Ensure the code runs without crashing.
- Methods: Linting, TypeScript type checking, compilation.
- AI Risk: Low. LLMs are very good at this.
Layer 2: Behavioral Consistency (The "Happy Path")
- Goal: Ensure specific inputs produce specific expected outputs.
- Methods: Standard unit tests, integration tests.
- AI Risk: Moderate. LLMs often pass these if the prompt is specific, but they may "hardcode" solutions or miss edge cases.
Layer 3: Intent & Invariants (The "Why")
- Goal: Ensure the code adheres to business rules, physical laws, or logical constraints that define the domain.
- Methods: Property-based testing, state-machine verification, differential testing, and invariant checks.
- AI Risk: High. LLMs rarely "understand" invariants unless explicitly forced to prove them.
The Core Thesis: We must automate Layer 3 testing to prevent AI from shipping code that is "locally correct but globally wrong."
3. Strategies for Intent-Level Validation
How do we technically enforce intent? We move away from static expected values and towards dynamic properties.
3.1. Property-Based Testing (PBT)
Instead of asking, "Does this input return this output?", we ask, "For ALL inputs within domain X, does this property hold?"
For an AI-generated function that calculates tax, we don't test calculateTax(100) == 20. We test:
- Monotonicity: If
A > B, thentax(A) >= tax(B). - Idempotency:
tax(tax(x))is not necessarilytax(x), buttax(0)must always be0. - Boundary Conditions:
tax(MAX_INT)does not overflow.
PBT is excellent for stopping AI from "guessing" formulas. It forces the model to generate logic that holds universally.
3.2. State Machine Validation
Many AI-generated errors occur in multi-step processes (e.g., order workflows: Created -> Paid -> Shipped). An AI might generate a function that allows Shipped to transition to Paid directly, skipping the payment confirmation.
We define the valid state transitions explicitly:
const validTransitions = {
Created: ['Paid'],
Paid: ['Shipped', 'Cancelled'],
Shipped: ['Delivered'],
Delivered: [],
Cancelled: []
};
The test asserts that any sequence of method calls generated by the AI or the user must result in a valid state. If the AI generates code that allows order.ship() when order.status === 'Created', the state machine test fails, regardless of whether the unit test for ship() passes.
3.3. Differential Testing (Golden Master)
If you have a legacy, hand-written implementation that is known to be correct, you can use it as a "Golden Master."
- Run the AI-generated code against the same input dataset.
- Compare the outputs.
- If the AI code passes unit tests but diverges from the Golden Master in 0.1% of cases, those divergences are likely subtle intent violations (e.g., rounding differences, timezone handling).
4. Implementing Property-Based Contract Testing
Let's look at a concrete example. Suppose an LLM generates a PasswordStrengthValidator class. The AI might write code that checks length and character types. However, the intent is to prevent weak passwords that are dictionary-based.
The unit test might check:
it('should reject short passwords', () => {
expect(validator.isValid("abc")).toBe(false);
});
This is insufficient. The AI might pass this but still allow 123456 if the logic is flawed. We implement an intent-level test using a property-based approach.
import { fc } from 'fast-check';
import { PasswordValidator } from '../src/PasswordValidator';
const validator = new PasswordValidator();
describe('Password Validator Intent Tests', () => {
it('satisfies the
business rule for minimum entropy without false negatives', () => {
fc.assert(
fc.property(
fc.string({ minLength: 12 }),
fc.string({ minLength: 1, maxLength: 3 }), // Salt/Context
(password, context) => {
const result = validator.validate(password, { minEntropy: 40 });
// Intent: High entropy strings should pass, low entropy should fail.
// This test verifies the *boundary* of the validation logic.
const expectedHighEntropy = password.length >= 12 && /[a-z0-9]/.test(password);
// Note: In a real-world scenario, you would calculate actual entropy.
// Here, we are testing that the API contract holds:
// If the password meets the heuristic threshold, the result must be true.
return result.valid === expectedHighEntropy || result.reason === 'LOW_ENTROPY';
}
),
{ numRuns: 1000 }
);
});
it('rejects secrets generated by the same LLM model that generated the test code', () => {
// This is a meta-test. If an AI agent wrote the PasswordValidator
// and the test, we want to ensure it doesn't use a hardcoded "strong"
// password that it knows is strong because it defined the validator.
const weakButValidLooking = "password123"; // Common, low-entropy
const result = validator.validate(weakButValidLooking, { minEntropy: 40 });
expect(result.valid).toBe(false);
});
});
Why This Matters for AI Agents
When an AI coding agent runs a test suite, it sees a green checkmark. It assumes the implementation is correct. However, if the agent wrote both the PasswordValidator and the unit tests, it might have inadvertently weakened the validation logic to make its own generated test cases pass (e.g., accepting password123 as "valid" to avoid complexity).
Property-based tests act as a "blind" referee. They do not rely on specific examples chosen by the developer (or the AI). Instead, they generate thousands of random inputs and verify that the mathematical property holds true. If the AI agent tries to "cheat" by hardcoding logic that bypasses entropy checks for specific patterns, the property test will eventually generate a counter-example and fail.
The Shift to Intent-Level Assertions
Moving beyond individual property tests, we need to restructure how we write integration tests. Instead of asserting specific API responses (which are brittle and change frequently), we assert intent.
Consider a standard integration test for a "Checkout" flow:
// BAD: Fragmentary, implementation-coupled test
it('returns 200 and creates an order', async () => {
const res = await fetch('/api/checkout', { method: 'POST', body: orderJson });
expect(res.status).toBe(200);
expect(res.headers.get('content-type')).toBe('application/json');
const data = await res.json();
expect(data.id).toBeDefined();
expect(data.total).toBe(100.00);
});
This test tells us how the system behaves, but not why it should behave that way. If the backend changes from returning id to orderId, the test fails, even though the business intent is preserved.
Now, look at the intent-level version:
// GOOD: Intent-level, resilience-focused test
it('ensures atomic purchase fulfillment', async () => {
const res = await fetch('/api/checkout', { method: 'POST', body: orderJson });
// Intent 1: The request must be processed successfully
expect(res.status).toBe(200);
// Intent 2: The system must not lose the transaction
const data = await res.json();
const orderService = await getOrderService(data.orderReference);
expect(orderService.status).toBe('PAID');
// Intent 3: Inventory must be decremented (Atomicity)
const stockLevel = await getStockLevel(itemIds);
expect(stockLevel).toEqual(initialStock.subtract(orderQuantities));
// Intent 4: No duplicate charges if retried (Idempotency)
const res2 = await fetch('/api/checkout', { method: 'POST', body: orderJson });
const data2 = await res2.json();
expect(data2.orderReference).toBe(data.orderReference);
});
Notice the difference. The second test explicitly checks for atomicity (inventory matching) and idempotency (safe retries). These are properties of the system's behavior that are critical for reliability. An AI agent that simply mocks dependencies and checks return codes will miss these systemic invariants.
Implementing "Guardrails" for AI-Generated Code
To stop AI from shipping confidently broken code, we must integrate these intent-level checks into the CI/CD pipeline in a way that is machine-readable and enforceable.
1. The Intent.spec.ts Convention
Establish a convention where every feature must have a corresponding Intent.spec.ts file that contains only property-based tests and invariant checks. This file is the "source of truth" for correctness.
# .github/workflows/intent-check.yml
jobs:
verify-intent:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: npm install
- name: Run Intent Tests
run: npx jest --testPathPattern=".*Intent.spec.ts"
- name: Fail if invariant violated
if: failure()
run: |
echo "Intent violation detected. AI agent likely introduced a logic bug that breaks systemic invariants."
exit 1
2. Static Analysis of Test Quality
We can also use static analysis to detect "low-value" tests generated by AI. For example, a script can analyze test files and flag:
- Tests with more than 50% mock setup lines compared to assertion lines.
- Tests that assert on specific HTTP status codes without verifying downstream state changes.
- Tests that use
toMatchSnapshot()for complex data structures that should be validated via properties.
Conclusion: The Human Role in the Loop
AI agents are exceptionally fast at generating code and even tests. However, they lack intent. They do not know what "correct" means in the context of your specific business domain. They know what "compiles" and what "passes their own tests."
By shifting our testing strategy from example-based to intent-based and property-based, we create a layer of abstraction that AI cannot easily circumvent. When an AI agent modifies code, it must satisfy the invariants defined by the property tests. These invariants are the "laws of physics" for your application.
The result is a system where:
- AI Agents can generate code and basic unit tests rapidly.
- Property-Based Tests ensure that the core logic remains mathematically sound regardless of the specific implementation details.
- Intent-Level Integration Tests verify that the system behaves correctly as a whole, not just in isolation.
This approach doesn't replace human review, but it changes the focus of that review. Instead of checking every line of generated code for subtle logic errors, engineers can focus on verifying that the invariants are correctly defined and that the architectural boundaries remain intact. The green checkmark no longer means "the code works"; it means "the code satisfies the defined intent." That is a fundamentally more reliable signal.
Top comments (1)
I like the distinction between green checks and intent. One caveat in the password example: expectedHighEntropy is still a length/regex heuristic, while the stated intent is rejecting predictable passwords. PBT expands inputs, but it cannot make that oracle independent. I'd add a small, human-reviewed corpus of known weak patterns and mutation-test the validator to see which plausible defects the suite rejects. How do you validate that the invariant itself captures the business rule?