claude-dev-env 8.48.3 → 8.48.5
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/package.json
CHANGED
|
@@ -607,6 +607,7 @@ def read_pull_request(slug: str, number: int, token: str) -> Mapping[str, object
|
|
|
607
607
|
def read_blocked_evidence(
|
|
608
608
|
slug: str,
|
|
609
609
|
all_pull_request_fields: Mapping[str, object],
|
|
610
|
+
all_rules: list[object] | MergeCheckError,
|
|
610
611
|
token: str,
|
|
611
612
|
) -> BlockedEvidence:
|
|
612
613
|
"""Read what stands behind a ``blocked`` merge state.
|
|
@@ -615,6 +616,8 @@ def read_blocked_evidence(
|
|
|
615
616
|
slug: The repository as ``owner/name``.
|
|
616
617
|
all_pull_request_fields: The pull request, carrying its base branch
|
|
617
618
|
and its head commit.
|
|
619
|
+
all_rules: The base branch rules, or the failure that kept them
|
|
620
|
+
unread.
|
|
618
621
|
token: The GitHub token the requests authenticate with.
|
|
619
622
|
|
|
620
623
|
Returns:
|
|
@@ -628,7 +631,8 @@ def read_blocked_evidence(
|
|
|
628
631
|
str(_nested_field(all_pull_request_fields, BASE_KEY, REF_KEY))
|
|
629
632
|
)
|
|
630
633
|
head_sha = str(_nested_field(all_pull_request_fields, HEAD_KEY, SHA_KEY))
|
|
631
|
-
|
|
634
|
+
if isinstance(all_rules, MergeCheckError):
|
|
635
|
+
raise all_rules
|
|
632
636
|
return BlockedEvidence(
|
|
633
637
|
all_unmet_checks=_read_unmet_checks(slug, head_sha, all_rules, token),
|
|
634
638
|
behind_by=_read_behind_by(slug, base_ref, head_sha, token),
|
|
@@ -636,13 +640,30 @@ def read_blocked_evidence(
|
|
|
636
640
|
)
|
|
637
641
|
|
|
638
642
|
|
|
639
|
-
def
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
643
|
+
def read_branch_rules(
|
|
644
|
+
slug: str, base_ref: str, token: str
|
|
645
|
+
) -> list[object] | MergeCheckError:
|
|
646
|
+
"""Read the rules GitHub reports as active on the base branch.
|
|
647
|
+
|
|
648
|
+
Args:
|
|
649
|
+
slug: The repository as ``owner/name``.
|
|
650
|
+
base_ref: The branch the pull request merges into.
|
|
651
|
+
token: The GitHub token the request authenticates with.
|
|
652
|
+
|
|
653
|
+
Returns:
|
|
654
|
+
The rules, or the failure that kept them unread.
|
|
655
|
+
"""
|
|
656
|
+
try:
|
|
657
|
+
return _request_list(
|
|
658
|
+
BRANCH_RULES_ENDPOINT_TEMPLATE.format(
|
|
659
|
+
api_root=GITHUB_API_ROOT,
|
|
660
|
+
slug=slug,
|
|
661
|
+
branch=urllib.parse.quote(base_ref),
|
|
662
|
+
),
|
|
663
|
+
token,
|
|
664
|
+
)
|
|
665
|
+
except MergeCheckError as failure:
|
|
666
|
+
return failure
|
|
646
667
|
|
|
647
668
|
|
|
648
669
|
def read_unstable_checks(
|
|
@@ -879,7 +900,11 @@ def _field_at(document: object, all_fields: object, key: str) -> object:
|
|
|
879
900
|
|
|
880
901
|
|
|
881
902
|
def read_merge_queue_ejection(
|
|
882
|
-
slug: str,
|
|
903
|
+
slug: str,
|
|
904
|
+
number: int,
|
|
905
|
+
base_ref: str,
|
|
906
|
+
all_rules: list[object] | MergeCheckError,
|
|
907
|
+
token: str,
|
|
883
908
|
) -> bool:
|
|
884
909
|
"""Read whether the merge queue ejected the pull request's current head.
|
|
885
910
|
|
|
@@ -891,6 +916,8 @@ def read_merge_queue_ejection(
|
|
|
891
916
|
slug: The repository as ``owner/name``.
|
|
892
917
|
number: The pull request number.
|
|
893
918
|
base_ref: The branch the pull request merges into.
|
|
919
|
+
all_rules: The base branch rules, or the failure that kept them
|
|
920
|
+
unread.
|
|
894
921
|
token: The GitHub token the request authenticates with.
|
|
895
922
|
|
|
896
923
|
Returns:
|
|
@@ -901,7 +928,9 @@ def read_merge_queue_ejection(
|
|
|
901
928
|
MergeCheckError: A read failed, or answered with another shape. A
|
|
902
929
|
refused GraphQL query names the merge queue that needed it.
|
|
903
930
|
"""
|
|
904
|
-
if not
|
|
931
|
+
if not isinstance(all_rules, MergeCheckError) and not has_merge_queue_rule(
|
|
932
|
+
all_rules
|
|
933
|
+
):
|
|
905
934
|
return False
|
|
906
935
|
document = _merge_queue_removal_document(slug, number, base_ref, token)
|
|
907
936
|
return _is_ejected_on_head(
|
|
@@ -924,14 +953,6 @@ def _merge_queue_removal_document(
|
|
|
924
953
|
) from failure
|
|
925
954
|
|
|
926
955
|
|
|
927
|
-
def _may_merge_through_a_queue(slug: str, base_ref: str, token: str) -> bool:
|
|
928
|
-
try:
|
|
929
|
-
all_rules = _read_branch_rules(slug, urllib.parse.quote(base_ref), token)
|
|
930
|
-
except MergeCheckError:
|
|
931
|
-
return True
|
|
932
|
-
return has_merge_queue_rule(all_rules)
|
|
933
|
-
|
|
934
|
-
|
|
935
956
|
def _list_along(document: object, all_keys: Sequence[str]) -> list[object]:
|
|
936
957
|
all_records = functools.reduce(
|
|
937
958
|
functools.partial(_field_at, document), all_keys, document
|
|
@@ -1009,11 +1030,10 @@ def main(all_arguments: Sequence[str]) -> int:
|
|
|
1009
1030
|
unresolved_thread_count = read_unresolved_thread_count(
|
|
1010
1031
|
parsed.slug, parsed.number, token
|
|
1011
1032
|
)
|
|
1033
|
+
base_ref = str(_nested_field(all_pull_request_fields, BASE_KEY, REF_KEY))
|
|
1034
|
+
all_rules = read_branch_rules(parsed.slug, base_ref, token)
|
|
1012
1035
|
is_ejected_from_merge_queue = read_merge_queue_ejection(
|
|
1013
|
-
parsed.slug,
|
|
1014
|
-
parsed.number,
|
|
1015
|
-
str(_nested_field(all_pull_request_fields, BASE_KEY, REF_KEY)),
|
|
1016
|
-
token,
|
|
1036
|
+
parsed.slug, parsed.number, base_ref, all_rules, token
|
|
1017
1037
|
)
|
|
1018
1038
|
except MergeCheckError as failure:
|
|
1019
1039
|
print(failure, file=sys.stderr)
|
|
@@ -1022,7 +1042,7 @@ def main(all_arguments: Sequence[str]) -> int:
|
|
|
1022
1042
|
if all_pull_request_fields.get(MERGEABLE_STATE_KEY) == MERGEABLE_STATE_BLOCKED:
|
|
1023
1043
|
try:
|
|
1024
1044
|
blocked_evidence = read_blocked_evidence(
|
|
1025
|
-
parsed.slug, all_pull_request_fields, token
|
|
1045
|
+
parsed.slug, all_pull_request_fields, all_rules, token
|
|
1026
1046
|
)
|
|
1027
1047
|
except MergeCheckError as failure:
|
|
1028
1048
|
print(failure, file=sys.stderr)
|
|
@@ -28,11 +28,31 @@ class WeeklyUtilizationProbeError(Exception):
|
|
|
28
28
|
"""Raised when an account's usage meters cannot be measured."""
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
def _usage_pause_scripts_directory() -> Path:
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
def _usage_pause_scripts_directory(module_file: Path) -> Path:
|
|
32
|
+
"""Return the usage-pause scripts directory beside this scripts tree.
|
|
33
|
+
|
|
34
|
+
::
|
|
35
|
+
|
|
36
|
+
packages/claude-dev-env/scripts/x.py -> packages/claude-dev-env/.agents/skills/usage-pause/scripts
|
|
37
|
+
~/.claude/scripts/x.py (link) -> ~/.agents/skills/usage-pause/scripts
|
|
38
|
+
|
|
39
|
+
The installer links ~/.claude/scripts to ~/.agents/scripts, so the
|
|
40
|
+
resolved scripts tree already sits inside the agents home.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
module_file: Path of a module inside the scripts tree.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
The directory that holds the usage-pause resolver.
|
|
47
|
+
"""
|
|
48
|
+
package_root = module_file.resolve().parent.parent
|
|
49
|
+
agents_home = (
|
|
34
50
|
package_root
|
|
35
|
-
|
|
51
|
+
if package_root.name.endswith(USAGE_PAUSE_AGENTS_HOME_DIRECTORY_NAME)
|
|
52
|
+
else package_root / USAGE_PAUSE_AGENTS_HOME_DIRECTORY_NAME
|
|
53
|
+
)
|
|
54
|
+
return (
|
|
55
|
+
agents_home
|
|
36
56
|
/ USAGE_PAUSE_SKILL_DIRECTORY_NAME
|
|
37
57
|
/ USAGE_PAUSE_SKILL_NAME
|
|
38
58
|
/ USAGE_PAUSE_SCRIPTS_DIRECTORY_NAME
|
|
@@ -48,7 +68,7 @@ def _load_resolve_usage_window_module() -> ModuleType:
|
|
|
48
68
|
already_loaded = sys.modules.get(RESOLVE_USAGE_WINDOW_MODULE_NAME)
|
|
49
69
|
if already_loaded is not None:
|
|
50
70
|
return already_loaded
|
|
51
|
-
scripts_directory = _usage_pause_scripts_directory()
|
|
71
|
+
scripts_directory = _usage_pause_scripts_directory(Path(__file__))
|
|
52
72
|
module_path = scripts_directory / RESOLVE_USAGE_WINDOW_FILENAME
|
|
53
73
|
if not module_path.is_file():
|
|
54
74
|
raise WeeklyUtilizationProbeError(
|
|
@@ -90,12 +110,43 @@ class AccountUsageMeters:
|
|
|
90
110
|
weekly_resets_at: datetime | None
|
|
91
111
|
|
|
92
112
|
|
|
113
|
+
def _read_access_token(
|
|
114
|
+
usage_window_resolver: ModuleType, credentials_path: Path, now: datetime
|
|
115
|
+
) -> str | None:
|
|
116
|
+
"""Return the bearer token that reads one account's meters.
|
|
117
|
+
|
|
118
|
+
::
|
|
119
|
+
|
|
120
|
+
credential file holds a token -> that token
|
|
121
|
+
no token, the session's own credential path -> the session ingress token
|
|
122
|
+
no token, another account's credential path -> None
|
|
123
|
+
|
|
124
|
+
A cloud session has no credential file, only the ingress token, and that
|
|
125
|
+
token belongs to the account the session runs on.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
usage_window_resolver: The loaded usage-pause resolver module.
|
|
129
|
+
credentials_path: The account's CLI credential file.
|
|
130
|
+
now: The current time, for the token expiry check.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
A bearer token, or None when the account has none.
|
|
134
|
+
"""
|
|
135
|
+
file_token = usage_window_resolver.read_oauth_access_token(credentials_path, now)
|
|
136
|
+
if file_token is not None:
|
|
137
|
+
return file_token
|
|
138
|
+
session_credentials_path = usage_window_resolver.default_credentials_path()
|
|
139
|
+
if credentials_path.resolve() != session_credentials_path.resolve():
|
|
140
|
+
return None
|
|
141
|
+
return usage_window_resolver.read_session_ingress_token()
|
|
142
|
+
|
|
143
|
+
|
|
93
144
|
def _fetch_account_usage_payload(
|
|
94
145
|
usage_window_resolver: ModuleType, credentials_path: Path
|
|
95
146
|
) -> dict[str, object]:
|
|
96
147
|
now = datetime.now().astimezone()
|
|
97
148
|
try:
|
|
98
|
-
access_token = usage_window_resolver
|
|
149
|
+
access_token = _read_access_token(usage_window_resolver, credentials_path, now)
|
|
99
150
|
if access_token is None:
|
|
100
151
|
raise WeeklyUtilizationProbeError(
|
|
101
152
|
NO_ACCESS_TOKEN_ERROR_TEMPLATE.format(credentials_path=credentials_path)
|
|
@@ -291,10 +291,15 @@ def test_a_ready_pull_request_exits_zero(monkeypatch: pytest.MonkeyPatch) -> Non
|
|
|
291
291
|
"read_unresolved_thread_count",
|
|
292
292
|
lambda slug, number, token: 0,
|
|
293
293
|
)
|
|
294
|
+
monkeypatch.setattr(
|
|
295
|
+
agent_merge_check,
|
|
296
|
+
"read_branch_rules",
|
|
297
|
+
lambda slug, base_ref, token: [],
|
|
298
|
+
)
|
|
294
299
|
monkeypatch.setattr(
|
|
295
300
|
agent_merge_check,
|
|
296
301
|
"read_merge_queue_ejection",
|
|
297
|
-
lambda slug, number, base_ref, token: False,
|
|
302
|
+
lambda slug, number, base_ref, all_rules, token: False,
|
|
298
303
|
)
|
|
299
304
|
assert agent_merge_check.main(["jl-cmd/claude-dev-env", "1442"]) == 0
|
|
300
305
|
|
|
@@ -311,10 +316,15 @@ def test_a_held_pull_request_exits_one(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
|
311
316
|
"read_unresolved_thread_count",
|
|
312
317
|
lambda slug, number, token: 0,
|
|
313
318
|
)
|
|
319
|
+
monkeypatch.setattr(
|
|
320
|
+
agent_merge_check,
|
|
321
|
+
"read_branch_rules",
|
|
322
|
+
lambda slug, base_ref, token: [],
|
|
323
|
+
)
|
|
314
324
|
monkeypatch.setattr(
|
|
315
325
|
agent_merge_check,
|
|
316
326
|
"read_merge_queue_ejection",
|
|
317
|
-
lambda slug, number, base_ref, token: False,
|
|
327
|
+
lambda slug, number, base_ref, all_rules, token: False,
|
|
318
328
|
)
|
|
319
329
|
assert agent_merge_check.main(["jl-cmd/claude-dev-env", "1442"]) == 1
|
|
320
330
|
|
|
@@ -491,6 +501,65 @@ def test_an_unreadable_branch_rule_keeps_the_generic_blocked_reason(
|
|
|
491
501
|
assert BLOCKED_HOLD_REASON in line
|
|
492
502
|
|
|
493
503
|
|
|
504
|
+
def test_a_blocked_pull_request_reads_the_branch_rules_once(
|
|
505
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
506
|
+
capsys: pytest.CaptureFixture[str],
|
|
507
|
+
) -> None:
|
|
508
|
+
routed_answer = _github_answers(
|
|
509
|
+
MAIN_BRANCH_RULES,
|
|
510
|
+
[
|
|
511
|
+
_check_run("Ruff", "success", run_id=11),
|
|
512
|
+
_check_run("Tip Local green", "success", run_id=12),
|
|
513
|
+
],
|
|
514
|
+
behind_by=225,
|
|
515
|
+
)
|
|
516
|
+
all_rules_urls: list[str] = []
|
|
517
|
+
|
|
518
|
+
def _answer(url: str, token: str, all_payload_fields: object) -> object:
|
|
519
|
+
if "/rules/branches/" in url:
|
|
520
|
+
all_rules_urls.append(url)
|
|
521
|
+
return routed_answer(url, token, all_payload_fields)
|
|
522
|
+
|
|
523
|
+
exit_code, line = _run_main(monkeypatch, capsys, _answer)
|
|
524
|
+
assert exit_code == 1
|
|
525
|
+
assert BEHIND_MERGE_QUEUE_HOLD_TEMPLATE.format(count=225) in line
|
|
526
|
+
assert len(all_rules_urls) == 1
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def test_read_branch_rules_quotes_the_base_branch_and_returns_its_rules(
|
|
530
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
531
|
+
) -> None:
|
|
532
|
+
all_rules_urls: list[str] = []
|
|
533
|
+
|
|
534
|
+
def _answer(url: str, token: str, all_payload_fields: object) -> object:
|
|
535
|
+
all_rules_urls.append(url)
|
|
536
|
+
return MAIN_BRANCH_RULES
|
|
537
|
+
|
|
538
|
+
monkeypatch.setattr(agent_merge_check, "_request_json", _answer)
|
|
539
|
+
assert (
|
|
540
|
+
agent_merge_check.read_branch_rules(
|
|
541
|
+
"jl-cmd/claude-dev-env", "release/2026 q4", "token"
|
|
542
|
+
)
|
|
543
|
+
== MAIN_BRANCH_RULES
|
|
544
|
+
)
|
|
545
|
+
assert all_rules_urls[0].endswith("/rules/branches/release/2026%20q4")
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def test_read_branch_rules_returns_the_failure_of_a_refused_read(
|
|
549
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
550
|
+
) -> None:
|
|
551
|
+
refusal = agent_merge_check.MergeCheckError("HTTP Error 403: Forbidden")
|
|
552
|
+
|
|
553
|
+
def _refuse(url: str, token: str, all_payload_fields: object) -> object:
|
|
554
|
+
raise refusal
|
|
555
|
+
|
|
556
|
+
monkeypatch.setattr(agent_merge_check, "_request_json", _refuse)
|
|
557
|
+
assert (
|
|
558
|
+
agent_merge_check.read_branch_rules("jl-cmd/claude-dev-env", "main", "token")
|
|
559
|
+
is refusal
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
|
|
494
563
|
def _evidence(
|
|
495
564
|
all_unmet_checks: tuple[str, ...] = (),
|
|
496
565
|
behind_by: int = 0,
|
|
@@ -626,7 +695,7 @@ def test_read_blocked_evidence_reads_the_pull_request_4922_shape(
|
|
|
626
695
|
),
|
|
627
696
|
)
|
|
628
697
|
assert agent_merge_check.read_blocked_evidence(
|
|
629
|
-
"jl-cmd/claude-dev-env", PULL_REQUEST_4922, "token"
|
|
698
|
+
"jl-cmd/claude-dev-env", PULL_REQUEST_4922, MAIN_BRANCH_RULES, "token"
|
|
630
699
|
) == _evidence(behind_by=225)
|
|
631
700
|
|
|
632
701
|
|
|
@@ -821,7 +890,7 @@ def test_read_merge_queue_ejection_compares_failed_checks_removals_to_the_head(
|
|
|
821
890
|
)
|
|
822
891
|
assert (
|
|
823
892
|
agent_merge_check.read_merge_queue_ejection(
|
|
824
|
-
"jl-cmd/claude-dev-env", 1442, "main", "token"
|
|
893
|
+
"jl-cmd/claude-dev-env", 1442, "main", MAIN_BRANCH_RULES, "token"
|
|
825
894
|
)
|
|
826
895
|
is expected_ejection
|
|
827
896
|
)
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import importlib
|
|
6
|
+
import os
|
|
5
7
|
import subprocess
|
|
6
8
|
import sys
|
|
7
9
|
import urllib.error
|
|
@@ -44,18 +46,95 @@ def test_probe_reads_both_meters_and_resets(monkeypatch: pytest.MonkeyPatch) ->
|
|
|
44
46
|
)
|
|
45
47
|
|
|
46
48
|
|
|
47
|
-
def
|
|
49
|
+
def test_probe_reads_the_session_token_for_the_session_account(
|
|
50
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
51
|
+
) -> None:
|
|
52
|
+
session_credentials = Path("home") / ".claude" / ".credentials.json"
|
|
53
|
+
all_fetched_tokens: list[str] = []
|
|
54
|
+
|
|
55
|
+
class Windows:
|
|
56
|
+
session_utilization = 15.0
|
|
57
|
+
session_resets_at = None
|
|
58
|
+
weekly_utilization = 70.0
|
|
59
|
+
weekly_resets_at = None
|
|
60
|
+
|
|
48
61
|
class Resolver:
|
|
49
62
|
def read_oauth_access_token(self, credentials_path: Path, now: datetime) -> None:
|
|
50
63
|
return None
|
|
51
64
|
|
|
65
|
+
def default_credentials_path(self) -> Path:
|
|
66
|
+
return session_credentials
|
|
67
|
+
|
|
68
|
+
def read_session_ingress_token(self) -> str:
|
|
69
|
+
return "session-token"
|
|
70
|
+
|
|
71
|
+
def _fetch_usage_payload(self, access_token: str) -> dict[str, object]:
|
|
72
|
+
all_fetched_tokens.append(access_token)
|
|
73
|
+
return {"usage": 70.0}
|
|
74
|
+
|
|
75
|
+
def extract_usage_windows(self, payload: dict[str, object]) -> Windows:
|
|
76
|
+
return Windows()
|
|
77
|
+
|
|
78
|
+
monkeypatch.setattr(usage, "_load_resolve_usage_window_module", lambda: Resolver())
|
|
79
|
+
|
|
80
|
+
assert usage.probe_account_meters(session_credentials) == usage.AccountUsageMeters(
|
|
81
|
+
15.0, None, 70.0, None
|
|
82
|
+
)
|
|
83
|
+
assert all_fetched_tokens == ["session-token"]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_probe_rejects_missing_access_token_for_another_account(
|
|
87
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
88
|
+
) -> None:
|
|
89
|
+
class Resolver:
|
|
90
|
+
def read_oauth_access_token(self, credentials_path: Path, now: datetime) -> None:
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
def default_credentials_path(self) -> Path:
|
|
94
|
+
return Path("home") / ".claude" / ".credentials.json"
|
|
95
|
+
|
|
96
|
+
def read_session_ingress_token(self) -> str:
|
|
97
|
+
return "session-token"
|
|
98
|
+
|
|
52
99
|
def _fetch_usage_payload(self, access_token: str) -> dict[str, object]:
|
|
53
100
|
pytest.fail("meter request must not run")
|
|
54
101
|
|
|
55
102
|
monkeypatch.setattr(usage, "_load_resolve_usage_window_module", lambda: Resolver())
|
|
56
103
|
|
|
57
104
|
with pytest.raises(usage.WeeklyUtilizationProbeError, match="bearer token"):
|
|
58
|
-
usage.probe_account_meters(Path("
|
|
105
|
+
usage.probe_account_meters(Path("home") / ".claude-profiles" / "ev" / ".credentials.json")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _create_directory_link(*, from_link: Path, to_target: Path) -> None:
|
|
109
|
+
if sys.platform.startswith("win32"):
|
|
110
|
+
importlib.import_module("_winapi").CreateJunction(str(to_target), str(from_link))
|
|
111
|
+
return
|
|
112
|
+
os.symlink(to_target, from_link, target_is_directory=True)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_scripts_directory_follows_the_installed_scripts_link(tmp_path: Path) -> None:
|
|
116
|
+
home = tmp_path.resolve()
|
|
117
|
+
(home / ".agents" / "scripts").mkdir(parents=True)
|
|
118
|
+
(home / ".claude").mkdir()
|
|
119
|
+
_create_directory_link(
|
|
120
|
+
from_link=home / ".claude" / "scripts", to_target=home / ".agents" / "scripts"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
scripts_directory = usage._usage_pause_scripts_directory(
|
|
124
|
+
home / ".claude" / "scripts" / "claude_chain_usage.py"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
assert scripts_directory == home / ".agents" / "skills" / "usage-pause" / "scripts"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_scripts_directory_in_the_package_tree(tmp_path: Path) -> None:
|
|
131
|
+
package_root = tmp_path.resolve() / "claude-dev-env"
|
|
132
|
+
|
|
133
|
+
scripts_directory = usage._usage_pause_scripts_directory(
|
|
134
|
+
package_root / "scripts" / "claude_chain_usage.py"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
assert scripts_directory == package_root / ".agents" / "skills" / "usage-pause" / "scripts"
|
|
59
138
|
|
|
60
139
|
|
|
61
140
|
def test_probe_wraps_network_failure(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
@@ -79,7 +158,7 @@ def test_failed_module_load_does_not_poison_cache(
|
|
|
79
158
|
(tmp_path / "resolve_usage_window.py").write_text(
|
|
80
159
|
"raise ImportError('intentional load failure')\n", encoding="utf-8"
|
|
81
160
|
)
|
|
82
|
-
monkeypatch.setattr(usage, "_usage_pause_scripts_directory", lambda: tmp_path)
|
|
161
|
+
monkeypatch.setattr(usage, "_usage_pause_scripts_directory", lambda _module_file: tmp_path)
|
|
83
162
|
|
|
84
163
|
with pytest.raises(ImportError, match="intentional load failure"):
|
|
85
164
|
usage._load_resolve_usage_window_module()
|