skills/testing/metamorphic-test-generator/SKILL.md
Generates metamorphic tests — tests that check relationships between multiple runs instead of checking exact outputs, useful when the correct output is unknown or expensive to compute. Use when there's no oracle, when testing ML/numerical/search code, or when the spec describes properties rather than values.
npx skillsauth add santosomar/general-secure-coding-agent-skills metamorphic-test-generatorInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Normal test: "for input X, output should be Y." But what if you don't know Y? Metamorphic test: "for inputs X and X', the outputs should relate in a known way." You don't need an oracle — you need a metamorphic relation.
f
X ─────────► Y You don't know Y.
│ │
│ transform │ relation But you know: if X' = transform(X),
▼ ▼ then f(X') should relate to f(X).
X' ────────► Y'
f
If the relation fails, f is wrong. You found a bug without knowing the right answer.
| Domain | Relation | Transform → expected output change | | ------------------- | --------------------------------------------------------------- | ---------------------------------------- | | Sorting | Permutation invariance | shuffle(X) → same output | | Search | Adding irrelevant docs doesn't change top-k for a fixed query | X ∪ {irrelevant} → same top-k | | Search | Subset monotonicity | results(A∪B) ⊇ results(A) for query q | | Numerical | Scaling | f(k·X) = k·f(X) (if f is linear) | | Numerical | Additivity | f(X+Y) = f(X) + f(Y) (if linear) | | Pathfinding | Adding an edge can't make shortest path longer | G + edge → dist ≤ old dist | | Compression | Round-trip | decompress(compress(X)) = X | | Parsing | Pretty-print invariance | parse(print(parse(X))) = parse(X) | | ML classifier | Semantic-preserving input → same class | image + tiny noise → same prediction | | Aggregation (sum) | Partition invariance | sum(A∪B) = sum(A) + sum(B) (disjoint) | | Caching | Idempotence | f(X); f(X) → same result, second is fast |
Ask: what transformation of the input has a predictable effect on the output?
f(g(x)) = x)f(f(x)) = f(x))Each "yes" is a metamorphic relation.
System: search(query, corpus) -> list[doc] ranked by relevance. You don't know what's "correct" — relevance is subjective.
Relations:
from hypothesis import given, strategies as st
# MR1: Permuting the corpus doesn't change results (order-independence of index)
@given(query=queries(), corpus=corpora())
def test_mr_corpus_order_invariant(query, corpus):
r1 = search(query, corpus)
r2 = search(query, shuffled(corpus))
assert r1 == r2
# MR2: Adding a document irrelevant to the query doesn't change top-k
@given(query=queries(), corpus=corpora(), k=st.integers(1, 10))
def test_mr_irrelevant_addition(query, corpus, k):
r1 = search(query, corpus)[:k]
irrelevant = make_doc_with_no_overlap(query) # no shared terms
r2 = search(query, corpus + [irrelevant])[:k]
assert r1 == r2
# MR3: Duplicating a top result keeps it in top results
@given(query=queries(), corpus=corpora())
def test_mr_duplicate_stays_top(query, corpus):
r1 = search(query, corpus)
if not r1:
return
top = r1[0]
r2 = search(query, corpus + [top]) # add another copy
assert top in r2[:2] # original or copy should be in top-2
# MR4: Query with more terms → results are a subset (conjunctive search)
@given(base_query=queries(), extra_term=terms(), corpus=corpora())
def test_mr_conjunction_narrows(base_query, extra_term, corpus):
r_broad = set(search(base_query, corpus))
r_narrow = set(search(base_query + " " + extra_term, corpus))
assert r_narrow <= r_broad
MR4 assumes conjunctive (AND) semantics. If search is disjunctive (OR), the relation flips. The relation encodes a spec claim — if it fails, either the relation is wrong (you misunderstood the spec) or the code is wrong.
Function: std_dev(samples: list[float]) -> float. You could compute the expected value by hand for each test input. Or:
# MR: std dev is shift-invariant — adding a constant doesn't change spread
@given(st.lists(st.floats(allow_nan=False, allow_infinity=False), min_size=2),
st.floats(allow_nan=False, allow_infinity=False))
def test_mr_shift_invariant(samples, c):
assert abs(std_dev(samples) - std_dev([s + c for s in samples])) < 1e-9
# MR: std dev scales linearly with the data
@given(st.lists(st.floats(1, 100), min_size=2), st.floats(0.1, 10))
def test_mr_scale(samples, k):
assert abs(std_dev([s * k for s in samples]) - k * std_dev(samples)) < 1e-9
These test the algebra of std dev. A buggy implementation (e.g., forgot the square root, or divides by N instead of N-1) will break at least one.
test_mr_conjunction_narrows above is wrong for OR-search. Verify the relation against the spec before encoding it.assert len(r1) >= 0 is always true. The relation has to be tight enough to catch bugs.== on floats → flaky test. Use abs(a - b) < ε.## System under test
<function — and why an oracle is hard>
## Metamorphic relations
| # | Relation | Transform | Expected output relation | Spec basis |
| - | -------- | --------- | ------------------------ | ---------- |
## Tests
### MR-<N>: <name>
<code — property-test style, Hypothesis/QuickCheck>
Bug classes this catches: <what would violate this relation>
## Relation validity
<for each MR: why you believe this relation holds — cite the spec or the math>
development
Extracts human-readable pseudocode from a verified formal artifact (Dafny, Lean, TLA+) while preserving the verified properties as annotations, so the proof-carrying logic can be reimplemented in a production language. Use when porting verified code to an unverified target, when documenting what a formal spec actually does, or when handing a verified algorithm to an implementer.
development
Translates natural-language or pseudocode descriptions of concurrent and distributed systems into TLA+ specifications ready for the TLC model checker. Identifies state variables, actions, type invariants, safety properties, and liveness properties from the description. Use when formalizing a protocol, when the user describes a distributed algorithm to verify, when designing a consensus or locking scheme, or when starting formal verification of a concurrent system.
testing
Reduces a TLA+ model so TLC can actually check it — shrinks constants, adds state constraints, abstracts data, or applies symmetry — when the state space is too large to enumerate. Use when TLC runs out of memory, when checking takes hours, or when a spec works at N=2 and you need confidence at larger scale.
development
TLA+-specific instance of model-guided repair — reads a TLC error trace, identifies the enabling condition that should have been false, strengthens the corresponding action, and maps the fix to source code. Use when TLC reports an invariant violation or deadlock and you have the code-to-TLA+ mapping from extraction.