claude-dev-env 2.13.0 → 2.14.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.
Files changed (241) hide show
  1. package/AGENTS.md +61 -0
  2. package/CLAUDE.md +1 -57
  3. package/_shared/AGENTS.md +15 -0
  4. package/_shared/CLAUDE.md +1 -15
  5. package/_shared/advisor/AGENTS.md +22 -0
  6. package/_shared/advisor/CLAUDE.md +1 -22
  7. package/_shared/pr-loop/AGENTS.md +27 -0
  8. package/_shared/pr-loop/CLAUDE.md +1 -27
  9. package/_shared/pr-loop/scripts/AGENTS.md +35 -0
  10. package/_shared/pr-loop/scripts/CLAUDE.md +1 -35
  11. package/_shared/pr-loop/scripts/code_rules_gate_parts/AGENTS.md +42 -0
  12. package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +1 -42
  13. package/_shared/pr-loop/scripts/pr_loop_shared_constants/AGENTS.md +26 -0
  14. package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -26
  15. package/_shared/pr-loop/scripts/tests/AGENTS.md +44 -0
  16. package/_shared/pr-loop/scripts/tests/CLAUDE.md +1 -44
  17. package/_shared/process-tree/AGENTS.md +41 -0
  18. package/_shared/process-tree/CLAUDE.md +1 -41
  19. package/agents/AGENTS.md +31 -0
  20. package/agents/CLAUDE.md +1 -31
  21. package/agents/clean-coder.md +6 -6
  22. package/agents/test_agent_frontmatter.py +4 -4
  23. package/audit-rubrics/AGENTS.md +43 -0
  24. package/audit-rubrics/CLAUDE.md +1 -43
  25. package/audit-rubrics/category_rubrics/AGENTS.md +37 -0
  26. package/audit-rubrics/category_rubrics/CLAUDE.md +1 -37
  27. package/audit-rubrics/prompts/AGENTS.md +37 -0
  28. package/audit-rubrics/prompts/CLAUDE.md +1 -37
  29. package/bin/AGENTS.md +112 -0
  30. package/bin/CLAUDE.md +1 -112
  31. package/bin/ever-shipped-skills.mjs +1 -0
  32. package/bin/install-constants.mjs +6 -7
  33. package/bin/install.mjs +24 -5
  34. package/bin/install.test.mjs +34 -0
  35. package/commands/AGENTS.md +23 -0
  36. package/commands/CLAUDE.md +1 -23
  37. package/commands/hook-log-extract.md +3 -3
  38. package/docs/AGENTS.md +32 -0
  39. package/docs/CLAUDE.md +1 -32
  40. package/docs/CODE_RULES.md +1 -1
  41. package/docs/imagegen.md +22 -0
  42. package/docs/references/AGENTS.md +17 -0
  43. package/docs/references/CLAUDE.md +1 -17
  44. package/hooks/AGENTS.md +31 -0
  45. package/hooks/CLAUDE.md +1 -31
  46. package/hooks/_gh_pr_author_swap_utils.py +14 -0
  47. package/hooks/advisory/AGENTS.md +16 -0
  48. package/hooks/advisory/CLAUDE.md +1 -16
  49. package/hooks/blocking/AGENTS.md +123 -0
  50. package/hooks/blocking/CLAUDE.md +1 -123
  51. package/hooks/blocking/claude_md_orphan_file_blocker_parts/AGENTS.md +28 -0
  52. package/hooks/blocking/claude_md_orphan_file_blocker_parts/CLAUDE.md +1 -28
  53. package/hooks/blocking/code_rules_banned_identifiers.py +1 -1
  54. package/hooks/blocking/code_rules_dead_module_constant.py +149 -37
  55. package/hooks/blocking/code_rules_probe_chains.py +6 -2
  56. package/hooks/blocking/config/AGENTS.md +10 -0
  57. package/hooks/blocking/config/CLAUDE.md +1 -10
  58. package/hooks/blocking/gh_pr_author_restore.py +7 -2
  59. package/hooks/blocking/intent_only_ending_blocker.py +11 -16
  60. package/hooks/blocking/inventory_intent_records/AGENTS.md +26 -0
  61. package/hooks/blocking/inventory_intent_records/CLAUDE.md +1 -26
  62. package/hooks/blocking/package_inventory_stale_blocker_parts/AGENTS.md +26 -0
  63. package/hooks/blocking/package_inventory_stale_blocker_parts/CLAUDE.md +1 -26
  64. package/hooks/blocking/pii_prevention_blocker_parts/AGENTS.md +24 -0
  65. package/hooks/blocking/pii_prevention_blocker_parts/CLAUDE.md +1 -24
  66. package/hooks/blocking/tdd_enforcer_parts/AGENTS.md +30 -0
  67. package/hooks/blocking/tdd_enforcer_parts/CLAUDE.md +1 -30
  68. package/hooks/blocking/test_code_rules_enforcer_banned_prefixes.py +1 -1
  69. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +171 -0
  70. package/hooks/blocking/test_code_rules_enforcer_magic_allowlist.py +1 -1
  71. package/hooks/blocking/test_gh_pr_author_restore.py +2 -1
  72. package/hooks/blocking/test_intent_only_ending_blocker.py +4 -0
  73. package/hooks/diagnostic/AGENTS.md +43 -0
  74. package/hooks/diagnostic/CLAUDE.md +1 -43
  75. package/hooks/diagnostic/hook_log_extractor.py +2 -2
  76. package/hooks/diagnostic/migrations/AGENTS.md +16 -0
  77. package/hooks/diagnostic/migrations/CLAUDE.md +1 -16
  78. package/hooks/diagnostic/queries/AGENTS.md +19 -0
  79. package/hooks/diagnostic/queries/CLAUDE.md +1 -19
  80. package/hooks/diagnostic/test_hook_log_extractor.py +2 -2
  81. package/hooks/git-hooks/AGENTS.md +31 -0
  82. package/hooks/git-hooks/CLAUDE.md +1 -31
  83. package/hooks/git-hooks/git_hooks_constants/AGENTS.md +21 -0
  84. package/hooks/git-hooks/git_hooks_constants/CLAUDE.md +1 -21
  85. package/hooks/git-hooks/git_hooks_constants/__init__.py +4 -6
  86. package/hooks/git-hooks/pre_push.py +47 -34
  87. package/hooks/git-hooks/test_pre_push.py +111 -7
  88. package/hooks/hooks_constants/AGENTS.md +104 -0
  89. package/hooks/hooks_constants/CLAUDE.md +1 -104
  90. package/hooks/hooks_constants/hook_log_extractor_constants.py +1 -2
  91. package/hooks/hooks_constants/task_list_loop_starter_constants.py +4 -3
  92. package/hooks/hooks_constants/working_style_prompt_constants.py +29 -20
  93. package/hooks/lifecycle/AGENTS.md +18 -0
  94. package/hooks/lifecycle/CLAUDE.md +1 -18
  95. package/hooks/observability/AGENTS.md +20 -0
  96. package/hooks/observability/CLAUDE.md +1 -20
  97. package/hooks/session/AGENTS.md +33 -0
  98. package/hooks/session/CLAUDE.md +1 -33
  99. package/hooks/session/gh_pr_author_session_cleanup.py +7 -2
  100. package/hooks/session/task_list_loop_starter.py +3 -3
  101. package/hooks/session/test_gh_pr_author_session_cleanup.py +2 -1
  102. package/hooks/session/test_task_list_loop_starter.py +10 -2
  103. package/hooks/session/test_working_style_prompt.py +9 -4
  104. package/hooks/validation/AGENTS.md +20 -0
  105. package/hooks/validation/CLAUDE.md +1 -20
  106. package/hooks/validators/AGENTS.md +51 -0
  107. package/hooks/validators/CLAUDE.md +1 -51
  108. package/hooks/validators/health_check.py +52 -19
  109. package/hooks/validators/python_style_checks.py +1 -1
  110. package/hooks/validators/test_health_check.py +112 -27
  111. package/hooks/workflow/AGENTS.md +16 -0
  112. package/hooks/workflow/CLAUDE.md +1 -16
  113. package/installable-surfaces.manifest.json +1 -0
  114. package/output-styles/AGENTS.md +15 -0
  115. package/output-styles/CLAUDE.md +1 -15
  116. package/package.json +2 -1
  117. package/rules/AGENTS.md +61 -0
  118. package/rules/CLAUDE.md +1 -61
  119. package/rules/code-standards.md +5 -9
  120. package/rules/doc-prose-cuts.md +1 -1
  121. package/scripts/AGENTS.md +62 -0
  122. package/scripts/CLAUDE.md +1 -62
  123. package/scripts/codex_compat_materializer.py +4 -0
  124. package/scripts/dev_env_scripts_constants/AGENTS.md +21 -0
  125. package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -21
  126. package/scripts/sync_to_cursor/AGENTS.md +23 -0
  127. package/scripts/sync_to_cursor/CLAUDE.md +1 -23
  128. package/scripts/test_validate_instruction_pairs.py +90 -0
  129. package/scripts/test_verify_installable_package.py +5 -1
  130. package/scripts/tests/AGENTS.md +33 -0
  131. package/scripts/tests/CLAUDE.md +1 -33
  132. package/scripts/tests/test_codex_compat_materializer.py +36 -1
  133. package/scripts/validate_instruction_pairs.py +183 -0
  134. package/skills/AGENTS.md +72 -0
  135. package/skills/CLAUDE.md +1 -72
  136. package/skills/_shared/AGENTS.md +44 -0
  137. package/skills/_shared/CLAUDE.md +1 -44
  138. package/skills/_shared/advisor/AGENTS.md +9 -0
  139. package/skills/_shared/advisor/CLAUDE.md +1 -9
  140. package/skills/_shared/pr-loop/AGENTS.md +50 -0
  141. package/skills/_shared/pr-loop/CLAUDE.md +1 -50
  142. package/skills/_shared/pr-loop/prompts/AGENTS.md +9 -0
  143. package/skills/_shared/pr-loop/prompts/CLAUDE.md +1 -9
  144. package/skills/_shared/pr-loop/scripts/AGENTS.md +35 -0
  145. package/skills/_shared/pr-loop/scripts/CLAUDE.md +1 -35
  146. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/AGENTS.md +24 -0
  147. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +1 -24
  148. package/skills/anthropic-plan/AGENTS.md +34 -0
  149. package/skills/anthropic-plan/CLAUDE.md +1 -34
  150. package/skills/anthropic-plan/scripts/AGENTS.md +11 -0
  151. package/skills/anthropic-plan/scripts/CLAUDE.md +1 -11
  152. package/skills/anthropic-plan/scripts/anthropic_plan_scripts_constants/AGENTS.md +16 -0
  153. package/skills/anthropic-plan/scripts/anthropic_plan_scripts_constants/CLAUDE.md +1 -16
  154. package/skills/anthropic-plan/templates/AGENTS.md +13 -0
  155. package/skills/anthropic-plan/templates/CLAUDE.md +1 -13
  156. package/skills/anthropic-plan/workflow/AGENTS.md +14 -0
  157. package/skills/anthropic-plan/workflow/CLAUDE.md +1 -14
  158. package/skills/auditing-claude-config/AGENTS.md +21 -0
  159. package/skills/auditing-claude-config/CLAUDE.md +1 -21
  160. package/skills/autoconverge/AGENTS.md +36 -0
  161. package/skills/autoconverge/CLAUDE.md +1 -36
  162. package/skills/autoconverge/reference/AGENTS.md +16 -0
  163. package/skills/autoconverge/reference/CLAUDE.md +1 -16
  164. package/skills/autoconverge/workflow/AGENTS.md +26 -0
  165. package/skills/autoconverge/workflow/CLAUDE.md +1 -26
  166. package/skills/autoconverge/workflow/autoconverge_report_constants/AGENTS.md +16 -0
  167. package/skills/autoconverge/workflow/autoconverge_report_constants/CLAUDE.md +1 -16
  168. package/skills/bugteam/AGENTS.md +30 -0
  169. package/skills/bugteam/CLAUDE.md +1 -30
  170. package/skills/bugteam/reference/AGENTS.md +20 -0
  171. package/skills/bugteam/reference/CLAUDE.md +1 -20
  172. package/skills/bugteam/reference/obstacles/AGENTS.md +24 -0
  173. package/skills/bugteam/reference/obstacles/CLAUDE.md +1 -24
  174. package/skills/bugteam/scripts/AGENTS.md +30 -0
  175. package/skills/bugteam/scripts/CLAUDE.md +1 -30
  176. package/skills/bugteam/scripts/bugteam_scripts_constants/AGENTS.md +18 -0
  177. package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +1 -18
  178. package/skills/closeout/reference/handoff-prompt-template.md +1 -1
  179. package/skills/codex-review/AGENTS.md +46 -0
  180. package/skills/codex-review/CLAUDE.md +1 -46
  181. package/skills/codex-review/reference/AGENTS.md +15 -0
  182. package/skills/codex-review/reference/CLAUDE.md +1 -15
  183. package/skills/codex-review/scripts/codex_review_scripts_constants/AGENTS.md +18 -0
  184. package/skills/codex-review/scripts/codex_review_scripts_constants/CLAUDE.md +1 -18
  185. package/skills/codex-review/test_skill_scaffold.py +3 -3
  186. package/skills/copilot-review/AGENTS.md +18 -0
  187. package/skills/copilot-review/CLAUDE.md +1 -18
  188. package/skills/everything-search/AGENTS.md +17 -0
  189. package/skills/everything-search/CLAUDE.md +1 -17
  190. package/skills/fresh-branch/AGENTS.md +14 -0
  191. package/skills/fresh-branch/CLAUDE.md +1 -14
  192. package/skills/grok-spawn/AGENTS.md +28 -0
  193. package/skills/grok-spawn/CLAUDE.md +1 -28
  194. package/skills/pr-converge/AGENTS.md +32 -0
  195. package/skills/pr-converge/CLAUDE.md +1 -32
  196. package/skills/pr-converge/pr_converge_skill_constants/AGENTS.md +26 -0
  197. package/skills/pr-converge/pr_converge_skill_constants/CLAUDE.md +1 -26
  198. package/skills/pr-converge/reference/AGENTS.md +28 -0
  199. package/skills/pr-converge/reference/CLAUDE.md +1 -28
  200. package/skills/pr-converge/reference/obstacles/AGENTS.md +23 -0
  201. package/skills/pr-converge/reference/obstacles/CLAUDE.md +1 -23
  202. package/skills/pr-converge/scripts/AGENTS.md +46 -0
  203. package/skills/pr-converge/scripts/CLAUDE.md +1 -46
  204. package/skills/pr-converge/scripts/pr_converge_scripts_constants/AGENTS.md +18 -0
  205. package/skills/pr-converge/scripts/pr_converge_scripts_constants/CLAUDE.md +1 -18
  206. package/skills/pr-converge/workflows/AGENTS.md +16 -0
  207. package/skills/pr-converge/workflows/CLAUDE.md +1 -16
  208. package/skills/prototype/reference/honest-limitations.md +1 -1
  209. package/skills/rebase/AGENTS.md +32 -0
  210. package/skills/rebase/CLAUDE.md +1 -32
  211. package/skills/recall/AGENTS.md +30 -0
  212. package/skills/recall/CLAUDE.md +1 -30
  213. package/skills/remember/AGENTS.md +31 -0
  214. package/skills/remember/CLAUDE.md +1 -31
  215. package/skills/reviews/SKILL.md +1 -1
  216. package/skills/session-log/AGENTS.md +32 -0
  217. package/skills/session-log/CLAUDE.md +1 -32
  218. package/skills/session-tidy/AGENTS.md +36 -0
  219. package/skills/session-tidy/CLAUDE.md +1 -36
  220. package/skills/skill-builder/AGENTS.md +49 -0
  221. package/skills/skill-builder/CLAUDE.md +1 -49
  222. package/skills/skill-builder/references/AGENTS.md +25 -0
  223. package/skills/skill-builder/references/CLAUDE.md +1 -25
  224. package/skills/skill-builder/references/deterministic-elements.md +1 -1
  225. package/skills/skill-builder/templates/AGENTS.md +14 -0
  226. package/skills/skill-builder/templates/CLAUDE.md +1 -14
  227. package/skills/skill-builder/workflows/AGENTS.md +19 -0
  228. package/skills/skill-builder/workflows/CLAUDE.md +1 -19
  229. package/skills/task-build/AGENTS.md +29 -0
  230. package/skills/task-build/CLAUDE.md +1 -29
  231. package/skills/update/AGENTS.md +38 -0
  232. package/skills/update/CLAUDE.md +1 -38
  233. package/system-prompts/AGENTS.md +17 -0
  234. package/system-prompts/CLAUDE.md +1 -17
  235. package/system-prompts/software-engineer.xml +3 -3
  236. package/skills/imagegen/SKILL.md +0 -21
  237. package/skills/imagegen/scripts/config/__init__.py +0 -1
  238. package/skills/imagegen/scripts/config/constants.py +0 -28
  239. package/skills/imagegen/scripts/imagegen.py +0 -45
  240. package/skills/imagegen/scripts/imagegen_core.py +0 -528
  241. package/skills/imagegen/scripts/test_imagegen.py +0 -204
@@ -1,204 +0,0 @@
1
- """Focused tests for exact-resolution image generation."""
2
-
3
- from __future__ import annotations
4
-
5
- import base64
6
- import hashlib
7
- import json
8
- import sys
9
- from collections.abc import Mapping, Sequence
10
- from io import BytesIO
11
- from pathlib import Path
12
-
13
- import pytest
14
- from PIL import Image
15
-
16
- sys.path.insert(0, str(Path(__file__).parent))
17
-
18
- import imagegen_core
19
- from imagegen_core import (
20
- ImagegenError,
21
- ImageSize,
22
- build_oauth_environment,
23
- decode_image,
24
- download_https_image,
25
- generate_image,
26
- parse_size,
27
- publish_artifact,
28
- resize_image,
29
- )
30
-
31
-
32
- def make_png(image_size: tuple[int, int]) -> bytes:
33
- """Create real PNG bytes for provider-boundary tests."""
34
- destination = BytesIO()
35
- Image.new("RGB", image_size, "purple").save(destination, format="PNG")
36
- return destination.getvalue()
37
-
38
-
39
- def test_native_output_preserves_source_bytes_and_receipt(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
40
- source_bytes = make_png((2880, 2880))
41
- monkeypatch.setenv("OPENAI_API_KEY", "present")
42
- provider_payload = b'{"data":[{"b64_json":"' + base64.b64encode(source_bytes) + b'"}]}'
43
- receipt = generate_image("native", "openai-api", ImageSize(2880, 2880), tmp_path / "native.png", "forbid", False, transport=lambda _url, _body, _headers: provider_payload)
44
- assert (tmp_path / "native.png").read_bytes() == source_bytes
45
- assert receipt["transformation"] == "native"
46
- assert receipt["source_sha256"] == hashlib.sha256(source_bytes).hexdigest()
47
-
48
-
49
- def test_oauth_output_resizes_and_strips_api_key(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
50
- monkeypatch.setenv("OPENAI_API_KEY", "secret")
51
- captured_environment: dict[str, str] = {}
52
-
53
- def runner(_arguments: Sequence[str], work_directory: Path, environment: Mapping[str, str]) -> None:
54
- captured_environment.update(environment)
55
- (work_directory / "generated.png").write_bytes(make_png((1254, 1254)))
56
-
57
- receipt = generate_image("resize", "codex-oauth", ImageSize(2880, 2880), tmp_path / "resized.png", "allow", False, runner=runner)
58
- assert decode_image((tmp_path / "resized.png").read_bytes()) == ImageSize(2880, 2880)
59
- assert receipt["transformation"] == "resized"
60
- assert "OPENAI_API_KEY" not in captured_environment
61
-
62
-
63
- @pytest.mark.parametrize("invalid_size", ("2880", "2880x1440", "2881x2881", "3841x3841", "3840x3840"))
64
- def test_invalid_sizes_fail(invalid_size: str) -> None:
65
- with pytest.raises(ImagegenError):
66
- parse_size(invalid_size)
67
-
68
-
69
- @pytest.mark.parametrize("valid_size", ("256x256", "512x512", "1028x1028", "2880x2880"))
70
- def test_final_sizes_are_independent_from_native_api_sizes(valid_size: str) -> None:
71
- assert parse_size(valid_size).as_text() == valid_size
72
-
73
-
74
- def test_openai_unsupported_size_fails_before_authentication(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
75
- monkeypatch.delenv("OPENAI_API_KEY", raising=False)
76
-
77
- with pytest.raises(ImagegenError, match="resize-policy"):
78
- generate_image("prompt", "openai-api", ImageSize(1028, 1028), tmp_path / "artifact.png", "forbid", False)
79
-
80
-
81
- def test_openai_native_size_requires_api_key(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
82
- monkeypatch.delenv("OPENAI_API_KEY", raising=False)
83
-
84
- with pytest.raises(ImagegenError, match="OPENAI_API_KEY"):
85
- generate_image("prompt", "openai-api", ImageSize(2880, 2880), tmp_path / "artifact.png", "forbid", False)
86
-
87
-
88
- def test_openai_unsupported_size_requests_valid_provider_size_before_resize(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
89
- monkeypatch.setenv("OPENAI_API_KEY", "present")
90
- request_sizes: list[str] = []
91
- request_formats: list[str] = []
92
- source_bytes = make_png((2048, 2048))
93
- provider_payload = b'{"data":[{"b64_json":"' + base64.b64encode(source_bytes) + b'"}]}'
94
-
95
- def transport(_url: str, body: bytes, _headers: Mapping[str, str]) -> bytes:
96
- request_payload = json.loads(body)
97
- request_sizes.append(request_payload["size"])
98
- request_formats.append(request_payload["output_format"])
99
- return provider_payload
100
-
101
- receipt = generate_image("prompt", "openai-api", ImageSize(1028, 1028), tmp_path / "artifact.png", "allow", False, transport=transport)
102
-
103
- assert request_sizes == ["2048x2048"]
104
- assert request_formats == ["png"]
105
- assert receipt["source_size"] == "2048x2048"
106
- assert receipt["final_size"] == "1028x1028"
107
- assert receipt["transformation"] == "resized"
108
-
109
-
110
- def test_oauth_environment_strips_api_key(monkeypatch: pytest.MonkeyPatch) -> None:
111
- monkeypatch.setenv("OPENAI_API_KEY", "secret")
112
- assert "OPENAI_API_KEY" not in build_oauth_environment()
113
-
114
-
115
- def test_library_validates_square_size_and_png_destination(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
116
- monkeypatch.delenv("OPENAI_API_KEY", raising=False)
117
-
118
- with pytest.raises(ImagegenError, match="size exceeds"):
119
- generate_image("prompt", "openai-api", ImageSize(1028, 1024), tmp_path / "artifact.png", "forbid", False)
120
- with pytest.raises(ImagegenError, match=".png"):
121
- generate_image("prompt", "openai-api", ImageSize(2880, 2880), tmp_path / "artifact.jpg", "forbid", False)
122
-
123
-
124
- def test_http_provider_image_url_is_rejected() -> None:
125
- with pytest.raises(ImagegenError, match="HTTPS"):
126
- download_https_image("http://example.invalid/image.png")
127
-
128
-
129
- def test_provider_image_url_redirects_are_rejected() -> None:
130
- handler = imagegen_core._RejectRedirectHandler()
131
- with pytest.raises(ImagegenError, match="redirects"):
132
- handler.redirect_request(None, None, 302, "Found", {}, "https://example.invalid/other.png")
133
-
134
-
135
- def test_non_png_provider_bytes_are_rejected() -> None:
136
- destination = BytesIO()
137
- Image.new("RGB", (16, 16), "purple").save(destination, format="JPEG")
138
-
139
- with pytest.raises(ImagegenError, match="PNG"):
140
- decode_image(destination.getvalue())
141
-
142
-
143
- @pytest.mark.parametrize("artifact_names", ((), ("one.png", "two.png")))
144
- def test_missing_or_multiple_artifacts_fail(tmp_path: Path, artifact_names: tuple[str, ...]) -> None:
145
- def runner(_arguments: Sequence[str], work_directory: Path, _environment: Mapping[str, str]) -> None:
146
- for each_artifact_name in artifact_names:
147
- (work_directory / each_artifact_name).write_bytes(make_png((1254, 1254)))
148
-
149
- with pytest.raises(ImagegenError, match="exactly one"):
150
- generate_image("prompt", "codex-oauth", ImageSize(2880, 2880), tmp_path / "artifact.png", "allow", False, runner=runner)
151
-
152
-
153
- def test_mismatched_aspect_ratio_is_rejected(tmp_path: Path) -> None:
154
- def runner(_arguments: Sequence[str], work_directory: Path, _environment: Mapping[str, str]) -> None:
155
- (work_directory / "generated.png").write_bytes(make_png((1254, 1000)))
156
-
157
- with pytest.raises(ImagegenError, match="square"):
158
- generate_image("prompt", "codex-oauth", ImageSize(2880, 2880), tmp_path / "artifact.png", "allow", False, runner=runner)
159
-
160
-
161
- def test_transparency_survives_native_publication(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
162
- destination = BytesIO()
163
- Image.new("RGBA", (16, 16), (10, 20, 30, 0)).save(destination, format="PNG")
164
- monkeypatch.setenv("OPENAI_API_KEY", "present")
165
- provider_payload = b'{"data":[{"b64_json":"' + base64.b64encode(destination.getvalue()) + b'"}]}'
166
- generate_image("transparent", "openai-api", ImageSize(16, 16), tmp_path / "transparent.png", "allow", False, transport=lambda _url, _body, _headers: provider_payload)
167
- with Image.open(tmp_path / "transparent.png") as image:
168
- assert image.mode == "RGBA"
169
- assert image.getpixel((0, 0)) == (10, 20, 30, 0)
170
-
171
-
172
- def test_overwrite_refusal_and_cleanup(tmp_path: Path) -> None:
173
- output_path = tmp_path / "existing.png"
174
- output_path.write_bytes(make_png((16, 16)))
175
- with pytest.raises(ImagegenError, match="already exists"):
176
- publish_artifact(output_path, output_path.read_bytes(), {"transformation": "native"}, False)
177
- assert not list(tmp_path.glob("*.tmp"))
178
-
179
-
180
- def test_publication_rolls_back_when_receipt_replace_fails(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
181
- destination_path = tmp_path / "artifact.png"
182
- original_replace = imagegen_core.os.replace
183
- replacement_count = 0
184
-
185
- def replace_with_failure(source: Path, target: Path) -> None:
186
- nonlocal replacement_count
187
- replacement_count += 1
188
- if replacement_count == 2:
189
- raise OSError("receipt replacement failed")
190
- original_replace(source, target)
191
-
192
- monkeypatch.setattr(imagegen_core.os, "replace", replace_with_failure)
193
- with pytest.raises(ImagegenError, match="publication failed"):
194
- publish_artifact(destination_path, make_png((16, 16)), {"transformation": "native"}, False)
195
- assert not destination_path.exists()
196
- assert not destination_path.with_suffix(".json").exists()
197
- assert not list(tmp_path.glob("*.tmp"))
198
-
199
-
200
- def test_resize_and_decode_have_expected_hashes() -> None:
201
- source_bytes = make_png((1254, 1254))
202
- final_bytes = resize_image(source_bytes, ImageSize(2880, 2880))
203
- assert decode_image(final_bytes) == ImageSize(2880, 2880)
204
- assert hashlib.sha256(source_bytes).hexdigest() != hashlib.sha256(final_bytes).hexdigest()