support.py
python
sha256:4671b7f787ddbe63ced31c895b688c77ab495653b65a730b423329f26b3c1439
feat: K1-P1 complete — agent provenance, build-verification…
Sonnet 4.6
patch
55 days ago
| 1 | """Test helpers (not pytest fixtures).""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | import os |
| 6 | from pathlib import Path |
| 7 | |
| 8 | from adapters.config import OverseerConfig, load_config |
| 9 | from adapters.factory import create_adapter |
| 10 | from adapters.runner import CommandResult, RecordingRunner |
| 11 | |
| 12 | FIXTURES = Path(__file__).resolve().parent / "fixtures" |
| 13 | PILOT = FIXTURES / "pilot" |
| 14 | CHECKPOINTS = FIXTURES / "checkpoints" |
| 15 | HONESTY = FIXTURES / "honesty" |
| 16 | |
| 17 | |
| 18 | def write_config(repo_root: Path, name: str) -> Path: |
| 19 | src = FIXTURES / name |
| 20 | dest = repo_root / ".overseer" / "config.yaml" |
| 21 | dest.parent.mkdir(parents=True, exist_ok=True) |
| 22 | dest.write_text(src.read_text(encoding="utf-8"), encoding="utf-8") |
| 23 | return dest |
| 24 | |
| 25 | |
| 26 | def load_fixture_config(repo_root: Path, name: str) -> OverseerConfig: |
| 27 | return load_config(write_config(repo_root, name)) |
| 28 | |
| 29 | |
| 30 | def ok(stdout: str = "") -> CommandResult: |
| 31 | return CommandResult(stdout=stdout, stderr="", exit_code=0) |
| 32 | |
| 33 | |
| 34 | def fail(stderr: str = "error", code: int = 1) -> CommandResult: |
| 35 | return CommandResult(stdout="", stderr=stderr, exit_code=code) |
| 36 | |
| 37 | |
| 38 | def make_runner(responses: dict[str, CommandResult]) -> RecordingRunner: |
| 39 | return RecordingRunner(responses=responses, calls=[]) |
| 40 | |
| 41 | |
| 42 | def adapter_for(config: OverseerConfig, repo_root: Path, runner: RecordingRunner): |
| 43 | return create_adapter(config, repo_root, runner=runner) |
| 44 | |
| 45 | |
| 46 | def git_status_runner(branch: str = "main", dirty: bool = False) -> RecordingRunner: |
| 47 | """Recording runner with git-only ``status()`` responses.""" |
| 48 | dirty_out = " M file" if dirty else "" |
| 49 | return make_runner( |
| 50 | { |
| 51 | "git rev-parse --abbrev-ref HEAD": ok(branch), |
| 52 | "git status --porcelain": ok(dirty_out), |
| 53 | } |
| 54 | ) |
| 55 | |
| 56 | |
| 57 | def muse_status_runner( |
| 58 | repo_root: Path, |
| 59 | branch: str = "main", |
| 60 | dirty: bool = False, |
| 61 | ) -> RecordingRunner: |
| 62 | """Recording runner with muse-only ``status()`` responses.""" |
| 63 | root = str(repo_root.resolve()) |
| 64 | dirty_out = " M file" if dirty else "" |
| 65 | return make_runner( |
| 66 | { |
| 67 | f"muse -C {root} branch --show-current": ok(branch), |
| 68 | f"muse -C {root} status --porcelain": ok(dirty_out), |
| 69 | } |
| 70 | ) |
| 71 | |
| 72 | |
| 73 | def muse_mirror_status_runner( |
| 74 | repo_root: Path, |
| 75 | branch: str = "main", |
| 76 | dirty: bool = False, |
| 77 | ) -> RecordingRunner: |
| 78 | """Recording runner with muse+git-mirror ``status()`` responses.""" |
| 79 | root = str(repo_root.resolve()) |
| 80 | dirty_out = " M file" if dirty else "" |
| 81 | return make_runner( |
| 82 | { |
| 83 | f"muse -C {root} rev-parse --abbrev-ref HEAD": ok(branch), |
| 84 | f"muse -C {root} status --porcelain": ok(dirty_out), |
| 85 | "git rev-parse --abbrev-ref HEAD": ok(branch), |
| 86 | "git status --porcelain": ok(dirty_out), |
| 87 | } |
| 88 | ) |
| 89 | |
| 90 | |
| 91 | def run_cli( |
| 92 | argv: list[str], |
| 93 | *, |
| 94 | cwd: Path, |
| 95 | runner: RecordingRunner | None = None, |
| 96 | kit: Path | None = None, |
| 97 | review_provider_factory=None, |
| 98 | script_executor=None, |
| 99 | json_mode: bool = False, |
| 100 | ) -> int: |
| 101 | """Invoke ``cli.main`` with an injected runner and working directory.""" |
| 102 | from cli.context import CliContext |
| 103 | from cli.main import main |
| 104 | from cli.output import OutputContext |
| 105 | |
| 106 | old_cwd = Path.cwd() |
| 107 | os.chdir(cwd) |
| 108 | try: |
| 109 | ctx = CliContext.create( |
| 110 | runner=runner or make_runner({}), |
| 111 | cwd=cwd, |
| 112 | kit=kit, |
| 113 | output=OutputContext(json_mode=json_mode), |
| 114 | review_provider_factory=review_provider_factory, |
| 115 | script_executor=script_executor, |
| 116 | ) |
| 117 | return main(argv, ctx=ctx) |
| 118 | finally: |
| 119 | os.chdir(old_cwd) |
| 120 | |
| 121 | |
| 122 | def seed_muse_substrate(repo_root: Path) -> None: |
| 123 | """Create minimal healthy ``.muse/`` for muse-backed regime tests.""" |
| 124 | muse = repo_root / ".muse" |
| 125 | muse.mkdir(parents=True, exist_ok=True) |
| 126 | (muse / "HEAD").write_text("ref: refs/heads/main\n", encoding="utf-8") |
| 127 | (muse / "repo.json").write_text("{}", encoding="utf-8") |
| 128 | (muse / "config.toml").write_text("", encoding="utf-8") |
| 129 | |
| 130 | |
| 131 | def seed_freeze_repo(repo_root: Path, *, config_name: str = "config-git-only.yaml") -> Path: |
| 132 | """Write config and copy a freeze artifact fixture into a temp repo.""" |
| 133 | write_config(repo_root, config_name) |
| 134 | if "muse" in config_name: |
| 135 | seed_muse_substrate(repo_root) |
| 136 | docs = repo_root / "docs" |
| 137 | docs.mkdir(parents=True, exist_ok=True) |
| 138 | artifact = docs / "FREEZE.md" |
| 139 | artifact.write_text((FIXTURES / "freeze-artifact.md").read_text(encoding="utf-8"), encoding="utf-8") |
| 140 | return artifact |
| 141 | |
| 142 | |
| 143 | def pass_provider_factory(): |
| 144 | """Factory returning a provider that always passes.""" |
| 145 | from tools.freeze_reviewer.providers.base import LocalReviewProvider |
| 146 | |
| 147 | def _factory(_provider_name: str) -> LocalReviewProvider: |
| 148 | return LocalReviewProvider(scripted_findings=[]) |
| 149 | |
| 150 | return _factory |
| 151 | |
| 152 | |
| 153 | def findings_provider_factory(findings): |
| 154 | """Factory returning a provider with scripted findings.""" |
| 155 | from tools.freeze_reviewer.providers.base import LocalReviewProvider |
| 156 | |
| 157 | def _factory(_provider_name: str) -> LocalReviewProvider: |
| 158 | return LocalReviewProvider(scripted_findings=list(findings)) |
| 159 | |
| 160 | return _factory |
| 161 | |
| 162 | |
| 163 | def unreachable_provider_factory(cause: str = "offline"): |
| 164 | """Factory returning an unreachable local provider.""" |
| 165 | from tools.freeze_reviewer.providers.base import LocalReviewProvider |
| 166 | |
| 167 | def _factory(_provider_name: str) -> LocalReviewProvider: |
| 168 | return LocalReviewProvider(force_unreachable=True, unreachable_cause=cause) |
| 169 | |
| 170 | return _factory |
| 171 | |
| 172 | |
| 173 | class FakeHttpTransport: |
| 174 | """Recording HTTP transport for API provider tests (no network).""" |
| 175 | |
| 176 | def __init__( |
| 177 | self, |
| 178 | *, |
| 179 | health_status: int = 200, |
| 180 | health_body: bytes = b'{"status":"ok"}', |
| 181 | review_status: int = 200, |
| 182 | review_body: bytes | None = None, |
| 183 | fail_health: bool = False, |
| 184 | fail_review: bool = False, |
| 185 | ) -> None: |
| 186 | self.health_status = health_status |
| 187 | self.health_body = health_body |
| 188 | self.review_status = review_status |
| 189 | self.review_body = review_body or b'{"findings":[]}' |
| 190 | self.fail_health = fail_health |
| 191 | self.fail_review = fail_review |
| 192 | self.calls: list[dict] = [] |
| 193 | |
| 194 | def request( |
| 195 | self, |
| 196 | *, |
| 197 | method: str, |
| 198 | url: str, |
| 199 | headers: dict[str, str], |
| 200 | body: bytes | None = None, |
| 201 | timeout: float = 30.0, |
| 202 | ) -> tuple[int, bytes]: |
| 203 | from tools.freeze_reviewer.providers.api_client import ProviderTransportError |
| 204 | |
| 205 | self.calls.append( |
| 206 | { |
| 207 | "method": method, |
| 208 | "url": url, |
| 209 | "headers": dict(headers), |
| 210 | "body": body, |
| 211 | } |
| 212 | ) |
| 213 | if "/health" in url: |
| 214 | if self.fail_health: |
| 215 | raise ProviderTransportError("health transport failure") |
| 216 | return self.health_status, self.health_body |
| 217 | if self.fail_review: |
| 218 | raise ProviderTransportError("review transport failure") |
| 219 | return self.review_status, self.review_body |
| 220 | |
| 221 | |
| 222 | def api_provider_factory(transport: FakeHttpTransport): |
| 223 | """Factory returning an API provider wired to a fake HTTP transport.""" |
| 224 | from tools.freeze_reviewer.providers.api_client import ReviewApiClient |
| 225 | from tools.freeze_reviewer.providers.base import ApiReviewProvider |
| 226 | |
| 227 | def _factory(_provider_name: str) -> ApiReviewProvider: |
| 228 | client = ReviewApiClient(transport=transport) |
| 229 | return ApiReviewProvider(client=client) |
| 230 | |
| 231 | return _factory |
| 232 | |
| 233 | |
| 234 | def api_unreachable_provider_factory(cause: str = "API provider unavailable"): |
| 235 | """Factory returning a forced-unreachable API provider.""" |
| 236 | from tools.freeze_reviewer.providers.base import ApiReviewProvider |
| 237 | |
| 238 | def _factory(_provider_name: str) -> ApiReviewProvider: |
| 239 | return ApiReviewProvider(force_unreachable=True, unreachable_cause=cause) |
| 240 | |
| 241 | return _factory |
| 242 | |
| 243 | |
| 244 | def seed_checkpoint_repo(repo_root: Path) -> None: |
| 245 | """Copy checkpoint fixture pack into a temp repo and mark scripts executable.""" |
| 246 | import shutil |
| 247 | import stat |
| 248 | |
| 249 | (repo_root / ".overseer").mkdir(parents=True, exist_ok=True) |
| 250 | config_src = CHECKPOINTS / "config-checkpoints-enabled.yaml" |
| 251 | (repo_root / ".overseer" / "config.yaml").write_text( |
| 252 | config_src.read_text(encoding="utf-8"), |
| 253 | encoding="utf-8", |
| 254 | ) |
| 255 | for rel in ("policy", "manifests", "scripts"): |
| 256 | src = CHECKPOINTS / rel |
| 257 | dest = repo_root / rel |
| 258 | if dest.exists(): |
| 259 | shutil.rmtree(dest) |
| 260 | shutil.copytree(src, dest) |
| 261 | for script in (repo_root / "scripts" / "verify").glob("*.py"): |
| 262 | script.chmod(script.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) |
| 263 | |
| 264 | |
| 265 | def load_checkpoint_config(repo_root: Path) -> OverseerConfig: |
| 266 | """Load checkpoint-enabled config from a seeded repo.""" |
| 267 | seed_checkpoint_repo(repo_root) |
| 268 | return load_config(repo_root / ".overseer" / "config.yaml") |
| 269 | |
| 270 | |
| 271 | def seed_honesty_repo(repo_root: Path) -> None: |
| 272 | """Copy honesty fixture pack into a temp repo.""" |
| 273 | import shutil |
| 274 | |
| 275 | (repo_root / ".overseer").mkdir(parents=True, exist_ok=True) |
| 276 | config_src = HONESTY / "config-honesty-enabled.yaml" |
| 277 | (repo_root / ".overseer" / "config.yaml").write_text( |
| 278 | config_src.read_text(encoding="utf-8"), |
| 279 | encoding="utf-8", |
| 280 | ) |
| 281 | for rel in ("artifacts", "entries"): |
| 282 | src = HONESTY / rel |
| 283 | dest = repo_root / rel |
| 284 | if dest.exists(): |
| 285 | shutil.rmtree(dest) |
| 286 | shutil.copytree(src, dest) |
| 287 | |
| 288 | |
| 289 | def load_honesty_config(repo_root: Path) -> OverseerConfig: |
| 290 | """Load honesty-enabled config from a seeded repo.""" |
| 291 | seed_honesty_repo(repo_root) |
| 292 | return load_config(repo_root / ".overseer" / "config.yaml") |
| 293 | |
| 294 | |
| 295 | def honesty_artifact_hash(repo_root: Path) -> str: |
| 296 | """SHA-256 of the fixture sample artifact.""" |
| 297 | from tools.honesty.artifact import sha256_file_bytes |
| 298 | |
| 299 | return sha256_file_bytes(repo_root / "artifacts" / "sample.txt") |
| 300 | |
| 301 | |
| 302 | def load_honesty_entry(repo_root: Path, name: str, *, artifact_hash: str | None = None) -> dict: |
| 303 | """Load a verdict entry fixture with artifact hash substituted.""" |
| 304 | import json |
| 305 | |
| 306 | text = (HONESTY / "entries" / name).read_text(encoding="utf-8") |
| 307 | if artifact_hash is None: |
| 308 | artifact_hash = honesty_artifact_hash(repo_root) |
| 309 | text = text.replace("PLACEHOLDER", artifact_hash) |
| 310 | return json.loads(text) |
| 311 | |
| 312 | |
| 313 | def seed_pilot_tree( |
| 314 | repo_root: Path, |
| 315 | *, |
| 316 | handover_rel: str, |
| 317 | handover_text: str = "# Hand preserved handover\n", |
| 318 | roadmap_rel: str | None = None, |
| 319 | roadmap_text: str | None = None, |
| 320 | extra_cursor_rules: dict[str, str] | None = None, |
| 321 | ) -> None: |
| 322 | """Create a pre-existing living-doc layout for migrate fixtures.""" |
| 323 | hand = repo_root / handover_rel |
| 324 | hand.parent.mkdir(parents=True, exist_ok=True) |
| 325 | hand.write_text(handover_text, encoding="utf-8") |
| 326 | if roadmap_rel is not None: |
| 327 | road = repo_root / roadmap_rel |
| 328 | road.parent.mkdir(parents=True, exist_ok=True) |
| 329 | road.write_text(roadmap_text or "# Hand preserved roadmap\n", encoding="utf-8") |
| 330 | if extra_cursor_rules: |
| 331 | rules = repo_root / ".cursor" / "rules" |
| 332 | rules.mkdir(parents=True, exist_ok=True) |
| 333 | for name, text in extra_cursor_rules.items(): |
| 334 | (rules / name).write_text(text, encoding="utf-8") |
| 335 | |
| 336 | |
| 337 | def lock_origins(repo_root: Path) -> dict[str, str]: |
| 338 | """Return path → origin map from ``version.lock``.""" |
| 339 | from cli.version_lock import entry_origin, read_version_lock |
| 340 | |
| 341 | lock = read_version_lock(repo_root / ".overseer" / "version.lock") |
| 342 | return {e.path: entry_origin(e) for e in lock.footprint} |
| 343 | |
| 344 | |
| 345 | def generate_ed25519_keypair() -> tuple[object, str]: |
| 346 | """Return ``(private_key, ed25519:<base64> pubkey token)`` for tests only.""" |
| 347 | from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey |
| 348 | |
| 349 | from tools.honesty.ed25519_util import encode_ed25519_token |
| 350 | |
| 351 | private_key = Ed25519PrivateKey.generate() |
| 352 | pubkey_token = encode_ed25519_token(private_key.public_key().public_bytes_raw()) |
| 353 | return private_key, pubkey_token |
| 354 | |
| 355 | |
| 356 | def sign_entry_hash(private_key: object, entry_hash_hex: str) -> str: |
| 357 | """Sign lowercase hex ``entry_hash_hex``; return ``ed25519:<base64>`` token (tests only).""" |
| 358 | from tools.honesty.ed25519_util import encode_ed25519_token |
| 359 | |
| 360 | sig_bytes = private_key.sign(entry_hash_hex.encode("utf-8")) # type: ignore[attr-defined] |
| 361 | return encode_ed25519_token(sig_bytes) |
| 362 | |
| 363 | |
| 364 | def attach_signed_provenance( |
| 365 | body: dict, |
| 366 | *, |
| 367 | pubkey_token: str, |
| 368 | agent_id: str = "cursor-agent", |
| 369 | model_id: str = "gpt-5.6", |
| 370 | human_ref: str | None = None, |
| 371 | ) -> dict: |
| 372 | """Return a copy of ``body`` with unsigned provenance identity fields.""" |
| 373 | provenance: dict = {"agent_id": agent_id, "model_id": model_id} |
| 374 | if human_ref is not None: |
| 375 | provenance["human_ref"] = human_ref |
| 376 | signed = dict(body) |
| 377 | signed["provenance"] = provenance |
| 378 | signed["_test_pubkey"] = pubkey_token |
| 379 | return signed |
| 380 | |
| 381 | |
| 382 | def sign_append_body( |
| 383 | body: dict, |
| 384 | *, |
| 385 | kind: str, |
| 386 | prev_hash: str, |
| 387 | private_key: object, |
| 388 | pubkey_token: str | None = None, |
| 389 | ) -> dict: |
| 390 | """Validate, hash, sign, and return an append-ready body with ``provenance.sig``.""" |
| 391 | from tools.honesty.validate import validate_append_body |
| 392 | |
| 393 | pubkey = pubkey_token or body.pop("_test_pubkey", None) |
| 394 | if pubkey is None: |
| 395 | raise ValueError("pubkey_token required") |
| 396 | draft = dict(body) |
| 397 | draft.pop("_test_pubkey", None) |
| 398 | validated = validate_append_body(kind=kind, body=draft) |
| 399 | preview = dict(validated) |
| 400 | preview["prev_hash"] = prev_hash |
| 401 | preview["provenance"] = {**validated["provenance"], "pubkey": pubkey} |
| 402 | entry_hash = compute_entry_hash(preview) |
| 403 | signed = dict(validated) |
| 404 | signed["provenance"] = { |
| 405 | **validated["provenance"], |
| 406 | "pubkey": pubkey, |
| 407 | "sig": sign_entry_hash(private_key, entry_hash), |
| 408 | } |
| 409 | return signed |
| 410 | |
| 411 | |
| 412 | def compute_entry_hash(body: dict) -> str: |
| 413 | """Re-export for tests building signing previews.""" |
| 414 | from tools.honesty.canonical import compute_entry_hash as _compute |
| 415 | |
| 416 | return _compute(body) |
| 417 | |
| 418 | |
| 419 | def finalize_signed_body(body: dict, *, private_key: object, prev_hash: str) -> dict: |
| 420 | """Compute envelope hashes and attach ``provenance.sig`` (tests / direct ledger writes).""" |
| 421 | from tools.honesty.canonical import compute_entry_hash |
| 422 | from tools.honesty.genesis import utc_now_z |
| 423 | |
| 424 | entry = dict(body) |
| 425 | if not entry.get("ts"): |
| 426 | entry["ts"] = utc_now_z() |
| 427 | entry["prev_hash"] = prev_hash |
| 428 | entry_hash = compute_entry_hash(entry) |
| 429 | entry["entry_hash"] = entry_hash |
| 430 | provenance = dict(entry.get("provenance", {})) |
| 431 | provenance["sig"] = sign_entry_hash(private_key, entry_hash) |
| 432 | entry["provenance"] = provenance |
| 433 | return entry |
| 434 |
File History
1 commit
sha256:4671b7f787ddbe63ced31c895b688c77ab495653b65a730b423329f26b3c1439
feat: K1-P1 complete — agent provenance, build-verification…
Sonnet 4.6
patch
55 days ago