claude-dev-env 7.0.0 → 7.2.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 +213 -0
- package/.agents/skills/session-continuity/SKILL.md +125 -0
- package/.agents/skills/session-continuity/continuity.mjs +447 -0
- package/.agents/skills/session-continuity/continuity.test.mjs +373 -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 +88 -0
- package/bin/install-session-continuity.test.mjs +55 -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,157 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path, PurePosixPath
|
|
5
|
+
|
|
6
|
+
SCRIPTS_DIRECTORY = Path(__file__).resolve().parents[1]
|
|
7
|
+
if str(SCRIPTS_DIRECTORY) not in sys.path:
|
|
8
|
+
sys.path.insert(0, str(SCRIPTS_DIRECTORY))
|
|
9
|
+
|
|
10
|
+
from automatic_advisory.model import (
|
|
11
|
+
AdvisoryRegistration,
|
|
12
|
+
AdvisorySettings,
|
|
13
|
+
AdvisoryState,
|
|
14
|
+
)
|
|
15
|
+
from automatic_advisory.publisher import Publication
|
|
16
|
+
from automatic_advisory.runner import AdvisoryGitHub, AutomaticAdvisoryRunner
|
|
17
|
+
from automatic_advisory.state import read_state, write_state
|
|
18
|
+
from pr_verification.config.constants import LOCAL_CHECKS_PASSED_LABEL
|
|
19
|
+
from pr_verification.model import PullRequestCandidate, RepositorySettings
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ClosedPullRequestGitHub(AdvisoryGitHub):
|
|
23
|
+
def __init__(self, state_path: Path, failed_removal_count: int) -> None:
|
|
24
|
+
self.state_path = state_path
|
|
25
|
+
self.failed_removal_count = failed_removal_count
|
|
26
|
+
self.removed_labels: list[str] = []
|
|
27
|
+
self.observed_statuses: list[str | None] = []
|
|
28
|
+
|
|
29
|
+
def list_open_candidates(
|
|
30
|
+
self,
|
|
31
|
+
repository: RepositorySettings,
|
|
32
|
+
*,
|
|
33
|
+
should_require_merge_commit: bool = True,
|
|
34
|
+
) -> tuple[PullRequestCandidate, ...]:
|
|
35
|
+
return ()
|
|
36
|
+
|
|
37
|
+
def remove_label(
|
|
38
|
+
self, repository: RepositorySettings, pull_request_number: int, label: str
|
|
39
|
+
) -> None:
|
|
40
|
+
maybe_state = read_state(self.state_path)
|
|
41
|
+
self.observed_statuses.append(maybe_state.status if maybe_state else None)
|
|
42
|
+
self.removed_labels.append(label)
|
|
43
|
+
if self.failed_removal_count:
|
|
44
|
+
self.failed_removal_count -= 1
|
|
45
|
+
raise RuntimeError("label service unavailable")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _build_registration(tmp_path: Path) -> AdvisoryRegistration:
|
|
49
|
+
return AdvisoryRegistration(
|
|
50
|
+
RepositorySettings("JonEcho/python-automation", "unused"),
|
|
51
|
+
3082,
|
|
52
|
+
tmp_path / "checkout",
|
|
53
|
+
PurePosixPath("manifest.json"),
|
|
54
|
+
tmp_path / "report.json",
|
|
55
|
+
tmp_path / "state.json",
|
|
56
|
+
"main",
|
|
57
|
+
"origin",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _build_runner(
|
|
62
|
+
registration: AdvisoryRegistration,
|
|
63
|
+
github: ClosedPullRequestGitHub,
|
|
64
|
+
) -> AutomaticAdvisoryRunner:
|
|
65
|
+
settings = AdvisorySettings(
|
|
66
|
+
"https://api.github.com",
|
|
67
|
+
4841271,
|
|
68
|
+
159293880,
|
|
69
|
+
registration.state_path.with_name("private-key.pem"),
|
|
70
|
+
60.0,
|
|
71
|
+
30.0,
|
|
72
|
+
(registration,),
|
|
73
|
+
)
|
|
74
|
+
return AutomaticAdvisoryRunner(
|
|
75
|
+
settings,
|
|
76
|
+
github,
|
|
77
|
+
child_runner=_unexpected_child,
|
|
78
|
+
publisher=_unexpected_publisher,
|
|
79
|
+
clock=lambda: "2026-09-05T00:00:00+00:00",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _unexpected_child(
|
|
84
|
+
*all_arguments: object,
|
|
85
|
+
**all_keyword_arguments: object,
|
|
86
|
+
) -> object:
|
|
87
|
+
raise AssertionError("closed pull requests must not run checks")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _unexpected_publisher(*all_arguments: object) -> Publication:
|
|
91
|
+
raise AssertionError("closed pull requests must not publish reports")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _write_passed_state(registration: AdvisoryRegistration) -> None:
|
|
95
|
+
write_state(
|
|
96
|
+
registration.state_path,
|
|
97
|
+
AdvisoryState(
|
|
98
|
+
registration.repository.slug,
|
|
99
|
+
registration.pull_request_number,
|
|
100
|
+
"passed",
|
|
101
|
+
"local checks passed",
|
|
102
|
+
"old-head",
|
|
103
|
+
"base-head",
|
|
104
|
+
"2026-09-04T00:00:00+00:00",
|
|
105
|
+
"2026-09-09T00:00:00+00:00",
|
|
106
|
+
str(registration.report_path),
|
|
107
|
+
),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_closed_pull_request_removes_stale_label_before_saving_closed_state(
|
|
112
|
+
tmp_path: Path,
|
|
113
|
+
) -> None:
|
|
114
|
+
registration = _build_registration(tmp_path)
|
|
115
|
+
_write_passed_state(registration)
|
|
116
|
+
github = ClosedPullRequestGitHub(registration.state_path, 0)
|
|
117
|
+
runner = _build_runner(registration, github)
|
|
118
|
+
|
|
119
|
+
state = runner.run_once()[0]
|
|
120
|
+
|
|
121
|
+
assert state.status == "closed"
|
|
122
|
+
assert github.removed_labels == [LOCAL_CHECKS_PASSED_LABEL]
|
|
123
|
+
assert github.observed_statuses == ["passed"]
|
|
124
|
+
assert read_state(registration.state_path).status == "closed"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_closed_pull_request_retries_failed_label_removal(
|
|
128
|
+
tmp_path: Path,
|
|
129
|
+
) -> None:
|
|
130
|
+
registration = _build_registration(tmp_path)
|
|
131
|
+
_write_passed_state(registration)
|
|
132
|
+
github = ClosedPullRequestGitHub(registration.state_path, 1)
|
|
133
|
+
runner = _build_runner(registration, github)
|
|
134
|
+
|
|
135
|
+
error_state = runner.run_once()[0]
|
|
136
|
+
closed_state = runner.run_once()[0]
|
|
137
|
+
|
|
138
|
+
assert error_state.status == "error"
|
|
139
|
+
assert closed_state.status == "closed"
|
|
140
|
+
assert github.removed_labels == [
|
|
141
|
+
LOCAL_CHECKS_PASSED_LABEL,
|
|
142
|
+
LOCAL_CHECKS_PASSED_LABEL,
|
|
143
|
+
]
|
|
144
|
+
assert github.observed_statuses == ["passed", "error"]
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def test_closed_pull_request_does_not_run_checks(
|
|
148
|
+
tmp_path: Path,
|
|
149
|
+
) -> None:
|
|
150
|
+
registration = _build_registration(tmp_path)
|
|
151
|
+
github = ClosedPullRequestGitHub(registration.state_path, 0)
|
|
152
|
+
runner = _build_runner(registration, github)
|
|
153
|
+
|
|
154
|
+
state = runner.run_once()[0]
|
|
155
|
+
|
|
156
|
+
assert state.status == "closed"
|
|
157
|
+
assert github.removed_labels == [LOCAL_CHECKS_PASSED_LABEL]
|
|
@@ -462,13 +462,35 @@ def _apply_source_agent(source_root: Path, target_root: Path, description: str)
|
|
|
462
462
|
return target_root / "Luna.toml"
|
|
463
463
|
|
|
464
464
|
|
|
465
|
-
def
|
|
465
|
+
def _register_source_codex_enforcer(hooks_path: Path) -> None:
|
|
466
|
+
all_manifest = cast(
|
|
467
|
+
dict[str, object], json.loads(hooks_path.read_text(encoding="utf-8"))
|
|
468
|
+
)
|
|
469
|
+
all_events = cast(dict[str, object], all_manifest["hooks"])
|
|
470
|
+
all_pre_tool_use = cast(list[object], all_events["PreToolUse"])
|
|
471
|
+
all_pre_tool_use.append(
|
|
472
|
+
{
|
|
473
|
+
"matcher": materializer.codex_hook_matcher,
|
|
474
|
+
"hooks": [
|
|
475
|
+
{
|
|
476
|
+
"type": "command",
|
|
477
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/code_rules_enforcer.py",
|
|
478
|
+
"timeout": materializer.codex_hook_timeout_seconds,
|
|
479
|
+
}
|
|
480
|
+
],
|
|
481
|
+
}
|
|
482
|
+
)
|
|
483
|
+
hooks_path.write_text(json.dumps(all_manifest), encoding="utf-8")
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def _write_codex_hook_source(source_root: Path, has_enforcer: bool) -> None:
|
|
466
487
|
package_root = Path(__file__).parents[2]
|
|
467
488
|
source_hooks = source_root / "hooks"
|
|
468
489
|
source_hooks.mkdir(parents=True, exist_ok=True)
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
490
|
+
hooks_path = source_hooks / "hooks.json"
|
|
491
|
+
hooks_path.write_bytes((package_root / "hooks" / "hooks.json").read_bytes())
|
|
492
|
+
if has_enforcer:
|
|
493
|
+
_register_source_codex_enforcer(hooks_path)
|
|
472
494
|
for each_relative_path in materializer.codex_hook_dependency_manifest:
|
|
473
495
|
source_path = source_root / each_relative_path
|
|
474
496
|
source_path.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -479,7 +501,7 @@ def test_build_plan_rejects_reparse_hook_dependencies(
|
|
|
479
501
|
tmp_path: Path,
|
|
480
502
|
) -> None:
|
|
481
503
|
source = tmp_path / "source"
|
|
482
|
-
_write_codex_hook_source(source)
|
|
504
|
+
_write_codex_hook_source(source, has_enforcer=True)
|
|
483
505
|
dependency_path = source / materializer.codex_enforcer_script_relative_path
|
|
484
506
|
outside_path = tmp_path / "outside.py"
|
|
485
507
|
outside_path.write_text("secret", encoding="utf-8")
|
|
@@ -498,7 +520,7 @@ def test_publish_plan_preserves_modified_enforcer_hook_as_conflict(
|
|
|
498
520
|
) -> None:
|
|
499
521
|
source = tmp_path / "source"
|
|
500
522
|
target = tmp_path / "target"
|
|
501
|
-
_write_codex_hook_source(source)
|
|
523
|
+
_write_codex_hook_source(source, has_enforcer=True)
|
|
502
524
|
target.mkdir()
|
|
503
525
|
modified_command = f'python3 "{target / materializer.codex_enforcer_script_relative_path}" --custom'
|
|
504
526
|
existing_manifest = {
|
|
@@ -892,6 +914,7 @@ def _prepare_projection_fixture(
|
|
|
892
914
|
(Path(__file__).parents[2] / "hooks" / "hooks.json").read_text(encoding="utf-8"),
|
|
893
915
|
encoding="utf-8",
|
|
894
916
|
)
|
|
917
|
+
_register_source_codex_enforcer(source_hooks)
|
|
895
918
|
package_root = Path(__file__).parents[2]
|
|
896
919
|
for each_relative_path in materializer.codex_hook_dependency_manifest:
|
|
897
920
|
target_dependency = source / each_relative_path
|
|
@@ -1082,6 +1105,166 @@ def _assert_detached_enforcer_behavior(target: Path) -> None:
|
|
|
1082
1105
|
assert target_entrypoint.is_file()
|
|
1083
1106
|
|
|
1084
1107
|
|
|
1108
|
+
def _managed_enforcer_hook(target: Path) -> dict[str, object]:
|
|
1109
|
+
return {
|
|
1110
|
+
"type": "command",
|
|
1111
|
+
"command": (
|
|
1112
|
+
f'python3 "{target / materializer.codex_enforcer_script_relative_path}"'
|
|
1113
|
+
),
|
|
1114
|
+
"timeout": materializer.codex_hook_timeout_seconds,
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
|
|
1118
|
+
def _foreign_same_basename_hook() -> dict[str, object]:
|
|
1119
|
+
return {
|
|
1120
|
+
"type": "command",
|
|
1121
|
+
"command": "python D:/custom/hooks/blocking/code_rules_enforcer.py",
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
|
|
1125
|
+
def _codex_hook_group(
|
|
1126
|
+
*all_hook_records: dict[str, object],
|
|
1127
|
+
) -> dict[str, object]:
|
|
1128
|
+
return {
|
|
1129
|
+
"matcher": materializer.codex_hook_matcher,
|
|
1130
|
+
"hooks": list(all_hook_records),
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
|
|
1134
|
+
def _codex_hook_manifest(
|
|
1135
|
+
*all_hook_groups: dict[str, object],
|
|
1136
|
+
) -> dict[str, object]:
|
|
1137
|
+
return {"hooks": {"PreToolUse": list(all_hook_groups)}}
|
|
1138
|
+
|
|
1139
|
+
|
|
1140
|
+
def _write_current_source_without_enforcer(source: Path) -> None:
|
|
1141
|
+
_write_codex_hook_source(source, has_enforcer=False)
|
|
1142
|
+
source_manifest = json.loads(
|
|
1143
|
+
(source / "hooks" / "hooks.json").read_text(encoding="utf-8")
|
|
1144
|
+
)
|
|
1145
|
+
all_source_commands = _hook_commands(source_manifest, "PreToolUse")
|
|
1146
|
+
assert not any(
|
|
1147
|
+
"code_rules_enforcer.py" in each_command
|
|
1148
|
+
for each_command in all_source_commands
|
|
1149
|
+
)
|
|
1150
|
+
|
|
1151
|
+
|
|
1152
|
+
def _publish_codex_hook_fixture(
|
|
1153
|
+
source: Path, target: Path, target_manifest: dict[str, object]
|
|
1154
|
+
) -> tuple[dict[str, Any], MaterializationReport, list[PlannedFile]]:
|
|
1155
|
+
target.mkdir()
|
|
1156
|
+
target_hooks_path = target / materializer.codex_hook_manifest_target_path
|
|
1157
|
+
target_hooks_path.write_text(json.dumps(target_manifest), encoding="utf-8")
|
|
1158
|
+
config = MaterializerConfig(source, target, should_apply=True)
|
|
1159
|
+
planned, report = build_plan(config, all_agents=[])
|
|
1160
|
+
publication = publish_plan(config, planned, report)
|
|
1161
|
+
projected_manifest = json.loads(target_hooks_path.read_text(encoding="utf-8"))
|
|
1162
|
+
return projected_manifest, publication, planned
|
|
1163
|
+
|
|
1164
|
+
|
|
1165
|
+
def test_current_source_manifest_retires_the_managed_codex_enforcer(
|
|
1166
|
+
tmp_path: Path,
|
|
1167
|
+
) -> None:
|
|
1168
|
+
source = tmp_path / "source"
|
|
1169
|
+
target = tmp_path / "target"
|
|
1170
|
+
_write_current_source_without_enforcer(source)
|
|
1171
|
+
foreign_hook: dict[str, object] = {
|
|
1172
|
+
"type": "command",
|
|
1173
|
+
"command": "python foreign_advisory.py",
|
|
1174
|
+
}
|
|
1175
|
+
foreign_named_hook = _foreign_same_basename_hook()
|
|
1176
|
+
target_manifest = _codex_hook_manifest(
|
|
1177
|
+
_codex_hook_group(_managed_enforcer_hook(target)),
|
|
1178
|
+
_codex_hook_group(foreign_hook, foreign_named_hook),
|
|
1179
|
+
)
|
|
1180
|
+
|
|
1181
|
+
projected_manifest, _, planned = _publish_codex_hook_fixture(
|
|
1182
|
+
source, target, target_manifest
|
|
1183
|
+
)
|
|
1184
|
+
assert projected_manifest["hooks"]["PreToolUse"] == [
|
|
1185
|
+
{
|
|
1186
|
+
"matcher": "apply_patch",
|
|
1187
|
+
"hooks": [foreign_hook, foreign_named_hook],
|
|
1188
|
+
}
|
|
1189
|
+
]
|
|
1190
|
+
all_planned_paths = {each_file.target_relative_path for each_file in planned}
|
|
1191
|
+
assert all_planned_paths.isdisjoint(materializer.codex_hook_dependency_manifest)
|
|
1192
|
+
|
|
1193
|
+
|
|
1194
|
+
def test_explicit_source_enforcer_preserves_foreign_same_basename_hook(
|
|
1195
|
+
tmp_path: Path,
|
|
1196
|
+
) -> None:
|
|
1197
|
+
source = tmp_path / "source"
|
|
1198
|
+
target = tmp_path / "target"
|
|
1199
|
+
_write_codex_hook_source(source, has_enforcer=True)
|
|
1200
|
+
managed_hook = _managed_enforcer_hook(target)
|
|
1201
|
+
foreign_hook = _foreign_same_basename_hook()
|
|
1202
|
+
target_manifest = _codex_hook_manifest(
|
|
1203
|
+
_codex_hook_group(managed_hook, foreign_hook)
|
|
1204
|
+
)
|
|
1205
|
+
projected_manifest, publication, _ = _publish_codex_hook_fixture(
|
|
1206
|
+
source, target, target_manifest
|
|
1207
|
+
)
|
|
1208
|
+
projected_hooks = projected_manifest["hooks"]["PreToolUse"][0]["hooks"]
|
|
1209
|
+
assert publication.conflicted == 0
|
|
1210
|
+
assert projected_hooks == [foreign_hook, managed_hook]
|
|
1211
|
+
|
|
1212
|
+
|
|
1213
|
+
@pytest.mark.parametrize("has_source_enforcer", [False, True])
|
|
1214
|
+
def test_posix_case_distinct_enforcer_path_remains_foreign(
|
|
1215
|
+
tmp_path: Path,
|
|
1216
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
1217
|
+
has_source_enforcer: bool,
|
|
1218
|
+
) -> None:
|
|
1219
|
+
source = tmp_path / "source"
|
|
1220
|
+
target = tmp_path / "target"
|
|
1221
|
+
if has_source_enforcer:
|
|
1222
|
+
_write_codex_hook_source(source, has_enforcer=True)
|
|
1223
|
+
if not has_source_enforcer:
|
|
1224
|
+
_write_current_source_without_enforcer(source)
|
|
1225
|
+
managed_hook = _managed_enforcer_hook(target)
|
|
1226
|
+
managed_command = cast(str, managed_hook["command"])
|
|
1227
|
+
foreign_command = managed_command.replace(str(target), str(target).upper())
|
|
1228
|
+
assert foreign_command != managed_command
|
|
1229
|
+
assert foreign_command.casefold() == managed_command.casefold()
|
|
1230
|
+
foreign_hook = {**managed_hook, "command": foreign_command}
|
|
1231
|
+
target_manifest = _codex_hook_manifest(
|
|
1232
|
+
_codex_hook_group(managed_hook, foreign_hook)
|
|
1233
|
+
)
|
|
1234
|
+
monkeypatch.setattr(materializer.os, "name", "posix")
|
|
1235
|
+
|
|
1236
|
+
projected_manifest, _, _ = _publish_codex_hook_fixture(
|
|
1237
|
+
source, target, target_manifest
|
|
1238
|
+
)
|
|
1239
|
+
|
|
1240
|
+
all_expected_commands = [foreign_command]
|
|
1241
|
+
if has_source_enforcer:
|
|
1242
|
+
all_expected_commands.append(managed_command)
|
|
1243
|
+
assert _hook_commands(projected_manifest, "PreToolUse") == all_expected_commands
|
|
1244
|
+
|
|
1245
|
+
|
|
1246
|
+
def test_windows_case_variant_enforcer_path_is_managed(
|
|
1247
|
+
tmp_path: Path,
|
|
1248
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
1249
|
+
) -> None:
|
|
1250
|
+
source = tmp_path / "source"
|
|
1251
|
+
target = tmp_path / "target"
|
|
1252
|
+
_write_current_source_without_enforcer(source)
|
|
1253
|
+
managed_hook = _managed_enforcer_hook(target)
|
|
1254
|
+
managed_command = cast(str, managed_hook["command"])
|
|
1255
|
+
case_variant_command = managed_command.replace(str(target), str(target).upper())
|
|
1256
|
+
target_manifest = _codex_hook_manifest(
|
|
1257
|
+
_codex_hook_group({**managed_hook, "command": case_variant_command})
|
|
1258
|
+
)
|
|
1259
|
+
monkeypatch.setattr(materializer.os, "name", "nt")
|
|
1260
|
+
|
|
1261
|
+
projected_manifest, _, _ = _publish_codex_hook_fixture(
|
|
1262
|
+
source, target, target_manifest
|
|
1263
|
+
)
|
|
1264
|
+
|
|
1265
|
+
assert projected_manifest["hooks"] == {}
|
|
1266
|
+
|
|
1267
|
+
|
|
1085
1268
|
def test_exact_cli_publishes_instruction_and_additive_hook_projection(
|
|
1086
1269
|
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
1087
1270
|
) -> None:
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
SCRIPTS_DIRECTORY = Path(__file__).resolve().parents[1]
|
|
7
|
+
if str(SCRIPTS_DIRECTORY) not in sys.path:
|
|
8
|
+
sys.path.insert(0, str(SCRIPTS_DIRECTORY))
|
|
9
|
+
|
|
10
|
+
from pr_verification.github_labels import remove_label_if_present
|
|
11
|
+
from pr_verification.model import RepositorySettings
|
|
12
|
+
|
|
13
|
+
PASS_LABEL = "local-checks:passed"
|
|
14
|
+
FULL_PAGE_SIZE = 100
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def repository_settings() -> RepositorySettings:
|
|
18
|
+
return RepositorySettings(
|
|
19
|
+
slug="owner/repository",
|
|
20
|
+
clone_url="https://github.test/owner/repository.git",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def label_page(all_label_names: list[str]) -> list[dict[str, str]]:
|
|
25
|
+
return [{"name": each_label_name} for each_label_name in all_label_names]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class RecordingLabelReader:
|
|
29
|
+
def __init__(self, all_pages: list[object]) -> None:
|
|
30
|
+
self.all_pages = all_pages
|
|
31
|
+
self.all_endpoints: list[str] = []
|
|
32
|
+
|
|
33
|
+
def __call__(self, endpoint: str) -> object:
|
|
34
|
+
self.all_endpoints.append(endpoint)
|
|
35
|
+
return self.all_pages.pop(0)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class RecordingLabelDeleter:
|
|
39
|
+
def __init__(self) -> None:
|
|
40
|
+
self.all_endpoints: list[str] = []
|
|
41
|
+
|
|
42
|
+
def __call__(self, endpoint: str) -> None:
|
|
43
|
+
self.all_endpoints.append(endpoint)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_removes_a_label_a_later_page_holds() -> None:
|
|
47
|
+
all_first_page_names = [
|
|
48
|
+
f"topic-{each_index}" for each_index in range(FULL_PAGE_SIZE)
|
|
49
|
+
]
|
|
50
|
+
read_labels_page = RecordingLabelReader(
|
|
51
|
+
[label_page(all_first_page_names), label_page([PASS_LABEL])]
|
|
52
|
+
)
|
|
53
|
+
delete_label = RecordingLabelDeleter()
|
|
54
|
+
|
|
55
|
+
remove_label_if_present(
|
|
56
|
+
read_labels_page, delete_label, repository_settings(), 7, PASS_LABEL
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
assert read_labels_page.all_endpoints == [
|
|
60
|
+
"/repos/owner/repository/issues/7/labels?per_page=100&page=1",
|
|
61
|
+
"/repos/owner/repository/issues/7/labels?per_page=100&page=2",
|
|
62
|
+
]
|
|
63
|
+
assert delete_label.all_endpoints == [
|
|
64
|
+
"/repos/owner/repository/issues/7/labels/local-checks%3Apassed"
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_reads_one_page_when_a_short_page_lacks_the_label() -> None:
|
|
69
|
+
read_labels_page = RecordingLabelReader([label_page(["topic"])])
|
|
70
|
+
delete_label = RecordingLabelDeleter()
|
|
71
|
+
|
|
72
|
+
remove_label_if_present(
|
|
73
|
+
read_labels_page, delete_label, repository_settings(), 7, PASS_LABEL
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
assert len(read_labels_page.all_endpoints) == 1
|
|
77
|
+
assert delete_label.all_endpoints == []
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_reads_one_page_when_the_first_page_holds_the_label() -> None:
|
|
81
|
+
read_labels_page = RecordingLabelReader([label_page([PASS_LABEL])])
|
|
82
|
+
delete_label = RecordingLabelDeleter()
|
|
83
|
+
|
|
84
|
+
remove_label_if_present(
|
|
85
|
+
read_labels_page, delete_label, repository_settings(), 7, PASS_LABEL
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
assert len(read_labels_page.all_endpoints) == 1
|
|
89
|
+
assert len(delete_label.all_endpoints) == 1
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_reads_no_further_page_when_a_page_is_not_a_list() -> None:
|
|
93
|
+
read_labels_page = RecordingLabelReader([{"message": "Not Found"}])
|
|
94
|
+
delete_label = RecordingLabelDeleter()
|
|
95
|
+
|
|
96
|
+
remove_label_if_present(
|
|
97
|
+
read_labels_page, delete_label, repository_settings(), 7, PASS_LABEL
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
assert len(read_labels_page.all_endpoints) == 1
|
|
101
|
+
assert delete_label.all_endpoints == []
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
import sys
|
|
5
|
+
import urllib.error
|
|
6
|
+
import urllib.request
|
|
7
|
+
from collections.abc import Iterator
|
|
8
|
+
from contextlib import AbstractContextManager, contextmanager
|
|
9
|
+
from email.message import Message
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from unittest.mock import Mock
|
|
12
|
+
|
|
13
|
+
import jwt
|
|
14
|
+
import pytest
|
|
15
|
+
|
|
16
|
+
SCRIPTS_DIRECTORY = Path(__file__).resolve().parents[1]
|
|
17
|
+
if str(SCRIPTS_DIRECTORY) not in sys.path:
|
|
18
|
+
sys.path.insert(0, str(SCRIPTS_DIRECTORY))
|
|
19
|
+
|
|
20
|
+
from pr_verification.config.timing import HTTP_TIMEOUT_SECONDS
|
|
21
|
+
from pr_verification.github_parsing import GitHubError
|
|
22
|
+
from pr_verification.github_transport import HttpReply, encode_app_jwt, request_http
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@contextmanager
|
|
26
|
+
def reply_context(http_reply_mock: Mock) -> Iterator[Mock]:
|
|
27
|
+
yield http_reply_mock
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def make_http_reply_mock(
|
|
31
|
+
status_code: int, headers: dict[str, str], body: bytes
|
|
32
|
+
) -> Mock:
|
|
33
|
+
http_reply_mock = Mock()
|
|
34
|
+
http_reply_mock.status = status_code
|
|
35
|
+
http_reply_mock.headers.items.return_value = list(headers.items())
|
|
36
|
+
http_reply_mock.read.return_value = body
|
|
37
|
+
return http_reply_mock
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_request_http_returns_success_reply_and_request_details(
|
|
41
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
42
|
+
) -> None:
|
|
43
|
+
http_reply_mock = make_http_reply_mock(
|
|
44
|
+
200, {"Content-Type": "application/json"}, b'{"ok":true}'
|
|
45
|
+
)
|
|
46
|
+
all_requests: list[tuple[urllib.request.Request, float]] = []
|
|
47
|
+
|
|
48
|
+
def open_url(
|
|
49
|
+
request: urllib.request.Request, timeout: float
|
|
50
|
+
) -> AbstractContextManager[Mock]:
|
|
51
|
+
all_requests.append((request, timeout))
|
|
52
|
+
return reply_context(http_reply_mock)
|
|
53
|
+
|
|
54
|
+
monkeypatch.setattr(urllib.request, "urlopen", open_url)
|
|
55
|
+
|
|
56
|
+
reply = request_http(
|
|
57
|
+
"GET",
|
|
58
|
+
"https://github.test/resource",
|
|
59
|
+
{"Accept": "application/json"},
|
|
60
|
+
None,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
assert reply == HttpReply(200, {"Content-Type": "application/json"}, b'{"ok":true}')
|
|
64
|
+
assert all_requests[0][0].full_url == "https://github.test/resource"
|
|
65
|
+
assert all_requests[0][0].get_method() == "GET"
|
|
66
|
+
assert all_requests[0][1] == HTTP_TIMEOUT_SECONDS
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_request_http_returns_http_error_reply(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
70
|
+
http_headers = Message()
|
|
71
|
+
http_headers["X-Request-Id"] = "request-id"
|
|
72
|
+
http_error = urllib.error.HTTPError(
|
|
73
|
+
"https://github.test/resource",
|
|
74
|
+
404,
|
|
75
|
+
"missing",
|
|
76
|
+
http_headers,
|
|
77
|
+
io.BytesIO(b"not found"),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def open_url(request: urllib.request.Request, timeout: float) -> Mock:
|
|
81
|
+
raise http_error
|
|
82
|
+
|
|
83
|
+
monkeypatch.setattr(urllib.request, "urlopen", open_url)
|
|
84
|
+
|
|
85
|
+
reply = request_http("GET", "https://github.test/resource", {}, None)
|
|
86
|
+
|
|
87
|
+
assert reply == HttpReply(404, {"X-Request-Id": "request-id"}, b"not found")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_request_http_propagates_url_error_as_github_error(
|
|
91
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
92
|
+
) -> None:
|
|
93
|
+
def open_url(request: urllib.request.Request, timeout: float) -> Mock:
|
|
94
|
+
raise urllib.error.URLError("offline")
|
|
95
|
+
|
|
96
|
+
monkeypatch.setattr(urllib.request, "urlopen", open_url)
|
|
97
|
+
|
|
98
|
+
with pytest.raises(GitHubError, match="offline"):
|
|
99
|
+
request_http("GET", "https://github.test/resource", {}, None)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_encode_app_jwt_preserves_arguments_and_normalizes_bytes_and_strings(
|
|
103
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
104
|
+
) -> None:
|
|
105
|
+
all_calls: list[tuple[dict[str, int | str], str, str]] = []
|
|
106
|
+
encoded_tokens = iter((b"encoded-bytes", "encoded-string"))
|
|
107
|
+
|
|
108
|
+
def encode_jwt(
|
|
109
|
+
claims: dict[str, int | str], private_key: str, algorithm: str
|
|
110
|
+
) -> bytes | str:
|
|
111
|
+
all_calls.append((claims, private_key, algorithm))
|
|
112
|
+
return next(encoded_tokens)
|
|
113
|
+
|
|
114
|
+
monkeypatch.setattr(jwt, "encode", encode_jwt)
|
|
115
|
+
claims: dict[str, int | str] = {"iss": "42", "iat": 940, "exp": 1540}
|
|
116
|
+
|
|
117
|
+
bytes_token = encode_app_jwt(claims, "private-key", "RS256")
|
|
118
|
+
string_token = encode_app_jwt(claims, "private-key", "RS256")
|
|
119
|
+
|
|
120
|
+
assert bytes_token == "encoded-bytes"
|
|
121
|
+
assert string_token == "encoded-string"
|
|
122
|
+
assert all_calls == [
|
|
123
|
+
(claims, "private-key", "RS256"),
|
|
124
|
+
(claims, "private-key", "RS256"),
|
|
125
|
+
]
|