claude-dev-env 8.33.2 → 8.35.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/bin/install-pstack-plugin.mjs +65 -33
- package/bin/install-pstack-plugin.test.mjs +93 -17
- package/bin/install.agents-home.test.mjs +1 -0
- package/bin/install.codex-rules.test.mjs +2 -0
- package/bin/install.cursor-rules.test.mjs +1 -0
- package/bin/install.mjs +23 -9
- package/bin/install.prune.test.mjs +1 -0
- package/bin/install.question-presentation.test.mjs +4 -0
- package/bin/install.settings-defaults.test.mjs +1 -0
- package/bin/install.test.mjs +76 -0
- package/bin/install.transaction.test.mjs +8 -0
- package/bin/install.uninstall-transaction.test.mjs +2 -0
- package/docs/codex-accounts.md +65 -12
- package/package.json +1 -1
- package/scripts/claude_account_profile.py +68 -21
- package/scripts/codex_account_choice.py +389 -24
- package/scripts/dev_env_scripts_constants/codex_account_constants.py +65 -2
- package/scripts/test_claude_account_profile.py +34 -0
- package/scripts/test_codex_account_choice.py +369 -1
|
@@ -4,15 +4,85 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
6
|
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
from collections.abc import Callable, Iterator
|
|
7
9
|
from datetime import datetime, timedelta, timezone
|
|
8
10
|
from pathlib import Path
|
|
9
11
|
|
|
10
12
|
import pytest
|
|
11
13
|
|
|
12
14
|
import codex_account_choice as choice
|
|
15
|
+
from claude_account_profile import links_to
|
|
13
16
|
from codex_account_meters import CodexAccountMeters, CodexMeterUnreadError, UsageWindow
|
|
14
17
|
|
|
15
18
|
NOW = datetime(2026, 9, 23, 17, 0, tzinfo=timezone.utc)
|
|
19
|
+
ALL_SHARED_ENTRY_NAMES = (
|
|
20
|
+
"AGENTS.md",
|
|
21
|
+
"agents",
|
|
22
|
+
"config.toml",
|
|
23
|
+
"hooks",
|
|
24
|
+
"hooks.json",
|
|
25
|
+
"plugins",
|
|
26
|
+
"rules",
|
|
27
|
+
"skills",
|
|
28
|
+
)
|
|
29
|
+
NPM_CODEX_SHIM_TEXT = (
|
|
30
|
+
"@ECHO off\r\n"
|
|
31
|
+
"GOTO start\r\n"
|
|
32
|
+
":find_dp0\r\n"
|
|
33
|
+
"SET dp0=%~dp0\r\n"
|
|
34
|
+
"EXIT /b\r\n"
|
|
35
|
+
":start\r\n"
|
|
36
|
+
"SETLOCAL\r\n"
|
|
37
|
+
"CALL :find_dp0\r\n"
|
|
38
|
+
'SET "_prog=cmd"\r\n'
|
|
39
|
+
"endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% &"
|
|
40
|
+
' "%_prog%" /d /c "echo %%CODEX_HOME%%"\r\n'
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@pytest.fixture(autouse=True)
|
|
45
|
+
def clear_account_roster_environment(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
46
|
+
monkeypatch.delenv("CODEX_ACCOUNT_PROFILES", raising=False)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def build_codex_main_home(tmp_path: Path) -> Path:
|
|
50
|
+
main_home = tmp_path / "main"
|
|
51
|
+
for each_directory in ("agents", "hooks", "plugins", "rules", "skills", "sessions"):
|
|
52
|
+
(main_home / each_directory).mkdir(parents=True)
|
|
53
|
+
for each_file in ("AGENTS.md", "config.toml", "hooks.json", "auth.json", "history.jsonl"):
|
|
54
|
+
(main_home / each_file).write_text(each_file, encoding="utf-8")
|
|
55
|
+
return main_home
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def save_roster(profiles_root: Path, document: object) -> None:
|
|
59
|
+
profiles_root.mkdir(parents=True, exist_ok=True)
|
|
60
|
+
(profiles_root / "account-launchers.json").write_text(
|
|
61
|
+
json.dumps(document), encoding="utf-8"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def typed_lines(*all_lines: str) -> Callable[[str], str]:
|
|
66
|
+
remaining_lines: Iterator[str] = iter(all_lines)
|
|
67
|
+
|
|
68
|
+
def read_line(prompt: str) -> str:
|
|
69
|
+
assert prompt == "Name for this Codex account launcher (blank to finish): "
|
|
70
|
+
try:
|
|
71
|
+
return next(remaining_lines)
|
|
72
|
+
except StopIteration as error:
|
|
73
|
+
raise EOFError from error
|
|
74
|
+
|
|
75
|
+
return read_line
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def install(main_home: Path, tmp_path: Path, *all_names: str) -> dict[str, dict[str, object]]:
|
|
79
|
+
return choice.install_codex_account_launchers(
|
|
80
|
+
main_home=main_home,
|
|
81
|
+
profiles_root=tmp_path / "profiles",
|
|
82
|
+
launcher_directory=tmp_path / "bin",
|
|
83
|
+
all_names=all_names,
|
|
84
|
+
now=NOW,
|
|
85
|
+
)
|
|
16
86
|
|
|
17
87
|
|
|
18
88
|
def reading(
|
|
@@ -251,7 +321,7 @@ class TestSync:
|
|
|
251
321
|
class TestSmallHelpers:
|
|
252
322
|
@pytest.mark.parametrize(
|
|
253
323
|
("entry_name", "is_local"),
|
|
254
|
-
[("auth.json", True), ("sessions", True), ("state_5.sqlite", True), ("config.toml", False), ("plugins", False)],
|
|
324
|
+
[("auth.json", True), ("sessions", True), ("state_5.sqlite", True), ("config.toml", False), ("plugins", False), ("agents", False)],
|
|
255
325
|
)
|
|
256
326
|
def should_keep_every_entry_outside_the_shared_set_per_account(
|
|
257
327
|
self, entry_name: str, is_local: bool
|
|
@@ -274,3 +344,301 @@ class TestSmallHelpers:
|
|
|
274
344
|
payload = choice.reading_payload(reading("codex-1", weekly_used=40.0, short_used=5.0))
|
|
275
345
|
assert payload["percent_left"] == 60.0
|
|
276
346
|
assert [each["minutes"] for each in payload["windows"]] == [300, 10080]
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
class TestAccountRoster:
|
|
350
|
+
def should_take_the_environment_roster_over_the_saved_file(
|
|
351
|
+
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
352
|
+
) -> None:
|
|
353
|
+
save_roster(tmp_path, ["gamma"])
|
|
354
|
+
monkeypatch.setenv("CODEX_ACCOUNT_PROFILES", " beta , alpha,, ")
|
|
355
|
+
assert choice.saved_codex_account_names(tmp_path) == ("beta", "alpha")
|
|
356
|
+
|
|
357
|
+
def should_read_the_saved_file_when_the_environment_is_unset(
|
|
358
|
+
self, tmp_path: Path
|
|
359
|
+
) -> None:
|
|
360
|
+
save_roster(tmp_path, ["gamma", "alpha"])
|
|
361
|
+
assert choice.saved_codex_account_names(tmp_path) == ("gamma", "alpha")
|
|
362
|
+
|
|
363
|
+
def should_name_no_account_when_neither_is_set(self, tmp_path: Path) -> None:
|
|
364
|
+
assert choice.saved_codex_account_names(tmp_path) == ()
|
|
365
|
+
|
|
366
|
+
def should_refuse_an_invalid_name_from_the_environment(
|
|
367
|
+
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
368
|
+
) -> None:
|
|
369
|
+
monkeypatch.setenv("CODEX_ACCOUNT_PROFILES", "alpha,bad name")
|
|
370
|
+
with pytest.raises(ValueError, match="CODEX_ACCOUNT_PROFILES names 'bad name'"):
|
|
371
|
+
choice.saved_codex_account_names(tmp_path)
|
|
372
|
+
|
|
373
|
+
@pytest.mark.parametrize("document", [["alpha", "con"], ["alpha", 3], {"alpha": 1}])
|
|
374
|
+
def should_refuse_a_saved_file_with_an_invalid_name(
|
|
375
|
+
self, tmp_path: Path, document: object
|
|
376
|
+
) -> None:
|
|
377
|
+
save_roster(tmp_path, document)
|
|
378
|
+
with pytest.raises(ValueError, match="account-launchers.json"):
|
|
379
|
+
choice.saved_codex_account_names(tmp_path)
|
|
380
|
+
|
|
381
|
+
def should_refuse_a_saved_file_that_is_not_json(
|
|
382
|
+
self, tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
383
|
+
) -> None:
|
|
384
|
+
(tmp_path / "account-launchers.json").write_text('["alpha",', encoding="utf-8")
|
|
385
|
+
with pytest.raises(SystemExit) as exit_info:
|
|
386
|
+
choice.main(["--profiles-root", str(tmp_path), "check", "alpha"])
|
|
387
|
+
assert exit_info.value.code == 2
|
|
388
|
+
assert "account-launchers.json" in capsys.readouterr().err
|
|
389
|
+
|
|
390
|
+
def should_drop_duplicates_and_keep_the_first_order(self, tmp_path: Path) -> None:
|
|
391
|
+
save_roster(tmp_path, ["beta", "alpha", "beta", "gamma", "alpha"])
|
|
392
|
+
assert choice.saved_codex_account_names(tmp_path) == ("beta", "alpha", "gamma")
|
|
393
|
+
|
|
394
|
+
def should_save_the_roster_as_a_json_list_ending_in_a_newline(
|
|
395
|
+
self, tmp_path: Path
|
|
396
|
+
) -> None:
|
|
397
|
+
profiles_root = tmp_path / "profiles"
|
|
398
|
+
choice.save_codex_account_names(profiles_root, ("alpha", "beta"))
|
|
399
|
+
assert (profiles_root / "account-launchers.json").read_bytes() == (
|
|
400
|
+
b'[\n "alpha",\n "beta"\n]\n'
|
|
401
|
+
)
|
|
402
|
+
assert choice.saved_codex_account_names(profiles_root) == ("alpha", "beta")
|
|
403
|
+
|
|
404
|
+
def should_fall_back_to_the_four_fixed_accounts_without_a_roster(
|
|
405
|
+
self, tmp_path: Path
|
|
406
|
+
) -> None:
|
|
407
|
+
assert choice.codex_account_names(tmp_path) == (
|
|
408
|
+
"codex-1",
|
|
409
|
+
"codex-2",
|
|
410
|
+
"codex-3",
|
|
411
|
+
"codex-4",
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
class TestRosterDrivesTheJobCommands:
|
|
416
|
+
def should_try_the_roster_in_order_and_skip_the_fixed_accounts(
|
|
417
|
+
self,
|
|
418
|
+
tmp_path: Path,
|
|
419
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
420
|
+
capsys: pytest.CaptureFixture[str],
|
|
421
|
+
) -> None:
|
|
422
|
+
all_weekly_used = {"beta": 95.0, "alpha": 50.0, "codex-1": 0.0}
|
|
423
|
+
for each_name in all_weekly_used:
|
|
424
|
+
(tmp_path / each_name).mkdir()
|
|
425
|
+
(tmp_path / each_name / "auth.json").write_text("{}")
|
|
426
|
+
monkeypatch.setenv("CODEX_ACCOUNT_PROFILES", "beta,alpha")
|
|
427
|
+
monkeypatch.setattr(
|
|
428
|
+
choice,
|
|
429
|
+
"_meter_reader",
|
|
430
|
+
lambda codex_path: (
|
|
431
|
+
lambda codex_home: CodexAccountMeters(
|
|
432
|
+
(UsageWindow(10080, all_weekly_used[codex_home.name], None),)
|
|
433
|
+
)
|
|
434
|
+
),
|
|
435
|
+
)
|
|
436
|
+
assert choice.main(["--profiles-root", str(tmp_path), "choose"]) == 0
|
|
437
|
+
payload = json.loads(capsys.readouterr().out)
|
|
438
|
+
assert (payload["tier"], payload["account"]) == ("normal", "alpha")
|
|
439
|
+
assert [each["name"] for each in payload["accounts"]] == ["beta", "alpha"]
|
|
440
|
+
|
|
441
|
+
def should_choose_among_the_four_fixed_accounts_without_a_roster(
|
|
442
|
+
self,
|
|
443
|
+
tmp_path: Path,
|
|
444
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
445
|
+
capsys: pytest.CaptureFixture[str],
|
|
446
|
+
) -> None:
|
|
447
|
+
monkeypatch.setattr(choice, "_meter_reader", lambda codex_path: None)
|
|
448
|
+
assert choice.main(["--profiles-root", str(tmp_path), "choose"]) == 0
|
|
449
|
+
payload = json.loads(capsys.readouterr().out)
|
|
450
|
+
assert [each["name"] for each in payload["accounts"]] == [
|
|
451
|
+
"codex-1",
|
|
452
|
+
"codex-2",
|
|
453
|
+
"codex-3",
|
|
454
|
+
"codex-4",
|
|
455
|
+
]
|
|
456
|
+
|
|
457
|
+
def should_sync_only_the_roster_accounts(
|
|
458
|
+
self,
|
|
459
|
+
tmp_path: Path,
|
|
460
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
461
|
+
capsys: pytest.CaptureFixture[str],
|
|
462
|
+
) -> None:
|
|
463
|
+
main_home = build_codex_main_home(tmp_path)
|
|
464
|
+
profiles_root = tmp_path / "profiles"
|
|
465
|
+
monkeypatch.setenv("CODEX_ACCOUNT_PROFILES", "alpha")
|
|
466
|
+
all_arguments = ["--profiles-root", str(profiles_root), "sync"]
|
|
467
|
+
assert choice.main([*all_arguments, "--main-home", str(main_home)]) == 0
|
|
468
|
+
assert list(json.loads(capsys.readouterr().out)) == ["alpha"]
|
|
469
|
+
assert links_to(profiles_root / "alpha" / "agents", main_home / "agents")
|
|
470
|
+
assert not (profiles_root / "codex-1").exists()
|
|
471
|
+
|
|
472
|
+
def should_check_a_saved_roster_account(
|
|
473
|
+
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
474
|
+
) -> None:
|
|
475
|
+
save_roster(tmp_path, ["alpha"])
|
|
476
|
+
(tmp_path / "alpha").mkdir()
|
|
477
|
+
(tmp_path / "alpha" / "auth.json").write_text("{}")
|
|
478
|
+
monkeypatch.setattr(
|
|
479
|
+
choice,
|
|
480
|
+
"_meter_reader",
|
|
481
|
+
lambda codex_path: (
|
|
482
|
+
lambda codex_home: CodexAccountMeters((UsageWindow(10080, 40.0, None),))
|
|
483
|
+
),
|
|
484
|
+
)
|
|
485
|
+
assert choice.main(["--profiles-root", str(tmp_path), "check", "alpha"]) == 0
|
|
486
|
+
|
|
487
|
+
@pytest.mark.parametrize("account", ["gamma", "codex-1"])
|
|
488
|
+
def should_refuse_to_check_an_account_outside_the_roster(
|
|
489
|
+
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, account: str
|
|
490
|
+
) -> None:
|
|
491
|
+
save_roster(tmp_path, ["alpha"])
|
|
492
|
+
monkeypatch.setattr(choice, "_meter_reader", lambda codex_path: None)
|
|
493
|
+
with pytest.raises(SystemExit) as exit_info:
|
|
494
|
+
choice.main(["--profiles-root", str(tmp_path), "check", account])
|
|
495
|
+
assert exit_info.value.code == 2
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
class TestInstallLaunchers:
|
|
499
|
+
def should_link_the_shared_setup_and_keep_each_sign_in_per_account(
|
|
500
|
+
self, tmp_path: Path
|
|
501
|
+
) -> None:
|
|
502
|
+
main_home = build_codex_main_home(tmp_path)
|
|
503
|
+
report = install(main_home, tmp_path, "alpha", "beta")
|
|
504
|
+
assert list(report) == ["alpha", "beta"]
|
|
505
|
+
for each_name in ("alpha", "beta"):
|
|
506
|
+
each_home = tmp_path / "profiles" / each_name
|
|
507
|
+
for each_entry in ALL_SHARED_ENTRY_NAMES:
|
|
508
|
+
assert links_to(each_home / each_entry, main_home / each_entry)
|
|
509
|
+
for each_local_entry in ("auth.json", "history.jsonl", "sessions"):
|
|
510
|
+
assert not os.path.lexists(each_home / each_local_entry)
|
|
511
|
+
assert report[each_name]["launcher"] == str(
|
|
512
|
+
tmp_path / "bin" / f"codex-{each_name}.cmd"
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
def should_change_nothing_on_a_second_run(self, tmp_path: Path) -> None:
|
|
516
|
+
main_home = build_codex_main_home(tmp_path)
|
|
517
|
+
install(main_home, tmp_path, "alpha")
|
|
518
|
+
launcher_path = tmp_path / "bin" / "codex-alpha.cmd"
|
|
519
|
+
first_launcher_bytes = launcher_path.read_bytes()
|
|
520
|
+
second_report = install(main_home, tmp_path, "alpha")
|
|
521
|
+
assert second_report["alpha"]["linked"] == []
|
|
522
|
+
assert second_report["alpha"]["moved_aside"] == []
|
|
523
|
+
assert second_report["alpha"]["unlinked"] == []
|
|
524
|
+
assert launcher_path.read_bytes() == first_launcher_bytes
|
|
525
|
+
assert sorted(each.name for each in (tmp_path / "bin").iterdir()) == [
|
|
526
|
+
"codex-alpha.cmd"
|
|
527
|
+
]
|
|
528
|
+
|
|
529
|
+
def should_set_codex_home_and_call_codex_in_the_launcher(
|
|
530
|
+
self, tmp_path: Path
|
|
531
|
+
) -> None:
|
|
532
|
+
install(build_codex_main_home(tmp_path), tmp_path, "alpha")
|
|
533
|
+
launcher_lines = (tmp_path / "bin" / "codex-alpha.cmd").read_bytes().split(b"\r\n")
|
|
534
|
+
profile_home = tmp_path / "profiles" / "alpha"
|
|
535
|
+
assert f'set "CODEX_HOME={profile_home}"'.encode() in launcher_lines
|
|
536
|
+
assert b"call codex %*" in launcher_lines
|
|
537
|
+
|
|
538
|
+
def should_install_the_environment_roster_from_the_command_line(
|
|
539
|
+
self,
|
|
540
|
+
tmp_path: Path,
|
|
541
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
542
|
+
capsys: pytest.CaptureFixture[str],
|
|
543
|
+
) -> None:
|
|
544
|
+
main_home = build_codex_main_home(tmp_path)
|
|
545
|
+
save_roster(tmp_path / "profiles", ["gamma"])
|
|
546
|
+
monkeypatch.setenv("CODEX_ACCOUNT_PROFILES", "beta")
|
|
547
|
+
all_arguments = [
|
|
548
|
+
"--profiles-root",
|
|
549
|
+
str(tmp_path / "profiles"),
|
|
550
|
+
"install",
|
|
551
|
+
"--main-home",
|
|
552
|
+
str(main_home),
|
|
553
|
+
"--launcher-directory",
|
|
554
|
+
str(tmp_path / "bin"),
|
|
555
|
+
]
|
|
556
|
+
assert choice.main(all_arguments) == 0
|
|
557
|
+
assert list(json.loads(capsys.readouterr().out)) == ["beta"]
|
|
558
|
+
assert (tmp_path / "bin" / "codex-beta.cmd").is_file()
|
|
559
|
+
assert not (tmp_path / "bin" / "codex-gamma.cmd").exists()
|
|
560
|
+
|
|
561
|
+
def should_print_an_empty_report_for_an_empty_roster(
|
|
562
|
+
self, tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
563
|
+
) -> None:
|
|
564
|
+
all_arguments = ["--profiles-root", str(tmp_path / "profiles"), "install"]
|
|
565
|
+
assert choice.main([*all_arguments, "--launcher-directory", str(tmp_path / "bin")]) == 0
|
|
566
|
+
assert json.loads(capsys.readouterr().out) == {}
|
|
567
|
+
assert not (tmp_path / "bin").exists()
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
class TestSetupRoster:
|
|
571
|
+
def should_save_each_valid_name_once_in_typed_order(self, tmp_path: Path) -> None:
|
|
572
|
+
all_shown: list[str] = []
|
|
573
|
+
all_names, all_retired = choice.update_codex_account_roster(
|
|
574
|
+
profiles_root=tmp_path,
|
|
575
|
+
launcher_directory=tmp_path / "bin",
|
|
576
|
+
now=NOW,
|
|
577
|
+
read_line=typed_lines("alpha", "bad name", "alpha", "beta", "", "gamma"),
|
|
578
|
+
write_line=all_shown.append,
|
|
579
|
+
)
|
|
580
|
+
assert (all_names, all_retired) == (("alpha", "beta"), {})
|
|
581
|
+
assert choice.saved_codex_account_names(tmp_path) == ("alpha", "beta")
|
|
582
|
+
assert all_shown == [
|
|
583
|
+
"Saved Codex account launchers: none",
|
|
584
|
+
"profile name must use letters, digits, hyphens, or underscores",
|
|
585
|
+
]
|
|
586
|
+
|
|
587
|
+
def should_finish_at_the_end_of_input(self, tmp_path: Path) -> None:
|
|
588
|
+
all_names, _ = choice.update_codex_account_roster(
|
|
589
|
+
profiles_root=tmp_path,
|
|
590
|
+
launcher_directory=tmp_path / "bin",
|
|
591
|
+
now=NOW,
|
|
592
|
+
read_line=typed_lines("gamma"),
|
|
593
|
+
write_line=lambda line: None,
|
|
594
|
+
)
|
|
595
|
+
assert all_names == ("gamma",)
|
|
596
|
+
|
|
597
|
+
def should_move_a_dropped_launcher_aside_and_keep_its_home(
|
|
598
|
+
self, tmp_path: Path
|
|
599
|
+
) -> None:
|
|
600
|
+
main_home = build_codex_main_home(tmp_path)
|
|
601
|
+
profiles_root = tmp_path / "profiles"
|
|
602
|
+
choice.save_codex_account_names(profiles_root, ("alpha", "beta"))
|
|
603
|
+
install(main_home, tmp_path, "alpha", "beta")
|
|
604
|
+
(profiles_root / "beta" / "auth.json").write_text("beta sign-in", encoding="utf-8")
|
|
605
|
+
all_shown: list[str] = []
|
|
606
|
+
all_names, all_retired = choice.update_codex_account_roster(
|
|
607
|
+
profiles_root=profiles_root,
|
|
608
|
+
launcher_directory=tmp_path / "bin",
|
|
609
|
+
now=NOW,
|
|
610
|
+
read_line=typed_lines("alpha", ""),
|
|
611
|
+
write_line=all_shown.append,
|
|
612
|
+
)
|
|
613
|
+
moved_launcher = tmp_path / "bin" / "codex-beta.cmd.replaced-20260923T170000Z"
|
|
614
|
+
assert all_names == ("alpha",)
|
|
615
|
+
assert all_retired == {"beta": str(moved_launcher)}
|
|
616
|
+
assert all_shown == ["Saved Codex account launchers: alpha, beta"]
|
|
617
|
+
assert moved_launcher.is_file()
|
|
618
|
+
assert not (tmp_path / "bin" / "codex-beta.cmd").exists()
|
|
619
|
+
assert (tmp_path / "bin" / "codex-alpha.cmd").is_file()
|
|
620
|
+
assert (profiles_root / "beta" / "auth.json").read_text(encoding="utf-8") == (
|
|
621
|
+
"beta sign-in"
|
|
622
|
+
)
|
|
623
|
+
assert choice.saved_codex_account_names(profiles_root) == ("alpha",)
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
class TestCodexLauncher:
|
|
627
|
+
@pytest.mark.skipif(os.name != "nt", reason="runs the launcher through cmd.exe")
|
|
628
|
+
def should_hand_codex_home_to_the_npm_codex_shim(self, tmp_path: Path) -> None:
|
|
629
|
+
install(build_codex_main_home(tmp_path), tmp_path, "alpha")
|
|
630
|
+
launcher_path = tmp_path / "bin" / "codex-alpha.cmd"
|
|
631
|
+
fake_directory = tmp_path / "fake"
|
|
632
|
+
fake_directory.mkdir()
|
|
633
|
+
(fake_directory / "codex.cmd").write_bytes(NPM_CODEX_SHIM_TEXT.encode("utf-8"))
|
|
634
|
+
environment = dict(os.environ)
|
|
635
|
+
environment["PATH"] = str(fake_directory) + os.pathsep + environment["PATH"]
|
|
636
|
+
environment["CODEX_HOME"] = str(tmp_path / "decoy")
|
|
637
|
+
completed = subprocess.run(
|
|
638
|
+
["cmd", "/c", str(launcher_path)],
|
|
639
|
+
env=environment,
|
|
640
|
+
capture_output=True,
|
|
641
|
+
text=True,
|
|
642
|
+
check=False,
|
|
643
|
+
)
|
|
644
|
+
assert completed.stdout.strip() == str(tmp_path / "profiles" / "alpha")
|