claude-dev-env 8.14.0 → 8.15.1
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/.agents/agents/test_agent_frontmatter.py +0 -72
- package/.agents/agents-archived/code-quality-agent.md +16 -16
- package/.agents/skills/_shared/end-of-run-gotcha-recommendations.md +5 -21
- package/.agents/skills/e-code-review/SKILL.md +1 -3
- package/.agents/skills/e-simplify/SKILL.md +6 -6
- package/.agents/skills/eli5/SKILL.md +4 -14
- package/.agents/skills/eli5/reference/task-seeds.md +3 -4
- package/.agents/skills/everything-search/SKILL.md +3 -3
- package/.agents/skills/fresh-branch/SKILL.md +2 -3
- package/.agents/skills/grok-spawn/SKILL.md +3 -8
- package/.agents/skills/issue-tracker/SKILL.md +4 -6
- package/.agents/skills/orchestrator-refresh/SKILL.md +7 -7
- package/.agents/skills/plugin-eval-standalone-skill/SKILL.md +3 -3
- package/.agents/skills/privacy-hygiene/SKILL.md +6 -10
- package/.agents/skills/privacy-hygiene/reference/sweep-procedure.md +1 -1
- package/.agents/skills/pull-request/SKILL.md +11 -14
- package/.agents/skills/pull-request/reference/publication-tasks.md +2 -4
- package/.agents/skills/recovering-codex-startup/SKILL.md +3 -6
- package/.agents/skills/recovering-codex-startup/reference/task-seeds.md +1 -1
- package/.agents/skills/repairing-hook-boundaries/SKILL.md +4 -7
- package/.agents/skills/repairing-hook-boundaries/reference/task-seeds.md +1 -2
- package/.agents/skills/second-account-workers/.claude/CLAUDE.md +1 -0
- package/.agents/skills/second-account-workers/AGENTS.md +1 -0
- package/.agents/skills/second-account-workers/SKILL.md +65 -0
- package/.agents/skills/syncing-submodules/SKILL.md +7 -17
- package/.agents/skills/syncing-submodules/reference/run-tasks.md +1 -1
- package/.agents/skills/task-build/SKILL.md +1 -1
- package/.agents/skills/task-build/reference/tool-routing.md +1 -1
- package/.agents/skills/team-advisor/SKILL.md +2 -4
- package/.agents/skills/usage-pause/SKILL.md +1 -1
- package/.agents/skills/windows-scheduled-task/SKILL.md +1 -1
- package/AGENTS.md +1 -1
- package/_shared/advisor/advisor-protocol.md +4 -4
- package/_shared/advisor/reference/consult-format.md +1 -1
- package/_shared/advisor/reference/identity.md +2 -2
- package/_shared/advisor/reference/warm-up.md +1 -1
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
- package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -16
- package/bin/ever-shipped-skills.mjs +1 -0
- package/bin/install.mjs +1 -0
- package/bin/install.prompt-context.test.mjs +46 -0
- package/bin/install.test.mjs +3 -2
- package/commands/sr-loop.md +2 -2
- package/docs/CODE_RULES.md +2 -2
- package/docs/DJANGO_PATTERNS.md +5 -5
- package/docs/REACT_PATTERNS.md +6 -6
- package/docs/TEST_QUALITY.md +10 -28
- package/docs/agent-spawn-protocol.md +4 -4
- package/docs/codex-compatibility.md +4 -33
- package/docs/references/team-advisor-skill.md +0 -1
- package/docs/second-claude-account.md +4 -0
- package/docs/worker-completion-gate.md +0 -1
- package/hooks/advisory/migration_safety_advisor.py +3 -2
- package/hooks/advisory/refactor_guard.py +3 -3
- package/hooks/hooks.json +0 -5
- package/hooks/hooks_constants/issue_tracker_session_starter_constants.py +1 -2
- package/hooks/hooks_constants/orchestrator_auto_starter_constants.py +1 -2
- package/package.json +1 -1
- package/rules/explore-thoroughly.md +0 -1
- package/rules/filesystem-search.md +1 -1
- package/rules/git-workflow.md +5 -5
- package/rules/long-horizon-autonomy.md +1 -5
- package/rules/re-stage-before-commit.md +0 -2
- package/rules/research-mode.md +1 -1
- package/rules/shell-invocation.md +1 -1
- package/scripts/claude_account_worker.py +347 -0
- package/scripts/claude_account_worker_process.py +169 -0
- package/scripts/claude_account_worker_report.py +170 -0
- package/scripts/dev_env_scripts_constants/claude_account_worker_constants.py +139 -0
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +8 -2
- package/scripts/grok_headless_runner.py +2 -5
- package/scripts/policy_lint/config/constants.py +1 -0
- package/scripts/policy_lint/engine.py +5 -2
- package/scripts/repository_checks/config/constants.py +1 -1
- package/scripts/spawn_grok_batch.py +12 -1
- package/scripts/subprocess_stream_text.py +28 -0
- package/scripts/task_usage_events.py +387 -0
- package/scripts/task_usage_report.py +391 -0
- package/scripts/test_claude_account_worker.py +331 -0
- package/scripts/test_spawn_grok_batch.py +22 -0
- package/scripts/test_task_usage_events.py +47 -0
- package/scripts/test_task_usage_report.py +603 -0
- package/scripts/tests/test_banned_prose_words.py +1 -1
- package/scripts/tests/test_policy_lint_rules_archive.py +16 -0
- package/scripts/tests/test_policy_lint_rules_engine.py +2 -2
- package/scripts/tests/test_rules.py +9 -9
- package/system-prompts/software-engineer.xml +15 -30
- package/audit-rubrics/audit-categories.json +0 -700
- package/audit-rubrics/category_rubrics/category-a-api-contracts.md +0 -90
- package/audit-rubrics/category_rubrics/category-b-selector-engine-compat.md +0 -36
- package/audit-rubrics/category_rubrics/category-c-resource-cleanup.md +0 -35
- package/audit-rubrics/category_rubrics/category-d-scoping-and-ordering.md +0 -35
- package/audit-rubrics/category_rubrics/category-e-dead-code.md +0 -40
- package/audit-rubrics/category_rubrics/category-f-silent-failures.md +0 -41
- package/audit-rubrics/category_rubrics/category-g-bounds-and-overflow.md +0 -38
- package/audit-rubrics/category_rubrics/category-h-security-boundaries.md +0 -40
- package/audit-rubrics/category_rubrics/category-i-concurrency.md +0 -38
- package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +0 -59
- package/audit-rubrics/category_rubrics/category-k-codebase-conflicts.md +0 -66
- package/audit-rubrics/category_rubrics/category-l-behavior-equivalence.md +0 -48
- package/audit-rubrics/category_rubrics/category-m-producer-consumer-cardinality.md +0 -44
- package/audit-rubrics/category_rubrics/category-n-test-name-scenario-verifier.md +0 -54
- package/audit-rubrics/category_rubrics/category-p-name-vs-behavior-contract.md +0 -59
- package/audit-rubrics/category_rubrics/category-q-cross-surface-claims.md +0 -46
- package/audit-rubrics/prompts/category-a-api-contracts.md +0 -407
- package/audit-rubrics/prompts/category-b-selector-engine-compat.md +0 -401
- package/audit-rubrics/prompts/category-c-resource-cleanup.md +0 -420
- package/audit-rubrics/prompts/category-d-scoping-and-ordering.md +0 -414
- package/audit-rubrics/prompts/category-e-dead-code.md +0 -434
- package/audit-rubrics/prompts/category-f-silent-failures.md +0 -432
- package/audit-rubrics/prompts/category-g-bounds-and-overflow.md +0 -383
- package/audit-rubrics/prompts/category-h-security-boundaries.md +0 -423
- package/audit-rubrics/prompts/category-i-concurrency.md +0 -429
- package/audit-rubrics/prompts/category-j-code-rules-compliance.md +0 -458
- package/audit-rubrics/prompts/category-k-codebase-conflicts.md +0 -338
- package/audit-rubrics/prompts/category-l-behavior-equivalence.md +0 -131
- package/audit-rubrics/prompts/category-m-producer-consumer-cardinality.md +0 -129
- package/audit-rubrics/prompts/category-n-test-name-scenario-verifier.md +0 -139
- package/audit-rubrics/prompts/category-p-name-vs-behavior-contract.md +0 -89
- package/audit-rubrics/prompts/category-q-cross-surface-claims.md +0 -51
- package/hooks/hooks_constants/style_reminder_prompt_constants.py +0 -9
- package/hooks/session/style_reminder_prompt.py +0 -41
- package/hooks/session/test_style_reminder_prompt.py +0 -32
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
import claude_account_worker
|
|
12
|
+
from claude_account_worker import (
|
|
13
|
+
AccountSelection,
|
|
14
|
+
WorkerDependencies,
|
|
15
|
+
build_argument_parser,
|
|
16
|
+
select_account,
|
|
17
|
+
run_worker,
|
|
18
|
+
)
|
|
19
|
+
from dev_env_scripts_constants.claude_account_worker_constants import (
|
|
20
|
+
CHOICE_MAIN,
|
|
21
|
+
CHOICE_SECOND,
|
|
22
|
+
CHOICE_WAIT,
|
|
23
|
+
CLAUDE_CONFIG_DIR_ENV_VAR,
|
|
24
|
+
STDOUT_TAIL_CHARACTER_LIMIT,
|
|
25
|
+
TIMEOUT_EXIT_CODE,
|
|
26
|
+
WAIT_EXIT_CODE,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class FakeProcess:
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
*,
|
|
34
|
+
stdout: str = '{"result":"ready","is_error":false}',
|
|
35
|
+
returncode: int = 0,
|
|
36
|
+
timeout_first: bool = False,
|
|
37
|
+
timeout_output: str = "",
|
|
38
|
+
drain_timeout_count: int = 0,
|
|
39
|
+
) -> None:
|
|
40
|
+
self.stdout = stdout
|
|
41
|
+
self.returncode = returncode
|
|
42
|
+
self.timeout_first = timeout_first
|
|
43
|
+
self.timeout_output = timeout_output
|
|
44
|
+
self.drain_timeout_count = drain_timeout_count
|
|
45
|
+
self.pid = 42
|
|
46
|
+
self.communicate_calls: list[tuple[str | None, float | None]] = []
|
|
47
|
+
self.terminated = False
|
|
48
|
+
|
|
49
|
+
def communicate(
|
|
50
|
+
self,
|
|
51
|
+
input: str | None = None,
|
|
52
|
+
timeout: float | None = None,
|
|
53
|
+
) -> tuple[str, str]:
|
|
54
|
+
self.communicate_calls.append((input, timeout))
|
|
55
|
+
if self.timeout_first and len(self.communicate_calls) == 1:
|
|
56
|
+
raise subprocess.TimeoutExpired(
|
|
57
|
+
cmd="claude",
|
|
58
|
+
timeout=timeout,
|
|
59
|
+
output=self.timeout_output,
|
|
60
|
+
)
|
|
61
|
+
if self.drain_timeout_count:
|
|
62
|
+
self.drain_timeout_count -= 1
|
|
63
|
+
raise subprocess.TimeoutExpired(
|
|
64
|
+
cmd="claude",
|
|
65
|
+
timeout=timeout,
|
|
66
|
+
output=self.timeout_output,
|
|
67
|
+
)
|
|
68
|
+
return self.stdout, ""
|
|
69
|
+
|
|
70
|
+
def poll(self) -> int | None:
|
|
71
|
+
return self.returncode if self.terminated else None
|
|
72
|
+
|
|
73
|
+
def kill(self) -> None:
|
|
74
|
+
self.terminated = True
|
|
75
|
+
self.returncode = -9
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _selection(
|
|
79
|
+
account: str,
|
|
80
|
+
*,
|
|
81
|
+
config_dir: Path | None = None,
|
|
82
|
+
reason: str = "selected by test",
|
|
83
|
+
) -> AccountSelection:
|
|
84
|
+
return AccountSelection(account=account, config_dir=config_dir, reason=reason)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _make_runner(
|
|
88
|
+
process: FakeProcess,
|
|
89
|
+
captured: dict[str, Any],
|
|
90
|
+
) -> Callable[..., FakeProcess]:
|
|
91
|
+
def run(arguments: list[str], **options: Any) -> FakeProcess:
|
|
92
|
+
captured["arguments"] = arguments
|
|
93
|
+
captured.update(options)
|
|
94
|
+
return process
|
|
95
|
+
|
|
96
|
+
return run
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _run_worker(
|
|
100
|
+
tmp_path: Path,
|
|
101
|
+
*,
|
|
102
|
+
selection: AccountSelection,
|
|
103
|
+
process: FakeProcess,
|
|
104
|
+
parent_environment: dict[str, str] | None = None,
|
|
105
|
+
process_terminator: Callable[[FakeProcess], None] | None = None,
|
|
106
|
+
model: str | None = None,
|
|
107
|
+
permission_mode: str = "auto",
|
|
108
|
+
) -> tuple[int, dict[str, Any], dict[str, Any]]:
|
|
109
|
+
prompt_file = tmp_path / "brief.md"
|
|
110
|
+
prompt_file.write_text("standalone brief", encoding="utf-8")
|
|
111
|
+
report_file = tmp_path / "report.json"
|
|
112
|
+
captured: dict[str, Any] = {}
|
|
113
|
+
runner = _make_runner(process, captured)
|
|
114
|
+
dependencies = WorkerDependencies(
|
|
115
|
+
process_factory=runner,
|
|
116
|
+
all_parent_environment_variables=(
|
|
117
|
+
{"KEEP_ME": "present"} if parent_environment is None else parent_environment
|
|
118
|
+
),
|
|
119
|
+
binary_resolver=lambda _: "claude.exe",
|
|
120
|
+
process_terminator=(
|
|
121
|
+
process_terminator if process_terminator is not None else lambda _: None
|
|
122
|
+
),
|
|
123
|
+
monotonic_clock=lambda: 1.0,
|
|
124
|
+
)
|
|
125
|
+
exit_code = run_worker(
|
|
126
|
+
prompt_file=prompt_file,
|
|
127
|
+
cwd=tmp_path,
|
|
128
|
+
report_file=report_file,
|
|
129
|
+
model=model,
|
|
130
|
+
permission_mode=permission_mode,
|
|
131
|
+
timeout_minutes=60,
|
|
132
|
+
decision_selector=lambda: selection,
|
|
133
|
+
dependencies=dependencies,
|
|
134
|
+
)
|
|
135
|
+
return exit_code, json.loads(report_file.read_text(encoding="utf-8")), captured
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_should_set_second_profile_and_pass_parent_environment(
|
|
139
|
+
tmp_path: Path,
|
|
140
|
+
) -> None:
|
|
141
|
+
profile_dir = tmp_path / "second-profile"
|
|
142
|
+
process = FakeProcess()
|
|
143
|
+
exit_code, report, captured = _run_worker(
|
|
144
|
+
tmp_path,
|
|
145
|
+
selection=_selection(CHOICE_SECOND, config_dir=profile_dir),
|
|
146
|
+
process=process,
|
|
147
|
+
parent_environment={
|
|
148
|
+
CLAUDE_CONFIG_DIR_ENV_VAR: "inherited-profile",
|
|
149
|
+
"KEEP_ME": "present",
|
|
150
|
+
},
|
|
151
|
+
model="sonnet",
|
|
152
|
+
permission_mode="plan",
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
assert exit_code == 0
|
|
156
|
+
assert report["account"] == CHOICE_SECOND
|
|
157
|
+
assert captured["env"][CLAUDE_CONFIG_DIR_ENV_VAR] == str(profile_dir)
|
|
158
|
+
assert captured["env"]["KEEP_ME"] == "present"
|
|
159
|
+
assert captured["arguments"] == [
|
|
160
|
+
"claude.exe",
|
|
161
|
+
"-p",
|
|
162
|
+
"--output-format",
|
|
163
|
+
"json",
|
|
164
|
+
"--permission-mode",
|
|
165
|
+
"plan",
|
|
166
|
+
"--model",
|
|
167
|
+
"sonnet",
|
|
168
|
+
]
|
|
169
|
+
assert captured["cwd"] == str(tmp_path)
|
|
170
|
+
assert process.communicate_calls[0][0] == "standalone brief"
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_should_remove_inherited_profile_for_main_account(tmp_path: Path) -> None:
|
|
174
|
+
process = FakeProcess()
|
|
175
|
+
exit_code, report, captured = _run_worker(
|
|
176
|
+
tmp_path,
|
|
177
|
+
selection=_selection(CHOICE_MAIN, config_dir=tmp_path / "main-profile"),
|
|
178
|
+
process=process,
|
|
179
|
+
parent_environment={
|
|
180
|
+
CLAUDE_CONFIG_DIR_ENV_VAR: "inherited-profile",
|
|
181
|
+
"KEEP_ME": "present",
|
|
182
|
+
},
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
assert exit_code == 0
|
|
186
|
+
assert report["account"] == CHOICE_MAIN
|
|
187
|
+
assert CLAUDE_CONFIG_DIR_ENV_VAR not in captured["env"]
|
|
188
|
+
assert captured["env"]["KEEP_ME"] == "present"
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def test_should_write_wait_reason_and_start_no_process(tmp_path: Path) -> None:
|
|
192
|
+
report_file = tmp_path / "wait-report.json"
|
|
193
|
+
process_started = False
|
|
194
|
+
|
|
195
|
+
def forbidden_runner(*_: Any, **__: Any) -> FakeProcess:
|
|
196
|
+
nonlocal process_started
|
|
197
|
+
process_started = True
|
|
198
|
+
raise AssertionError("wait must not start a process")
|
|
199
|
+
|
|
200
|
+
exit_code = run_worker(
|
|
201
|
+
prompt_file=tmp_path / "missing-brief.md",
|
|
202
|
+
cwd=tmp_path,
|
|
203
|
+
report_file=report_file,
|
|
204
|
+
model=None,
|
|
205
|
+
permission_mode="auto",
|
|
206
|
+
timeout_minutes=60,
|
|
207
|
+
decision_selector=lambda: _selection(
|
|
208
|
+
CHOICE_WAIT,
|
|
209
|
+
reason="second account resets at 2026-09-24T12:00:00Z",
|
|
210
|
+
),
|
|
211
|
+
dependencies=WorkerDependencies(
|
|
212
|
+
process_factory=forbidden_runner,
|
|
213
|
+
all_parent_environment_variables={},
|
|
214
|
+
binary_resolver=lambda _: (_ for _ in ()).throw(
|
|
215
|
+
AssertionError("wait must not resolve a binary")
|
|
216
|
+
),
|
|
217
|
+
process_terminator=lambda _: None,
|
|
218
|
+
monotonic_clock=lambda: 1.0,
|
|
219
|
+
),
|
|
220
|
+
)
|
|
221
|
+
report = json.loads(report_file.read_text(encoding="utf-8"))
|
|
222
|
+
|
|
223
|
+
assert exit_code == WAIT_EXIT_CODE
|
|
224
|
+
assert process_started is False
|
|
225
|
+
assert report["account"] == CHOICE_WAIT
|
|
226
|
+
assert report["reason"] == "second account resets at 2026-09-24T12:00:00Z"
|
|
227
|
+
assert report["is_error"] is True
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_should_kill_child_on_timeout_and_exit_124(tmp_path: Path) -> None:
|
|
231
|
+
process = FakeProcess(
|
|
232
|
+
stdout='{"result":"partial"}',
|
|
233
|
+
returncode=-9,
|
|
234
|
+
timeout_first=True,
|
|
235
|
+
timeout_output='{"result":"partial"}',
|
|
236
|
+
)
|
|
237
|
+
termination_calls: list[FakeProcess] = []
|
|
238
|
+
exit_code, report, _ = _run_worker(
|
|
239
|
+
tmp_path,
|
|
240
|
+
selection=_selection(CHOICE_SECOND, config_dir=tmp_path / "profile"),
|
|
241
|
+
process=process,
|
|
242
|
+
process_terminator=lambda child: termination_calls.append(child),
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
assert exit_code == TIMEOUT_EXIT_CODE
|
|
246
|
+
assert termination_calls == [process]
|
|
247
|
+
assert len(process.communicate_calls) == 2
|
|
248
|
+
assert report["exit_code"] == TIMEOUT_EXIT_CODE
|
|
249
|
+
assert report["is_error"] is True
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def test_should_copy_parsed_result_into_report(tmp_path: Path) -> None:
|
|
253
|
+
process = FakeProcess(stdout='{"result":"ready","is_error":false}')
|
|
254
|
+
exit_code, report, _ = _run_worker(
|
|
255
|
+
tmp_path,
|
|
256
|
+
selection=_selection(CHOICE_SECOND, config_dir=tmp_path / "profile"),
|
|
257
|
+
process=process,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
assert exit_code == 0
|
|
261
|
+
assert report["result"] == "ready"
|
|
262
|
+
assert report["is_error"] is False
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def test_should_write_raw_stdout_tail_when_output_is_not_json(tmp_path: Path) -> None:
|
|
266
|
+
stdout_text = "x" * (STDOUT_TAIL_CHARACTER_LIMIT - 4) + "tail"
|
|
267
|
+
process = FakeProcess(stdout=stdout_text)
|
|
268
|
+
exit_code, report, _ = _run_worker(
|
|
269
|
+
tmp_path,
|
|
270
|
+
selection=_selection(CHOICE_SECOND, config_dir=tmp_path / "profile"),
|
|
271
|
+
process=process,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
assert exit_code == 0
|
|
275
|
+
assert report["result"] == stdout_text[-STDOUT_TAIL_CHARACTER_LIMIT:]
|
|
276
|
+
assert report["is_error"] is True
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def test_should_preserve_child_exit_code_three_for_selected_account(
|
|
280
|
+
tmp_path: Path,
|
|
281
|
+
) -> None:
|
|
282
|
+
process = FakeProcess(returncode=3)
|
|
283
|
+
exit_code, report, _ = _run_worker(
|
|
284
|
+
tmp_path,
|
|
285
|
+
selection=_selection(CHOICE_SECOND, config_dir=tmp_path / "profile"),
|
|
286
|
+
process=process,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
assert exit_code == 3
|
|
290
|
+
assert report["account"] == CHOICE_SECOND
|
|
291
|
+
assert report["exit_code"] == 3
|
|
292
|
+
assert report["is_error"] is True
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def test_should_keep_claude_json_error_flag(tmp_path: Path) -> None:
|
|
296
|
+
process = FakeProcess(stdout='{"result":"failed","is_error":true}')
|
|
297
|
+
exit_code, report, _ = _run_worker(
|
|
298
|
+
tmp_path,
|
|
299
|
+
selection=_selection(CHOICE_SECOND, config_dir=tmp_path / "profile"),
|
|
300
|
+
process=process,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
assert exit_code == 0
|
|
304
|
+
assert report["result"] == "failed"
|
|
305
|
+
assert report["is_error"] is True
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def test_should_pick_second_account_when_neither_meter_is_readable(
|
|
309
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
310
|
+
) -> None:
|
|
311
|
+
main_home = tmp_path / "main-home"
|
|
312
|
+
second_home = tmp_path / "second-home"
|
|
313
|
+
monkeypatch.setattr(Path, "home", lambda: main_home)
|
|
314
|
+
monkeypatch.setattr(claude_account_worker, "default_profile_home", lambda: second_home)
|
|
315
|
+
|
|
316
|
+
selection = select_account()
|
|
317
|
+
|
|
318
|
+
assert selection.account == CHOICE_SECOND
|
|
319
|
+
assert selection.config_dir == second_home
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def test_should_require_prompt_file_and_report_file_flags() -> None:
|
|
323
|
+
parser = build_argument_parser()
|
|
324
|
+
|
|
325
|
+
arguments = parser.parse_args(
|
|
326
|
+
["--prompt-file", "brief.md", "--report-file", "report.json"]
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
assert arguments.prompt_file == Path("brief.md")
|
|
330
|
+
assert arguments.report_file == Path("report.json")
|
|
331
|
+
assert arguments.permission_mode == "auto"
|
|
@@ -1514,6 +1514,7 @@ def test_bind_unique_worker_advisor_rejects_placeholder(
|
|
|
1514
1514
|
batch.bind_unique_worker_advisor(
|
|
1515
1515
|
advisor_spec=batch.AdvisorSpec(launcher=batch.DEFAULT_ADVISOR_LAUNCHER_PLACEHOLDER),
|
|
1516
1516
|
role_name="lens",
|
|
1517
|
+
assignment_text="assignment",
|
|
1517
1518
|
all_used_session_ids=set(),
|
|
1518
1519
|
)
|
|
1519
1520
|
|
|
@@ -1528,12 +1529,33 @@ def test_bind_unique_worker_advisor_returns_session(
|
|
|
1528
1529
|
session_id, signal = batch.bind_unique_worker_advisor(
|
|
1529
1530
|
advisor_spec=batch.AdvisorSpec(launcher="fixture-advisor-launcher"),
|
|
1530
1531
|
role_name="lens",
|
|
1532
|
+
assignment_text="assignment",
|
|
1531
1533
|
all_used_session_ids=set(),
|
|
1532
1534
|
)
|
|
1533
1535
|
assert session_id == "sess-unique-1"
|
|
1534
1536
|
assert signal == "ENDORSE"
|
|
1535
1537
|
|
|
1536
1538
|
|
|
1539
|
+
def test_bind_unique_worker_advisor_prompt_carries_assignment_text(
|
|
1540
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
1541
|
+
) -> None:
|
|
1542
|
+
all_captured_prompts: list[object] = []
|
|
1543
|
+
|
|
1544
|
+
def fake(**kwargs: object) -> tuple[str | None, str, int]:
|
|
1545
|
+
all_captured_prompts.append(kwargs["prompt_text"])
|
|
1546
|
+
return "sess-assignment-1", "ENDORSE\nok", 0
|
|
1547
|
+
|
|
1548
|
+
monkeypatch.setattr(batch, "batch_invoke_advisor", fake)
|
|
1549
|
+
batch.bind_unique_worker_advisor(
|
|
1550
|
+
advisor_spec=batch.AdvisorSpec(launcher="fixture-advisor-launcher"),
|
|
1551
|
+
role_name="lens",
|
|
1552
|
+
assignment_text="Audit module alpha for unused imports.",
|
|
1553
|
+
all_used_session_ids=set(),
|
|
1554
|
+
)
|
|
1555
|
+
assert len(all_captured_prompts) == 1
|
|
1556
|
+
assert "Audit module alpha for unused imports." in str(all_captured_prompts[0])
|
|
1557
|
+
|
|
1558
|
+
|
|
1537
1559
|
def test_obtain_advisor_completion_verdict_endorses(
|
|
1538
1560
|
monkeypatch: pytest.MonkeyPatch,
|
|
1539
1561
|
) -> None:
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Check vendor rate parsing through the task report command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
SCRIPT_PATH = Path(__file__).with_name("task_usage_report.py")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_rejects_rates_missing_a_token_category() -> None:
|
|
14
|
+
all_events = [
|
|
15
|
+
{"type": "thread.started", "thread_id": "thread-1"},
|
|
16
|
+
{"type": "turn.completed", "model": "gpt-x", "usage": {"input_tokens": 1}},
|
|
17
|
+
]
|
|
18
|
+
completed = subprocess.run(
|
|
19
|
+
[
|
|
20
|
+
sys.executable,
|
|
21
|
+
str(SCRIPT_PATH),
|
|
22
|
+
"run",
|
|
23
|
+
"--task-id",
|
|
24
|
+
"task-1",
|
|
25
|
+
"--run-id",
|
|
26
|
+
"run-1",
|
|
27
|
+
"--repository",
|
|
28
|
+
"team/project",
|
|
29
|
+
"--outcome",
|
|
30
|
+
"pass",
|
|
31
|
+
"--source-format",
|
|
32
|
+
"codex-json",
|
|
33
|
+
"--rate",
|
|
34
|
+
"gpt-x=1,2,3",
|
|
35
|
+
"--rate-source",
|
|
36
|
+
"vendor-price-card",
|
|
37
|
+
"--rate-effective-date",
|
|
38
|
+
"2026-09-24",
|
|
39
|
+
],
|
|
40
|
+
input="\n".join(map(json.dumps, all_events)),
|
|
41
|
+
capture_output=True,
|
|
42
|
+
text=True,
|
|
43
|
+
check=False,
|
|
44
|
+
)
|
|
45
|
+
assert completed.returncode == 2
|
|
46
|
+
assert completed.stdout == ""
|
|
47
|
+
assert "rate needs four prices per million tokens" in completed.stderr
|