claude-dev-env 1.62.1 → 1.64.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,6 +11,8 @@ import pathlib
11
11
  import subprocess
12
12
  import sys
13
13
 
14
+ import pytest
15
+
14
16
  _HOOK_DIR = pathlib.Path(__file__).parent
15
17
  if str(_HOOK_DIR) not in sys.path:
16
18
  sys.path.insert(0, str(_HOOK_DIR))
@@ -28,6 +30,10 @@ resolve_merge_base = store_module.resolve_merge_base
28
30
  branch_surface_manifest = store_module.branch_surface_manifest
29
31
  manifest_sha256 = store_module.manifest_sha256
30
32
  workflow_verdict_covers_surface = store_module.workflow_verdict_covers_surface
33
+ minted_verdict_covers_surface = store_module.minted_verdict_covers_surface
34
+ write_verdict = store_module.write_verdict
35
+ worktree_path_for_branch = store_module.worktree_path_for_branch
36
+ empty_surface_hash = store_module.empty_surface_hash
31
37
 
32
38
  constants_spec = importlib.util.spec_from_file_location(
33
39
  "verified_commit_constants",
@@ -38,6 +44,8 @@ assert constants_spec.loader is not None
38
44
  constants_module = importlib.util.module_from_spec(constants_spec)
39
45
  constants_spec.loader.exec_module(constants_module)
40
46
  CORRECTIVE_MESSAGE = constants_module.CORRECTIVE_MESSAGE
47
+ EMPTY_SURFACE_GUARD_MESSAGE = constants_module.EMPTY_SURFACE_GUARD_MESSAGE
48
+ BRANCH_WORKTREE_ABSENT_MESSAGE = constants_module.BRANCH_WORKTREE_ABSENT_MESSAGE
41
49
 
42
50
  PRODUCTION_SOURCE = "def add(left: int, right: int) -> int:\n return left + right\n"
43
51
  TEST_SOURCE = "def test_add() -> None:\n assert 1 + 1 == 2\n"
@@ -488,3 +496,227 @@ def test_manifest_hash_cli_prints_live_surface_hash(tmp_path: pathlib.Path) -> N
488
496
  text=True,
489
497
  )
490
498
  assert completed_process.stdout.strip() == expected_hash
499
+
500
+
501
+ def _isolate_home(monkeypatch: pytest.MonkeyPatch, fake_home: pathlib.Path) -> None:
502
+ home_text = str(fake_home)
503
+ monkeypatch.setenv("HOME", home_text)
504
+ monkeypatch.setenv("USERPROFILE", home_text)
505
+ monkeypatch.delenv("HOMEDRIVE", raising=False)
506
+ monkeypatch.delenv("HOMEPATH", raising=False)
507
+
508
+
509
+ def test_minted_verdict_covers_surface_matches_other_worktree_by_hash(
510
+ monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
511
+ ) -> None:
512
+ fake_home = tmp_path / "home"
513
+ fake_home.mkdir()
514
+ _isolate_home(monkeypatch, fake_home)
515
+ write_verdict(
516
+ str(tmp_path / "other" / "worktree"),
517
+ MATCHING_MANIFEST_SHA256,
518
+ True,
519
+ [],
520
+ "agent-x",
521
+ )
522
+ assert minted_verdict_covers_surface(MATCHING_MANIFEST_SHA256) is True
523
+
524
+
525
+ def test_minted_verdict_covers_surface_false_for_other_hash(
526
+ monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
527
+ ) -> None:
528
+ fake_home = tmp_path / "home"
529
+ fake_home.mkdir()
530
+ _isolate_home(monkeypatch, fake_home)
531
+ write_verdict(
532
+ str(tmp_path / "other" / "worktree"),
533
+ OTHER_MANIFEST_SHA256,
534
+ True,
535
+ [],
536
+ "agent-x",
537
+ )
538
+ assert minted_verdict_covers_surface(MATCHING_MANIFEST_SHA256) is False
539
+
540
+
541
+ def test_minted_verdict_covers_surface_false_for_failing_verdict(
542
+ monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
543
+ ) -> None:
544
+ fake_home = tmp_path / "home"
545
+ fake_home.mkdir()
546
+ _isolate_home(monkeypatch, fake_home)
547
+ write_verdict(
548
+ str(tmp_path / "other" / "worktree"),
549
+ MATCHING_MANIFEST_SHA256,
550
+ False,
551
+ [{"severity": "P0", "summary": "boom"}],
552
+ "agent-x",
553
+ )
554
+ assert minted_verdict_covers_surface(MATCHING_MANIFEST_SHA256) is False
555
+
556
+
557
+ def test_minted_verdict_covers_surface_false_when_directory_absent(
558
+ monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
559
+ ) -> None:
560
+ fake_home = tmp_path / "home"
561
+ fake_home.mkdir()
562
+ _isolate_home(monkeypatch, fake_home)
563
+ assert minted_verdict_covers_surface(MATCHING_MANIFEST_SHA256) is False
564
+
565
+
566
+ def _make_repo_with_branch_worktree(
567
+ tmp_path: pathlib.Path, branch_name: str
568
+ ) -> tuple[pathlib.Path, pathlib.Path]:
569
+ """Create a repo with a branch checked out in a separate worktree.
570
+
571
+ Returns:
572
+ A tuple of (main worktree path, branch worktree path).
573
+ """
574
+ empty_hooks_dir = tmp_path / "nohooks"
575
+ empty_hooks_dir.mkdir()
576
+
577
+ main_dir = tmp_path / "main"
578
+ main_dir.mkdir()
579
+ _run_git(main_dir, "init", "--initial-branch=main")
580
+ _run_git(main_dir, "config", "user.email", "tests@example.com")
581
+ _run_git(main_dir, "config", "user.name", "Worktree Tests")
582
+ _run_git(main_dir, "config", "core.hooksPath", str(empty_hooks_dir))
583
+ (main_dir / "app.py").write_text(PRODUCTION_SOURCE, encoding="utf-8")
584
+ _run_git(main_dir, "add", "-A")
585
+ _run_git(main_dir, "commit", "-m", "base")
586
+
587
+ origin_dir = tmp_path / "origin.git"
588
+ subprocess.run(
589
+ ["git", "init", "--bare", "--initial-branch=main", str(origin_dir)],
590
+ check=True,
591
+ capture_output=True,
592
+ text=True,
593
+ )
594
+ _run_git(main_dir, "remote", "add", "origin", str(origin_dir))
595
+ _run_git(main_dir, "push", "-u", "origin", "main")
596
+
597
+ _run_git(main_dir, "branch", branch_name)
598
+
599
+ branch_worktree_dir = tmp_path / "branch-worktree"
600
+ _run_git(main_dir, "worktree", "add", str(branch_worktree_dir), branch_name)
601
+
602
+ return main_dir, branch_worktree_dir
603
+
604
+
605
+ def test_worktree_path_for_branch_returns_path_when_branch_present(
606
+ tmp_path: pathlib.Path,
607
+ ) -> None:
608
+ _main_dir, branch_worktree_dir = _make_repo_with_branch_worktree(
609
+ tmp_path, "feature-x"
610
+ )
611
+ resolved_path = worktree_path_for_branch(str(branch_worktree_dir), "feature-x")
612
+ assert resolved_path is not None
613
+ assert pathlib.Path(resolved_path).resolve() == branch_worktree_dir.resolve()
614
+
615
+
616
+ def test_worktree_path_for_branch_returns_none_when_branch_absent(
617
+ tmp_path: pathlib.Path,
618
+ ) -> None:
619
+ main_dir, _branch_worktree_dir = _make_repo_with_branch_worktree(
620
+ tmp_path, "feature-x"
621
+ )
622
+ resolved_path = worktree_path_for_branch(str(main_dir), "branch-never-checked-out")
623
+ assert resolved_path is None
624
+
625
+
626
+ def test_empty_surface_hash_equals_hash_of_empty_string() -> None:
627
+ assert empty_surface_hash() == manifest_sha256("")
628
+
629
+
630
+ def test_manifest_hash_cli_empty_surface_writes_guard_message_to_stderr(
631
+ tmp_path: pathlib.Path,
632
+ ) -> None:
633
+ work_dir = _make_repo_with_origin(tmp_path)
634
+ completed_process = subprocess.run(
635
+ [
636
+ sys.executable,
637
+ str(_HOOK_DIR / "verification_verdict_store.py"),
638
+ "--manifest-hash",
639
+ str(work_dir),
640
+ ],
641
+ capture_output=True,
642
+ text=True,
643
+ )
644
+ assert completed_process.returncode != 0
645
+ assert completed_process.stdout.strip() == ""
646
+ lowered_stderr = completed_process.stderr.lower()
647
+ assert "wrong work tree" in lowered_stderr or "empty" in lowered_stderr
648
+
649
+
650
+ def test_manifest_hash_cli_empty_surface_prints_nothing_on_stdout(
651
+ tmp_path: pathlib.Path,
652
+ ) -> None:
653
+ work_dir = _make_repo_with_origin(tmp_path)
654
+ completed_process = subprocess.run(
655
+ [
656
+ sys.executable,
657
+ str(_HOOK_DIR / "verification_verdict_store.py"),
658
+ "--manifest-hash",
659
+ str(work_dir),
660
+ ],
661
+ capture_output=True,
662
+ text=True,
663
+ )
664
+ assert completed_process.stdout.strip() == ""
665
+
666
+
667
+ def test_manifest_hash_for_branch_cli_prints_same_hash_as_explicit_dir(
668
+ tmp_path: pathlib.Path,
669
+ ) -> None:
670
+ _main_dir, branch_worktree_dir = _make_repo_with_branch_worktree(
671
+ tmp_path, "feature-branch"
672
+ )
673
+ (branch_worktree_dir / "app.py").write_text(
674
+ "def add(left: int, right: int) -> int:\n return left - right\n",
675
+ encoding="utf-8",
676
+ )
677
+ direct_process = subprocess.run(
678
+ [
679
+ sys.executable,
680
+ str(_HOOK_DIR / "verification_verdict_store.py"),
681
+ "--manifest-hash",
682
+ str(branch_worktree_dir),
683
+ ],
684
+ capture_output=True,
685
+ text=True,
686
+ check=True,
687
+ )
688
+ branch_process = subprocess.run(
689
+ [
690
+ sys.executable,
691
+ str(_HOOK_DIR / "verification_verdict_store.py"),
692
+ "--manifest-hash-for-branch",
693
+ "feature-branch",
694
+ ],
695
+ capture_output=True,
696
+ text=True,
697
+ check=True,
698
+ cwd=str(branch_worktree_dir),
699
+ )
700
+ assert direct_process.stdout.strip() == branch_process.stdout.strip()
701
+ assert direct_process.stdout.strip() != ""
702
+
703
+
704
+ def test_manifest_hash_for_branch_cli_returns_nonzero_when_branch_absent(
705
+ tmp_path: pathlib.Path,
706
+ ) -> None:
707
+ main_dir, _branch_worktree_dir = _make_repo_with_branch_worktree(
708
+ tmp_path, "feature-branch"
709
+ )
710
+ completed_process = subprocess.run(
711
+ [
712
+ sys.executable,
713
+ str(_HOOK_DIR / "verification_verdict_store.py"),
714
+ "--manifest-hash-for-branch",
715
+ "branch-never-checked-out",
716
+ ],
717
+ capture_output=True,
718
+ text=True,
719
+ cwd=str(main_dir),
720
+ )
721
+ assert completed_process.returncode != 0
722
+ assert completed_process.stdout.strip() == ""
@@ -525,3 +525,46 @@ def test_verification_bypass_marker_allows_an_otherwise_gated_commit(
525
525
  assert "VERIFIED_COMMIT_GATE" in capsys.readouterr().out
526
526
  _run_gate_main(monkeypatch, "git commit -m x # verify-skip", work_dir)
527
527
  assert capsys.readouterr().out == ""
528
+
529
+
530
+ def test_minted_verdict_from_other_worktree_allows_commit_by_hash(
531
+ monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
532
+ ) -> None:
533
+ fake_home = tmp_path / "home"
534
+ fake_home.mkdir()
535
+ _isolate_home(monkeypatch, fake_home)
536
+ work_dir = _make_gated_repo(tmp_path)
537
+ live_surface_hash = _live_surface_hash(work_dir)
538
+ store_module.write_verdict(
539
+ str(tmp_path / "sibling" / "worktree"),
540
+ live_surface_hash,
541
+ True,
542
+ [],
543
+ "agent-x",
544
+ )
545
+ transcript_path = tmp_path / "projects" / "demo" / "sess1.jsonl"
546
+ transcript_path.parent.mkdir(parents=True)
547
+ transcript_path.write_text("", encoding="utf-8")
548
+ assert deny_reason_for_directory(str(work_dir), str(transcript_path)) is None
549
+
550
+
551
+ def test_minted_verdict_from_other_worktree_with_wrong_hash_denies(
552
+ monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
553
+ ) -> None:
554
+ fake_home = tmp_path / "home"
555
+ fake_home.mkdir()
556
+ _isolate_home(monkeypatch, fake_home)
557
+ work_dir = _make_gated_repo(tmp_path)
558
+ store_module.write_verdict(
559
+ str(tmp_path / "sibling" / "worktree"),
560
+ "d" * 64,
561
+ True,
562
+ [],
563
+ "agent-x",
564
+ )
565
+ transcript_path = tmp_path / "projects" / "demo" / "sess1.jsonl"
566
+ transcript_path.parent.mkdir(parents=True)
567
+ transcript_path.write_text("", encoding="utf-8")
568
+ deny_reason = deny_reason_for_directory(str(work_dir), str(transcript_path))
569
+ assert deny_reason is not None
570
+ assert "VERIFIED_COMMIT_GATE" in deny_reason
@@ -37,6 +37,16 @@ minter_spec.loader.exec_module(minter_module)
37
37
  mint_for_payload = minter_module.mint_for_payload
38
38
  resolved_subagent_type = minter_module.resolved_subagent_type
39
39
 
40
+ store_spec = importlib.util.spec_from_file_location(
41
+ "verification_verdict_store",
42
+ _HOOK_DIR / "verification_verdict_store.py",
43
+ )
44
+ assert store_spec is not None
45
+ assert store_spec.loader is not None
46
+ store_module = importlib.util.module_from_spec(store_spec)
47
+ store_spec.loader.exec_module(store_module)
48
+ empty_surface_hash = store_module.empty_surface_hash
49
+
40
50
  constants_spec = importlib.util.spec_from_file_location(
41
51
  "verified_commit_constants",
42
52
  _HOOK_DIR / "config" / "verified_commit_constants.py",
@@ -191,3 +201,132 @@ def test_settings_deny_verdict_directory_write() -> None:
191
201
 
192
202
  def test_settings_deny_verdict_directory_edit() -> None:
193
203
  assert "Edit($HOME/.claude/verification/**)" in _deny_rules()
204
+
205
+
206
+ def test_minter_refuses_when_attested_hash_equals_empty_surface_hash(
207
+ tmp_path: pathlib.Path,
208
+ ) -> None:
209
+ repo_root = tmp_path / "repo"
210
+ repo_root.mkdir()
211
+ _init_repo_with_upstream_and_edit(repo_root)
212
+ attested_empty = empty_surface_hash()
213
+ verdict_fence = json.dumps(
214
+ {"all_pass": True, "findings": [], "manifest_sha256": attested_empty}
215
+ )
216
+ agent_transcript = tmp_path / "agent-7.jsonl"
217
+ agent_transcript.write_text(
218
+ json.dumps(
219
+ {
220
+ "type": "assistant",
221
+ "message": {
222
+ "content": [
223
+ {
224
+ "type": "text",
225
+ "text": f"ok\n```verdict\n{verdict_fence}\n```\n",
226
+ }
227
+ ]
228
+ },
229
+ }
230
+ )
231
+ + "\n",
232
+ encoding="utf-8",
233
+ )
234
+ _write_sidecar(agent_transcript, MINTING_AGENT_TYPE)
235
+ payload = {
236
+ "agent_transcript_path": str(agent_transcript),
237
+ "cwd": str(repo_root),
238
+ "agent_id": "empty-surface-1",
239
+ }
240
+ assert mint_for_payload(payload) is None
241
+
242
+
243
+ def test_minter_refuses_when_recomputed_surface_is_empty(
244
+ tmp_path: pathlib.Path,
245
+ ) -> None:
246
+ repo_root = tmp_path / "repo"
247
+ repo_root.mkdir()
248
+ subprocess.run(["git", "-C", str(repo_root), "init", "-q"], check=True)
249
+ subprocess.run(
250
+ ["git", "-C", str(repo_root), "config", "user.email", "verifier@test"],
251
+ check=True,
252
+ )
253
+ subprocess.run(
254
+ ["git", "-C", str(repo_root), "config", "user.name", "verifier"],
255
+ check=True,
256
+ )
257
+ (repo_root / "module.py").write_text("answer = 1\n", encoding="utf-8")
258
+ subprocess.run(["git", "-C", str(repo_root), "add", "-A"], check=True)
259
+ subprocess.run(
260
+ ["git", "-C", str(repo_root), "commit", "-qm", "init"], check=True
261
+ )
262
+ subprocess.run(
263
+ ["git", "-C", str(repo_root), "branch", "-f", "origin/main", "HEAD"],
264
+ check=True,
265
+ )
266
+ agent_transcript = tmp_path / "agent-7.jsonl"
267
+ agent_transcript.write_text(
268
+ json.dumps(
269
+ {
270
+ "type": "assistant",
271
+ "message": {
272
+ "content": [
273
+ {
274
+ "type": "text",
275
+ "text": 'ok\n```verdict\n{"all_pass": true, "findings": []}\n```\n',
276
+ }
277
+ ]
278
+ },
279
+ }
280
+ )
281
+ + "\n",
282
+ encoding="utf-8",
283
+ )
284
+ _write_sidecar(agent_transcript, MINTING_AGENT_TYPE)
285
+ payload = {
286
+ "agent_transcript_path": str(agent_transcript),
287
+ "cwd": str(repo_root),
288
+ "agent_id": "empty-recompute-1",
289
+ }
290
+ assert mint_for_payload(payload) is None
291
+
292
+
293
+ def test_attested_manifest_hash_binds_over_cwd_surface(tmp_path: pathlib.Path) -> None:
294
+ repo_root = tmp_path / "repo"
295
+ repo_root.mkdir()
296
+ _init_repo_with_upstream_and_edit(repo_root)
297
+ attested_hash = "c" * 64
298
+ agent_transcript = tmp_path / "agent-7.jsonl"
299
+ verdict_fence = json.dumps(
300
+ {"all_pass": True, "findings": [], "manifest_sha256": attested_hash}
301
+ )
302
+ agent_transcript.write_text(
303
+ json.dumps(
304
+ {
305
+ "type": "assistant",
306
+ "message": {
307
+ "content": [
308
+ {
309
+ "type": "text",
310
+ "text": f"ok\n```verdict\n{verdict_fence}\n```\n",
311
+ }
312
+ ]
313
+ },
314
+ }
315
+ )
316
+ + "\n",
317
+ encoding="utf-8",
318
+ )
319
+ _write_sidecar(agent_transcript, MINTING_AGENT_TYPE)
320
+ payload = {
321
+ "agent_transcript_path": str(agent_transcript),
322
+ "cwd": str(repo_root),
323
+ "agent_id": "attest-1",
324
+ }
325
+ verdict_path = mint_for_payload(payload)
326
+ try:
327
+ assert verdict_path is not None
328
+ verdict_record = json.loads(verdict_path.read_text(encoding="utf-8"))
329
+ assert verdict_record["manifest_sha256"] == attested_hash
330
+ finally:
331
+ if verdict_path is not None and verdict_path.exists():
332
+ verdict_path.unlink()