claude-dev-env 2.2.0 → 2.2.1

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.
@@ -18,7 +18,9 @@ SCRIPTS_DIRECTORY = Path(__file__).resolve().parent
18
18
  if str(SCRIPTS_DIRECTORY) not in sys.path:
19
19
  sys.path.insert(0, str(SCRIPTS_DIRECTORY))
20
20
 
21
- from usage_pause_constants.resolve_usage_window_constants import (
21
+ from usage_pause_constants.resolve_usage_window_constants import ( # noqa: E402
22
+ DESKTOP_ENTRYPOINT_VALUE,
23
+ ENTRYPOINT_ENV_VAR,
22
24
  SESSION_INGRESS_TOKEN_FILE_ENV_VAR as INGRESS_TOKEN_FILE_ENV_VAR,
23
25
  )
24
26
 
@@ -188,8 +190,7 @@ class TestReadOauthAccessToken:
188
190
  credentials_path.write_text("{not valid json", encoding="utf-8")
189
191
  with caplog.at_level(logging.WARNING):
190
192
  assert (
191
- resolver.read_oauth_access_token(credentials_path, local_now())
192
- is None
193
+ resolver.read_oauth_access_token(credentials_path, local_now()) is None
193
194
  )
194
195
  assert any("unreadable" in each_message for each_message in caplog.messages)
195
196
 
@@ -240,7 +241,43 @@ class TestReadSessionIngressToken:
240
241
  assert INGRESS_TOKEN_FILE_ENV_VAR == "CLAUDE_SESSION_INGRESS_TOKEN_FILE"
241
242
 
242
243
 
244
+ class TestRunningOnDesktopHost:
245
+ def should_be_true_when_entrypoint_names_the_desktop_app(
246
+ self, monkeypatch: pytest.MonkeyPatch
247
+ ) -> None:
248
+ resolver = load_resolver_module()
249
+ monkeypatch.setenv(ENTRYPOINT_ENV_VAR, DESKTOP_ENTRYPOINT_VALUE)
250
+ assert resolver.running_on_desktop_host() is True
251
+
252
+ def should_be_false_when_entrypoint_names_a_non_desktop_host(
253
+ self, monkeypatch: pytest.MonkeyPatch
254
+ ) -> None:
255
+ resolver = load_resolver_module()
256
+ monkeypatch.setenv(ENTRYPOINT_ENV_VAR, "cli")
257
+ assert resolver.running_on_desktop_host() is False
258
+
259
+ def should_be_false_when_entrypoint_is_unset(
260
+ self, monkeypatch: pytest.MonkeyPatch
261
+ ) -> None:
262
+ resolver = load_resolver_module()
263
+ monkeypatch.delenv(ENTRYPOINT_ENV_VAR, raising=False)
264
+ assert resolver.running_on_desktop_host() is False
265
+
266
+ def should_pin_the_desktop_entrypoint_contract_to_the_literal_marker(
267
+ self, monkeypatch: pytest.MonkeyPatch
268
+ ) -> None:
269
+ resolver = load_resolver_module()
270
+ monkeypatch.setenv("CLAUDE_CODE_ENTRYPOINT", "claude-desktop")
271
+ assert resolver.running_on_desktop_host() is True
272
+ assert ENTRYPOINT_ENV_VAR == "CLAUDE_CODE_ENTRYPOINT"
273
+ assert DESKTOP_ENTRYPOINT_VALUE == "claude-desktop"
274
+
275
+
243
276
  class TestResolveAccessToken:
277
+ @pytest.fixture(autouse=True)
278
+ def force_non_desktop_host(self, monkeypatch: pytest.MonkeyPatch) -> None:
279
+ monkeypatch.delenv(ENTRYPOINT_ENV_VAR, raising=False)
280
+
244
281
  def should_use_ingress_token_when_credential_file_missing(
245
282
  self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
246
283
  ) -> None:
@@ -295,6 +332,57 @@ class TestResolveAccessToken:
295
332
  assert resolver.resolve_access_token(missing_credentials, local_now()) is None
296
333
 
297
334
 
335
+ class TestResolveAccessTokenHostAware:
336
+ def should_ignore_a_valid_cli_credential_token_on_the_desktop_host(
337
+ self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
338
+ ) -> None:
339
+ resolver = load_resolver_module()
340
+ now = local_now()
341
+ credentials_path = tmp_path / ".credentials.json"
342
+ future_milliseconds = int((now + timedelta(hours=1)).timestamp() * 1000)
343
+ write_credentials(
344
+ credentials_path, future_milliseconds, access_token="cli-credential-token"
345
+ )
346
+ monkeypatch.setenv(ENTRYPOINT_ENV_VAR, DESKTOP_ENTRYPOINT_VALUE)
347
+ monkeypatch.delenv(INGRESS_TOKEN_FILE_ENV_VAR, raising=False)
348
+ chosen_token = resolver.resolve_access_token(credentials_path, now)
349
+ assert chosen_token != "cli-credential-token"
350
+ assert chosen_token is None
351
+
352
+ def should_use_the_ingress_token_over_the_cli_credential_on_the_desktop_host(
353
+ self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
354
+ ) -> None:
355
+ resolver = load_resolver_module()
356
+ now = local_now()
357
+ credentials_path = tmp_path / ".credentials.json"
358
+ future_milliseconds = int((now + timedelta(hours=1)).timestamp() * 1000)
359
+ write_credentials(
360
+ credentials_path, future_milliseconds, access_token="cli-credential-token"
361
+ )
362
+ token_file = tmp_path / "ingress-token"
363
+ token_file.write_text("ingress-token-value", encoding="utf-8")
364
+ monkeypatch.setenv(ENTRYPOINT_ENV_VAR, DESKTOP_ENTRYPOINT_VALUE)
365
+ monkeypatch.setenv(INGRESS_TOKEN_FILE_ENV_VAR, str(token_file))
366
+ chosen_token = resolver.resolve_access_token(credentials_path, now)
367
+ assert chosen_token == "ingress-token-value"
368
+ assert chosen_token != "cli-credential-token"
369
+
370
+ def should_return_the_cli_credential_token_on_a_non_desktop_host(
371
+ self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
372
+ ) -> None:
373
+ resolver = load_resolver_module()
374
+ now = local_now()
375
+ credentials_path = tmp_path / ".credentials.json"
376
+ future_milliseconds = int((now + timedelta(hours=1)).timestamp() * 1000)
377
+ write_credentials(
378
+ credentials_path, future_milliseconds, access_token="cli-credential-token"
379
+ )
380
+ monkeypatch.setenv(ENTRYPOINT_ENV_VAR, "cli")
381
+ monkeypatch.delenv(INGRESS_TOKEN_FILE_ENV_VAR, raising=False)
382
+ chosen_token = resolver.resolve_access_token(credentials_path, now)
383
+ assert chosen_token == "cli-credential-token"
384
+
385
+
298
386
  class TestExtractUsageWindows:
299
387
  def should_extract_session_and_weekly_buckets(self) -> None:
300
388
  resolver = load_resolver_module()
@@ -361,13 +449,20 @@ class TestBuildPausePlan:
361
449
 
362
450
  class TestCommandLine:
363
451
  def run_resolver(
364
- self, *arguments: str, ingress_token_file: str | None = None
452
+ self,
453
+ *arguments: str,
454
+ ingress_token_file: str | None = None,
455
+ entrypoint: str | None = None,
365
456
  ) -> subprocess.CompletedProcess[str]:
366
457
  child_environment = dict(os.environ)
367
458
  if ingress_token_file is None:
368
459
  child_environment.pop(INGRESS_TOKEN_FILE_ENV_VAR, None)
369
460
  else:
370
461
  child_environment[INGRESS_TOKEN_FILE_ENV_VAR] = ingress_token_file
462
+ if entrypoint is None:
463
+ child_environment.pop(ENTRYPOINT_ENV_VAR, None)
464
+ else:
465
+ child_environment[ENTRYPOINT_ENV_VAR] = entrypoint
371
466
  return subprocess.run(
372
467
  [sys.executable, str(RESOLVER_PATH), *arguments],
373
468
  capture_output=True,
@@ -403,9 +498,7 @@ class TestCommandLine:
403
498
  assert str(missing_path) in resolved_payload["error"]
404
499
  assert f"{INGRESS_TOKEN_FILE_ENV_VAR} unset" in resolved_payload["error"]
405
500
 
406
- def should_name_the_ingress_token_path_in_the_error(
407
- self, tmp_path: Path
408
- ) -> None:
501
+ def should_name_the_ingress_token_path_in_the_error(self, tmp_path: Path) -> None:
409
502
  missing_credentials = tmp_path / "absent.json"
410
503
  stale_ingress = tmp_path / "stale-ingress-token"
411
504
  completed = self.run_resolver(
@@ -429,10 +522,29 @@ class TestCommandLine:
429
522
  assert completed.returncode == 2
430
523
  resolved_payload = json.loads(completed.stdout)
431
524
  assert (
432
- f"{INGRESS_TOKEN_FILE_ENV_VAR} set but empty"
433
- in resolved_payload["error"]
525
+ f"{INGRESS_TOKEN_FILE_ENV_VAR} set but empty" in resolved_payload["error"]
434
526
  )
435
527
 
528
+ def should_exit_two_with_a_host_aware_error_on_the_desktop_host(
529
+ self, tmp_path: Path
530
+ ) -> None:
531
+ credentials_path = tmp_path / ".credentials.json"
532
+ future_milliseconds = int((local_now() + timedelta(hours=1)).timestamp() * 1000)
533
+ write_credentials(
534
+ credentials_path, future_milliseconds, access_token="cli-credential-token"
535
+ )
536
+ completed = self.run_resolver(
537
+ "--credentials-path",
538
+ str(credentials_path),
539
+ entrypoint="claude-desktop",
540
+ )
541
+ assert completed.returncode == 2
542
+ assert "cli-credential-token" not in completed.stdout
543
+ error_text = json.loads(completed.stdout)["error"]
544
+ assert "different auth session" in error_text
545
+ assert "/usage" in error_text
546
+ assert "/usage-pause" in error_text
547
+
436
548
  def should_reject_invalid_override_with_error_payload(self) -> None:
437
549
  completed = self.run_resolver("--override", "soon")
438
550
  assert completed.returncode == 2
@@ -1,9 +1,10 @@
1
1
  """Constants for the usage-window resolver.
2
2
 
3
3
  Groups: the OAuth usage-endpoint probe, the CLI credential file keys, the
4
- session ingress token file environment variable, the usage-response field
5
- keys, the override parse patterns, the wakeup stage sizing, the weekly warn
6
- threshold, the result JSON keys, the source labels, and the exit codes.
4
+ session ingress token file environment variable, the host entrypoint
5
+ detection, the usage-response field keys, the override parse patterns, the
6
+ wakeup stage sizing, the weekly warn threshold, the result JSON keys, the
7
+ source labels, and the exit codes.
7
8
  """
8
9
 
9
10
  from __future__ import annotations
@@ -24,6 +25,9 @@ CREDENTIALS_EXPIRES_AT_KEY = "expiresAt"
24
25
  SESSION_INGRESS_TOKEN_FILE_ENV_VAR = "CLAUDE_SESSION_INGRESS_TOKEN_FILE"
25
26
  MILLISECONDS_PER_SECOND = 1000
26
27
 
28
+ ENTRYPOINT_ENV_VAR = "CLAUDE_CODE_ENTRYPOINT"
29
+ DESKTOP_ENTRYPOINT_VALUE = "claude-desktop"
30
+
27
31
  FIVE_HOUR_BUCKET_KEY = "five_hour"
28
32
  SEVEN_DAY_BUCKET_KEY = "seven_day"
29
33
  UTILIZATION_KEY = "utilization"