How to use this issue with /opsx-propose
/opsx-propose test-mapping
Use this entire issue body as the change description. Do not ask clarifying questions. Every decision is already made below. If something is still unspecified, apply these defaults in order:
- Match
test_mapping protocol v1.1.0 field names exactly.
- Lift gaze-py pairing + assertion detection; write a new pipeline (do not lift
quality/pipeline.py).
- Do not compute CRAP, GazeCRAP, quadrants, or contract-coverage percentages — Gaze does that from the mappings.
- Astroid strategy 3 is optional: if astroid fails, skip it; still return strategies 1–2.
OpenSpec change name: test-mapping
Depends on: #4 (analysis-methods) merged.
Independent of: classify-signals. May be implemented in parallel. Do not modify signal extractors here.
Context
test_mapping is the optional method that lets Gaze compute GazeCRAP (contract coverage). snake-eyes pairs tests to production functions, finds assertions, and guesses which side-effect type each assertion is checking.
Lift (mpeter/gaze-py, permission granted, keep copyright):
src/gaze_py/quality/pairing.py → src/snake_eyes/quality/pairing.py
- assertion helpers from gaze-py quality package (whatever file detects
assert / pytest.raises / unittest asserts) → src/snake_eyes/quality/assertions.py
Do not lift quality/pipeline.py. Write src/snake_eyes/quality/pipeline.py against snake-eyes models and the protocol mapping record.
astroid may already be a dependency from classify-signals; if this issue lands first, add astroid>=3.0,<5 here.
Protocol
Request:
{"root_path": "/abs/path", "patterns": ["./..."]}
Result:
{
"mappings": [
{
"test_function": "test_multiply",
"test_file": "tests/test_ops.py",
"assertion_location": "tests/test_ops.py:10",
"assertion_type": "equality",
"target_function": "multiply",
"target_package": "math_utils",
"side_effect_type": "ReturnValue",
"confidence": 80
}
]
}
confidence is int 0–100 (not 0.0–1.0). Convert lifted 0.9 → 90.
assertion_location format: path:line (no column required). Path relative to root_path.
Allowed assertion_type strings:
equality | error_check | membership | identity | comparison | generic
Flip capabilities.test_mapping to true.
If no tests or no pairs: {"mappings": []}, not an error.
What to build
1. Pairing (lift + adapt)
Three strategies, first match wins, do not emit duplicate pairs for the same (test_function, test_file, target_function):
| Priority |
Strategy |
confidence |
| 1 |
Name convention: test_foo / testFoo / TestFoo ↔ foo. Strip test_ / Test prefix. Case-insensitive fallback confidence 70 if only case differs |
90 / 70 |
| 2 |
Direct call: test function AST contains a Call to the target name |
80 |
| 3 |
Astroid transitive call graph, BFS depth_limit=5 |
75 |
Unpaired tests produce no mapping rows (do not emit confidence 0 rows).
target_package from the production file's dotted module path (same helper as analyze).
2. Assertion detection (lift + adapt)
For each paired test function, collect assertions:
| AST / pattern |
assertion_type |
assert x == y, assertEqual, assertEquals |
equality |
assert x != y, assertNotEqual |
comparison |
assert x < y / > / <= / >=, assertLess* |
comparison |
assert x is y, assertIs, assertIsNone |
identity |
assert x in y, assertIn, assertNotIn |
membership |
pytest.raises, unittest.assertRaises, raises( |
error_check |
other assert / assertTrue / assertFalse |
generic |
One mapping row per assertion, not per pair. A test with 3 asserts → 3 rows (same target, different assertion_location).
3. Effect-type guess (new, mapping.py)
For each assertion, set side_effect_type as:
| assertion_type |
side_effect_type |
error_check |
ErrorReturn if the target has that effect, else ErrorSignal if present, else ErrorReturn anyway |
equality, comparison, identity, membership |
ReturnValue if the target has it, else the first P0 effect on the target, else ReturnValue |
generic |
first effect on the target if any, else ReturnValue |
Use the detector output for the target function. Do not re-parse ad hoc.
4. Pipeline (new)
run_test_mapping(root_path, patterns) -> list[dict]:
discover()
analyze_path() on the same root/patterns (need effects on targets)
- Parse test files, collect
FunctionDef/AsyncFunctionDef whose names start with test_ or are methods of classes subclassing unittest.TestCase whose names start with test
- Pair
- Assertions
- Effect guess
- Serialize protocol dicts
Do not run pytest. Do not read coverage here.
5. Fixture project
tests/fixtures/sample_project/:
src/sample/__init__.py
src/sample/calculator.py # add(a,b)->a+b; divide(a,b) raises ZeroDivisionError; Counter.inc mutates self
tests/test_calculator.py # test_add (name strategy), a test that calls divide without matching name,
# pytest.raises for divide
Include pyproject.toml only if needed for imports; tests should set root_path to this fixture dir and patterns to ["./..."].
Tests required
- Name strategy:
test_add ↔ add, confidence 90
- Case-insensitive:
test_Add ↔ add, confidence 70
- Direct call: test named
test_it_works that calls divide ↔ divide, confidence 80
- No match: test that calls nothing and name-matches nothing → no row
pytest.raises → assertion_type=error_check, side_effect_type ErrorReturn or ErrorSignal
assert result == 3 → equality, ReturnValue
- Pipeline on
sample_project → at least two mapping rows with all required keys present
- JSON-RPC e2e;
initialize has test_mapping: true
- Missing root →
-32602
Do not require strategy 3 to fire in CI if astroid cannot import the fixture; still unit-test the BFS helper with a mocked graph or a tiny on-disk package that astroid can parse.
Coverage strategy (Constitution IV)
| Layer |
Target |
pairing.py |
90%+ |
assertions.py |
90%+ |
mapping.py |
95%+ |
pipeline.py |
85%+ |
| Project gate |
85% |
Out of scope
- gaze-py
quality/pipeline.py
- Running coverage/pytest for the analyzed project
- CRAP formulas, quadrants, fix strategies
classify_signals changes
- Streaming
Done when
test_mapping RPC returns protocol-shaped mappings
- capability flag true
- three pairing strategies exist in code (3rd degrades without failing the method)
- copyright retained on lifted pairing/assertion files
- snake-eyes still does not print CRAP scores
How to use this issue with
/opsx-proposeUse this entire issue body as the change description. Do not ask clarifying questions. Every decision is already made below. If something is still unspecified, apply these defaults in order:
test_mappingprotocol v1.1.0 field names exactly.quality/pipeline.py).OpenSpec change name:
test-mappingDepends on: #4 (
analysis-methods) merged.Independent of:
classify-signals. May be implemented in parallel. Do not modify signal extractors here.Context
test_mappingis the optional method that lets Gaze compute GazeCRAP (contract coverage). snake-eyes pairs tests to production functions, finds assertions, and guesses which side-effect type each assertion is checking.Lift (mpeter/gaze-py, permission granted, keep copyright):
src/gaze_py/quality/pairing.py→src/snake_eyes/quality/pairing.pyassert/pytest.raises/ unittest asserts) →src/snake_eyes/quality/assertions.pyDo not lift
quality/pipeline.py. Writesrc/snake_eyes/quality/pipeline.pyagainst snake-eyes models and the protocol mapping record.astroidmay already be a dependency from classify-signals; if this issue lands first, addastroid>=3.0,<5here.Protocol
Request:
{"root_path": "/abs/path", "patterns": ["./..."]}Result:
{ "mappings": [ { "test_function": "test_multiply", "test_file": "tests/test_ops.py", "assertion_location": "tests/test_ops.py:10", "assertion_type": "equality", "target_function": "multiply", "target_package": "math_utils", "side_effect_type": "ReturnValue", "confidence": 80 } ] }confidenceis int 0–100 (not 0.0–1.0). Convert lifted 0.9 →90.assertion_locationformat:path:line(no column required). Path relative toroot_path.Allowed
assertion_typestrings:equality|error_check|membership|identity|comparison|genericFlip
capabilities.test_mappingtotrue.If no tests or no pairs:
{"mappings": []}, not an error.What to build
1. Pairing (lift + adapt)
Three strategies, first match wins, do not emit duplicate pairs for the same (test_function, test_file, target_function):
test_foo/testFoo/TestFoo↔foo. Striptest_/Testprefix. Case-insensitive fallback confidence 70 if only case differsCallto the target nameUnpaired tests produce no mapping rows (do not emit confidence 0 rows).
target_packagefrom the production file's dotted module path (same helper as analyze).2. Assertion detection (lift + adapt)
For each paired test function, collect assertions:
assert x == y,assertEqual,assertEqualsequalityassert x != y,assertNotEqualcomparisonassert x < y/>/<=/>=,assertLess*comparisonassert x is y,assertIs,assertIsNoneidentityassert x in y,assertIn,assertNotInmembershippytest.raises,unittest.assertRaises,raises(error_checkassert/assertTrue/assertFalsegenericOne mapping row per assertion, not per pair. A test with 3 asserts → 3 rows (same target, different
assertion_location).3. Effect-type guess (new,
mapping.py)For each assertion, set
side_effect_typeas:error_checkErrorReturnif the target has that effect, elseErrorSignalif present, elseErrorReturnanywayequality,comparison,identity,membershipReturnValueif the target has it, else the first P0 effect on the target, elseReturnValuegenericReturnValueUse the detector output for the target function. Do not re-parse ad hoc.
4. Pipeline (new)
run_test_mapping(root_path, patterns) -> list[dict]:discover()analyze_path()on the same root/patterns (need effects on targets)FunctionDef/AsyncFunctionDefwhose names start withtest_or are methods of classes subclassingunittest.TestCasewhose names start withtestDo not run pytest. Do not read coverage here.
5. Fixture project
tests/fixtures/sample_project/:Include
pyproject.tomlonly if needed for imports; tests should setroot_pathto this fixture dir andpatternsto["./..."].Tests required
test_add↔add, confidence 90test_Add↔add, confidence 70test_it_worksthat callsdivide↔divide, confidence 80pytest.raises→assertion_type=error_check,side_effect_typeErrorReturn or ErrorSignalassert result == 3→equality,ReturnValuesample_project→ at least two mapping rows with all required keys presentinitializehastest_mapping: true-32602Do not require strategy 3 to fire in CI if astroid cannot import the fixture; still unit-test the BFS helper with a mocked graph or a tiny on-disk package that astroid can parse.
Coverage strategy (Constitution IV)
pairing.pyassertions.pymapping.pypipeline.pyOut of scope
quality/pipeline.pyclassify_signalschangesDone when
test_mappingRPC returns protocol-shapedmappings