claude-dev-env 2.5.0 → 2.7.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/CLAUDE.md +20 -57
- package/_shared/pr-loop/scripts/code_rules_gate.py +2 -1
- package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +12 -2
- package/_shared/pr-loop/scripts/code_rules_gate_parts/baseline_import_isolation.py +309 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_regression.py +540 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +206 -70
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/__init__.py +1 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/_repo_test_helpers.py +76 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_baseline_import_isolation.py +248 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_regression.py +309 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_running.py +91 -58
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +202 -0
- package/_shared/pr-loop/worker-spawn.md +3 -1
- package/agents/CLAUDE.md +1 -1
- package/agents/code-verifier.md +36 -7
- package/bin/codex-compat.mjs +104 -0
- package/bin/codex-compat.test.mjs +51 -0
- package/codex-capability-map.json +13 -0
- package/docs/CODE_RULES.md +2 -0
- package/docs/codex-compatibility.md +25 -0
- package/docs/nas-ssh-invocation.md +96 -12
- package/docs/references/code-review-enforcement.md +31 -6
- package/hooks/blocking/CLAUDE.md +3 -0
- package/hooks/blocking/config/code_review_enforcement_constants.py +40 -10
- package/hooks/blocking/config/test_code_review_enforcement_constants.py +56 -3
- package/hooks/blocking/eli11_reply_enforcer.py +479 -0
- package/hooks/blocking/gh_body_arg_blocker.py +1 -1
- package/hooks/blocking/nas_ssh_binary_enforcer.py +8 -46
- package/hooks/blocking/shell_substitution_blocker.py +129 -0
- package/hooks/blocking/state_description_blocker.py +1 -1
- package/hooks/blocking/stop_dispatcher.py +1 -1
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +2 -3
- package/hooks/blocking/test_eli11_reply_enforcer.py +457 -0
- package/hooks/blocking/test_shell_substitution_blocker.py +124 -0
- package/hooks/blocking/test_stop_dispatcher.py +23 -0
- package/hooks/blocking/test_unscoped_search_blocker.py +102 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +4 -8
- package/hooks/blocking/unscoped_search_blocker.py +391 -0
- package/hooks/git-hooks/CLAUDE.md +3 -0
- package/hooks/git-hooks/conftest.py +30 -0
- package/hooks/git-hooks/gate_utils.py +2 -2
- package/hooks/git-hooks/git_hooks_constants/__init__.py +41 -2
- package/hooks/git-hooks/pre_push.py +75 -4
- package/hooks/git-hooks/pre_push_base_reference.py +166 -0
- package/hooks/git-hooks/test_config.py +0 -15
- package/hooks/git-hooks/test_gate_utils.py +3 -15
- package/hooks/git-hooks/test_pre_commit.py +1 -15
- package/hooks/git-hooks/test_pre_push.py +236 -27
- package/hooks/git-hooks/test_pre_push_base_reference.py +339 -0
- package/hooks/hooks.json +0 -12
- package/hooks/hooks_constants/CLAUDE.md +5 -1
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +4 -4
- package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +101 -0
- package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +2 -8
- package/hooks/hooks_constants/shell_command_segments.py +82 -0
- package/hooks/hooks_constants/shell_substitution_blocker_constants.py +67 -0
- package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -0
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +5 -6
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +1 -0
- package/hooks/hooks_constants/unscoped_search_blocker_constants.py +153 -0
- package/package.json +4 -2
- package/rules/CLAUDE.md +17 -23
- package/rules/agent-spawn-protocol.md +6 -6
- package/rules/anti-corollary-tests.md +1 -1
- package/rules/bdd.md +1 -1
- package/rules/cleanup-temp-files.md +10 -4
- package/rules/code-standards.md +7 -0
- package/rules/conservative-action.md +1 -5
- package/rules/context7.md +0 -4
- package/rules/destructive-commands.md +47 -0
- package/rules/doc-inventory-integrity.md +48 -0
- package/rules/doc-prose-cuts.md +58 -0
- package/rules/docstring-prose-matches-implementation.md +10 -2
- package/rules/durable-post-artifacts.md +0 -4
- package/rules/eli11-replies.md +31 -0
- package/rules/explore-thoroughly.md +4 -4
- package/rules/falsify-before-green.md +68 -0
- package/rules/file-global-constants.md +1 -1
- package/rules/filesystem-search.md +51 -0
- package/rules/gh-cli-conventions.md +27 -0
- package/rules/git-workflow.md +26 -0
- package/rules/hedging-claims.md +9 -0
- package/rules/long-horizon-autonomy.md +0 -4
- package/rules/measurement-denominators.md +48 -0
- package/rules/nas-ssh-invocation.md +23 -5
- package/rules/parallel-tools.md +2 -2
- package/rules/plain-illustrative-docstrings.md +3 -7
- package/rules/plain-language.md +2 -0
- package/rules/proof-of-work-pr-comments.md +0 -4
- package/rules/re-stage-before-commit.md +2 -0
- package/rules/research-mode.md +10 -0
- package/rules/shell-invocation.md +21 -0
- package/rules/testing.md +4 -0
- package/rules/verified-commit-gate-skip.md +3 -27
- package/rules/verify-before-asking.md +5 -0
- package/rules/windows-filesystem-safe.md +1 -1
- package/rules/workers-done-before-complete.md +4 -0
- package/scripts/CLAUDE.md +3 -3
- package/scripts/Migrate-ShellPolicy.ps1 +1 -1
- package/scripts/codex_capability_bridge.py +171 -0
- package/scripts/codex_compat_materializer.py +1087 -0
- package/scripts/codex_compat_watcher.py +502 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -1
- package/scripts/dev_env_scripts_constants/code_review_constants.py +37 -0
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +79 -13
- package/scripts/grok_headless_runner.py +213 -16
- package/scripts/invoke_code_review.py +11 -4
- package/scripts/resolve_worker_spawn.py +56 -10
- package/scripts/spawn_grok_batch.py +43 -22
- package/scripts/sync_to_cursor/rules.py +0 -10
- package/scripts/test_grok_headless_runner.py +592 -10
- package/scripts/test_invoke_code_review.py +143 -0
- package/scripts/test_invoke_code_review_chain.py +1 -1
- package/scripts/test_invoke_code_review_contract.py +1 -1
- package/scripts/test_resolve_worker_spawn.py +179 -15
- package/scripts/test_spawn_grok_batch.py +225 -22
- package/scripts/tests/test_code_review_constants.py +80 -0
- package/scripts/tests/test_codex_capability_bridge.py +91 -0
- package/scripts/tests/test_codex_compat_materializer.py +632 -0
- package/scripts/tests/test_codex_compat_watcher.py +599 -0
- package/scripts/tests/test_sync_to_cursor.py +0 -1
- package/skills/autoconverge/workflow/converge.contract.test.mjs +28 -6
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +73 -0
- package/skills/autoconverge/workflow/converge.mjs +64 -15
- package/skills/bugteam/reference/copilot-gap-analysis.md +1 -1
- package/skills/condensing-instructions/SKILL.md +42 -51
- package/skills/fresh-branch/CLAUDE.md +1 -1
- package/skills/fresh-branch/SKILL.md +5 -6
- package/skills/fresh-branch/scripts/create_fresh_branch.py +42 -24
- package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -3
- package/skills/fresh-branch/scripts/test_create_fresh_branch.py +30 -126
- package/skills/grok-spawn/SKILL.md +5 -3
- package/skills/grok-spawn/reference/flag-profiles.md +3 -1
- package/skills/orchestrator/SKILL.md +23 -9
- package/skills/orchestrator-refresh/SKILL.md +20 -1
- package/skills/privacy-hygiene/reference/sweep-procedure.md +1 -1
- package/skills/session-log/SKILL.md +1 -1
- package/rules/claude-md-orphan-file.md +0 -28
- package/rules/cleanup-command-forms.md +0 -23
- package/rules/code-reviews.md +0 -11
- package/rules/env-var-table-code-drift.md +0 -10
- package/rules/gh-body-file.md +0 -5
- package/rules/gh-paginate.md +0 -3
- package/rules/hook-prose-matches-detector.md +0 -15
- package/rules/no-historical-clutter.md +0 -26
- package/rules/no-inline-destructive-literals.md +0 -9
- package/rules/no-justification-noise.md +0 -61
- package/rules/package-inventory-stale-entry.md +0 -25
- package/rules/right-sized-engineering.md +0 -28
- package/rules/self-contained-docs.md +0 -17
- package/rules/shell-invocation-policy.md +0 -5
- package/rules/state-what-is.md +0 -25
- package/rules/tdd.md +0 -7
|
@@ -22,15 +22,17 @@ from dev_env_scripts_constants.grok_worker_constants import ( # noqa: E402
|
|
|
22
22
|
BUILD_PROFILE_PROMPT_HEADER,
|
|
23
23
|
CLASSIFICATION_ERROR,
|
|
24
24
|
CLASSIFICATION_OK,
|
|
25
|
+
CLASSIFICATION_TIMEOUT,
|
|
25
26
|
CLASSIFICATION_USAGE_LIMIT,
|
|
26
27
|
DEBUG_FILE_FLAG,
|
|
27
28
|
DEFAULT_ROLE,
|
|
28
|
-
DEFAULT_WORKER_MAX_TURNS,
|
|
29
29
|
DEFAULT_WORKER_TIMEOUT_SECONDS,
|
|
30
30
|
DISABLE_WEB_SEARCH_FLAG,
|
|
31
31
|
DISALLOWED_TOOLS_FLAG,
|
|
32
32
|
LEADER_SOCKET_FILENAME_PREFIX,
|
|
33
33
|
LEADER_SOCKET_FILENAME_SUFFIX,
|
|
34
|
+
MAX_TURNS_FLAG,
|
|
35
|
+
MAXIMUM_WORKER_TIMEOUT_SECONDS,
|
|
34
36
|
OUTPUT_FILENAME_PREFIX,
|
|
35
37
|
PROMPT_FILENAME_PREFIX,
|
|
36
38
|
READONLY_DISALLOWED_TOOLS_VALUE,
|
|
@@ -53,7 +55,6 @@ from dev_env_scripts_constants.grok_worker_constants import ( # noqa: E402
|
|
|
53
55
|
TOOL_PROFILE_READONLY,
|
|
54
56
|
UTF8_ENCODING,
|
|
55
57
|
WORKER_SPEC_AGENT_NAME_KEY,
|
|
56
|
-
WORKER_SPEC_MAX_TURNS_KEY,
|
|
57
58
|
WORKER_SPEC_PROMPT_PARTS_KEY,
|
|
58
59
|
WORKER_SPEC_TIMEOUT_KEY,
|
|
59
60
|
)
|
|
@@ -61,8 +62,10 @@ from dev_env_scripts_constants.timing import WORKER_STAGGER_SECONDS # noqa: E40
|
|
|
61
62
|
from grok_headless_runner import GrokRunnerOutcome # noqa: E402
|
|
62
63
|
from grok_worker_preflight import PreflightOutcome # noqa: E402
|
|
63
64
|
|
|
65
|
+
RETIRED_MAX_TURNS_KEYWORD = "max_turns"
|
|
64
66
|
FIXTURE_REPORT_TEXT = '{"status":"done","role":"investigator"}'
|
|
65
67
|
FIXTURE_USAGE_LIMIT_TEXT = "rate limit exceeded (HTTP 429): quota exceeded"
|
|
68
|
+
FIXTURE_TIMEOUT_KILL_TEXT = "worker exceeded its timeout and was killed"
|
|
66
69
|
|
|
67
70
|
|
|
68
71
|
def _write_prompt_parts(
|
|
@@ -871,7 +874,7 @@ def test_load_batch_spec_missing_worker_keys_raise_value_error(
|
|
|
871
874
|
).lower() or "must be" in str(raised_error.value).lower()
|
|
872
875
|
|
|
873
876
|
|
|
874
|
-
def
|
|
877
|
+
def test_load_batch_spec_rejects_non_positive_timeout(
|
|
875
878
|
tmp_path: Path,
|
|
876
879
|
) -> None:
|
|
877
880
|
header_part, body_part = _write_prompt_parts(tmp_path)
|
|
@@ -891,27 +894,11 @@ def test_load_batch_spec_rejects_non_positive_timeout_and_max_turns(
|
|
|
891
894
|
zero_timeout_dir,
|
|
892
895
|
all_worker_payloads=[zero_timeout_payload],
|
|
893
896
|
)
|
|
894
|
-
with pytest.raises(ValueError, match=
|
|
897
|
+
with pytest.raises(ValueError, match="MIN_WORKER_TIMEOUT_SECONDS"):
|
|
895
898
|
batch.load_batch_spec(zero_timeout_path)
|
|
896
899
|
|
|
897
|
-
negative_turns_dir = tmp_path / "negative-turns"
|
|
898
|
-
negative_turns_dir.mkdir()
|
|
899
|
-
negative_turns_payload = _worker_payload(
|
|
900
|
-
role_name="negative-turns",
|
|
901
|
-
all_prompt_parts=[str(header_part), str(body_part)],
|
|
902
|
-
working_directory=working_directory,
|
|
903
|
-
tool_profile=TOOL_PROFILE_BUILD,
|
|
904
|
-
)
|
|
905
|
-
negative_turns_payload[WORKER_SPEC_MAX_TURNS_KEY] = -1
|
|
906
|
-
negative_turns_path = _write_batch_spec(
|
|
907
|
-
negative_turns_dir,
|
|
908
|
-
all_worker_payloads=[negative_turns_payload],
|
|
909
|
-
)
|
|
910
|
-
with pytest.raises(ValueError, match=WORKER_SPEC_MAX_TURNS_KEY):
|
|
911
|
-
batch.load_batch_spec(negative_turns_path)
|
|
912
|
-
|
|
913
900
|
|
|
914
|
-
def
|
|
901
|
+
def test_load_batch_spec_accepts_the_default_timeout(
|
|
915
902
|
tmp_path: Path,
|
|
916
903
|
) -> None:
|
|
917
904
|
header_part, body_part = _write_prompt_parts(tmp_path)
|
|
@@ -931,7 +918,223 @@ def test_load_batch_spec_accepts_default_timeout_and_max_turns(
|
|
|
931
918
|
|
|
932
919
|
assert len(batch_spec.all_workers) == 1
|
|
933
920
|
assert batch_spec.all_workers[0].timeout_seconds == DEFAULT_WORKER_TIMEOUT_SECONDS
|
|
934
|
-
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
def test_timeout_over_the_ceiling_is_refused_and_at_the_ceiling_passes(
|
|
924
|
+
tmp_path: Path,
|
|
925
|
+
) -> None:
|
|
926
|
+
"""The launcher refuses a spec past the 90-minute ceiling; it never clamps.
|
|
927
|
+
|
|
928
|
+
::
|
|
929
|
+
|
|
930
|
+
timeout_seconds 5401 flag: ValueError naming MAXIMUM_WORKER_TIMEOUT_SECONDS
|
|
931
|
+
timeout_seconds 5400 ok: loads, value untouched
|
|
932
|
+
timeout_seconds 30 ok: loads, value untouched
|
|
933
|
+
"""
|
|
934
|
+
header_part, body_part = _write_prompt_parts(tmp_path)
|
|
935
|
+
working_directory = tmp_path / "project"
|
|
936
|
+
working_directory.mkdir()
|
|
937
|
+
|
|
938
|
+
over_ceiling_directory = tmp_path / "over-ceiling"
|
|
939
|
+
over_ceiling_directory.mkdir()
|
|
940
|
+
over_ceiling_path = _write_batch_spec(
|
|
941
|
+
over_ceiling_directory,
|
|
942
|
+
all_worker_payloads=[
|
|
943
|
+
_worker_payload(
|
|
944
|
+
role_name="over-ceiling",
|
|
945
|
+
all_prompt_parts=[str(header_part), str(body_part)],
|
|
946
|
+
working_directory=working_directory,
|
|
947
|
+
tool_profile=TOOL_PROFILE_BUILD,
|
|
948
|
+
timeout_seconds=MAXIMUM_WORKER_TIMEOUT_SECONDS + 1,
|
|
949
|
+
)
|
|
950
|
+
],
|
|
951
|
+
)
|
|
952
|
+
with pytest.raises(ValueError, match="MAXIMUM_WORKER_TIMEOUT_SECONDS"):
|
|
953
|
+
batch.load_batch_spec(over_ceiling_path)
|
|
954
|
+
|
|
955
|
+
at_ceiling_directory = tmp_path / "at-ceiling"
|
|
956
|
+
at_ceiling_directory.mkdir()
|
|
957
|
+
at_ceiling_path = _write_batch_spec(
|
|
958
|
+
at_ceiling_directory,
|
|
959
|
+
all_worker_payloads=[
|
|
960
|
+
_worker_payload(
|
|
961
|
+
role_name="at-ceiling",
|
|
962
|
+
all_prompt_parts=[str(header_part), str(body_part)],
|
|
963
|
+
working_directory=working_directory,
|
|
964
|
+
tool_profile=TOOL_PROFILE_BUILD,
|
|
965
|
+
timeout_seconds=MAXIMUM_WORKER_TIMEOUT_SECONDS,
|
|
966
|
+
),
|
|
967
|
+
_worker_payload(
|
|
968
|
+
role_name="well-under-ceiling",
|
|
969
|
+
all_prompt_parts=[str(header_part), str(body_part)],
|
|
970
|
+
working_directory=working_directory,
|
|
971
|
+
tool_profile=TOOL_PROFILE_BUILD,
|
|
972
|
+
timeout_seconds=30,
|
|
973
|
+
),
|
|
974
|
+
],
|
|
975
|
+
)
|
|
976
|
+
at_ceiling_spec = batch.load_batch_spec(at_ceiling_path)
|
|
977
|
+
|
|
978
|
+
assert at_ceiling_spec.all_workers[0].timeout_seconds == (
|
|
979
|
+
MAXIMUM_WORKER_TIMEOUT_SECONDS
|
|
980
|
+
)
|
|
981
|
+
assert at_ceiling_spec.all_workers[1].timeout_seconds == 30
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
def test_ceiling_timeout_reaches_the_runner_untouched(
|
|
985
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
986
|
+
) -> None:
|
|
987
|
+
header_part, body_part = _write_prompt_parts(tmp_path, role_marker="long-worker")
|
|
988
|
+
working_directory = tmp_path / "project"
|
|
989
|
+
working_directory.mkdir()
|
|
990
|
+
run_state_directory = tmp_path / "run-state"
|
|
991
|
+
batch_spec = batch.load_batch_spec(
|
|
992
|
+
_write_batch_spec(
|
|
993
|
+
tmp_path,
|
|
994
|
+
all_worker_payloads=[
|
|
995
|
+
_worker_payload(
|
|
996
|
+
role_name="long-worker",
|
|
997
|
+
all_prompt_parts=[str(header_part), str(body_part)],
|
|
998
|
+
working_directory=working_directory,
|
|
999
|
+
tool_profile=TOOL_PROFILE_READONLY,
|
|
1000
|
+
timeout_seconds=MAXIMUM_WORKER_TIMEOUT_SECONDS,
|
|
1001
|
+
)
|
|
1002
|
+
],
|
|
1003
|
+
)
|
|
1004
|
+
)
|
|
1005
|
+
recorder = _RunnerRecorder({"long-worker": _ok_outcome()})
|
|
1006
|
+
monkeypatch.setattr(
|
|
1007
|
+
batch, "batch_preflight", lambda **_kwargs: PreflightOutcome(True, None)
|
|
1008
|
+
)
|
|
1009
|
+
monkeypatch.setattr(batch, "batch_headless_runner", recorder)
|
|
1010
|
+
monkeypatch.setattr(batch, "batch_sleep", lambda _seconds: None)
|
|
1011
|
+
|
|
1012
|
+
batch.run_grok_batch(
|
|
1013
|
+
batch_spec=batch_spec,
|
|
1014
|
+
run_state_directory=run_state_directory,
|
|
1015
|
+
)
|
|
1016
|
+
|
|
1017
|
+
assert recorder.all_keyword_arguments[0]["timeout_seconds"] == (
|
|
1018
|
+
MAXIMUM_WORKER_TIMEOUT_SECONDS
|
|
1019
|
+
)
|
|
1020
|
+
|
|
1021
|
+
|
|
1022
|
+
def test_worker_invocations_carry_no_turn_cap(
|
|
1023
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
1024
|
+
) -> None:
|
|
1025
|
+
header_part, body_part = _write_prompt_parts(tmp_path, role_marker="uncapped")
|
|
1026
|
+
working_directory = tmp_path / "project"
|
|
1027
|
+
working_directory.mkdir()
|
|
1028
|
+
run_state_directory = tmp_path / "run-state"
|
|
1029
|
+
batch_spec = batch.load_batch_spec(
|
|
1030
|
+
_write_batch_spec(
|
|
1031
|
+
tmp_path,
|
|
1032
|
+
all_worker_payloads=[
|
|
1033
|
+
_worker_payload(
|
|
1034
|
+
role_name="uncapped",
|
|
1035
|
+
all_prompt_parts=[str(header_part), str(body_part)],
|
|
1036
|
+
working_directory=working_directory,
|
|
1037
|
+
tool_profile=TOOL_PROFILE_READONLY,
|
|
1038
|
+
)
|
|
1039
|
+
],
|
|
1040
|
+
)
|
|
1041
|
+
)
|
|
1042
|
+
recorder = _RunnerRecorder({"uncapped": _ok_outcome()})
|
|
1043
|
+
monkeypatch.setattr(
|
|
1044
|
+
batch, "batch_preflight", lambda **_kwargs: PreflightOutcome(True, None)
|
|
1045
|
+
)
|
|
1046
|
+
monkeypatch.setattr(batch, "batch_headless_runner", recorder)
|
|
1047
|
+
monkeypatch.setattr(batch, "batch_sleep", lambda _seconds: None)
|
|
1048
|
+
|
|
1049
|
+
batch.run_grok_batch(
|
|
1050
|
+
batch_spec=batch_spec,
|
|
1051
|
+
run_state_directory=run_state_directory,
|
|
1052
|
+
)
|
|
1053
|
+
|
|
1054
|
+
launched_keyword_arguments = recorder.all_keyword_arguments[0]
|
|
1055
|
+
all_extra_arguments = launched_keyword_arguments["all_extra_arguments"]
|
|
1056
|
+
assert RETIRED_MAX_TURNS_KEYWORD not in launched_keyword_arguments
|
|
1057
|
+
assert isinstance(all_extra_arguments, tuple)
|
|
1058
|
+
assert MAX_TURNS_FLAG not in all_extra_arguments
|
|
1059
|
+
|
|
1060
|
+
|
|
1061
|
+
def test_timed_out_worker_reads_as_timeout_beside_a_completed_worker(
|
|
1062
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
1063
|
+
tmp_path: Path,
|
|
1064
|
+
capsys: pytest.CaptureFixture[str],
|
|
1065
|
+
) -> None:
|
|
1066
|
+
"""A killed worker stays distinguishable from a completed one in the summary.
|
|
1067
|
+
|
|
1068
|
+
::
|
|
1069
|
+
|
|
1070
|
+
killed worker ok: classification timeout, is_ok False, exit code 1
|
|
1071
|
+
completed worker ok: classification ok, is_ok True
|
|
1072
|
+
"""
|
|
1073
|
+
header_done, body_done = _write_prompt_parts(tmp_path, role_marker="done-worker")
|
|
1074
|
+
header_killed, body_killed = _write_prompt_parts(
|
|
1075
|
+
tmp_path, role_marker="killed-worker"
|
|
1076
|
+
)
|
|
1077
|
+
working_directory = tmp_path / "project"
|
|
1078
|
+
working_directory.mkdir()
|
|
1079
|
+
run_state_directory = tmp_path / "run-state"
|
|
1080
|
+
specification_path = _write_batch_spec(
|
|
1081
|
+
tmp_path,
|
|
1082
|
+
all_worker_payloads=[
|
|
1083
|
+
_worker_payload(
|
|
1084
|
+
role_name="done-worker",
|
|
1085
|
+
all_prompt_parts=[str(header_done), str(body_done)],
|
|
1086
|
+
working_directory=working_directory,
|
|
1087
|
+
tool_profile=TOOL_PROFILE_BUILD,
|
|
1088
|
+
),
|
|
1089
|
+
_worker_payload(
|
|
1090
|
+
role_name="killed-worker",
|
|
1091
|
+
all_prompt_parts=[str(header_killed), str(body_killed)],
|
|
1092
|
+
working_directory=working_directory,
|
|
1093
|
+
tool_profile=TOOL_PROFILE_BUILD,
|
|
1094
|
+
),
|
|
1095
|
+
],
|
|
1096
|
+
)
|
|
1097
|
+
recorder = _RunnerRecorder(
|
|
1098
|
+
{
|
|
1099
|
+
"done-worker": _ok_outcome(),
|
|
1100
|
+
"killed-worker": GrokRunnerOutcome(
|
|
1101
|
+
is_ok=False,
|
|
1102
|
+
returncode=-9,
|
|
1103
|
+
classification=CLASSIFICATION_TIMEOUT,
|
|
1104
|
+
stdout="",
|
|
1105
|
+
stderr=FIXTURE_TIMEOUT_KILL_TEXT,
|
|
1106
|
+
),
|
|
1107
|
+
}
|
|
1108
|
+
)
|
|
1109
|
+
monkeypatch.setattr(
|
|
1110
|
+
batch, "batch_preflight", lambda **_kwargs: PreflightOutcome(True, None)
|
|
1111
|
+
)
|
|
1112
|
+
monkeypatch.setattr(batch, "batch_headless_runner", recorder)
|
|
1113
|
+
monkeypatch.setattr(batch, "batch_sleep", lambda _seconds: None)
|
|
1114
|
+
|
|
1115
|
+
exit_code = batch.main(
|
|
1116
|
+
[
|
|
1117
|
+
"--spec",
|
|
1118
|
+
str(specification_path),
|
|
1119
|
+
"--run-temp-dir",
|
|
1120
|
+
str(run_state_directory),
|
|
1121
|
+
]
|
|
1122
|
+
)
|
|
1123
|
+
|
|
1124
|
+
summary_payload = json.loads(capsys.readouterr().out)
|
|
1125
|
+
payload_by_role_name = {
|
|
1126
|
+
each_payload[SUMMARY_ROLE_NAME_KEY]: each_payload
|
|
1127
|
+
for each_payload in summary_payload[SUMMARY_WORKERS_KEY]
|
|
1128
|
+
}
|
|
1129
|
+
killed_payload = payload_by_role_name["killed-worker"]
|
|
1130
|
+
done_payload = payload_by_role_name["done-worker"]
|
|
1131
|
+
|
|
1132
|
+
assert exit_code == 1
|
|
1133
|
+
assert killed_payload[SUMMARY_CLASSIFICATION_KEY] == CLASSIFICATION_TIMEOUT
|
|
1134
|
+
assert killed_payload[SUMMARY_IS_OK_KEY] is False
|
|
1135
|
+
assert killed_payload[SUMMARY_CLASSIFICATION_KEY] != CLASSIFICATION_OK
|
|
1136
|
+
assert done_payload[SUMMARY_CLASSIFICATION_KEY] == CLASSIFICATION_OK
|
|
1137
|
+
assert done_payload[SUMMARY_IS_OK_KEY] is True
|
|
935
1138
|
|
|
936
1139
|
|
|
937
1140
|
def test_unwritable_report_file_keeps_the_worker_outcome(
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Specifications for the permission modes the review binary is asked for.
|
|
2
|
+
|
|
3
|
+
The review binary accepts one permission mode for an ordinary caller and a
|
|
4
|
+
different one for root. Naming the wrong mode means the binary refuses the
|
|
5
|
+
call, no review runs, and no stamp is ever minted.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import importlib
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
from dev_env_scripts_constants import code_review_constants
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
BYPASS_MODE_TOKEN: str = "bypassPermissions"
|
|
19
|
+
ACCEPT_EDITS_MODE_TOKEN: str = "acceptEdits"
|
|
20
|
+
ROOT_USER_ID: int = 0
|
|
21
|
+
UNPRIVILEGED_USER_ID: int = 1000
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _permission_mode_seen_by(
|
|
25
|
+
monkeypatch: pytest.MonkeyPatch, effective_user_id: int
|
|
26
|
+
) -> str:
|
|
27
|
+
"""Read the review permission mode a caller with this user id resolves."""
|
|
28
|
+
monkeypatch.setattr(os, "geteuid", lambda: effective_user_id)
|
|
29
|
+
reloaded_constants = importlib.reload(code_review_constants)
|
|
30
|
+
return str(reloaded_constants.REVIEW_PERMISSION_MODE)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_root_detection_agrees_with_this_process(
|
|
34
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
35
|
+
) -> None:
|
|
36
|
+
monkeypatch.setattr(os, "geteuid", lambda: ROOT_USER_ID)
|
|
37
|
+
reloaded_constants = importlib.reload(code_review_constants)
|
|
38
|
+
|
|
39
|
+
assert reloaded_constants.IS_ROOT_CALLER is True
|
|
40
|
+
assert reloaded_constants.ROOT_EFFECTIVE_USER_ID == ROOT_USER_ID
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_bypass_mode_names_the_token_the_binary_reads() -> None:
|
|
44
|
+
assert code_review_constants.PERMISSION_MODE_BYPASS == BYPASS_MODE_TOKEN
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_accept_edits_mode_names_the_token_the_binary_reads() -> None:
|
|
48
|
+
assert code_review_constants.PERMISSION_MODE_ACCEPT_EDITS == ACCEPT_EDITS_MODE_TOKEN
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_a_root_caller_resolves_the_mode_the_binary_accepts(
|
|
52
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
53
|
+
) -> None:
|
|
54
|
+
assert _permission_mode_seen_by(monkeypatch, ROOT_USER_ID) == (
|
|
55
|
+
ACCEPT_EDITS_MODE_TOKEN
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_an_ordinary_caller_resolves_the_bypass_mode(
|
|
60
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
61
|
+
) -> None:
|
|
62
|
+
assert _permission_mode_seen_by(monkeypatch, UNPRIVILEGED_USER_ID) == (
|
|
63
|
+
BYPASS_MODE_TOKEN
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_a_platform_without_a_user_id_still_loads_the_constants(
|
|
68
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
69
|
+
) -> None:
|
|
70
|
+
"""Windows exposes no ``os.geteuid``, and the module still imports.
|
|
71
|
+
|
|
72
|
+
Reading a missing ``os.geteuid`` raises, and every caller of the review
|
|
73
|
+
invoker dies at import. A platform that reports no user id is never the
|
|
74
|
+
root the review binary refuses, so it reads as an ordinary caller.
|
|
75
|
+
"""
|
|
76
|
+
monkeypatch.delattr(os, "geteuid", raising=False)
|
|
77
|
+
reloaded_constants = importlib.reload(code_review_constants)
|
|
78
|
+
|
|
79
|
+
assert reloaded_constants.IS_ROOT_CALLER is False
|
|
80
|
+
assert reloaded_constants.REVIEW_PERMISSION_MODE == BYPASS_MODE_TOKEN
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import subprocess
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from codex_capability_bridge import translate_capability
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.mark.parametrize("source_surface, expected_target, capability_payload", [("TaskList", "update_plan", {"plan": []}), ("TaskCreate", "update_plan", {"name": "review", "status": "pending"}), ("TaskUpdate", "update_plan", {"name": "review", "status": "pending"}), ("Task", "multi_agent_v1__spawn_agent", {"prompt": "review"}), ("SendMessage", "multi_agent_v1__send_input", {"message": "review"}), ("TaskOutput", "multi_agent_v1__wait_agent", {"task_id": "task-1"}), ("TaskStop", "multi_agent_v1__close_agent", {"task_id": "task-1"})])
|
|
12
|
+
def test_maps_capability(source_surface: str, expected_target: str, capability_payload: dict[str, object]) -> None:
|
|
13
|
+
translated_record = translate_capability(source_surface, capability_payload)
|
|
14
|
+
assert translated_record["target"] == expected_target
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_normalizes_alias_and_task_payload() -> None:
|
|
18
|
+
translated_record = translate_capability("task_create", {"subject": "write tests", "status": "in-progress"})
|
|
19
|
+
assert translated_record["payload"] == {"plan": [{"step": "write tests", "status": "in_progress"}]}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_rejects_bad_payload_and_schedule() -> None:
|
|
23
|
+
assert translate_capability("TaskUpdate", {"name": "x", "status": "later"})["status"] == "malformed"
|
|
24
|
+
assert translate_capability("ScheduleWakeup", {})["status"] == "unsupported"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_rejects_unknown_and_private_paths() -> None:
|
|
28
|
+
assert translate_capability("NoSuchSurface", {})["status"] == "unknown"
|
|
29
|
+
assert translate_capability("SendMessage", {"message": "C:\\Users\\melan\\secret"})["status"] == "rejected"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@pytest.mark.parametrize("private_path", ["C:/private/file", "\\\\server\\share\\file", "/private/file", "~/secret", "$HOME/secret", "%USERPROFILE%\\secret", "safe/../secret"])
|
|
33
|
+
def test_rejects_absolute_private_and_traversal_paths(private_path: str) -> None:
|
|
34
|
+
assert translate_capability("SendMessage", {"message": private_path})["status"] == "rejected"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_validates_mapped_surface_payloads() -> None:
|
|
38
|
+
assert translate_capability("spawn", {"message": "hello"})["status"] == "malformed"
|
|
39
|
+
assert translate_capability("stop", {"task_id": 3})["status"] == "malformed"
|
|
40
|
+
assert translate_capability("TaskList", {"plan": [{"step": "review"}]})["status"] == "malformed"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_translation_is_byte_identical_for_repeated_input() -> None:
|
|
44
|
+
payload = {"message": "hello", "agent_id": "agent-1"}
|
|
45
|
+
first_json = json.dumps(translate_capability("message", payload), sort_keys=True, separators=(",", ":"))
|
|
46
|
+
second_json = json.dumps(translate_capability("message", payload), sort_keys=True, separators=(",", ":"))
|
|
47
|
+
assert first_json == second_json
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_translation_does_not_execute_payload() -> None:
|
|
51
|
+
payload = {"message": "__import__('subprocess').run('whoami')"}
|
|
52
|
+
translated_record = translate_capability("message", payload)
|
|
53
|
+
assert translated_record["payload"] == payload
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_translation_is_deterministic_and_cli_is_json() -> None:
|
|
57
|
+
payload = {"message": "hello", "agent_id": "agent-1"}
|
|
58
|
+
first_record = translate_capability("SendMessage", payload)
|
|
59
|
+
second_record = translate_capability("SendMessage", payload)
|
|
60
|
+
assert first_record == second_record
|
|
61
|
+
script_path = Path(__file__).parents[1] / "codex_capability_bridge.py"
|
|
62
|
+
cli_run = subprocess.run([sys.executable, str(script_path), "SendMessage", json.dumps(payload)], capture_output=True, text=True, check=True)
|
|
63
|
+
assert json.loads(cli_run.stdout) == first_record
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_cli_rejects_malformed_payload_with_one_json_error_record() -> None:
|
|
67
|
+
script_path = Path(__file__).parents[1] / "codex_capability_bridge.py"
|
|
68
|
+
cli_run = subprocess.run(
|
|
69
|
+
[sys.executable, str(script_path), "SendMessage", "{"],
|
|
70
|
+
capture_output=True,
|
|
71
|
+
text=True,
|
|
72
|
+
)
|
|
73
|
+
assert cli_run.returncode != 0
|
|
74
|
+
assert json.loads(cli_run.stdout) == {
|
|
75
|
+
"source": "unknown",
|
|
76
|
+
"status": "error",
|
|
77
|
+
"error": "payload must be valid JSON",
|
|
78
|
+
}
|
|
79
|
+
assert cli_run.stderr == ""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_cli_rejects_malformed_arguments_with_one_json_error_record() -> None:
|
|
83
|
+
script_path = Path(__file__).parents[1] / "codex_capability_bridge.py"
|
|
84
|
+
cli_run = subprocess.run(
|
|
85
|
+
[sys.executable, str(script_path)],
|
|
86
|
+
capture_output=True,
|
|
87
|
+
text=True,
|
|
88
|
+
)
|
|
89
|
+
assert cli_run.returncode != 0
|
|
90
|
+
assert json.loads(cli_run.stdout)["status"] == "error"
|
|
91
|
+
assert cli_run.stderr == ""
|