claude-dev-env 8.46.20 → 8.46.22
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.
|
@@ -42,7 +42,7 @@ for each_member in all_members:
|
|
|
42
42
|
|
|
43
43
|
The re-raise comes first, sending an escalation directly through the boundary.
|
|
44
44
|
|
|
45
|
-
**The boundary recognizes declared types.** A runtime crash inside member work — a `TypeError`, an `AttributeError` — ends the run because its type identifies a code defect. `except Exception` triggers the rule (`CODE_RULES.md` §
|
|
45
|
+
**The boundary recognizes declared types.** A runtime crash inside member work — a `TypeError`, an `AttributeError` — ends the run because its type identifies a code defect. `except Exception` triggers the rule (`CODE_RULES.md` §9.7).
|
|
46
46
|
|
|
47
47
|
## Repair, park, and the deliverable
|
|
48
48
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from claude_account_worker_process import invoke_worker
|
|
6
|
+
from dev_env_scripts_constants.account_broker_constants import JobOutcome, Product
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_should_send_job_to_runner_and_return_outcome_with_elapsed_seconds(
|
|
10
|
+
tmp_path: Path,
|
|
11
|
+
) -> None:
|
|
12
|
+
expected_outcome = JobOutcome(
|
|
13
|
+
0, "{}", "", "extra_2", (("extra_2", "served"),), "served", None, None
|
|
14
|
+
)
|
|
15
|
+
captured_calls: list[tuple[Product, list[str], dict[str, object]]] = []
|
|
16
|
+
clock_readings = iter([100.0, 107.5])
|
|
17
|
+
|
|
18
|
+
def recording_runner(
|
|
19
|
+
product: Product, argv: list[str], **options: object
|
|
20
|
+
) -> JobOutcome:
|
|
21
|
+
captured_calls.append((product, argv, options))
|
|
22
|
+
return expected_outcome
|
|
23
|
+
|
|
24
|
+
outcome, elapsed_seconds = invoke_worker(
|
|
25
|
+
all_arguments=["claude", "-p"],
|
|
26
|
+
cwd=tmp_path,
|
|
27
|
+
prompt_text="standalone brief",
|
|
28
|
+
timeout_minutes=2,
|
|
29
|
+
runner=recording_runner,
|
|
30
|
+
monotonic_clock=lambda: next(clock_readings),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
assert outcome is expected_outcome
|
|
34
|
+
assert elapsed_seconds == 7.5
|
|
35
|
+
assert captured_calls == [
|
|
36
|
+
(
|
|
37
|
+
Product.CLAUDE,
|
|
38
|
+
["claude", "-p"],
|
|
39
|
+
{
|
|
40
|
+
"timeout_seconds": 120,
|
|
41
|
+
"stdin_text": "standalone brief",
|
|
42
|
+
"cwd": tmp_path,
|
|
43
|
+
"encoding": "utf-8",
|
|
44
|
+
"errors": "replace",
|
|
45
|
+
},
|
|
46
|
+
)
|
|
47
|
+
]
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from claude_account_worker_report import (
|
|
10
|
+
WorkerReport,
|
|
11
|
+
finalize_report,
|
|
12
|
+
make_report,
|
|
13
|
+
pre_launch_failure_report,
|
|
14
|
+
wait_report,
|
|
15
|
+
write_report,
|
|
16
|
+
)
|
|
17
|
+
from dev_env_scripts_constants.account_broker_constants import WAIT_EXIT_CODE
|
|
18
|
+
from dev_env_scripts_constants.claude_account_worker_constants import (
|
|
19
|
+
STDOUT_TAIL_CHARACTER_LIMIT,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_should_build_wait_report_with_wait_exit_code_and_reset_time() -> None:
|
|
24
|
+
reset_at = datetime(2026, 10, 4, 12, tzinfo=timezone.utc)
|
|
25
|
+
|
|
26
|
+
report = wait_report("wait", "no account has room", reset_at)
|
|
27
|
+
|
|
28
|
+
assert report == WorkerReport(
|
|
29
|
+
account="wait",
|
|
30
|
+
reason="no account has room",
|
|
31
|
+
exit_code=WAIT_EXIT_CODE,
|
|
32
|
+
duration_seconds=0.0,
|
|
33
|
+
payload=None,
|
|
34
|
+
is_error=False,
|
|
35
|
+
wait_reset_at=reset_at,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_should_build_report_from_json_result_and_round_duration() -> None:
|
|
40
|
+
report = make_report(
|
|
41
|
+
"extra_2",
|
|
42
|
+
"served",
|
|
43
|
+
exit_code=0,
|
|
44
|
+
duration_seconds=1.23456,
|
|
45
|
+
stdout_text='{"result":"ready","is_error":false}',
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
assert report.payload == "ready"
|
|
49
|
+
assert report.is_error is False
|
|
50
|
+
assert report.duration_seconds == 1.235
|
|
51
|
+
assert report.wait_reset_at is None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_should_mark_report_as_error_when_json_flags_error() -> None:
|
|
55
|
+
report = make_report(
|
|
56
|
+
"extra_2",
|
|
57
|
+
"served",
|
|
58
|
+
exit_code=0,
|
|
59
|
+
duration_seconds=0.0,
|
|
60
|
+
stdout_text='{"result":"failed","is_error":true}',
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
assert report.payload == "failed"
|
|
64
|
+
assert report.is_error is True
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_should_mark_report_as_error_when_exit_code_is_nonzero() -> None:
|
|
68
|
+
report = make_report(
|
|
69
|
+
"extra_2",
|
|
70
|
+
"served",
|
|
71
|
+
exit_code=5,
|
|
72
|
+
duration_seconds=0.0,
|
|
73
|
+
stdout_text='{"result":"ready","is_error":false}',
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
assert report.exit_code == 5
|
|
77
|
+
assert report.is_error is True
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_should_keep_bounded_stdout_tail_when_stdout_is_not_json() -> None:
|
|
81
|
+
stdout_text = "x" * STDOUT_TAIL_CHARACTER_LIMIT + "tail"
|
|
82
|
+
|
|
83
|
+
report = make_report(
|
|
84
|
+
"extra_2", "served", exit_code=0, duration_seconds=0.0, stdout_text=stdout_text
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
assert report.payload == stdout_text[-STDOUT_TAIL_CHARACTER_LIMIT:]
|
|
88
|
+
assert len(report.payload) == STDOUT_TAIL_CHARACTER_LIMIT
|
|
89
|
+
assert report.is_error is True
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_should_drop_payload_when_json_has_no_result_key() -> None:
|
|
93
|
+
report = make_report(
|
|
94
|
+
"extra_2",
|
|
95
|
+
"served",
|
|
96
|
+
exit_code=0,
|
|
97
|
+
duration_seconds=0.0,
|
|
98
|
+
stdout_text='{"other":1}',
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
assert report.payload is None
|
|
102
|
+
assert report.is_error is True
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_should_build_pre_launch_failure_report_with_zero_duration() -> None:
|
|
106
|
+
report = pre_launch_failure_report("none", "prompt file is unreadable", 1)
|
|
107
|
+
|
|
108
|
+
assert report.account == "none"
|
|
109
|
+
assert report.reason == "prompt file is unreadable"
|
|
110
|
+
assert report.exit_code == 1
|
|
111
|
+
assert report.duration_seconds == 0.0
|
|
112
|
+
assert report.is_error is True
|
|
113
|
+
assert report.wait_reset_at is None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_should_write_report_as_json_and_create_parent_directory(
|
|
117
|
+
tmp_path: Path,
|
|
118
|
+
) -> None:
|
|
119
|
+
reset_at = datetime(2026, 10, 4, 12, tzinfo=timezone.utc)
|
|
120
|
+
report_file = tmp_path / "nested" / "report.json"
|
|
121
|
+
|
|
122
|
+
write_report(report_file, wait_report("wait", "no account has room", reset_at))
|
|
123
|
+
|
|
124
|
+
report_text = report_file.read_text(encoding="utf-8")
|
|
125
|
+
assert report_text.endswith("\n")
|
|
126
|
+
assert json.loads(report_text) == {
|
|
127
|
+
"account": "wait",
|
|
128
|
+
"reason": "no account has room",
|
|
129
|
+
"exit_code": WAIT_EXIT_CODE,
|
|
130
|
+
"duration_seconds": 0.0,
|
|
131
|
+
"result": None,
|
|
132
|
+
"is_error": False,
|
|
133
|
+
"wait_reset_at": reset_at.isoformat(),
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_should_finalize_report_by_writing_it_printing_summary_and_returning_exit_code(
|
|
138
|
+
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
139
|
+
) -> None:
|
|
140
|
+
report_file = tmp_path / "report.json"
|
|
141
|
+
report = make_report(
|
|
142
|
+
"extra_2",
|
|
143
|
+
"served",
|
|
144
|
+
exit_code=4,
|
|
145
|
+
duration_seconds=2.0,
|
|
146
|
+
stdout_text='{"result":"done","is_error":false}',
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
exit_code = finalize_report(report_file, report)
|
|
150
|
+
|
|
151
|
+
assert exit_code == 4
|
|
152
|
+
written_report = json.loads(report_file.read_text(encoding="utf-8"))
|
|
153
|
+
assert written_report["result"] == "done"
|
|
154
|
+
assert written_report["wait_reset_at"] is None
|
|
155
|
+
assert (
|
|
156
|
+
capsys.readouterr().out == f"account=extra_2 exit_code=4 report={report_file}\n"
|
|
157
|
+
)
|
|
@@ -27,7 +27,8 @@ _TEXT_SUFFIXES = frozenset({".md", ".py", ".js", ".mjs", ".ts", ".json", ".xml"}
|
|
|
27
27
|
_MARKDOWN_LINK = re.compile(r"(?<!!)\[[^]]+\]\(([^)\s]+)(?:\s+[^)]*)?\)")
|
|
28
28
|
_INDEX_ANCHOR = re.compile(r"CODE_RULES\.md#([^\s)\]<>\"`]+)", re.IGNORECASE)
|
|
29
29
|
_NUMBERED_CITATION = re.compile(
|
|
30
|
-
r"(
|
|
30
|
+
r"CODE_RULES(?:\.md)?[`'\"]*\s*(?:\u00a7\s*|section\s+)(\d+(?:\.\d+)?)",
|
|
31
|
+
re.IGNORECASE,
|
|
31
32
|
)
|
|
32
33
|
|
|
33
34
|
|
|
@@ -129,6 +130,27 @@ def test_should_report_dead_code_rules_anchor() -> None:
|
|
|
129
130
|
_assert_citations_resolve(index_text, [dead_citation])
|
|
130
131
|
|
|
131
132
|
|
|
133
|
+
def test_should_ignore_section_number_of_another_document() -> None:
|
|
134
|
+
index_text = _INDEX_PATH.read_text(encoding="utf-8")
|
|
135
|
+
foreign_citation = (_INDEX_PATH, "See the runbook section 12 appendix row.")
|
|
136
|
+
_assert_citations_resolve(index_text, [foreign_citation])
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@pytest.mark.parametrize(
|
|
140
|
+
"citation_text",
|
|
141
|
+
[
|
|
142
|
+
"CODE_RULES.md" + " section 12",
|
|
143
|
+
"``docs/CODE_RULES.md``" + " section 12",
|
|
144
|
+
"CODE_RULES" + " \u00a712",
|
|
145
|
+
"CODE_RULES.md" + " \u00a712",
|
|
146
|
+
],
|
|
147
|
+
)
|
|
148
|
+
def test_should_report_dead_code_rules_section(citation_text: str) -> None:
|
|
149
|
+
index_text = _INDEX_PATH.read_text(encoding="utf-8")
|
|
150
|
+
with pytest.raises(AssertionError, match="dead CODE_RULES" + " section 12"):
|
|
151
|
+
_assert_citations_resolve(index_text, [(_INDEX_PATH, citation_text)])
|
|
152
|
+
|
|
153
|
+
|
|
132
154
|
def _assert_destination_resolves(markdown_path: Path, destination: str) -> None:
|
|
133
155
|
if "://" in destination or destination.startswith("#"):
|
|
134
156
|
return
|