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.
@@ -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")