claude-dev-env 1.89.0 → 1.90.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.
- package/_shared/pr-loop/scripts/code_rules_gate.py +106 -14
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +26 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +160 -0
- package/hooks/blocking/CLAUDE.md +1 -0
- package/hooks/blocking/test_volatile_path_in_post_blocker.py +210 -0
- package/hooks/blocking/volatile_path_in_post_blocker.py +351 -0
- package/hooks/hooks.json +25 -0
- package/hooks/hooks_constants/CLAUDE.md +2 -0
- package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -0
- package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +48 -0
- package/hooks/lifecycle/CLAUDE.md +3 -1
- package/hooks/lifecycle/enter_worktree_origin_prefetch.py +146 -0
- package/hooks/lifecycle/test_enter_worktree_origin_prefetch.py +178 -0
- package/package.json +1 -1
- package/rules/CLAUDE.md +1 -0
- package/rules/durable-post-artifacts.md +35 -0
- package/scripts/CLAUDE.md +1 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -0
- package/scripts/dev_env_scripts_constants/gh_artifact_upload_constants.py +43 -0
- package/scripts/gh_artifact_upload.py +256 -0
- package/scripts/tests/test_gh_artifact_upload.py +205 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Behavioral tests for gh_artifact_upload.py using a stubbed GitHub CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import types
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
_SCRIPTS_DIR = Path(__file__).resolve().parent.parent
|
|
14
|
+
if str(_SCRIPTS_DIR) not in sys.path:
|
|
15
|
+
sys.path.insert(0, str(_SCRIPTS_DIR))
|
|
16
|
+
|
|
17
|
+
import gh_artifact_upload as mod
|
|
18
|
+
|
|
19
|
+
_DEFAULT_READBACK_ASSET = {
|
|
20
|
+
"name": "20260707_140233_contact_sheet.png",
|
|
21
|
+
"url": (
|
|
22
|
+
"https://github.com/owner/repo/releases/download/artifacts/"
|
|
23
|
+
"20260707_140233_contact_sheet.png"
|
|
24
|
+
),
|
|
25
|
+
"createdAt": "2026-07-07T14:02:33Z",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _make_gh_stub(
|
|
30
|
+
recorded_calls: list[list[str]],
|
|
31
|
+
view_return_code: int,
|
|
32
|
+
all_readback_assets: list[dict[str, str]] | None = None,
|
|
33
|
+
) -> object:
|
|
34
|
+
readback_assets = (
|
|
35
|
+
[_DEFAULT_READBACK_ASSET] if all_readback_assets is None else all_readback_assets
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def fake_run(
|
|
39
|
+
all_command_arguments: list[str],
|
|
40
|
+
**_keyword_arguments: object,
|
|
41
|
+
) -> types.SimpleNamespace:
|
|
42
|
+
recorded_calls.append(all_command_arguments)
|
|
43
|
+
is_release_view = "view" in all_command_arguments
|
|
44
|
+
is_asset_read_back = is_release_view and "assets" in all_command_arguments
|
|
45
|
+
if is_asset_read_back:
|
|
46
|
+
return types.SimpleNamespace(
|
|
47
|
+
returncode=0,
|
|
48
|
+
stdout=json.dumps({"assets": readback_assets}),
|
|
49
|
+
stderr="",
|
|
50
|
+
)
|
|
51
|
+
return_code = view_return_code if is_release_view else 0
|
|
52
|
+
return types.SimpleNamespace(returncode=return_code, stdout="", stderr="")
|
|
53
|
+
|
|
54
|
+
return fake_run
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_timestamped_asset_name_prefixes_basename() -> None:
|
|
58
|
+
asset_name = mod.timestamped_asset_name(r"C:\stage\contact_sheet.png")
|
|
59
|
+
assert asset_name.endswith("_contact_sheet.png")
|
|
60
|
+
assert len(asset_name) > len("_contact_sheet.png")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_upload_artifact_returns_the_readback_url(
|
|
64
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
65
|
+
) -> None:
|
|
66
|
+
source_file = tmp_path / "contact_sheet.png"
|
|
67
|
+
source_file.write_bytes(b"binary")
|
|
68
|
+
recorded_calls: list[list[str]] = []
|
|
69
|
+
monkeypatch.setattr(
|
|
70
|
+
subprocess, "run", _make_gh_stub(recorded_calls, view_return_code=0)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
asset_url = mod.upload_artifact(str(source_file), "owner/repo")
|
|
74
|
+
|
|
75
|
+
assert asset_url == _DEFAULT_READBACK_ASSET["url"]
|
|
76
|
+
assert any("assets" in call for call in recorded_calls)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_upload_artifact_prints_the_sanitized_url_github_serves(
|
|
80
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
81
|
+
) -> None:
|
|
82
|
+
source_file = tmp_path / "My Report.png"
|
|
83
|
+
source_file.write_bytes(b"binary")
|
|
84
|
+
sanitized_asset = {
|
|
85
|
+
"name": "20260707_140233_My.Report.png",
|
|
86
|
+
"url": (
|
|
87
|
+
"https://github.com/owner/repo/releases/download/artifacts/"
|
|
88
|
+
"20260707_140233_My.Report.png"
|
|
89
|
+
),
|
|
90
|
+
"createdAt": "2026-07-07T14:02:33Z",
|
|
91
|
+
}
|
|
92
|
+
monkeypatch.setattr(
|
|
93
|
+
subprocess,
|
|
94
|
+
"run",
|
|
95
|
+
_make_gh_stub([], view_return_code=0, all_readback_assets=[sanitized_asset]),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
asset_url = mod.upload_artifact(str(source_file), "owner/repo")
|
|
99
|
+
|
|
100
|
+
assert asset_url == sanitized_asset["url"]
|
|
101
|
+
assert " " not in asset_url
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_upload_artifact_returns_the_newest_asset_url(
|
|
105
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
106
|
+
) -> None:
|
|
107
|
+
source_file = tmp_path / "out.png"
|
|
108
|
+
source_file.write_bytes(b"binary")
|
|
109
|
+
older_asset = {
|
|
110
|
+
"name": "20260707_140000_out.png",
|
|
111
|
+
"url": "https://github.com/owner/repo/releases/download/artifacts/old.png",
|
|
112
|
+
"createdAt": "2026-07-07T14:00:00Z",
|
|
113
|
+
}
|
|
114
|
+
newest_asset = {
|
|
115
|
+
"name": "20260707_140233_out.png",
|
|
116
|
+
"url": "https://github.com/owner/repo/releases/download/artifacts/new.png",
|
|
117
|
+
"createdAt": "2026-07-07T14:02:33Z",
|
|
118
|
+
}
|
|
119
|
+
monkeypatch.setattr(
|
|
120
|
+
subprocess,
|
|
121
|
+
"run",
|
|
122
|
+
_make_gh_stub(
|
|
123
|
+
[], view_return_code=0, all_readback_assets=[older_asset, newest_asset]
|
|
124
|
+
),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
asset_url = mod.upload_artifact(str(source_file), "owner/repo")
|
|
128
|
+
|
|
129
|
+
assert asset_url == newest_asset["url"]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_upload_artifact_raises_when_asset_missing_on_readback(
|
|
133
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
134
|
+
) -> None:
|
|
135
|
+
source_file = tmp_path / "out.png"
|
|
136
|
+
source_file.write_bytes(b"binary")
|
|
137
|
+
monkeypatch.setattr(
|
|
138
|
+
subprocess, "run", _make_gh_stub([], view_return_code=0, all_readback_assets=[])
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
with pytest.raises(mod.ArtifactUploadError):
|
|
142
|
+
mod.upload_artifact(str(source_file), "owner/repo")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_upload_artifact_uploads_to_tag_without_clobber(
|
|
146
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
147
|
+
) -> None:
|
|
148
|
+
source_file = tmp_path / "out.png"
|
|
149
|
+
source_file.write_bytes(b"binary")
|
|
150
|
+
recorded_calls: list[list[str]] = []
|
|
151
|
+
monkeypatch.setattr(
|
|
152
|
+
subprocess, "run", _make_gh_stub(recorded_calls, view_return_code=0)
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
mod.upload_artifact(str(source_file), "owner/repo")
|
|
156
|
+
|
|
157
|
+
upload_call = next(call for call in recorded_calls if "upload" in call)
|
|
158
|
+
assert "artifacts" in upload_call
|
|
159
|
+
assert "--clobber" not in upload_call
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def test_upload_artifact_creates_release_when_absent(
|
|
163
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
164
|
+
) -> None:
|
|
165
|
+
source_file = tmp_path / "out.png"
|
|
166
|
+
source_file.write_bytes(b"binary")
|
|
167
|
+
recorded_calls: list[list[str]] = []
|
|
168
|
+
monkeypatch.setattr(
|
|
169
|
+
subprocess, "run", _make_gh_stub(recorded_calls, view_return_code=1)
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
mod.upload_artifact(str(source_file), "owner/repo")
|
|
173
|
+
|
|
174
|
+
assert any("create" in call for call in recorded_calls)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_artifacts_release_exists_true_when_view_succeeds(
|
|
178
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
179
|
+
) -> None:
|
|
180
|
+
monkeypatch.setattr(subprocess, "run", _make_gh_stub([], view_return_code=0))
|
|
181
|
+
assert mod.artifacts_release_exists("owner/repo") is True
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_artifacts_release_exists_false_when_view_fails(
|
|
185
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
186
|
+
) -> None:
|
|
187
|
+
monkeypatch.setattr(subprocess, "run", _make_gh_stub([], view_return_code=1))
|
|
188
|
+
assert mod.artifacts_release_exists("owner/repo") is False
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def test_ensure_artifacts_release_skips_create_when_present(
|
|
192
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
193
|
+
) -> None:
|
|
194
|
+
recorded_calls: list[list[str]] = []
|
|
195
|
+
monkeypatch.setattr(
|
|
196
|
+
subprocess, "run", _make_gh_stub(recorded_calls, view_return_code=0)
|
|
197
|
+
)
|
|
198
|
+
mod.ensure_artifacts_release("owner/repo")
|
|
199
|
+
assert not any("create" in call for call in recorded_calls)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_upload_artifact_missing_file_raises(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
203
|
+
monkeypatch.setattr(subprocess, "run", _make_gh_stub([], view_return_code=0))
|
|
204
|
+
with pytest.raises(mod.ArtifactUploadError):
|
|
205
|
+
mod.upload_artifact("does_not_exist_9f3a.png", "owner/repo")
|