claude-dev-env 1.95.0 → 2.0.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/_shared/advisor/CLAUDE.md +2 -2
- package/_shared/advisor/advisor-protocol.md +20 -20
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +15 -12
- package/_shared/advisor/scripts/model_tier_run_validator.py +11 -10
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +25 -19
- package/_shared/advisor/scripts/tests/test_tier_model_ids.py +17 -17
- package/_shared/advisor/scripts/tier_model_ids.py +18 -18
- package/_shared/pr-loop/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/CLAUDE.md +2 -1
- package/_shared/pr-loop/scripts/README.md +1 -0
- package/_shared/pr-loop/scripts/code_rules_gate.py +253 -1980
- package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +32 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/__init__.py +7 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/added_line_maps.py +268 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/enforcer_loading.py +172 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_arguments.py +70 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_running.py +326 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +85 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/git_file_sets.py +331 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +369 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/conftest.py +14 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_added_line_maps.py +118 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_enforcer_loading.py +17 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_arguments.py +29 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_running.py +99 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_blob_readers.py +69 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_file_sets.py +137 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_running.py +116 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_violation_scoping.py +75 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_wrapper_plumb_check.py +49 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/violation_scoping.py +328 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/wrapper_plumb_check.py +206 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +24 -17
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +1 -0
- package/_shared/pr-loop/scripts/reviews_disabled.py +19 -2
- package/_shared/pr-loop/scripts/test_code_rules_gate.py +278 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate_constants.py +6 -39
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +43 -0
- package/_shared/pr-loop/worker-spawn.md +186 -0
- package/agents/code-verifier.md +1 -1
- package/bin/ever-shipped-skills.mjs +3 -0
- package/bin/expand_home_directory_tokens.mjs +1 -1
- package/bin/install.mjs +5 -2
- package/hooks/advisory/refactor_guard.py +3 -4
- package/hooks/blocking/CLAUDE.md +7 -1
- package/hooks/blocking/block_main_commit.py +2 -2
- package/hooks/blocking/claude_md_orphan_file_blocker.py +75 -699
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/CLAUDE.md +28 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/__init__.py +1 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/__init__.py +1 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/orphan_blocker_constants.py +18 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/decision.py +81 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/references.py +307 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/scan_plan.py +124 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/subtree_scan.py +179 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/conftest.py +10 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_decision.py +34 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_references.py +42 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_scan_plan.py +27 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_subtree_scan.py +30 -0
- package/hooks/blocking/code_rules_boolean_mustcheck.py +1 -1
- package/hooks/blocking/code_rules_mock_completeness.py +1 -1
- package/hooks/blocking/code_rules_optional_params.py +2 -2
- package/hooks/blocking/code_rules_shared.py +1 -1
- package/hooks/blocking/code_rules_test_assertions.py +1 -1
- package/hooks/blocking/code_rules_typeddict_stub.py +1 -1
- package/hooks/blocking/gh_pr_author_enforcer.py +1 -1
- package/hooks/blocking/inventory_intent_records/CLAUDE.md +26 -0
- package/hooks/blocking/inventory_intent_records/__init__.py +1 -0
- package/hooks/blocking/inventory_intent_records/config/__init__.py +1 -0
- package/hooks/blocking/inventory_intent_records/config/intent_records_constants.py +20 -0
- package/hooks/blocking/inventory_intent_records/records.py +271 -0
- package/hooks/blocking/inventory_intent_records/tests/conftest.py +10 -0
- package/hooks/blocking/inventory_intent_records/tests/test_records.py +80 -0
- package/hooks/blocking/package_inventory_stale_blocker.py +54 -384
- package/hooks/blocking/package_inventory_stale_blocker_parts/CLAUDE.md +26 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/__init__.py +1 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/config/__init__.py +1 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/config/inventory_blocker_constants.py +16 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/decision.py +84 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/inventory_detection.py +307 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/tests/conftest.py +10 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_decision.py +38 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_inventory_detection.py +61 -0
- package/hooks/blocking/pii_payload_scan.py +138 -42
- package/hooks/blocking/pii_prevention_blocker.py +185 -291
- package/hooks/blocking/pii_prevention_blocker_parts/CLAUDE.md +24 -0
- package/hooks/blocking/pii_prevention_blocker_parts/__init__.py +1 -0
- package/hooks/blocking/pii_prevention_blocker_parts/config/__init__.py +1 -0
- package/hooks/blocking/pii_prevention_blocker_parts/config/repository_resolution_constants.py +28 -0
- package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +214 -0
- package/hooks/blocking/pii_prevention_blocker_parts/repository_resolution.py +208 -0
- package/hooks/blocking/pr_description_command_parser.py +8 -4
- package/hooks/blocking/precommit_code_rules_gate.py +3 -3
- package/hooks/blocking/tdd_enforcer.py +97 -608
- package/hooks/blocking/tdd_enforcer_parts/CLAUDE.md +30 -0
- package/hooks/blocking/tdd_enforcer_parts/__init__.py +1 -0
- package/hooks/blocking/tdd_enforcer_parts/candidate_paths.py +142 -0
- package/hooks/blocking/tdd_enforcer_parts/config/__init__.py +1 -0
- package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +32 -0
- package/hooks/blocking/tdd_enforcer_parts/content_analysis.py +268 -0
- package/hooks/blocking/tdd_enforcer_parts/decisions.py +92 -0
- package/hooks/blocking/tdd_enforcer_parts/freshness.py +80 -0
- package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +63 -0
- package/hooks/blocking/tdd_enforcer_parts/path_classification.py +119 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/conftest.py +10 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_candidate_paths.py +31 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_content_analysis.py +30 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_decisions.py +34 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_freshness.py +28 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_git_tracking.py +48 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_path_classification.py +36 -0
- package/hooks/blocking/test_inventory_deadlock_resolution.py +154 -0
- package/hooks/blocking/test_pii_payload_scan.py +168 -0
- package/hooks/blocking/test_tdd_enforcer_restore.py +108 -0
- package/hooks/blocking/test_verifier_verdict_minter.py +55 -158
- package/hooks/blocking/tests/conftest.py +10 -0
- package/hooks/blocking/tests/test_pii_prevention_blocker.py +260 -0
- package/hooks/blocking/tests/test_repository_exemption.py +105 -0
- package/hooks/blocking/tests/test_repository_resolution.py +108 -0
- package/hooks/diagnostic/hook_log_extractor.py +12 -10
- package/hooks/git-hooks/post_commit.py +3 -4
- package/hooks/hooks_constants/CLAUDE.md +2 -2
- package/hooks/hooks_constants/banned_identifiers_constants.py +0 -1
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
- package/hooks/hooks_constants/local_identity.py +59 -8
- package/hooks/hooks_constants/pii_prevention_constants.py +0 -6
- package/hooks/hooks_constants/test_local_identity.py +105 -3
- package/hooks/pyproject.toml +13 -36
- package/hooks/session/plugin_data_dir_cleanup.py +0 -1
- package/hooks/validation/mypy_validator.py +2 -2
- package/hooks/validators/health_check.py +1 -0
- package/hooks/validators/mypy_integration.py +2 -0
- package/hooks/validators/ruff_integration.py +3 -0
- package/hooks/workflow/auto_formatter.py +5 -4
- package/package.json +1 -1
- package/scripts/CLAUDE.md +4 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +6 -4
- package/scripts/dev_env_scripts_constants/code_review_constants.py +71 -0
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +435 -0
- package/scripts/dev_env_scripts_constants/timing.py +7 -1
- package/scripts/grok_headless_runner.py +294 -0
- package/scripts/grok_worker_preflight.py +410 -0
- package/scripts/invoke_code_review.py +463 -0
- package/scripts/resolve_worker_spawn.py +619 -0
- package/scripts/spawn_grok_batch.py +672 -0
- package/scripts/test_grok_headless_runner.py +626 -0
- package/scripts/test_grok_worker_preflight.py +1054 -0
- package/scripts/test_invoke_code_review.py +672 -0
- package/scripts/test_resolve_worker_spawn.py +1014 -0
- package/scripts/test_spawn_grok_batch.py +1017 -0
- package/skills/CLAUDE.md +5 -3
- package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +72 -13
- package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +121 -14
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +78 -0
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +121 -0
- package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +196 -6
- package/skills/autoconverge/CLAUDE.md +3 -3
- package/skills/autoconverge/SKILL.md +9 -3
- package/skills/autoconverge/reference/CLAUDE.md +2 -2
- package/skills/autoconverge/reference/convergence.md +33 -11
- package/skills/autoconverge/reference/stop-conditions.md +16 -5
- package/skills/autoconverge/workflow/CLAUDE.md +2 -1
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +7 -2
- package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +300 -0
- package/skills/autoconverge/workflow/converge.contract.test.mjs +5 -5
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +29 -29
- package/skills/autoconverge/workflow/converge.fix-progress.test.mjs +1 -1
- package/skills/autoconverge/workflow/converge.mjs +200 -16
- package/skills/bugteam/CLAUDE.md +2 -2
- package/skills/bugteam/CONSTRAINTS.md +3 -2
- package/skills/bugteam/PROMPTS.md +7 -6
- package/skills/bugteam/SKILL.md +18 -13
- package/skills/bugteam/reference/audit-and-teammates.md +215 -35
- package/skills/bugteam/reference/design-rationale.md +1 -1
- package/skills/bugteam/reference/obstacles/CLAUDE.md +1 -1
- package/skills/bugteam/reference/team-setup.md +8 -2
- package/skills/codex-review/CLAUDE.md +46 -0
- package/skills/codex-review/SKILL.md +181 -0
- package/skills/codex-review/reference/CLAUDE.md +15 -0
- package/skills/codex-review/reference/cli-contract.md +253 -0
- package/skills/codex-review/reference/loop-integration.md +118 -0
- package/skills/codex-review/scripts/codex_down_classifier.py +98 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/CLAUDE.md +18 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/__init__.py +1 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/classifier_constants.py +35 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +86 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/findings_constants.py +18 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +45 -0
- package/skills/codex-review/scripts/codex_usage_probe.py +573 -0
- package/skills/codex-review/scripts/fixtures/auth_failure_synthetic.txt +1 -0
- package/skills/codex-review/scripts/fixtures/config_load_failure_v0.125.0.txt +1 -0
- package/skills/codex-review/scripts/fixtures/freeform_findings_v0.144.3.txt +6 -0
- package/skills/codex-review/scripts/fixtures/model_rejection_v0.125.0.jsonl +5 -0
- package/skills/codex-review/scripts/fixtures/structured_findings.txt +13 -0
- package/skills/codex-review/scripts/fixtures/success_stream_v0.144.3.jsonl +6 -0
- package/skills/codex-review/scripts/fixtures/unknown_failure_synthetic.txt +1 -0
- package/skills/codex-review/scripts/fixtures/usage_limit_synthetic.txt +1 -0
- package/skills/codex-review/scripts/parse_codex_findings.py +207 -0
- package/skills/codex-review/scripts/run_codex_review.py +415 -0
- package/skills/codex-review/scripts/test_codex_down_classifier.py +143 -0
- package/skills/codex-review/scripts/test_codex_usage_probe.py +678 -0
- package/skills/codex-review/scripts/test_parse_codex_findings.py +130 -0
- package/skills/codex-review/scripts/test_run_codex_review.py +812 -0
- package/skills/codex-review/test_skill_scaffold.py +192 -0
- package/skills/grok-spawn/CLAUDE.md +28 -0
- package/skills/grok-spawn/SKILL.md +226 -0
- package/skills/grok-spawn/reference/flag-profiles.md +132 -0
- package/skills/grok-spawn/reference/worker-briefs.md +152 -0
- package/skills/grokify/SKILL.md +9 -1
- package/skills/grokify/capability-claims.test.mjs +28 -0
- package/skills/grokify/evals/README.md +72 -0
- package/skills/grokify/evals/parse-payload.test.mjs +171 -0
- package/skills/grokify/evals/run-capability-evals.mjs +545 -0
- package/skills/orchestrator/SKILL.md +42 -29
- package/skills/orchestrator-refresh/SKILL.md +17 -9
- package/skills/pr-converge/SKILL.md +34 -13
- package/skills/pr-converge/reference/convergence-gates.md +42 -15
- package/skills/pr-converge/reference/fix-protocol.md +1 -1
- package/skills/pr-converge/reference/ground-rules.md +1 -1
- package/skills/pr-converge/reference/per-tick.md +130 -42
- package/skills/pr-converge/reference/state-schema.md +10 -0
- package/skills/pr-converge/scripts/CLAUDE.md +2 -0
- package/skills/pr-converge/scripts/_pr_converge_path_setup.py +5 -1
- package/skills/pr-converge/scripts/check_convergence.py +605 -29
- package/skills/pr-converge/scripts/check_convergence_availability.py +232 -0
- package/skills/pr-converge/scripts/check_convergence_gates.py +279 -235
- package/skills/pr-converge/scripts/check_convergence_thread_gates.py +1 -1
- package/skills/pr-converge/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +36 -2
- package/skills/pr-converge/scripts/test__pr_converge_path_setup.py +4 -0
- package/skills/pr-converge/scripts/test_check_convergence.py +71 -3
- package/skills/pr-converge/scripts/test_check_convergence_availability.py +326 -0
- package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -0
- package/skills/pr-converge/scripts/test_check_convergence_contract.py +89 -17
- package/skills/pr-converge/scripts/test_check_convergence_fixture.py +179 -0
- package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -68
- package/skills/pr-converge/scripts/test_check_convergence_thread_gates.py +24 -0
- package/skills/pr-converge/test_step5_host_branch.py +106 -0
- package/skills/pr-loop-cloud-transport/SKILL.md +2 -0
- package/skills/reviewer-gates/SKILL.md +7 -5
- package/skills/team-advisor/SKILL.md +7 -7
|
@@ -0,0 +1,1054 @@
|
|
|
1
|
+
"""Behavioral tests for the grok worker preflight soft gate."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
_SCRIPTS_DIR = Path(__file__).resolve().parent
|
|
13
|
+
if str(_SCRIPTS_DIR) not in sys.path:
|
|
14
|
+
sys.path.insert(0, str(_SCRIPTS_DIR))
|
|
15
|
+
|
|
16
|
+
import grok_worker_preflight as preflight # noqa: E402
|
|
17
|
+
from dev_env_scripts_constants.grok_worker_constants import ( # noqa: E402
|
|
18
|
+
AGENTS_SUBDIRECTORY,
|
|
19
|
+
ALL_AGENT_FILENAMES_BY_ROLE,
|
|
20
|
+
ALL_AUTH_FAILURE_SIGNATURES,
|
|
21
|
+
ALL_USAGE_EXHAUSTION_SIGNATURES,
|
|
22
|
+
AUTH_LEADER_SOCKET_FILENAME,
|
|
23
|
+
CLI_PING_FLAG,
|
|
24
|
+
CLI_ROLE_FLAG,
|
|
25
|
+
CLI_RUN_STATE_DIR_FLAG,
|
|
26
|
+
DEFAULT_ROLE,
|
|
27
|
+
EXIT_FALLTHROUGH,
|
|
28
|
+
EXIT_USABLE,
|
|
29
|
+
GROK_BINARY_NAME,
|
|
30
|
+
LEADER_SOCKET_FLAG,
|
|
31
|
+
MANIFEST_FILENAME,
|
|
32
|
+
MODELS_SUBCOMMAND,
|
|
33
|
+
PING_CACHE_CHECKED_AT_KEY,
|
|
34
|
+
PING_CACHE_FILENAME,
|
|
35
|
+
PING_CACHE_IS_OK_KEY,
|
|
36
|
+
PING_LEADER_SOCKET_FILENAME,
|
|
37
|
+
PING_MAX_TURNS,
|
|
38
|
+
PING_PROMPT,
|
|
39
|
+
PING_TTL_SECONDS,
|
|
40
|
+
REASON_CLAUDE_DEV_ENV_CONFIG_MISSING,
|
|
41
|
+
REASON_GROK_AUTH_FAILED,
|
|
42
|
+
REASON_GROK_BINARY_MISSING,
|
|
43
|
+
REASON_GROK_USAGE_EXHAUSTED,
|
|
44
|
+
ROLE_BUGTEAM,
|
|
45
|
+
SINGLE_TURN_FLAG,
|
|
46
|
+
STDOUT_FALLTHROUGH_TEMPLATE,
|
|
47
|
+
STDOUT_OK_LINE,
|
|
48
|
+
UTF8_DECODE_ERRORS,
|
|
49
|
+
UTF8_ENCODING,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _completed(
|
|
54
|
+
command: list[str],
|
|
55
|
+
returncode: int,
|
|
56
|
+
stdout: str = "",
|
|
57
|
+
stderr: str = "",
|
|
58
|
+
) -> subprocess.CompletedProcess[str]:
|
|
59
|
+
return subprocess.CompletedProcess(
|
|
60
|
+
args=command, returncode=returncode, stdout=stdout, stderr=stderr
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class _Recorder:
|
|
65
|
+
def __init__(self, all_completions: list[subprocess.CompletedProcess[str]]) -> None:
|
|
66
|
+
self.all_completions = list(all_completions)
|
|
67
|
+
self.invocations: list[list[str]] = []
|
|
68
|
+
|
|
69
|
+
def __call__(
|
|
70
|
+
self,
|
|
71
|
+
invocation: list[str],
|
|
72
|
+
**_keyword_arguments: object,
|
|
73
|
+
) -> subprocess.CompletedProcess[str]:
|
|
74
|
+
self.invocations.append(list(invocation))
|
|
75
|
+
if not self.all_completions:
|
|
76
|
+
raise AssertionError(f"unexpected invocation: {invocation}")
|
|
77
|
+
return self.all_completions.pop(0)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _write_install_layout(claude_home: Path, *, role: str = ROLE_BUGTEAM) -> None:
|
|
81
|
+
claude_home.mkdir(parents=True, exist_ok=True)
|
|
82
|
+
(claude_home / MANIFEST_FILENAME).write_text("{}", encoding=UTF8_ENCODING)
|
|
83
|
+
agents_directory = claude_home / AGENTS_SUBDIRECTORY
|
|
84
|
+
agents_directory.mkdir(parents=True, exist_ok=True)
|
|
85
|
+
for each_filename in ALL_AGENT_FILENAMES_BY_ROLE[role]:
|
|
86
|
+
(agents_directory / each_filename).write_text(
|
|
87
|
+
f"# {each_filename}\n", encoding=UTF8_ENCODING
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _install_ok_static_seams(
|
|
92
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
93
|
+
claude_home: Path,
|
|
94
|
+
recorder: _Recorder,
|
|
95
|
+
) -> None:
|
|
96
|
+
_write_install_layout(claude_home)
|
|
97
|
+
monkeypatch.setattr(preflight, "claude_config_home", lambda: claude_home)
|
|
98
|
+
monkeypatch.setattr(preflight, "preflight_which", lambda name: f"/fake/bin/{name}")
|
|
99
|
+
monkeypatch.setattr(preflight, "preflight_subprocess_runner", recorder)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_binary_missing_falls_through_with_reason(
|
|
103
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
104
|
+
) -> None:
|
|
105
|
+
claude_home = tmp_path / "claude"
|
|
106
|
+
run_state_directory = tmp_path / "run"
|
|
107
|
+
run_state_directory.mkdir()
|
|
108
|
+
_write_install_layout(claude_home)
|
|
109
|
+
monkeypatch.setattr(preflight, "claude_config_home", lambda: claude_home)
|
|
110
|
+
monkeypatch.setattr(preflight, "preflight_which", lambda _name: None)
|
|
111
|
+
monkeypatch.setattr(
|
|
112
|
+
preflight,
|
|
113
|
+
"preflight_subprocess_runner",
|
|
114
|
+
_Recorder([_completed([GROK_BINARY_NAME], 0)]),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
exit_code = preflight.main([CLI_RUN_STATE_DIR_FLAG, str(run_state_directory)])
|
|
118
|
+
|
|
119
|
+
captured = capsys.readouterr()
|
|
120
|
+
assert exit_code == EXIT_FALLTHROUGH
|
|
121
|
+
assert captured.out.strip() == STDOUT_FALLTHROUGH_TEMPLATE.format(
|
|
122
|
+
reason=REASON_GROK_BINARY_MISSING
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_auth_failed_falls_through_with_reason(
|
|
127
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
128
|
+
) -> None:
|
|
129
|
+
claude_home = tmp_path / "claude"
|
|
130
|
+
run_state_directory = tmp_path / "run"
|
|
131
|
+
run_state_directory.mkdir()
|
|
132
|
+
recorder = _Recorder(
|
|
133
|
+
[
|
|
134
|
+
_completed(
|
|
135
|
+
[GROK_BINARY_NAME, MODELS_SUBCOMMAND],
|
|
136
|
+
1,
|
|
137
|
+
stderr=ALL_AUTH_FAILURE_SIGNATURES[0],
|
|
138
|
+
)
|
|
139
|
+
]
|
|
140
|
+
)
|
|
141
|
+
_install_ok_static_seams(monkeypatch, claude_home, recorder)
|
|
142
|
+
|
|
143
|
+
exit_code = preflight.main([CLI_RUN_STATE_DIR_FLAG, str(run_state_directory)])
|
|
144
|
+
|
|
145
|
+
captured = capsys.readouterr()
|
|
146
|
+
assert exit_code == EXIT_FALLTHROUGH
|
|
147
|
+
assert captured.out.strip() == STDOUT_FALLTHROUGH_TEMPLATE.format(
|
|
148
|
+
reason=REASON_GROK_AUTH_FAILED
|
|
149
|
+
)
|
|
150
|
+
assert recorder.invocations[0][0] == GROK_BINARY_NAME
|
|
151
|
+
assert MODELS_SUBCOMMAND in recorder.invocations[0]
|
|
152
|
+
assert LEADER_SOCKET_FLAG in recorder.invocations[0]
|
|
153
|
+
leader_socket_path = Path(
|
|
154
|
+
recorder.invocations[0][recorder.invocations[0].index(LEADER_SOCKET_FLAG) + 1]
|
|
155
|
+
)
|
|
156
|
+
assert leader_socket_path.name == AUTH_LEADER_SOCKET_FILENAME
|
|
157
|
+
assert leader_socket_path.parent == run_state_directory
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_config_missing_manifest_falls_through(
|
|
161
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
162
|
+
) -> None:
|
|
163
|
+
claude_home = tmp_path / "claude"
|
|
164
|
+
claude_home.mkdir()
|
|
165
|
+
run_state_directory = tmp_path / "run"
|
|
166
|
+
run_state_directory.mkdir()
|
|
167
|
+
recorder = _Recorder(
|
|
168
|
+
[_completed([GROK_BINARY_NAME, MODELS_SUBCOMMAND], 0, stdout="logged in")]
|
|
169
|
+
)
|
|
170
|
+
monkeypatch.setattr(preflight, "claude_config_home", lambda: claude_home)
|
|
171
|
+
monkeypatch.setattr(preflight, "preflight_which", lambda name: f"/fake/bin/{name}")
|
|
172
|
+
monkeypatch.setattr(preflight, "preflight_subprocess_runner", recorder)
|
|
173
|
+
|
|
174
|
+
exit_code = preflight.main([CLI_RUN_STATE_DIR_FLAG, str(run_state_directory)])
|
|
175
|
+
|
|
176
|
+
captured = capsys.readouterr()
|
|
177
|
+
assert exit_code == EXIT_FALLTHROUGH
|
|
178
|
+
assert captured.out.strip() == STDOUT_FALLTHROUGH_TEMPLATE.format(
|
|
179
|
+
reason=REASON_CLAUDE_DEV_ENV_CONFIG_MISSING
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def test_config_missing_role_agent_falls_through(
|
|
184
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
185
|
+
) -> None:
|
|
186
|
+
claude_home = tmp_path / "claude"
|
|
187
|
+
claude_home.mkdir()
|
|
188
|
+
(claude_home / MANIFEST_FILENAME).write_text("{}", encoding=UTF8_ENCODING)
|
|
189
|
+
agents_directory = claude_home / AGENTS_SUBDIRECTORY
|
|
190
|
+
agents_directory.mkdir()
|
|
191
|
+
(agents_directory / "code-quality-agent.md").write_text(
|
|
192
|
+
"# present\n", encoding=UTF8_ENCODING
|
|
193
|
+
)
|
|
194
|
+
run_state_directory = tmp_path / "run"
|
|
195
|
+
run_state_directory.mkdir()
|
|
196
|
+
recorder = _Recorder(
|
|
197
|
+
[_completed([GROK_BINARY_NAME, MODELS_SUBCOMMAND], 0, stdout="logged in")]
|
|
198
|
+
)
|
|
199
|
+
monkeypatch.setattr(preflight, "claude_config_home", lambda: claude_home)
|
|
200
|
+
monkeypatch.setattr(preflight, "preflight_which", lambda name: f"/fake/bin/{name}")
|
|
201
|
+
monkeypatch.setattr(preflight, "preflight_subprocess_runner", recorder)
|
|
202
|
+
|
|
203
|
+
exit_code = preflight.main(
|
|
204
|
+
[
|
|
205
|
+
CLI_ROLE_FLAG,
|
|
206
|
+
ROLE_BUGTEAM,
|
|
207
|
+
CLI_RUN_STATE_DIR_FLAG,
|
|
208
|
+
str(run_state_directory),
|
|
209
|
+
]
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
captured = capsys.readouterr()
|
|
213
|
+
assert exit_code == EXIT_FALLTHROUGH
|
|
214
|
+
assert captured.out.strip() == STDOUT_FALLTHROUGH_TEMPLATE.format(
|
|
215
|
+
reason=REASON_CLAUDE_DEV_ENV_CONFIG_MISSING
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def test_ok_path_without_ping(
|
|
220
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
221
|
+
) -> None:
|
|
222
|
+
claude_home = tmp_path / "claude"
|
|
223
|
+
run_state_directory = tmp_path / "run"
|
|
224
|
+
run_state_directory.mkdir()
|
|
225
|
+
recorder = _Recorder(
|
|
226
|
+
[_completed([GROK_BINARY_NAME, MODELS_SUBCOMMAND], 0, stdout="logged in")]
|
|
227
|
+
)
|
|
228
|
+
_install_ok_static_seams(monkeypatch, claude_home, recorder)
|
|
229
|
+
|
|
230
|
+
exit_code = preflight.main([CLI_RUN_STATE_DIR_FLAG, str(run_state_directory)])
|
|
231
|
+
|
|
232
|
+
captured = capsys.readouterr()
|
|
233
|
+
assert exit_code == EXIT_USABLE
|
|
234
|
+
assert captured.out.strip() == STDOUT_OK_LINE
|
|
235
|
+
assert len(recorder.invocations) == 1
|
|
236
|
+
assert MODELS_SUBCOMMAND in recorder.invocations[0]
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def test_ping_cache_miss_invokes_live_call_and_writes_cache(
|
|
240
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
241
|
+
) -> None:
|
|
242
|
+
claude_home = tmp_path / "claude"
|
|
243
|
+
run_state_directory = tmp_path / "run"
|
|
244
|
+
run_state_directory.mkdir()
|
|
245
|
+
fixed_now = 1_700_000_000.0
|
|
246
|
+
recorder = _Recorder(
|
|
247
|
+
[
|
|
248
|
+
_completed([GROK_BINARY_NAME, MODELS_SUBCOMMAND], 0, stdout="logged in"),
|
|
249
|
+
_completed([GROK_BINARY_NAME, SINGLE_TURN_FLAG], 0, stdout="ok"),
|
|
250
|
+
]
|
|
251
|
+
)
|
|
252
|
+
_install_ok_static_seams(monkeypatch, claude_home, recorder)
|
|
253
|
+
monkeypatch.setattr(preflight, "preflight_time", lambda: fixed_now)
|
|
254
|
+
|
|
255
|
+
exit_code = preflight.main(
|
|
256
|
+
[
|
|
257
|
+
CLI_PING_FLAG,
|
|
258
|
+
CLI_RUN_STATE_DIR_FLAG,
|
|
259
|
+
str(run_state_directory),
|
|
260
|
+
]
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
captured = capsys.readouterr()
|
|
264
|
+
assert exit_code == EXIT_USABLE
|
|
265
|
+
assert captured.out.strip() == STDOUT_OK_LINE
|
|
266
|
+
assert len(recorder.invocations) == 2
|
|
267
|
+
ping_invocation = recorder.invocations[1]
|
|
268
|
+
assert SINGLE_TURN_FLAG in ping_invocation
|
|
269
|
+
assert PING_PROMPT in ping_invocation
|
|
270
|
+
assert PING_MAX_TURNS in ping_invocation
|
|
271
|
+
assert LEADER_SOCKET_FLAG in ping_invocation
|
|
272
|
+
ping_socket = Path(ping_invocation[ping_invocation.index(LEADER_SOCKET_FLAG) + 1])
|
|
273
|
+
assert ping_socket.name == PING_LEADER_SOCKET_FILENAME
|
|
274
|
+
cache_path = run_state_directory / PING_CACHE_FILENAME
|
|
275
|
+
all_cache_payload = json.loads(cache_path.read_text(encoding=UTF8_ENCODING))
|
|
276
|
+
assert all_cache_payload[PING_CACHE_IS_OK_KEY] is True
|
|
277
|
+
assert all_cache_payload[PING_CACHE_CHECKED_AT_KEY] == fixed_now
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def test_ping_cache_hit_skips_live_call(
|
|
281
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
282
|
+
) -> None:
|
|
283
|
+
claude_home = tmp_path / "claude"
|
|
284
|
+
run_state_directory = tmp_path / "run"
|
|
285
|
+
run_state_directory.mkdir()
|
|
286
|
+
fixed_now = 1_700_000_100.0
|
|
287
|
+
cache_path = run_state_directory / PING_CACHE_FILENAME
|
|
288
|
+
cache_path.write_text(
|
|
289
|
+
json.dumps(
|
|
290
|
+
{
|
|
291
|
+
PING_CACHE_CHECKED_AT_KEY: fixed_now - 10.0,
|
|
292
|
+
PING_CACHE_IS_OK_KEY: True,
|
|
293
|
+
}
|
|
294
|
+
),
|
|
295
|
+
encoding=UTF8_ENCODING,
|
|
296
|
+
)
|
|
297
|
+
recorder = _Recorder(
|
|
298
|
+
[_completed([GROK_BINARY_NAME, MODELS_SUBCOMMAND], 0, stdout="logged in")]
|
|
299
|
+
)
|
|
300
|
+
_install_ok_static_seams(monkeypatch, claude_home, recorder)
|
|
301
|
+
monkeypatch.setattr(preflight, "preflight_time", lambda: fixed_now)
|
|
302
|
+
|
|
303
|
+
exit_code = preflight.main(
|
|
304
|
+
[
|
|
305
|
+
CLI_PING_FLAG,
|
|
306
|
+
CLI_RUN_STATE_DIR_FLAG,
|
|
307
|
+
str(run_state_directory),
|
|
308
|
+
]
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
captured = capsys.readouterr()
|
|
312
|
+
assert exit_code == EXIT_USABLE
|
|
313
|
+
assert captured.out.strip() == STDOUT_OK_LINE
|
|
314
|
+
assert len(recorder.invocations) == 1
|
|
315
|
+
assert MODELS_SUBCOMMAND in recorder.invocations[0]
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def test_ping_cache_ttl_expired_reinvokes_live_call(
|
|
319
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
320
|
+
) -> None:
|
|
321
|
+
claude_home = tmp_path / "claude"
|
|
322
|
+
run_state_directory = tmp_path / "run"
|
|
323
|
+
run_state_directory.mkdir()
|
|
324
|
+
fixed_now = 1_700_000_000.0 + PING_TTL_SECONDS + 1.0
|
|
325
|
+
cache_path = run_state_directory / PING_CACHE_FILENAME
|
|
326
|
+
cache_path.write_text(
|
|
327
|
+
json.dumps(
|
|
328
|
+
{
|
|
329
|
+
PING_CACHE_CHECKED_AT_KEY: 1_700_000_000.0,
|
|
330
|
+
PING_CACHE_IS_OK_KEY: True,
|
|
331
|
+
}
|
|
332
|
+
),
|
|
333
|
+
encoding=UTF8_ENCODING,
|
|
334
|
+
)
|
|
335
|
+
recorder = _Recorder(
|
|
336
|
+
[
|
|
337
|
+
_completed([GROK_BINARY_NAME, MODELS_SUBCOMMAND], 0, stdout="logged in"),
|
|
338
|
+
_completed([GROK_BINARY_NAME, SINGLE_TURN_FLAG], 0, stdout="ok"),
|
|
339
|
+
]
|
|
340
|
+
)
|
|
341
|
+
_install_ok_static_seams(monkeypatch, claude_home, recorder)
|
|
342
|
+
monkeypatch.setattr(preflight, "preflight_time", lambda: fixed_now)
|
|
343
|
+
|
|
344
|
+
exit_code = preflight.main(
|
|
345
|
+
[
|
|
346
|
+
CLI_PING_FLAG,
|
|
347
|
+
CLI_RUN_STATE_DIR_FLAG,
|
|
348
|
+
str(run_state_directory),
|
|
349
|
+
]
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
captured = capsys.readouterr()
|
|
353
|
+
assert exit_code == EXIT_USABLE
|
|
354
|
+
assert captured.out.strip() == STDOUT_OK_LINE
|
|
355
|
+
assert len(recorder.invocations) == 2
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def test_auth_failure_invalidates_ping_cache(
|
|
359
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
360
|
+
) -> None:
|
|
361
|
+
claude_home = tmp_path / "claude"
|
|
362
|
+
run_state_directory = tmp_path / "run"
|
|
363
|
+
run_state_directory.mkdir()
|
|
364
|
+
cache_path = run_state_directory / PING_CACHE_FILENAME
|
|
365
|
+
cache_path.write_text(
|
|
366
|
+
json.dumps(
|
|
367
|
+
{
|
|
368
|
+
PING_CACHE_CHECKED_AT_KEY: 1_700_000_000.0,
|
|
369
|
+
PING_CACHE_IS_OK_KEY: True,
|
|
370
|
+
}
|
|
371
|
+
),
|
|
372
|
+
encoding=UTF8_ENCODING,
|
|
373
|
+
)
|
|
374
|
+
recorder = _Recorder(
|
|
375
|
+
[
|
|
376
|
+
_completed(
|
|
377
|
+
[GROK_BINARY_NAME, MODELS_SUBCOMMAND],
|
|
378
|
+
1,
|
|
379
|
+
stderr="not logged in",
|
|
380
|
+
)
|
|
381
|
+
]
|
|
382
|
+
)
|
|
383
|
+
_install_ok_static_seams(monkeypatch, claude_home, recorder)
|
|
384
|
+
|
|
385
|
+
exit_code = preflight.main(
|
|
386
|
+
[
|
|
387
|
+
CLI_PING_FLAG,
|
|
388
|
+
CLI_RUN_STATE_DIR_FLAG,
|
|
389
|
+
str(run_state_directory),
|
|
390
|
+
]
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
captured = capsys.readouterr()
|
|
394
|
+
assert exit_code == EXIT_FALLTHROUGH
|
|
395
|
+
assert captured.out.strip() == STDOUT_FALLTHROUGH_TEMPLATE.format(
|
|
396
|
+
reason=REASON_GROK_AUTH_FAILED
|
|
397
|
+
)
|
|
398
|
+
assert not cache_path.exists()
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def test_ping_usage_exhaustion_is_distinct_reason(
|
|
402
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
403
|
+
) -> None:
|
|
404
|
+
claude_home = tmp_path / "claude"
|
|
405
|
+
run_state_directory = tmp_path / "run"
|
|
406
|
+
run_state_directory.mkdir()
|
|
407
|
+
recorder = _Recorder(
|
|
408
|
+
[
|
|
409
|
+
_completed([GROK_BINARY_NAME, MODELS_SUBCOMMAND], 0, stdout="logged in"),
|
|
410
|
+
_completed(
|
|
411
|
+
[GROK_BINARY_NAME, SINGLE_TURN_FLAG],
|
|
412
|
+
1,
|
|
413
|
+
stderr=ALL_USAGE_EXHAUSTION_SIGNATURES[0],
|
|
414
|
+
),
|
|
415
|
+
]
|
|
416
|
+
)
|
|
417
|
+
_install_ok_static_seams(monkeypatch, claude_home, recorder)
|
|
418
|
+
monkeypatch.setattr(preflight, "preflight_time", lambda: 1_700_000_000.0)
|
|
419
|
+
|
|
420
|
+
exit_code = preflight.main(
|
|
421
|
+
[
|
|
422
|
+
CLI_PING_FLAG,
|
|
423
|
+
CLI_RUN_STATE_DIR_FLAG,
|
|
424
|
+
str(run_state_directory),
|
|
425
|
+
]
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
captured = capsys.readouterr()
|
|
429
|
+
assert exit_code == EXIT_FALLTHROUGH
|
|
430
|
+
assert captured.out.strip() == STDOUT_FALLTHROUGH_TEMPLATE.format(
|
|
431
|
+
reason=REASON_GROK_USAGE_EXHAUSTED
|
|
432
|
+
)
|
|
433
|
+
assert not (run_state_directory / PING_CACHE_FILENAME).exists()
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def test_ping_auth_failure_is_auth_reason_not_usage(
|
|
437
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
438
|
+
) -> None:
|
|
439
|
+
claude_home = tmp_path / "claude"
|
|
440
|
+
run_state_directory = tmp_path / "run"
|
|
441
|
+
run_state_directory.mkdir()
|
|
442
|
+
recorder = _Recorder(
|
|
443
|
+
[
|
|
444
|
+
_completed([GROK_BINARY_NAME, MODELS_SUBCOMMAND], 0, stdout="logged in"),
|
|
445
|
+
_completed(
|
|
446
|
+
[GROK_BINARY_NAME, SINGLE_TURN_FLAG],
|
|
447
|
+
1,
|
|
448
|
+
stderr=ALL_AUTH_FAILURE_SIGNATURES[0],
|
|
449
|
+
),
|
|
450
|
+
]
|
|
451
|
+
)
|
|
452
|
+
_install_ok_static_seams(monkeypatch, claude_home, recorder)
|
|
453
|
+
monkeypatch.setattr(preflight, "preflight_time", lambda: 1_700_000_000.0)
|
|
454
|
+
|
|
455
|
+
exit_code = preflight.main(
|
|
456
|
+
[
|
|
457
|
+
CLI_PING_FLAG,
|
|
458
|
+
CLI_RUN_STATE_DIR_FLAG,
|
|
459
|
+
str(run_state_directory),
|
|
460
|
+
]
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
captured = capsys.readouterr()
|
|
464
|
+
assert exit_code == EXIT_FALLTHROUGH
|
|
465
|
+
assert captured.out.strip() == STDOUT_FALLTHROUGH_TEMPLATE.format(
|
|
466
|
+
reason=REASON_GROK_AUTH_FAILED
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def test_reason_strings_are_pinned_to_constants() -> None:
|
|
471
|
+
assert REASON_GROK_BINARY_MISSING == "grok_binary_missing"
|
|
472
|
+
assert REASON_GROK_AUTH_FAILED == "grok_auth_failed"
|
|
473
|
+
assert REASON_CLAUDE_DEV_ENV_CONFIG_MISSING == "claude_dev_env_config_missing"
|
|
474
|
+
assert REASON_GROK_USAGE_EXHAUSTED == "grok_usage_exhausted"
|
|
475
|
+
assert STDOUT_OK_LINE == "grok_preflight: ok"
|
|
476
|
+
assert "reason=" in STDOUT_FALLTHROUGH_TEMPLATE
|
|
477
|
+
assert DEFAULT_ROLE == ROLE_BUGTEAM
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def test_claude_config_home_points_at_user_claude_home() -> None:
|
|
481
|
+
config_home = preflight.claude_config_home()
|
|
482
|
+
assert config_home.name == ".claude"
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def test_run_preflight_binary_missing_returns_fallthrough(
|
|
486
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
487
|
+
) -> None:
|
|
488
|
+
claude_home = tmp_path / "claude"
|
|
489
|
+
run_state_directory = tmp_path / "run"
|
|
490
|
+
run_state_directory.mkdir()
|
|
491
|
+
_write_install_layout(claude_home)
|
|
492
|
+
monkeypatch.setattr(preflight, "claude_config_home", lambda: claude_home)
|
|
493
|
+
monkeypatch.setattr(preflight, "preflight_which", lambda _name: None)
|
|
494
|
+
|
|
495
|
+
outcome = preflight.run_preflight(
|
|
496
|
+
role=ROLE_BUGTEAM,
|
|
497
|
+
should_ping=False,
|
|
498
|
+
run_state_directory=run_state_directory,
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
assert outcome.is_usable is False
|
|
502
|
+
assert outcome.reason == REASON_GROK_BINARY_MISSING
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def test_run_preflight_auth_failure_invalidates_cache_and_returns_reason(
|
|
506
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
507
|
+
) -> None:
|
|
508
|
+
claude_home = tmp_path / "claude"
|
|
509
|
+
run_state_directory = tmp_path / "run"
|
|
510
|
+
run_state_directory.mkdir()
|
|
511
|
+
cache_path = run_state_directory / PING_CACHE_FILENAME
|
|
512
|
+
cache_path.write_text(
|
|
513
|
+
json.dumps(
|
|
514
|
+
{
|
|
515
|
+
PING_CACHE_CHECKED_AT_KEY: 1_700_000_000.0,
|
|
516
|
+
PING_CACHE_IS_OK_KEY: True,
|
|
517
|
+
}
|
|
518
|
+
),
|
|
519
|
+
encoding=UTF8_ENCODING,
|
|
520
|
+
)
|
|
521
|
+
recorder = _Recorder(
|
|
522
|
+
[
|
|
523
|
+
_completed(
|
|
524
|
+
[GROK_BINARY_NAME, MODELS_SUBCOMMAND],
|
|
525
|
+
1,
|
|
526
|
+
stderr=ALL_AUTH_FAILURE_SIGNATURES[0],
|
|
527
|
+
)
|
|
528
|
+
]
|
|
529
|
+
)
|
|
530
|
+
_install_ok_static_seams(monkeypatch, claude_home, recorder)
|
|
531
|
+
|
|
532
|
+
outcome = preflight.run_preflight(
|
|
533
|
+
role=ROLE_BUGTEAM,
|
|
534
|
+
should_ping=False,
|
|
535
|
+
run_state_directory=run_state_directory,
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
assert outcome.is_usable is False
|
|
539
|
+
assert outcome.reason == REASON_GROK_AUTH_FAILED
|
|
540
|
+
assert not cache_path.exists()
|
|
541
|
+
assert MODELS_SUBCOMMAND in recorder.invocations[0]
|
|
542
|
+
leader_socket_path = Path(
|
|
543
|
+
recorder.invocations[0][recorder.invocations[0].index(LEADER_SOCKET_FLAG) + 1]
|
|
544
|
+
)
|
|
545
|
+
assert leader_socket_path.name == AUTH_LEADER_SOCKET_FILENAME
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def test_run_preflight_config_missing_returns_fallthrough(
|
|
549
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
550
|
+
) -> None:
|
|
551
|
+
claude_home = tmp_path / "claude"
|
|
552
|
+
claude_home.mkdir()
|
|
553
|
+
run_state_directory = tmp_path / "run"
|
|
554
|
+
run_state_directory.mkdir()
|
|
555
|
+
recorder = _Recorder(
|
|
556
|
+
[_completed([GROK_BINARY_NAME, MODELS_SUBCOMMAND], 0, stdout="logged in")]
|
|
557
|
+
)
|
|
558
|
+
monkeypatch.setattr(preflight, "claude_config_home", lambda: claude_home)
|
|
559
|
+
monkeypatch.setattr(preflight, "preflight_which", lambda name: f"/fake/bin/{name}")
|
|
560
|
+
monkeypatch.setattr(preflight, "preflight_subprocess_runner", recorder)
|
|
561
|
+
|
|
562
|
+
outcome = preflight.run_preflight(
|
|
563
|
+
role=ROLE_BUGTEAM,
|
|
564
|
+
should_ping=False,
|
|
565
|
+
run_state_directory=run_state_directory,
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
assert outcome.is_usable is False
|
|
569
|
+
assert outcome.reason == REASON_CLAUDE_DEV_ENV_CONFIG_MISSING
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def test_run_preflight_ok_without_ping_returns_usable(
|
|
573
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
574
|
+
) -> None:
|
|
575
|
+
claude_home = tmp_path / "claude"
|
|
576
|
+
run_state_directory = tmp_path / "run"
|
|
577
|
+
run_state_directory.mkdir()
|
|
578
|
+
recorder = _Recorder(
|
|
579
|
+
[_completed([GROK_BINARY_NAME, MODELS_SUBCOMMAND], 0, stdout="logged in")]
|
|
580
|
+
)
|
|
581
|
+
_install_ok_static_seams(monkeypatch, claude_home, recorder)
|
|
582
|
+
|
|
583
|
+
outcome = preflight.run_preflight(
|
|
584
|
+
role=ROLE_BUGTEAM,
|
|
585
|
+
should_ping=False,
|
|
586
|
+
run_state_directory=run_state_directory,
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
assert outcome.is_usable is True
|
|
590
|
+
assert outcome.reason is None
|
|
591
|
+
assert len(recorder.invocations) == 1
|
|
592
|
+
assert MODELS_SUBCOMMAND in recorder.invocations[0]
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def test_run_preflight_ping_cache_miss_writes_cache_and_returns_usable(
|
|
596
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
597
|
+
) -> None:
|
|
598
|
+
claude_home = tmp_path / "claude"
|
|
599
|
+
run_state_directory = tmp_path / "run"
|
|
600
|
+
run_state_directory.mkdir()
|
|
601
|
+
fixed_now = 1_700_000_000.0
|
|
602
|
+
recorder = _Recorder(
|
|
603
|
+
[
|
|
604
|
+
_completed([GROK_BINARY_NAME, MODELS_SUBCOMMAND], 0, stdout="logged in"),
|
|
605
|
+
_completed([GROK_BINARY_NAME, SINGLE_TURN_FLAG], 0, stdout="ok"),
|
|
606
|
+
]
|
|
607
|
+
)
|
|
608
|
+
_install_ok_static_seams(monkeypatch, claude_home, recorder)
|
|
609
|
+
monkeypatch.setattr(preflight, "preflight_time", lambda: fixed_now)
|
|
610
|
+
|
|
611
|
+
outcome = preflight.run_preflight(
|
|
612
|
+
role=ROLE_BUGTEAM,
|
|
613
|
+
should_ping=True,
|
|
614
|
+
run_state_directory=run_state_directory,
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
assert outcome.is_usable is True
|
|
618
|
+
assert outcome.reason is None
|
|
619
|
+
assert len(recorder.invocations) == 2
|
|
620
|
+
ping_invocation = recorder.invocations[1]
|
|
621
|
+
assert SINGLE_TURN_FLAG in ping_invocation
|
|
622
|
+
assert PING_PROMPT in ping_invocation
|
|
623
|
+
ping_socket = Path(ping_invocation[ping_invocation.index(LEADER_SOCKET_FLAG) + 1])
|
|
624
|
+
assert ping_socket.name == PING_LEADER_SOCKET_FILENAME
|
|
625
|
+
cache_path = run_state_directory / PING_CACHE_FILENAME
|
|
626
|
+
all_cache_payload = json.loads(cache_path.read_text(encoding=UTF8_ENCODING))
|
|
627
|
+
assert all_cache_payload[PING_CACHE_IS_OK_KEY] is True
|
|
628
|
+
assert all_cache_payload[PING_CACHE_CHECKED_AT_KEY] == fixed_now
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def test_run_preflight_ping_cache_hit_skips_live_call(
|
|
632
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
633
|
+
) -> None:
|
|
634
|
+
claude_home = tmp_path / "claude"
|
|
635
|
+
run_state_directory = tmp_path / "run"
|
|
636
|
+
run_state_directory.mkdir()
|
|
637
|
+
fixed_now = 1_700_000_100.0
|
|
638
|
+
cache_path = run_state_directory / PING_CACHE_FILENAME
|
|
639
|
+
cache_path.write_text(
|
|
640
|
+
json.dumps(
|
|
641
|
+
{
|
|
642
|
+
PING_CACHE_CHECKED_AT_KEY: fixed_now - 10.0,
|
|
643
|
+
PING_CACHE_IS_OK_KEY: True,
|
|
644
|
+
}
|
|
645
|
+
),
|
|
646
|
+
encoding=UTF8_ENCODING,
|
|
647
|
+
)
|
|
648
|
+
recorder = _Recorder(
|
|
649
|
+
[_completed([GROK_BINARY_NAME, MODELS_SUBCOMMAND], 0, stdout="logged in")]
|
|
650
|
+
)
|
|
651
|
+
_install_ok_static_seams(monkeypatch, claude_home, recorder)
|
|
652
|
+
monkeypatch.setattr(preflight, "preflight_time", lambda: fixed_now)
|
|
653
|
+
|
|
654
|
+
outcome = preflight.run_preflight(
|
|
655
|
+
role=ROLE_BUGTEAM,
|
|
656
|
+
should_ping=True,
|
|
657
|
+
run_state_directory=run_state_directory,
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
assert outcome.is_usable is True
|
|
661
|
+
assert outcome.reason is None
|
|
662
|
+
assert len(recorder.invocations) == 1
|
|
663
|
+
assert MODELS_SUBCOMMAND in recorder.invocations[0]
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def test_run_preflight_ping_usage_exhaustion_returns_distinct_reason(
|
|
667
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
668
|
+
) -> None:
|
|
669
|
+
claude_home = tmp_path / "claude"
|
|
670
|
+
run_state_directory = tmp_path / "run"
|
|
671
|
+
run_state_directory.mkdir()
|
|
672
|
+
recorder = _Recorder(
|
|
673
|
+
[
|
|
674
|
+
_completed([GROK_BINARY_NAME, MODELS_SUBCOMMAND], 0, stdout="logged in"),
|
|
675
|
+
_completed(
|
|
676
|
+
[GROK_BINARY_NAME, SINGLE_TURN_FLAG],
|
|
677
|
+
1,
|
|
678
|
+
stderr=ALL_USAGE_EXHAUSTION_SIGNATURES[0],
|
|
679
|
+
),
|
|
680
|
+
]
|
|
681
|
+
)
|
|
682
|
+
_install_ok_static_seams(monkeypatch, claude_home, recorder)
|
|
683
|
+
monkeypatch.setattr(preflight, "preflight_time", lambda: 1_700_000_000.0)
|
|
684
|
+
|
|
685
|
+
outcome = preflight.run_preflight(
|
|
686
|
+
role=ROLE_BUGTEAM,
|
|
687
|
+
should_ping=True,
|
|
688
|
+
run_state_directory=run_state_directory,
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
assert outcome.is_usable is False
|
|
692
|
+
assert outcome.reason == REASON_GROK_USAGE_EXHAUSTED
|
|
693
|
+
assert not (run_state_directory / PING_CACHE_FILENAME).exists()
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def test_run_preflight_ping_auth_failure_invalidates_cache(
|
|
697
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
698
|
+
) -> None:
|
|
699
|
+
claude_home = tmp_path / "claude"
|
|
700
|
+
run_state_directory = tmp_path / "run"
|
|
701
|
+
run_state_directory.mkdir()
|
|
702
|
+
cache_path = run_state_directory / PING_CACHE_FILENAME
|
|
703
|
+
cache_path.write_text(
|
|
704
|
+
json.dumps(
|
|
705
|
+
{
|
|
706
|
+
PING_CACHE_CHECKED_AT_KEY: 1_700_000_000.0,
|
|
707
|
+
PING_CACHE_IS_OK_KEY: True,
|
|
708
|
+
}
|
|
709
|
+
),
|
|
710
|
+
encoding=UTF8_ENCODING,
|
|
711
|
+
)
|
|
712
|
+
recorder = _Recorder(
|
|
713
|
+
[
|
|
714
|
+
_completed([GROK_BINARY_NAME, MODELS_SUBCOMMAND], 0, stdout="logged in"),
|
|
715
|
+
_completed(
|
|
716
|
+
[GROK_BINARY_NAME, SINGLE_TURN_FLAG],
|
|
717
|
+
1,
|
|
718
|
+
stderr=ALL_AUTH_FAILURE_SIGNATURES[0],
|
|
719
|
+
),
|
|
720
|
+
]
|
|
721
|
+
)
|
|
722
|
+
_install_ok_static_seams(monkeypatch, claude_home, recorder)
|
|
723
|
+
monkeypatch.setattr(
|
|
724
|
+
preflight, "preflight_time", lambda: 1_700_000_000.0 + PING_TTL_SECONDS + 1.0
|
|
725
|
+
)
|
|
726
|
+
|
|
727
|
+
outcome = preflight.run_preflight(
|
|
728
|
+
role=ROLE_BUGTEAM,
|
|
729
|
+
should_ping=True,
|
|
730
|
+
run_state_directory=run_state_directory,
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
assert outcome.is_usable is False
|
|
734
|
+
assert outcome.reason == REASON_GROK_AUTH_FAILED
|
|
735
|
+
assert not cache_path.exists()
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def test_run_preflight_ping_missing_binary_invalidates_cache(
|
|
739
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
740
|
+
) -> None:
|
|
741
|
+
claude_home = tmp_path / "claude"
|
|
742
|
+
run_state_directory = tmp_path / "run"
|
|
743
|
+
run_state_directory.mkdir()
|
|
744
|
+
cache_path = run_state_directory / PING_CACHE_FILENAME
|
|
745
|
+
cache_path.write_text(
|
|
746
|
+
json.dumps(
|
|
747
|
+
{
|
|
748
|
+
PING_CACHE_CHECKED_AT_KEY: 1_700_000_000.0,
|
|
749
|
+
PING_CACHE_IS_OK_KEY: True,
|
|
750
|
+
}
|
|
751
|
+
),
|
|
752
|
+
encoding=UTF8_ENCODING,
|
|
753
|
+
)
|
|
754
|
+
all_completions = [
|
|
755
|
+
_completed([GROK_BINARY_NAME, MODELS_SUBCOMMAND], 0, stdout="logged in"),
|
|
756
|
+
]
|
|
757
|
+
|
|
758
|
+
def _raise_on_ping(
|
|
759
|
+
invocation: list[str], **_keyword_arguments: object
|
|
760
|
+
) -> subprocess.CompletedProcess[str]:
|
|
761
|
+
if SINGLE_TURN_FLAG in invocation:
|
|
762
|
+
raise FileNotFoundError(GROK_BINARY_NAME)
|
|
763
|
+
if not all_completions:
|
|
764
|
+
raise AssertionError(f"unexpected invocation: {invocation}")
|
|
765
|
+
return all_completions.pop(0)
|
|
766
|
+
|
|
767
|
+
_write_install_layout(claude_home)
|
|
768
|
+
monkeypatch.setattr(preflight, "claude_config_home", lambda: claude_home)
|
|
769
|
+
monkeypatch.setattr(preflight, "preflight_which", lambda name: f"/fake/bin/{name}")
|
|
770
|
+
monkeypatch.setattr(preflight, "preflight_subprocess_runner", _raise_on_ping)
|
|
771
|
+
monkeypatch.setattr(
|
|
772
|
+
preflight, "preflight_time", lambda: 1_700_000_000.0 + PING_TTL_SECONDS + 1.0
|
|
773
|
+
)
|
|
774
|
+
|
|
775
|
+
outcome = preflight.run_preflight(
|
|
776
|
+
role=ROLE_BUGTEAM,
|
|
777
|
+
should_ping=True,
|
|
778
|
+
run_state_directory=run_state_directory,
|
|
779
|
+
)
|
|
780
|
+
|
|
781
|
+
assert outcome.is_usable is False
|
|
782
|
+
assert outcome.reason == REASON_GROK_AUTH_FAILED
|
|
783
|
+
assert not cache_path.exists()
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
def test_main_uncreatable_run_state_dir_falls_through_without_crashing(
|
|
787
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
788
|
+
) -> None:
|
|
789
|
+
"""An uncreatable run-state dir yields a parseable fallthrough line, not a crash."""
|
|
790
|
+
blocker_file = tmp_path / "blocker"
|
|
791
|
+
blocker_file.write_text("x", encoding=UTF8_ENCODING)
|
|
792
|
+
run_state_directory = blocker_file / "run"
|
|
793
|
+
monkeypatch.setattr(preflight, "preflight_which", lambda _name: None)
|
|
794
|
+
|
|
795
|
+
exit_code = preflight.main([CLI_RUN_STATE_DIR_FLAG, str(run_state_directory)])
|
|
796
|
+
|
|
797
|
+
captured = capsys.readouterr()
|
|
798
|
+
assert exit_code == EXIT_FALLTHROUGH
|
|
799
|
+
assert captured.out.strip() == STDOUT_FALLTHROUGH_TEMPLATE.format(
|
|
800
|
+
reason=REASON_GROK_BINARY_MISSING
|
|
801
|
+
)
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
def test_auth_returncode_zero_with_none_stdout_falls_through(
|
|
805
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
806
|
+
) -> None:
|
|
807
|
+
"""returncode 0 with undecoded None streams must not report usable."""
|
|
808
|
+
claude_home = tmp_path / "claude"
|
|
809
|
+
run_state_directory = tmp_path / "run"
|
|
810
|
+
run_state_directory.mkdir()
|
|
811
|
+
none_stream_completion = subprocess.CompletedProcess(
|
|
812
|
+
args=[GROK_BINARY_NAME, MODELS_SUBCOMMAND],
|
|
813
|
+
returncode=0,
|
|
814
|
+
stdout=None,
|
|
815
|
+
stderr=None,
|
|
816
|
+
)
|
|
817
|
+
recorder = _Recorder([none_stream_completion])
|
|
818
|
+
_install_ok_static_seams(monkeypatch, claude_home, recorder)
|
|
819
|
+
|
|
820
|
+
outcome = preflight.run_preflight(
|
|
821
|
+
role=ROLE_BUGTEAM,
|
|
822
|
+
should_ping=False,
|
|
823
|
+
run_state_directory=run_state_directory,
|
|
824
|
+
)
|
|
825
|
+
|
|
826
|
+
assert outcome.is_usable is False
|
|
827
|
+
assert outcome.reason == REASON_GROK_AUTH_FAILED
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
def test_run_grok_command_passes_utf8_decode_errors_replace(
|
|
831
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
832
|
+
) -> None:
|
|
833
|
+
"""Subprocess text mode must use errors=replace so invalid UTF-8 never yields None."""
|
|
834
|
+
captured_keyword_arguments: dict[str, object] = {}
|
|
835
|
+
|
|
836
|
+
def _capture_runner(
|
|
837
|
+
invocation: list[str], **keyword_arguments: object
|
|
838
|
+
) -> subprocess.CompletedProcess[str]:
|
|
839
|
+
captured_keyword_arguments.update(keyword_arguments)
|
|
840
|
+
return _completed(invocation, 0, stdout="logged in")
|
|
841
|
+
|
|
842
|
+
claude_home = tmp_path / "claude"
|
|
843
|
+
run_state_directory = tmp_path / "run"
|
|
844
|
+
run_state_directory.mkdir()
|
|
845
|
+
_write_install_layout(claude_home)
|
|
846
|
+
monkeypatch.setattr(preflight, "claude_config_home", lambda: claude_home)
|
|
847
|
+
monkeypatch.setattr(preflight, "preflight_which", lambda name: f"/fake/bin/{name}")
|
|
848
|
+
monkeypatch.setattr(preflight, "preflight_subprocess_runner", _capture_runner)
|
|
849
|
+
|
|
850
|
+
outcome = preflight.run_preflight(
|
|
851
|
+
role=ROLE_BUGTEAM,
|
|
852
|
+
should_ping=False,
|
|
853
|
+
run_state_directory=run_state_directory,
|
|
854
|
+
)
|
|
855
|
+
|
|
856
|
+
assert outcome.is_usable is True
|
|
857
|
+
assert captured_keyword_arguments.get("encoding") == UTF8_ENCODING
|
|
858
|
+
assert captured_keyword_arguments.get("errors") == UTF8_DECODE_ERRORS
|
|
859
|
+
assert UTF8_DECODE_ERRORS == "replace"
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
def test_run_preflight_mkdirs_missing_run_state_directory_for_cache(
|
|
863
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
864
|
+
) -> None:
|
|
865
|
+
"""Library path creates the run state directory so ping cache write succeeds."""
|
|
866
|
+
claude_home = tmp_path / "claude"
|
|
867
|
+
run_state_directory = tmp_path / "missing-run"
|
|
868
|
+
assert not run_state_directory.exists()
|
|
869
|
+
fixed_now = 1_700_000_000.0
|
|
870
|
+
recorder = _Recorder(
|
|
871
|
+
[
|
|
872
|
+
_completed([GROK_BINARY_NAME, MODELS_SUBCOMMAND], 0, stdout="logged in"),
|
|
873
|
+
_completed([GROK_BINARY_NAME, SINGLE_TURN_FLAG], 0, stdout="ok"),
|
|
874
|
+
]
|
|
875
|
+
)
|
|
876
|
+
_install_ok_static_seams(monkeypatch, claude_home, recorder)
|
|
877
|
+
monkeypatch.setattr(preflight, "preflight_time", lambda: fixed_now)
|
|
878
|
+
|
|
879
|
+
outcome = preflight.run_preflight(
|
|
880
|
+
role=ROLE_BUGTEAM,
|
|
881
|
+
should_ping=True,
|
|
882
|
+
run_state_directory=run_state_directory,
|
|
883
|
+
)
|
|
884
|
+
|
|
885
|
+
assert outcome.is_usable is True
|
|
886
|
+
assert outcome.reason is None
|
|
887
|
+
assert run_state_directory.is_dir()
|
|
888
|
+
cache_path = run_state_directory / PING_CACHE_FILENAME
|
|
889
|
+
assert cache_path.is_file()
|
|
890
|
+
all_cache_payload = json.loads(cache_path.read_text(encoding=UTF8_ENCODING))
|
|
891
|
+
assert all_cache_payload[PING_CACHE_IS_OK_KEY] is True
|
|
892
|
+
assert all_cache_payload[PING_CACHE_CHECKED_AT_KEY] == fixed_now
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
def test_ping_cache_write_oserror_still_reports_usable(
|
|
896
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
897
|
+
) -> None:
|
|
898
|
+
"""Soft-gate stays usable when the successful ping cache write fails."""
|
|
899
|
+
claude_home = tmp_path / "claude"
|
|
900
|
+
run_state_directory = tmp_path / "run"
|
|
901
|
+
run_state_directory.mkdir()
|
|
902
|
+
fixed_now = 1_700_000_000.0
|
|
903
|
+
recorder = _Recorder(
|
|
904
|
+
[
|
|
905
|
+
_completed([GROK_BINARY_NAME, MODELS_SUBCOMMAND], 0, stdout="logged in"),
|
|
906
|
+
_completed([GROK_BINARY_NAME, SINGLE_TURN_FLAG], 0, stdout="ok"),
|
|
907
|
+
]
|
|
908
|
+
)
|
|
909
|
+
_install_ok_static_seams(monkeypatch, claude_home, recorder)
|
|
910
|
+
monkeypatch.setattr(preflight, "preflight_time", lambda: fixed_now)
|
|
911
|
+
|
|
912
|
+
def _raise_oserror_on_write(
|
|
913
|
+
self: Path, *_arguments: object, **_keywords: object
|
|
914
|
+
) -> None:
|
|
915
|
+
raise OSError("simulated cache write failure")
|
|
916
|
+
|
|
917
|
+
monkeypatch.setattr(Path, "write_text", _raise_oserror_on_write)
|
|
918
|
+
|
|
919
|
+
outcome = preflight.run_preflight(
|
|
920
|
+
role=ROLE_BUGTEAM,
|
|
921
|
+
should_ping=True,
|
|
922
|
+
run_state_directory=run_state_directory,
|
|
923
|
+
)
|
|
924
|
+
|
|
925
|
+
assert outcome.is_usable is True
|
|
926
|
+
assert outcome.reason is None
|
|
927
|
+
assert not (run_state_directory / PING_CACHE_FILENAME).exists()
|
|
928
|
+
|
|
929
|
+
|
|
930
|
+
def test_auth_timeout_falls_through_as_grok_auth_failed(
|
|
931
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
932
|
+
) -> None:
|
|
933
|
+
"""TimeoutExpired maps to the catch-all non-usage bucket grok_auth_failed."""
|
|
934
|
+
claude_home = tmp_path / "claude"
|
|
935
|
+
run_state_directory = tmp_path / "run"
|
|
936
|
+
run_state_directory.mkdir()
|
|
937
|
+
|
|
938
|
+
def _raise_timeout(
|
|
939
|
+
invocation: list[str], **_keyword_arguments: object
|
|
940
|
+
) -> subprocess.CompletedProcess[str]:
|
|
941
|
+
raise subprocess.TimeoutExpired(cmd=invocation, timeout=1)
|
|
942
|
+
|
|
943
|
+
_write_install_layout(claude_home)
|
|
944
|
+
monkeypatch.setattr(preflight, "claude_config_home", lambda: claude_home)
|
|
945
|
+
monkeypatch.setattr(preflight, "preflight_which", lambda name: f"/fake/bin/{name}")
|
|
946
|
+
monkeypatch.setattr(preflight, "preflight_subprocess_runner", _raise_timeout)
|
|
947
|
+
|
|
948
|
+
outcome = preflight.run_preflight(
|
|
949
|
+
role=ROLE_BUGTEAM,
|
|
950
|
+
should_ping=False,
|
|
951
|
+
run_state_directory=run_state_directory,
|
|
952
|
+
)
|
|
953
|
+
|
|
954
|
+
assert outcome.is_usable is False
|
|
955
|
+
assert outcome.reason == REASON_GROK_AUTH_FAILED
|
|
956
|
+
|
|
957
|
+
|
|
958
|
+
def test_ping_timeout_falls_through_as_grok_auth_failed(
|
|
959
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
960
|
+
) -> None:
|
|
961
|
+
"""Ping TimeoutExpired also uses the catch-all non-usage bucket."""
|
|
962
|
+
claude_home = tmp_path / "claude"
|
|
963
|
+
run_state_directory = tmp_path / "run"
|
|
964
|
+
run_state_directory.mkdir()
|
|
965
|
+
all_completions = [
|
|
966
|
+
_completed([GROK_BINARY_NAME, MODELS_SUBCOMMAND], 0, stdout="logged in"),
|
|
967
|
+
]
|
|
968
|
+
|
|
969
|
+
def _timeout_on_ping(
|
|
970
|
+
invocation: list[str], **_keyword_arguments: object
|
|
971
|
+
) -> subprocess.CompletedProcess[str]:
|
|
972
|
+
if SINGLE_TURN_FLAG in invocation:
|
|
973
|
+
raise subprocess.TimeoutExpired(cmd=invocation, timeout=1)
|
|
974
|
+
if not all_completions:
|
|
975
|
+
raise AssertionError(f"unexpected invocation: {invocation}")
|
|
976
|
+
return all_completions.pop(0)
|
|
977
|
+
|
|
978
|
+
_write_install_layout(claude_home)
|
|
979
|
+
monkeypatch.setattr(preflight, "claude_config_home", lambda: claude_home)
|
|
980
|
+
monkeypatch.setattr(preflight, "preflight_which", lambda name: f"/fake/bin/{name}")
|
|
981
|
+
monkeypatch.setattr(preflight, "preflight_subprocess_runner", _timeout_on_ping)
|
|
982
|
+
monkeypatch.setattr(preflight, "preflight_time", lambda: 1_700_000_000.0)
|
|
983
|
+
|
|
984
|
+
outcome = preflight.run_preflight(
|
|
985
|
+
role=ROLE_BUGTEAM,
|
|
986
|
+
should_ping=True,
|
|
987
|
+
run_state_directory=run_state_directory,
|
|
988
|
+
)
|
|
989
|
+
|
|
990
|
+
assert outcome.is_usable is False
|
|
991
|
+
assert outcome.reason == REASON_GROK_AUTH_FAILED
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
def test_ping_dual_match_prefers_auth_over_usage(
|
|
995
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
996
|
+
) -> None:
|
|
997
|
+
claude_home = tmp_path / "claude"
|
|
998
|
+
run_state_directory = tmp_path / "run"
|
|
999
|
+
run_state_directory.mkdir()
|
|
1000
|
+
dual_match_stderr = "HTTP 401 rate limit exceeded: unauthorized"
|
|
1001
|
+
recorder = _Recorder(
|
|
1002
|
+
[
|
|
1003
|
+
_completed([GROK_BINARY_NAME, MODELS_SUBCOMMAND], 0, stdout="logged in"),
|
|
1004
|
+
_completed(
|
|
1005
|
+
[GROK_BINARY_NAME, SINGLE_TURN_FLAG],
|
|
1006
|
+
1,
|
|
1007
|
+
stderr=dual_match_stderr,
|
|
1008
|
+
),
|
|
1009
|
+
]
|
|
1010
|
+
)
|
|
1011
|
+
_install_ok_static_seams(monkeypatch, claude_home, recorder)
|
|
1012
|
+
monkeypatch.setattr(preflight, "preflight_time", lambda: 1_700_000_000.0)
|
|
1013
|
+
|
|
1014
|
+
outcome = preflight.run_preflight(
|
|
1015
|
+
role=ROLE_BUGTEAM,
|
|
1016
|
+
should_ping=True,
|
|
1017
|
+
run_state_directory=run_state_directory,
|
|
1018
|
+
)
|
|
1019
|
+
|
|
1020
|
+
assert outcome.is_usable is False
|
|
1021
|
+
assert outcome.reason == REASON_GROK_AUTH_FAILED
|
|
1022
|
+
|
|
1023
|
+
|
|
1024
|
+
def test_ping_incidental_credit_and_port_429_are_not_usage_exhaustion(
|
|
1025
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
1026
|
+
) -> None:
|
|
1027
|
+
claude_home = tmp_path / "claude"
|
|
1028
|
+
run_state_directory = tmp_path / "run"
|
|
1029
|
+
run_state_directory.mkdir()
|
|
1030
|
+
incidental_stderr = (
|
|
1031
|
+
"accreditation failed while probing; port 429 refused; quota of tools open"
|
|
1032
|
+
)
|
|
1033
|
+
recorder = _Recorder(
|
|
1034
|
+
[
|
|
1035
|
+
_completed([GROK_BINARY_NAME, MODELS_SUBCOMMAND], 0, stdout="logged in"),
|
|
1036
|
+
_completed(
|
|
1037
|
+
[GROK_BINARY_NAME, SINGLE_TURN_FLAG],
|
|
1038
|
+
1,
|
|
1039
|
+
stderr=incidental_stderr,
|
|
1040
|
+
),
|
|
1041
|
+
]
|
|
1042
|
+
)
|
|
1043
|
+
_install_ok_static_seams(monkeypatch, claude_home, recorder)
|
|
1044
|
+
monkeypatch.setattr(preflight, "preflight_time", lambda: 1_700_000_000.0)
|
|
1045
|
+
|
|
1046
|
+
outcome = preflight.run_preflight(
|
|
1047
|
+
role=ROLE_BUGTEAM,
|
|
1048
|
+
should_ping=True,
|
|
1049
|
+
run_state_directory=run_state_directory,
|
|
1050
|
+
)
|
|
1051
|
+
|
|
1052
|
+
assert outcome.is_usable is False
|
|
1053
|
+
assert outcome.reason == REASON_GROK_AUTH_FAILED
|
|
1054
|
+
assert outcome.reason != REASON_GROK_USAGE_EXHAUSTED
|