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
|
@@ -1,966 +1,143 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Specifications for what a blocked mint tells the person it blocked.
|
|
2
|
+
|
|
3
|
+
Enforcement refuses every push until a clean stamp covers the branch surface,
|
|
4
|
+
and ``invoke_code_review.py --record-stamp`` is the only way to mint one. When
|
|
5
|
+
that call cannot run, the reason it names is the person's only route back to a
|
|
6
|
+
working push.
|
|
7
|
+
"""
|
|
2
8
|
|
|
3
9
|
from __future__ import annotations
|
|
4
10
|
|
|
5
|
-
import json
|
|
6
|
-
import subprocess
|
|
7
|
-
import sys
|
|
8
|
-
from collections.abc import Sequence
|
|
9
|
-
from dataclasses import dataclass, field
|
|
10
11
|
from pathlib import Path
|
|
11
12
|
|
|
12
13
|
import pytest
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
import claude_chain_runner as chain_runner # noqa: E402
|
|
19
|
-
import invoke_code_review as invoker # noqa: E402
|
|
20
|
-
from claude_chain_runner import ( # noqa: E402
|
|
21
|
-
ChainAttempt,
|
|
22
|
-
ChainConfigurationError,
|
|
23
|
-
ChainInvocationOutcome,
|
|
24
|
-
)
|
|
25
|
-
from dev_env_scripts_constants.claude_chain_constants import ( # noqa: E402
|
|
26
|
-
CHAIN_CONFIG_ERROR_EXIT_CODE,
|
|
27
|
-
)
|
|
28
|
-
from dev_env_scripts_constants.code_review_constants import ( # noqa: E402
|
|
29
|
-
CLI_SESSION_MODEL_FLAG,
|
|
30
|
-
CODE_REVIEW_MODEL_ALIAS,
|
|
15
|
+
import invoke_code_review as invoker
|
|
16
|
+
from claude_chain_runner import ChainConfigurationError, ChainInvocationOutcome
|
|
17
|
+
from _code_review_test_support import FIXTURE_SESSION_OPUS
|
|
18
|
+
from dev_env_scripts_constants.code_review_constants import (
|
|
31
19
|
DEFAULT_CODE_REVIEW_EFFORT,
|
|
32
|
-
|
|
33
|
-
GIT_PORCELAIN_FLAG,
|
|
34
|
-
GIT_STATUS_SUBCOMMAND,
|
|
35
|
-
HOST_PROFILE_ERROR_RETURNCODE,
|
|
36
|
-
IN_SESSION_RETURNCODE,
|
|
37
|
-
MODE_CHAIN,
|
|
38
|
-
MODE_IN_SESSION,
|
|
20
|
+
PERMISSION_MODE_ACCEPT_EDITS,
|
|
39
21
|
PERMISSION_MODE_BYPASS,
|
|
40
|
-
|
|
41
|
-
RESULT_KEY_DIRTY_TREE,
|
|
42
|
-
RESULT_KEY_MODE,
|
|
43
|
-
RESULT_KEY_RETURNCODE,
|
|
44
|
-
RESULT_KEY_SERVED_COMMAND,
|
|
45
|
-
)
|
|
46
|
-
from dev_env_scripts_constants.grok_worker_constants import ( # noqa: E402
|
|
47
|
-
CLI_TIMEOUT_FLAG,
|
|
48
|
-
CWD_FLAG,
|
|
49
|
-
MODEL_FLAG,
|
|
50
|
-
OUTPUT_FORMAT_FLAG,
|
|
51
|
-
OUTPUT_FORMAT_JSON,
|
|
52
|
-
SINGLE_TURN_FLAG,
|
|
53
|
-
)
|
|
54
|
-
from dev_env_scripts_constants.timing import ( # noqa: E402
|
|
55
|
-
DEFAULT_CODE_REVIEW_TIMEOUT_SECONDS,
|
|
22
|
+
REVIEW_PERMISSION_MODE,
|
|
56
23
|
)
|
|
57
24
|
|
|
58
|
-
HOST_PROFILE_CLAUDE = "Claude"
|
|
59
|
-
HOST_PROFILE_THIRD_PARTY = "ThirdParty"
|
|
60
25
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
FIXTURE_FAILED_RETURNCODE = 1
|
|
64
|
-
FIXTURE_GIT_STATUS_FAILURE_RETURNCODE = 128
|
|
65
|
-
FIXTURE_CHAIN_STDOUT = '{"result":"review done"}'
|
|
66
|
-
FIXTURE_SESSION_OPUS = "opus"
|
|
67
|
-
FIXTURE_SESSION_OPUS_UPPER = "Opus"
|
|
68
|
-
FIXTURE_SESSION_SONNET = "sonnet"
|
|
69
|
-
FIXTURE_SESSION_HAIKU = "haiku"
|
|
70
|
-
FIXTURE_CHAIN_CONFIG_ERROR_MESSAGE = "chain config missing"
|
|
71
|
-
FIXTURE_HOST_PROFILE_ERROR_MESSAGE = "unknown host profile"
|
|
72
|
-
DIRTY_FILE_NAME = "review_fix.txt"
|
|
73
|
-
DIRTY_FILE_CONTENTS = "applied fix\n"
|
|
74
|
-
GIT_INIT_TIMEOUT_SECONDS = 30
|
|
26
|
+
def test_review_arguments_carry_the_permission_mode_this_caller_resolves() -> None:
|
|
27
|
+
"""The review command asks for a permission mode the binary accepts here.
|
|
75
28
|
|
|
29
|
+
The binary refuses the bypass mode outright for a root caller, so asking
|
|
30
|
+
for it there means no review runs and no stamp is ever minted.
|
|
31
|
+
"""
|
|
32
|
+
all_arguments = invoker.build_code_review_arguments(DEFAULT_CODE_REVIEW_EFFORT)
|
|
76
33
|
|
|
77
|
-
|
|
78
|
-
*,
|
|
79
|
-
returncode: int = FIXTURE_CHAIN_RETURNCODE,
|
|
80
|
-
stdout: str = FIXTURE_CHAIN_STDOUT,
|
|
81
|
-
) -> ChainInvocationOutcome:
|
|
82
|
-
return ChainInvocationOutcome(
|
|
83
|
-
served_command=FIXTURE_SERVED_COMMAND,
|
|
84
|
-
returncode=returncode,
|
|
85
|
-
stdout=stdout,
|
|
86
|
-
stderr="",
|
|
87
|
-
attempts=(
|
|
88
|
-
ChainAttempt(command=FIXTURE_SERVED_COMMAND, status="served"),
|
|
89
|
-
),
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def _claude_failed() -> ChainInvocationOutcome:
|
|
94
|
-
return ChainInvocationOutcome(
|
|
95
|
-
served_command=None,
|
|
96
|
-
returncode=FIXTURE_FAILED_RETURNCODE,
|
|
97
|
-
stdout="",
|
|
98
|
-
stderr="chain exhausted",
|
|
99
|
-
attempts=(
|
|
100
|
-
ChainAttempt(command=FIXTURE_SERVED_COMMAND, status="usage_limited"),
|
|
101
|
-
),
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def _init_git_repository(repository_directory: Path) -> Path:
|
|
106
|
-
repository_directory.mkdir(parents=True, exist_ok=True)
|
|
107
|
-
subprocess.run(
|
|
108
|
-
[GIT_BINARY, "init"],
|
|
109
|
-
cwd=str(repository_directory),
|
|
110
|
-
check=True,
|
|
111
|
-
capture_output=True,
|
|
112
|
-
text=True,
|
|
113
|
-
timeout=GIT_INIT_TIMEOUT_SECONDS,
|
|
114
|
-
)
|
|
115
|
-
subprocess.run(
|
|
116
|
-
[GIT_BINARY, "config", "user.email", "reviewer@example.com"],
|
|
117
|
-
cwd=str(repository_directory),
|
|
118
|
-
check=True,
|
|
119
|
-
capture_output=True,
|
|
120
|
-
text=True,
|
|
121
|
-
timeout=GIT_INIT_TIMEOUT_SECONDS,
|
|
122
|
-
)
|
|
123
|
-
subprocess.run(
|
|
124
|
-
[GIT_BINARY, "config", "user.name", "Reviewer"],
|
|
125
|
-
cwd=str(repository_directory),
|
|
126
|
-
check=True,
|
|
127
|
-
capture_output=True,
|
|
128
|
-
text=True,
|
|
129
|
-
timeout=GIT_INIT_TIMEOUT_SECONDS,
|
|
130
|
-
)
|
|
131
|
-
tracked_file = repository_directory / "README.md"
|
|
132
|
-
tracked_file.write_text("baseline\n", encoding="utf-8")
|
|
133
|
-
subprocess.run(
|
|
134
|
-
[GIT_BINARY, "add", "README.md"],
|
|
135
|
-
cwd=str(repository_directory),
|
|
136
|
-
check=True,
|
|
137
|
-
capture_output=True,
|
|
138
|
-
text=True,
|
|
139
|
-
timeout=GIT_INIT_TIMEOUT_SECONDS,
|
|
140
|
-
)
|
|
141
|
-
subprocess.run(
|
|
142
|
-
[GIT_BINARY, "commit", "-m", "baseline"],
|
|
143
|
-
cwd=str(repository_directory),
|
|
144
|
-
check=True,
|
|
145
|
-
capture_output=True,
|
|
146
|
-
text=True,
|
|
147
|
-
timeout=GIT_INIT_TIMEOUT_SECONDS,
|
|
148
|
-
)
|
|
149
|
-
return repository_directory
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
@dataclass
|
|
153
|
-
class SeamCallLog:
|
|
154
|
-
claude_calls: int = 0
|
|
155
|
-
claude_arguments: list[str] | None = None
|
|
156
|
-
host_profile_calls: int = 0
|
|
157
|
-
is_stdin_empty: bool = False
|
|
158
|
-
claude_working_directory: Path | None = None
|
|
159
|
-
all_observed_working_directories: list[Path] = field(default_factory=list)
|
|
160
|
-
all_git_status_commands: list[list[str]] = field(default_factory=list)
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
def _install_seams(
|
|
164
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
165
|
-
*,
|
|
166
|
-
host_profile: str = HOST_PROFILE_CLAUDE,
|
|
167
|
-
claude_outcome: ChainInvocationOutcome | BaseException | None = None,
|
|
168
|
-
should_dirty_tree_on_chain: bool = False,
|
|
169
|
-
working_directory: Path | None = None,
|
|
170
|
-
) -> SeamCallLog:
|
|
171
|
-
call_log = SeamCallLog()
|
|
34
|
+
assert REVIEW_PERMISSION_MODE in all_arguments
|
|
172
35
|
|
|
173
|
-
def fake_host_profile(
|
|
174
|
-
setting_by_name: object | None = None,
|
|
175
|
-
) -> str:
|
|
176
|
-
del setting_by_name
|
|
177
|
-
call_log.host_profile_calls += 1
|
|
178
|
-
return host_profile
|
|
179
36
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
call_log.claude_calls += 1
|
|
184
|
-
call_log.claude_arguments = list(all_claude_arguments)
|
|
185
|
-
chain_runner.chain_subprocess_runner(
|
|
186
|
-
["claude", *all_claude_arguments],
|
|
187
|
-
capture_output=True,
|
|
188
|
-
text=True,
|
|
189
|
-
timeout=timeout_seconds,
|
|
190
|
-
check=False,
|
|
191
|
-
)
|
|
192
|
-
if should_dirty_tree_on_chain and working_directory is not None:
|
|
193
|
-
dirty_file = working_directory / DIRTY_FILE_NAME
|
|
194
|
-
dirty_file.write_text(DIRTY_FILE_CONTENTS, encoding="utf-8")
|
|
195
|
-
if isinstance(claude_outcome, BaseException):
|
|
196
|
-
raise claude_outcome
|
|
197
|
-
assert isinstance(claude_outcome, ChainInvocationOutcome)
|
|
198
|
-
return claude_outcome
|
|
199
|
-
|
|
200
|
-
def _tracking_subprocess_runner(
|
|
201
|
-
all_invocation_tokens: Sequence[str],
|
|
202
|
-
*all_positionals: object,
|
|
203
|
-
**all_keywords: object,
|
|
204
|
-
) -> subprocess.CompletedProcess[str]:
|
|
205
|
-
del all_positionals
|
|
206
|
-
maybe_stdin = all_keywords.get("stdin")
|
|
207
|
-
if maybe_stdin is subprocess.DEVNULL:
|
|
208
|
-
call_log.is_stdin_empty = True
|
|
209
|
-
elif maybe_stdin is not None:
|
|
210
|
-
maybe_read = getattr(maybe_stdin, "read", None)
|
|
211
|
-
if callable(maybe_read):
|
|
212
|
-
call_log.is_stdin_empty = maybe_read() == ""
|
|
213
|
-
maybe_seek = getattr(maybe_stdin, "seek", None)
|
|
214
|
-
if callable(maybe_seek):
|
|
215
|
-
maybe_seek(0)
|
|
216
|
-
maybe_cwd = all_keywords.get("cwd")
|
|
217
|
-
if maybe_cwd is not None:
|
|
218
|
-
resolved_directory = Path(str(maybe_cwd))
|
|
219
|
-
call_log.claude_working_directory = resolved_directory
|
|
220
|
-
call_log.all_observed_working_directories.append(resolved_directory)
|
|
221
|
-
return subprocess.CompletedProcess(
|
|
222
|
-
args=list(all_invocation_tokens),
|
|
223
|
-
returncode=0,
|
|
224
|
-
stdout="{}",
|
|
225
|
-
stderr="",
|
|
226
|
-
)
|
|
227
|
-
|
|
228
|
-
monkeypatch.setattr(invoker, "review_host_profile_detector", fake_host_profile)
|
|
229
|
-
monkeypatch.setattr(invoker, "review_claude_runner", fake_claude)
|
|
230
|
-
monkeypatch.setattr(
|
|
231
|
-
chain_runner, "chain_subprocess_runner", _tracking_subprocess_runner
|
|
232
|
-
)
|
|
233
|
-
return call_log
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
@pytest.mark.parametrize(
|
|
237
|
-
("host_profile", "session_model", "expected_mode"),
|
|
238
|
-
[
|
|
239
|
-
(HOST_PROFILE_CLAUDE, FIXTURE_SESSION_OPUS, MODE_IN_SESSION),
|
|
240
|
-
(HOST_PROFILE_CLAUDE, FIXTURE_SESSION_OPUS_UPPER, MODE_IN_SESSION),
|
|
241
|
-
(HOST_PROFILE_CLAUDE, FIXTURE_SESSION_SONNET, MODE_CHAIN),
|
|
242
|
-
(HOST_PROFILE_CLAUDE, FIXTURE_SESSION_HAIKU, MODE_CHAIN),
|
|
243
|
-
(HOST_PROFILE_THIRD_PARTY, FIXTURE_SESSION_OPUS, MODE_CHAIN),
|
|
244
|
-
(HOST_PROFILE_THIRD_PARTY, FIXTURE_SESSION_SONNET, MODE_CHAIN),
|
|
245
|
-
],
|
|
246
|
-
)
|
|
247
|
-
def test_mode_decision_host_and_model_matrix(
|
|
248
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
249
|
-
tmp_path: Path,
|
|
250
|
-
host_profile: str,
|
|
251
|
-
session_model: str,
|
|
252
|
-
expected_mode: str,
|
|
253
|
-
) -> None:
|
|
254
|
-
working_directory = _init_git_repository(tmp_path / "repo")
|
|
255
|
-
claude_outcome = _claude_served() if expected_mode == MODE_CHAIN else None
|
|
256
|
-
call_log = _install_seams(
|
|
257
|
-
monkeypatch,
|
|
258
|
-
host_profile=host_profile,
|
|
259
|
-
claude_outcome=claude_outcome,
|
|
260
|
-
working_directory=working_directory,
|
|
261
|
-
)
|
|
262
|
-
|
|
263
|
-
review_outcome = invoker.invoke_code_review(
|
|
264
|
-
working_directory=working_directory,
|
|
265
|
-
session_model=session_model,
|
|
266
|
-
timeout_seconds=DEFAULT_CODE_REVIEW_TIMEOUT_SECONDS,
|
|
267
|
-
)
|
|
268
|
-
|
|
269
|
-
assert review_outcome.mode == expected_mode
|
|
270
|
-
assert call_log.host_profile_calls == 1
|
|
271
|
-
if expected_mode == MODE_IN_SESSION:
|
|
272
|
-
assert call_log.claude_calls == 0
|
|
273
|
-
assert review_outcome.served_command is None
|
|
274
|
-
assert review_outcome.returncode == IN_SESSION_RETURNCODE
|
|
275
|
-
assert review_outcome.is_dirty_tree is False
|
|
276
|
-
else:
|
|
277
|
-
assert call_log.claude_calls == 1
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
def test_chain_argv_assembly(
|
|
281
|
-
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
282
|
-
) -> None:
|
|
283
|
-
working_directory = _init_git_repository(tmp_path / "repo")
|
|
284
|
-
call_log = _install_seams(
|
|
285
|
-
monkeypatch,
|
|
286
|
-
host_profile=HOST_PROFILE_THIRD_PARTY,
|
|
287
|
-
claude_outcome=_claude_served(),
|
|
288
|
-
working_directory=working_directory,
|
|
289
|
-
)
|
|
290
|
-
|
|
291
|
-
invoker.invoke_code_review(
|
|
292
|
-
working_directory=working_directory,
|
|
293
|
-
session_model=FIXTURE_SESSION_SONNET,
|
|
294
|
-
timeout_seconds=DEFAULT_CODE_REVIEW_TIMEOUT_SECONDS,
|
|
295
|
-
)
|
|
296
|
-
|
|
297
|
-
assert call_log.claude_arguments == [
|
|
298
|
-
SINGLE_TURN_FLAG,
|
|
299
|
-
invoker.build_code_review_prompt(DEFAULT_CODE_REVIEW_EFFORT),
|
|
300
|
-
MODEL_FLAG,
|
|
301
|
-
CODE_REVIEW_MODEL_ALIAS,
|
|
302
|
-
OUTPUT_FORMAT_FLAG,
|
|
303
|
-
OUTPUT_FORMAT_JSON,
|
|
304
|
-
PERMISSION_MODE_FLAG,
|
|
37
|
+
def test_the_resolved_permission_mode_is_one_the_binary_knows() -> None:
|
|
38
|
+
assert REVIEW_PERMISSION_MODE in (
|
|
39
|
+
PERMISSION_MODE_ACCEPT_EDITS,
|
|
305
40
|
PERMISSION_MODE_BYPASS,
|
|
306
|
-
]
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
def test_chain_redirects_empty_stdin_and_sets_cwd(
|
|
310
|
-
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
311
|
-
) -> None:
|
|
312
|
-
working_directory = _init_git_repository(tmp_path / "repo")
|
|
313
|
-
call_log = _install_seams(
|
|
314
|
-
monkeypatch,
|
|
315
|
-
host_profile=HOST_PROFILE_THIRD_PARTY,
|
|
316
|
-
claude_outcome=_claude_served(),
|
|
317
|
-
working_directory=working_directory,
|
|
318
|
-
)
|
|
319
|
-
|
|
320
|
-
invoker.invoke_code_review(
|
|
321
|
-
working_directory=working_directory,
|
|
322
|
-
session_model=FIXTURE_SESSION_OPUS,
|
|
323
|
-
timeout_seconds=DEFAULT_CODE_REVIEW_TIMEOUT_SECONDS,
|
|
324
|
-
)
|
|
325
|
-
|
|
326
|
-
assert call_log.is_stdin_empty is True
|
|
327
|
-
assert call_log.claude_working_directory == working_directory
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
def test_dirty_tree_true_after_chain_writes_file(
|
|
331
|
-
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
332
|
-
) -> None:
|
|
333
|
-
working_directory = _init_git_repository(tmp_path / "repo")
|
|
334
|
-
_install_seams(
|
|
335
|
-
monkeypatch,
|
|
336
|
-
host_profile=HOST_PROFILE_THIRD_PARTY,
|
|
337
|
-
claude_outcome=_claude_served(),
|
|
338
|
-
should_dirty_tree_on_chain=True,
|
|
339
|
-
working_directory=working_directory,
|
|
340
|
-
)
|
|
341
|
-
|
|
342
|
-
review_outcome = invoker.invoke_code_review(
|
|
343
|
-
working_directory=working_directory,
|
|
344
|
-
session_model=FIXTURE_SESSION_OPUS,
|
|
345
|
-
timeout_seconds=DEFAULT_CODE_REVIEW_TIMEOUT_SECONDS,
|
|
346
|
-
)
|
|
347
|
-
|
|
348
|
-
assert review_outcome.mode == MODE_CHAIN
|
|
349
|
-
assert review_outcome.is_dirty_tree is True
|
|
350
|
-
assert review_outcome.served_command == FIXTURE_SERVED_COMMAND
|
|
351
|
-
assert review_outcome.returncode == FIXTURE_CHAIN_RETURNCODE
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
def test_dirty_tree_false_when_chain_leaves_tree_clean(
|
|
355
|
-
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
356
|
-
) -> None:
|
|
357
|
-
working_directory = _init_git_repository(tmp_path / "repo")
|
|
358
|
-
_install_seams(
|
|
359
|
-
monkeypatch,
|
|
360
|
-
host_profile=HOST_PROFILE_THIRD_PARTY,
|
|
361
|
-
claude_outcome=_claude_served(),
|
|
362
|
-
should_dirty_tree_on_chain=False,
|
|
363
|
-
working_directory=working_directory,
|
|
364
|
-
)
|
|
365
|
-
|
|
366
|
-
review_outcome = invoker.invoke_code_review(
|
|
367
|
-
working_directory=working_directory,
|
|
368
|
-
session_model=FIXTURE_SESSION_OPUS,
|
|
369
|
-
timeout_seconds=DEFAULT_CODE_REVIEW_TIMEOUT_SECONDS,
|
|
370
|
-
)
|
|
371
|
-
|
|
372
|
-
assert review_outcome.is_dirty_tree is False
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
def test_cli_prints_result_json_only(
|
|
376
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
377
|
-
tmp_path: Path,
|
|
378
|
-
capsys: pytest.CaptureFixture[str],
|
|
379
|
-
) -> None:
|
|
380
|
-
working_directory = _init_git_repository(tmp_path / "repo")
|
|
381
|
-
_install_seams(
|
|
382
|
-
monkeypatch,
|
|
383
|
-
host_profile=HOST_PROFILE_CLAUDE,
|
|
384
|
-
claude_outcome=None,
|
|
385
|
-
working_directory=working_directory,
|
|
386
|
-
)
|
|
387
|
-
|
|
388
|
-
exit_code = invoker.main(
|
|
389
|
-
[
|
|
390
|
-
CWD_FLAG,
|
|
391
|
-
str(working_directory),
|
|
392
|
-
CLI_SESSION_MODEL_FLAG,
|
|
393
|
-
FIXTURE_SESSION_OPUS,
|
|
394
|
-
CLI_TIMEOUT_FLAG,
|
|
395
|
-
str(DEFAULT_CODE_REVIEW_TIMEOUT_SECONDS),
|
|
396
|
-
]
|
|
397
|
-
)
|
|
398
|
-
|
|
399
|
-
assert exit_code == IN_SESSION_RETURNCODE
|
|
400
|
-
captured = capsys.readouterr()
|
|
401
|
-
assert captured.err == ""
|
|
402
|
-
parsed_payload = json.loads(captured.out)
|
|
403
|
-
assert parsed_payload == {
|
|
404
|
-
RESULT_KEY_MODE: MODE_IN_SESSION,
|
|
405
|
-
RESULT_KEY_SERVED_COMMAND: None,
|
|
406
|
-
RESULT_KEY_RETURNCODE: IN_SESSION_RETURNCODE,
|
|
407
|
-
RESULT_KEY_DIRTY_TREE: False,
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
def test_build_code_review_arguments_matches_contract() -> None:
|
|
412
|
-
all_arguments = invoker.build_code_review_arguments()
|
|
413
|
-
assert all_arguments == [
|
|
414
|
-
SINGLE_TURN_FLAG,
|
|
415
|
-
invoker.build_code_review_prompt(DEFAULT_CODE_REVIEW_EFFORT),
|
|
416
|
-
MODEL_FLAG,
|
|
417
|
-
CODE_REVIEW_MODEL_ALIAS,
|
|
418
|
-
OUTPUT_FORMAT_FLAG,
|
|
419
|
-
OUTPUT_FORMAT_JSON,
|
|
420
|
-
PERMISSION_MODE_FLAG,
|
|
421
|
-
PERMISSION_MODE_BYPASS,
|
|
422
|
-
]
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
def test_is_working_tree_dirty_against_real_git_repo(tmp_path: Path) -> None:
|
|
426
|
-
working_directory = _init_git_repository(tmp_path / "repo")
|
|
427
|
-
assert invoker.is_working_tree_dirty(working_directory) is False
|
|
428
|
-
(working_directory / DIRTY_FILE_NAME).write_text(
|
|
429
|
-
DIRTY_FILE_CONTENTS, encoding="utf-8"
|
|
430
|
-
)
|
|
431
|
-
assert invoker.is_working_tree_dirty(working_directory) is True
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
def test_git_status_command_uses_porcelain(
|
|
435
|
-
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
436
|
-
) -> None:
|
|
437
|
-
working_directory = _init_git_repository(tmp_path / "repo")
|
|
438
|
-
all_commands: list[list[str]] = []
|
|
439
|
-
|
|
440
|
-
def fake_git_status(
|
|
441
|
-
all_command_tokens: Sequence[str],
|
|
442
|
-
*all_positionals: object,
|
|
443
|
-
**all_keywords: object,
|
|
444
|
-
) -> subprocess.CompletedProcess[str]:
|
|
445
|
-
del all_positionals, all_keywords
|
|
446
|
-
all_commands.append(list(all_command_tokens))
|
|
447
|
-
return subprocess.CompletedProcess(
|
|
448
|
-
args=list(all_command_tokens),
|
|
449
|
-
returncode=0,
|
|
450
|
-
stdout="",
|
|
451
|
-
stderr="",
|
|
452
|
-
)
|
|
453
|
-
|
|
454
|
-
monkeypatch.setattr(invoker, "review_git_status_runner", fake_git_status)
|
|
455
|
-
invoker.is_working_tree_dirty(working_directory)
|
|
456
|
-
assert all_commands == [
|
|
457
|
-
[GIT_BINARY, GIT_STATUS_SUBCOMMAND, GIT_PORCELAIN_FLAG]
|
|
458
|
-
]
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
def test_chain_failure_preserves_returncode(
|
|
462
|
-
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
463
|
-
) -> None:
|
|
464
|
-
working_directory = _init_git_repository(tmp_path / "repo")
|
|
465
|
-
_install_seams(
|
|
466
|
-
monkeypatch,
|
|
467
|
-
host_profile=HOST_PROFILE_THIRD_PARTY,
|
|
468
|
-
claude_outcome=_claude_failed(),
|
|
469
|
-
working_directory=working_directory,
|
|
470
|
-
)
|
|
471
|
-
|
|
472
|
-
review_outcome = invoker.invoke_code_review(
|
|
473
|
-
working_directory=working_directory,
|
|
474
|
-
session_model=FIXTURE_SESSION_OPUS,
|
|
475
|
-
timeout_seconds=DEFAULT_CODE_REVIEW_TIMEOUT_SECONDS,
|
|
476
41
|
)
|
|
477
42
|
|
|
478
|
-
assert review_outcome.mode == MODE_CHAIN
|
|
479
|
-
assert review_outcome.served_command is None
|
|
480
|
-
assert review_outcome.returncode == FIXTURE_FAILED_RETURNCODE
|
|
481
|
-
assert review_outcome.is_dirty_tree is False
|
|
482
|
-
assert invoker.is_successful_code_review(review_outcome) is False
|
|
483
|
-
assert invoker.is_code_review_clean_stamp_allowed(review_outcome) is False
|
|
484
43
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
)
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
returncode=FIXTURE_GIT_STATUS_FAILURE_RETURNCODE,
|
|
500
|
-
stdout="",
|
|
501
|
-
stderr="fatal: not a git repository",
|
|
502
|
-
)
|
|
503
|
-
|
|
504
|
-
monkeypatch.setattr(invoker, "review_git_status_runner", fake_git_status)
|
|
505
|
-
assert invoker.is_working_tree_dirty(working_directory) is True
|
|
44
|
+
CHAIN_CONFIG_REMEDY_TEXT: str = (
|
|
45
|
+
"Claude chain config not found at the path this specification names. "
|
|
46
|
+
"Copy the example config there and list your account binaries."
|
|
47
|
+
)
|
|
48
|
+
HOST_PROFILE_FAILURE_TEXT: str = "session model alias carries no host profile"
|
|
49
|
+
MINT_TIMEOUT_SECONDS: int = 1
|
|
50
|
+
SERVED_COMMAND_NAME: str = "claude"
|
|
51
|
+
REVIEW_BINARY_REFUSAL_TEXT: str = (
|
|
52
|
+
"--dangerously-skip-permissions cannot be used with root privileges"
|
|
53
|
+
)
|
|
54
|
+
REVIEW_FAILURE_RETURNCODE: int = 1
|
|
55
|
+
EMPTY_REVIEW_STDOUT: str = ""
|
|
56
|
+
ROOT_USER_ID: int = 0
|
|
57
|
+
UNPRIVILEGED_USER_ID: int = 1000
|
|
506
58
|
|
|
507
59
|
|
|
508
|
-
def
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
claude_outcome=ChainConfigurationError(FIXTURE_CHAIN_CONFIG_ERROR_MESSAGE),
|
|
518
|
-
working_directory=working_directory,
|
|
519
|
-
)
|
|
520
|
-
|
|
521
|
-
exit_code = invoker.main(
|
|
522
|
-
[
|
|
523
|
-
CWD_FLAG,
|
|
524
|
-
str(working_directory),
|
|
525
|
-
CLI_SESSION_MODEL_FLAG,
|
|
526
|
-
FIXTURE_SESSION_OPUS,
|
|
527
|
-
CLI_TIMEOUT_FLAG,
|
|
528
|
-
str(DEFAULT_CODE_REVIEW_TIMEOUT_SECONDS),
|
|
529
|
-
]
|
|
60
|
+
def _serve_a_refusing_binary(
|
|
61
|
+
*_all_positional: object, **_all_keyword: object
|
|
62
|
+
) -> ChainInvocationOutcome:
|
|
63
|
+
return ChainInvocationOutcome(
|
|
64
|
+
served_command=SERVED_COMMAND_NAME,
|
|
65
|
+
returncode=REVIEW_FAILURE_RETURNCODE,
|
|
66
|
+
stdout=EMPTY_REVIEW_STDOUT,
|
|
67
|
+
stderr=REVIEW_BINARY_REFUSAL_TEXT,
|
|
68
|
+
attempts=(),
|
|
530
69
|
)
|
|
531
70
|
|
|
532
|
-
assert exit_code == CHAIN_CONFIG_ERROR_EXIT_CODE
|
|
533
|
-
captured = capsys.readouterr()
|
|
534
|
-
parsed_payload = json.loads(captured.out)
|
|
535
|
-
assert parsed_payload == {
|
|
536
|
-
RESULT_KEY_MODE: MODE_CHAIN,
|
|
537
|
-
RESULT_KEY_SERVED_COMMAND: None,
|
|
538
|
-
RESULT_KEY_RETURNCODE: CHAIN_CONFIG_ERROR_EXIT_CODE,
|
|
539
|
-
RESULT_KEY_DIRTY_TREE: False,
|
|
540
|
-
}
|
|
541
|
-
assert invoker.is_code_review_clean_stamp_allowed(
|
|
542
|
-
invoker.CodeReviewOutcome(
|
|
543
|
-
mode=MODE_CHAIN,
|
|
544
|
-
served_command=None,
|
|
545
|
-
returncode=CHAIN_CONFIG_ERROR_EXIT_CODE,
|
|
546
|
-
is_dirty_tree=False,
|
|
547
|
-
)
|
|
548
|
-
) is False
|
|
549
|
-
|
|
550
71
|
|
|
551
|
-
def
|
|
72
|
+
def test_failed_review_reports_what_the_served_binary_wrote(
|
|
552
73
|
monkeypatch: pytest.MonkeyPatch,
|
|
553
74
|
tmp_path: Path,
|
|
554
75
|
capsys: pytest.CaptureFixture[str],
|
|
555
76
|
) -> None:
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
def fake_host_profile_raises(
|
|
559
|
-
setting_by_name: object | None = None,
|
|
560
|
-
) -> str:
|
|
561
|
-
del setting_by_name
|
|
562
|
-
raise ValueError(FIXTURE_HOST_PROFILE_ERROR_MESSAGE)
|
|
77
|
+
"""A review that a served binary refused names the refusal.
|
|
563
78
|
|
|
79
|
+
The binary can decline for reasons the caller must act on, such as a
|
|
80
|
+
permission mode it will not accept. Dropping its words leaves a bare
|
|
81
|
+
exit code, and no stamp is minted either way.
|
|
82
|
+
"""
|
|
564
83
|
monkeypatch.setattr(
|
|
565
|
-
invoker, "
|
|
84
|
+
invoker, "_run_claude_with_empty_stdin", _serve_a_refusing_binary
|
|
566
85
|
)
|
|
567
86
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
CLI_SESSION_MODEL_FLAG,
|
|
573
|
-
FIXTURE_SESSION_OPUS,
|
|
574
|
-
CLI_TIMEOUT_FLAG,
|
|
575
|
-
str(DEFAULT_CODE_REVIEW_TIMEOUT_SECONDS),
|
|
576
|
-
]
|
|
577
|
-
)
|
|
578
|
-
|
|
579
|
-
assert exit_code == HOST_PROFILE_ERROR_RETURNCODE
|
|
580
|
-
captured = capsys.readouterr()
|
|
581
|
-
parsed_payload = json.loads(captured.out)
|
|
582
|
-
assert parsed_payload == {
|
|
583
|
-
RESULT_KEY_MODE: MODE_CHAIN,
|
|
584
|
-
RESULT_KEY_SERVED_COMMAND: None,
|
|
585
|
-
RESULT_KEY_RETURNCODE: HOST_PROFILE_ERROR_RETURNCODE,
|
|
586
|
-
RESULT_KEY_DIRTY_TREE: False,
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
def test_clean_stamp_allowed_only_on_successful_clean_serve() -> None:
|
|
591
|
-
clean_success = invoker.CodeReviewOutcome(
|
|
592
|
-
mode=MODE_CHAIN,
|
|
593
|
-
served_command=FIXTURE_SERVED_COMMAND,
|
|
594
|
-
returncode=FIXTURE_CHAIN_RETURNCODE,
|
|
595
|
-
is_dirty_tree=False,
|
|
596
|
-
)
|
|
597
|
-
dirty_success = invoker.CodeReviewOutcome(
|
|
598
|
-
mode=MODE_CHAIN,
|
|
599
|
-
served_command=FIXTURE_SERVED_COMMAND,
|
|
600
|
-
returncode=FIXTURE_CHAIN_RETURNCODE,
|
|
601
|
-
is_dirty_tree=True,
|
|
602
|
-
)
|
|
603
|
-
failed_serve = invoker.CodeReviewOutcome(
|
|
604
|
-
mode=MODE_CHAIN,
|
|
605
|
-
served_command=None,
|
|
606
|
-
returncode=FIXTURE_FAILED_RETURNCODE,
|
|
607
|
-
is_dirty_tree=False,
|
|
87
|
+
outcome = invoker._run_chain_review(
|
|
88
|
+
working_directory=tmp_path,
|
|
89
|
+
timeout_seconds=MINT_TIMEOUT_SECONDS,
|
|
90
|
+
effort=DEFAULT_CODE_REVIEW_EFFORT,
|
|
608
91
|
)
|
|
609
|
-
in_session_ready = invoker.CodeReviewOutcome(
|
|
610
|
-
mode=MODE_IN_SESSION,
|
|
611
|
-
served_command=None,
|
|
612
|
-
returncode=IN_SESSION_RETURNCODE,
|
|
613
|
-
is_dirty_tree=False,
|
|
614
|
-
)
|
|
615
|
-
|
|
616
|
-
assert invoker.is_code_review_clean_stamp_allowed(clean_success) is True
|
|
617
|
-
assert invoker.is_code_review_clean_stamp_allowed(dirty_success) is False
|
|
618
|
-
assert invoker.is_code_review_clean_stamp_allowed(failed_serve) is False
|
|
619
|
-
assert invoker.is_code_review_clean_stamp_allowed(in_session_ready) is True
|
|
620
|
-
assert invoker.is_successful_code_review(failed_serve) is False
|
|
621
|
-
assert invoker.is_successful_code_review(clean_success) is True
|
|
622
92
|
|
|
93
|
+
captured_streams = capsys.readouterr()
|
|
94
|
+
assert outcome.returncode == REVIEW_FAILURE_RETURNCODE
|
|
95
|
+
assert REVIEW_BINARY_REFUSAL_TEXT in captured_streams.err
|
|
623
96
|
|
|
624
|
-
@pytest.mark.parametrize(
|
|
625
|
-
("session_model", "expected_is_opus"),
|
|
626
|
-
[
|
|
627
|
-
(FIXTURE_SESSION_OPUS, True),
|
|
628
|
-
(FIXTURE_SESSION_OPUS_UPPER, True),
|
|
629
|
-
(FIXTURE_SESSION_SONNET, False),
|
|
630
|
-
(" opus ", True),
|
|
631
|
-
],
|
|
632
|
-
)
|
|
633
|
-
def test_is_opus_session_model(
|
|
634
|
-
session_model: str, expected_is_opus: bool
|
|
635
|
-
) -> None:
|
|
636
|
-
assert invoker.is_opus_session_model(session_model) is expected_is_opus
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
@pytest.mark.parametrize(
|
|
640
|
-
("host_profile", "session_model", "expected_mode"),
|
|
641
|
-
[
|
|
642
|
-
(HOST_PROFILE_CLAUDE, FIXTURE_SESSION_OPUS, MODE_IN_SESSION),
|
|
643
|
-
(HOST_PROFILE_CLAUDE, FIXTURE_SESSION_SONNET, MODE_CHAIN),
|
|
644
|
-
(HOST_PROFILE_THIRD_PARTY, FIXTURE_SESSION_OPUS, MODE_CHAIN),
|
|
645
|
-
],
|
|
646
|
-
)
|
|
647
|
-
def test_decide_review_mode(
|
|
648
|
-
host_profile: str, session_model: str, expected_mode: str
|
|
649
|
-
) -> None:
|
|
650
|
-
assert (
|
|
651
|
-
invoker.decide_review_mode(
|
|
652
|
-
host_profile=host_profile,
|
|
653
|
-
session_model=session_model,
|
|
654
|
-
)
|
|
655
|
-
== expected_mode
|
|
656
|
-
)
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
def test_encode_code_review_outcome_shape() -> None:
|
|
660
|
-
review_outcome = invoker.CodeReviewOutcome(
|
|
661
|
-
mode=MODE_CHAIN,
|
|
662
|
-
served_command=FIXTURE_SERVED_COMMAND,
|
|
663
|
-
returncode=FIXTURE_CHAIN_RETURNCODE,
|
|
664
|
-
is_dirty_tree=True,
|
|
665
|
-
)
|
|
666
|
-
encoded_payload = invoker.encode_code_review_outcome(review_outcome)
|
|
667
|
-
assert encoded_payload == {
|
|
668
|
-
RESULT_KEY_MODE: MODE_CHAIN,
|
|
669
|
-
RESULT_KEY_SERVED_COMMAND: FIXTURE_SERVED_COMMAND,
|
|
670
|
-
RESULT_KEY_RETURNCODE: FIXTURE_CHAIN_RETURNCODE,
|
|
671
|
-
RESULT_KEY_DIRTY_TREE: True,
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
EFFORT_LOW = "low"
|
|
676
|
-
REJECTED_ULTRA_EFFORT = "ultra"
|
|
677
|
-
RECORD_STAMP_MINT_CAP = 3
|
|
678
|
-
SINGLE_PASS_CAP = 1
|
|
679
|
-
INVALID_EFFORT_EXIT_CODE = 2
|
|
680
|
-
DID_NOT_CONVERGE_EXIT_CODE = 1
|
|
681
|
-
RECORD_STAMP_CLI_FLAG = "--record-stamp"
|
|
682
|
-
RESULT_STAMP_MINTED_KEY = "stamp_minted"
|
|
683
|
-
RESULT_PASS_COUNT_KEY = "pass_count"
|
|
684
|
-
RESULT_BOUND_HASH_KEY = "bound_hash"
|
|
685
|
-
MISSING_STORE_FILE_NAME = "code_review_stamp_store_absent.py"
|
|
686
|
-
SURFACE_SOURCE = "def add(left: int, right: int) -> int:\n return left + right\n"
|
|
687
|
-
SURFACE_CHANGE_SOURCE = "def add(left: int, right: int) -> int:\n return left - right\n"
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
def _run_git(repository_directory: Path, *git_arguments: str) -> None:
|
|
691
|
-
subprocess.run(
|
|
692
|
-
[GIT_BINARY, "-C", str(repository_directory), *git_arguments],
|
|
693
|
-
check=True,
|
|
694
|
-
capture_output=True,
|
|
695
|
-
text=True,
|
|
696
|
-
timeout=GIT_INIT_TIMEOUT_SECONDS,
|
|
697
|
-
)
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
def _make_repo_with_change_surface(tmp_path: Path) -> Path:
|
|
701
|
-
origin_directory = tmp_path / "origin.git"
|
|
702
|
-
work_directory = tmp_path / "work"
|
|
703
|
-
work_directory.mkdir()
|
|
704
|
-
subprocess.run(
|
|
705
|
-
[GIT_BINARY, "init", "--bare", "--initial-branch=main", str(origin_directory)],
|
|
706
|
-
check=True,
|
|
707
|
-
capture_output=True,
|
|
708
|
-
text=True,
|
|
709
|
-
timeout=GIT_INIT_TIMEOUT_SECONDS,
|
|
710
|
-
)
|
|
711
|
-
_run_git(work_directory, "init", "--initial-branch=main")
|
|
712
|
-
_run_git(work_directory, "config", "user.email", "tests@example.com")
|
|
713
|
-
_run_git(work_directory, "config", "user.name", "Reviewer")
|
|
714
|
-
(work_directory / "app.py").write_text(SURFACE_SOURCE, encoding="utf-8")
|
|
715
|
-
_run_git(work_directory, "add", "-A")
|
|
716
|
-
_run_git(work_directory, "commit", "-m", "base")
|
|
717
|
-
_run_git(work_directory, "remote", "add", "origin", str(origin_directory))
|
|
718
|
-
_run_git(work_directory, "push", "-u", "origin", "main")
|
|
719
|
-
(work_directory / "app.py").write_text(SURFACE_CHANGE_SOURCE, encoding="utf-8")
|
|
720
|
-
return work_directory
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
def _isolate_home(monkeypatch: pytest.MonkeyPatch, fake_home: Path) -> None:
|
|
724
|
-
home_text = str(fake_home)
|
|
725
|
-
monkeypatch.setenv("HOME", home_text)
|
|
726
|
-
monkeypatch.setenv("USERPROFILE", home_text)
|
|
727
|
-
monkeypatch.delenv("HOMEDRIVE", raising=False)
|
|
728
|
-
monkeypatch.delenv("HOMEPATH", raising=False)
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
def _prepared_surface_repo(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path:
|
|
732
|
-
fake_home = tmp_path / "home"
|
|
733
|
-
fake_home.mkdir()
|
|
734
|
-
_isolate_home(monkeypatch, fake_home)
|
|
735
|
-
return _make_repo_with_change_surface(tmp_path)
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
def _chain_clean_outcome() -> invoker.CodeReviewOutcome:
|
|
739
|
-
return invoker.CodeReviewOutcome(
|
|
740
|
-
mode=MODE_CHAIN,
|
|
741
|
-
served_command=FIXTURE_SERVED_COMMAND,
|
|
742
|
-
returncode=FIXTURE_CHAIN_RETURNCODE,
|
|
743
|
-
is_dirty_tree=True,
|
|
744
|
-
)
|
|
745
97
|
|
|
746
|
-
|
|
747
|
-
def _stable_clean_review(**_review_keywords: object) -> invoker.CodeReviewOutcome:
|
|
748
|
-
return _chain_clean_outcome()
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
def _surface_changing_review(
|
|
752
|
-
*, working_directory: Path, **_review_keywords: object
|
|
753
|
-
) -> invoker.CodeReviewOutcome:
|
|
754
|
-
applied_fix_path = working_directory / DIRTY_FILE_NAME
|
|
755
|
-
applied_fix_path.write_text(DIRTY_FILE_CONTENTS, encoding="utf-8")
|
|
756
|
-
return _chain_clean_outcome()
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
class _DriftingReview:
|
|
760
|
-
def __init__(self) -> None:
|
|
761
|
-
self.pass_count = 0
|
|
762
|
-
|
|
763
|
-
def __call__(
|
|
764
|
-
self, *, working_directory: Path, **_review_keywords: object
|
|
765
|
-
) -> invoker.CodeReviewOutcome:
|
|
766
|
-
self.pass_count += 1
|
|
767
|
-
drift_path = working_directory / f"fix_{self.pass_count}.txt"
|
|
768
|
-
drift_path.write_text(str(self.pass_count), encoding="utf-8")
|
|
769
|
-
return _chain_clean_outcome()
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
@pytest.mark.parametrize("valid_effort", ["low", "medium", "high", "xhigh", "max"])
|
|
773
|
-
def test_validate_effort_token_accepts_known_tokens(valid_effort: str) -> None:
|
|
774
|
-
assert invoker.validate_effort_token(valid_effort) is None
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
def test_validate_effort_token_rejects_ultra_loudly() -> None:
|
|
778
|
-
error_message = invoker.validate_effort_token(REJECTED_ULTRA_EFFORT)
|
|
779
|
-
assert error_message is not None
|
|
780
|
-
assert REJECTED_ULTRA_EFFORT in error_message
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
def test_validate_effort_token_rejects_unknown_token() -> None:
|
|
784
|
-
error_message = invoker.validate_effort_token("bogus")
|
|
785
|
-
assert error_message is not None
|
|
786
|
-
assert "bogus" in error_message
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
def test_build_code_review_prompt_reads_as_slash_command() -> None:
|
|
790
|
-
assert invoker.build_code_review_prompt(EFFORT_LOW) == "/code-review low --fix"
|
|
791
|
-
assert invoker.build_code_review_prompt("xhigh") == "/code-review xhigh --fix"
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
def test_cli_rejects_ultra_effort_with_nonzero_exit(
|
|
98
|
+
def test_missing_chain_config_reports_its_remedy_on_stderr(
|
|
795
99
|
monkeypatch: pytest.MonkeyPatch,
|
|
796
100
|
tmp_path: Path,
|
|
797
101
|
capsys: pytest.CaptureFixture[str],
|
|
798
102
|
) -> None:
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
monkeypatch,
|
|
802
|
-
host_profile=HOST_PROFILE_CLAUDE,
|
|
803
|
-
claude_outcome=None,
|
|
804
|
-
working_directory=working_directory,
|
|
805
|
-
)
|
|
806
|
-
exit_code = invoker.main(
|
|
807
|
-
[
|
|
808
|
-
CWD_FLAG,
|
|
809
|
-
str(working_directory),
|
|
810
|
-
CLI_SESSION_MODEL_FLAG,
|
|
811
|
-
FIXTURE_SESSION_OPUS,
|
|
812
|
-
REJECTED_ULTRA_EFFORT,
|
|
813
|
-
]
|
|
814
|
-
)
|
|
815
|
-
assert exit_code == INVALID_EFFORT_EXIT_CODE
|
|
816
|
-
assert REJECTED_ULTRA_EFFORT in capsys.readouterr().err
|
|
103
|
+
def raise_chain_configuration_error(**_all_keyword_arguments: object) -> None:
|
|
104
|
+
raise ChainConfigurationError(CHAIN_CONFIG_REMEDY_TEXT)
|
|
817
105
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
821
|
-
) -> None:
|
|
822
|
-
working_directory = _prepared_surface_repo(monkeypatch, tmp_path)
|
|
823
|
-
monkeypatch.setattr(invoker, "invoke_code_review", _stable_clean_review)
|
|
824
|
-
mint_outcome = invoker.invoke_code_review_and_record_stamp(
|
|
825
|
-
working_directory=working_directory,
|
|
826
|
-
session_model=CODE_REVIEW_MODEL_ALIAS,
|
|
827
|
-
timeout_seconds=DEFAULT_CODE_REVIEW_TIMEOUT_SECONDS,
|
|
828
|
-
effort=EFFORT_LOW,
|
|
829
|
-
)
|
|
830
|
-
assert mint_outcome.is_stamp_minted is True
|
|
831
|
-
assert mint_outcome.bound_hash is not None
|
|
832
|
-
store_module = invoker.load_code_review_stamp_store()
|
|
833
|
-
assert store_module.stamp_covers_surface(
|
|
834
|
-
str(working_directory), mint_outcome.bound_hash, EFFORT_LOW
|
|
106
|
+
monkeypatch.setattr(
|
|
107
|
+
invoker, "invoke_code_review_and_record_stamp", raise_chain_configuration_error
|
|
835
108
|
)
|
|
836
109
|
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
monkeypatch.setattr(invoker, "invoke_code_review", _surface_changing_review)
|
|
843
|
-
mint_outcome = invoker.invoke_code_review_and_record_stamp(
|
|
844
|
-
working_directory=working_directory,
|
|
845
|
-
session_model=CODE_REVIEW_MODEL_ALIAS,
|
|
846
|
-
timeout_seconds=DEFAULT_CODE_REVIEW_TIMEOUT_SECONDS,
|
|
847
|
-
effort=EFFORT_LOW,
|
|
848
|
-
maximum_passes=SINGLE_PASS_CAP,
|
|
110
|
+
outcome = invoker._mint_or_config_outcome(
|
|
111
|
+
working_directory=tmp_path,
|
|
112
|
+
session_model=FIXTURE_SESSION_OPUS,
|
|
113
|
+
timeout_seconds=MINT_TIMEOUT_SECONDS,
|
|
114
|
+
effort=DEFAULT_CODE_REVIEW_EFFORT,
|
|
849
115
|
)
|
|
850
|
-
assert mint_outcome.is_stamp_minted is False
|
|
851
|
-
assert mint_outcome.pass_count == SINGLE_PASS_CAP
|
|
852
|
-
assert mint_outcome.bound_hash is None
|
|
853
|
-
|
|
854
116
|
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
working_directory = _prepared_surface_repo(monkeypatch, tmp_path)
|
|
859
|
-
monkeypatch.setattr(invoker, "invoke_code_review", _DriftingReview())
|
|
860
|
-
mint_outcome = invoker.invoke_code_review_and_record_stamp(
|
|
861
|
-
working_directory=working_directory,
|
|
862
|
-
session_model=CODE_REVIEW_MODEL_ALIAS,
|
|
863
|
-
timeout_seconds=DEFAULT_CODE_REVIEW_TIMEOUT_SECONDS,
|
|
864
|
-
effort=EFFORT_LOW,
|
|
865
|
-
maximum_passes=RECORD_STAMP_MINT_CAP,
|
|
866
|
-
)
|
|
867
|
-
assert mint_outcome.is_stamp_minted is False
|
|
868
|
-
assert mint_outcome.pass_count == RECORD_STAMP_MINT_CAP
|
|
117
|
+
captured_streams = capsys.readouterr()
|
|
118
|
+
assert outcome.is_stamp_minted is False
|
|
119
|
+
assert CHAIN_CONFIG_REMEDY_TEXT in captured_streams.err
|
|
869
120
|
|
|
870
121
|
|
|
871
|
-
def
|
|
122
|
+
def test_host_profile_failure_reports_its_reason_on_stderr(
|
|
872
123
|
monkeypatch: pytest.MonkeyPatch,
|
|
873
124
|
tmp_path: Path,
|
|
874
125
|
capsys: pytest.CaptureFixture[str],
|
|
875
126
|
) -> None:
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
exit_code = invoker.main(
|
|
879
|
-
[
|
|
880
|
-
CWD_FLAG,
|
|
881
|
-
str(working_directory),
|
|
882
|
-
CLI_SESSION_MODEL_FLAG,
|
|
883
|
-
CODE_REVIEW_MODEL_ALIAS,
|
|
884
|
-
RECORD_STAMP_CLI_FLAG,
|
|
885
|
-
EFFORT_LOW,
|
|
886
|
-
]
|
|
887
|
-
)
|
|
888
|
-
assert exit_code == DID_NOT_CONVERGE_EXIT_CODE
|
|
889
|
-
parsed_payload = json.loads(capsys.readouterr().out)
|
|
890
|
-
assert parsed_payload[RESULT_STAMP_MINTED_KEY] is False
|
|
891
|
-
assert parsed_payload[RESULT_PASS_COUNT_KEY] == RECORD_STAMP_MINT_CAP
|
|
892
|
-
assert parsed_payload[RESULT_BOUND_HASH_KEY] is None
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
def test_load_code_review_stamp_store_records_and_covers_surface(
|
|
896
|
-
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
897
|
-
) -> None:
|
|
898
|
-
working_directory = _prepared_surface_repo(monkeypatch, tmp_path)
|
|
899
|
-
store_module = invoker.load_code_review_stamp_store()
|
|
900
|
-
surface_hash = store_module.live_surface_hash(str(working_directory))
|
|
901
|
-
assert surface_hash is not None
|
|
902
|
-
stamp_path = store_module.record_clean_stamp(
|
|
903
|
-
str(working_directory), surface_hash, EFFORT_LOW
|
|
904
|
-
)
|
|
905
|
-
assert stamp_path.exists()
|
|
906
|
-
assert store_module.stamp_covers_surface(
|
|
907
|
-
str(working_directory), surface_hash, EFFORT_LOW
|
|
908
|
-
)
|
|
909
|
-
|
|
127
|
+
def raise_host_profile_error(**_all_keyword_arguments: object) -> None:
|
|
128
|
+
raise ValueError(HOST_PROFILE_FAILURE_TEXT)
|
|
910
129
|
|
|
911
|
-
def test_load_code_review_stamp_store_raises_when_file_absent(
|
|
912
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
913
|
-
) -> None:
|
|
914
130
|
monkeypatch.setattr(
|
|
915
|
-
invoker, "
|
|
131
|
+
invoker, "invoke_code_review_and_record_stamp", raise_host_profile_error
|
|
916
132
|
)
|
|
917
|
-
with pytest.raises(ModuleNotFoundError):
|
|
918
|
-
invoker.load_code_review_stamp_store()
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
def test_cli_record_stamp_reports_missing_store_dependency(
|
|
922
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
923
|
-
tmp_path: Path,
|
|
924
|
-
capsys: pytest.CaptureFixture[str],
|
|
925
|
-
) -> None:
|
|
926
|
-
working_directory = _init_git_repository(tmp_path / "repo")
|
|
927
|
-
|
|
928
|
-
def raise_missing_store(*all_args: object, **all_keywords: object) -> object:
|
|
929
|
-
del all_args, all_keywords
|
|
930
|
-
raise ModuleNotFoundError("store missing", name="code_review_stamp_store")
|
|
931
133
|
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
CLI_SESSION_MODEL_FLAG,
|
|
938
|
-
CODE_REVIEW_MODEL_ALIAS,
|
|
939
|
-
RECORD_STAMP_CLI_FLAG,
|
|
940
|
-
EFFORT_LOW,
|
|
941
|
-
]
|
|
134
|
+
outcome = invoker._mint_or_config_outcome(
|
|
135
|
+
working_directory=tmp_path,
|
|
136
|
+
session_model=FIXTURE_SESSION_OPUS,
|
|
137
|
+
timeout_seconds=MINT_TIMEOUT_SECONDS,
|
|
138
|
+
effort=DEFAULT_CODE_REVIEW_EFFORT,
|
|
942
139
|
)
|
|
943
|
-
assert exit_code == INVALID_EFFORT_EXIT_CODE
|
|
944
|
-
captured = capsys.readouterr()
|
|
945
|
-
assert "stamp store" in captured.err
|
|
946
|
-
parsed_payload = json.loads(captured.out)
|
|
947
|
-
assert parsed_payload[RESULT_STAMP_MINTED_KEY] is False
|
|
948
140
|
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
mode=MODE_CHAIN,
|
|
953
|
-
served_command=FIXTURE_SERVED_COMMAND,
|
|
954
|
-
returncode=FIXTURE_CHAIN_RETURNCODE,
|
|
955
|
-
is_dirty_tree=False,
|
|
956
|
-
)
|
|
957
|
-
mint_outcome = invoker.StampMintOutcome(
|
|
958
|
-
review_outcome=review_outcome,
|
|
959
|
-
is_stamp_minted=True,
|
|
960
|
-
pass_count=SINGLE_PASS_CAP,
|
|
961
|
-
bound_hash="abc123",
|
|
962
|
-
)
|
|
963
|
-
encoded_payload = invoker.encode_stamp_mint_outcome(mint_outcome)
|
|
964
|
-
assert encoded_payload[RESULT_STAMP_MINTED_KEY] is True
|
|
965
|
-
assert encoded_payload[RESULT_PASS_COUNT_KEY] == SINGLE_PASS_CAP
|
|
966
|
-
assert encoded_payload[RESULT_BOUND_HASH_KEY] == "abc123"
|
|
141
|
+
captured_streams = capsys.readouterr()
|
|
142
|
+
assert outcome.is_stamp_minted is False
|
|
143
|
+
assert HOST_PROFILE_FAILURE_TEXT in captured_streams.err
|