feat(learning-harness): declared twins and a fixture-difficulty check

The five byte-identical vibe-py twins now say so in the catalog instead of
only hashing to it, and a declared limit that names one of the unscorable
vocabulary's own reasons keeps that spelling, so `identical_twin` stays one
fact. Leave-one-out over vibe-py consequently runs on 30 rows, not 35.

The difficulty check measures three axes the corpus varies on: binding hops
from the identity source to the constrained use, registration distance, and
guard distance. Measured first, and it failed: the shared obligations
fixture reached (1, 0, 2) where the vibe-py absence rows reach (5, 2, 2).
Every fixture handler carried its own route decorator, so nothing in the
suite exercised a handler registered from another document and no binding
chain was longer than one hop -- the checker was passing on a simpler world
than the one it meets. `from_spec` closes that with the connexion shape
those rows really have. The verdict is committed beside the numbers.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
This commit is contained in:
Marius Ciepluch
2026-09-06 13:09:56 +02:00
co-authored by Claude Opus 5
parent 1843ef9a02
commit d1faa04771
10 changed files with 393 additions and 9 deletions
+4 -1
View File
@@ -143,7 +143,7 @@
- _Depends: 1.3_
- _Requirements: 10.2_
- [ ] 4.2 Identical twins and the fixture-difficulty check
- [x] 4.2 Identical twins and the fixture-difficulty check
- The five identical vibe-py twins carry the identical-twin known-limit reason in the catalog; a test derives a difficulty vector for a unit fixture (binding hops to the identity source, registration present, cross-file registration) and for the corpus rows its docstring names, and fails when the fixture is strictly easier.
- Observable: the twins are excluded from every denominator with their reason listed; the shared obligations fixture is compared against the re-harvested vampi rows and the check's verdict is recorded.
- _Depends: 4.1_
@@ -233,3 +233,6 @@
- **3.1, refusals were `continue` statements.** `Refusal.degradation()` existed and nothing called it. The mechanism lever now records every candidate it declined with the holdout pairs that taught it, leave-one-out records the pairs it kept out of the teaching set, and `improve` carries both into the round outcome. `semantic.seed` reported `split:train` for a train-split identical twin, which is simply untrue; an unscorable row now reports its own reason. `evaluate_profiles` and `evaluate_mechanism_profiles` are deliberately split-blind and now say so in their docstrings: they measure a ruleset and never learn from a pair, so a filter there would only shrink the evidence.
- Test-quality repairs the review asked for, each of which changed a number: two split tests were measuring an `x = 1` fixture that teaches nothing whatever the rule says, so both arms now use a pair that really carries a shape and the lever test asserts the same shape *is* admitted when a train pair taught it; the two CLI scoreboard tests asserted against output that was all zeros, and now use a side-aware detector stub (the shipped `scripted` stub answers twice, then goes quiet, and puts its finding outside the labeled function — three separate ways to make a family number zero); `ScriptedChatClient` gained an opt-in `cycle` so a stub cannot silently run out of answers mid-run; the sandbox-candidate assertion in `tests/test_learning_scan.py` compared verdict paths against a prefix no implementation would produce and now compares finding ids; the manifest's classifier block reported `measure_classifier([])`, all zeros, and now reports what this scan actually routed.
- Carry-forward, unchanged and still capping every holdout number: the nineteen straddling repositories, and — found during task 4.1 — **nine of the ten duplicate groups sit on both sides of the split**, which the repository-level split cannot see because the duplicates are different repositories.
- Task 4.2 (2026-09-06, RED first: 2 failing tests in `tests/test_fixture_difficulty.py`, and the check's own arm — `test_the_check_catches_a_fixture_that_is_easier_on_one_axis` — fails if the fixture hardening is reverted). Two parts:
- **The five twins declare their limit.** A content hash is evidence; the catalog row is the maintainer standing behind it, and without one a re-harvest that changes a byte turns a known-unscorable row back into a scored one with no review. A declared limit that names one of the vocabulary's own reasons keeps that spelling, so `identical_twin` declared and `identical_twin` computed stay one fact instead of splitting across two buckets in every aggregation. `semantic.seed._skip_reason` was reordered for the same reason: an unscorable row reports `unscorable:<reason>` before either the known-limit or the split branch can claim it. Consequence, recorded because it moves a published number: leave-one-out over vibe-py now runs on **30 rows, not 35** — a pair whose two sides are the same code cannot be held out against itself.
- **The fixture-difficulty check** (`learning/difficulty.py`), on three axes the corpus really varies on: binding hops from the identity source to the constrained use, registration distance (0 decorator on the handler, 1 statement elsewhere in the file, 2 another document), and guard distance (0 decorator, 1 call in the body, 2 nothing visible). The comparison is per axis over the whole set, because a suite covers a bug shape with several handlers between them. **Measured first, and it failed: the shared obligations fixture reached (1, 0, 2) where the vibe-py absence rows reach (5, 2, 2).** Every shared handler carried its own route decorator, so nothing in the suite exercised a handler registered from another document and no chain was longer than one binding — the checker was passing on a simpler world than the one it meets. `from_spec` was added to the fixture: the connexion shape those rows really have, a token validated by a call rather than a decorator and five bindings between the token and the constraint. The verdict is committed as `benchmarks/measurements/2026-09-06-fixture-difficulty.json` with both reaches and the history of the first measurement. One existing expectation moved with it (`from_spec` reads the user row and then the book: two real operations).
@@ -0,0 +1,88 @@
{
"axes": [
"binding_hops",
"registration_distance",
"guard_distance"
],
"command": "uv run pytest tests/test_fixture_difficulty.py",
"corpus": [
{
"binding_hops": 5,
"function": "get_by_title",
"guard_distance": 1,
"registration_distance": 2
},
{
"binding_hops": 2,
"function": "update_password",
"guard_distance": 1,
"registration_distance": 2
},
{
"binding_hops": 0,
"function": "get",
"guard_distance": 2,
"registration_distance": 0
},
{
"binding_hops": 0,
"function": "delete",
"guard_distance": 2,
"registration_distance": 0
}
],
"corpus_reach": [
5,
2,
2
],
"corpus_rows": [
"vampi-books-get-by-title",
"vampi-users-update-password",
"threatbyte-api-v1-get",
"threatbyte-api-v1-delete"
],
"fixture": [
{
"binding_hops": 0,
"function": "guarded",
"guard_distance": 0,
"registration_distance": 0
},
{
"binding_hops": 1,
"function": "constrained",
"guard_distance": 0,
"registration_distance": 0
},
{
"binding_hops": 1,
"function": "constrained_too",
"guard_distance": 1,
"registration_distance": 0
},
{
"binding_hops": 0,
"function": "leaky",
"guard_distance": 2,
"registration_distance": 0
},
{
"binding_hops": 5,
"function": "from_spec",
"guard_distance": 1,
"registration_distance": 2
}
],
"fixture_reach": [
5,
2,
2
],
"fixture_source": "tests/test_obligation_operations.py::APP",
"generated": "2026-09-06",
"history": "First measured with the fixture reaching (1, 0, 2) against a corpus reaching (5, 2, 2): every shared obligations handler carried its route decorator, so nothing in the suite exercised a handler registered from another document, and no chain was longer than one binding. `from_spec` was added to the fixture to close that, and it is the connexion shape the vibe-py rows really have.",
"note": "learning-harness Req 10.4. Higher is harder on every axis. `binding_hops` counts the assignments between the identity source and the constrained use; `registration_distance` is 0 for a route decorator on the handler, 1 for a statement elsewhere in the same file, 2 for another document entirely; `guard_distance` is 0 for a decorator guard, 1 for a call in the body, 2 for no visible guard. The comparison is per axis over the whole set: a suite covers a bug shape with several handlers between them, but its hardest case on an axis must not fall short of the corpus rows its tests claim to cover.",
"strictly_easier": false,
"verdict": "fixture_matches_corpus"
}
+5
View File
@@ -2324,6 +2324,7 @@ fix_policy = "silent"
provenance = "human"
split = "holdout"
review_tier = "seeded"
known_limit = "identical_twin"
[[pair.expected]]
cwe = "CWE-89"
@@ -3207,6 +3208,7 @@ fix_policy = "silent"
provenance = "human"
split = "train"
review_tier = "seeded"
known_limit = "identical_twin"
[[pair.expected]]
cwe = "CWE-1336"
@@ -3236,6 +3238,7 @@ fix_policy = "silent"
provenance = "human"
split = "holdout"
review_tier = "seeded"
known_limit = "identical_twin"
[[pair.expected]]
cwe = "CWE-79"
@@ -3354,6 +3357,7 @@ fix_policy = "silent"
provenance = "human"
split = "holdout"
review_tier = "seeded"
known_limit = "identical_twin"
[[pair.context]]
relpath = "openapi_specs/openapi3.yml"
@@ -3656,6 +3660,7 @@ fix_policy = "silent"
provenance = "human"
split = "train"
review_tier = "seeded"
known_limit = "identical_twin"
[[pair.expected]]
cwe = "CWE-502"
+5
View File
@@ -2290,6 +2290,7 @@ trap_id = "dsvw-fp-001"
acceptable_cwes = ["CWE-89", "CWE-564", "CWE-943"]
evidence = "SQL injection via id parameter directly concatenated into SELECT query without sanitization"
commit_url = "https://github.com/stamparm/DSVW/blob/7d40f4b7939c901610ed9b85724552d60e7d63fa/dsvw.py#L30"
known_limit = "identical_twin"
split = "holdout"
[[recipe]]
@@ -3167,6 +3168,7 @@ trap_id = "python-insecure-app-fp-001"
acceptable_cwes = ["CWE-1336", "CWE-94", "CWE-95"]
evidence = "Server-Side Template Injection via Jinja2. User-controlled 'name' query parameter is interpolated into an f-string on line 38 to build 'content', which is then passed to Template(content).render() on line 41. An attacker can inject Jinja2 t"
commit_url = "https://github.com/kolega-ai/realvuln-python-insecure-app/blob/80b894a2f754590747a31c92b2088be086421953/app/main.py#L41"
known_limit = "identical_twin"
split = "train"
[[recipe]]
@@ -3198,6 +3200,7 @@ trap_id = "python-ssti-fp-001"
acceptable_cwes = ["CWE-79", "CWE-80"]
evidence = "The Jinja2 Environment is created with default settings (autoescape=False). User input from the 'username' query parameter is concatenated into the template string and the rendered output is returned as HTMLResponse. An attacker can inject "
commit_url = "https://github.com/TheWation/PythonSSTI/blob/76339c27d5a870c0a81bad6aee5f9bd516592145/main.py#L14"
known_limit = "identical_twin"
split = "holdout"
[[recipe]]
@@ -3328,6 +3331,7 @@ evidence = "Broken Object Level Authorization (BOLA). When vuln=1, get_by_title
commit_url = "https://github.com/erev0s/VAmPI/blob/1713b54b601ad29582581eeda4b31fceb1319874/api_views/books.py#L51"
family = "access_control"
context = ["openapi_specs/openapi3.yml"]
known_limit = "identical_twin"
split = "holdout"
[[recipe]]
@@ -3672,6 +3676,7 @@ trap_id = "vulnpy-fp-011"
acceptable_cwes = ["CWE-502", "CWE-94"]
evidence = "User input encoded to bytes and passed to pickle.load(), which can execute arbitrary code via crafted pickle payloads"
commit_url = "https://github.com/Contrast-Security-OSS/vulnpy/blob/d1d0f64c06f038fe59a049e0e6001bfaf9839697/src/vulnpy/trigger/deserialization.py#L8"
known_limit = "identical_twin"
split = "train"
[[recipe]]
+139
View File
@@ -0,0 +1,139 @@
"""How hard a piece of code makes an absence bug to see (learning-harness, Req 10.4).
A unit fixture that is easier than the corpus is worse than no fixture: the checker passes on it, the number
looks like capability, and the corpus rows it claims to cover keep failing for reasons nobody is looking at. The
three dimensions below are the ones the corpus actually varies on, measured on the vulnerable side:
- ``binding_hops`` — how many assignments sit between the identity source and the constrained use. `filter_by(
user_id=request.user.id)` is nothing to trace; `resp = token_validator(...)` then `User.query.filter_by(
username=resp['sub'])` then `filter_by(user=user, ...)` is two.
- ``registration_distance`` — 0 when the route decorator sits on the handler, 1 when a statement elsewhere in the
same file registers it, 2 when the registration lives in another document entirely (connexion's `operationId`,
an express router). Two is where the entry-point mapper stops being able to name the handler at all.
- ``guard_distance`` — 0 when a decorator carries the guard, 1 when the guard is a call in the body, 2 when the
handler carries no visible guard. A decorator is a pattern; a call in the body has to be understood.
Higher is harder on every axis, so "strictly easier" is: no dimension harder, at least one easier.
"""
from __future__ import annotations
import ast
import re
from collections.abc import Sequence
from dataclasses import dataclass
IDENTITY_SOURCES = ("current_user", "request.user", "session", "g.user", "auth", "token", "jwt", "claims", "sub", "principal")
GUARD_TOKENS = ("login_required", "token_required", "requires_auth", "authenticate", "authorize", "token_validator", "current_user")
_ROUTE_DECORATOR = re.compile(r"\.(route|get|post|put|patch|delete)\b")
@dataclass(frozen=True)
class DifficultyVector:
"""One handler's difficulty. Higher is harder on every axis."""
function: str
binding_hops: int
registration_distance: int
guard_distance: int
def axes(self) -> tuple[int, int, int]:
return (self.binding_hops, self.registration_distance, self.guard_distance)
def to_dict(self) -> dict[str, object]:
return {
"function": self.function,
"binding_hops": self.binding_hops,
"registration_distance": self.registration_distance,
"guard_distance": self.guard_distance,
}
def difficulty_of(source: str, function: str, *, context: Sequence[str] = ()) -> DifficultyVector:
"""The vector for one Python handler. ``context`` is any document that registers it from outside the file."""
try:
tree = ast.parse(source)
except SyntaxError:
return DifficultyVector(function=function, binding_hops=0, registration_distance=2, guard_distance=2)
node = _function(tree, function)
if node is None:
return DifficultyVector(function=function, binding_hops=0, registration_distance=2, guard_distance=2)
decorators = [ast.unparse(item) for item in node.decorator_list] + _class_decorators(tree, node)
return DifficultyVector(
function=function,
binding_hops=_binding_hops(node),
registration_distance=_registration_distance(source, tree, function, decorators, context),
guard_distance=_guard_distance(node, decorators),
)
def hardest(vectors: Sequence[DifficultyVector]) -> tuple[int, int, int]:
"""The per-axis maximum over a set of handlers: what the set can be asked to cope with."""
if not vectors:
return (0, 0, 0)
return tuple(max(row.axes()[index] for row in vectors) for index in range(3)) # type: ignore[return-value]
def strictly_easier(fixture: Sequence[DifficultyVector], corpus: Sequence[DifficultyVector]) -> bool:
"""True when the fixture set reaches no further than the corpus on any axis and falls short on at least one.
Per axis and over the whole set, because a suite covers a bug shape with several handlers between them: one
fixture handler may carry the long binding chain and another the missing guard. What must not happen is a suite
whose hardest case on some axis is easier than the corpus rows its tests claim to cover."""
if not corpus:
return False
limit, reach = hardest(corpus), hardest(fixture)
return all(reach[index] <= limit[index] for index in range(3)) and any(reach[index] < limit[index] for index in range(3))
def _function(tree: ast.AST, name: str) -> ast.FunctionDef | ast.AsyncFunctionDef | None:
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name == name:
return node
return None
def _class_decorators(tree: ast.AST, node: ast.AST) -> list[str]:
for klass in ast.walk(tree):
if isinstance(klass, ast.ClassDef) and any(child is node for child in klass.body):
return [ast.unparse(item) for item in klass.decorator_list]
return []
def _binding_hops(node: ast.FunctionDef | ast.AsyncFunctionDef) -> int:
"""Assignments that carry the identity forward before it constrains anything."""
tainted: set[str] = set()
hops = 0
for statement in ast.walk(node):
if not isinstance(statement, ast.Assign):
continue
text = ast.unparse(statement.value)
if any(token in text for token in IDENTITY_SOURCES) or any(name in text for name in tainted):
hops += 1
for target in statement.targets:
tainted.update(part.id for part in ast.walk(target) if isinstance(part, ast.Name))
return hops
def _registration_distance(source: str, tree: ast.AST, function: str, decorators: Sequence[str], context: Sequence[str]) -> int:
if any(_ROUTE_DECORATOR.search(item) for item in decorators):
return 0
mention = re.compile(rf"(?<![\w.]){re.escape(function)}(?!\w)")
body = _function(tree, function)
span = range(getattr(body, "lineno", 0), getattr(body, "end_lineno", 0) + 1) if body is not None else range(0)
for number, line in enumerate(source.splitlines(), start=1):
if number not in span and mention.search(line) and not line.strip().startswith(("def ", "async def ", "#")):
return 1
if any(re.search(rf"(?<![\w]){re.escape(function)}(?!\w)", item) for item in context):
return 2
return 2 # nothing in reach registers it, which is at least as hard as a registration in another document
def _guard_distance(node: ast.FunctionDef | ast.AsyncFunctionDef, decorators: Sequence[str]) -> int:
if any(token in item.lower() for item in decorators for token in GUARD_TOKENS):
return 0
body = ast.unparse(node).lower()
return 1 if any(token in body for token in GUARD_TOKENS) else 2
__all__ = ["GUARD_TOKENS", "IDENTITY_SOURCES", "DifficultyVector", "difficulty_of", "hardest", "strictly_easier"]
+10 -2
View File
@@ -625,10 +625,18 @@ def _body_digest(path: Path) -> str | None:
return hashlib.sha256(body.encode()).hexdigest() if body else None
# The reasons a row may carry verbatim from the catalog, because the loader computes them under the same names.
UNSCORABLE_REASONS = frozenset({"identical_twin"})
def _unscorable_reason(known_limit: object, vuln_file: Path, fixed_file: Path, *, vendored: bool) -> str | None:
"""Why this pair cannot be scored, or None. A declared known limit wins over anything computed."""
"""Why this pair cannot be scored, or None. A declared known limit wins over anything computed.
A limit that names one of the vocabulary's own reasons keeps that spelling: `identical_twin` declared and
`identical_twin` computed are one corpus fact, and two spellings would split it across two buckets in every
aggregation (Req 4.1, 10.3)."""
if known_limit:
return f"known_limit:{known_limit}"
return str(known_limit) if str(known_limit) in UNSCORABLE_REASONS else f"known_limit:{known_limit}"
if not vendored:
return None
digest = _body_digest(vuln_file)
+6 -4
View File
@@ -195,14 +195,16 @@ def _obligation_facts() -> ObligationFacts:
def _skip_reason(case: PairCase) -> str | None:
from ..learning.split import is_teacher
if not is_teacher(case) and case.review_tier in GATING_TIERS and case.vendored and not case.known_limit:
# learning-harness Req 5.1: only train-split, scorable pairs teach. Say which of the two it was: two
# vibe-py rows are train-split identical twins, and calling them `split:train` is simply untrue.
return f"unscorable:{case.unscorable}" if case.unscorable else f"split:{case.split}"
if case.review_tier not in GATING_TIERS:
return f"tier:{case.review_tier}"
if case.unscorable:
# One spelling per corpus fact. `identical_twin` declared in the catalog and `identical_twin` computed from
# the bytes are the same reason, and calling one of them `split:train` is simply untrue (Req 5.1, 10.3).
return f"unscorable:{case.unscorable}"
if case.known_limit:
return f"known_limit:{case.known_limit}"
if not is_teacher(case) and case.vendored:
return f"split:{case.split}"
if not case.vuln_file.is_file() or not case.fixed_file.is_file():
return "pointer_pair_not_cached" if not case.vendored else "excerpt_missing"
return None
+117
View File
@@ -0,0 +1,117 @@
"""A unit fixture may not be easier than the corpus it claims to cover (learning-harness 4.2, Req 10.3, 10.4).
Two things a corpus number cannot survive: a byte-identical pair counted as a detector result, and a fixture that
passes because the world it describes is simpler than the one the detector meets. The first is excluded by name;
the second is measured on three axes the corpus actually varies on, and the comparison is recorded.
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).parent))
from test_obligation_operations import APP # noqa: E402
from openultrasast.learning.difficulty import DifficultyVector, difficulty_of, hardest, strictly_easier # noqa: E402
from openultrasast.pairs import DEFAULT_CATALOG, load_pair_catalog, select_vendored # noqa: E402
TWINS = {
"dsvw-dsvw-do-get",
"python-insecure-app-main-try-hack-me",
"pythonssti-main-read-root",
"vampi-books-get-by-title",
"vulnpy-deserialization-do-pickle-load",
}
# The rows the shared obligations fixture claims to cover: the vibe-py absence pairs.
COVERED = ("vampi-books-get-by-title", "vampi-users-update-password", "threatbyte-api-v1-get", "threatbyte-api-v1-delete")
FIXTURE_HANDLERS = ("guarded", "constrained", "constrained_too", "leaky", "from_spec")
REPORT = Path("benchmarks/measurements/2026-09-06-fixture-difficulty.json")
# --- identical twins ---------------------------------------------------------
def test_every_identical_twin_declares_the_limit_rather_than_only_hashing_to_it() -> None:
"""A content hash is evidence; the catalog row is the maintainer standing behind it.
A re-harvest that changes one byte would otherwise turn a known-unscorable row back into a scored one with no
review, and the number would move for a reason nobody chose."""
cases = {case.name: case for case in select_vendored(load_pair_catalog(DEFAULT_CATALOG))}
for name in sorted(TWINS):
assert cases[name].known_limit == "identical_twin", f"{name} is a twin by content but says nothing in the catalog"
assert cases[name].unscorable == "identical_twin", "the declared limit and the computed one must be one reason, not two"
def test_the_twins_leave_every_denominator_with_their_reason_listed() -> None:
from openultrasast.learning.families import load_families
from openultrasast.learning.scoring import PairFamilyScore, aggregate
cases = [case for case in select_vendored(load_pair_catalog(DEFAULT_CATALOG)) if case.name in TWINS]
scores = [
PairFamilyScore(
pair=case.name, family="injection", slice=case.slice, runs=(), outcome="unscorable", unscorable_reason=case.unscorable
)
for case in cases
]
metrics = aggregate(scores, taxonomy=load_families())["injection"]
assert metrics.scorable == 0
assert metrics.unscorable == {"identical_twin": len(TWINS)}
# --- the difficulty check ----------------------------------------------------
def _corpus_vectors() -> list[DifficultyVector]:
cases = {case.name: case for case in select_vendored(load_pair_catalog(DEFAULT_CATALOG))}
vectors = []
for name in COVERED:
case = cases[name]
function = next(row.function for row in case.expected if row.function)
context = [vuln.read_text() for _relpath, vuln, _fixed in case.context_files]
vectors.append(difficulty_of(case.vuln_file.read_text(), function, context=context))
return vectors
def test_the_shared_obligations_fixture_is_not_easier_than_the_rows_it_covers() -> None:
"""The check Req 10.4 asks for, on the fixture every obligations test shares."""
fixture = [difficulty_of(APP, name) for name in FIXTURE_HANDLERS]
corpus = _corpus_vectors()
assert not strictly_easier(fixture, corpus), (
f"the fixture reaches {hardest(fixture)} where the corpus reaches {hardest(corpus)} "
"(binding hops, registration distance, guard distance)"
)
def test_the_check_catches_a_fixture_that_is_easier_on_one_axis(tmp_path: Path) -> None:
"""Mutation of the check itself: drop the corpus-shaped handler and it has to fail."""
without = [difficulty_of(APP, name) for name in FIXTURE_HANDLERS if name != "from_spec"]
assert strictly_easier(without, _corpus_vectors())
assert hardest(without)[1] == 0, "every remaining handler carries its route decorator; nothing is registered elsewhere"
def test_the_verdict_is_recorded_beside_the_numbers_it_explains() -> None:
assert REPORT.is_file(), "the fixture-difficulty verdict is committed, not recomputed from memory"
payload = json.loads(REPORT.read_text())
fixture = [difficulty_of(APP, name) for name in FIXTURE_HANDLERS]
corpus = _corpus_vectors()
assert payload["fixture"] == [row.to_dict() for row in fixture]
assert payload["corpus"] == [row.to_dict() for row in corpus]
assert payload["strictly_easier"] is False and payload["verdict"] == "fixture_matches_corpus"
assert payload["axes"] == ["binding_hops", "registration_distance", "guard_distance"]
@pytest.mark.parametrize(
("source", "expected"),
[
("@app.route('/x')\ndef h():\n return Book.query.filter_by(user_id=request.user.id).first()\n", (0, 0, 2)),
("@app.route('/x')\n@login_required\ndef h():\n return 1\n", (0, 0, 0)),
("def h():\n u = current_user.id\n v = u\n return v\n", (2, 2, 1)),
("app.add_url_rule('/x', view_func=h)\n\n\ndef h():\n return 1\n", (0, 1, 2)),
],
)
def test_each_axis_is_read_off_the_code(source: str, expected: tuple[int, int, int]) -> None:
assert difficulty_of(source, "h").axes() == expected
+3 -1
View File
@@ -103,7 +103,9 @@ def test_cli_pairs_loo_writes_the_artifact_offline(tmp_path: Path, monkeypatch:
with redirect_stdout(buf):
assert main(["pairs", "--slice", "vibe-py", "--loo", "--json", "--loo-out", str(tmp_path / "loo.json")]) == 0
payload = json.loads(buf.getvalue())
assert payload["per_slice"]["vibe-py"]["pairs"] == 35 and "found_by" in payload["outcomes"][0]
# 35 vendored rows less the five byte-identical twins, which task 4.2 declared unscorable in the catalog: a
# pair whose two sides are the same code cannot be held out against itself.
assert payload["per_slice"]["vibe-py"]["pairs"] == 30 and "found_by" in payload["outcomes"][0]
assert json.loads((tmp_path / "loo.json").read_text()) == payload
+16 -1
View File
@@ -19,7 +19,19 @@ APP = (
"@app.route('/books/any/<title>')\ndef leaky(title):\n"
" owner = request.args.get('owner')\n"
" return Book.query.filter_by(user_id=owner, book_title=title).first()\n\n\n"
"@app.route('/health')\ndef health():\n return 'ok'\n"
"@app.route('/health')\ndef health():\n return 'ok'\n\n\n"
# Registered from a separate OpenAPI document, guarded by a call rather than a decorator, and five bindings
# between the token and the constraint: the shape the vibe-py absence rows really have. Without it this fixture
# is strictly easier than the corpus it claims to cover (learning-harness Req 10.4).
"def from_spec(book_title):\n"
" resp = token_validator(request.headers.get('Authorization'))\n"
" if 'error' in resp:\n"
" return None\n"
" claims = resp\n"
" subject = claims['sub']\n"
" user = User.query.filter_by(username=subject).first()\n"
" owner = user.id\n"
" return Book.query.filter_by(user_id=owner, book_title=book_title).first()\n"
)
@@ -60,6 +72,9 @@ def test_find_operations_names_kind_fact_function_and_resource() -> None:
("constrained", "protected_read", "book"),
("constrained_too", "protected_read", "book"),
("leaky", "protected_read", "book"),
# `from_spec` reads the user row to resolve the token subject and then the book: two operations, both real
("from_spec", "protected_read", "user"),
("from_spec", "protected_read", "book"),
]
assert all(op.path == "app.py" and op.fact_id == "orm-read" and op.line > 0 for op in ops)
assert not [op for op in ops if op.function == "health"] # a constant return is no operation