property-based-testing
SKILL.md
Property-Based Testing
fast-check patterns for JavaScript/TypeScript that find edge cases unit tests miss. Includes 6 canonical property categories with worked examples targeting agent-studio's own utilities.
When to Use
- New utility functions (path normalization, parsers, transformers)
- Bug fixes where the fix has a general invariant (not just the specific repro case)
- Security-sensitive string handling (avoid escaping bugs, injection vectors)
- Any function where "this property should hold for ALL inputs" can be stated
The 6 Canonical Property Categories
1. Inverse Operations (Round-trip)
import fc from 'fast-check';
// If you serialize then deserialize, you get back the original
fc.assert(
fc.property(fc.anything(), value => {
expect(deserialize(serialize(value))).toEqual(value);
})
);
2. Idempotency
// Applying a function twice gives same result as once
fc.assert(
fc.property(fc.string(), str => {
expect(normalize(normalize(str))).toEqual(normalize(str));
})
);
3. Commutativity / Order Independence
// Order of inputs shouldn't matter
fc.assert(
fc.property(fc.array(fc.integer()), arr => {
expect(sum(arr)).toEqual(sum([...arr].reverse()));
})
);
4. Invariants (Properties that must always hold)
// Output always has certain structural properties
fc.assert(
fc.property(fc.string(), input => {
const result = parseArgs(input);
expect(result).toHaveProperty('args');
expect(Array.isArray(result.args)).toBe(true);
})
);
5. Oracle Comparison (Simpler reference implementation)
// Compare fast implementation against slow-but-correct reference
fc.assert(
fc.property(fc.array(fc.integer()), arr => {
expect(fastSort(arr)).toEqual(referenceSort(arr));
})
);
6. Metamorphic Relations
// If input changes in a known way, output changes in a predictable way
fc.assert(
fc.property(fc.array(fc.integer()), arr => {
const sorted = sort(arr);
const sortedWithExtra = sort([...arr, Number.MAX_SAFE_INTEGER]);
expect(sortedWithExtra[sortedWithExtra.length - 1]).toBe(Number.MAX_SAFE_INTEGER);
})
);
Agent-Studio Specific Examples
Path Normalization (targets SE-01 from sharp-edges)
import fc from 'fast-check';
const { normalizePath } = require('.claude/lib/utils/path-constants.cjs');
// Property: normalized path never contains backslashes
fc.assert(
fc.property(fc.string(), path => {
expect(normalizePath(path)).not.toMatch(/\\/);
})
);
// Property: idempotent
fc.assert(
fc.property(fc.string(), path => {
expect(normalizePath(normalizePath(path))).toEqual(normalizePath(path));
})
);
Safe JSON Parser (targets SE-02 from sharp-edges)
const { safeParseJSON } = require('.claude/lib/utils/safe-json.cjs');
// Property: never throws, always returns { success, data }
fc.assert(
fc.property(fc.string(), input => {
const result = safeParseJSON(input, null);
expect(result).toHaveProperty('success');
expect(typeof result.success).toBe('boolean');
})
);
// Property: prototype pollution not possible
fc.assert(
fc.property(fc.string(), input => {
const before = Object.prototype.toString;
safeParseJSON(input, null);
expect(Object.prototype.toString).toBe(before);
})
);
Glob-to-Regex (targets SE-05 from sharp-edges)
// Property: patterns with **/dir/** match root-level dir
fc.assert(
fc.property(fc.constantFrom('foo', 'bar', 'baz'), dir => {
const pattern = `**/${dir}/**`;
const regex = globToRegex(pattern);
expect(`${dir}/file.js`).toMatch(regex); // root-level match
expect(`a/${dir}/file.js`).toMatch(regex); // nested match
})
);
Routing Logic
// Property: routing is deterministic (same input -> same agent)
fc.assert(
fc.property(fc.string(), intent => {
expect(route(intent)).toEqual(route(intent));
})
);
// Property: routing never returns null/undefined
fc.assert(
fc.property(fc.string(), intent => {
expect(route(intent)).toBeTruthy();
})
);
Installation
pnpm add -D fast-check
Running Property Tests
# Run all property tests
node --test tests/**/*.property.test.cjs
# Run with verbose output (shows counterexamples on failure)
FAST_CHECK_VERBOSE=true node --test tests/**/*.property.test.cjs
Shrinkage (Automatic Counterexample Minimization)
fast-check automatically shrinks failing inputs to the minimal counterexample. Example:
- Test fails on:
"C:\\Users\\foo\\deep\\nested\\path" - fast-check shrinks to:
"a\\b"(minimal backslash case)
This makes debugging far faster than traditional fuzzing.
Memory Protocol (MANDATORY)
Before starting:
Read .claude/context/memory/learnings.md
After completing:
- New pattern ->
.claude/context/memory/learnings.md - Issue found ->
.claude/context/memory/issues.md - Decision made ->
.claude/context/memory/decisions.md
ASSUME INTERRUPTION: If it's not in memory, it didn't happen.
Weekly Installs
20
Repository
oimiragieo/agent-studioGitHub Stars
16
First Seen
Feb 25, 2026
Security Audits
Installed on
github-copilot20
codex20
kimi-cli20
gemini-cli20
cursor20
opencode20