claude-dev-env 2.4.0 → 2.7.0
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 +26 -59
- package/_shared/pr-loop/scripts/_claude_permissions_common.py +84 -0
- package/_shared/pr-loop/scripts/code_rules_gate.py +6 -3
- 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/grant_project_claude_permissions.py +306 -306
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +44 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +202 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -24
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/stale_worktree_rule_sweep_constants.py +107 -107
- package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +290 -48
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +42 -2
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +36 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +100 -1
- package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -497
- package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +311 -2
- package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep.py +301 -301
- package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep_constants.py +85 -85
- package/_shared/pr-loop/worker-spawn.md +1 -1
- package/agents/CLAUDE.md +3 -2
- package/agents/caveman.md +0 -1
- package/agents/clasp-deployment-orchestrator.md +0 -1
- package/agents/clean-coder.md +0 -1
- package/agents/code-advisor.md +0 -1
- package/agents/code-quality-agent.md +1 -2
- package/agents/code-verifier.md +36 -8
- package/agents/deep-research.md +0 -1
- package/agents/docs-agent.md +0 -1
- package/agents/git-commit-crafter.md +0 -1
- package/agents/issue-tracker.md +42 -0
- package/agents/plan-packet-validator.md +0 -1
- package/agents/pr-description-writer.md +0 -1
- package/agents/test_agent_frontmatter.py +67 -18
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +143 -141
- package/bin/CLAUDE.md +68 -5
- package/bin/codex-compat.mjs +104 -0
- package/bin/codex-compat.test.mjs +51 -0
- package/bin/ever-shipped-skills.mjs +1 -0
- package/bin/install-constants.mjs +88 -0
- package/bin/install.mjs +1138 -114
- package/bin/install.prune.test.mjs +869 -19
- package/bin/install.test.mjs +906 -2
- package/codex-capability-map.json +13 -0
- package/commands/implement.md +1 -1
- package/commands/right-size.md +1 -1
- package/docs/CLAUDE.md +1 -0
- package/docs/CODE_RULES.md +2 -0
- package/docs/codex-compatibility.md +25 -0
- package/docs/host-pool-health-monitor.md +102 -0
- package/docs/nas-ssh-invocation.md +96 -12
- package/docs/references/CLAUDE.md +4 -2
- package/docs/references/advisor-tool.md +13 -0
- package/docs/references/code-review-enforcement.md +35 -0
- package/docs/references/team-advisor-skill.md +14 -0
- package/hooks/blocking/CLAUDE.md +4 -0
- package/hooks/blocking/code_review_pr_create_gate.py +7 -3
- package/hooks/blocking/code_review_push_gate.py +9 -4
- package/hooks/blocking/code_review_stamp_directory_write_blocker.py +8 -0
- package/hooks/blocking/config/__init__.py +5 -5
- package/hooks/blocking/config/code_review_enforcement_constants.py +40 -7
- package/hooks/blocking/config/test_code_review_enforcement_constants.py +58 -0
- package/hooks/blocking/config/verified_commit_constants.py +160 -159
- 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/orchestrator_refresh_reschedule_gate.py +256 -0
- package/hooks/blocking/pre_tool_use_dispatcher.py +24 -24
- 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_code_review_pr_create_gate.py +14 -0
- package/hooks/blocking/test_code_review_push_gate.py +16 -0
- package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +19 -0
- package/hooks/blocking/test_eli11_reply_enforcer.py +457 -0
- package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +231 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -1
- 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 +804 -808
- package/hooks/blocking/test_verification_verdict_store.py +54 -0
- package/hooks/blocking/test_verified_commit_gate.py +581 -581
- package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -131
- package/hooks/blocking/unscoped_search_blocker.py +391 -0
- package/hooks/blocking/verdict_directory_write_blocker.py +687 -687
- package/hooks/blocking/verification_verdict_store.py +1039 -1036
- package/hooks/blocking/verified_commit_message_accuracy_blocker.py +167 -167
- package/hooks/blocking/verifier_verdict_minter.py +280 -280
- 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 +257 -23
- package/hooks/git-hooks/test_pre_push_base_reference.py +339 -0
- package/hooks/hooks.json +10 -12
- package/hooks/hooks_constants/CLAUDE.md +7 -2
- 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/enter_worktree_prefetch_constants.py +18 -18
- package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +2 -8
- package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +48 -0
- package/hooks/hooks_constants/ruff_integration_constants.py +16 -0
- 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/hooks/lifecycle/enter_worktree_origin_prefetch.py +163 -146
- package/hooks/lifecycle/test_enter_worktree_origin_prefetch.py +185 -178
- package/hooks/pyproject.toml +1 -0
- package/hooks/validators/CLAUDE.md +1 -0
- package/hooks/validators/config/__init__.py +0 -0
- package/hooks/validators/config/directory_exemption_constants.py +183 -0
- package/hooks/validators/config/test_directory_exemption_constants.py +21 -0
- package/hooks/validators/conftest.py +4 -0
- package/hooks/validators/ruff_integration.py +49 -5
- package/hooks/validators/run_all_validators.py +206 -9
- package/hooks/validators/test_directory_exemption_constants.py +185 -0
- package/hooks/validators/test_python_antipattern_checks.py +110 -5
- package/hooks/validators/test_ruff_integration.py +92 -1
- package/hooks/validators/test_run_all_validators.py +115 -68
- package/hooks/validators/test_run_all_validators_pretooluse.py +159 -1
- package/package.json +13 -3
- package/rules/CLAUDE.md +17 -22
- 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 +53 -44
- 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 +1 -0
- package/scripts/Capture-PoolHealth.ps1 +410 -0
- package/scripts/Migrate-ShellPolicy.ps1 +1 -1
- package/scripts/_code_review_test_support.py +404 -0
- package/scripts/claude_chain_runner.py +141 -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/conftest.py +16 -1
- package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -1
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +9 -0
- package/scripts/dev_env_scripts_constants/code_review_constants.py +37 -0
- package/scripts/invoke_code_review.py +11 -4
- package/scripts/resolve_worker_spawn.py +626 -626
- package/scripts/spawn_grok_batch.py +672 -672
- package/scripts/sync_to_cursor/rules.py +0 -10
- package/scripts/test_claude_chain_runner.py +131 -0
- package/scripts/test_invoke_code_review.py +85 -908
- package/scripts/test_invoke_code_review_chain.py +70 -0
- package/scripts/test_invoke_code_review_cli.py +192 -0
- package/scripts/test_invoke_code_review_contract.py +256 -0
- package/scripts/test_invoke_code_review_git.py +123 -0
- package/scripts/test_invoke_code_review_mode.py +99 -0
- package/scripts/test_resolve_worker_spawn.py +1014 -1014
- 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/CLAUDE.md +2 -0
- package/skills/auditing-claude-config/SKILL.md +114 -114
- package/skills/autoconverge/SKILL.md +427 -427
- package/skills/autoconverge/reference/convergence.md +24 -3
- package/skills/autoconverge/workflow/CLAUDE.md +1 -0
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +3 -3
- package/skills/autoconverge/workflow/converge.contract.test.mjs +1263 -1263
- package/skills/autoconverge/workflow/converge.mjs +168 -1
- package/skills/autoconverge/workflow/converge.p2-advance.test.mjs +202 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +265 -265
- package/skills/bugteam/reference/copilot-gap-analysis.md +1 -1
- package/skills/closeout/SKILL.md +33 -50
- package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +8 -0
- package/skills/codex-review/scripts/run_codex_review.py +233 -1
- package/skills/codex-review/scripts/test_run_codex_review.py +189 -0
- package/skills/condensing-instructions/SKILL.md +72 -0
- package/skills/copilot-review/SKILL.md +119 -119
- package/skills/e-code-review/SKILL.md +52 -0
- package/skills/e-code-review/reference/fix.md +54 -0
- package/skills/e-code-review/reference/loop.md +43 -0
- package/skills/e-code-review/reference/low.md +57 -0
- package/skills/e-code-review/reference/medium.md +153 -0
- package/skills/e-code-review/reference/xhigh.md +182 -0
- package/skills/e-simplify/SKILL.md +97 -0
- 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/issue-tracker/SKILL.md +92 -0
- package/skills/issue-tracker/reference/epic-and-sub-issue-model.md +55 -0
- package/skills/issue-tracker/reference/handoff-schema.md +64 -0
- package/skills/issue-tracker/reference/operation-matrix.md +41 -0
- package/skills/orchestrator/SKILL.md +177 -22
- package/skills/orchestrator/scripts/status_gate.py +625 -0
- package/skills/orchestrator/scripts/status_gate_constants/__init__.py +1 -0
- package/skills/orchestrator/scripts/status_gate_constants/config/__init__.py +1 -0
- package/skills/orchestrator/scripts/status_gate_constants/config/constants.py +47 -0
- package/skills/orchestrator/scripts/test_status_gate.py +439 -0
- package/skills/orchestrator-refresh/SKILL.md +129 -35
- package/skills/plan-to-pr/SKILL.md +155 -0
- package/skills/plan-to-pr/reference/final-validation-tasks.md +15 -0
- package/skills/plan-to-pr/reference/model-routing.md +36 -0
- package/skills/plan-to-pr/reference/packet-contract.md +43 -0
- package/skills/plan-to-pr/reference/packet-schema.json +57 -0
- package/skills/plan-to-pr/reference/process-inventory.md +22 -0
- package/skills/plan-to-pr/reference/review-loop.md +33 -0
- package/skills/plan-to-pr/reference/run-record.schema.json +27 -0
- package/skills/plan-to-pr/reference/self-audit-tasks.md +15 -0
- package/skills/plan-to-pr/reference/task-seeds.md +14 -0
- package/skills/plan-to-pr/reference/task-ticket.md +38 -0
- package/skills/plan-to-pr/scripts/config/__init__.py +1 -0
- package/skills/plan-to-pr/scripts/config/constants.py +193 -0
- package/skills/plan-to-pr/scripts/create_packet.py +173 -0
- package/skills/plan-to-pr/scripts/test_create_packet.py +102 -0
- package/skills/plan-to-pr/scripts/test_validate_packet.py +256 -0
- package/skills/plan-to-pr/scripts/test_validate_protocol.py +135 -0
- package/skills/plan-to-pr/scripts/test_validate_run.py +158 -0
- package/skills/plan-to-pr/scripts/validate_packet.py +655 -0
- package/skills/plan-to-pr/scripts/validate_protocol.py +622 -0
- package/skills/plan-to-pr/scripts/validate_run.py +173 -0
- package/skills/plan-to-pr/test_skill_contract.py +207 -0
- package/skills/plan-to-pr/test_task_ticket_contract.py +151 -0
- package/skills/pr-converge/SKILL.md +472 -469
- package/skills/pr-converge/reference/examples.md +3 -3
- package/skills/pr-converge/reference/fix-protocol.md +1 -1
- package/skills/pr-converge/reference/ground-rules.md +7 -4
- package/skills/pr-converge/reference/multi-pr-orchestration.md +4 -1
- package/skills/pr-converge/reference/per-tick.md +5 -5
- package/skills/pr-converge/reference/progress-checklist.md +1 -1
- package/skills/pr-converge/scripts/check_convergence_gates.py +279 -279
- package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -507
- package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -84
- package/skills/pr-converge/test_step5_host_branch.py +1 -1
- package/skills/pr-fix-protocol/SKILL.md +1 -1
- package/skills/privacy-hygiene/SKILL.md +68 -68
- package/skills/privacy-hygiene/reference/sweep-procedure.md +1 -1
- package/skills/prototype/workflows/promotion.md +1 -1
- package/skills/release-notes-html/SKILL.md +164 -0
- package/skills/session-log/SKILL.md +1 -1
- package/skills/task-build/CLAUDE.md +8 -7
- package/skills/task-build/SKILL.md +16 -8
- package/skills/task-build/reference/tool-routing.md +19 -0
- 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/tdd.md +0 -7
- package/skills/closeout/reference/issue-body-templates.md +0 -108
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Chain-invocation argv assembly, empty stdin, and working directory."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
import invoke_code_review as invoker
|
|
10
|
+
from _code_review_test_support import (
|
|
11
|
+
FIXTURE_SESSION_OPUS,
|
|
12
|
+
FIXTURE_SESSION_SONNET,
|
|
13
|
+
HOST_PROFILE_THIRD_PARTY,
|
|
14
|
+
claude_served,
|
|
15
|
+
init_git_repository,
|
|
16
|
+
install_seams,
|
|
17
|
+
run_review,
|
|
18
|
+
)
|
|
19
|
+
from dev_env_scripts_constants.code_review_constants import (
|
|
20
|
+
CODE_REVIEW_MODEL_ALIAS,
|
|
21
|
+
DEFAULT_CODE_REVIEW_EFFORT,
|
|
22
|
+
REVIEW_PERMISSION_MODE as PERMISSION_MODE_BYPASS,
|
|
23
|
+
PERMISSION_MODE_FLAG,
|
|
24
|
+
)
|
|
25
|
+
from dev_env_scripts_constants.grok_worker_constants import (
|
|
26
|
+
MODEL_FLAG,
|
|
27
|
+
OUTPUT_FORMAT_FLAG,
|
|
28
|
+
OUTPUT_FORMAT_JSON,
|
|
29
|
+
SINGLE_TURN_FLAG,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_chain_argv_assembly(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
34
|
+
working_directory = init_git_repository(tmp_path / "repo")
|
|
35
|
+
call_log = install_seams(
|
|
36
|
+
monkeypatch,
|
|
37
|
+
host_profile=HOST_PROFILE_THIRD_PARTY,
|
|
38
|
+
claude_outcome=claude_served(),
|
|
39
|
+
working_directory=working_directory,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
run_review(working_directory, session_model=FIXTURE_SESSION_SONNET)
|
|
43
|
+
|
|
44
|
+
assert call_log.claude_arguments == [
|
|
45
|
+
SINGLE_TURN_FLAG,
|
|
46
|
+
invoker.build_code_review_prompt(DEFAULT_CODE_REVIEW_EFFORT),
|
|
47
|
+
MODEL_FLAG,
|
|
48
|
+
CODE_REVIEW_MODEL_ALIAS,
|
|
49
|
+
OUTPUT_FORMAT_FLAG,
|
|
50
|
+
OUTPUT_FORMAT_JSON,
|
|
51
|
+
PERMISSION_MODE_FLAG,
|
|
52
|
+
PERMISSION_MODE_BYPASS,
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_chain_redirects_empty_stdin_and_sets_cwd(
|
|
57
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
58
|
+
) -> None:
|
|
59
|
+
working_directory = init_git_repository(tmp_path / "repo")
|
|
60
|
+
call_log = install_seams(
|
|
61
|
+
monkeypatch,
|
|
62
|
+
host_profile=HOST_PROFILE_THIRD_PARTY,
|
|
63
|
+
claude_outcome=claude_served(),
|
|
64
|
+
working_directory=working_directory,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
run_review(working_directory, session_model=FIXTURE_SESSION_OPUS)
|
|
68
|
+
|
|
69
|
+
assert call_log.is_stdin_empty is True
|
|
70
|
+
assert call_log.claude_working_directory == working_directory
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""CLI JSON output for in-session, error, effort, and record-stamp paths."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
import invoke_code_review as invoker
|
|
11
|
+
from _code_review_test_support import (
|
|
12
|
+
DriftingReview,
|
|
13
|
+
EFFORT_LOW,
|
|
14
|
+
FIXTURE_CHAIN_CONFIG_ERROR_MESSAGE,
|
|
15
|
+
FIXTURE_HOST_PROFILE_ERROR_MESSAGE,
|
|
16
|
+
FIXTURE_SESSION_OPUS,
|
|
17
|
+
HOST_PROFILE_CLAUDE,
|
|
18
|
+
HOST_PROFILE_THIRD_PARTY,
|
|
19
|
+
REJECTED_ULTRA_EFFORT,
|
|
20
|
+
init_git_repository,
|
|
21
|
+
install_seams,
|
|
22
|
+
prepared_surface_repo,
|
|
23
|
+
run_record_stamp_cli,
|
|
24
|
+
run_review_cli,
|
|
25
|
+
)
|
|
26
|
+
from claude_chain_runner import ChainConfigurationError
|
|
27
|
+
from dev_env_scripts_constants.claude_chain_constants import (
|
|
28
|
+
CHAIN_CONFIG_ERROR_EXIT_CODE,
|
|
29
|
+
)
|
|
30
|
+
from dev_env_scripts_constants.code_review_constants import (
|
|
31
|
+
CLI_SESSION_MODEL_FLAG,
|
|
32
|
+
HOST_PROFILE_ERROR_RETURNCODE,
|
|
33
|
+
IN_SESSION_RETURNCODE,
|
|
34
|
+
INVALID_EFFORT_RETURNCODE,
|
|
35
|
+
MAXIMUM_STAMP_MINT_PASSES,
|
|
36
|
+
MODE_CHAIN,
|
|
37
|
+
MODE_IN_SESSION,
|
|
38
|
+
RESULT_KEY_BOUND_HASH,
|
|
39
|
+
RESULT_KEY_DIRTY_TREE,
|
|
40
|
+
RESULT_KEY_MODE,
|
|
41
|
+
RESULT_KEY_PASS_COUNT,
|
|
42
|
+
RESULT_KEY_RETURNCODE,
|
|
43
|
+
RESULT_KEY_SERVED_COMMAND,
|
|
44
|
+
RESULT_KEY_STAMP_MINTED,
|
|
45
|
+
STAMP_DID_NOT_CONVERGE_RETURNCODE,
|
|
46
|
+
)
|
|
47
|
+
from dev_env_scripts_constants.grok_worker_constants import CWD_FLAG
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_cli_prints_result_json_only(
|
|
51
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
52
|
+
tmp_path: Path,
|
|
53
|
+
capsys: pytest.CaptureFixture[str],
|
|
54
|
+
) -> None:
|
|
55
|
+
working_directory = init_git_repository(tmp_path / "repo")
|
|
56
|
+
install_seams(
|
|
57
|
+
monkeypatch,
|
|
58
|
+
host_profile=HOST_PROFILE_CLAUDE,
|
|
59
|
+
claude_outcome=None,
|
|
60
|
+
working_directory=working_directory,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
exit_code = run_review_cli(working_directory, session_model=FIXTURE_SESSION_OPUS)
|
|
64
|
+
|
|
65
|
+
assert exit_code == IN_SESSION_RETURNCODE
|
|
66
|
+
captured = capsys.readouterr()
|
|
67
|
+
assert captured.err == ""
|
|
68
|
+
parsed_payload = json.loads(captured.out)
|
|
69
|
+
assert parsed_payload == {
|
|
70
|
+
RESULT_KEY_MODE: MODE_IN_SESSION,
|
|
71
|
+
RESULT_KEY_SERVED_COMMAND: None,
|
|
72
|
+
RESULT_KEY_RETURNCODE: IN_SESSION_RETURNCODE,
|
|
73
|
+
RESULT_KEY_DIRTY_TREE: False,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_cli_emits_json_on_chain_configuration_error(
|
|
78
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
79
|
+
tmp_path: Path,
|
|
80
|
+
capsys: pytest.CaptureFixture[str],
|
|
81
|
+
) -> None:
|
|
82
|
+
working_directory = init_git_repository(tmp_path / "repo")
|
|
83
|
+
install_seams(
|
|
84
|
+
monkeypatch,
|
|
85
|
+
host_profile=HOST_PROFILE_THIRD_PARTY,
|
|
86
|
+
claude_outcome=ChainConfigurationError(FIXTURE_CHAIN_CONFIG_ERROR_MESSAGE),
|
|
87
|
+
working_directory=working_directory,
|
|
88
|
+
)
|
|
89
|
+
exit_code = run_review_cli(working_directory, session_model=FIXTURE_SESSION_OPUS)
|
|
90
|
+
|
|
91
|
+
assert exit_code == CHAIN_CONFIG_ERROR_EXIT_CODE
|
|
92
|
+
captured = capsys.readouterr()
|
|
93
|
+
parsed_payload = json.loads(captured.out)
|
|
94
|
+
assert parsed_payload == {
|
|
95
|
+
RESULT_KEY_MODE: MODE_CHAIN,
|
|
96
|
+
RESULT_KEY_SERVED_COMMAND: None,
|
|
97
|
+
RESULT_KEY_RETURNCODE: CHAIN_CONFIG_ERROR_EXIT_CODE,
|
|
98
|
+
RESULT_KEY_DIRTY_TREE: False,
|
|
99
|
+
}
|
|
100
|
+
config_error_outcome = invoker.CodeReviewOutcome(
|
|
101
|
+
mode=MODE_CHAIN,
|
|
102
|
+
served_command=None,
|
|
103
|
+
returncode=CHAIN_CONFIG_ERROR_EXIT_CODE,
|
|
104
|
+
is_dirty_tree=False,
|
|
105
|
+
)
|
|
106
|
+
assert invoker.is_code_review_clean_stamp_allowed(config_error_outcome) is False
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_cli_emits_json_on_host_profile_value_error(
|
|
110
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
111
|
+
tmp_path: Path,
|
|
112
|
+
capsys: pytest.CaptureFixture[str],
|
|
113
|
+
) -> None:
|
|
114
|
+
working_directory = init_git_repository(tmp_path / "repo")
|
|
115
|
+
|
|
116
|
+
def fake_host_profile_raises(setting_by_name: object | None = None) -> str:
|
|
117
|
+
del setting_by_name
|
|
118
|
+
raise ValueError(FIXTURE_HOST_PROFILE_ERROR_MESSAGE)
|
|
119
|
+
|
|
120
|
+
monkeypatch.setattr(
|
|
121
|
+
invoker, "review_host_profile_detector", fake_host_profile_raises
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
exit_code = run_review_cli(working_directory, session_model=FIXTURE_SESSION_OPUS)
|
|
125
|
+
|
|
126
|
+
assert exit_code == HOST_PROFILE_ERROR_RETURNCODE
|
|
127
|
+
captured = capsys.readouterr()
|
|
128
|
+
parsed_payload = json.loads(captured.out)
|
|
129
|
+
assert parsed_payload == {
|
|
130
|
+
RESULT_KEY_MODE: MODE_CHAIN,
|
|
131
|
+
RESULT_KEY_SERVED_COMMAND: None,
|
|
132
|
+
RESULT_KEY_RETURNCODE: HOST_PROFILE_ERROR_RETURNCODE,
|
|
133
|
+
RESULT_KEY_DIRTY_TREE: False,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_cli_rejects_ultra_effort_with_nonzero_exit(
|
|
138
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
139
|
+
tmp_path: Path,
|
|
140
|
+
capsys: pytest.CaptureFixture[str],
|
|
141
|
+
) -> None:
|
|
142
|
+
install_seams(
|
|
143
|
+
monkeypatch,
|
|
144
|
+
host_profile=HOST_PROFILE_CLAUDE,
|
|
145
|
+
claude_outcome=None,
|
|
146
|
+
working_directory=tmp_path,
|
|
147
|
+
)
|
|
148
|
+
exit_code = invoker.main(
|
|
149
|
+
[
|
|
150
|
+
CWD_FLAG,
|
|
151
|
+
str(tmp_path),
|
|
152
|
+
CLI_SESSION_MODEL_FLAG,
|
|
153
|
+
FIXTURE_SESSION_OPUS,
|
|
154
|
+
REJECTED_ULTRA_EFFORT,
|
|
155
|
+
]
|
|
156
|
+
)
|
|
157
|
+
assert exit_code == INVALID_EFFORT_RETURNCODE
|
|
158
|
+
assert REJECTED_ULTRA_EFFORT in capsys.readouterr().err
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_cli_record_stamp_returns_non_convergence_code_on_cap(
|
|
162
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
163
|
+
tmp_path: Path,
|
|
164
|
+
capsys: pytest.CaptureFixture[str],
|
|
165
|
+
) -> None:
|
|
166
|
+
working_directory = prepared_surface_repo(monkeypatch, tmp_path)
|
|
167
|
+
monkeypatch.setattr(invoker, "invoke_code_review", DriftingReview())
|
|
168
|
+
exit_code = run_record_stamp_cli(working_directory, effort=EFFORT_LOW)
|
|
169
|
+
assert exit_code == STAMP_DID_NOT_CONVERGE_RETURNCODE
|
|
170
|
+
parsed_payload = json.loads(capsys.readouterr().out)
|
|
171
|
+
assert parsed_payload[RESULT_KEY_STAMP_MINTED] is False
|
|
172
|
+
assert parsed_payload[RESULT_KEY_PASS_COUNT] == MAXIMUM_STAMP_MINT_PASSES
|
|
173
|
+
assert parsed_payload[RESULT_KEY_BOUND_HASH] is None
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def test_cli_record_stamp_reports_missing_store_dependency(
|
|
177
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
178
|
+
tmp_path: Path,
|
|
179
|
+
capsys: pytest.CaptureFixture[str],
|
|
180
|
+
) -> None:
|
|
181
|
+
def raise_missing_store(*all_args: object, **all_keywords: object) -> object:
|
|
182
|
+
del all_args, all_keywords
|
|
183
|
+
raise ModuleNotFoundError("store missing", name="code_review_stamp_store")
|
|
184
|
+
|
|
185
|
+
working_directory = prepared_surface_repo(monkeypatch, tmp_path)
|
|
186
|
+
monkeypatch.setattr(invoker, "load_code_review_stamp_store", raise_missing_store)
|
|
187
|
+
exit_code = run_record_stamp_cli(working_directory, effort=EFFORT_LOW)
|
|
188
|
+
assert exit_code == INVALID_EFFORT_RETURNCODE
|
|
189
|
+
captured = capsys.readouterr()
|
|
190
|
+
assert "stamp store" in captured.err
|
|
191
|
+
parsed_payload = json.loads(captured.out)
|
|
192
|
+
assert parsed_payload[RESULT_KEY_STAMP_MINTED] is False
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""Argument contract, encoding, clean-stamp rules, effort tokens, and stamps."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
import invoke_code_review as invoker
|
|
10
|
+
from _code_review_test_support import (
|
|
11
|
+
DriftingReview,
|
|
12
|
+
EFFORT_LOW,
|
|
13
|
+
FIXTURE_CHAIN_RETURNCODE,
|
|
14
|
+
FIXTURE_FAILED_RETURNCODE,
|
|
15
|
+
FIXTURE_SERVED_COMMAND,
|
|
16
|
+
FIXTURE_SESSION_OPUS,
|
|
17
|
+
HOST_PROFILE_THIRD_PARTY,
|
|
18
|
+
MISSING_STORE_FILE_NAME,
|
|
19
|
+
REJECTED_ULTRA_EFFORT,
|
|
20
|
+
SINGLE_PASS_CAP,
|
|
21
|
+
claude_failed,
|
|
22
|
+
init_git_repository,
|
|
23
|
+
install_seams,
|
|
24
|
+
prepared_surface_repo,
|
|
25
|
+
run_review,
|
|
26
|
+
stable_clean_review,
|
|
27
|
+
surface_changing_review,
|
|
28
|
+
)
|
|
29
|
+
from dev_env_scripts_constants.code_review_constants import (
|
|
30
|
+
CODE_REVIEW_MODEL_ALIAS,
|
|
31
|
+
DEFAULT_CODE_REVIEW_EFFORT,
|
|
32
|
+
IN_SESSION_RETURNCODE,
|
|
33
|
+
MAXIMUM_STAMP_MINT_PASSES,
|
|
34
|
+
MODE_CHAIN,
|
|
35
|
+
MODE_IN_SESSION,
|
|
36
|
+
REVIEW_PERMISSION_MODE as PERMISSION_MODE_BYPASS,
|
|
37
|
+
PERMISSION_MODE_FLAG,
|
|
38
|
+
RESULT_KEY_BOUND_HASH,
|
|
39
|
+
RESULT_KEY_DIRTY_TREE,
|
|
40
|
+
RESULT_KEY_MODE,
|
|
41
|
+
RESULT_KEY_PASS_COUNT,
|
|
42
|
+
RESULT_KEY_RETURNCODE,
|
|
43
|
+
RESULT_KEY_SERVED_COMMAND,
|
|
44
|
+
RESULT_KEY_STAMP_MINTED,
|
|
45
|
+
)
|
|
46
|
+
from dev_env_scripts_constants.grok_worker_constants import (
|
|
47
|
+
MODEL_FLAG,
|
|
48
|
+
OUTPUT_FORMAT_FLAG,
|
|
49
|
+
OUTPUT_FORMAT_JSON,
|
|
50
|
+
SINGLE_TURN_FLAG,
|
|
51
|
+
)
|
|
52
|
+
from dev_env_scripts_constants.timing import DEFAULT_CODE_REVIEW_TIMEOUT_SECONDS
|
|
53
|
+
|
|
54
|
+
CLEAN_SUCCESS_OUTCOME = invoker.CodeReviewOutcome(
|
|
55
|
+
mode=MODE_CHAIN,
|
|
56
|
+
served_command=FIXTURE_SERVED_COMMAND,
|
|
57
|
+
returncode=FIXTURE_CHAIN_RETURNCODE,
|
|
58
|
+
is_dirty_tree=False,
|
|
59
|
+
)
|
|
60
|
+
DIRTY_SUCCESS_OUTCOME = invoker.CodeReviewOutcome(
|
|
61
|
+
mode=MODE_CHAIN,
|
|
62
|
+
served_command=FIXTURE_SERVED_COMMAND,
|
|
63
|
+
returncode=FIXTURE_CHAIN_RETURNCODE,
|
|
64
|
+
is_dirty_tree=True,
|
|
65
|
+
)
|
|
66
|
+
FAILED_SERVE_OUTCOME = invoker.CodeReviewOutcome(
|
|
67
|
+
mode=MODE_CHAIN,
|
|
68
|
+
served_command=None,
|
|
69
|
+
returncode=FIXTURE_FAILED_RETURNCODE,
|
|
70
|
+
is_dirty_tree=False,
|
|
71
|
+
)
|
|
72
|
+
IN_SESSION_READY_OUTCOME = invoker.CodeReviewOutcome(
|
|
73
|
+
mode=MODE_IN_SESSION,
|
|
74
|
+
served_command=None,
|
|
75
|
+
returncode=IN_SESSION_RETURNCODE,
|
|
76
|
+
is_dirty_tree=False,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_build_code_review_arguments_matches_contract() -> None:
|
|
81
|
+
all_arguments = invoker.build_code_review_arguments()
|
|
82
|
+
assert all_arguments == [
|
|
83
|
+
SINGLE_TURN_FLAG,
|
|
84
|
+
invoker.build_code_review_prompt(DEFAULT_CODE_REVIEW_EFFORT),
|
|
85
|
+
MODEL_FLAG,
|
|
86
|
+
CODE_REVIEW_MODEL_ALIAS,
|
|
87
|
+
OUTPUT_FORMAT_FLAG,
|
|
88
|
+
OUTPUT_FORMAT_JSON,
|
|
89
|
+
PERMISSION_MODE_FLAG,
|
|
90
|
+
PERMISSION_MODE_BYPASS,
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_chain_failure_preserves_returncode(
|
|
95
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
96
|
+
) -> None:
|
|
97
|
+
working_directory = init_git_repository(tmp_path / "repo")
|
|
98
|
+
install_seams(
|
|
99
|
+
monkeypatch,
|
|
100
|
+
host_profile=HOST_PROFILE_THIRD_PARTY,
|
|
101
|
+
claude_outcome=claude_failed(),
|
|
102
|
+
working_directory=working_directory,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
review_outcome = run_review(working_directory, session_model=FIXTURE_SESSION_OPUS)
|
|
106
|
+
|
|
107
|
+
assert review_outcome.mode == MODE_CHAIN
|
|
108
|
+
assert review_outcome.served_command is None
|
|
109
|
+
assert review_outcome.returncode == FIXTURE_FAILED_RETURNCODE
|
|
110
|
+
assert review_outcome.is_dirty_tree is False
|
|
111
|
+
assert invoker.is_successful_code_review(review_outcome) is False
|
|
112
|
+
assert invoker.is_code_review_clean_stamp_allowed(review_outcome) is False
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_clean_stamp_allowed_only_on_successful_clean_serve() -> None:
|
|
116
|
+
assert invoker.is_code_review_clean_stamp_allowed(CLEAN_SUCCESS_OUTCOME) is True
|
|
117
|
+
assert invoker.is_code_review_clean_stamp_allowed(DIRTY_SUCCESS_OUTCOME) is False
|
|
118
|
+
assert invoker.is_code_review_clean_stamp_allowed(FAILED_SERVE_OUTCOME) is False
|
|
119
|
+
assert invoker.is_code_review_clean_stamp_allowed(IN_SESSION_READY_OUTCOME) is True
|
|
120
|
+
assert invoker.is_successful_code_review(FAILED_SERVE_OUTCOME) is False
|
|
121
|
+
assert invoker.is_successful_code_review(CLEAN_SUCCESS_OUTCOME) is True
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_encode_code_review_outcome_shape() -> None:
|
|
125
|
+
review_outcome = invoker.CodeReviewOutcome(
|
|
126
|
+
mode=MODE_CHAIN,
|
|
127
|
+
served_command=FIXTURE_SERVED_COMMAND,
|
|
128
|
+
returncode=FIXTURE_CHAIN_RETURNCODE,
|
|
129
|
+
is_dirty_tree=True,
|
|
130
|
+
)
|
|
131
|
+
encoded_payload = invoker.encode_code_review_outcome(review_outcome)
|
|
132
|
+
assert encoded_payload == {
|
|
133
|
+
RESULT_KEY_MODE: MODE_CHAIN,
|
|
134
|
+
RESULT_KEY_SERVED_COMMAND: FIXTURE_SERVED_COMMAND,
|
|
135
|
+
RESULT_KEY_RETURNCODE: FIXTURE_CHAIN_RETURNCODE,
|
|
136
|
+
RESULT_KEY_DIRTY_TREE: True,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@pytest.mark.parametrize("valid_effort", ["low", "medium", "high", "xhigh", "max"])
|
|
141
|
+
def test_validate_effort_token_accepts_known_tokens(valid_effort: str) -> None:
|
|
142
|
+
assert invoker.validate_effort_token(valid_effort) is None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_validate_effort_token_rejects_ultra_loudly() -> None:
|
|
146
|
+
error_message = invoker.validate_effort_token(REJECTED_ULTRA_EFFORT)
|
|
147
|
+
assert error_message is not None
|
|
148
|
+
assert REJECTED_ULTRA_EFFORT in error_message
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_validate_effort_token_rejects_unknown_token() -> None:
|
|
152
|
+
error_message = invoker.validate_effort_token("bogus")
|
|
153
|
+
assert error_message is not None
|
|
154
|
+
assert "bogus" in error_message
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def test_build_code_review_prompt_reads_as_slash_command() -> None:
|
|
158
|
+
assert invoker.build_code_review_prompt(EFFORT_LOW) == "/code-review low --fix"
|
|
159
|
+
assert invoker.build_code_review_prompt("xhigh") == "/code-review xhigh --fix"
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def test_encode_stamp_mint_outcome_includes_mint_metadata() -> None:
|
|
163
|
+
review_outcome = invoker.CodeReviewOutcome(
|
|
164
|
+
mode=MODE_CHAIN,
|
|
165
|
+
served_command=FIXTURE_SERVED_COMMAND,
|
|
166
|
+
returncode=FIXTURE_CHAIN_RETURNCODE,
|
|
167
|
+
is_dirty_tree=False,
|
|
168
|
+
)
|
|
169
|
+
mint_outcome = invoker.StampMintOutcome(
|
|
170
|
+
review_outcome=review_outcome,
|
|
171
|
+
is_stamp_minted=True,
|
|
172
|
+
pass_count=SINGLE_PASS_CAP,
|
|
173
|
+
bound_hash="abc123",
|
|
174
|
+
)
|
|
175
|
+
encoded_payload = invoker.encode_stamp_mint_outcome(mint_outcome)
|
|
176
|
+
assert encoded_payload[RESULT_KEY_STAMP_MINTED] is True
|
|
177
|
+
assert encoded_payload[RESULT_KEY_PASS_COUNT] == SINGLE_PASS_CAP
|
|
178
|
+
assert encoded_payload[RESULT_KEY_BOUND_HASH] == "abc123"
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_record_stamp_mints_on_surface_stable_clean_pass(
|
|
182
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
183
|
+
) -> None:
|
|
184
|
+
working_directory = prepared_surface_repo(monkeypatch, tmp_path)
|
|
185
|
+
monkeypatch.setattr(invoker, "invoke_code_review", stable_clean_review)
|
|
186
|
+
mint_outcome = invoker.invoke_code_review_and_record_stamp(
|
|
187
|
+
working_directory=working_directory,
|
|
188
|
+
session_model=CODE_REVIEW_MODEL_ALIAS,
|
|
189
|
+
timeout_seconds=DEFAULT_CODE_REVIEW_TIMEOUT_SECONDS,
|
|
190
|
+
effort=EFFORT_LOW,
|
|
191
|
+
)
|
|
192
|
+
assert mint_outcome.is_stamp_minted is True
|
|
193
|
+
assert mint_outcome.bound_hash is not None
|
|
194
|
+
store_module = invoker.load_code_review_stamp_store()
|
|
195
|
+
assert store_module.stamp_covers_surface(
|
|
196
|
+
str(working_directory), mint_outcome.bound_hash, EFFORT_LOW
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def test_record_stamp_does_not_mint_when_review_changes_surface(
|
|
201
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
202
|
+
) -> None:
|
|
203
|
+
working_directory = prepared_surface_repo(monkeypatch, tmp_path)
|
|
204
|
+
monkeypatch.setattr(invoker, "invoke_code_review", surface_changing_review)
|
|
205
|
+
mint_outcome = invoker.invoke_code_review_and_record_stamp(
|
|
206
|
+
working_directory=working_directory,
|
|
207
|
+
session_model=CODE_REVIEW_MODEL_ALIAS,
|
|
208
|
+
timeout_seconds=DEFAULT_CODE_REVIEW_TIMEOUT_SECONDS,
|
|
209
|
+
effort=EFFORT_LOW,
|
|
210
|
+
maximum_passes=SINGLE_PASS_CAP,
|
|
211
|
+
)
|
|
212
|
+
assert mint_outcome.is_stamp_minted is False
|
|
213
|
+
assert mint_outcome.pass_count == SINGLE_PASS_CAP
|
|
214
|
+
assert mint_outcome.bound_hash is None
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def test_record_stamp_hits_cap_without_minting(
|
|
218
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
219
|
+
) -> None:
|
|
220
|
+
working_directory = prepared_surface_repo(monkeypatch, tmp_path)
|
|
221
|
+
monkeypatch.setattr(invoker, "invoke_code_review", DriftingReview())
|
|
222
|
+
mint_outcome = invoker.invoke_code_review_and_record_stamp(
|
|
223
|
+
working_directory=working_directory,
|
|
224
|
+
session_model=CODE_REVIEW_MODEL_ALIAS,
|
|
225
|
+
timeout_seconds=DEFAULT_CODE_REVIEW_TIMEOUT_SECONDS,
|
|
226
|
+
effort=EFFORT_LOW,
|
|
227
|
+
maximum_passes=MAXIMUM_STAMP_MINT_PASSES,
|
|
228
|
+
)
|
|
229
|
+
assert mint_outcome.is_stamp_minted is False
|
|
230
|
+
assert mint_outcome.pass_count == MAXIMUM_STAMP_MINT_PASSES
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def test_load_code_review_stamp_store_records_and_covers_surface(
|
|
234
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
235
|
+
) -> None:
|
|
236
|
+
working_directory = prepared_surface_repo(monkeypatch, tmp_path)
|
|
237
|
+
store_module = invoker.load_code_review_stamp_store()
|
|
238
|
+
surface_hash = store_module.live_surface_hash(str(working_directory))
|
|
239
|
+
assert surface_hash is not None
|
|
240
|
+
stamp_path = store_module.record_clean_stamp(
|
|
241
|
+
str(working_directory), surface_hash, EFFORT_LOW
|
|
242
|
+
)
|
|
243
|
+
assert stamp_path.exists()
|
|
244
|
+
assert store_module.stamp_covers_surface(
|
|
245
|
+
str(working_directory), surface_hash, EFFORT_LOW
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def test_load_code_review_stamp_store_raises_when_file_absent(
|
|
250
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
251
|
+
) -> None:
|
|
252
|
+
monkeypatch.setattr(
|
|
253
|
+
invoker, "STAMP_STORE_MODULE_FILE_NAME", MISSING_STORE_FILE_NAME
|
|
254
|
+
)
|
|
255
|
+
with pytest.raises(ModuleNotFoundError):
|
|
256
|
+
invoker.load_code_review_stamp_store()
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Working-tree dirty detection and the git status porcelain command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
import invoke_code_review as invoker
|
|
12
|
+
from _code_review_test_support import (
|
|
13
|
+
DIRTY_FILE_CONTENTS,
|
|
14
|
+
DIRTY_FILE_NAME,
|
|
15
|
+
FIXTURE_CHAIN_RETURNCODE,
|
|
16
|
+
FIXTURE_GIT_STATUS_FAILURE_RETURNCODE,
|
|
17
|
+
FIXTURE_SERVED_COMMAND,
|
|
18
|
+
FIXTURE_SESSION_OPUS,
|
|
19
|
+
HOST_PROFILE_THIRD_PARTY,
|
|
20
|
+
claude_served,
|
|
21
|
+
init_git_repository,
|
|
22
|
+
install_seams,
|
|
23
|
+
run_review,
|
|
24
|
+
)
|
|
25
|
+
from dev_env_scripts_constants.code_review_constants import (
|
|
26
|
+
GIT_BINARY,
|
|
27
|
+
GIT_PORCELAIN_FLAG,
|
|
28
|
+
GIT_STATUS_SUBCOMMAND,
|
|
29
|
+
MODE_CHAIN,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_dirty_tree_true_after_chain_writes_file(
|
|
34
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
35
|
+
) -> None:
|
|
36
|
+
working_directory = init_git_repository(tmp_path / "repo")
|
|
37
|
+
install_seams(
|
|
38
|
+
monkeypatch,
|
|
39
|
+
host_profile=HOST_PROFILE_THIRD_PARTY,
|
|
40
|
+
claude_outcome=claude_served(),
|
|
41
|
+
should_dirty_tree_on_chain=True,
|
|
42
|
+
working_directory=working_directory,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
review_outcome = run_review(working_directory, session_model=FIXTURE_SESSION_OPUS)
|
|
46
|
+
|
|
47
|
+
assert review_outcome.mode == MODE_CHAIN
|
|
48
|
+
assert review_outcome.is_dirty_tree is True
|
|
49
|
+
assert review_outcome.served_command == FIXTURE_SERVED_COMMAND
|
|
50
|
+
assert review_outcome.returncode == FIXTURE_CHAIN_RETURNCODE
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_dirty_tree_false_when_chain_leaves_tree_clean(
|
|
54
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
55
|
+
) -> None:
|
|
56
|
+
working_directory = init_git_repository(tmp_path / "repo")
|
|
57
|
+
install_seams(
|
|
58
|
+
monkeypatch,
|
|
59
|
+
host_profile=HOST_PROFILE_THIRD_PARTY,
|
|
60
|
+
claude_outcome=claude_served(),
|
|
61
|
+
should_dirty_tree_on_chain=False,
|
|
62
|
+
working_directory=working_directory,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
review_outcome = run_review(working_directory, session_model=FIXTURE_SESSION_OPUS)
|
|
66
|
+
|
|
67
|
+
assert review_outcome.is_dirty_tree is False
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_is_working_tree_dirty_against_real_git_repo(tmp_path: Path) -> None:
|
|
71
|
+
working_directory = init_git_repository(tmp_path / "repo")
|
|
72
|
+
assert invoker.is_working_tree_dirty(working_directory) is False
|
|
73
|
+
(working_directory / DIRTY_FILE_NAME).write_text(
|
|
74
|
+
DIRTY_FILE_CONTENTS, encoding="utf-8"
|
|
75
|
+
)
|
|
76
|
+
assert invoker.is_working_tree_dirty(working_directory) is True
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_git_status_command_uses_porcelain(
|
|
80
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
81
|
+
) -> None:
|
|
82
|
+
working_directory = init_git_repository(tmp_path / "repo")
|
|
83
|
+
all_commands: list[list[str]] = []
|
|
84
|
+
|
|
85
|
+
def fake_git_status(
|
|
86
|
+
all_command_tokens: Sequence[str],
|
|
87
|
+
*all_positionals: object,
|
|
88
|
+
**all_keywords: object,
|
|
89
|
+
) -> subprocess.CompletedProcess[str]:
|
|
90
|
+
del all_positionals, all_keywords
|
|
91
|
+
all_commands.append(list(all_command_tokens))
|
|
92
|
+
return subprocess.CompletedProcess(
|
|
93
|
+
args=list(all_command_tokens),
|
|
94
|
+
returncode=0,
|
|
95
|
+
stdout="",
|
|
96
|
+
stderr="",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
monkeypatch.setattr(invoker, "review_git_status_runner", fake_git_status)
|
|
100
|
+
invoker.is_working_tree_dirty(working_directory)
|
|
101
|
+
assert all_commands == [[GIT_BINARY, GIT_STATUS_SUBCOMMAND, GIT_PORCELAIN_FLAG]]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_is_working_tree_dirty_nonzero_returncode_is_not_clean(
|
|
105
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
106
|
+
) -> None:
|
|
107
|
+
working_directory = init_git_repository(tmp_path / "repo")
|
|
108
|
+
|
|
109
|
+
def fake_git_status(
|
|
110
|
+
all_command_tokens: Sequence[str],
|
|
111
|
+
*all_positionals: object,
|
|
112
|
+
**all_keywords: object,
|
|
113
|
+
) -> subprocess.CompletedProcess[str]:
|
|
114
|
+
del all_positionals, all_keywords
|
|
115
|
+
return subprocess.CompletedProcess(
|
|
116
|
+
args=list(all_command_tokens),
|
|
117
|
+
returncode=FIXTURE_GIT_STATUS_FAILURE_RETURNCODE,
|
|
118
|
+
stdout="",
|
|
119
|
+
stderr="fatal: not a git repository",
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
monkeypatch.setattr(invoker, "review_git_status_runner", fake_git_status)
|
|
123
|
+
assert invoker.is_working_tree_dirty(working_directory) is True
|