claude-multiacc 2.0.64 → 2.0.65
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/claude-accounts +4 -4
- package/bin/codex +25 -7
- package/bin/codex-accounts +16 -3
- package/docs/AUTORESUME.md +10 -1
- package/docs/CODEX.md +39 -0
- package/lib/__pycache__/audit.cpython-314.pyc +0 -0
- package/lib/__pycache__/autoresume.cpython-314.pyc +0 -0
- package/lib/__pycache__/autoresume_cross.cpython-314.pyc +0 -0
- package/lib/__pycache__/autoresume_cross_dispatch.cpython-314.pyc +0 -0
- package/lib/__pycache__/autoresume_cross_receipt.cpython-314.pyc +0 -0
- package/lib/__pycache__/autoresume_handoff.cpython-314.pyc +0 -0
- package/lib/__pycache__/autoresume_probe.cpython-314.pyc +0 -0
- package/lib/__pycache__/autoresume_receipt.cpython-314.pyc +0 -0
- package/lib/__pycache__/claude_client_error.cpython-314.pyc +0 -0
- package/lib/__pycache__/claude_reset.cpython-314.pyc +0 -0
- package/lib/__pycache__/claude_token_identity.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_autoresume_mode.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_config_edit.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_credit_controls.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_credit_local_scope.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_credit_selection.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_credits.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_limits_lock.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_mcp_oauth.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_python.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_resume_cwd.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_speed.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_trust.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_usage_fetch.cpython-314.pyc +0 -0
- package/lib/__pycache__/keychain.cpython-314.pyc +0 -0
- package/lib/__pycache__/mcp_mirror.cpython-314.pyc +0 -0
- package/lib/__pycache__/mcp_oauth.cpython-314.pyc +0 -0
- package/lib/__pycache__/mcp_registry.cpython-314.pyc +0 -0
- package/lib/__pycache__/model_catalog.cpython-314.pyc +0 -0
- package/lib/__pycache__/model_match.cpython-314.pyc +0 -0
- package/lib/__pycache__/report.cpython-314.pyc +0 -0
- package/lib/__pycache__/selector_credit_scope.cpython-314.pyc +0 -0
- package/lib/__pycache__/selector_credits.cpython-314.pyc +0 -0
- package/lib/__pycache__/selector_funding.cpython-314.pyc +0 -0
- package/lib/__pycache__/selector_policy.cpython-314.pyc +0 -0
- package/lib/__pycache__/selector_primitives.cpython-314.pyc +0 -0
- package/lib/__pycache__/session_settings.cpython-314.pyc +0 -0
- package/lib/__pycache__/shim_path.cpython-314.pyc +0 -0
- package/lib/autoresume.py +28 -8
- package/lib/codex_speed.py +351 -0
- package/lib/codex_speed.sh +39 -0
- package/lib/common.sh +13 -3
- package/lib/model_match.py +4 -1
- package/lib/session_settings.py +25 -6
- package/lib/shim_path.py +6 -0
- package/package.json +2 -2
- package/tests/__pycache__/autoresume_cross_receipt_support.cpython-314.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_tmux_environment.cpython-314.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_tmux_support.cpython-314.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_unit_support.cpython-314.pyc +0 -0
- package/tests/__pycache__/autoresume_tmux_support.cpython-314.pyc +0 -0
- package/tests/__pycache__/packaged_command_support.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_catalog.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_catalog_cli.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_codex_records.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_codex_trust.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_claude_holders.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_guards.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_holders.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_origin.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_policy.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_receipt.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_tmux.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_tmux_races.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_tokens.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cursor.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cwd.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cwd_handoff.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cwd_reader.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cwd_tmux.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_fallback.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_handoff_argv.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_handoff_history.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_handoff_origins.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_memory.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_memory_tmux.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_mode.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_probe.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_receipt.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_relaunch_settings.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_settings.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_sidechain.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_sidechain_tmux.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_tmux.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_tmux_modes.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_trust_tmux.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_claude_reset.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_claude_token_identity.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_exec_stdin.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_mcp_oauth.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits_polling.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits_report.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_fetch.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_polling.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_reporting.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_windows.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_mcp_mirror.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_mcp_mirror_args.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_mcp_oauth.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_mcp_registry.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_selector.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_selector_cached_credits.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_selector_credit_scope.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_selector_credit_shim.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_selector_credits.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_selector_spread.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_token_identity_ceremony.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_token_identity_integration.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_token_identity_retry.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_token_identity_sync.cpython-314.pyc +0 -0
- package/tests/run-tests.sh +23 -7
- package/tests/test_codex_speed.py +236 -0
- package/tests/test_mutate_lock.py +107 -0
- package/tests/test_shim_path.py +44 -4
- package/lib/__pycache__/audit.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_cross.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_cross_dispatch.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_cross_receipt.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_handoff.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_probe.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_receipt.cpython-312.pyc +0 -0
- package/lib/__pycache__/claude_client_error.cpython-312.pyc +0 -0
- package/lib/__pycache__/claude_reset.cpython-312.pyc +0 -0
- package/lib/__pycache__/claude_token_identity.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_autoresume_mode.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_config_edit.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_credit_controls.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_credit_local_scope.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_credit_selection.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_credits.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_limits_lock.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_mcp_oauth.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_python.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_resume_cwd.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_trust.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_usage_fetch.cpython-312.pyc +0 -0
- package/lib/__pycache__/keychain.cpython-312.pyc +0 -0
- package/lib/__pycache__/mcp_mirror.cpython-312.pyc +0 -0
- package/lib/__pycache__/mcp_oauth.cpython-312.pyc +0 -0
- package/lib/__pycache__/mcp_registry.cpython-312.pyc +0 -0
- package/lib/__pycache__/model_catalog.cpython-312.pyc +0 -0
- package/lib/__pycache__/model_match.cpython-312.pyc +0 -0
- package/lib/__pycache__/report.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_credit_scope.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_credits.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_funding.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_policy.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_primitives.cpython-312.pyc +0 -0
- package/lib/__pycache__/session_settings.cpython-312.pyc +0 -0
- package/lib/__pycache__/shim_path.cpython-312.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_receipt_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_tmux_environment.cpython-312.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_tmux_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_unit_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/autoresume_tmux_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/packaged_command_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_catalog.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_catalog_cli.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_codex_records.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_codex_trust.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_claude_holders.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_guards.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_holders.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_origin.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_policy.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_receipt.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_tmux_races.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_tokens.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cursor.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cwd.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cwd_handoff.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cwd_reader.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cwd_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_fallback.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_handoff_argv.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_handoff_history.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_handoff_origins.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_memory.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_memory_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_mode.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_probe.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_receipt.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_relaunch_settings.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_settings.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_sidechain.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_sidechain_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_tmux_modes.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_trust_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_claude_reset.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_claude_token_identity.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_exec_stdin.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_mcp_oauth.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits_polling.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits_report.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_fetch.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_polling.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_reporting.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_windows.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_mcp_mirror.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_mcp_mirror_args.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_mcp_oauth.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_mcp_registry.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_cached_credits.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_credit_scope.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_credit_shim.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_credits.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_spread.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_token_identity_ceremony.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_token_identity_integration.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_token_identity_retry.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_token_identity_sync.cpython-312.pyc +0 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/tests/run-tests.sh
CHANGED
|
@@ -2632,16 +2632,27 @@ regB="$(claude-accounts list 2>&1 | grep par2@test | awk '{print $1}')"
|
|
|
2632
2632
|
[ -n "$regB" ] && claude-accounts remove "$regB" --yes >/dev/null 2>&1
|
|
2633
2633
|
|
|
2634
2634
|
# ---- 15h. two parallel adds signing into the SAME email: exactly one wins ------------
|
|
2635
|
+
if python3 "$REPO_DIR/tests/test_mutate_lock.py"; then
|
|
2636
|
+
t_ok "mutation lock: missing-stat race preserves the replacement owner"
|
|
2637
|
+
else
|
|
2638
|
+
t_fail "mutation lock ownership suite" "see unittest output above"
|
|
2639
|
+
fi
|
|
2635
2640
|
( CLAUDE_MULTIACC_FORCE_TTY=1 FAKE_EMAIL=same@test claude-accounts add >"$WORK/s1.out" 2>&1 ) &
|
|
2636
2641
|
sA=$!
|
|
2637
2642
|
( CLAUDE_MULTIACC_FORCE_TTY=1 FAKE_EMAIL=same@test claude-accounts add >"$WORK/s2.out" 2>&1 ) &
|
|
2638
2643
|
sB=$!
|
|
2639
|
-
|
|
2644
|
+
sArc=0; sBrc=0
|
|
2645
|
+
wait "$sA" || sArc=$?
|
|
2646
|
+
wait "$sB" || sBrc=$?
|
|
2647
|
+
both="$(cat "$WORK/s1.out" "$WORK/s2.out" 2>/dev/null)"
|
|
2648
|
+
[ "$sArc:$sBrc" = "0:0" ] && t_ok "same-email parallel add: both callers succeed" \
|
|
2649
|
+
|| t_fail "same-email parallel add exit status" "first=$sArc second=$sBrc; $both"
|
|
2640
2650
|
n="$(claude-accounts list 2>&1 | grep -c same@test)"
|
|
2641
2651
|
[ "$n" = "1" ] && t_ok "same-email parallel add: exactly one registered (other skipped)" \
|
|
2642
2652
|
|| t_fail "same-email parallel add" "count=$n (expected 1)"
|
|
2643
|
-
|
|
2644
|
-
&& t_ok "same-email parallel add: loser skipped gracefully"
|
|
2653
|
+
grep -qE "already added as|skipping" "$WORK/s1.out" "$WORK/s2.out" \
|
|
2654
|
+
&& t_ok "same-email parallel add: loser skipped gracefully" \
|
|
2655
|
+
|| t_fail "same-email skip msg" "first=$sArc second=$sBrc; no skip message in: $both"
|
|
2645
2656
|
regS="$(claude-accounts list 2>&1 | grep same@test | awk '{print $1}')"
|
|
2646
2657
|
[ -n "$regS" ] && claude-accounts remove "$regS" --yes >/dev/null 2>&1
|
|
2647
2658
|
|
|
@@ -7797,18 +7808,23 @@ fi
|
|
|
7797
7808
|
|
|
7798
7809
|
# two parallel imports get distinct ids and both land in the manifest (lock coverage;
|
|
7799
7810
|
# codex-review finding)
|
|
7800
|
-
codex-accounts import p1@cx --no-sync
|
|
7811
|
+
codex-accounts import p1@cx --no-sync >"$WORK/cx-import1.out" 2>&1 &
|
|
7801
7812
|
imp1=$!
|
|
7802
|
-
codex-accounts import p2@cx --no-sync
|
|
7813
|
+
codex-accounts import p2@cx --no-sync >"$WORK/cx-import2.out" 2>&1 &
|
|
7803
7814
|
imp2=$!
|
|
7804
|
-
|
|
7815
|
+
imp1rc=0; imp2rc=0
|
|
7816
|
+
wait "$imp1" || imp1rc=$?
|
|
7817
|
+
wait "$imp2" || imp2rc=$?
|
|
7818
|
+
both="$(cat "$WORK/cx-import1.out" "$WORK/cx-import2.out" 2>/dev/null)"
|
|
7819
|
+
[ "$imp1rc:$imp2rc" = "0:0" ] && t_ok "codex: both parallel import callers succeed" \
|
|
7820
|
+
|| t_fail "codex parallel import exit status" "first=$imp1rc second=$imp2rc; $both"
|
|
7805
7821
|
n1="$(grep -c '"email": "p1@cx"' "$CX/accounts.json")"
|
|
7806
7822
|
n2="$(grep -c '"email": "p2@cx"' "$CX/accounts.json")"
|
|
7807
7823
|
id1="$(python3 -c "import json,sys; print(next((a['id'] for a in json.load(open('$CX/accounts.json'))['accounts'] if a['email']=='p1@cx'), ''))")"
|
|
7808
7824
|
id2="$(python3 -c "import json,sys; print(next((a['id'] for a in json.load(open('$CX/accounts.json'))['accounts'] if a['email']=='p2@cx'), ''))")"
|
|
7809
7825
|
{ [ "$n1" = "1" ] && [ "$n2" = "1" ] && [ -n "$id1" ] && [ -n "$id2" ] && [ "$id1" != "$id2" ]; } \
|
|
7810
7826
|
&& t_ok "codex: parallel imports get distinct ids ($id1, $id2)" \
|
|
7811
|
-
|| t_fail "codex parallel import" "p1=$n1($id1) p2=$n2($id2)"
|
|
7827
|
+
|| t_fail "codex parallel import" "p1=$n1($id1) p2=$n2($id2); first=$imp1rc second=$imp2rc; $both"
|
|
7812
7828
|
codex-accounts remove "$id1" --yes >/dev/null 2>&1
|
|
7813
7829
|
codex-accounts remove "$id2" --yes >/dev/null 2>&1
|
|
7814
7830
|
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Account-specific speed, native argv, and quota-resume regression coverage."""
|
|
3
|
+
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import tempfile
|
|
11
|
+
import unittest
|
|
12
|
+
|
|
13
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
14
|
+
sys.path.insert(0, str(ROOT / "lib"))
|
|
15
|
+
sys.path.insert(0, str(ROOT / "tests"))
|
|
16
|
+
import codex_speed as cs
|
|
17
|
+
import session_settings
|
|
18
|
+
import autoresume as ar
|
|
19
|
+
import test_codex_exec_stdin as stdin_fixtures
|
|
20
|
+
from test_autoresume_relaunch_settings import shell_gate
|
|
21
|
+
|
|
22
|
+
MODEL = "gpt-6.1-sol"
|
|
23
|
+
NODE = stdin_fixtures.NODE
|
|
24
|
+
SID = "01a135db-f14d-7a60-b656-d5ff72abd71d"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def catalog(directory, ultra=False, model=MODEL, **changes):
|
|
28
|
+
document = {"fetched_at": datetime.now(timezone.utc).isoformat(), "identity": "native-account-identity",
|
|
29
|
+
"models": [{"slug": model, "visibility": "list", "priority": 0,
|
|
30
|
+
"service_tiers": [{"id": "priority"}] + ([{"id": "ultrafast"}] if ultra else [])}]}
|
|
31
|
+
document.update(changes)
|
|
32
|
+
(Path(directory) / "models_cache.json").write_text(json.dumps(document))
|
|
33
|
+
return document
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SpeedResolutionTests(unittest.TestCase):
|
|
37
|
+
def setUp(self):
|
|
38
|
+
self.tmp = tempfile.TemporaryDirectory()
|
|
39
|
+
self.addCleanup(self.tmp.cleanup)
|
|
40
|
+
self.home = Path(self.tmp.name)
|
|
41
|
+
|
|
42
|
+
def test_capability_is_per_account_and_per_model(self):
|
|
43
|
+
catalog(self.home, ultra=True)
|
|
44
|
+
self.assertEqual(cs.resolve(self.home, "ultrafast", MODEL)["effective"], "ultrafast")
|
|
45
|
+
self.assertEqual(cs.resolve(self.home, "ultrafast", "other-model")["effective"], "fast")
|
|
46
|
+
catalog(self.home)
|
|
47
|
+
result = cs.resolve(self.home, "ultrafast", MODEL)
|
|
48
|
+
self.assertEqual((result["effective"], result["service_tier"], result["fallback"]),
|
|
49
|
+
("fast", "priority", True))
|
|
50
|
+
|
|
51
|
+
def test_stale_malformed_or_reauthenticated_catalog_cannot_grant_ultrafast(self):
|
|
52
|
+
for changes in ({"fetched_at": "2000-01-01T00:00:00Z"}, {"fetched_at": None}, {"models": None}):
|
|
53
|
+
catalog(self.home, ultra=True, **changes)
|
|
54
|
+
self.assertEqual(cs.resolve(self.home, "ultrafast", MODEL)["effective"], "fast")
|
|
55
|
+
catalog(self.home, ultra=True)
|
|
56
|
+
(self.home / "auth.json").write_text("{}")
|
|
57
|
+
future = (self.home / "models_cache.json").stat().st_mtime + 2
|
|
58
|
+
os.utime(self.home / "auth.json", (future, future))
|
|
59
|
+
self.assertEqual(cs.resolve(self.home, "ultrafast", MODEL)["effective"], "fast")
|
|
60
|
+
|
|
61
|
+
def test_service_tiers_are_authoritative_and_legacy_cache_is_supported(self):
|
|
62
|
+
doc = catalog(self.home)
|
|
63
|
+
row = doc["models"][0]
|
|
64
|
+
row.update(service_tiers=[], additional_speed_tiers=["fast", "ultrafast"])
|
|
65
|
+
self.assertEqual(cs.resolve(self.home, "ultrafast", MODEL, doc)["effective"], "fast")
|
|
66
|
+
del row["service_tiers"]
|
|
67
|
+
self.assertEqual(cs.resolve(self.home, "ultrafast", MODEL, doc)["effective"], "ultrafast")
|
|
68
|
+
|
|
69
|
+
def test_env_override_cli_precedence_and_prompt_fidelity(self):
|
|
70
|
+
catalog(self.home, ultra=True)
|
|
71
|
+
(self.home / "config.toml").write_text('model = "gpt-6.1-sol"\nservice_tier = "ultrafast"\n')
|
|
72
|
+
result, args = cs.launch_argv(self.home, ["exec", "--", "-c service_tier=ultrafast"],
|
|
73
|
+
{"CODEX_SPEED": "normal"})
|
|
74
|
+
self.assertEqual(result["effective"], "normal")
|
|
75
|
+
self.assertEqual(args, ["-c", "service_tier=default", "exec", "--", "-c service_tier=ultrafast"])
|
|
76
|
+
result, args = cs.launch_argv(self.home, ["-c", 'service_tier="fast"', "continue"],
|
|
77
|
+
{"CODEX_SPEED": "ultrafast"})
|
|
78
|
+
self.assertEqual(result["effective"], "fast")
|
|
79
|
+
self.assertNotIn('service_tier="fast"', args)
|
|
80
|
+
self.assertEqual(args[-1], "continue")
|
|
81
|
+
|
|
82
|
+
def test_app_server_model_hint_and_profile_defaults(self):
|
|
83
|
+
catalog(self.home, ultra=True)
|
|
84
|
+
(self.home / "config.toml").write_text('model="other-model"\n')
|
|
85
|
+
result, _ = cs.launch_argv(self.home, ["app-server"], {"CODEX_SPEED": "ultrafast", "CODEX_SPEED_MODEL": MODEL})
|
|
86
|
+
self.assertEqual(result["effective"], "ultrafast")
|
|
87
|
+
(self.home / "quick.config.toml").write_text('model="gpt-6.1-sol"\nservice_tier="fast"\n')
|
|
88
|
+
result, _ = cs.launch_argv(self.home, ["--profile", "quick", "continue"], {})
|
|
89
|
+
self.assertEqual(result["effective"], "fast")
|
|
90
|
+
|
|
91
|
+
def test_ignore_user_config_does_not_reintroduce_account_speed(self):
|
|
92
|
+
(self.home / "config.toml").write_text('service_tier="ultrafast"\n')
|
|
93
|
+
argv = ["exec", "--ignore-user-config", "do the work"]
|
|
94
|
+
result, args = cs.launch_argv(self.home, argv, {})
|
|
95
|
+
self.assertIsNone(result)
|
|
96
|
+
self.assertEqual(args, argv)
|
|
97
|
+
result, _ = cs.launch_argv(self.home, argv, {"CODEX_SPEED": "normal"})
|
|
98
|
+
self.assertEqual(result["effective"], "normal")
|
|
99
|
+
|
|
100
|
+
def test_normal_and_fast_never_request_catalog_refresh(self):
|
|
101
|
+
for choice in ("normal", "fast"):
|
|
102
|
+
result, args = cs.launch_argv(self.home, ["app-server"], {"CODEX_SPEED": choice}, executable="/missing")
|
|
103
|
+
self.assertEqual(result["effective"], choice)
|
|
104
|
+
self.assertIn("service_tier=" + ("default" if choice == "normal" else "fast"), args)
|
|
105
|
+
|
|
106
|
+
def test_cold_native_catalog_refresh_and_failure_fallback(self):
|
|
107
|
+
native = self.home / "native"
|
|
108
|
+
native.write_text(f'#!{sys.executable}\n' + '''import json,sys
|
|
109
|
+
for line in sys.stdin:
|
|
110
|
+
request=json.loads(line)
|
|
111
|
+
if 'id' not in request: continue
|
|
112
|
+
result={'data':[{'id':'gpt-6.1-sol','serviceTiers':[{'id':'priority'},{'id':'ultrafast'}]}]} if request['method']=='model/list' else {}
|
|
113
|
+
print(json.dumps({'id':request['id'],'result':result}),flush=True)
|
|
114
|
+
''')
|
|
115
|
+
native.chmod(0o755)
|
|
116
|
+
result, _ = cs.launch_argv(self.home, ["app-server"], {"CODEX_SPEED": "ultrafast", "CODEX_SPEED_MODEL": MODEL}, str(native))
|
|
117
|
+
self.assertEqual(result["effective"], "ultrafast")
|
|
118
|
+
catalog(self.home, ultra=True, fetched_at="2000-01-01T00:00:00Z")
|
|
119
|
+
result, _ = cs.launch_argv(self.home, ["app-server"], {"CODEX_SPEED": "ultrafast", "CODEX_SPEED_MODEL": MODEL}, "/missing")
|
|
120
|
+
self.assertEqual(result["effective"], "fast")
|
|
121
|
+
|
|
122
|
+
def test_native_resume_preparation_saves_only_speed_and_refuses_failed_writes(self):
|
|
123
|
+
native, requests = self.home / "native", self.home / "requests.jsonl"
|
|
124
|
+
native.write_text(f'#!{sys.executable}\n' + '''import json,sys,pathlib
|
|
125
|
+
for line in sys.stdin:
|
|
126
|
+
request=json.loads(line)
|
|
127
|
+
if 'id' not in request: continue
|
|
128
|
+
with pathlib.Path(__file__).with_name('requests.jsonl').open('a') as log: log.write(json.dumps(request)+'\\n')
|
|
129
|
+
result={'serviceTier':request['params']['serviceTier']} if request['method']=='thread/resume' else {}
|
|
130
|
+
print(json.dumps({'id':request['id'],'result':result}),flush=True)
|
|
131
|
+
''')
|
|
132
|
+
native.chmod(0o755)
|
|
133
|
+
result, args = cs.launch_argv(self.home, ["resume", SID, "continue now"], {"CODEX_SPEED": "normal"}, str(native))
|
|
134
|
+
calls = [json.loads(line) for line in requests.read_text().splitlines()]
|
|
135
|
+
self.assertEqual([call["method"] for call in calls], ["initialize", "thread/resume"])
|
|
136
|
+
self.assertEqual(calls[-1]["params"], {"threadId": SID, "serviceTier": "default"})
|
|
137
|
+
self.assertIn("service_tier=default", args)
|
|
138
|
+
with self.assertRaisesRegex(ValueError, "could not preserve speed"):
|
|
139
|
+
cs.launch_argv(self.home, ["resume", SID], {"CODEX_SPEED": "normal"}, "/missing")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@unittest.skipUnless(NODE, "Node is required for the actual child-process transport")
|
|
143
|
+
class SpeedShimTests(unittest.TestCase):
|
|
144
|
+
setUp = stdin_fixtures.CodexExecStdinTests.setUp
|
|
145
|
+
run_node = stdin_fixtures.CodexExecStdinTests.run_node
|
|
146
|
+
|
|
147
|
+
def prepare(self):
|
|
148
|
+
self.env["CLAUDE_MULTIACC_PYTHON"] = sys.executable
|
|
149
|
+
for number in (1, 2):
|
|
150
|
+
catalog(self.pool / f"acct-{number:02d}", ultra=number == 1)
|
|
151
|
+
native = self.root / "bin" / "codex"
|
|
152
|
+
source = native.read_text().split("\n", 1)[1]
|
|
153
|
+
native.write_text(f'#!{sys.executable}\n' + '''import sys,os,json,pathlib
|
|
154
|
+
if sys.argv[1:2] == ['app-server']:
|
|
155
|
+
for line in sys.stdin:
|
|
156
|
+
request=json.loads(line)
|
|
157
|
+
if 'id' not in request: continue
|
|
158
|
+
result=json.loads((pathlib.Path(os.environ['CODEX_HOME'])/'models_cache.json').read_text()) if request['method']=='model/list' else {}
|
|
159
|
+
print(json.dumps({'id':request['id'],'result':result}),flush=True)
|
|
160
|
+
sys.exit(0)
|
|
161
|
+
''' + source)
|
|
162
|
+
|
|
163
|
+
def test_ultrafast_quota_rotation_recomputes_target_capability(self):
|
|
164
|
+
self.prepare()
|
|
165
|
+
result, calls = self.run_node(CODEX_SPEED="ultrafast", CODEX_SPEED_MODEL=MODEL)
|
|
166
|
+
self.assertEqual(result.returncode, 0, result.stderr)
|
|
167
|
+
self.assertEqual([row["account"] for row in calls], ["acct-01", "acct-02"])
|
|
168
|
+
self.assertIn("service_tier=ultrafast", calls[0]["argv"])
|
|
169
|
+
self.assertIn("service_tier=priority", calls[1]["argv"])
|
|
170
|
+
self.assertNotIn("service_tier=ultrafast", calls[1]["argv"])
|
|
171
|
+
|
|
172
|
+
def test_normal_overrides_pinned_home_fast_and_single_word_prompts(self):
|
|
173
|
+
self.prepare()
|
|
174
|
+
home = self.pool / "acct-01"
|
|
175
|
+
(home / "config.toml").write_text('service_tier="fast"\n')
|
|
176
|
+
for pin in ({"CODEX_HOME": str(home)}, {"CODEX_ACCOUNT": "acct-01"}):
|
|
177
|
+
(self.root / "calls.jsonl").unlink(missing_ok=True)
|
|
178
|
+
result, calls = self.run_node(["continue"], FAKE_MODE="success", CODEX_SPEED="normal", **pin)
|
|
179
|
+
self.assertEqual(result.returncode, 0, result.stderr)
|
|
180
|
+
self.assertIn("service_tier=default", calls[0]["argv"])
|
|
181
|
+
|
|
182
|
+
def test_administrative_commands_and_disabled_shim_remain_unchanged(self):
|
|
183
|
+
self.prepare()
|
|
184
|
+
for args, extra in [(["login"], {}), (["exec", "--help"], {}), (["continue"], {"CODEX_MULTIACC_DISABLE": "1"})]:
|
|
185
|
+
(self.root / "calls.jsonl").unlink(missing_ok=True)
|
|
186
|
+
result, calls = self.run_node(args, CODEX_HOME=str(self.pool / "acct-01"),
|
|
187
|
+
FAKE_MODE="success", CODEX_SPEED="ultrafast", **extra)
|
|
188
|
+
self.assertEqual(result.returncode, 0, result.stderr)
|
|
189
|
+
self.assertEqual(calls[0]["argv"], args)
|
|
190
|
+
|
|
191
|
+
def test_invalid_explicit_speed_or_unavailable_helper_never_silently_launches(self):
|
|
192
|
+
self.prepare()
|
|
193
|
+
for extra in ({"CODEX_SPEED": "turbo"}, {"CODEX_SPEED": "normal", "CLAUDE_MULTIACC_PYTHON": "/missing"}):
|
|
194
|
+
result, calls = self.run_node(["continue"], CODEX_HOME=str(self.pool / "acct-01"), **extra)
|
|
195
|
+
self.assertEqual(result.returncode, 2, result.stderr)
|
|
196
|
+
self.assertFalse(calls)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class ResumeSpeedTests(unittest.TestCase):
|
|
200
|
+
def test_history_restores_latest_own_tier_even_when_turn_context_omits_it(self):
|
|
201
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
202
|
+
path = Path(directory) / "history.jsonl"
|
|
203
|
+
records = [
|
|
204
|
+
{"type": "session_meta", "payload": {"id": SID}},
|
|
205
|
+
{"type": "event_msg", "payload": {"type": "thread_settings_applied", "thread_id": SID,
|
|
206
|
+
"thread_settings": {"model": MODEL, "reasoning_effort": "high", "service_tier": "ultrafast"}}},
|
|
207
|
+
{"type": "turn_context", "payload": {"model": MODEL, "effort": "low"}},
|
|
208
|
+
{"type": "event_msg", "payload": {"type": "thread_settings_applied", "thread_id": "other",
|
|
209
|
+
"thread_settings": {"model": MODEL, "service_tier": "default"}}},
|
|
210
|
+
]
|
|
211
|
+
path.write_text("".join(json.dumps(row) + "\n" for row in records))
|
|
212
|
+
self.assertEqual(session_settings.session_settings("codex", path),
|
|
213
|
+
{"model": MODEL, "effort": "low", "speed": "ultrafast"})
|
|
214
|
+
isolated = subprocess.run([sys.executable, "-I", str(ROOT / "lib/session_settings.py"), "codex", str(path)],
|
|
215
|
+
capture_output=True, text=True)
|
|
216
|
+
self.assertEqual(isolated.returncode, 0, isolated.stderr)
|
|
217
|
+
self.assertEqual(json.loads(isolated.stdout), {"model": MODEL, "effort": "low", "speed": "ultrafast"})
|
|
218
|
+
records.append({"type": "turn_context", "payload": {"model": MODEL, "service_tier": None}})
|
|
219
|
+
path.write_text("".join(json.dumps(row) + "\n" for row in records))
|
|
220
|
+
self.assertEqual(session_settings.session_settings("codex", path)["speed"], "normal")
|
|
221
|
+
|
|
222
|
+
def test_relaunch_replaces_speed_without_losing_effort_and_both_gates_agree(self):
|
|
223
|
+
original = ["-m", MODEL, "-c", "model_reasoning_effort=low", "-c", 'service_tier="ultrafast"']
|
|
224
|
+
resumed = ar.relaunch_argv("codex", original, SID, "quota", settings={"speed": "normal"})
|
|
225
|
+
self.assertIn("model_reasoning_effort=low", resumed)
|
|
226
|
+
self.assertIn("service_tier=default", resumed)
|
|
227
|
+
self.assertNotIn('service_tier="ultrafast"', resumed)
|
|
228
|
+
for args in (original, resumed):
|
|
229
|
+
self.assertTrue(ar.argv_ok("codex", args))
|
|
230
|
+
gate = subprocess.run(["bash", "-c", shell_gate(), "gate", *args], capture_output=True)
|
|
231
|
+
self.assertEqual(gate.returncode, 0, gate.stderr)
|
|
232
|
+
self.assertIsNone(ar.relaunch_argv("claude", original, SID, "quota"))
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
if __name__ == "__main__":
|
|
236
|
+
unittest.main()
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Exercise real mutation-lock ownership when a failed mkdir races with release."""
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import subprocess
|
|
7
|
+
import tempfile
|
|
8
|
+
import unittest
|
|
9
|
+
|
|
10
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MutationLockTests(unittest.TestCase):
|
|
14
|
+
def run_shell(self, body):
|
|
15
|
+
with tempfile.TemporaryDirectory(prefix="multiacc-mutation-lock-") as directory:
|
|
16
|
+
root = Path(directory)
|
|
17
|
+
env = {"PATH": os.environ.get("PATH", "/usr/bin:/bin"), "HOME": str(root),
|
|
18
|
+
"CLAUDE_ACCOUNTS_ROOT": str(root / "pool"), "MULTIACC_PROVIDER": "claude"}
|
|
19
|
+
script = '''set -u
|
|
20
|
+
. "$1/lib/common.sh"
|
|
21
|
+
lock="$ACC_ROOT/.locks/mutate"
|
|
22
|
+
mkdir -p "$lock"
|
|
23
|
+
''' + body
|
|
24
|
+
result = subprocess.run(["bash", "-c", script, "mutation-lock-test", str(ROOT)],
|
|
25
|
+
env=env, capture_output=True, text=True, timeout=10)
|
|
26
|
+
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
27
|
+
|
|
28
|
+
def test_acquisition_does_not_trust_nonexclusive_system_mkdir(self):
|
|
29
|
+
self.run_shell('''
|
|
30
|
+
: > "$lock/competing-owner"
|
|
31
|
+
mkdir() {
|
|
32
|
+
# uutils 0.10 can report success when a competing creator won. Model that
|
|
33
|
+
# behavior deterministically: mkdir -p accepts the other holder's directory.
|
|
34
|
+
command mkdir -p "$@"
|
|
35
|
+
}
|
|
36
|
+
sleep() {
|
|
37
|
+
[ -f "$lock/competing-owner" ] || exit 10
|
|
38
|
+
: > "$ACC_ROOT/waited-for-owner"
|
|
39
|
+
rm "$lock/competing-owner"
|
|
40
|
+
rmdir "$lock"
|
|
41
|
+
}
|
|
42
|
+
mutate_lock || exit 11
|
|
43
|
+
[ -f "$ACC_ROOT/waited-for-owner" ] || exit 12
|
|
44
|
+
[ -d "$lock" ] || exit 13
|
|
45
|
+
mutate_unlock
|
|
46
|
+
''')
|
|
47
|
+
|
|
48
|
+
def test_missing_stat_never_removes_a_replacement_owner(self):
|
|
49
|
+
self.run_shell('''
|
|
50
|
+
file_mtime() {
|
|
51
|
+
# The first holder releases after our failed mkdir, before stat. A second
|
|
52
|
+
# holder acquires after that failed stat and before its zero reaches us.
|
|
53
|
+
rmdir "$1" || return 1
|
|
54
|
+
local observed
|
|
55
|
+
if [ "$(uname -s)" = Darwin ]; then
|
|
56
|
+
observed="$(stat -f %m "$1" 2>/dev/null || echo 0)"
|
|
57
|
+
else
|
|
58
|
+
observed="$(stat -c %Y "$1" 2>/dev/null || echo 0)"
|
|
59
|
+
fi
|
|
60
|
+
mkdir "$1" || return 1
|
|
61
|
+
: > "$1/replacement-owner"
|
|
62
|
+
printf '%s\\n' "$observed"
|
|
63
|
+
}
|
|
64
|
+
sleep() {
|
|
65
|
+
# Only the replacement owner's own release permits the waiter to acquire.
|
|
66
|
+
[ -f "$lock/replacement-owner" ] || exit 20
|
|
67
|
+
: > "$ACC_ROOT/owner-preserved"
|
|
68
|
+
rm "$lock/replacement-owner"
|
|
69
|
+
rmdir "$lock"
|
|
70
|
+
}
|
|
71
|
+
mutate_lock || exit 21
|
|
72
|
+
[ -f "$ACC_ROOT/owner-preserved" ] || exit 22
|
|
73
|
+
[ "$MUTATE_LOCK_HELD" = 1 ] || exit 23
|
|
74
|
+
mutate_unlock
|
|
75
|
+
[ ! -e "$lock" ]
|
|
76
|
+
''')
|
|
77
|
+
|
|
78
|
+
def test_unreadable_mtime_uses_the_existing_backoff(self):
|
|
79
|
+
self.run_shell('''
|
|
80
|
+
: > "$lock/owner"
|
|
81
|
+
file_mtime() { printf 'unreadable\\n'; }
|
|
82
|
+
waits=0
|
|
83
|
+
sleep() {
|
|
84
|
+
[ -f "$lock/owner" ] || exit 30
|
|
85
|
+
waits=$((waits + 1))
|
|
86
|
+
if [ "$waits" = 2 ]; then rm "$lock/owner"; rmdir "$lock"; fi
|
|
87
|
+
}
|
|
88
|
+
mutate_lock || exit 31
|
|
89
|
+
[ "$waits" = 2 ] || exit 32
|
|
90
|
+
mutate_unlock
|
|
91
|
+
''')
|
|
92
|
+
|
|
93
|
+
def test_observed_old_lock_remains_reclaimable(self):
|
|
94
|
+
self.run_shell('''
|
|
95
|
+
: > "$lock/abandoned"
|
|
96
|
+
file_mtime() { printf '1\\n'; }
|
|
97
|
+
epoch_now() { printf '100\\n'; }
|
|
98
|
+
sleep() { exit 40; }
|
|
99
|
+
mutate_lock || exit 41
|
|
100
|
+
[ "$MUTATE_LOCK_HELD" = 1 ] || exit 42
|
|
101
|
+
[ ! -e "$lock/abandoned" ] || exit 43
|
|
102
|
+
mutate_unlock
|
|
103
|
+
''')
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
if __name__ == "__main__":
|
|
107
|
+
unittest.main()
|
package/tests/test_shim_path.py
CHANGED
|
@@ -40,6 +40,39 @@ def legacy_block(repo_dir):
|
|
|
40
40
|
return '\n'.join(lines[:3] + [MARK_END]) + '\n'
|
|
41
41
|
|
|
42
42
|
|
|
43
|
+
class ShimPathParserTests(unittest.TestCase):
|
|
44
|
+
def test_complete_osc_shell_integration_wraps_answers(self):
|
|
45
|
+
marker = shim_path.SENTINEL
|
|
46
|
+
for terminator in ('\x07', '\x1b\\'):
|
|
47
|
+
with self.subTest(terminator=repr(terminator)):
|
|
48
|
+
start = '\x1b]3008;start=fixture' + terminator
|
|
49
|
+
end = '\x1b]3008;end=fixture' + terminator
|
|
50
|
+
text = (f'{start}{start}{marker} claude=/pool/claude{end}\n'
|
|
51
|
+
f'{start}{marker} codex=/pool/codex{end}{end}\n')
|
|
52
|
+
self.assertEqual(shim_path._parse(text, shim_path.NAMES),
|
|
53
|
+
{'claude': '/pool/claude', 'codex': '/pool/codex'})
|
|
54
|
+
|
|
55
|
+
def test_osc_removal_keeps_strict_framing_and_first_answer(self):
|
|
56
|
+
marker = shim_path.SENTINEL
|
|
57
|
+
text = (f'\x1b]3008;{marker} claude=/inside-control\x07\n'
|
|
58
|
+
f'echo "{marker} claude=/echoed"\n'
|
|
59
|
+
f'prompt {marker} claude=/prompt\n'
|
|
60
|
+
f'\x1b]3008;start=x\x07{marker} claude=/first\n'
|
|
61
|
+
f'{marker} claude=/later\n'
|
|
62
|
+
f'{marker} codex=/plain\n')
|
|
63
|
+
self.assertEqual(shim_path._parse(text, shim_path.NAMES),
|
|
64
|
+
{'claude': '/first', 'codex': '/plain'})
|
|
65
|
+
|
|
66
|
+
def test_incomplete_osc_cannot_consume_another_line(self):
|
|
67
|
+
marker = shim_path.SENTINEL
|
|
68
|
+
text = (f'\x1b]3008;partial {marker} claude=/unframed\n'
|
|
69
|
+
f'{marker} claude=/complete\n'
|
|
70
|
+
f'\x1b]broken\x1b[31m{marker} codex=/malformed\n'
|
|
71
|
+
f'\x1b]3008;start=x\x1b\\{marker} codex=/complete\n')
|
|
72
|
+
self.assertEqual(shim_path._parse(text, shim_path.NAMES),
|
|
73
|
+
{'claude': '/complete', 'codex': '/complete'})
|
|
74
|
+
|
|
75
|
+
|
|
43
76
|
class ShimPathTests(unittest.TestCase):
|
|
44
77
|
def setUp(self):
|
|
45
78
|
self.tmp = Path(tempfile.mkdtemp(prefix='multiacc-shim-path-'))
|
|
@@ -212,12 +245,19 @@ class ShimPathTests(unittest.TestCase):
|
|
|
212
245
|
block = installed_block(self.repo)
|
|
213
246
|
for rc in ('.zshenv', '.zprofile', '.zshrc', '.profile'):
|
|
214
247
|
self.write(rc, block)
|
|
215
|
-
before = sorted(p.name for p in self.home.iterdir())
|
|
216
248
|
self.probe()
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
for hist in ('.zsh_history', '.bash_history', '.sh_history'):
|
|
249
|
+
histories = ('.zsh_history', '.bash_history', '.sh_history')
|
|
250
|
+
for hist in histories:
|
|
220
251
|
self.assertFalse((self.home / hist).exists(), hist)
|
|
252
|
+
# System rc files may create unrelated completion caches such as
|
|
253
|
+
# .zcompdump. The guarantee is that probe commands neither create history
|
|
254
|
+
# nor change history the operator already has.
|
|
255
|
+
prior = b'echo existing-user-history\n'
|
|
256
|
+
for hist in histories:
|
|
257
|
+
(self.home / hist).write_bytes(prior)
|
|
258
|
+
self.probe()
|
|
259
|
+
for hist in histories:
|
|
260
|
+
self.assertEqual((self.home / hist).read_bytes(), prior, hist)
|
|
221
261
|
|
|
222
262
|
def test_rc_that_reads_stdin_costs_an_answer_not_a_verdict(self):
|
|
223
263
|
"""A prompt or `read` in an rc file eats one line of the fed script. The lost
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|