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,812 @@
|
|
|
1
|
+
"""Behavioral tests for the headless codex review wrapper."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Callable
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
SCRIPTS_DIRECTORY = Path(__file__).resolve().parent
|
|
14
|
+
if str(SCRIPTS_DIRECTORY) not in sys.path:
|
|
15
|
+
sys.path.insert(0, str(SCRIPTS_DIRECTORY))
|
|
16
|
+
|
|
17
|
+
import run_codex_review as wrapper # noqa: E402
|
|
18
|
+
from codex_review_scripts_constants.run_constants import ( # noqa: E402
|
|
19
|
+
ALL_SHAPE_PROBE_REQUIRED_FLAGS,
|
|
20
|
+
BASE_TARGET_FLAG,
|
|
21
|
+
CODEX_BINARY_NAME,
|
|
22
|
+
CODEX_HOME_ENV_VAR,
|
|
23
|
+
COMMIT_TARGET_FLAG,
|
|
24
|
+
CUSTOM_INSTRUCTIONS_PROMPT,
|
|
25
|
+
DEFAULT_TIMEOUT_SECONDS,
|
|
26
|
+
EXEC_SUBCOMMAND,
|
|
27
|
+
HELP_FLAG,
|
|
28
|
+
JSON_FLAG,
|
|
29
|
+
JSONL_CAPTURE_FILENAME,
|
|
30
|
+
MISSING_BINARY_EXIT_CODE,
|
|
31
|
+
MODEL_FLAG,
|
|
32
|
+
OUTCOME_CLASS_CODEX_DOWN,
|
|
33
|
+
OUTCOME_CLASS_COMPLETED,
|
|
34
|
+
REVIEW_SUBCOMMAND,
|
|
35
|
+
SUBPROCESS_DECODE_EXIT_CODE,
|
|
36
|
+
TIMEOUT_EXIT_CODE,
|
|
37
|
+
UNCOMMITTED_TARGET_FLAG,
|
|
38
|
+
UTF8_ENCODING,
|
|
39
|
+
VERSION_FLAG,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
HELP_TEXT_WITH_TARGETS = (
|
|
43
|
+
"Usage: codex exec review [OPTIONS] [PROMPT]\n"
|
|
44
|
+
" --uncommitted\n"
|
|
45
|
+
" --base <BRANCH>\n"
|
|
46
|
+
" --commit <SHA>\n"
|
|
47
|
+
" --json\n"
|
|
48
|
+
)
|
|
49
|
+
VERSION_STDOUT = "codex-cli 0.144.3\n"
|
|
50
|
+
EXPECTED_BINARY_VERSION = "0.144.3"
|
|
51
|
+
AGENT_MESSAGE_BODY = "No issues found.\n\n```json\n[]\n```"
|
|
52
|
+
CARRIAGE_RETURN_LINE_ENDING = b"\r\n"
|
|
53
|
+
SUCCESS_JSONL = "\n".join(
|
|
54
|
+
[
|
|
55
|
+
json.dumps({"type": "thread.started", "thread_id": "thread-1"}),
|
|
56
|
+
json.dumps({"type": "turn.started"}),
|
|
57
|
+
json.dumps(
|
|
58
|
+
{
|
|
59
|
+
"type": "item.completed",
|
|
60
|
+
"item": {
|
|
61
|
+
"type": "agent_message",
|
|
62
|
+
"text": AGENT_MESSAGE_BODY,
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
),
|
|
66
|
+
json.dumps(
|
|
67
|
+
{
|
|
68
|
+
"type": "turn.completed",
|
|
69
|
+
"usage": {
|
|
70
|
+
"input_tokens": 10,
|
|
71
|
+
"output_tokens": 5,
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
),
|
|
75
|
+
]
|
|
76
|
+
) + "\n"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@pytest.fixture
|
|
80
|
+
def repository_directory(tmp_path: Path) -> Path:
|
|
81
|
+
existing_repository_directory = tmp_path / "repo"
|
|
82
|
+
existing_repository_directory.mkdir()
|
|
83
|
+
return existing_repository_directory
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@pytest.fixture
|
|
87
|
+
def run_state_directory(tmp_path: Path) -> Path:
|
|
88
|
+
existing_run_state_directory = tmp_path / "run_state"
|
|
89
|
+
existing_run_state_directory.mkdir()
|
|
90
|
+
return existing_run_state_directory
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class _SubprocessRecorder:
|
|
94
|
+
def __init__(
|
|
95
|
+
self,
|
|
96
|
+
behavior_for_invocation: Callable[
|
|
97
|
+
[list[str]], subprocess.CompletedProcess[str] | BaseException
|
|
98
|
+
],
|
|
99
|
+
) -> None:
|
|
100
|
+
self.behavior_for_invocation = behavior_for_invocation
|
|
101
|
+
self.all_invocations: list[list[str]] = []
|
|
102
|
+
self.all_keyword_arguments: list[dict[str, object]] = []
|
|
103
|
+
|
|
104
|
+
def __call__(
|
|
105
|
+
self,
|
|
106
|
+
invocation: list[str],
|
|
107
|
+
**keyword_arguments: object,
|
|
108
|
+
) -> subprocess.CompletedProcess[str]:
|
|
109
|
+
self.all_invocations.append(list(invocation))
|
|
110
|
+
self.all_keyword_arguments.append(keyword_arguments)
|
|
111
|
+
behavior = self.behavior_for_invocation(list(invocation))
|
|
112
|
+
if isinstance(behavior, BaseException):
|
|
113
|
+
raise behavior
|
|
114
|
+
return behavior
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _completed(
|
|
118
|
+
all_arguments: list[str],
|
|
119
|
+
returncode: int,
|
|
120
|
+
stdout: str = "",
|
|
121
|
+
stderr: str = "",
|
|
122
|
+
) -> subprocess.CompletedProcess[str]:
|
|
123
|
+
return subprocess.CompletedProcess(
|
|
124
|
+
args=all_arguments,
|
|
125
|
+
returncode=returncode,
|
|
126
|
+
stdout=stdout,
|
|
127
|
+
stderr=stderr,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _is_version_probe(all_arguments: list[str]) -> bool:
|
|
132
|
+
return all_arguments == [CODEX_BINARY_NAME, VERSION_FLAG]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _is_shape_probe(all_arguments: list[str]) -> bool:
|
|
136
|
+
return all_arguments == [
|
|
137
|
+
CODEX_BINARY_NAME,
|
|
138
|
+
EXEC_SUBCOMMAND,
|
|
139
|
+
REVIEW_SUBCOMMAND,
|
|
140
|
+
HELP_FLAG,
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _is_review_run(all_arguments: list[str]) -> bool:
|
|
145
|
+
return (
|
|
146
|
+
len(all_arguments) >= 4
|
|
147
|
+
and all_arguments[0] == CODEX_BINARY_NAME
|
|
148
|
+
and all_arguments[1] == EXEC_SUBCOMMAND
|
|
149
|
+
and REVIEW_SUBCOMMAND in all_arguments
|
|
150
|
+
and JSON_FLAG in all_arguments
|
|
151
|
+
and HELP_FLAG not in all_arguments
|
|
152
|
+
and VERSION_FLAG not in all_arguments
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _install_recorder(
|
|
157
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
158
|
+
behavior_for_invocation: Callable[
|
|
159
|
+
[list[str]], subprocess.CompletedProcess[str] | BaseException
|
|
160
|
+
],
|
|
161
|
+
) -> _SubprocessRecorder:
|
|
162
|
+
recorder = _SubprocessRecorder(behavior_for_invocation)
|
|
163
|
+
monkeypatch.setattr(wrapper, "codex_subprocess_runner", recorder)
|
|
164
|
+
monkeypatch.setattr(
|
|
165
|
+
wrapper, "_resolve_codex_command_prefix", lambda: [CODEX_BINARY_NAME]
|
|
166
|
+
)
|
|
167
|
+
return recorder
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _healthy_probe_then_review(
|
|
171
|
+
review_returncode: int = 0,
|
|
172
|
+
review_stdout: str = SUCCESS_JSONL,
|
|
173
|
+
review_stderr: str = "",
|
|
174
|
+
) -> Callable[[list[str]], subprocess.CompletedProcess[str] | BaseException]:
|
|
175
|
+
def behavior(
|
|
176
|
+
all_arguments: list[str],
|
|
177
|
+
) -> subprocess.CompletedProcess[str] | BaseException:
|
|
178
|
+
if _is_version_probe(all_arguments):
|
|
179
|
+
return _completed(all_arguments, 0, stdout=VERSION_STDOUT)
|
|
180
|
+
if _is_shape_probe(all_arguments):
|
|
181
|
+
return _completed(all_arguments, 0, stdout=HELP_TEXT_WITH_TARGETS)
|
|
182
|
+
if _is_review_run(all_arguments):
|
|
183
|
+
return _completed(
|
|
184
|
+
all_arguments,
|
|
185
|
+
review_returncode,
|
|
186
|
+
stdout=review_stdout,
|
|
187
|
+
stderr=review_stderr,
|
|
188
|
+
)
|
|
189
|
+
raise AssertionError(f"unexpected invocation: {all_arguments!r}")
|
|
190
|
+
|
|
191
|
+
return behavior
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _healthy_probe_then_failing_shape(
|
|
195
|
+
shape_stdout: str,
|
|
196
|
+
) -> Callable[[list[str]], subprocess.CompletedProcess[str] | BaseException]:
|
|
197
|
+
def behavior(
|
|
198
|
+
all_arguments: list[str],
|
|
199
|
+
) -> subprocess.CompletedProcess[str] | BaseException:
|
|
200
|
+
if _is_version_probe(all_arguments):
|
|
201
|
+
return _completed(all_arguments, 0, stdout=VERSION_STDOUT)
|
|
202
|
+
if _is_shape_probe(all_arguments):
|
|
203
|
+
return _completed(all_arguments, 0, stdout=shape_stdout)
|
|
204
|
+
raise AssertionError("review must not run after a failed shape probe")
|
|
205
|
+
|
|
206
|
+
return behavior
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _only_review_invocation(recorder: _SubprocessRecorder) -> list[str]:
|
|
210
|
+
return next(
|
|
211
|
+
each_invocation
|
|
212
|
+
for each_invocation in recorder.all_invocations
|
|
213
|
+
if _is_review_run(each_invocation)
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def test_argv_assembly_for_uncommitted_target(
|
|
218
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
219
|
+
repository_directory: Path,
|
|
220
|
+
run_state_directory: Path,
|
|
221
|
+
) -> None:
|
|
222
|
+
recorder = _install_recorder(monkeypatch, _healthy_probe_then_review())
|
|
223
|
+
|
|
224
|
+
review_outcome = wrapper.run_codex_review(
|
|
225
|
+
repository_directory=repository_directory,
|
|
226
|
+
run_state_directory=run_state_directory,
|
|
227
|
+
is_uncommitted=True,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
review_invocation = _only_review_invocation(recorder)
|
|
231
|
+
assert review_invocation == [
|
|
232
|
+
CODEX_BINARY_NAME,
|
|
233
|
+
EXEC_SUBCOMMAND,
|
|
234
|
+
REVIEW_SUBCOMMAND,
|
|
235
|
+
JSON_FLAG,
|
|
236
|
+
UNCOMMITTED_TARGET_FLAG,
|
|
237
|
+
]
|
|
238
|
+
assert CUSTOM_INSTRUCTIONS_PROMPT not in review_invocation
|
|
239
|
+
assert review_outcome.outcome_class == OUTCOME_CLASS_COMPLETED
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def test_argv_assembly_for_base_branch_target(
|
|
243
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
244
|
+
repository_directory: Path,
|
|
245
|
+
run_state_directory: Path,
|
|
246
|
+
) -> None:
|
|
247
|
+
recorder = _install_recorder(monkeypatch, _healthy_probe_then_review())
|
|
248
|
+
base_branch = "origin/main"
|
|
249
|
+
|
|
250
|
+
wrapper.run_codex_review(
|
|
251
|
+
repository_directory=repository_directory,
|
|
252
|
+
run_state_directory=run_state_directory,
|
|
253
|
+
base_branch=base_branch,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
review_invocation = _only_review_invocation(recorder)
|
|
257
|
+
assert review_invocation == [
|
|
258
|
+
CODEX_BINARY_NAME,
|
|
259
|
+
EXEC_SUBCOMMAND,
|
|
260
|
+
REVIEW_SUBCOMMAND,
|
|
261
|
+
JSON_FLAG,
|
|
262
|
+
BASE_TARGET_FLAG,
|
|
263
|
+
base_branch,
|
|
264
|
+
]
|
|
265
|
+
assert CUSTOM_INSTRUCTIONS_PROMPT not in review_invocation
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def test_argv_assembly_for_commit_target(
|
|
269
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
270
|
+
repository_directory: Path,
|
|
271
|
+
run_state_directory: Path,
|
|
272
|
+
) -> None:
|
|
273
|
+
recorder = _install_recorder(monkeypatch, _healthy_probe_then_review())
|
|
274
|
+
commit_sha = "abc123def456"
|
|
275
|
+
|
|
276
|
+
wrapper.run_codex_review(
|
|
277
|
+
repository_directory=repository_directory,
|
|
278
|
+
run_state_directory=run_state_directory,
|
|
279
|
+
commit_sha=commit_sha,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
review_invocation = _only_review_invocation(recorder)
|
|
283
|
+
assert review_invocation == [
|
|
284
|
+
CODEX_BINARY_NAME,
|
|
285
|
+
EXEC_SUBCOMMAND,
|
|
286
|
+
REVIEW_SUBCOMMAND,
|
|
287
|
+
JSON_FLAG,
|
|
288
|
+
COMMIT_TARGET_FLAG,
|
|
289
|
+
commit_sha,
|
|
290
|
+
]
|
|
291
|
+
assert CUSTOM_INSTRUCTIONS_PROMPT not in review_invocation
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def test_argv_assembly_for_prompt_target(
|
|
295
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
296
|
+
repository_directory: Path,
|
|
297
|
+
run_state_directory: Path,
|
|
298
|
+
) -> None:
|
|
299
|
+
recorder = _install_recorder(monkeypatch, _healthy_probe_then_review())
|
|
300
|
+
|
|
301
|
+
review_outcome = wrapper.run_codex_review(
|
|
302
|
+
repository_directory=repository_directory,
|
|
303
|
+
run_state_directory=run_state_directory,
|
|
304
|
+
is_prompt_target=True,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
review_invocation = _only_review_invocation(recorder)
|
|
308
|
+
assert review_invocation == [
|
|
309
|
+
CODEX_BINARY_NAME,
|
|
310
|
+
EXEC_SUBCOMMAND,
|
|
311
|
+
REVIEW_SUBCOMMAND,
|
|
312
|
+
JSON_FLAG,
|
|
313
|
+
CUSTOM_INSTRUCTIONS_PROMPT,
|
|
314
|
+
]
|
|
315
|
+
assert UNCOMMITTED_TARGET_FLAG not in review_invocation
|
|
316
|
+
assert BASE_TARGET_FLAG not in review_invocation
|
|
317
|
+
assert COMMIT_TARGET_FLAG not in review_invocation
|
|
318
|
+
assert review_outcome.outcome_class == OUTCOME_CLASS_COMPLETED
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def test_shape_probe_missing_target_flags_returns_codex_down(
|
|
322
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
323
|
+
repository_directory: Path,
|
|
324
|
+
run_state_directory: Path,
|
|
325
|
+
) -> None:
|
|
326
|
+
recorder = _install_recorder(
|
|
327
|
+
monkeypatch,
|
|
328
|
+
_healthy_probe_then_failing_shape("Usage: codex exec review\n --legacy-only\n"),
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
review_outcome = wrapper.run_codex_review(
|
|
332
|
+
repository_directory=repository_directory,
|
|
333
|
+
run_state_directory=run_state_directory,
|
|
334
|
+
is_uncommitted=True,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
assert review_outcome.outcome_class == OUTCOME_CLASS_CODEX_DOWN
|
|
338
|
+
assert review_outcome.binary_version == EXPECTED_BINARY_VERSION
|
|
339
|
+
assert review_outcome.agent_message == ""
|
|
340
|
+
assert review_outcome.jsonl_path is None
|
|
341
|
+
assert not any(
|
|
342
|
+
_is_review_run(each_invocation)
|
|
343
|
+
for each_invocation in recorder.all_invocations
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def test_shape_probe_lookalike_flags_do_not_match_required_tokens(
|
|
348
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
349
|
+
repository_directory: Path,
|
|
350
|
+
run_state_directory: Path,
|
|
351
|
+
) -> None:
|
|
352
|
+
lookalike_help_text = (
|
|
353
|
+
"Usage: codex exec review [OPTIONS]\n"
|
|
354
|
+
" --baseline <BRANCH>\n"
|
|
355
|
+
" --commit-message <TEXT>\n"
|
|
356
|
+
" --uncommitted-only\n"
|
|
357
|
+
" --json-output\n"
|
|
358
|
+
)
|
|
359
|
+
recorder = _install_recorder(
|
|
360
|
+
monkeypatch, _healthy_probe_then_failing_shape(lookalike_help_text)
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
review_outcome = wrapper.run_codex_review(
|
|
364
|
+
repository_directory=repository_directory,
|
|
365
|
+
run_state_directory=run_state_directory,
|
|
366
|
+
is_uncommitted=True,
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
assert review_outcome.outcome_class == OUTCOME_CLASS_CODEX_DOWN
|
|
370
|
+
assert review_outcome.exit_code == 0
|
|
371
|
+
assert review_outcome.jsonl_path is None
|
|
372
|
+
assert not any(
|
|
373
|
+
_is_review_run(each_invocation)
|
|
374
|
+
for each_invocation in recorder.all_invocations
|
|
375
|
+
)
|
|
376
|
+
for each_required_flag in ALL_SHAPE_PROBE_REQUIRED_FLAGS:
|
|
377
|
+
assert each_required_flag in lookalike_help_text
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def test_shape_probe_nonzero_help_returns_codex_down(
|
|
381
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
382
|
+
repository_directory: Path,
|
|
383
|
+
run_state_directory: Path,
|
|
384
|
+
) -> None:
|
|
385
|
+
def behavior(
|
|
386
|
+
all_arguments: list[str],
|
|
387
|
+
) -> subprocess.CompletedProcess[str] | BaseException:
|
|
388
|
+
if _is_version_probe(all_arguments):
|
|
389
|
+
return _completed(all_arguments, 0, stdout=VERSION_STDOUT)
|
|
390
|
+
if _is_shape_probe(all_arguments):
|
|
391
|
+
return _completed(
|
|
392
|
+
all_arguments,
|
|
393
|
+
2,
|
|
394
|
+
stderr="error: unrecognized subcommand 'review'\n",
|
|
395
|
+
)
|
|
396
|
+
raise AssertionError("review must not run after a failed shape probe")
|
|
397
|
+
|
|
398
|
+
_install_recorder(monkeypatch, behavior)
|
|
399
|
+
|
|
400
|
+
review_outcome = wrapper.run_codex_review(
|
|
401
|
+
repository_directory=repository_directory,
|
|
402
|
+
run_state_directory=run_state_directory,
|
|
403
|
+
is_uncommitted=True,
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
assert review_outcome.outcome_class == OUTCOME_CLASS_CODEX_DOWN
|
|
407
|
+
assert review_outcome.exit_code == 2
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def test_missing_binary_returns_codex_down(
|
|
411
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
412
|
+
repository_directory: Path,
|
|
413
|
+
run_state_directory: Path,
|
|
414
|
+
) -> None:
|
|
415
|
+
def behavior(
|
|
416
|
+
all_arguments: list[str],
|
|
417
|
+
) -> subprocess.CompletedProcess[str] | BaseException:
|
|
418
|
+
raise FileNotFoundError(all_arguments[0])
|
|
419
|
+
|
|
420
|
+
_install_recorder(monkeypatch, behavior)
|
|
421
|
+
|
|
422
|
+
review_outcome = wrapper.run_codex_review(
|
|
423
|
+
repository_directory=repository_directory,
|
|
424
|
+
run_state_directory=run_state_directory,
|
|
425
|
+
is_uncommitted=True,
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
assert review_outcome.outcome_class == OUTCOME_CLASS_CODEX_DOWN
|
|
429
|
+
assert review_outcome.exit_code == MISSING_BINARY_EXIT_CODE
|
|
430
|
+
assert review_outcome.jsonl_path is None
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def test_jsonl_capture_and_agent_message_extraction(
|
|
434
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
435
|
+
repository_directory: Path,
|
|
436
|
+
run_state_directory: Path,
|
|
437
|
+
) -> None:
|
|
438
|
+
recorder = _install_recorder(monkeypatch, _healthy_probe_then_review())
|
|
439
|
+
|
|
440
|
+
review_outcome = wrapper.run_codex_review(
|
|
441
|
+
repository_directory=repository_directory,
|
|
442
|
+
run_state_directory=run_state_directory,
|
|
443
|
+
is_uncommitted=True,
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
expected_jsonl_path = run_state_directory / JSONL_CAPTURE_FILENAME
|
|
447
|
+
assert review_outcome.outcome_class == OUTCOME_CLASS_COMPLETED
|
|
448
|
+
assert review_outcome.exit_code == 0
|
|
449
|
+
assert review_outcome.jsonl_path == expected_jsonl_path
|
|
450
|
+
assert expected_jsonl_path.read_text(encoding=UTF8_ENCODING) == SUCCESS_JSONL
|
|
451
|
+
assert review_outcome.agent_message == AGENT_MESSAGE_BODY
|
|
452
|
+
assert review_outcome.binary_version == EXPECTED_BINARY_VERSION
|
|
453
|
+
review_keyword_arguments = next(
|
|
454
|
+
each_keyword_arguments
|
|
455
|
+
for each_invocation, each_keyword_arguments in zip(
|
|
456
|
+
recorder.all_invocations,
|
|
457
|
+
recorder.all_keyword_arguments,
|
|
458
|
+
strict=True,
|
|
459
|
+
)
|
|
460
|
+
if _is_review_run(each_invocation)
|
|
461
|
+
)
|
|
462
|
+
assert review_keyword_arguments["cwd"] == str(repository_directory)
|
|
463
|
+
assert review_keyword_arguments["timeout"] == DEFAULT_TIMEOUT_SECONDS
|
|
464
|
+
assert review_keyword_arguments["capture_output"] is True
|
|
465
|
+
assert review_keyword_arguments["text"] is True
|
|
466
|
+
assert review_keyword_arguments["encoding"] == UTF8_ENCODING
|
|
467
|
+
assert review_keyword_arguments["check"] is False
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def test_jsonl_capture_preserves_stream_line_endings(
|
|
471
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
472
|
+
repository_directory: Path,
|
|
473
|
+
run_state_directory: Path,
|
|
474
|
+
) -> None:
|
|
475
|
+
_install_recorder(monkeypatch, _healthy_probe_then_review())
|
|
476
|
+
|
|
477
|
+
review_outcome = wrapper.run_codex_review(
|
|
478
|
+
repository_directory=repository_directory,
|
|
479
|
+
run_state_directory=run_state_directory,
|
|
480
|
+
is_uncommitted=True,
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
assert review_outcome.jsonl_path is not None
|
|
484
|
+
captured_bytes = review_outcome.jsonl_path.read_bytes()
|
|
485
|
+
assert CARRIAGE_RETURN_LINE_ENDING not in captured_bytes
|
|
486
|
+
assert captured_bytes == SUCCESS_JSONL.encode(UTF8_ENCODING)
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def test_unicode_decode_error_returns_codex_down(
|
|
490
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
491
|
+
repository_directory: Path,
|
|
492
|
+
run_state_directory: Path,
|
|
493
|
+
) -> None:
|
|
494
|
+
def behavior(
|
|
495
|
+
all_arguments: list[str],
|
|
496
|
+
) -> subprocess.CompletedProcess[str] | BaseException:
|
|
497
|
+
raise UnicodeDecodeError("utf-8", b"\xff", 0, 1, "invalid start byte")
|
|
498
|
+
|
|
499
|
+
_install_recorder(monkeypatch, behavior)
|
|
500
|
+
|
|
501
|
+
review_outcome = wrapper.run_codex_review(
|
|
502
|
+
repository_directory=repository_directory,
|
|
503
|
+
run_state_directory=run_state_directory,
|
|
504
|
+
is_uncommitted=True,
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
assert review_outcome.outcome_class == OUTCOME_CLASS_CODEX_DOWN
|
|
508
|
+
assert review_outcome.exit_code == SUBPROCESS_DECODE_EXIT_CODE
|
|
509
|
+
assert review_outcome.jsonl_path is None
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def test_timeout_returns_codex_down(
|
|
513
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
514
|
+
repository_directory: Path,
|
|
515
|
+
run_state_directory: Path,
|
|
516
|
+
) -> None:
|
|
517
|
+
def behavior(
|
|
518
|
+
all_arguments: list[str],
|
|
519
|
+
) -> subprocess.CompletedProcess[str] | BaseException:
|
|
520
|
+
if _is_version_probe(all_arguments):
|
|
521
|
+
return _completed(all_arguments, 0, stdout=VERSION_STDOUT)
|
|
522
|
+
if _is_shape_probe(all_arguments):
|
|
523
|
+
return _completed(all_arguments, 0, stdout=HELP_TEXT_WITH_TARGETS)
|
|
524
|
+
if _is_review_run(all_arguments):
|
|
525
|
+
return subprocess.TimeoutExpired(cmd=all_arguments, timeout=1)
|
|
526
|
+
raise AssertionError(f"unexpected invocation: {all_arguments!r}")
|
|
527
|
+
|
|
528
|
+
_install_recorder(monkeypatch, behavior)
|
|
529
|
+
|
|
530
|
+
review_outcome = wrapper.run_codex_review(
|
|
531
|
+
repository_directory=repository_directory,
|
|
532
|
+
run_state_directory=run_state_directory,
|
|
533
|
+
is_uncommitted=True,
|
|
534
|
+
timeout_seconds=1,
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
assert review_outcome.outcome_class == OUTCOME_CLASS_CODEX_DOWN
|
|
538
|
+
assert review_outcome.exit_code == TIMEOUT_EXIT_CODE
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def test_codex_home_env_is_passed_through_when_set(
|
|
542
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
543
|
+
tmp_path: Path,
|
|
544
|
+
repository_directory: Path,
|
|
545
|
+
run_state_directory: Path,
|
|
546
|
+
) -> None:
|
|
547
|
+
codex_home_directory = tmp_path / "codex_home"
|
|
548
|
+
codex_home_directory.mkdir()
|
|
549
|
+
monkeypatch.setenv(CODEX_HOME_ENV_VAR, str(codex_home_directory))
|
|
550
|
+
recorder = _install_recorder(monkeypatch, _healthy_probe_then_review())
|
|
551
|
+
|
|
552
|
+
wrapper.run_codex_review(
|
|
553
|
+
repository_directory=repository_directory,
|
|
554
|
+
run_state_directory=run_state_directory,
|
|
555
|
+
is_uncommitted=True,
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
for each_keyword_arguments in recorder.all_keyword_arguments:
|
|
559
|
+
process_environment = each_keyword_arguments.get("env")
|
|
560
|
+
assert process_environment is not None
|
|
561
|
+
assert isinstance(process_environment, dict)
|
|
562
|
+
assert process_environment[CODEX_HOME_ENV_VAR] == str(codex_home_directory)
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def test_rejects_multiple_targets(
|
|
566
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
567
|
+
repository_directory: Path,
|
|
568
|
+
run_state_directory: Path,
|
|
569
|
+
) -> None:
|
|
570
|
+
recorder = _install_recorder(monkeypatch, _healthy_probe_then_review())
|
|
571
|
+
|
|
572
|
+
with pytest.raises(ValueError):
|
|
573
|
+
wrapper.run_codex_review(
|
|
574
|
+
repository_directory=repository_directory,
|
|
575
|
+
run_state_directory=run_state_directory,
|
|
576
|
+
is_uncommitted=True,
|
|
577
|
+
base_branch="main",
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
assert recorder.all_invocations == []
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def test_rejects_missing_target(
|
|
584
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
585
|
+
repository_directory: Path,
|
|
586
|
+
run_state_directory: Path,
|
|
587
|
+
) -> None:
|
|
588
|
+
recorder = _install_recorder(monkeypatch, _healthy_probe_then_review())
|
|
589
|
+
|
|
590
|
+
with pytest.raises(ValueError):
|
|
591
|
+
wrapper.run_codex_review(
|
|
592
|
+
repository_directory=repository_directory,
|
|
593
|
+
run_state_directory=run_state_directory,
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
assert recorder.all_invocations == []
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def test_shape_probe_missing_json_flag_returns_codex_down(
|
|
600
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
601
|
+
repository_directory: Path,
|
|
602
|
+
run_state_directory: Path,
|
|
603
|
+
) -> None:
|
|
604
|
+
help_without_json = (
|
|
605
|
+
"Usage: codex exec review [OPTIONS]\n"
|
|
606
|
+
" --uncommitted\n"
|
|
607
|
+
" --base <BRANCH>\n"
|
|
608
|
+
" --commit <SHA>\n"
|
|
609
|
+
)
|
|
610
|
+
recorder = _install_recorder(
|
|
611
|
+
monkeypatch, _healthy_probe_then_failing_shape(help_without_json)
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
review_outcome = wrapper.run_codex_review(
|
|
615
|
+
repository_directory=repository_directory,
|
|
616
|
+
run_state_directory=run_state_directory,
|
|
617
|
+
is_uncommitted=True,
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
assert review_outcome.outcome_class == OUTCOME_CLASS_CODEX_DOWN
|
|
621
|
+
assert review_outcome.jsonl_path is None
|
|
622
|
+
assert not any(
|
|
623
|
+
_is_review_run(each_invocation)
|
|
624
|
+
for each_invocation in recorder.all_invocations
|
|
625
|
+
)
|
|
626
|
+
assert JSON_FLAG not in help_without_json
|
|
627
|
+
assert JSON_FLAG in ALL_SHAPE_PROBE_REQUIRED_FLAGS
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def test_missing_repository_directory_raises_value_error(
|
|
631
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
632
|
+
tmp_path: Path,
|
|
633
|
+
run_state_directory: Path,
|
|
634
|
+
) -> None:
|
|
635
|
+
missing_repository_directory = tmp_path / "missing-repo"
|
|
636
|
+
recorder = _install_recorder(monkeypatch, _healthy_probe_then_review())
|
|
637
|
+
|
|
638
|
+
with pytest.raises(ValueError, match="repository_directory"):
|
|
639
|
+
wrapper.run_codex_review(
|
|
640
|
+
repository_directory=missing_repository_directory,
|
|
641
|
+
run_state_directory=run_state_directory,
|
|
642
|
+
is_uncommitted=True,
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
assert recorder.all_invocations == []
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def test_missing_run_state_directory_raises_before_any_codex_process(
|
|
649
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
650
|
+
tmp_path: Path,
|
|
651
|
+
repository_directory: Path,
|
|
652
|
+
) -> None:
|
|
653
|
+
missing_run_state_directory = tmp_path / "missing-run-state"
|
|
654
|
+
recorder = _install_recorder(monkeypatch, _healthy_probe_then_review())
|
|
655
|
+
|
|
656
|
+
with pytest.raises(ValueError, match="run_state_directory"):
|
|
657
|
+
wrapper.run_codex_review(
|
|
658
|
+
repository_directory=repository_directory,
|
|
659
|
+
run_state_directory=missing_run_state_directory,
|
|
660
|
+
is_uncommitted=True,
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
assert recorder.all_invocations == []
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def test_custom_instructions_prompt_demands_finding_bullet_format() -> None:
|
|
667
|
+
assert "- [P1]" in CUSTOM_INSTRUCTIONS_PROMPT
|
|
668
|
+
assert "Review comment:" in CUSTOM_INSTRUCTIONS_PROMPT
|
|
669
|
+
assert "fenced JSON" not in CUSTOM_INSTRUCTIONS_PROMPT
|
|
670
|
+
assert "JSON array" not in CUSTOM_INSTRUCTIONS_PROMPT
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def test_model_pin_assembles_model_flag_before_review_subcommand(
|
|
674
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
675
|
+
repository_directory: Path,
|
|
676
|
+
run_state_directory: Path,
|
|
677
|
+
) -> None:
|
|
678
|
+
model_pin = "gpt-5-codex"
|
|
679
|
+
monkeypatch.setattr(wrapper, "CODEX_MODEL_PIN", model_pin)
|
|
680
|
+
recorder = _install_recorder(monkeypatch, _healthy_probe_then_review())
|
|
681
|
+
|
|
682
|
+
wrapper.run_codex_review(
|
|
683
|
+
repository_directory=repository_directory,
|
|
684
|
+
run_state_directory=run_state_directory,
|
|
685
|
+
is_uncommitted=True,
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
review_invocation = _only_review_invocation(recorder)
|
|
689
|
+
model_flag_index = review_invocation.index(MODEL_FLAG)
|
|
690
|
+
review_subcommand_index = review_invocation.index(REVIEW_SUBCOMMAND)
|
|
691
|
+
assert review_invocation[model_flag_index : model_flag_index + 2] == [
|
|
692
|
+
MODEL_FLAG,
|
|
693
|
+
model_pin,
|
|
694
|
+
]
|
|
695
|
+
assert model_flag_index < review_subcommand_index
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def test_review_exit_1_config_error_classifies_codex_down(
|
|
699
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
700
|
+
repository_directory: Path,
|
|
701
|
+
run_state_directory: Path,
|
|
702
|
+
) -> None:
|
|
703
|
+
config_error_stderr = (
|
|
704
|
+
"Error loading config.toml: unknown variant 'default', "
|
|
705
|
+
"expected 'fast' or 'flex' in 'service_tier'\n"
|
|
706
|
+
)
|
|
707
|
+
recorder = _install_recorder(
|
|
708
|
+
monkeypatch,
|
|
709
|
+
_healthy_probe_then_review(
|
|
710
|
+
review_returncode=1,
|
|
711
|
+
review_stdout="",
|
|
712
|
+
review_stderr=config_error_stderr,
|
|
713
|
+
),
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
review_outcome = wrapper.run_codex_review(
|
|
717
|
+
repository_directory=repository_directory,
|
|
718
|
+
run_state_directory=run_state_directory,
|
|
719
|
+
is_uncommitted=True,
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
assert review_outcome.outcome_class == OUTCOME_CLASS_CODEX_DOWN
|
|
723
|
+
assert review_outcome.exit_code == 1
|
|
724
|
+
assert review_outcome.agent_message == config_error_stderr
|
|
725
|
+
assert review_outcome.jsonl_path is not None
|
|
726
|
+
assert review_outcome.jsonl_path.read_text(encoding=UTF8_ENCODING) == ""
|
|
727
|
+
assert any(
|
|
728
|
+
_is_review_run(each_invocation)
|
|
729
|
+
for each_invocation in recorder.all_invocations
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
def test_review_exit_2_argument_error_classifies_codex_down(
|
|
734
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
735
|
+
repository_directory: Path,
|
|
736
|
+
run_state_directory: Path,
|
|
737
|
+
) -> None:
|
|
738
|
+
argument_error_stderr = (
|
|
739
|
+
"error: the argument '--uncommitted' cannot be used with '[PROMPT]'\n"
|
|
740
|
+
"Usage: codex exec review --json --uncommitted [PROMPT]\n"
|
|
741
|
+
)
|
|
742
|
+
recorder = _install_recorder(
|
|
743
|
+
monkeypatch,
|
|
744
|
+
_healthy_probe_then_review(
|
|
745
|
+
review_returncode=2,
|
|
746
|
+
review_stdout="",
|
|
747
|
+
review_stderr=argument_error_stderr,
|
|
748
|
+
),
|
|
749
|
+
)
|
|
750
|
+
|
|
751
|
+
review_outcome = wrapper.run_codex_review(
|
|
752
|
+
repository_directory=repository_directory,
|
|
753
|
+
run_state_directory=run_state_directory,
|
|
754
|
+
is_uncommitted=True,
|
|
755
|
+
)
|
|
756
|
+
|
|
757
|
+
assert review_outcome.outcome_class == OUTCOME_CLASS_CODEX_DOWN
|
|
758
|
+
assert review_outcome.exit_code == 2
|
|
759
|
+
assert review_outcome.agent_message == argument_error_stderr
|
|
760
|
+
assert review_outcome.jsonl_path is not None
|
|
761
|
+
assert review_outcome.jsonl_path.read_text(encoding=UTF8_ENCODING) == ""
|
|
762
|
+
assert any(
|
|
763
|
+
_is_review_run(each_invocation)
|
|
764
|
+
for each_invocation in recorder.all_invocations
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
def test_review_exit_0_stays_completed(
|
|
769
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
770
|
+
repository_directory: Path,
|
|
771
|
+
run_state_directory: Path,
|
|
772
|
+
) -> None:
|
|
773
|
+
_install_recorder(
|
|
774
|
+
monkeypatch,
|
|
775
|
+
_healthy_probe_then_review(review_returncode=0, review_stdout=SUCCESS_JSONL),
|
|
776
|
+
)
|
|
777
|
+
|
|
778
|
+
review_outcome = wrapper.run_codex_review(
|
|
779
|
+
repository_directory=repository_directory,
|
|
780
|
+
run_state_directory=run_state_directory,
|
|
781
|
+
is_uncommitted=True,
|
|
782
|
+
)
|
|
783
|
+
|
|
784
|
+
assert review_outcome.outcome_class == OUTCOME_CLASS_COMPLETED
|
|
785
|
+
assert review_outcome.exit_code == 0
|
|
786
|
+
assert review_outcome.agent_message == AGENT_MESSAGE_BODY
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
def test_resolve_prefix_wraps_windows_shim(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
790
|
+
shim_path = r"C:\\Users\\dev\\AppData\\Roaming\\npm\\codex.CMD"
|
|
791
|
+
monkeypatch.setattr(wrapper.shutil, "which", lambda _name: shim_path)
|
|
792
|
+
monkeypatch.setattr(wrapper.os, "name", "nt")
|
|
793
|
+
|
|
794
|
+
assert wrapper._resolve_codex_command_prefix() == ["cmd", "/c", shim_path]
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
def test_resolve_prefix_uses_resolved_path_on_posix(
|
|
798
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
799
|
+
) -> None:
|
|
800
|
+
resolved_path = "/usr/local/bin/codex"
|
|
801
|
+
monkeypatch.setattr(wrapper.shutil, "which", lambda _name: resolved_path)
|
|
802
|
+
monkeypatch.setattr(wrapper.os, "name", "posix")
|
|
803
|
+
|
|
804
|
+
assert wrapper._resolve_codex_command_prefix() == [resolved_path]
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
def test_resolve_prefix_falls_back_to_bare_name_when_absent(
|
|
808
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
809
|
+
) -> None:
|
|
810
|
+
monkeypatch.setattr(wrapper.shutil, "which", lambda _name: None)
|
|
811
|
+
|
|
812
|
+
assert wrapper._resolve_codex_command_prefix() == [CODEX_BINARY_NAME]
|