claude-dev-env 2.3.0 → 2.5.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 +53 -48
- package/_shared/pr-loop/scripts/_claude_permissions_common.py +84 -0
- package/_shared/pr-loop/scripts/code_rules_gate.py +4 -2
- 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/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 -1
- 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 +3 -4
- 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/skill-writer-agent.md +84 -0
- package/agents/test_agent_frontmatter.py +67 -18
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +105 -3
- package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +29 -13
- package/bin/CLAUDE.md +68 -5
- 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/commands/implement.md +1 -1
- package/commands/right-size.md +1 -1
- package/docs/CLAUDE.md +2 -0
- package/docs/host-pool-health-monitor.md +102 -0
- package/docs/references/CLAUDE.md +5 -2
- package/docs/references/advisor-tool.md +13 -0
- package/docs/references/code-review-enforcement.md +107 -0
- package/docs/references/team-advisor-skill.md +14 -0
- package/docs/wsl-docker-cowork-starter-matrix.md +89 -0
- package/hooks/blocking/CLAUDE.md +9 -1
- package/hooks/blocking/code_review_enforcement_config_bootstrap.py +53 -0
- package/hooks/blocking/code_review_gate_deny.py +74 -0
- package/hooks/blocking/code_review_pr_create_gate.py +198 -0
- package/hooks/blocking/code_review_push_gate.py +145 -0
- package/hooks/blocking/code_review_stamp_directory_write_blocker.py +348 -0
- package/hooks/blocking/code_review_stamp_store.py +233 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +7 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +15 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +212 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +138 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +49 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +38 -0
- package/hooks/blocking/code_verifier_spawn_preflight_gate.py +39 -27
- package/hooks/blocking/config/__init__.py +5 -5
- package/hooks/blocking/config/code_review_enforcement_constants.py +113 -0
- package/hooks/blocking/config/test_code_review_enforcement_constants.py +113 -0
- package/hooks/blocking/config/verified_commit_constants.py +160 -155
- package/hooks/blocking/conftest.py +2 -0
- package/hooks/blocking/convergence_gate_blocker.py +112 -23
- package/hooks/blocking/destructive_command_blocker.py +19 -6
- package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +256 -0
- package/hooks/blocking/pr_description_proof_of_work.py +52 -34
- package/hooks/blocking/pre_tool_use_dispatcher.py +24 -24
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +4 -1
- package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +62 -0
- package/hooks/blocking/test_code_review_gate_deny.py +54 -0
- package/hooks/blocking/test_code_review_pr_create_gate.py +199 -0
- package/hooks/blocking/test_code_review_push_gate.py +205 -0
- package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +199 -0
- package/hooks/blocking/test_code_review_stamp_store.py +205 -0
- package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +124 -2
- package/hooks/blocking/test_convergence_gate_blocker.py +153 -5
- package/hooks/blocking/test_destructive_command_blocker.py +1 -1
- package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +45 -0
- package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +231 -0
- package/hooks/blocking/test_pr_description_proof_of_work.py +151 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +17 -8
- package/hooks/blocking/test_verdict_directory_write_blocker.py +808 -808
- package/hooks/blocking/test_verification_verdict_store.py +974 -903
- 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/test_volatile_path_in_post_blocker.py +114 -2
- package/hooks/blocking/verdict_directory_write_blocker.py +687 -687
- package/hooks/blocking/verification_verdict_store.py +1039 -1014
- package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +29 -17
- package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +35 -0
- package/hooks/blocking/verified_commit_message_accuracy_blocker.py +167 -167
- package/hooks/blocking/verifier_verdict_minter.py +280 -280
- package/hooks/blocking/volatile_path_in_post_blocker.py +69 -8
- package/hooks/git-hooks/git_hooks_constants/__init__.py +6 -0
- package/hooks/git-hooks/pre_push.py +89 -2
- package/hooks/git-hooks/test_pre_push.py +128 -0
- package/hooks/hooks.json +26 -1
- package/hooks/hooks_constants/CLAUDE.md +3 -1
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +8 -0
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
- package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +26 -11
- package/hooks/hooks_constants/convergence_gate_blocker_constants.py +20 -3
- package/hooks/hooks_constants/destructive_command_segment_constants.py +3 -1
- package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -18
- package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +48 -0
- package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -4
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
- package/hooks/hooks_constants/pyproject_config_discovery_constants.py +16 -0
- package/hooks/hooks_constants/ruff_integration_constants.py +16 -0
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +24 -0
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +6 -0
- package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +8 -1
- 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 +2 -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/mypy_integration.py +63 -50
- package/hooks/validators/pyproject_config_discovery.py +101 -0
- package/hooks/validators/ruff_integration.py +257 -25
- package/hooks/validators/run_all_validators.py +223 -19
- package/hooks/validators/test_directory_exemption_constants.py +185 -0
- package/hooks/validators/test_mypy_integration.py +32 -0
- package/hooks/validators/test_pyproject_config_discovery.py +94 -0
- package/hooks/validators/test_python_antipattern_checks.py +110 -5
- package/hooks/validators/test_ruff_integration.py +160 -2
- package/hooks/validators/test_run_all_validators.py +140 -68
- package/hooks/validators/test_run_all_validators_config_discovery.py +123 -0
- package/hooks/validators/test_run_all_validators_pretooluse.py +159 -1
- package/package.json +10 -2
- package/rules/CLAUDE.md +1 -0
- package/rules/docstring-prose-matches-implementation.md +45 -67
- package/rules/durable-post-artifacts.md +7 -0
- package/rules/state-what-is.md +25 -0
- package/rules/verified-commit-gate-skip.md +1 -1
- package/scripts/CLAUDE.md +1 -0
- package/scripts/Capture-PoolHealth.ps1 +410 -0
- package/scripts/_code_review_test_support.py +404 -0
- package/scripts/claude_chain_runner.py +141 -1
- package/scripts/codec_forwarding_test_support.py +83 -0
- package/scripts/conftest.py +23 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +2 -2
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +53 -1
- package/scripts/dev_env_scripts_constants/code_review_constants.py +129 -12
- package/scripts/dev_env_scripts_constants/test_code_review_constants.py +55 -0
- package/scripts/invoke_code_review.py +550 -38
- package/scripts/resolve_worker_spawn.py +626 -619
- package/scripts/spawn_grok_batch.py +672 -672
- package/scripts/test_claude_chain_runner.py +131 -0
- 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_codec.py +77 -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/test_resolve_worker_spawn_codec.py +101 -0
- package/skills/CLAUDE.md +2 -0
- package/skills/auditing-claude-config/SKILL.md +114 -114
- package/skills/autoconverge/SKILL.md +427 -421
- package/skills/autoconverge/reference/convergence.md +26 -4
- package/skills/autoconverge/reference/multi-pr.md +6 -1
- package/skills/autoconverge/reference/stop-conditions.md +16 -10
- package/skills/autoconverge/workflow/CLAUDE.md +1 -0
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +4 -4
- package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +175 -3
- package/skills/autoconverge/workflow/converge.contract.test.mjs +1263 -1244
- package/skills/autoconverge/workflow/converge.mjs +191 -8
- package/skills/autoconverge/workflow/converge.p2-advance.test.mjs +202 -0
- package/skills/autoconverge/workflow/converge_multi.mjs +7 -3
- package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +5 -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/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 +81 -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 +2 -0
- package/skills/fresh-branch/SKILL.md +2 -0
- package/skills/fresh-branch/scripts/create_fresh_branch.py +78 -180
- package/skills/fresh-branch/scripts/fresh_branch_git_commands.py +285 -0
- package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -0
- package/skills/fresh-branch/scripts/pytest.ini +4 -0
- package/skills/fresh-branch/scripts/test_create_fresh_branch.py +98 -0
- package/skills/fresh-branch/scripts/test_fresh_branch_git_commands.py +310 -0
- 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 +162 -21
- 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 +110 -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/prototype/SKILL.md +86 -0
- package/skills/prototype/reference/honest-limitations.md +23 -0
- package/skills/prototype/reference/promotion-tasks.md +23 -0
- package/skills/prototype/scripts/build_sandbox_settings.py +249 -0
- package/skills/prototype/scripts/conftest.py +15 -0
- package/skills/prototype/scripts/launch_sandbox.py +205 -0
- package/skills/prototype/scripts/probe_sandbox_safety.py +311 -0
- package/skills/prototype/scripts/prototype_scripts_constants/__init__.py +1 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/__init__.py +0 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/build_sandbox_settings_constants.py +41 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/launch_sandbox_constants.py +23 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/probe_sandbox_safety_constants.py +45 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/prototype_common_constants.py +10 -0
- package/skills/prototype/scripts/test_build_sandbox_settings.py +275 -0
- package/skills/prototype/scripts/test_launch_sandbox.py +303 -0
- package/skills/prototype/scripts/test_probe_sandbox_safety.py +284 -0
- package/skills/prototype/workflows/promotion.md +27 -0
- package/skills/prototype/workflows/sandbox.md +35 -0
- package/skills/release-notes-html/SKILL.md +164 -0
- package/skills/skill-builder/CLAUDE.md +3 -3
- package/skills/skill-builder/SKILL.md +5 -5
- package/skills/skill-builder/references/CLAUDE.md +1 -1
- package/skills/skill-builder/references/delegation-map.md +3 -3
- package/skills/skill-builder/references/description-field.md +1 -1
- package/skills/skill-builder/references/skill-modularity.md +2 -3
- package/skills/skill-builder/workflows/CLAUDE.md +1 -1
- package/skills/skill-builder/workflows/improve-skill.md +1 -1
- package/skills/skill-builder/workflows/new-skill.md +2 -2
- 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/skills/team-advisor/SKILL.md +2 -2
- package/scripts/test_invoke_code_review.py +0 -672
- package/skills/closeout/reference/issue-body-templates.md +0 -108
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""Mutation-style tests for the Plan-to-PR packet validator."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
VALIDATOR_PATH = Path(__file__).with_name("validate_packet.py")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def write_packet(packet_directory: Path) -> None:
|
|
15
|
+
"""Create a complete packet and its grounded source file."""
|
|
16
|
+
packet_directory.mkdir(parents=True)
|
|
17
|
+
repository_root = packet_directory.parents[2]
|
|
18
|
+
source_file = repository_root / "src" / "auth.py"
|
|
19
|
+
source_file.parent.mkdir(parents=True, exist_ok=True)
|
|
20
|
+
source_file.write_text(
|
|
21
|
+
"def authenticate_user():\n return True\n", encoding="utf-8"
|
|
22
|
+
)
|
|
23
|
+
task_details = "## Tasks\n- task-1: Login validation; src/auth.py; python -m pytest tests/test_auth.py; python -m pytest tests/test_auth.py; python -m compileall src/auth.py\n"
|
|
24
|
+
for filename, sections in {
|
|
25
|
+
"context.md": (
|
|
26
|
+
"Request",
|
|
27
|
+
"Repository Facts",
|
|
28
|
+
"Constraints",
|
|
29
|
+
"Source References",
|
|
30
|
+
),
|
|
31
|
+
"plan.md": ("Implementation", "Decisions", "Dependencies", "Risks"),
|
|
32
|
+
"tasks.md": (),
|
|
33
|
+
"handoff.md": ("Approval", "Task Order"),
|
|
34
|
+
}.items():
|
|
35
|
+
headings = "\n".join(
|
|
36
|
+
f"## {section}\nGrounded packet detail." for section in sections
|
|
37
|
+
)
|
|
38
|
+
if filename == "tasks.md":
|
|
39
|
+
headings = task_details.rstrip()
|
|
40
|
+
if filename == "handoff.md":
|
|
41
|
+
headings += "\nPacket: add-login\nApproval: approved\nTask Order: task-1\nTask task-1: Login validation\nAllowed files: src/auth.py\nAcceptance command: python -m pytest tests/test_auth.py\nTest command: python -m pytest tests/test_auth.py\nVerification command: python -m compileall src/auth.py"
|
|
42
|
+
(packet_directory / filename).write_text(headings + "\n", encoding="utf-8")
|
|
43
|
+
payload = {
|
|
44
|
+
"schema_version": 1,
|
|
45
|
+
"slug": packet_directory.name,
|
|
46
|
+
"status": "approved",
|
|
47
|
+
"request": "Add login validation.",
|
|
48
|
+
"allowed_files": ["src/auth.py"],
|
|
49
|
+
"sources": [
|
|
50
|
+
{
|
|
51
|
+
"path": "src/auth.py",
|
|
52
|
+
"locator": "section authenticate_user",
|
|
53
|
+
"fact": "The helper returns the login decision.",
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
"decisions": ["Reuse the existing helper."],
|
|
57
|
+
"open_questions": [],
|
|
58
|
+
"tasks": [
|
|
59
|
+
{
|
|
60
|
+
"id": "task-1",
|
|
61
|
+
"deliverable": "Login validation",
|
|
62
|
+
"allowed_files": ["src/auth.py"],
|
|
63
|
+
"acceptance_command": "python -m pytest tests/test_auth.py",
|
|
64
|
+
"test_command": "python -m pytest tests/test_auth.py",
|
|
65
|
+
"verification_command": "python -m compileall src/auth.py",
|
|
66
|
+
"commit": 1,
|
|
67
|
+
}
|
|
68
|
+
],
|
|
69
|
+
"validation": {
|
|
70
|
+
"schema_valid": True,
|
|
71
|
+
"boundary_valid": True,
|
|
72
|
+
"markdown_matches": True,
|
|
73
|
+
"validated_by": "native-plan-to-pr",
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
(packet_directory / "packet.json").write_text(json.dumps(payload), encoding="utf-8")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def run_validator(packet_directory: Path) -> subprocess.CompletedProcess[str]:
|
|
80
|
+
"""Run the validator as a real CLI process."""
|
|
81
|
+
return subprocess.run(
|
|
82
|
+
[sys.executable, str(VALIDATOR_PATH), str(packet_directory)],
|
|
83
|
+
capture_output=True,
|
|
84
|
+
text=True,
|
|
85
|
+
check=False,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_valid_packet_passes(tmp_path: Path) -> None:
|
|
90
|
+
packet_directory = tmp_path / "docs" / "plans" / "add-login"
|
|
91
|
+
write_packet(packet_directory)
|
|
92
|
+
validator_run = run_validator(packet_directory)
|
|
93
|
+
assert validator_run.returncode == 0
|
|
94
|
+
assert validator_run.stdout.strip() == "packet validation passed"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_missing_packet_file_reports_field(tmp_path: Path) -> None:
|
|
98
|
+
packet_directory = tmp_path / "docs" / "plans" / "add-login"
|
|
99
|
+
write_packet(packet_directory)
|
|
100
|
+
(packet_directory / "plan.md").unlink()
|
|
101
|
+
validator_run = run_validator(packet_directory)
|
|
102
|
+
assert validator_run.returncode != 0
|
|
103
|
+
assert "missing required file: plan.md" in validator_run.stderr
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_placeholder_packet_fails(tmp_path: Path) -> None:
|
|
107
|
+
packet_directory = tmp_path / "docs" / "plans" / "add-login"
|
|
108
|
+
write_packet(packet_directory)
|
|
109
|
+
(packet_directory / "context.md").write_text(
|
|
110
|
+
"## Request\nTODO: fill this in", encoding="utf-8"
|
|
111
|
+
)
|
|
112
|
+
validator_run = run_validator(packet_directory)
|
|
113
|
+
assert validator_run.returncode != 0
|
|
114
|
+
assert "context.md: contains unresolved" in validator_run.stderr
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_task_scope_outside_allowed_files_fails(tmp_path: Path) -> None:
|
|
118
|
+
packet_directory = tmp_path / "docs" / "plans" / "add-login"
|
|
119
|
+
write_packet(packet_directory)
|
|
120
|
+
packet_file = packet_directory / "packet.json"
|
|
121
|
+
packet_payload = json.loads(packet_file.read_text(encoding="utf-8"))
|
|
122
|
+
packet_payload["tasks"][0]["allowed_files"] = ["src/other.py"]
|
|
123
|
+
packet_file.write_text(json.dumps(packet_payload), encoding="utf-8")
|
|
124
|
+
validator_run = run_validator(packet_directory)
|
|
125
|
+
assert validator_run.returncode != 0
|
|
126
|
+
assert "outside packet scope" in validator_run.stderr
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_unknown_schema_field_fails(tmp_path: Path) -> None:
|
|
130
|
+
"""Schema-shaped packets reject fields outside the packet contract."""
|
|
131
|
+
packet_directory = tmp_path / "docs" / "plans" / "add-login"
|
|
132
|
+
write_packet(packet_directory)
|
|
133
|
+
packet_file = packet_directory / "packet.json"
|
|
134
|
+
packet_payload = json.loads(packet_file.read_text(encoding="utf-8"))
|
|
135
|
+
packet_payload["unexpected"] = True
|
|
136
|
+
packet_file.write_text(json.dumps(packet_payload), encoding="utf-8")
|
|
137
|
+
validator_run = run_validator(packet_directory)
|
|
138
|
+
assert validator_run.returncode != 0
|
|
139
|
+
assert "unknown field: unexpected" in validator_run.stderr
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_cyclic_dependencies_fail(tmp_path: Path) -> None:
|
|
143
|
+
packet_directory = tmp_path / "docs" / "plans" / "add-login"
|
|
144
|
+
write_packet(packet_directory)
|
|
145
|
+
packet_file = packet_directory / "packet.json"
|
|
146
|
+
packet_payload = json.loads(packet_file.read_text(encoding="utf-8"))
|
|
147
|
+
packet_payload["tasks"].append(
|
|
148
|
+
{
|
|
149
|
+
"id": "task-2",
|
|
150
|
+
"deliverable": "Second change",
|
|
151
|
+
"allowed_files": ["src/auth.py"],
|
|
152
|
+
"acceptance_command": "python -m pytest tests/test_auth.py",
|
|
153
|
+
"test_command": "python -m pytest tests/test_auth.py",
|
|
154
|
+
"verification_command": "python -m compileall src/auth.py",
|
|
155
|
+
"commit": 1,
|
|
156
|
+
"dependencies": ["task-1"],
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
packet_payload["tasks"][0]["dependencies"] = ["task-2"]
|
|
160
|
+
packet_file.write_text(json.dumps(packet_payload), encoding="utf-8")
|
|
161
|
+
validator_run = run_validator(packet_directory)
|
|
162
|
+
assert validator_run.returncode != 0
|
|
163
|
+
assert "dependency cycle detected" in validator_run.stderr
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_duplicate_dependencies_fail(tmp_path: Path) -> None:
|
|
167
|
+
packet_directory = tmp_path / "docs" / "plans" / "add-login"
|
|
168
|
+
write_packet(packet_directory)
|
|
169
|
+
packet_file = packet_directory / "packet.json"
|
|
170
|
+
packet_payload = json.loads(packet_file.read_text(encoding="utf-8"))
|
|
171
|
+
packet_payload["tasks"][0]["dependencies"] = ["task-1", "task-1"]
|
|
172
|
+
packet_file.write_text(json.dumps(packet_payload), encoding="utf-8")
|
|
173
|
+
validator_run = run_validator(packet_directory)
|
|
174
|
+
assert validator_run.returncode != 0
|
|
175
|
+
assert "dependencies: entries must be unique" in validator_run.stderr
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def test_incomplete_handoff_fails(tmp_path: Path) -> None:
|
|
179
|
+
packet_directory = tmp_path / "docs" / "plans" / "add-login"
|
|
180
|
+
write_packet(packet_directory)
|
|
181
|
+
(packet_directory / "handoff.md").write_text(
|
|
182
|
+
"## Approval\nApproval: approved\n## Task Order\nTask Order: task-1\n",
|
|
183
|
+
encoding="utf-8",
|
|
184
|
+
)
|
|
185
|
+
validator_run = run_validator(packet_directory)
|
|
186
|
+
assert validator_run.returncode != 0
|
|
187
|
+
assert "handoff.md: task task-1 missing deliverable" in validator_run.stderr
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def test_handoff_rejects_wrong_slug_approval_order_and_suffixed_commands(
|
|
191
|
+
tmp_path: Path,
|
|
192
|
+
) -> None:
|
|
193
|
+
packet_directory = tmp_path / "docs" / "plans" / "add-login"
|
|
194
|
+
write_packet(packet_directory)
|
|
195
|
+
handoff_file = packet_directory / "handoff.md"
|
|
196
|
+
handoff_text = handoff_file.read_text(encoding="utf-8")
|
|
197
|
+
handoff_file.write_text(
|
|
198
|
+
handoff_text.replace("Packet: add-login", "Packet: add-login-extra")
|
|
199
|
+
.replace("Approval: approved", "Approval: pending")
|
|
200
|
+
.replace("Task Order: task-1", "Task Order: task-2, task-1")
|
|
201
|
+
.replace(
|
|
202
|
+
"Acceptance command: python -m pytest tests/test_auth.py",
|
|
203
|
+
"Acceptance command: python -m pytest tests/test_auth.py --quiet",
|
|
204
|
+
),
|
|
205
|
+
encoding="utf-8",
|
|
206
|
+
)
|
|
207
|
+
validator_run = run_validator(packet_directory)
|
|
208
|
+
assert validator_run.returncode != 0
|
|
209
|
+
assert "missing packet slug" in validator_run.stderr
|
|
210
|
+
assert "missing approval state" in validator_run.stderr
|
|
211
|
+
assert "task order does not match" in validator_run.stderr
|
|
212
|
+
assert "missing acceptance command" in validator_run.stderr
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def test_task_markdown_rejects_suffixed_commands(tmp_path: Path) -> None:
|
|
216
|
+
packet_directory = tmp_path / "docs" / "plans" / "add-login"
|
|
217
|
+
write_packet(packet_directory)
|
|
218
|
+
tasks_file = packet_directory / "tasks.md"
|
|
219
|
+
task_text = tasks_file.read_text(encoding="utf-8")
|
|
220
|
+
tasks_file.write_text(
|
|
221
|
+
task_text.replace(
|
|
222
|
+
"python -m pytest tests/test_auth.py;",
|
|
223
|
+
"python -m pytest tests/test_auth.py --quiet;",
|
|
224
|
+
),
|
|
225
|
+
encoding="utf-8",
|
|
226
|
+
)
|
|
227
|
+
validator_run = run_validator(packet_directory)
|
|
228
|
+
assert validator_run.returncode != 0
|
|
229
|
+
assert "tasks.md: task task-1 missing acceptance_command" in validator_run.stderr
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def test_handoff_rejects_undeclared_task(tmp_path: Path) -> None:
|
|
233
|
+
packet_directory = tmp_path / "docs" / "plans" / "add-login"
|
|
234
|
+
write_packet(packet_directory)
|
|
235
|
+
handoff_file = packet_directory / "handoff.md"
|
|
236
|
+
handoff_file.write_text(
|
|
237
|
+
handoff_file.read_text(encoding="utf-8") + "\nTask task-2: Undeclared task\n",
|
|
238
|
+
encoding="utf-8",
|
|
239
|
+
)
|
|
240
|
+
validator_run = run_validator(packet_directory)
|
|
241
|
+
assert validator_run.returncode != 0
|
|
242
|
+
assert "handoff.md: declared tasks do not match packet.json" in validator_run.stderr
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def test_source_locator_and_path_boundaries_fail(tmp_path: Path) -> None:
|
|
246
|
+
packet_directory = tmp_path / "docs" / "plans" / "add-login"
|
|
247
|
+
write_packet(packet_directory)
|
|
248
|
+
packet_file = packet_directory / "packet.json"
|
|
249
|
+
packet_payload = json.loads(packet_file.read_text(encoding="utf-8"))
|
|
250
|
+
packet_payload["sources"][0]["locator"] = "authenticate_user"
|
|
251
|
+
packet_payload["sources"][0]["path"] = "src/auth..py"
|
|
252
|
+
packet_file.write_text(json.dumps(packet_payload), encoding="utf-8")
|
|
253
|
+
validator_run = run_validator(packet_directory)
|
|
254
|
+
assert validator_run.returncode != 0
|
|
255
|
+
assert "locator: must be line, lines, or section locator" in validator_run.stderr
|
|
256
|
+
assert "path: must be a repository-relative path" in validator_run.stderr
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Exercise the protocol validator through its command-line interface."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import importlib.util
|
|
5
|
+
import json
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
SCRIPT_PATH = Path(__file__).with_name("validate_protocol.py")
|
|
11
|
+
SCHEMA_PATH = SCRIPT_PATH.parent.parent / "reference" / "run-record.schema.json"
|
|
12
|
+
sys.path.insert(0, str(SCRIPT_PATH.parent))
|
|
13
|
+
VALIDATE_PROTOCOL_SPEC = importlib.util.spec_from_file_location("validate_protocol", SCRIPT_PATH)
|
|
14
|
+
assert VALIDATE_PROTOCOL_SPEC is not None
|
|
15
|
+
assert VALIDATE_PROTOCOL_SPEC.loader is not None
|
|
16
|
+
VALIDATE_PROTOCOL_MODULE = importlib.util.module_from_spec(VALIDATE_PROTOCOL_SPEC)
|
|
17
|
+
VALIDATE_PROTOCOL_SPEC.loader.exec_module(VALIDATE_PROTOCOL_MODULE)
|
|
18
|
+
ProtocolValidationError = VALIDATE_PROTOCOL_MODULE.ProtocolValidationError
|
|
19
|
+
validate_record = VALIDATE_PROTOCOL_MODULE.validate_record
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def valid_record() -> dict[str, object]:
|
|
23
|
+
verification = {"acceptance_output": "passed: pytest", "verifier_output": "passed: clean", "verified_commit_gate": "passed: exact surface", "surface_hash": "a" * 64}
|
|
24
|
+
return {
|
|
25
|
+
"task_identity": "task-3", "deliverable": "validator", "allowed_files": ["validator.py"],
|
|
26
|
+
"acceptance_check": "pytest", "baseline": "clean", "worker_route": "implementation worker; effort=low", "commit": "a1b2c3d",
|
|
27
|
+
"review_record": {"resolved_model": "Luna", "effort": "low", "command": "/e-code-review low", "findings": [], "repair_status": "clean", "surface_hash": "a" * 64, "findings_only": True, "has_repair_flag": False},
|
|
28
|
+
"repair_record": {"resolved_model": "Luna", "effort": "low", "confirmed_findings": [], "repair_status": "not-required", "surface_hash": "a" * 64},
|
|
29
|
+
"reverification_record": verification, "verification_record": verification.copy(),
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def run_validator(record_path: Path) -> subprocess.CompletedProcess[str]:
|
|
34
|
+
return subprocess.run([sys.executable, str(SCRIPT_PATH), str(record_path)], capture_output=True, text=True, check=False)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def mutate_record(record: dict[str, object], field_name: str, mutated_field: object) -> None:
|
|
38
|
+
if field_name in ("findings_only", "review_extra"):
|
|
39
|
+
review_record = record["review_record"]
|
|
40
|
+
assert isinstance(review_record, dict)
|
|
41
|
+
review_record[field_name] = mutated_field
|
|
42
|
+
elif mutated_field is None:
|
|
43
|
+
del record[field_name]
|
|
44
|
+
else:
|
|
45
|
+
record[field_name] = mutated_field
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_cli_accepts_valid_record(tmp_path: Path) -> None:
|
|
49
|
+
record_path = tmp_path / "record.json"
|
|
50
|
+
record_path.write_text(json.dumps(valid_record()), encoding="utf-8")
|
|
51
|
+
completed_run = run_validator(record_path)
|
|
52
|
+
assert completed_run.returncode == 0
|
|
53
|
+
assert completed_run.stdout.strip() == "protocol validation passed"
|
|
54
|
+
assert completed_run.stderr == ""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_cli_rejects_invalid_utf8_record(tmp_path: Path) -> None:
|
|
58
|
+
record_path = tmp_path / "invalid-utf8.json"
|
|
59
|
+
record_path.write_bytes(b"{\xff")
|
|
60
|
+
|
|
61
|
+
completed_run = run_validator(record_path)
|
|
62
|
+
|
|
63
|
+
assert completed_run.returncode == 2
|
|
64
|
+
assert completed_run.stdout == ""
|
|
65
|
+
assert completed_run.stderr == "protocol validation failed: invalid UTF-8\n"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_cli_rejects_invalid_mutations(tmp_path: Path) -> None:
|
|
69
|
+
mutations: tuple[tuple[str, str, object], ...] = (("bad-commit", "commit", "not-a-commit"), ("bad-review", "findings_only", False), ("missing-evidence", "verification_record", None), ("bad-type", "allowed_files", "file.py"), ("empty-allowed-files", "allowed_files", []), ("extra-review-field", "review_extra", "unexpected"))
|
|
70
|
+
for each_name, each_field, mutated_field in mutations:
|
|
71
|
+
record = valid_record()
|
|
72
|
+
mutate_record(record, each_field, mutated_field)
|
|
73
|
+
record_path = tmp_path / f"{each_name}.json"
|
|
74
|
+
record_path.write_text(json.dumps(record), encoding="utf-8")
|
|
75
|
+
completed_run = run_validator(record_path)
|
|
76
|
+
assert completed_run.returncode == 2
|
|
77
|
+
assert completed_run.stdout == ""
|
|
78
|
+
assert completed_run.stderr.startswith("protocol validation failed:")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_validate_record_rejects_absolute_allowed_file_paths() -> None:
|
|
82
|
+
for each_absolute_path in ("/absolute/file.py", r"C:\absolute\file.py", "C:/absolute/file.py"):
|
|
83
|
+
record = valid_record()
|
|
84
|
+
record["allowed_files"] = [each_absolute_path]
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
validate_record(record, SCHEMA_PATH)
|
|
88
|
+
except ProtocolValidationError as error:
|
|
89
|
+
assert str(error) == "allowed_files contains an absolute path"
|
|
90
|
+
else:
|
|
91
|
+
raise AssertionError("absolute allowed file paths must be rejected")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_validate_record_enforces_modified_schema(tmp_path: Path) -> None:
|
|
95
|
+
schema = json.loads(SCHEMA_PATH.read_text(encoding="utf-8"))
|
|
96
|
+
assert isinstance(schema, dict)
|
|
97
|
+
required_fields = schema["required"]
|
|
98
|
+
assert isinstance(required_fields, list)
|
|
99
|
+
required_fields.append("schema_added_field")
|
|
100
|
+
modified_schema_path = tmp_path / "modified-schema.json"
|
|
101
|
+
modified_schema_path.write_text(json.dumps(schema), encoding="utf-8")
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
validate_record(valid_record(), modified_schema_path)
|
|
105
|
+
except ProtocolValidationError as error:
|
|
106
|
+
assert "schema_added_field" in str(error)
|
|
107
|
+
else:
|
|
108
|
+
raise AssertionError("modified schema must reject a record missing its new required field")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_validate_record_accepts_worktree_with_matching_committed_surface_hash(tmp_path: Path) -> None:
|
|
112
|
+
worktree_path = tmp_path / "worktree"
|
|
113
|
+
worktree_path.mkdir()
|
|
114
|
+
subprocess.run(["git", "init"], cwd=worktree_path, check=True, capture_output=True)
|
|
115
|
+
subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=worktree_path, check=True)
|
|
116
|
+
subprocess.run(["git", "config", "user.name", "Protocol Test"], cwd=worktree_path, check=True)
|
|
117
|
+
tracked_file = worktree_path / "validator.py"
|
|
118
|
+
tracked_file.write_text("baseline\n", encoding="utf-8")
|
|
119
|
+
subprocess.run(["git", "add", "validator.py"], cwd=worktree_path, check=True)
|
|
120
|
+
subprocess.run(["git", "commit", "-m", "baseline"], cwd=worktree_path, check=True, capture_output=True)
|
|
121
|
+
tracked_file.write_text("validated\n", encoding="utf-8")
|
|
122
|
+
subprocess.run(["git", "add", "validator.py"], cwd=worktree_path, check=True)
|
|
123
|
+
subprocess.run(["git", "commit", "-m", "validated"], cwd=worktree_path, check=True, capture_output=True)
|
|
124
|
+
commit_hash = subprocess.run(["git", "rev-parse", "HEAD"], cwd=worktree_path, check=True, capture_output=True, text=True).stdout.strip()
|
|
125
|
+
committed_surface = subprocess.run(["git", "diff", f"{commit_hash}^", commit_hash, "--", "validator.py"], cwd=worktree_path, check=True, capture_output=True).stdout
|
|
126
|
+
surface_hash = hashlib.sha256(committed_surface).hexdigest()
|
|
127
|
+
record = valid_record()
|
|
128
|
+
record["worktree"] = str(worktree_path)
|
|
129
|
+
record["commit"] = commit_hash
|
|
130
|
+
for each_field_name in ("review_record", "repair_record", "reverification_record", "verification_record"):
|
|
131
|
+
nested_record = record[each_field_name]
|
|
132
|
+
assert isinstance(nested_record, dict)
|
|
133
|
+
nested_record["surface_hash"] = surface_hash
|
|
134
|
+
|
|
135
|
+
validate_record(record, SCHEMA_PATH)
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Regression tests for set-level task validation."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import importlib.util
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
SCRIPT_DIRECTORY = Path(__file__).parent
|
|
13
|
+
sys.path.insert(0, str(SCRIPT_DIRECTORY))
|
|
14
|
+
VALIDATE_PROTOCOL_SPEC = importlib.util.spec_from_file_location("validate_protocol", SCRIPT_DIRECTORY / "validate_protocol.py")
|
|
15
|
+
assert VALIDATE_PROTOCOL_SPEC is not None
|
|
16
|
+
assert VALIDATE_PROTOCOL_SPEC.loader is not None
|
|
17
|
+
VALIDATE_PROTOCOL_MODULE = importlib.util.module_from_spec(VALIDATE_PROTOCOL_SPEC)
|
|
18
|
+
sys.modules["validate_protocol"] = VALIDATE_PROTOCOL_MODULE
|
|
19
|
+
VALIDATE_PROTOCOL_SPEC.loader.exec_module(VALIDATE_PROTOCOL_MODULE)
|
|
20
|
+
SCHEMA_PATH = SCRIPT_DIRECTORY.parent / "reference" / "run-record.schema.json"
|
|
21
|
+
ProtocolValidationError = VALIDATE_PROTOCOL_MODULE.ProtocolValidationError
|
|
22
|
+
TEST_VALIDATE_PROTOCOL_SPEC = importlib.util.spec_from_file_location("test_validate_protocol", SCRIPT_DIRECTORY / "test_validate_protocol.py")
|
|
23
|
+
assert TEST_VALIDATE_PROTOCOL_SPEC is not None
|
|
24
|
+
assert TEST_VALIDATE_PROTOCOL_SPEC.loader is not None
|
|
25
|
+
TEST_VALIDATE_PROTOCOL_MODULE = importlib.util.module_from_spec(TEST_VALIDATE_PROTOCOL_SPEC)
|
|
26
|
+
TEST_VALIDATE_PROTOCOL_SPEC.loader.exec_module(TEST_VALIDATE_PROTOCOL_MODULE)
|
|
27
|
+
valid_record = TEST_VALIDATE_PROTOCOL_MODULE.valid_record
|
|
28
|
+
VALIDATE_RUN_SPEC = importlib.util.spec_from_file_location("validate_run", SCRIPT_DIRECTORY / "validate_run.py")
|
|
29
|
+
assert VALIDATE_RUN_SPEC is not None
|
|
30
|
+
assert VALIDATE_RUN_SPEC.loader is not None
|
|
31
|
+
VALIDATE_RUN_MODULE = importlib.util.module_from_spec(VALIDATE_RUN_SPEC)
|
|
32
|
+
VALIDATE_RUN_SPEC.loader.exec_module(VALIDATE_RUN_MODULE)
|
|
33
|
+
main = VALIDATE_RUN_MODULE.main
|
|
34
|
+
validate_run = VALIDATE_RUN_MODULE.validate_run
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_validate_run_accepts_one_record_per_commit() -> None:
|
|
38
|
+
task_record = valid_record()
|
|
39
|
+
validate_run([task_record], {"a1b2c3d"}, SCHEMA_PATH, {})
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_validate_run_accepts_matching_explicit_mapping() -> None:
|
|
43
|
+
task_record = valid_record()
|
|
44
|
+
validate_run(
|
|
45
|
+
[task_record],
|
|
46
|
+
{"a1b2c3d"},
|
|
47
|
+
SCHEMA_PATH,
|
|
48
|
+
{"task-3": "a1b2c3d"},
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@pytest.mark.parametrize(
|
|
53
|
+
"explicit_mapping",
|
|
54
|
+
[
|
|
55
|
+
{"wrong-task": "a1b2c3d"},
|
|
56
|
+
{"task-3": "deadbeef"},
|
|
57
|
+
],
|
|
58
|
+
)
|
|
59
|
+
def test_validate_run_rejects_mismatched_explicit_mapping(
|
|
60
|
+
explicit_mapping: dict[str, str],
|
|
61
|
+
) -> None:
|
|
62
|
+
with pytest.raises(ProtocolValidationError, match="explicit task mappings"):
|
|
63
|
+
validate_run([valid_record()], {"a1b2c3d"}, SCHEMA_PATH, explicit_mapping)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_validate_run_rejects_duplicate_commits_in_explicit_mapping() -> None:
|
|
67
|
+
first_record = valid_record()
|
|
68
|
+
second_record = valid_record()
|
|
69
|
+
second_record["task_identity"] = "task-4"
|
|
70
|
+
second_record["commit"] = "deadbeef"
|
|
71
|
+
with pytest.raises(ProtocolValidationError, match="unique commits"):
|
|
72
|
+
validate_run(
|
|
73
|
+
[first_record, second_record],
|
|
74
|
+
{"a1b2c3d"},
|
|
75
|
+
SCHEMA_PATH,
|
|
76
|
+
{"task-1": "a1b2c3d", "task-4": "a1b2c3d"},
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@pytest.mark.parametrize(
|
|
81
|
+
"mutation",
|
|
82
|
+
[
|
|
83
|
+
lambda record: record.update(task_identity="task-3"),
|
|
84
|
+
lambda record: record.update(commit="outside"),
|
|
85
|
+
],
|
|
86
|
+
)
|
|
87
|
+
def test_validate_run_rejects_invalid_commit_or_identity(
|
|
88
|
+
mutation: Callable[[dict[str, object]], None],
|
|
89
|
+
) -> None:
|
|
90
|
+
first_record = valid_record()
|
|
91
|
+
second_record = valid_record()
|
|
92
|
+
second_record["task_identity"] = "task-4"
|
|
93
|
+
mutation(second_record)
|
|
94
|
+
with pytest.raises(ProtocolValidationError):
|
|
95
|
+
validate_run(
|
|
96
|
+
[first_record, second_record], {"a1b2c3d", "deadbeef"}, SCHEMA_PATH, {}
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_validate_run_rejects_count_mismatch_without_mapping() -> None:
|
|
101
|
+
with pytest.raises(ProtocolValidationError):
|
|
102
|
+
validate_run([valid_record()], {"a1b2c3d", "deadbeef"}, SCHEMA_PATH, {})
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def create_committed_worktree(worktree_path: Path) -> str:
|
|
106
|
+
worktree_path.mkdir()
|
|
107
|
+
initialize_worktree(worktree_path)
|
|
108
|
+
tracked_file = worktree_path / "validator.py"
|
|
109
|
+
tracked_file.write_text("validated\n", encoding="utf-8")
|
|
110
|
+
commit_file(worktree_path)
|
|
111
|
+
return read_head(worktree_path)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def initialize_worktree(worktree_path: Path) -> None:
|
|
115
|
+
subprocess.run(["git", "init"], cwd=worktree_path, check=True, capture_output=True)
|
|
116
|
+
subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=worktree_path, check=True)
|
|
117
|
+
subprocess.run(["git", "config", "user.name", "Protocol Test"], cwd=worktree_path, check=True)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def commit_file(worktree_path: Path) -> None:
|
|
121
|
+
subprocess.run(["git", "add", "validator.py"], cwd=worktree_path, check=True)
|
|
122
|
+
subprocess.run(["git", "commit", "-m", "validated"], cwd=worktree_path, check=True, capture_output=True)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def read_head(worktree_path: Path) -> str:
|
|
126
|
+
return subprocess.run(["git", "rev-parse", "HEAD"], cwd=worktree_path, check=True, capture_output=True, text=True).stdout.strip()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_main_rejects_surface_mismatch_when_worktree_is_selected(
|
|
130
|
+
tmp_path: Path,
|
|
131
|
+
) -> None:
|
|
132
|
+
worktree_path = tmp_path / "worktree"
|
|
133
|
+
commit_hash = create_committed_worktree(worktree_path)
|
|
134
|
+
task_record = valid_record()
|
|
135
|
+
task_record["commit"] = commit_hash
|
|
136
|
+
task_record["allowed_files"] = ["validator.py"]
|
|
137
|
+
for each_field_name in (
|
|
138
|
+
"review_record",
|
|
139
|
+
"repair_record",
|
|
140
|
+
"reverification_record",
|
|
141
|
+
"verification_record",
|
|
142
|
+
):
|
|
143
|
+
nested_record = task_record[each_field_name]
|
|
144
|
+
assert isinstance(nested_record, dict)
|
|
145
|
+
nested_record["surface_hash"] = "0" * 64
|
|
146
|
+
record_path = tmp_path / "records.json"
|
|
147
|
+
record_path.write_text(json.dumps([task_record]), encoding="utf-8")
|
|
148
|
+
exit_code = main(
|
|
149
|
+
[
|
|
150
|
+
"validate_run.py",
|
|
151
|
+
str(record_path),
|
|
152
|
+
"--commits",
|
|
153
|
+
commit_hash,
|
|
154
|
+
"--worktree",
|
|
155
|
+
str(worktree_path),
|
|
156
|
+
]
|
|
157
|
+
)
|
|
158
|
+
assert exit_code == 2
|