claude-dev-env 7.0.0 → 7.1.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/.agents/skills/pull-request/scripts/pull_request.py +2 -0
- package/.agents/skills/pull-request/scripts/test_pull_request.py +34 -0
- package/.agents/skills/session-continuity/AGENTS.md +1 -0
- package/.agents/skills/session-continuity/CLAUDE.md +1 -0
- package/.agents/skills/session-continuity/README.md +189 -0
- package/.agents/skills/session-continuity/SKILL.md +114 -0
- package/.agents/skills/session-continuity/continuity.mjs +375 -0
- package/.agents/skills/session-continuity/continuity.test.mjs +303 -0
- package/.agents/skills/syncing-submodules/scripts/test_sync_parent_pointer.py +0 -7
- package/.agents/skills/windows-scheduled-task/SKILL.md +153 -0
- package/bin/cde.mjs +36 -8
- package/bin/cde.test.mjs +48 -0
- package/bin/ever-shipped-skills.mjs +2 -20
- package/bin/ever-shipped-skills.test.mjs +8 -0
- package/bin/expand_home_directory_tokens.mjs +4 -5
- package/bin/expand_home_directory_tokens.test.mjs +128 -0
- package/bin/git_hooks_installer.mjs +22 -3
- package/bin/git_hooks_installer.test.mjs +159 -5
- package/bin/install-session-continuity.mjs +78 -0
- package/bin/install-session-continuity.test.mjs +42 -0
- package/bin/install.codex-rules.test.mjs +5 -2
- package/bin/install.cursor-rules.test.mjs +5 -2
- package/bin/install.mjs +478 -63
- package/bin/install.policy-lint.test.mjs +56 -4
- package/bin/install.profiles.test.mjs +10 -0
- package/bin/install.prune.test.mjs +227 -17
- package/bin/install.test.mjs +500 -120
- package/bin/install.transaction.test.mjs +3 -3
- package/docs/CODE_RULES.md +1 -1
- package/hooks/blocking/code_rules_banned_identifiers.py +3 -0
- package/hooks/blocking/precommit_code_rules_gate.py +5 -8
- package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +7 -52
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +0 -69
- package/hooks/blocking/test_code_rules_banned_identifiers.py +64 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -244
- package/hooks/blocking/test_shared_stdin_adoption.py +1 -47
- package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +116 -0
- package/hooks/git-hooks/post_commit.py +33 -0
- package/hooks/git-hooks/pre_commit.py +18 -90
- package/hooks/git-hooks/pre_push.py +45 -541
- package/hooks/git-hooks/test_installed_policy_lint.py +55 -6
- package/hooks/git-hooks/test_native_hook_support.py +129 -0
- package/hooks/git-hooks/test_native_policy_commit.py +39 -6
- package/hooks/git-hooks/test_post_commit.py +43 -0
- package/hooks/git-hooks/test_pre_commit.py +36 -392
- package/hooks/git-hooks/test_pre_push.py +48 -1631
- package/hooks/git-hooks/test_verification_notice.py +60 -0
- package/hooks/git-hooks/test_verification_start.py +173 -0
- package/hooks/git-hooks/verification_notice.py +192 -0
- package/hooks/git-hooks/verification_notice_context.py +253 -0
- package/hooks/git-hooks/verification_notice_state.py +237 -0
- package/hooks/git-hooks/verification_start.py +100 -0
- package/hooks/hooks.json +0 -119
- package/hooks/hooks_constants/banned_identifiers_constants.py +4 -0
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -15
- package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -4
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -50
- package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -4
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +3 -66
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +42 -185
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +3 -25
- package/hooks/validation/test_post_tool_use_dispatcher.py +35 -298
- package/hooks/validators/run_all_validators.py +5 -1
- package/hooks/validators/security_checks.py +57 -2
- package/hooks/validators/test_hook_timing_harness.py +2 -5
- package/hooks/validators/test_run_all_validators_mutation_tool_parity.py +3 -11
- package/hooks/validators/test_run_all_validators_policy_lint.py +36 -0
- package/hooks/validators/test_security_checks.py +57 -0
- package/package.json +1 -1
- package/rules/confirm-implementation-forks.md +2 -0
- package/rules/git-workflow.md +12 -0
- package/rules/pstack-host-mapping.md +48 -0
- package/rules/vault-context.md +3 -5
- package/rules/verify-runtime-state.md +7 -0
- package/scripts/audit-hooks-cli.mjs +5 -0
- package/scripts/audit-hooks.mjs +72 -16
- package/scripts/automatic_advisory/__init__.py +1 -0
- package/scripts/automatic_advisory/checkout.py +51 -0
- package/scripts/automatic_advisory/cli.py +334 -0
- package/scripts/automatic_advisory/config/__init__.py +1 -0
- package/scripts/automatic_advisory/config/constants.py +93 -0
- package/scripts/automatic_advisory/config/timing.py +4 -0
- package/scripts/automatic_advisory/configuration.py +197 -0
- package/scripts/automatic_advisory/execution.py +350 -0
- package/scripts/automatic_advisory/git.py +69 -0
- package/scripts/automatic_advisory/model.py +107 -0
- package/scripts/automatic_advisory/process_host.py +32 -0
- package/scripts/automatic_advisory/publisher.py +84 -0
- package/scripts/automatic_advisory/runner.py +399 -0
- package/scripts/automatic_advisory/state.py +116 -0
- package/scripts/automatic_advisory/window_flags.py +41 -0
- package/scripts/automatic_advisory/windows_job.py +122 -0
- package/scripts/codex_compat_materializer.py +99 -13
- package/scripts/durable_post_lint.py +30 -2
- package/scripts/durable_post_lint_config/config/constants.py +10 -0
- package/scripts/durable_post_lint_config/config/test_constants.py +13 -0
- package/scripts/local_report_cli.py +184 -0
- package/scripts/local_report_core.py +247 -0
- package/scripts/local_report_publisher.py +12 -0
- package/scripts/local_report_validation.py +357 -0
- package/scripts/local_verification/__init__.py +23 -0
- package/scripts/local_verification/check_runner.py +364 -0
- package/scripts/local_verification/check_support.py +99 -0
- package/scripts/local_verification/cli.py +158 -0
- package/scripts/local_verification/command_runner.py +84 -0
- package/scripts/local_verification/config/__init__.py +1 -0
- package/scripts/local_verification/config/constants.py +89 -0
- package/scripts/local_verification/config/timing.py +1 -0
- package/scripts/local_verification/git_state.py +146 -0
- package/scripts/local_verification/manifest.py +224 -0
- package/scripts/local_verification/model.py +174 -0
- package/scripts/local_verification/runner.py +234 -0
- package/scripts/policy_lint/adapter_pairing.py +58 -3
- package/scripts/policy_lint/config/approved_test_pairs.py +323 -0
- package/scripts/pr_verification/__init__.py +1 -0
- package/scripts/pr_verification/branch_refs.py +114 -0
- package/scripts/pr_verification/config/__init__.py +1 -0
- package/scripts/pr_verification/config/constants.py +84 -0
- package/scripts/pr_verification/config/timing.py +5 -0
- package/scripts/pr_verification/github.py +137 -0
- package/scripts/pr_verification/github_api.py +376 -0
- package/scripts/pr_verification/github_labels.py +88 -0
- package/scripts/pr_verification/github_parsing.py +182 -0
- package/scripts/pr_verification/github_transport.py +91 -0
- package/scripts/pr_verification/lock.py +122 -0
- package/scripts/pr_verification/model.py +43 -0
- package/scripts/pr_verification/requirements.txt +1 -0
- package/scripts/test_durable_post_lint.py +97 -0
- package/scripts/tests/test-audit-hooks-cli.mjs +150 -0
- package/scripts/tests/test-audit-hooks.mjs +94 -2
- package/scripts/tests/test_automatic_advisory.py +376 -0
- package/scripts/tests/test_automatic_advisory_job_cleanup.py +71 -0
- package/scripts/tests/test_automatic_advisory_polling.py +216 -0
- package/scripts/tests/test_automatic_advisory_recovery.py +320 -0
- package/scripts/tests/test_automatic_advisory_repairs.py +400 -0
- package/scripts/tests/test_automatic_advisory_settings.py +48 -0
- package/scripts/tests/test_automatic_advisory_window_flags.py +163 -0
- package/scripts/tests/test_closed_pr_label.py +157 -0
- package/scripts/tests/test_codex_compat_materializer.py +189 -6
- package/scripts/tests/test_github_labels.py +101 -0
- package/scripts/tests/test_github_transport.py +125 -0
- package/scripts/tests/test_local_report_publisher.py +396 -0
- package/scripts/tests/test_local_report_publisher_cli.py +85 -0
- package/scripts/tests/test_local_verification.py +348 -0
- package/scripts/tests/test_local_verification_cli.py +98 -0
- package/scripts/tests/test_policy_lint_rules_pairing.py +163 -0
- package/scripts/tests/test_pr_verification_github.py +392 -0
- package/scripts/tests/test_pr_verification_lock.py +168 -0
- package/scripts/tests/test_verification_notice.py +347 -0
- package/hooks/blocking/ask_user_question_shape_blocker.py +0 -83
- package/hooks/blocking/bot_mention_comment_blocker.py +0 -76
- package/hooks/blocking/claude_md_orphan_file_blocker.py +0 -129
- package/hooks/blocking/cursor_cli_python_misfire_blocker.py +0 -108
- package/hooks/blocking/destructive_command_blocker.py +0 -2073
- package/hooks/blocking/fable_spawn_gate.py +0 -187
- package/hooks/blocking/luna_fast_mode_gate.py +0 -160
- package/hooks/blocking/nas_ssh_binary_enforcer.py +0 -163
- package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +0 -260
- package/hooks/blocking/package_inventory_stale_blocker.py +0 -107
- package/hooks/blocking/pii_prevention_blocker.py +0 -365
- package/hooks/blocking/piped_pytest_blocker.py +0 -1223
- package/hooks/blocking/question_to_user_enforcer.py +0 -130
- package/hooks/blocking/send_user_file_open_locally_blocker.py +0 -71
- package/hooks/blocking/sensitive_file_protector.py +0 -167
- package/hooks/blocking/session_edit_stage_gate.py +0 -684
- package/hooks/blocking/session_handoff_blocker.py +0 -182
- package/hooks/blocking/shell_substitution_blocker.py +0 -350
- package/hooks/blocking/test_ask_user_question_shape.py +0 -69
- package/hooks/blocking/test_bot_mention_comment_blocker.py +0 -131
- package/hooks/blocking/test_claude_md_orphan_file_blocker.py +0 -1277
- package/hooks/blocking/test_cursor_cli_python_misfire_blocker.py +0 -208
- package/hooks/blocking/test_destructive_command_blocker.py +0 -2024
- package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +0 -105
- package/hooks/blocking/test_destructive_command_blocker_no_verify.py +0 -122
- package/hooks/blocking/test_destructive_command_blocker_patterns.py +0 -133
- package/hooks/blocking/test_fable_spawn_gate.py +0 -381
- package/hooks/blocking/test_inventory_deadlock_resolution.py +0 -154
- package/hooks/blocking/test_luna_fast_mode_gate.py +0 -211
- package/hooks/blocking/test_nas_ssh_binary_enforcer.py +0 -225
- package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +0 -231
- package/hooks/blocking/test_package_inventory_stale_blocker.py +0 -382
- package/hooks/blocking/test_pii_prevention_blocker.py +0 -1222
- package/hooks/blocking/test_pii_prevention_windows_git_path.py +0 -41
- package/hooks/blocking/test_pii_write_surface_ephemeral.py +0 -221
- package/hooks/blocking/test_piped_pytest_blocker.py +0 -587
- package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +0 -233
- package/hooks/blocking/test_question_to_user_enforcer.py +0 -169
- package/hooks/blocking/test_send_user_file_open_locally_blocker.py +0 -118
- package/hooks/blocking/test_sensitive_file_protector.py +0 -365
- package/hooks/blocking/test_session_edit_stage_gate.py +0 -537
- package/hooks/blocking/test_session_edit_stage_gate_staging.py +0 -64
- package/hooks/blocking/test_session_edit_stage_gate_support.py +0 -204
- package/hooks/blocking/test_session_edit_stage_gate_tracker.py +0 -116
- package/hooks/blocking/test_session_handoff_blocker.py +0 -318
- package/hooks/blocking/test_shell_substitution_blocker.py +0 -298
- package/hooks/blocking/test_unscoped_search_blocker.py +0 -102
- package/hooks/blocking/test_write_existing_file_blocker.py +0 -122
- package/hooks/blocking/tests/test_pii_prevention_blocker.py +0 -259
- package/hooks/blocking/unscoped_search_blocker.py +0 -391
- package/hooks/blocking/write_existing_file_blocker.py +0 -124
- package/hooks/hooks_constants/bot_mention_comment_blocker_constants.py +0 -19
- package/hooks/hooks_constants/cursor_cli_python_misfire_blocker_constants.py +0 -79
- package/hooks/hooks_constants/fable_spawn_gate_constants.py +0 -61
- package/hooks/hooks_constants/luna_fast_mode_gate_constants.py +0 -45
- package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +0 -20
- package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +0 -57
- package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +0 -17
- package/hooks/hooks_constants/sensitive_file_protector_constants.py +0 -40
- package/hooks/hooks_constants/session_handoff_blocker_constants.py +0 -10
- package/hooks/hooks_constants/shell_substitution_blocker_constants.py +0 -80
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
SCRIPTS_DIRECTORY = Path(__file__).resolve().parents[1]
|
|
9
|
+
if str(SCRIPTS_DIRECTORY) not in sys.path:
|
|
10
|
+
sys.path.insert(0, str(SCRIPTS_DIRECTORY))
|
|
11
|
+
|
|
12
|
+
from local_report_publisher import PublicationOutcome, publish_local_report
|
|
13
|
+
from local_verification.config import BASE_PLACEHOLDER
|
|
14
|
+
from local_verification.manifest import load_manifest
|
|
15
|
+
from local_verification.runner import run_verification
|
|
16
|
+
from pr_verification.github import GitHubApi, HttpReply
|
|
17
|
+
from pr_verification.model import RepositorySettings, StatusState
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RecordingRequester:
|
|
21
|
+
def __init__(self, all_replies: list[HttpReply]) -> None:
|
|
22
|
+
self.all_replies = all_replies
|
|
23
|
+
self.all_requests: list[tuple[str, str, dict[str, str], bytes | None]] = []
|
|
24
|
+
|
|
25
|
+
def __call__(
|
|
26
|
+
self,
|
|
27
|
+
method: str,
|
|
28
|
+
url: str,
|
|
29
|
+
headers: dict[str, str],
|
|
30
|
+
body: bytes | None,
|
|
31
|
+
) -> HttpReply:
|
|
32
|
+
self.all_requests.append((method, url, headers, body))
|
|
33
|
+
return self.all_replies.pop(0)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _assert_request_methods(
|
|
37
|
+
requester: RecordingRequester, expected_methods: str
|
|
38
|
+
) -> None:
|
|
39
|
+
request_methods = " ".join(
|
|
40
|
+
each_request[0] for each_request in requester.all_requests
|
|
41
|
+
)
|
|
42
|
+
assert request_methods == expected_methods
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _run_git(repository_path: Path, *all_arguments: str) -> str:
|
|
46
|
+
completed_process = subprocess.run(
|
|
47
|
+
["git", "-C", str(repository_path), *all_arguments],
|
|
48
|
+
capture_output=True,
|
|
49
|
+
check=False,
|
|
50
|
+
text=True,
|
|
51
|
+
)
|
|
52
|
+
assert completed_process.returncode == 0, completed_process.stderr
|
|
53
|
+
return completed_process.stdout.strip()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _create_git_repository(tmp_path: Path) -> tuple[Path, str]:
|
|
57
|
+
repository_path = tmp_path / "repository"
|
|
58
|
+
repository_path.mkdir()
|
|
59
|
+
_run_git(repository_path, "init", "--quiet")
|
|
60
|
+
_run_git(repository_path, "config", "user.email", "tests@example.com")
|
|
61
|
+
_run_git(repository_path, "config", "user.name", "Local Verification Tests")
|
|
62
|
+
(repository_path / "tracked.txt").write_text("stable\n", encoding="utf-8")
|
|
63
|
+
_run_git(repository_path, "add", "tracked.txt")
|
|
64
|
+
_run_git(repository_path, "commit", "--quiet", "-m", "initial")
|
|
65
|
+
return repository_path, _run_git(repository_path, "rev-parse", "HEAD")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _repository() -> RepositorySettings:
|
|
69
|
+
return RepositorySettings("owner/repository", "https://github.test/repository.git")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _pull_payload(
|
|
73
|
+
head_sha: str, base_sha: str, merge_sha: str | None = "merge"
|
|
74
|
+
) -> bytes:
|
|
75
|
+
return json.dumps(
|
|
76
|
+
{
|
|
77
|
+
"number": 7,
|
|
78
|
+
"draft": False,
|
|
79
|
+
"merge_commit_sha": merge_sha,
|
|
80
|
+
"base": {"ref": "main", "sha": base_sha},
|
|
81
|
+
"head": {"sha": head_sha},
|
|
82
|
+
}
|
|
83
|
+
).encode("utf-8")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _candidate_replies(
|
|
87
|
+
head_sha: str, base_sha: str, merge_sha: str | None = "merge"
|
|
88
|
+
) -> list[HttpReply]:
|
|
89
|
+
return [
|
|
90
|
+
HttpReply(200, {}, _pull_payload(head_sha, base_sha, merge_sha)),
|
|
91
|
+
HttpReply(
|
|
92
|
+
200,
|
|
93
|
+
{},
|
|
94
|
+
json.dumps({"object": {"sha": base_sha}}).encode("utf-8"),
|
|
95
|
+
),
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _advisory_replies(
|
|
100
|
+
head_sha: str, base_sha: str, merge_sha: str | None = "merge"
|
|
101
|
+
) -> list[HttpReply]:
|
|
102
|
+
return [
|
|
103
|
+
*_candidate_replies(head_sha, base_sha, merge_sha),
|
|
104
|
+
HttpReply(200, {}, b'[{"name":"local-checks:passed"}]'),
|
|
105
|
+
HttpReply(204, {}, b""),
|
|
106
|
+
HttpReply(201, {}, b"{}"),
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _write_manifest(
|
|
111
|
+
manifest_path: Path,
|
|
112
|
+
check_arguments: list[str],
|
|
113
|
+
minimum_tests: int | None = None,
|
|
114
|
+
timeout_seconds: float = 10.0,
|
|
115
|
+
) -> None:
|
|
116
|
+
manifest_path.write_text(
|
|
117
|
+
json.dumps(
|
|
118
|
+
{
|
|
119
|
+
"version": 1,
|
|
120
|
+
"checks": [
|
|
121
|
+
{
|
|
122
|
+
"id": "selected",
|
|
123
|
+
"argv": check_arguments,
|
|
124
|
+
"cwd": ".",
|
|
125
|
+
"timeout_seconds": timeout_seconds,
|
|
126
|
+
**(
|
|
127
|
+
{"minimum_tests": minimum_tests}
|
|
128
|
+
if minimum_tests is not None
|
|
129
|
+
else {}
|
|
130
|
+
),
|
|
131
|
+
}
|
|
132
|
+
],
|
|
133
|
+
}
|
|
134
|
+
),
|
|
135
|
+
encoding="utf-8",
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _run_report(
|
|
140
|
+
tmp_path: Path,
|
|
141
|
+
check_arguments: list[str],
|
|
142
|
+
minimum_tests: int | None = None,
|
|
143
|
+
timeout_seconds: float = 10.0,
|
|
144
|
+
base_revision: str | None = None,
|
|
145
|
+
) -> tuple[Path, Path, str, str]:
|
|
146
|
+
repository_path, base_sha = _create_git_repository(tmp_path)
|
|
147
|
+
manifest_path = tmp_path / "manifest.json"
|
|
148
|
+
_write_manifest(manifest_path, check_arguments, minimum_tests, timeout_seconds)
|
|
149
|
+
manifest = load_manifest(manifest_path)
|
|
150
|
+
report_path = tmp_path / "report.json"
|
|
151
|
+
report = run_verification(
|
|
152
|
+
manifest, repository_path, base_revision or base_sha, report_path
|
|
153
|
+
)
|
|
154
|
+
assert report.head_revision is not None
|
|
155
|
+
return repository_path, manifest_path, base_sha, report.head_revision
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _base_check_arguments() -> list[str]:
|
|
159
|
+
return [
|
|
160
|
+
sys.executable,
|
|
161
|
+
"-c",
|
|
162
|
+
"import sys; assert len(sys.argv[1]) == 40",
|
|
163
|
+
BASE_PLACEHOLDER,
|
|
164
|
+
]
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _assert_recorded_base(report_path: Path, base_sha: str) -> None:
|
|
168
|
+
report_fields = json.loads(report_path.read_text(encoding="utf-8"))
|
|
169
|
+
assert report_fields["checks"][0]["argv"][-1] == base_sha
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _publish_report(
|
|
173
|
+
repository_path: Path,
|
|
174
|
+
manifest_path: Path,
|
|
175
|
+
report_path: Path,
|
|
176
|
+
requester: RecordingRequester,
|
|
177
|
+
) -> PublicationOutcome:
|
|
178
|
+
return publish_local_report(
|
|
179
|
+
GitHubApi("https://api.github.test", "token", requester),
|
|
180
|
+
_repository(),
|
|
181
|
+
7,
|
|
182
|
+
repository_path,
|
|
183
|
+
manifest_path,
|
|
184
|
+
report_path,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _missing_key_arguments(tmp_path: Path) -> list[str]:
|
|
189
|
+
return [
|
|
190
|
+
"--api-url",
|
|
191
|
+
"https://api.github.test",
|
|
192
|
+
"--app-id",
|
|
193
|
+
"42",
|
|
194
|
+
"--installation-id",
|
|
195
|
+
"84",
|
|
196
|
+
"--private-key-path",
|
|
197
|
+
str(tmp_path / "missing.pem"),
|
|
198
|
+
"--repository",
|
|
199
|
+
"owner/repository",
|
|
200
|
+
"--pull-number",
|
|
201
|
+
"7",
|
|
202
|
+
"--local-repo",
|
|
203
|
+
str(tmp_path),
|
|
204
|
+
"--manifest",
|
|
205
|
+
str(tmp_path / "manifest.json"),
|
|
206
|
+
"--report",
|
|
207
|
+
str(tmp_path / "report.json"),
|
|
208
|
+
]
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def test_publishes_success_for_matching_clean_report(tmp_path: Path) -> None:
|
|
212
|
+
repository_path, manifest_path, base_sha, head_sha = _run_report(
|
|
213
|
+
tmp_path, _base_check_arguments(), base_revision="HEAD"
|
|
214
|
+
)
|
|
215
|
+
_assert_recorded_base(tmp_path / "report.json", base_sha)
|
|
216
|
+
requester = RecordingRequester(
|
|
217
|
+
[
|
|
218
|
+
*_candidate_replies(head_sha, base_sha),
|
|
219
|
+
*_candidate_replies(head_sha, base_sha),
|
|
220
|
+
HttpReply(201, {}, b"{}"),
|
|
221
|
+
HttpReply(200, {}, b"[]"),
|
|
222
|
+
*_candidate_replies(head_sha, base_sha),
|
|
223
|
+
]
|
|
224
|
+
)
|
|
225
|
+
outcome = _publish_report(
|
|
226
|
+
repository_path, manifest_path, tmp_path / "report.json", requester
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
assert outcome.status == StatusState.SUCCESS
|
|
230
|
+
assert outcome.publishable is True
|
|
231
|
+
assert "Selected local checks passed (1 checks)" == outcome.description
|
|
232
|
+
_assert_request_methods(requester, "GET GET GET GET POST POST GET GET")
|
|
233
|
+
assert requester.all_requests[1][1].endswith("/git/ref/heads/main")
|
|
234
|
+
assert requester.all_requests[4][1].endswith(f"/statuses/{head_sha}")
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def test_failed_report_removes_pass_label_and_posts_failure(tmp_path: Path) -> None:
|
|
238
|
+
repository_path, manifest_path, base_sha, head_sha = _run_report(
|
|
239
|
+
tmp_path, [sys.executable, "-c", "raise SystemExit(4)"]
|
|
240
|
+
)
|
|
241
|
+
requester = RecordingRequester(_advisory_replies(head_sha, base_sha))
|
|
242
|
+
outcome = _publish_report(
|
|
243
|
+
repository_path, manifest_path, tmp_path / "report.json", requester
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
assert outcome.status == StatusState.FAILURE
|
|
247
|
+
assert outcome.publishable is False
|
|
248
|
+
_assert_request_methods(requester, "GET GET GET DELETE POST")
|
|
249
|
+
assert requester.all_requests[2][1].endswith(
|
|
250
|
+
"/issues/7/labels?per_page=100&page=1"
|
|
251
|
+
)
|
|
252
|
+
assert requester.all_requests[3][1].endswith(
|
|
253
|
+
"/issues/7/labels/local-checks%3Apassed"
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def test_failed_execution_with_collection_floor_posts_failure(tmp_path: Path) -> None:
|
|
258
|
+
failing_test_path = tmp_path / "test_failure.py"
|
|
259
|
+
failing_test_path.write_text(
|
|
260
|
+
"def test_selected():\n assert False\n",
|
|
261
|
+
encoding="utf-8",
|
|
262
|
+
)
|
|
263
|
+
repository_path, manifest_path, base_sha, head_sha = _run_report(
|
|
264
|
+
tmp_path,
|
|
265
|
+
[sys.executable, "-m", "pytest", str(failing_test_path)],
|
|
266
|
+
1,
|
|
267
|
+
)
|
|
268
|
+
requester = RecordingRequester(_advisory_replies(head_sha, base_sha))
|
|
269
|
+
outcome = _publish_report(
|
|
270
|
+
repository_path, manifest_path, tmp_path / "report.json", requester
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
assert outcome.status == StatusState.FAILURE
|
|
274
|
+
assert outcome.publishable is False
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def test_incomplete_report_posts_error_without_pass_label(tmp_path: Path) -> None:
|
|
278
|
+
repository_path, manifest_path, base_sha, head_sha = _run_report(
|
|
279
|
+
tmp_path, ["missing-command"]
|
|
280
|
+
)
|
|
281
|
+
requester = RecordingRequester(_advisory_replies(head_sha, base_sha))
|
|
282
|
+
outcome = _publish_report(
|
|
283
|
+
repository_path, manifest_path, tmp_path / "report.json", requester
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
assert outcome.status == StatusState.ERROR
|
|
287
|
+
assert outcome.publishable is False
|
|
288
|
+
assert [each_request[0] for each_request in requester.all_requests[-3:]] == [
|
|
289
|
+
"GET",
|
|
290
|
+
"DELETE",
|
|
291
|
+
"POST",
|
|
292
|
+
]
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def test_timeout_after_collection_posts_error(tmp_path: Path) -> None:
|
|
296
|
+
sleeping_test_path = tmp_path / "test_timeout.py"
|
|
297
|
+
sleeping_test_path.write_text(
|
|
298
|
+
"import time\ndef test_slow():\n time.sleep(1)\n",
|
|
299
|
+
encoding="utf-8",
|
|
300
|
+
)
|
|
301
|
+
repository_path, manifest_path, base_sha, head_sha = _run_report(
|
|
302
|
+
tmp_path,
|
|
303
|
+
[sys.executable, "-m", "pytest", str(sleeping_test_path)],
|
|
304
|
+
1,
|
|
305
|
+
0.1,
|
|
306
|
+
)
|
|
307
|
+
requester = RecordingRequester(_advisory_replies(head_sha, base_sha))
|
|
308
|
+
outcome = _publish_report(
|
|
309
|
+
repository_path, manifest_path, tmp_path / "report.json", requester
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
assert outcome.status == StatusState.ERROR
|
|
313
|
+
assert outcome.publishable is False
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def test_collection_failure_posts_error(tmp_path: Path) -> None:
|
|
317
|
+
repository_path, manifest_path, base_sha, head_sha = _run_report(
|
|
318
|
+
tmp_path,
|
|
319
|
+
[sys.executable, "-m", "pytest", str(tmp_path / "missing_tests.py")],
|
|
320
|
+
1,
|
|
321
|
+
)
|
|
322
|
+
requester = RecordingRequester(_advisory_replies(head_sha, base_sha))
|
|
323
|
+
outcome = _publish_report(
|
|
324
|
+
repository_path, manifest_path, tmp_path / "report.json", requester
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
assert outcome.status == StatusState.ERROR
|
|
328
|
+
assert outcome.publishable is False
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def test_advisory_reads_pull_request_without_merge_commit(tmp_path: Path) -> None:
|
|
332
|
+
repository_path, manifest_path, base_sha, head_sha = _run_report(
|
|
333
|
+
tmp_path, [sys.executable, "-c", "raise SystemExit(4)"]
|
|
334
|
+
)
|
|
335
|
+
requester = RecordingRequester(
|
|
336
|
+
_advisory_replies(head_sha, base_sha, merge_sha=None)
|
|
337
|
+
)
|
|
338
|
+
outcome = _publish_report(
|
|
339
|
+
repository_path, manifest_path, tmp_path / "report.json", requester
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
assert outcome.status == StatusState.FAILURE
|
|
343
|
+
assert outcome.publishable is False
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def test_wrong_manifest_removes_pass_label_and_posts_error(tmp_path: Path) -> None:
|
|
347
|
+
repository_path, _manifest_path, base_sha, head_sha = _run_report(
|
|
348
|
+
tmp_path, [sys.executable, "-c", "pass"]
|
|
349
|
+
)
|
|
350
|
+
wrong_manifest_path = tmp_path / "wrong-manifest.json"
|
|
351
|
+
_write_manifest(wrong_manifest_path, [sys.executable, "-c", "pass"])
|
|
352
|
+
wrong_manifest_text = wrong_manifest_path.read_text(encoding="utf-8")
|
|
353
|
+
wrong_manifest_path.write_text(
|
|
354
|
+
wrong_manifest_text.replace('"selected"', '"other"'),
|
|
355
|
+
encoding="utf-8",
|
|
356
|
+
)
|
|
357
|
+
requester = RecordingRequester(_advisory_replies(head_sha, base_sha))
|
|
358
|
+
outcome = _publish_report(
|
|
359
|
+
repository_path, wrong_manifest_path, tmp_path / "report.json", requester
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
assert outcome.status == StatusState.ERROR
|
|
363
|
+
assert outcome.publishable is False
|
|
364
|
+
_assert_request_methods(requester, "GET GET GET DELETE POST")
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def test_success_drift_after_label_removes_label_and_posts_error(
|
|
368
|
+
tmp_path: Path,
|
|
369
|
+
) -> None:
|
|
370
|
+
repository_path, manifest_path, base_sha, head_sha = _run_report(
|
|
371
|
+
tmp_path, [sys.executable, "-c", "pass"]
|
|
372
|
+
)
|
|
373
|
+
changed_head = "changed-head"
|
|
374
|
+
requester = RecordingRequester(
|
|
375
|
+
[
|
|
376
|
+
*_candidate_replies(head_sha, base_sha),
|
|
377
|
+
*_candidate_replies(head_sha, base_sha),
|
|
378
|
+
HttpReply(201, {}, b"{}"),
|
|
379
|
+
HttpReply(200, {}, b"[]"),
|
|
380
|
+
*_candidate_replies(changed_head, base_sha),
|
|
381
|
+
HttpReply(200, {}, b'[{"name":"local-checks:passed"}]'),
|
|
382
|
+
HttpReply(204, {}, b""),
|
|
383
|
+
HttpReply(201, {}, b"{}"),
|
|
384
|
+
]
|
|
385
|
+
)
|
|
386
|
+
outcome = _publish_report(
|
|
387
|
+
repository_path, manifest_path, tmp_path / "report.json", requester
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
assert outcome.status == StatusState.ERROR
|
|
391
|
+
assert outcome.publishable is False
|
|
392
|
+
assert [each_request[0] for each_request in requester.all_requests[-3:]] == [
|
|
393
|
+
"GET",
|
|
394
|
+
"DELETE",
|
|
395
|
+
"POST",
|
|
396
|
+
]
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
import json
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
SCRIPTS_DIRECTORY = Path(__file__).resolve().parents[1]
|
|
10
|
+
if str(SCRIPTS_DIRECTORY) not in sys.path:
|
|
11
|
+
sys.path.insert(0, str(SCRIPTS_DIRECTORY))
|
|
12
|
+
TESTS_DIRECTORY = Path(__file__).resolve().parent
|
|
13
|
+
if str(TESTS_DIRECTORY) not in sys.path:
|
|
14
|
+
sys.path.insert(0, str(TESTS_DIRECTORY))
|
|
15
|
+
|
|
16
|
+
from local_report_publisher import main
|
|
17
|
+
from pr_verification.model import StatusState
|
|
18
|
+
from test_local_report_publisher import (
|
|
19
|
+
RecordingRequester,
|
|
20
|
+
_advisory_replies,
|
|
21
|
+
_assert_request_methods,
|
|
22
|
+
_missing_key_arguments,
|
|
23
|
+
_publish_report,
|
|
24
|
+
_run_report,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_tampered_command_removes_pass_label_and_posts_error(tmp_path: Path) -> None:
|
|
29
|
+
repository_path, manifest_path, base_sha, head_sha = _run_report(
|
|
30
|
+
tmp_path, [sys.executable, "-c", "pass"]
|
|
31
|
+
)
|
|
32
|
+
report_path = tmp_path / "report.json"
|
|
33
|
+
report_fields = json.loads(report_path.read_text(encoding="utf-8"))
|
|
34
|
+
report_fields["checks"][0]["argv"] = [sys.executable, "-c", "raise SystemExit(9)"]
|
|
35
|
+
report_path.write_text(json.dumps(report_fields), encoding="utf-8")
|
|
36
|
+
requester = RecordingRequester(_advisory_replies(head_sha, base_sha))
|
|
37
|
+
|
|
38
|
+
outcome = _publish_report(repository_path, manifest_path, report_path, requester)
|
|
39
|
+
|
|
40
|
+
assert outcome.status == StatusState.ERROR
|
|
41
|
+
assert outcome.publishable is False
|
|
42
|
+
_assert_request_methods(requester, "GET GET GET DELETE POST")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_cli_reports_missing_key_without_network_access(tmp_path: Path) -> None:
|
|
46
|
+
standard_output = io.StringIO()
|
|
47
|
+
standard_error = io.StringIO()
|
|
48
|
+
|
|
49
|
+
exit_code = main(
|
|
50
|
+
_missing_key_arguments(tmp_path),
|
|
51
|
+
stdout=standard_output,
|
|
52
|
+
stderr=standard_error,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
assert exit_code == 3
|
|
56
|
+
assert standard_output.getvalue() == ""
|
|
57
|
+
assert "missing.pem" in standard_error.getvalue()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_cli_rejects_malformed_repository_without_network_access(
|
|
61
|
+
tmp_path: Path,
|
|
62
|
+
) -> None:
|
|
63
|
+
all_arguments = _missing_key_arguments(tmp_path)
|
|
64
|
+
repository_index = all_arguments.index("--repository") + 1
|
|
65
|
+
all_arguments[repository_index] = "owner"
|
|
66
|
+
standard_error = io.StringIO()
|
|
67
|
+
|
|
68
|
+
exit_code = main(all_arguments, stderr=standard_error)
|
|
69
|
+
|
|
70
|
+
assert exit_code == 3
|
|
71
|
+
assert "owner/name" in standard_error.getvalue()
|
|
72
|
+
assert "Traceback" not in standard_error.getvalue()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_publisher_script_displays_help_without_authentication() -> None:
|
|
76
|
+
publisher_path = Path(__file__).resolve().parents[1] / "local_report_publisher.py"
|
|
77
|
+
completed_process = subprocess.run(
|
|
78
|
+
[sys.executable, str(publisher_path), "--help"],
|
|
79
|
+
capture_output=True,
|
|
80
|
+
text=True,
|
|
81
|
+
check=False,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
assert completed_process.returncode == 0
|
|
85
|
+
assert "usage:" in completed_process.stdout
|