mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
unseen: implement freeze p1 (group 3: bisect guard, freeze writer, protocol)
Group 3 was unimplemented, so there was no way to turn the draft into a frozen pool. Add: - bisect_guard.py: stages the cache draft and bisects it against the reserved-repository guard, run as a black box (pytest -k referenced, stdout/stderr discarded, exit code only; an exit other than 0/1 aborts as an instrument failure). Drops guard-tripping candidates and reports the dropped COUNT only -- never a name or URL. Guard runner is injectable for offline tests. - freeze.py: writes canonical pool-p1.toml (+ private counterpart, reusing draft's public/private split and private_digest) and freeze-p1.json (used-set digest, per-source counts, guard result, dropped count, label-check counts, freeze digest = sha256 of the canonical public manifest). A matching retry is a no-op; any changed frozen artifact requires a new pool id (never an overwrite). - protocol-p1.md: the frozen scoring/statistics protocol from design sections 3 and 4. Offline only; the guard subprocess is the sole subprocess and is mocked in tests. 42 new tests pass (incl the group-1.3 guard digest check on a fixture pool); neighbouring unseen suites green; ruff clean. New files only -- does not touch the journal-pinned draft/extract modules, so a running draw is unaffected. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
23ede07608
commit
197fe8a19c
@@ -130,14 +130,14 @@ Execution rules:
|
||||
- _Requirements: 2.1, 2.2_
|
||||
|
||||
- [ ] 3. Freeze p1 (design 1.2, 6; increment 3)
|
||||
- [ ] 3.1 Guard bisection tool
|
||||
- [x] 3.1 Guard bisection tool
|
||||
- `benchmarks/unseen/bisect_guard.py`: rewrites the draft with a candidate subset, runs
|
||||
`.venv/bin/pytest -q -p no:cacheprovider tests/test_independent_population.py -k referenced` with stdout and
|
||||
stderr discarded, reads only the exit code, and bisects until green. Reports the dropped count only.
|
||||
- `tests/test_unseen_bisect.py` with an injected fake guard (a hidden set): finds and drops exactly the hidden
|
||||
members; never logs a candidate; an exit code other than 0 or 1 aborts (instrument failure).
|
||||
- _Requirements: 1.1_
|
||||
- [ ] 3.2 Freeze writer
|
||||
- [x] 3.2 Freeze writer
|
||||
- `benchmarks/unseen/freeze.py`: canonical `pool-p1.toml` (and the private counterpart) from the bisected draft;
|
||||
`freeze-p1.json` with used-set digest and per-source counts, guard result, dropped count, label-check counts
|
||||
and the freeze digest (sha256 of the canonical manifest). Refuses to write over a frozen pool; a correction
|
||||
@@ -145,7 +145,7 @@ Execution rules:
|
||||
- Tests: digest stable under key order; overwrite refused; the guard's digest test (1.3) passes on a fixture
|
||||
pool.
|
||||
- _Requirements: 1.2_
|
||||
- [ ] 3.3 Protocol p1
|
||||
- [x] 3.3 Protocol p1
|
||||
- `benchmarks/unseen/protocol-p1.md`: the scoring rules of design section 4 (default flags, decision 4.0's
|
||||
long-deadline arm, catch and location rules, false alarm, coverage states), the statistics of section 3 and
|
||||
7, and the adoption gate of 7.3, stated before any replay.
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
"""Stage a cache draft, bisect with the black-box guard, and retain a digest receipt.
|
||||
|
||||
Coordinator: python -m benchmarks.unseen.bisect_guard --draft CACHE/draft-p1.toml
|
||||
The cache is never changed. Successful subsets remain at benchmarks/unseen/draft-p1.toml
|
||||
and private/draft-p1.toml; failures remove only this invocation's staging files.
|
||||
Stdout reports only the dropped count. No reservation is opened by this module.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
from .draft import manifest, private_digest
|
||||
from .extract import InstrumentFailure
|
||||
from .journal import atomic, digest
|
||||
from .source import PERMISSIVE
|
||||
|
||||
|
||||
def read_manifest(path: Path) -> dict:
|
||||
try:
|
||||
raw = path.read_bytes()
|
||||
if not raw:
|
||||
raise ValueError
|
||||
data = tomllib.loads(raw.decode("utf-8"))
|
||||
if set(data) - {"status", "pool", "seed", "repository"}:
|
||||
raise ValueError
|
||||
if data["status"] != "draft" or data["pool"] != "p1" or type(data["seed"]) is not int:
|
||||
raise ValueError
|
||||
if not isinstance(data.get("repository", []), list):
|
||||
raise ValueError
|
||||
return data
|
||||
except (OSError, ValueError, KeyError, TypeError):
|
||||
raise InstrumentFailure("invalid_draft") from None
|
||||
|
||||
|
||||
def read_pair(public_path: Path, private_path: Path) -> tuple[dict, dict]:
|
||||
"""Validate the licensing split and every private pointer before staging names."""
|
||||
public, private = read_manifest(public_path), read_manifest(private_path)
|
||||
try:
|
||||
if public["seed"] != private["seed"]:
|
||||
raise ValueError
|
||||
hidden = {row["id"]: row for row in private.get("repository", [])}
|
||||
rows = public.get("repository", [])
|
||||
if len(hidden) != len(private.get("repository", [])) or len({r["id"] for r in rows}) != len(rows):
|
||||
raise ValueError
|
||||
if set(hidden) != {r["id"] for r in rows if r.get("private")}:
|
||||
raise ValueError
|
||||
for row in rows:
|
||||
if type(row["slice"]) is not int or row["slice"] not in (1, 2, 3):
|
||||
raise ValueError
|
||||
if row.get("private"):
|
||||
full = hidden[row["id"]]
|
||||
expected = {"id": full["id"], "slice": full["slice"], "private": True, "sha256": private_digest(full)}
|
||||
if row != expected or full["license_class"] != "private":
|
||||
raise ValueError
|
||||
elif row["license_class"] != "permissive" or row["license_spdx"] not in PERMISSIVE:
|
||||
raise ValueError
|
||||
return public, private
|
||||
except (ValueError, KeyError, TypeError):
|
||||
raise InstrumentFailure("invalid_draft_pair") from None
|
||||
|
||||
|
||||
def pytest_guard(root: Path) -> int:
|
||||
"""Read only the exit code; never capture, inspect or forward either stream."""
|
||||
try:
|
||||
return subprocess.run(
|
||||
[".venv/bin/pytest", "-q", "-p", "no:cacheprovider", "tests/test_independent_population.py", "-k", "referenced"],
|
||||
cwd=root,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
check=False,
|
||||
).returncode
|
||||
except OSError:
|
||||
raise InstrumentFailure("guard_launch") from None
|
||||
|
||||
|
||||
def run(root: Path, draft: Path, *, private_draft: Path | None = None, guard_runner=None) -> dict:
|
||||
"""Inject guard_runner(root) -> exit code for offline tests; default is pytest_guard.
|
||||
|
||||
Run from an otherwise clean reservation directory. Existing staging/receipt or a
|
||||
frozen p1 is refused, so a crash cannot silently turn a partial subset into input.
|
||||
The receipt binds both retained manifests; freeze refuses edits after the guard.
|
||||
"""
|
||||
private_draft = private_draft or draft.with_name("private-draft-p1.toml")
|
||||
public, private = read_pair(draft, private_draft)
|
||||
rows = public.get("repository", [])
|
||||
if not rows:
|
||||
raise InstrumentFailure("empty_draft")
|
||||
directory = root / "benchmarks/unseen"
|
||||
public_path = directory / "draft-p1.toml"
|
||||
private_path = directory / "private/draft-p1.toml"
|
||||
receipt_path = directory / "bisect-p1.json"
|
||||
targets = (private_path, public_path, receipt_path)
|
||||
if any(p.exists() for p in (*targets, directory / "pool-p1.toml", directory / "freeze-p1.json")):
|
||||
raise FileExistsError("staging_exists")
|
||||
private_path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
||||
runner = guard_runner or pytest_guard
|
||||
created = []
|
||||
|
||||
def check(subset):
|
||||
ids = {r["id"] for r in subset}
|
||||
selected = [r for r in private.get("repository", []) if r["id"] in ids]
|
||||
atomic(private_path, manifest(selected, private["seed"]).encode("utf-8"))
|
||||
atomic(public_path, manifest(subset, public["seed"]).encode("utf-8"))
|
||||
try:
|
||||
code = runner(root)
|
||||
except Exception:
|
||||
raise InstrumentFailure("guard_launch") from None
|
||||
if type(code) is not int or code not in (0, 1):
|
||||
raise InstrumentFailure("guard_exit")
|
||||
return code
|
||||
|
||||
def retain(subset):
|
||||
if not subset or check(subset) == 0:
|
||||
return subset
|
||||
if len(subset) == 1:
|
||||
return []
|
||||
middle = len(subset) // 2
|
||||
return retain(subset[:middle]) + retain(subset[middle:])
|
||||
|
||||
try:
|
||||
# Reserve destinations exclusively before any rewrites; never truncate an
|
||||
# existing coordinator's files. Empty manifests establish the control.
|
||||
for path in targets:
|
||||
with path.open("xb") as handle:
|
||||
created.append(path)
|
||||
os.fchmod(handle.fileno(), 0o600)
|
||||
if check([]) != 0:
|
||||
raise InstrumentFailure("guard_baseline")
|
||||
kept = retain(rows)
|
||||
if check(kept) != 0:
|
||||
raise InstrumentFailure("guard_final")
|
||||
result = {
|
||||
"guard_result": 0,
|
||||
"dropped_count": len(rows) - len(kept),
|
||||
"input_count": len(rows),
|
||||
"draft_digest": digest(read_manifest(public_path)),
|
||||
"private_draft_digest": digest(read_manifest(private_path)),
|
||||
}
|
||||
atomic(receipt_path, (json.dumps(result, sort_keys=True) + "\n").encode())
|
||||
return result
|
||||
except BaseException:
|
||||
for path in reversed(created):
|
||||
path.unlink(missing_ok=True)
|
||||
raise
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--root", type=Path, default=Path.cwd())
|
||||
parser.add_argument("--draft", type=Path, required=True)
|
||||
parser.add_argument("--private-draft", type=Path)
|
||||
args = parser.parse_args(argv)
|
||||
try:
|
||||
result = run(args.root, args.draft, private_draft=args.private_draft)
|
||||
except Exception:
|
||||
print(json.dumps({"status": "instrument_failure"}))
|
||||
return 1
|
||||
print(json.dumps({"dropped_count": result["dropped_count"]}))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,150 @@
|
||||
"""Freeze the staged, bisected p1 without running a guard, git, network or model.
|
||||
|
||||
Coordinator: python -m benchmarks.unseen.freeze --used-report USED-SET-REPORT.json
|
||||
The report is eligibility.UsedSet.report() output. Label-check counts are derived
|
||||
from retained vulnerability changes' label_check fields (missing means unchecked).
|
||||
A matching retry is a no-op; any changed frozen artifact requires a new pool id.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
from .bisect_guard import read_pair
|
||||
from .draft import manifest
|
||||
from .eligibility import SOURCES
|
||||
from .journal import digest, sync_directory
|
||||
|
||||
|
||||
def count(value) -> int:
|
||||
if type(value) is not int or value < 0:
|
||||
raise ValueError("invalid_count")
|
||||
return value
|
||||
|
||||
|
||||
def sha256(value) -> str:
|
||||
if not isinstance(value, str) or not re.fullmatch(r"[0-9a-f]{64}", value):
|
||||
raise ValueError("invalid_digest")
|
||||
return value
|
||||
|
||||
|
||||
def metadata(used_report: dict) -> dict:
|
||||
"""Only known source labels and numeric counts can enter the public record."""
|
||||
try:
|
||||
used_digest = sha256(used_report["sha256"])
|
||||
sources = used_report["sources"]
|
||||
if not sources or set(sources) - {*SOURCES, "memory_local", "memory_s3"}:
|
||||
raise ValueError
|
||||
return {"used_set_digest": used_digest, "source_counts": {k: count(v["count"]) for k, v in sources.items()}}
|
||||
except (ValueError, KeyError, TypeError, AttributeError):
|
||||
raise ValueError("invalid_used_report") from None
|
||||
|
||||
|
||||
def verify_receipt(receipt: dict, public: dict, private: dict) -> None:
|
||||
try:
|
||||
if type(receipt["guard_result"]) is not int or receipt["guard_result"] != 0:
|
||||
raise ValueError
|
||||
if sha256(receipt["draft_digest"]) != digest(public) or sha256(receipt["private_draft_digest"]) != digest(private):
|
||||
raise ValueError
|
||||
if count(receipt["input_count"]) != len(public.get("repository", [])) + count(receipt["dropped_count"]):
|
||||
raise ValueError
|
||||
except (ValueError, KeyError, TypeError):
|
||||
raise ValueError("invalid_guard_receipt") from None
|
||||
|
||||
|
||||
def frozen_text(data: dict) -> bytes:
|
||||
# Reuse the draft serializer, changing only the status. Hash its parsed value,
|
||||
# not the TOML bytes: table/key order and whitespace are not pool identity.
|
||||
text = manifest(data.get("repository", []), data["seed"])
|
||||
return text.replace('status = "draft"', 'status = "frozen"', 1).encode("utf-8")
|
||||
|
||||
|
||||
def matches(path: Path, payload: bytes) -> bool:
|
||||
try:
|
||||
if path.suffix == ".toml":
|
||||
return digest(tomllib.loads(path.read_text(encoding="utf-8"))) == digest(tomllib.loads(payload.decode("utf-8")))
|
||||
return digest(json.loads(path.read_bytes())) == digest(json.loads(payload))
|
||||
except (OSError, ValueError):
|
||||
return False
|
||||
|
||||
|
||||
def run(root: Path, *, used_report: dict) -> dict:
|
||||
directory = root / "benchmarks/unseen"
|
||||
record = metadata(used_report)
|
||||
public, private = read_pair(directory / "draft-p1.toml", directory / "private/draft-p1.toml")
|
||||
try:
|
||||
receipt = json.loads((directory / "bisect-p1.json").read_bytes())
|
||||
except (OSError, ValueError):
|
||||
raise ValueError("invalid_guard_receipt") from None
|
||||
verify_receipt(receipt, public, private)
|
||||
if not public.get("repository"):
|
||||
raise ValueError("empty_pool")
|
||||
labels = dict.fromkeys(("confirmed", "refactor", "unclear", "unchecked"), 0)
|
||||
hidden = {r["id"]: r for r in private.get("repository", [])}
|
||||
try:
|
||||
for pointer in public["repository"]:
|
||||
row = hidden[pointer["id"]] if pointer.get("private") else pointer
|
||||
for change in row["changes"]:
|
||||
if change["kind"] == "vulnerability":
|
||||
labels[change.get("label_check", "unchecked")] += 1
|
||||
except (KeyError, TypeError):
|
||||
raise ValueError("invalid_label_check") from None
|
||||
try:
|
||||
protocol = (directory / "protocol-p1.md").read_bytes()
|
||||
if not protocol:
|
||||
raise ValueError
|
||||
except (OSError, ValueError):
|
||||
raise ValueError("invalid_protocol") from None
|
||||
public_bytes, private_bytes = frozen_text(public), frozen_text(private)
|
||||
record.update(
|
||||
guard_result=0,
|
||||
dropped_count=receipt["dropped_count"],
|
||||
label_check_counts=labels,
|
||||
freeze_digest=digest(tomllib.loads(public_bytes.decode("utf-8"))),
|
||||
protocol_digest=hashlib.sha256(protocol).hexdigest(),
|
||||
)
|
||||
outputs = (
|
||||
(directory / "private/pool-p1.toml", private_bytes),
|
||||
(directory / "pool-p1.toml", public_bytes),
|
||||
(directory / "freeze-p1.json", (json.dumps(record, sort_keys=True, indent=2) + "\n").encode()),
|
||||
)
|
||||
# Check EVERY existing artifact before writing anything. The record is the
|
||||
# completion marker and is written last; matching interrupted writes can resume.
|
||||
if any(path.exists() and not matches(path, payload) for path, payload in outputs):
|
||||
raise FileExistsError("frozen_pool_differs_use_new_pool")
|
||||
for path, payload in outputs:
|
||||
try:
|
||||
with path.open("xb") as handle:
|
||||
os.fchmod(handle.fileno(), 0o600)
|
||||
handle.write(payload)
|
||||
handle.flush()
|
||||
os.fsync(handle.fileno())
|
||||
sync_directory(path.parent)
|
||||
except FileExistsError:
|
||||
if not matches(path, payload):
|
||||
raise FileExistsError("frozen_pool_differs_use_new_pool") from None
|
||||
return record
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--root", type=Path, default=Path.cwd())
|
||||
parser.add_argument("--used-report", type=Path, required=True)
|
||||
args = parser.parse_args(argv)
|
||||
try:
|
||||
report = run(args.root, used_report=json.loads(args.used_report.read_bytes()))
|
||||
except Exception:
|
||||
print(json.dumps({"status": "freeze_refused"}))
|
||||
return 1
|
||||
print(json.dumps(report, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,180 @@
|
||||
# Pool p1: frozen scoring protocol
|
||||
|
||||
This protocol fixes the scoring, uncertainty estimates and adoption gate before any
|
||||
p1 replay. Its authority is the approved
|
||||
[unseen-repo-evaluation design](../../.kiro/specs/unseen-repo-evaluation/design.md),
|
||||
sections 3, 4 and 7 (including decisions 4.0, 4.0a and 7.3). The freeze record binds
|
||||
these exact UTF-8 file bytes by `protocol_digest`. Corrections require a new pool;
|
||||
neither p1 nor this protocol is revised in response to results.
|
||||
|
||||
## Population, slices and use
|
||||
|
||||
Design section 3 fixes three seeded, ecosystem- and `post_cutoff`-stratified slices
|
||||
of 100 repositories: one vulnerability change and 12 ordinary changes per
|
||||
repository, 100 + 1,200 = 1,300 changes per slice, 300 repositories and 3,900 changes
|
||||
overall. Guard-rejected candidates are dropped before freezing; actual retained
|
||||
counts are reported, with no replacement or silent claim that reduced slices meet
|
||||
the planned precision. Frozen slices are never resized.
|
||||
|
||||
Slice 1 supplies the baseline and G1 arms table. Slices 2 and 3 qualify the first
|
||||
two adoption decisions. Once spent, p2 is drawn by the same code and excludes p1.
|
||||
Every use is appended to `usage-p1.jsonl` with slice, decision or experiment id,
|
||||
purpose (`baseline`, `exploratory`, `informed`, `qualifies`), date, result digest and
|
||||
freeze digest. A slice that informed a change cannot qualify that change (3.1).
|
||||
|
||||
The label check reads every third `introducing` change before freezing, marking
|
||||
`confirmed`, `refactor` or `unclear`; report the confirmed share and catch on the
|
||||
confirmed subset. Report catch separately for `introducing` (the headline) and
|
||||
`last_touch` (a lower bound). The former inherits SZZ last-writer/refactor noise;
|
||||
the latter may already contain the flaw in base, making novelty-based silence
|
||||
correct even though scoring counts a miss (design 2.1).
|
||||
|
||||
## Replay arms
|
||||
|
||||
The default hook replay uses `--deadline 30 --engine inline --max-regions 500
|
||||
--mode advisory` (section 4). Retain the artifact and full text output, including
|
||||
every `Skipped:` line.
|
||||
|
||||
Decision 4.0 registers a second slice-1 arm with `--deadline 300` and every other
|
||||
flag unchanged. Commit its experiment manifest with
|
||||
`unseen: {pool: p1, slice: 1}` before its first replay. Report both arms to
|
||||
distinguish deadline misses from engine misses. This arm is a measurement;
|
||||
adopting a longer default deadline is a separate decision under section 7.2.
|
||||
|
||||
Decision 4.0a calibrates cluster overhead against native Docker runc. Use a
|
||||
deterministic paired sample of 20 changes by default, or all of a smaller proof
|
||||
manifest, at a 600-second calibration deadline. Record per-change stage times and
|
||||
median cluster/native slowdown with bootstrap 95% intervals separately for time
|
||||
through quick, engine build and engine queries. Incomplete timings or degraded
|
||||
scans invalidate calibration. Cluster arm deadlines are the 30/300-second
|
||||
deadlines multiplied by the median paired slowdown through quick, rounded to
|
||||
integer seconds; native deadlines stay at 30/300 seconds. Report applied
|
||||
deadlines and factors.
|
||||
|
||||
A deterministic native sample accompanies the cluster slice. Compare whether
|
||||
each push flags anything and report a Wilson 95% agreement interval. If its lower
|
||||
bound is below 95%, or any sample pair is missing or failed, flag the run and
|
||||
exclude cluster results from the baseline. Identify disagreements by opaque
|
||||
change IDs only. Calibration records contain counts, times and opaque digests.
|
||||
Docker checks its data-root free space against the unpacked image size upper
|
||||
bound plus 1 GiB before explicitly pulling; retain sanitized stderr tails and
|
||||
exit codes on failure (4.0a).
|
||||
|
||||
## Catch and false alarm
|
||||
|
||||
A vulnerability change is caught only if a finding matches its labelled family
|
||||
and head location (4.1). Engine findings carry the family; quick-rule CWEs map
|
||||
through the project's family CWE table. Match locations in the corresponding
|
||||
file using the following rules:
|
||||
|
||||
| Family | Required head location |
|
||||
| --- | --- |
|
||||
| `injection`, `path`, `deserialization`, `output_encoding`, `untrusted_destination` | Sink line in the vulnerable function, or within 10 lines of the head-side changed hunks |
|
||||
| `access_control` | The vulnerable function itself (the handler lacking the check), or within 10 lines of the head-side changed hunks |
|
||||
| `config_secrets` | The changed file and head-side changed lines ±10, at file scope (`<global>`) |
|
||||
|
||||
All three user-visible streams count: admitted defects, advisory engine findings
|
||||
and quick-rule matches. Record the stream responsible for each catch. A matching
|
||||
family at the wrong location does not count.
|
||||
|
||||
An ordinary change is a false alarm if **any** of those three streams contains at
|
||||
least one finding, of any family, anywhere (4.2). Report the admitted-defects-only
|
||||
alerts rate beside it. An ordinary change may contain an unknown vulnerability;
|
||||
adjudication does not change this definition. Adjudicate all flagged ordinary
|
||||
changes if there are 30 or fewer, otherwise every third, against the standard
|
||||
of protocol v2, and report the adjudicated share separately (design 2.2).
|
||||
|
||||
## Coverage and degradation
|
||||
|
||||
Assign exactly one state to every change (4.3):
|
||||
|
||||
| State | Definition |
|
||||
| --- | --- |
|
||||
| `instrument_failure` | No record, failed fetch, head commit not verified, or zero bytes of changed files read; also the `suspect_fast` failure path |
|
||||
| `unanalysable` | No changed file uses a language the hook covers (`language_not_covered`) |
|
||||
| `quick_only` | Quick read the changed files but no engine question completed on them |
|
||||
| `engine_covered` | At least one engine question completed on a changed file |
|
||||
|
||||
Prove input access: record checkout files and bytes, bytes of each changed file,
|
||||
JVM wall time and hook exit code. A replay faster than JVM startup is
|
||||
`suspect_fast`, never a clean zero. Failed fetches, missing commits and hook
|
||||
crashes yield failure records, never empty successes. Re-run an instrument
|
||||
failure once, then report it; it never counts as quiet or missed. Refuse to score
|
||||
a slice with more than 5% instrument failures after re-runs (design Error
|
||||
Handling). Preserve skipped/degraded coverage rather than interpreting silence
|
||||
as analysis. Report catch and false alarm overall and by coverage state, with
|
||||
failure counts explicit and failures excluded from outcome denominators.
|
||||
|
||||
## Intervals and planned precision
|
||||
|
||||
Use Wilson 95% intervals with z = 1.96 (section 3). For observed proportion p and
|
||||
denominator n, the centre is `(p + z²/(2n)) / (1 + z²/n)` and the half width is
|
||||
`z * sqrt(p*(1-p)/n + z²/(4n²)) / (1 + z²/n)`. The precision bar uses the larger
|
||||
distance from p to either bound, not just the interval's half width.
|
||||
|
||||
The design's exact minimum sample sizes are:
|
||||
|
||||
| Rate | Assumed p | Maximum distance | Required n |
|
||||
| --- | --- | --- | --- |
|
||||
| False alarm | 0.05 | 3 percentage points | 315 |
|
||||
| False alarm | 0.10 | 3 percentage points | 483 |
|
||||
| False alarm | 0.20 | 3 percentage points | 756 |
|
||||
| Catch | 0.10 | 10 percentage points | 62 |
|
||||
| Catch | 0.25 | 10 percentage points | 88 |
|
||||
| Catch | 0.40 | 10 percentage points | 97 |
|
||||
|
||||
At n = 100 and catch p = 0.4 the Wilson interval is [0.309, 0.498], within
|
||||
10 points. One vulnerability change per repository keeps those observations
|
||||
independent. The 12 ordinary changes within a repository are correlated. With
|
||||
m = 12 and assumed ICC = 0.05 the design effect `1 + (m - 1)*ICC` is 1.55;
|
||||
1,200 ordinary changes give effective n ≈774 ≥756. At p = 0.2 their plain Wilson
|
||||
interval is about ±2.3 points.
|
||||
|
||||
Report a repository-cluster bootstrap 95% interval beside the ordinary-change
|
||||
Wilson interval: resample whole repositories, retaining their within-repository
|
||||
changes. Measure and record ICC on slice 1. If ICC exceeds 0.05, increase the next
|
||||
pool's size, never the frozen slice's. Per-family intervals are reported but not
|
||||
sized; splitting 100 changes over seven families gives roughly ±20–25 points.
|
||||
The design leaves per-family BLOCK sizing to a later decision.
|
||||
|
||||
Report counts, denominators, rates and intervals overall and per family, method,
|
||||
post-cutoff group, stream, coverage state and confirmed subset (sections 3 and 7).
|
||||
Use actual observed counts, not planned sample sizes, for intervals.
|
||||
|
||||
## Fixed budgets and adoption gate
|
||||
|
||||
For score-bearing arms, report catch at false-alarm budgets of 1%, 5% and 10%
|
||||
(7.1). Choose the lowest score threshold whose ordinary-change false-alarm rate
|
||||
stays within the budget; report catch with its Wilson interval and realised
|
||||
false-alarm rate. A threshold selected on the same slice is `exploratory`. For
|
||||
qualification, fix it beforehand on an earlier slice or held-out folds and
|
||||
apply it unchanged. A binary arm has one operating point; report which budgets
|
||||
it meets. Do not substitute AUC for these operating points.
|
||||
|
||||
Registered experiments identify `unseen: {pool, slice}`. Write the unseen
|
||||
estimates beside held-out fold estimates, including each arm's catch at each
|
||||
budget and paired difference B − A with an exact McNemar test on the paired
|
||||
vulnerability changes (planned n = 100; 7.2). Record use in the ledger.
|
||||
|
||||
Adopt candidate B over production A only when both the existing fold rule and
|
||||
the untouched-slice gate pass (7.3):
|
||||
|
||||
1. At the 5% budget with B's threshold fixed beforehand, B's catch is not
|
||||
significantly lower than A's: exact two-sided McNemar on paired vulnerability
|
||||
changes, alpha 0.05.
|
||||
2. For a binary arm, B's false-alarm rate is not significantly higher than A's:
|
||||
exact two-sided McNemar on paired ordinary changes, alpha 0.05. Report the
|
||||
repository-cluster bootstrap interval for the false-alarm difference beside
|
||||
the test because those changes are clustered.
|
||||
|
||||
Write `unseen.gate: pass | regression | not_run`. No slice, a slice already
|
||||
`informed` for this change, or more than 5% instrument failures yields `not_run`;
|
||||
it blocks adoption just as `regression` does. Decision 4.0a also excludes flagged
|
||||
cluster/native agreement runs from baseline use. Failure to detect a significant
|
||||
regression is the specified gate, not evidence that the arms are equivalent.
|
||||
|
||||
Raw per-change artifacts remain outside the repository. Committed measurement
|
||||
records contain counts, rates, intervals and digests, never repository names or
|
||||
URLs. Public p1 names stay only in the permissive pool manifest; private entries
|
||||
remain in the gitignored private manifest and are bound through opaque pointers
|
||||
(design 1.4 and 6).
|
||||
@@ -0,0 +1,192 @@
|
||||
"""Offline guard controls: the hidden reservation exists only inside the fake runner."""
|
||||
|
||||
import importlib
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
d = importlib.import_module("benchmarks.unseen.draft")
|
||||
b = importlib.import_module("benchmarks.unseen.bisect_guard")
|
||||
|
||||
|
||||
def draft_pair(tmp_path, count=9):
|
||||
root, cache = tmp_path / "tree", tmp_path / "cache"
|
||||
root.mkdir()
|
||||
cache.mkdir()
|
||||
rows = []
|
||||
for i in range(count):
|
||||
changes = [{"kind": "vulnerability", "method": "introducing", "base": "a" * 40, "head": "b" * 40}]
|
||||
changes[0]["label_check"] = ("confirmed", "refactor", "unclear")[i % 3]
|
||||
changes += [{"kind": "ordinary", "base": f"{j:040x}", "head": f"{j + 20:040x}"} for j in range(12)]
|
||||
rows.append(
|
||||
{
|
||||
"repository": f"synthetic/candidate-{i}",
|
||||
"url": "https://" + f"github.com/synthetic/candidate-{i}",
|
||||
"license_class": "private" if i % 2 else "permissive",
|
||||
"license_spdx": "GPL-3.0" if i % 2 else "MIT",
|
||||
"slice": i % 3 + 1,
|
||||
"changes": changes,
|
||||
}
|
||||
)
|
||||
rows[-1]["id"] = "r-" + d.private_digest(rows[-1])[:24]
|
||||
public, private = [], []
|
||||
for row in rows:
|
||||
if row["license_class"] == "private":
|
||||
private.append(row)
|
||||
public.append({"id": row["id"], "slice": row["slice"], "private": True, "sha256": d.private_digest(row)})
|
||||
else:
|
||||
public.append(row)
|
||||
draft = cache / "draft-p1.toml"
|
||||
draft.write_text(d.manifest(public, 19))
|
||||
(cache / "private-draft-p1.toml").write_text(d.manifest(private, 19))
|
||||
return root, draft, rows
|
||||
|
||||
|
||||
def staged(root):
|
||||
directory = root / "benchmarks/unseen"
|
||||
public = tomllib.loads((directory / "draft-p1.toml").read_text())
|
||||
private = tomllib.loads((directory / "private/draft-p1.toml").read_text())
|
||||
hidden = {row["id"]: row for row in private.get("repository", [])}
|
||||
rows = [hidden[row["id"]] if row.get("private") else row for row in public.get("repository", [])]
|
||||
assert set(hidden) == {row["id"] for row in public.get("repository", []) if row.get("private")}
|
||||
return rows
|
||||
|
||||
|
||||
@pytest.mark.parametrize("members", [(), (0,), (1, 4, 7), tuple(range(9))])
|
||||
def test_bisects_hidden_members_only(tmp_path, capsys, caplog, members):
|
||||
root, draft, rows = draft_pair(tmp_path)
|
||||
original = draft.read_bytes()
|
||||
reserved = {rows[i]["repository"] for i in members}
|
||||
calls = []
|
||||
|
||||
def guard(root):
|
||||
names = {row["repository"] for row in staged(root)}
|
||||
calls.append(names)
|
||||
return int(bool(names & reserved))
|
||||
|
||||
result = b.run(root, draft, guard_runner=guard)
|
||||
assert result["dropped_count"] == len(members)
|
||||
assert result["guard_result"] == 0
|
||||
assert {r["repository"] for r in staged(root)} == {r["repository"] for r in rows} - reserved
|
||||
assert calls[0] == set() and not (calls[-1] & reserved)
|
||||
assert draft.read_bytes() == original
|
||||
assert capsys.readouterr() == ("", "") and not caplog.records
|
||||
receipt = (root / "benchmarks/unseen/bisect-p1.json").read_text()
|
||||
for row in rows:
|
||||
assert row["repository"] not in receipt and row["url"] not in receipt
|
||||
assert json.loads(receipt) == result
|
||||
|
||||
|
||||
@pytest.mark.parametrize("code", [-9, 2, 3, 4, 5, 127])
|
||||
def test_unknown_exit_aborts_and_removes_staging(tmp_path, code):
|
||||
root, draft, _ = draft_pair(tmp_path)
|
||||
calls = 0
|
||||
|
||||
def guard(root):
|
||||
nonlocal calls
|
||||
calls += 1
|
||||
return 0 if calls == 1 else code
|
||||
|
||||
with pytest.raises(b.InstrumentFailure, match="guard_exit"):
|
||||
b.run(root, draft, guard_runner=guard)
|
||||
assert not list((root / "benchmarks/unseen").rglob("*.toml"))
|
||||
assert not (root / "benchmarks/unseen/bisect-p1.json").exists()
|
||||
|
||||
|
||||
def test_red_empty_baseline_is_not_candidate_rejection(tmp_path):
|
||||
root, draft, _ = draft_pair(tmp_path)
|
||||
with pytest.raises(b.InstrumentFailure, match="guard_baseline"):
|
||||
b.run(root, draft, guard_runner=lambda root: 1)
|
||||
|
||||
|
||||
def test_final_combination_is_checked(tmp_path):
|
||||
root, draft, _ = draft_pair(tmp_path)
|
||||
codes = iter([0, 0, 1])
|
||||
with pytest.raises(b.InstrumentFailure, match="guard_final"):
|
||||
b.run(root, draft, guard_runner=lambda root: next(codes))
|
||||
|
||||
|
||||
def test_guard_subprocess_is_exact_and_discards_both_streams(tmp_path, monkeypatch):
|
||||
calls = []
|
||||
|
||||
def fake(argv, **kwargs):
|
||||
calls.append((argv, kwargs))
|
||||
return subprocess.CompletedProcess(argv, 1)
|
||||
|
||||
monkeypatch.setattr(b.subprocess, "run", fake)
|
||||
assert b.pytest_guard(tmp_path) == 1
|
||||
argv, kwargs = calls[0]
|
||||
assert argv == [".venv/bin/pytest", "-q", "-p", "no:cacheprovider", "tests/test_independent_population.py", "-k", "referenced"]
|
||||
assert kwargs["cwd"] == tmp_path
|
||||
assert kwargs["stdout"] == kwargs["stderr"] == subprocess.DEVNULL
|
||||
|
||||
|
||||
def test_existing_staging_is_never_overwritten(tmp_path):
|
||||
root, draft, _ = draft_pair(tmp_path)
|
||||
path = root / "benchmarks/unseen/draft-p1.toml"
|
||||
path.parent.mkdir(parents=True)
|
||||
path.write_bytes(b"preserve")
|
||||
with pytest.raises(FileExistsError):
|
||||
b.run(root, draft, guard_runner=lambda root: pytest.fail("guard should not run"))
|
||||
assert path.read_bytes() == b"preserve"
|
||||
|
||||
|
||||
def test_cli_outputs_dropped_count_only(tmp_path, monkeypatch, capsys):
|
||||
root, draft, _ = draft_pair(tmp_path)
|
||||
monkeypatch.setattr(b, "pytest_guard", lambda root: 0)
|
||||
assert b.main(["--root", str(root), "--draft", str(draft)]) == 0
|
||||
assert json.loads(capsys.readouterr().out) == {"dropped_count": 0}
|
||||
|
||||
|
||||
def test_cli_failure_does_not_echo_exception(tmp_path, monkeypatch, capsys):
|
||||
root, draft, rows = draft_pair(tmp_path)
|
||||
|
||||
def guard(root):
|
||||
raise OSError(rows[0]["url"])
|
||||
|
||||
monkeypatch.setattr(b, "pytest_guard", guard)
|
||||
assert b.main(["--root", str(root), "--draft", str(draft)]) == 1
|
||||
assert capsys.readouterr() == ('{"status": "instrument_failure"}\n', "")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("problem", ["empty_file", "empty_pool", "missing_private", "bad_pointer", "private_in_public"])
|
||||
def test_invalid_draft_never_runs_guard(tmp_path, problem):
|
||||
root, draft, rows = draft_pair(tmp_path)
|
||||
private = draft.with_name("private-draft-p1.toml")
|
||||
if problem == "empty_file":
|
||||
draft.write_bytes(b"")
|
||||
elif problem == "empty_pool":
|
||||
draft.write_text(d.manifest([], 19))
|
||||
private.write_text(d.manifest([], 19))
|
||||
elif problem == "missing_private":
|
||||
private.unlink()
|
||||
elif problem == "bad_pointer":
|
||||
data = tomllib.loads(draft.read_text())
|
||||
data["repository"][1]["sha256"] = "0" * 64
|
||||
draft.write_text(d.manifest(data["repository"], 19))
|
||||
else:
|
||||
draft.write_text(d.manifest(rows, 19))
|
||||
with pytest.raises(b.InstrumentFailure):
|
||||
b.run(root, draft, guard_runner=lambda root: pytest.fail("guard should not run"))
|
||||
assert not (root / "benchmarks/unseen/draft-p1.toml").exists()
|
||||
|
||||
|
||||
def test_interrupt_removes_partial_subset(tmp_path):
|
||||
root, draft, _ = draft_pair(tmp_path)
|
||||
calls = 0
|
||||
|
||||
def guard(root):
|
||||
nonlocal calls
|
||||
calls += 1
|
||||
if calls > 2:
|
||||
raise KeyboardInterrupt
|
||||
return 0 if calls == 1 else 1
|
||||
|
||||
with pytest.raises(KeyboardInterrupt):
|
||||
b.run(root, draft, guard_runner=guard)
|
||||
assert not list((root / "benchmarks/unseen").rglob("*.toml"))
|
||||
@@ -0,0 +1,197 @@
|
||||
"""Freeze fixture pools without reading reservations or running subprocesses."""
|
||||
|
||||
import hashlib
|
||||
import importlib
|
||||
import json
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from test_unseen_bisect import b, d, draft_pair
|
||||
|
||||
f = importlib.import_module("benchmarks.unseen.freeze")
|
||||
|
||||
|
||||
def prepared(tmp_path):
|
||||
root, draft, rows = draft_pair(tmp_path)
|
||||
b.run(root, draft, guard_runner=lambda root: 0)
|
||||
protocol = root / "benchmarks/unseen/protocol-p1.md"
|
||||
protocol.write_bytes(Path("benchmarks/unseen/protocol-p1.md").read_bytes())
|
||||
return root, rows
|
||||
|
||||
|
||||
def used():
|
||||
return {"sha256": "a" * 64, "sources": {"pairs": {"count": 7}, "memory_s3": {"count": 3}}}
|
||||
|
||||
|
||||
def test_freeze_split_receipt_and_existing_guard_digest(tmp_path, capsys, caplog, monkeypatch):
|
||||
root, rows = prepared(tmp_path)
|
||||
record = f.run(root, used_report=used())
|
||||
directory = root / "benchmarks/unseen"
|
||||
pool = directory / "pool-p1.toml"
|
||||
public = tomllib.loads(pool.read_text())
|
||||
private = tomllib.loads((directory / "private/pool-p1.toml").read_text())
|
||||
assert public["status"] == private["status"] == "frozen"
|
||||
assert record["used_set_digest"] == "a" * 64
|
||||
assert record["source_counts"] == {"pairs": 7, "memory_s3": 3}
|
||||
assert record["label_check_counts"] == {"confirmed": 3, "refactor": 3, "unclear": 3, "unchecked": 0}
|
||||
assert record["guard_result"] == 0 and record["dropped_count"] == 0
|
||||
assert record["protocol_digest"] == hashlib.sha256((directory / "protocol-p1.md").read_bytes()).hexdigest()
|
||||
assert record["freeze_digest"] == hashlib.sha256(d.canonical(public)).hexdigest()
|
||||
assert json.loads((directory / "freeze-p1.json").read_text()) == record
|
||||
for row in rows:
|
||||
assert row["repository"] not in json.dumps(record) and row["url"] not in json.dumps(record)
|
||||
if row["license_class"] == "private":
|
||||
assert row["url"] not in pool.read_text() and row["repository"] not in pool.read_text()
|
||||
pointer = next(p for p in public["repository"] if p["id"] == row["id"])
|
||||
assert pointer == {"id": row["id"], "slice": row["slice"], "private": True, "sha256": d.private_digest(row)}
|
||||
assert row in private["repository"]
|
||||
else:
|
||||
assert row in public["repository"]
|
||||
assert (directory / "private/pool-p1.toml").stat().st_mode & 0o777 == 0o600
|
||||
guard = importlib.import_module("test_independent_population")
|
||||
monkeypatch.setattr(guard, "ROOT", root)
|
||||
guard.test_frozen_pool_manifest_digest()
|
||||
assert capsys.readouterr() == ("", "") and not caplog.records
|
||||
|
||||
|
||||
def test_digest_and_retry_ignore_toml_key_order(tmp_path):
|
||||
root, _ = prepared(tmp_path)
|
||||
original = f.run(root, used_report=used())
|
||||
pool = root / "benchmarks/unseen/pool-p1.toml"
|
||||
text = pool.read_text()
|
||||
lines = text.splitlines()
|
||||
pool.write_text("\n".join([lines[2], lines[1], lines[0], *lines[3:]]) + "\n")
|
||||
before = pool.read_bytes()
|
||||
assert f.run(root, used_report=used()) == original
|
||||
assert pool.read_bytes() == before
|
||||
|
||||
|
||||
@pytest.mark.parametrize("target", ["pool-p1.toml", "private/pool-p1.toml", "freeze-p1.json"])
|
||||
def test_numeric_type_change_is_a_different_frozen_artifact(tmp_path, target):
|
||||
root, _ = prepared(tmp_path)
|
||||
f.run(root, used_report=used())
|
||||
path = root / "benchmarks/unseen" / target
|
||||
text = path.read_text()
|
||||
if path.suffix == ".toml":
|
||||
modified = text.replace("seed = 19", "seed = 19.0")
|
||||
else:
|
||||
modified = text.replace('"guard_result": 0', '"guard_result": false')
|
||||
assert text != modified
|
||||
path.write_text(modified)
|
||||
with pytest.raises(FileExistsError, match="new_pool"):
|
||||
f.run(root, used_report=used())
|
||||
assert path.read_text() == modified
|
||||
|
||||
|
||||
@pytest.mark.parametrize("target", ["pool-p1.toml", "private/pool-p1.toml", "freeze-p1.json", "protocol-p1.md"])
|
||||
def test_refuses_different_frozen_files(tmp_path, target):
|
||||
root, _ = prepared(tmp_path)
|
||||
f.run(root, used_report=used())
|
||||
directory = root / "benchmarks/unseen"
|
||||
path = directory / target
|
||||
if path.suffix == ".toml":
|
||||
path.write_text(path.read_text().replace('status = "frozen"', 'status = "modified"'))
|
||||
elif path.suffix == ".json":
|
||||
record = json.loads(path.read_text())
|
||||
record["dropped_count"] += 1
|
||||
path.write_text(json.dumps(record))
|
||||
else:
|
||||
path.write_text("changed protocol")
|
||||
before = {p: p.read_bytes() for p in directory.rglob("*") if p.is_file()}
|
||||
with pytest.raises(FileExistsError, match="new_pool"):
|
||||
f.run(root, used_report=used())
|
||||
assert all(p.read_bytes() == value for p, value in before.items())
|
||||
|
||||
|
||||
@pytest.mark.parametrize("target", ["draft-p1.toml", "private/draft-p1.toml", "bisect-p1.json"])
|
||||
def test_refuses_stale_or_red_guard_receipt(tmp_path, target):
|
||||
root, _ = prepared(tmp_path)
|
||||
path = root / "benchmarks/unseen" / target
|
||||
if target.endswith(".json"):
|
||||
receipt = json.loads(path.read_text())
|
||||
receipt["guard_result"] = 1
|
||||
path.write_text(json.dumps(receipt))
|
||||
else:
|
||||
before = path.read_text()
|
||||
after = before.replace('"head" = "', '"head" = "0', 1)
|
||||
assert before != after
|
||||
path.write_text(after)
|
||||
with pytest.raises((ValueError, b.InstrumentFailure)):
|
||||
f.run(root, used_report=used())
|
||||
assert not (root / "benchmarks/unseen/pool-p1.toml").exists()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mutation", ["digest", "source_name", "count"])
|
||||
def test_rejects_identity_bearing_metadata(tmp_path, mutation):
|
||||
root, rows = prepared(tmp_path)
|
||||
report = used()
|
||||
if mutation == "digest":
|
||||
report["sha256"] = rows[0]["url"]
|
||||
elif mutation == "source_name":
|
||||
report["sources"][rows[0]["repository"]] = {"count": 1}
|
||||
else:
|
||||
report["sources"]["pairs"]["count"] = rows[0]["url"]
|
||||
with pytest.raises(ValueError):
|
||||
f.run(root, used_report=report)
|
||||
assert not (root / "benchmarks/unseen/freeze-p1.json").exists()
|
||||
|
||||
|
||||
def test_missing_protocol_aborts_before_publication(tmp_path):
|
||||
root, _ = prepared(tmp_path)
|
||||
(root / "benchmarks/unseen/protocol-p1.md").unlink()
|
||||
with pytest.raises(ValueError, match="protocol"):
|
||||
f.run(root, used_report=used())
|
||||
assert not (root / "benchmarks/unseen/pool-p1.toml").exists()
|
||||
|
||||
|
||||
def test_drops_flow_into_freeze_counts_and_private_manifest(tmp_path):
|
||||
root, draft, rows = draft_pair(tmp_path)
|
||||
hidden = {rows[i]["repository"] for i in (1, 2)}
|
||||
from test_unseen_bisect import staged
|
||||
|
||||
b.run(root, draft, guard_runner=lambda root: int(any(r["repository"] in hidden for r in staged(root))))
|
||||
directory = root / "benchmarks/unseen"
|
||||
(directory / "protocol-p1.md").write_bytes(Path("benchmarks/unseen/protocol-p1.md").read_bytes())
|
||||
record = f.run(root, used_report=used())
|
||||
assert record["dropped_count"] == 2
|
||||
assert record["label_check_counts"] == {"confirmed": 3, "refactor": 2, "unclear": 2, "unchecked": 0}
|
||||
assert len(tomllib.loads((directory / "pool-p1.toml").read_text())["repository"]) == 7
|
||||
assert len(tomllib.loads((directory / "private/pool-p1.toml").read_text())["repository"]) == 3
|
||||
|
||||
|
||||
def test_changed_private_labels_invalidate_guard_receipt(tmp_path):
|
||||
root, _ = prepared(tmp_path)
|
||||
path = root / "benchmarks/unseen/private/draft-p1.toml"
|
||||
before = path.read_text()
|
||||
after = before.replace('"label_check" = "refactor"', '"label_check" = "confirmed"', 1)
|
||||
assert before != after
|
||||
path.write_text(after)
|
||||
with pytest.raises(ValueError, match="guard_receipt"):
|
||||
f.run(root, used_report=used())
|
||||
|
||||
|
||||
def test_utf8_digest_matches_guard_with_non_ascii_metadata(tmp_path, monkeypatch):
|
||||
root, draft, _ = draft_pair(tmp_path)
|
||||
data = tomllib.loads(draft.read_text())
|
||||
data["repository"][0]["license_path"] = "licence-ä.txt"
|
||||
draft.write_text(d.manifest(data["repository"], 19))
|
||||
b.run(root, draft, guard_runner=lambda root: 0)
|
||||
(root / "benchmarks/unseen/protocol-p1.md").write_bytes(Path("benchmarks/unseen/protocol-p1.md").read_bytes())
|
||||
f.run(root, used_report=used())
|
||||
guard = importlib.import_module("test_independent_population")
|
||||
monkeypatch.setattr(guard, "ROOT", root)
|
||||
guard.test_frozen_pool_manifest_digest()
|
||||
|
||||
|
||||
def test_freeze_cli_redacts_failure_and_reports_only_record(tmp_path, capsys):
|
||||
root, rows = prepared(tmp_path)
|
||||
report = tmp_path / "used.json"
|
||||
report.write_text(json.dumps(used()))
|
||||
assert f.main(["--root", str(root), "--used-report", str(report)]) == 0
|
||||
output = capsys.readouterr()
|
||||
assert json.loads(output.out)["guard_result"] == 0 and output.err == ""
|
||||
assert all(r["repository"] not in output.out for r in rows)
|
||||
report.write_text(rows[0]["url"])
|
||||
assert f.main(["--root", str(root), "--used-report", str(report)]) == 1
|
||||
assert capsys.readouterr() == ('{"status": "freeze_refused"}\n', "")
|
||||
Reference in New Issue
Block a user