claude-multiacc 1.0.7 → 1.0.8
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/README.md +78 -8
- package/bin/cli.mjs +22 -6
- package/bin/codex +453 -0
- package/bin/codex-accounts +1663 -0
- package/install.sh +90 -12
- package/lib/codex_audit.py +238 -0
- package/lib/common.sh +70 -24
- package/package.json +6 -2
- package/tests/run-tests.sh +791 -2
package/tests/run-tests.sh
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# claude-multiacc sandboxed test suite.
|
|
3
|
-
# No network, no real accounts, no quota: fake `claude`
|
|
2
|
+
# claude-multiacc sandboxed test suite — covers BOTH providers (claude + codex).
|
|
3
|
+
# No network, no real accounts, no quota: fake `claude`/`codex` binaries + file://
|
|
4
|
+
# usage/token fixtures.
|
|
4
5
|
set -u
|
|
5
6
|
|
|
6
7
|
REPO_DIR="$(cd "$(dirname "$0")/.." && pwd -P)"
|
|
@@ -88,6 +89,12 @@ export CLAUDE_MULTIACC_MIN_FETCH=0
|
|
|
88
89
|
# The oauth token endpoint must NEVER be hit for real from tests: default to a missing
|
|
89
90
|
# file:// fixture (refresh fails fast, offline); the refresh tests override per-case.
|
|
90
91
|
export CLAUDE_MULTIACC_TOKEN_URL="file://$WORK/token-endpoint-missing.json"
|
|
92
|
+
# Same for the usage endpoint: the SHIM's opportunistic background `limits --quiet`
|
|
93
|
+
# kick inherits this default, so it can never reach a real endpoint from tests
|
|
94
|
+
# (every explicit limits test overrides the URL inline). The pre-armed .limits-kick
|
|
95
|
+
# throttle keeps those background kicks from racing explicit limits runs.
|
|
96
|
+
export CLAUDE_MULTIACC_USAGE_URL="file://$WORK/usage-endpoint-missing.json"
|
|
97
|
+
: > "$ACC/.limits-kick"
|
|
91
98
|
|
|
92
99
|
now="$(date +%s)"
|
|
93
100
|
|
|
@@ -1245,6 +1252,788 @@ else
|
|
|
1245
1252
|
t_ok "npm-layer tests skipped (node not installed)"
|
|
1246
1253
|
fi
|
|
1247
1254
|
|
|
1255
|
+
# ============================ CODEX PROVIDER =====================================
|
|
1256
|
+
# The codex pool (bin/codex + bin/codex-accounts + lib/codex_audit.py) is separate
|
|
1257
|
+
# code over separate state (~/.codex-accounts), so it gets its own sandboxed pass:
|
|
1258
|
+
# fake `codex` binary, JWT-shaped auth fixtures, file:// usage/token endpoints.
|
|
1259
|
+
|
|
1260
|
+
export CODEX_ACCOUNTS_DIR="$WORK/codex-accounts"
|
|
1261
|
+
CX="$CODEX_ACCOUNTS_DIR"
|
|
1262
|
+
mkdir -p "$CX/tmp"
|
|
1263
|
+
unset CODEX_HOME CODEX_ACCOUNT CODEX_SHIM_ACTIVE 2>/dev/null || true
|
|
1264
|
+
export CODEX_MULTIACC_NO_SYNC=1
|
|
1265
|
+
export CODEX_MULTIACC_MIN_FETCH=0
|
|
1266
|
+
export CODEX_MULTIACC_TOKEN_URL="file://$WORK/cx-token-endpoint-missing.json"
|
|
1267
|
+
# Default usage URL is an offline missing fixture: the SHIM's opportunistic
|
|
1268
|
+
# background `limits --quiet` kick must never reach a real endpoint from tests
|
|
1269
|
+
# (each limits test overrides the URL inline). The pre-armed .limits-kick throttle
|
|
1270
|
+
# below keeps those background kicks from racing the explicit limits runs at all.
|
|
1271
|
+
export CODEX_MULTIACC_USAGE_URL="file://$WORK/cx-usage-endpoint-missing.json"
|
|
1272
|
+
export CODEX_MULTIACC_FORCE_TTY=1
|
|
1273
|
+
export FAKE_CTL2="$WORK/ctl2"
|
|
1274
|
+
: > "$CX/.limits-kick"
|
|
1275
|
+
|
|
1276
|
+
# Codex auth fixtures are real-shaped: auth.json carrying JWTs whose exp/email
|
|
1277
|
+
# claims the audit decodes offline (exactly what the CLI does).
|
|
1278
|
+
MKAUTH="$WORK/mk_cx_auth.py"
|
|
1279
|
+
cat > "$MKAUTH" <<'EOF'
|
|
1280
|
+
import base64, json, sys
|
|
1281
|
+
def jwt(claims):
|
|
1282
|
+
enc = lambda o: base64.urlsafe_b64encode(json.dumps(o).encode()).rstrip(b'=').decode()
|
|
1283
|
+
return f"{enc({'alg':'RS256'})}.{enc(claims)}.sig"
|
|
1284
|
+
path, email, exp, mode = sys.argv[1], sys.argv[2], float(sys.argv[3]), (sys.argv[4] if len(sys.argv) > 4 else '')
|
|
1285
|
+
if mode == 'apikey':
|
|
1286
|
+
doc = {"auth_mode": "apikey", "OPENAI_API_KEY": "sk-test-api-key", "tokens": None, "last_refresh": None}
|
|
1287
|
+
else:
|
|
1288
|
+
auth_claim = {"chatgpt_plan_type": "pro", "chatgpt_account_id": "acct-uuid"}
|
|
1289
|
+
tokens = {
|
|
1290
|
+
"id_token": jwt({"email": email, "exp": exp, "https://api.openai.com/auth": auth_claim}),
|
|
1291
|
+
"access_token": jwt({"exp": exp, "https://api.openai.com/auth": auth_claim}),
|
|
1292
|
+
"refresh_token": "rt-" + email,
|
|
1293
|
+
"account_id": "acct-uuid",
|
|
1294
|
+
}
|
|
1295
|
+
if mode == 'norefresh':
|
|
1296
|
+
tokens.pop('refresh_token')
|
|
1297
|
+
doc = {"auth_mode": "chatgpt", "OPENAI_API_KEY": None, "tokens": tokens,
|
|
1298
|
+
"last_refresh": "2026-01-01T00:00:00Z"}
|
|
1299
|
+
json.dump(doc, open(path, 'w'))
|
|
1300
|
+
EOF
|
|
1301
|
+
export MKAUTH
|
|
1302
|
+
mk_cx_auth() { python3 "$MKAUTH" "$@"; }
|
|
1303
|
+
FUTURE_EXP=$((now + 864000))
|
|
1304
|
+
|
|
1305
|
+
# Fake "real" codex: prints which CODEX_HOME it ran under; scriptable login +
|
|
1306
|
+
# failures via a control file. (Note: must not contain the hyphenated shim marker.)
|
|
1307
|
+
cat > "$FAKEBIN/codex" <<EOF
|
|
1308
|
+
#!/usr/bin/env bash
|
|
1309
|
+
# fake real codex for tests (not a shim)
|
|
1310
|
+
if [ "\${1:-}" = "login" ]; then
|
|
1311
|
+
shift
|
|
1312
|
+
# record the flags the CLI chose (device-auth default vs --browser opt-out)
|
|
1313
|
+
printf '%s\n' "\$*" > "\${FAKE_LOGIN_ARGS:-/dev/null}" 2>/dev/null || true
|
|
1314
|
+
[ -n "\${FAKE_LOGIN_FAIL:-}" ] && { echo "login aborted" >&2; exit 1; }
|
|
1315
|
+
# simulate a completed ChatGPT sign-in: write a JWT-shaped auth.json
|
|
1316
|
+
python3 "\$MKAUTH" "\${CODEX_HOME:-/dev/null}/auth.json" "\${FAKE_EMAIL:-fake@test}" "\$(( \$(date +%s) + 864000 ))"
|
|
1317
|
+
echo "Successfully logged in"
|
|
1318
|
+
exit 0
|
|
1319
|
+
fi
|
|
1320
|
+
ctl="\${FAKE_CTL2:-/nonexistent}"
|
|
1321
|
+
acct="\$(basename "\${CODEX_HOME:-none}")"
|
|
1322
|
+
if [ -f "\$ctl" ] && grep -qx "fail:\$acct" "\$ctl" 2>/dev/null; then
|
|
1323
|
+
echo "ERROR: 429 Too Many Requests — you have hit your usage limit" >&2
|
|
1324
|
+
exit 1
|
|
1325
|
+
fi
|
|
1326
|
+
if [ -f "\$ctl" ] && grep -qx "authfail:\$acct" "\$ctl" 2>/dev/null; then
|
|
1327
|
+
echo "Error: token expired. Please run codex login again" >&2
|
|
1328
|
+
exit 1
|
|
1329
|
+
fi
|
|
1330
|
+
if [ -f "\$ctl" ] && grep -qx "orgfail:\$acct" "\$ctl" 2>/dev/null; then
|
|
1331
|
+
echo "Codex has been disabled by your workspace admin"
|
|
1332
|
+
exit 1
|
|
1333
|
+
fi
|
|
1334
|
+
outfile=""
|
|
1335
|
+
prev=""
|
|
1336
|
+
for a in "\$@"; do
|
|
1337
|
+
case "\$prev" in -o|--output-last-message) outfile="\$a" ;; esac
|
|
1338
|
+
case "\$a" in
|
|
1339
|
+
--exit7) echo "ordinary failure, not auth related" >&2; exit 7 ;;
|
|
1340
|
+
--echo-stdin) cat; exit 0 ;;
|
|
1341
|
+
esac
|
|
1342
|
+
prev="\$a"
|
|
1343
|
+
done
|
|
1344
|
+
[ -n "\$outfile" ] && printf 'OK' > "\$outfile"
|
|
1345
|
+
echo "CFG=\$acct"
|
|
1346
|
+
EOF
|
|
1347
|
+
chmod +x "$FAKEBIN/codex"
|
|
1348
|
+
|
|
1349
|
+
# ---- C1. passthrough: no manifest yet ---------------------------------------------
|
|
1350
|
+
out="$(codex 2>&1)"
|
|
1351
|
+
check "codex: passthrough without manifest" "CFG=none" "$out"
|
|
1352
|
+
|
|
1353
|
+
# ---- codex manifest + two chatgpt accounts ----------------------------------------
|
|
1354
|
+
cat > "$CX/accounts.json" <<EOF
|
|
1355
|
+
{
|
|
1356
|
+
"version": 1,
|
|
1357
|
+
"server": "root@203.0.113.1",
|
|
1358
|
+
"server_root": "/root/.codex-accounts",
|
|
1359
|
+
"server_repo": "/root/claude-multiacc",
|
|
1360
|
+
"threshold": 90,
|
|
1361
|
+
"accounts": [
|
|
1362
|
+
{"id": "acct-01", "email": "a@cx", "home": "mac", "added_at": "2026-08-21T00:00:00Z"},
|
|
1363
|
+
{"id": "acct-02", "email": "b@cx", "home": "mac", "added_at": "2026-08-21T00:00:00Z"}
|
|
1364
|
+
]
|
|
1365
|
+
}
|
|
1366
|
+
EOF
|
|
1367
|
+
for i in 01 02; do
|
|
1368
|
+
mkdir -p "$CX/acct-$i"
|
|
1369
|
+
done
|
|
1370
|
+
mk_cx_auth "$CX/acct-01/auth.json" a@cx "$FUTURE_EXP"
|
|
1371
|
+
mk_cx_auth "$CX/acct-02/auth.json" b@cx "$FUTURE_EXP"
|
|
1372
|
+
|
|
1373
|
+
# ---- C2. passthrough guards --------------------------------------------------------
|
|
1374
|
+
out="$(CODEX_HOME=/tmp/other codex 2>&1)"
|
|
1375
|
+
check "codex: passthrough with CODEX_HOME" "CFG=other" "$out"
|
|
1376
|
+
out="$(CODEX_MULTIACC_DISABLE=1 codex 2>&1)"
|
|
1377
|
+
check "codex: passthrough when disabled" "CFG=none" "$out"
|
|
1378
|
+
|
|
1379
|
+
# ---- C3. headroom selection --------------------------------------------------------
|
|
1380
|
+
cxlj() { printf '{"fetched_at":%s,"weekly_percent":%s,"session_percent":%s,"max_percent":%s,"buckets":[]}' "$now" "$1" "$2" "$3"; }
|
|
1381
|
+
cxlj 80 10 80 > "$CX/acct-01/limits.json"
|
|
1382
|
+
cxlj 20 10 20 > "$CX/acct-02/limits.json"
|
|
1383
|
+
all2=1
|
|
1384
|
+
for _ in $(seq 1 15); do
|
|
1385
|
+
case "$(codex 2>&1)" in *CFG=acct-02*) ;; *) all2=0 ;; esac
|
|
1386
|
+
done
|
|
1387
|
+
[ "$all2" = "1" ] && t_ok "codex: picks the highest weekly-headroom account" \
|
|
1388
|
+
|| t_fail "codex headroom selection" "picked the more-utilized account"
|
|
1389
|
+
# high session must NOT beat better weekly headroom
|
|
1390
|
+
cxlj 10 85 85 > "$CX/acct-01/limits.json"
|
|
1391
|
+
cxlj 70 20 70 > "$CX/acct-02/limits.json"
|
|
1392
|
+
all1=1
|
|
1393
|
+
for _ in $(seq 1 15); do
|
|
1394
|
+
case "$(codex 2>&1)" in *CFG=acct-01*) ;; *) all1=0 ;; esac
|
|
1395
|
+
done
|
|
1396
|
+
[ "$all1" = "1" ] && t_ok "codex: high session does NOT beat better weekly headroom" \
|
|
1397
|
+
|| t_fail "codex weekly-over-session" "ranked the account with less weekly headroom higher"
|
|
1398
|
+
# equal scores spread load
|
|
1399
|
+
cxlj 10 10 10 > "$CX/acct-01/limits.json"
|
|
1400
|
+
cxlj 10 10 10 > "$CX/acct-02/limits.json"
|
|
1401
|
+
hits1=0; hits2=0
|
|
1402
|
+
for _ in $(seq 1 40); do
|
|
1403
|
+
case "$(codex 2>&1)" in
|
|
1404
|
+
*CFG=acct-01*) hits1=$((hits1+1)) ;;
|
|
1405
|
+
*CFG=acct-02*) hits2=$((hits2+1)) ;;
|
|
1406
|
+
esac
|
|
1407
|
+
done
|
|
1408
|
+
{ [ "$hits1" -gt 0 ] && [ "$hits2" -gt 0 ] && [ $((hits1+hits2)) -eq 40 ]; } \
|
|
1409
|
+
&& t_ok "codex: equal scores spread randomly (acct-01=$hits1 acct-02=$hits2)" \
|
|
1410
|
+
|| t_fail "codex tie spreading" "acct-01=$hits1 acct-02=$hits2 (want both >0, total 40)"
|
|
1411
|
+
# stale telemetry ranks neutral, never free
|
|
1412
|
+
printf '{"fetched_at":1,"weekly_percent":1,"session_percent":1,"max_percent":1,"buckets":[]}' > "$CX/acct-01/limits.json"
|
|
1413
|
+
cxlj 30 30 30 > "$CX/acct-02/limits.json"
|
|
1414
|
+
out="$(codex 2>&1)"
|
|
1415
|
+
check "codex: stale 1% loses to fresh 30%" "CFG=acct-02" "$out"
|
|
1416
|
+
rm -f "$CX"/acct-*/limits.json
|
|
1417
|
+
|
|
1418
|
+
# ---- C4. pin -----------------------------------------------------------------------
|
|
1419
|
+
out="$(CODEX_ACCOUNT=acct-02 codex 2>&1)"
|
|
1420
|
+
check "codex: CODEX_ACCOUNT pin" "CFG=acct-02" "$out"
|
|
1421
|
+
mkdir -p "$CX/acct-07"
|
|
1422
|
+
out="$(CODEX_ACCOUNT=acct-07 codex 2>&1)"
|
|
1423
|
+
check "codex: pin to auth-less dir (ceremony)" "CFG=acct-07" "$out"
|
|
1424
|
+
rmdir "$CX/acct-07"
|
|
1425
|
+
|
|
1426
|
+
# ---- C5. limited marker excludes; pin overrides; expired marker self-clears ---------
|
|
1427
|
+
printf '%s\nbucket=GPT-5.3-Codex-Spark:7d percent=95 reason=limits\n' "$((now+3600))" > "$CX/acct-01/.limited"
|
|
1428
|
+
all2=1
|
|
1429
|
+
for _ in $(seq 1 15); do
|
|
1430
|
+
case "$(codex 2>&1)" in *CFG=acct-02*) ;; *) all2=0 ;; esac
|
|
1431
|
+
done
|
|
1432
|
+
[ "$all2" = "1" ] && t_ok "codex: limited account excluded from pool" || t_fail "codex limited exclusion" "acct-01 was still picked"
|
|
1433
|
+
out="$(CODEX_ACCOUNT=acct-01 codex 2>&1)"
|
|
1434
|
+
check "codex: explicit pin wins over marker" "CFG=acct-01" "$out"
|
|
1435
|
+
printf '%s\nbucket=5h percent=95 reason=limits\n' "$((now-10))" > "$CX/acct-01/.limited"
|
|
1436
|
+
codex >/dev/null 2>&1
|
|
1437
|
+
[ ! -f "$CX/acct-01/.limited" ] && t_ok "codex: expired .limited marker self-clears" \
|
|
1438
|
+
|| t_fail "codex marker expiry" ".limited survived its reset time"
|
|
1439
|
+
|
|
1440
|
+
# ---- C6. all limited -> least-utilized fallback -------------------------------------
|
|
1441
|
+
printf '%s\nbucket=7d percent=95 reason=limits\n' "$((now+3600))" > "$CX/acct-01/.limited"
|
|
1442
|
+
printf '%s\nbucket=7d percent=99 reason=limits\n' "$((now+3600))" > "$CX/acct-02/.limited"
|
|
1443
|
+
cxlj 95 10 95 > "$CX/acct-01/limits.json"
|
|
1444
|
+
cxlj 99 10 99 > "$CX/acct-02/limits.json"
|
|
1445
|
+
out="$(codex 2>&1)"
|
|
1446
|
+
check "codex: all-limited falls back to least utilized" "CFG=acct-01" "$out"
|
|
1447
|
+
grep -q "all-limited fallback=acct-01" "$CX/selection.log" \
|
|
1448
|
+
&& t_ok "codex: all-limited fallback logged" || t_fail "codex fallback log" "no all-limited line"
|
|
1449
|
+
rm -f "$CX"/acct-*/.limited "$CX"/acct-*/limits.json
|
|
1450
|
+
|
|
1451
|
+
# ---- C7. dead logins: .expired excludes, heals on newer credential ------------------
|
|
1452
|
+
printf '%s\nreason=refresh-denied-http-400 marked_at=t detail=x\n' "$now" > "$CX/acct-01/.expired"
|
|
1453
|
+
all2=1
|
|
1454
|
+
for _ in $(seq 1 10); do
|
|
1455
|
+
case "$(codex 2>&1)" in *CFG=acct-02*) ;; *) all2=0 ;; esac
|
|
1456
|
+
done
|
|
1457
|
+
[ "$all2" = "1" ] && t_ok "codex: dead login (proven park) is never selected" \
|
|
1458
|
+
|| t_fail "codex dead login" "a parked account was selected"
|
|
1459
|
+
# ...not even as the all-limited fallback
|
|
1460
|
+
printf '%s\nbucket=7d percent=95 reason=limits\n' "$((now+3600))" > "$CX/acct-02/.limited"
|
|
1461
|
+
out="$(codex 2>&1)"
|
|
1462
|
+
check "codex: limited-but-alive beats dead in the fallback" "CFG=acct-02" "$out"
|
|
1463
|
+
rm -f "$CX/acct-02/.limited"
|
|
1464
|
+
# a NEWER credential heals a credential-scoped park
|
|
1465
|
+
sleep 1
|
|
1466
|
+
mk_cx_auth "$CX/acct-01/auth.json" a@cx "$FUTURE_EXP"
|
|
1467
|
+
out="$(CODEX_SHIM_SELECT=random codex 2>&1)"
|
|
1468
|
+
[ ! -f "$CX/acct-01/.expired" ] && t_ok "codex: newer auth.json clears a credential park" \
|
|
1469
|
+
|| t_fail "codex park heal" ".expired survived a newer credential"
|
|
1470
|
+
# an org-blocked park survives a newer credential (policy, not credential)
|
|
1471
|
+
printf '%s\nreason=org-blocked marked_at=t detail=x\n' "$now" > "$CX/acct-01/.expired"
|
|
1472
|
+
sleep 1
|
|
1473
|
+
mk_cx_auth "$CX/acct-01/auth.json" a@cx "$FUTURE_EXP"
|
|
1474
|
+
codex >/dev/null 2>&1
|
|
1475
|
+
[ -f "$CX/acct-01/.expired" ] && t_ok "codex: org-blocked park survives a credential refresh" \
|
|
1476
|
+
|| t_fail "codex org park" "a token write cleared an org-policy park"
|
|
1477
|
+
rm -f "$CX/acct-01/.expired"
|
|
1478
|
+
# a soft-stamped park expires on its own
|
|
1479
|
+
printf '%s\nreason=auth-error soft_until=%s marked_at=t detail=x\n' "$now" "$((now-5))" > "$CX/acct-02/.expired"
|
|
1480
|
+
codex >/dev/null 2>&1
|
|
1481
|
+
[ ! -f "$CX/acct-02/.expired" ] && t_ok "codex: soft_until park expires on its own" \
|
|
1482
|
+
|| t_fail "codex soft park" "an elapsed soft park still excluded the account"
|
|
1483
|
+
|
|
1484
|
+
# ---- C8. exec auto-retry ------------------------------------------------------------
|
|
1485
|
+
# acct-01 gets the strictly better score so headroom selection deterministically
|
|
1486
|
+
# picks it FIRST; the scripted failure then forces the retry onto acct-02.
|
|
1487
|
+
cx_first_01() { cxlj 5 5 5 > "$CX/acct-01/limits.json"; cxlj 20 20 20 > "$CX/acct-02/limits.json"; }
|
|
1488
|
+
cx_first_01
|
|
1489
|
+
# rate limit: retry on the other account + self-expiring cooldown for the failed one
|
|
1490
|
+
echo "fail:acct-01" > "$FAKE_CTL2"
|
|
1491
|
+
out="$(codex exec "hello" < /dev/null 2>&1)"
|
|
1492
|
+
case "$out" in *CFG=acct-02*) t_ok "codex: exec retries a rate-limited account on another" ;;
|
|
1493
|
+
*) t_fail "codex retry" "did not land on acct-02: $out" ;; esac
|
|
1494
|
+
if [ -f "$CX/acct-01/.limited" ]; then
|
|
1495
|
+
grep -q "error-cooldown" "$CX/acct-01/.limited" \
|
|
1496
|
+
&& t_ok "codex: rate-limited account got a cooldown, not a park" \
|
|
1497
|
+
|| t_fail "codex cooldown" "marker is not an error-cooldown"
|
|
1498
|
+
else
|
|
1499
|
+
t_fail "codex cooldown" "no .limited cooldown written"
|
|
1500
|
+
fi
|
|
1501
|
+
rm -f "$CX/acct-01/.limited" "$FAKE_CTL2"
|
|
1502
|
+
# auth failure parks (soft) instead of a cooldown
|
|
1503
|
+
cx_first_01
|
|
1504
|
+
echo "authfail:acct-01" > "$FAKE_CTL2"
|
|
1505
|
+
codex exec "hello" < /dev/null >/dev/null 2>&1
|
|
1506
|
+
if [ -f "$CX/acct-01/.expired" ]; then
|
|
1507
|
+
grep -q "reason=auth-error" "$CX/acct-01/.expired" && grep -q "soft_until=" "$CX/acct-01/.expired" \
|
|
1508
|
+
&& t_ok "codex: auth failure parks the account with a soft stamp" \
|
|
1509
|
+
|| t_fail "codex auth park" "marker malformed: $(cat "$CX/acct-01/.expired")"
|
|
1510
|
+
else
|
|
1511
|
+
t_fail "codex auth park" "no .expired written after an auth failure"
|
|
1512
|
+
fi
|
|
1513
|
+
rm -f "$CX/acct-01/.expired" "$FAKE_CTL2"
|
|
1514
|
+
# org-disabled failure parks as org-blocked
|
|
1515
|
+
cx_first_01
|
|
1516
|
+
echo "orgfail:acct-01" > "$FAKE_CTL2"
|
|
1517
|
+
codex exec "hello" < /dev/null >/dev/null 2>&1
|
|
1518
|
+
grep -q "reason=org-blocked" "$CX/acct-01/.expired" 2>/dev/null \
|
|
1519
|
+
&& t_ok "codex: workspace-disabled failure parks as org-blocked" \
|
|
1520
|
+
|| t_fail "codex org park from run" "marker: $(cat "$CX/acct-01/.expired" 2>/dev/null || echo none)"
|
|
1521
|
+
rm -f "$CX/acct-01/.expired" "$FAKE_CTL2"
|
|
1522
|
+
# ordinary failure: exit code passes through, no retry, no marker
|
|
1523
|
+
out="$(codex exec --exit7 < /dev/null 2>&1)"
|
|
1524
|
+
rc=$?
|
|
1525
|
+
[ "$rc" = "7" ] && t_ok "codex: non-auth failure exit code passes through" || t_fail "codex rc passthrough" "rc=$rc"
|
|
1526
|
+
[ ! -f "$CX/acct-01/.limited" ] && [ ! -f "$CX/acct-02/.limited" ] \
|
|
1527
|
+
&& t_ok "codex: ordinary failure marks nothing" || t_fail "codex ordinary failure" "a marker appeared"
|
|
1528
|
+
# retry only engages for exec: a plain (interactive-style) run with a failing account
|
|
1529
|
+
# passes the failure straight through — no retry, no marker classification.
|
|
1530
|
+
printf 'fail:acct-01\nfail:acct-02\n' > "$FAKE_CTL2"
|
|
1531
|
+
out="$(codex "prompt" < /dev/null 2>&1)"
|
|
1532
|
+
rc=$?
|
|
1533
|
+
[ "$rc" = "1" ] && t_ok "codex: non-exec run is never retried (rc passes through)" \
|
|
1534
|
+
|| t_fail "codex non-exec retry" "rc=$rc"
|
|
1535
|
+
[ ! -f "$CX/acct-01/.limited" ] && [ ! -f "$CX/acct-02/.limited" ] \
|
|
1536
|
+
&& t_ok "codex: non-exec failure writes no markers" \
|
|
1537
|
+
|| t_fail "codex non-exec markers" "a marker appeared without the retry path"
|
|
1538
|
+
rm -f "$FAKE_CTL2" "$CX"/acct-*/.limited "$CX"/acct-*/.expired
|
|
1539
|
+
# stdin/stdout byte fidelity through the retry path
|
|
1540
|
+
printf 'line1\nline2\n' > "$WORK/cx-stdin.txt"
|
|
1541
|
+
out="$(codex exec --echo-stdin < "$WORK/cx-stdin.txt" 2>&1)"
|
|
1542
|
+
[ "$out" = "$(printf 'line1\nline2')" ] && t_ok "codex: stdin passes byte-identically through retry buffering" \
|
|
1543
|
+
|| t_fail "codex stdin fidelity" "got: $out"
|
|
1544
|
+
# user args survive selection
|
|
1545
|
+
out="$(codex exec "two words" --exit7 < /dev/null 2>&1)"
|
|
1546
|
+
rc=$?
|
|
1547
|
+
[ "$rc" = "7" ] && t_ok "codex: user args survive selection (pick_best untouched \$@)" \
|
|
1548
|
+
|| t_fail "codex args" "rc=$rc out=$out"
|
|
1549
|
+
# HOME unset: fail open into passthrough
|
|
1550
|
+
out="$(env -u HOME -u CODEX_ACCOUNTS_DIR PATH="$PATH" "$REPO_DIR/bin/codex" 2>&1)"
|
|
1551
|
+
check "codex: HOME unset fails open into passthrough" "CFG=none" "$out"
|
|
1552
|
+
rm -f "$CX"/acct-*/limits.json
|
|
1553
|
+
|
|
1554
|
+
# ---- C9. selection log --------------------------------------------------------------
|
|
1555
|
+
grep -qE 'acct-0[12]' "$CX/selection.log" && t_ok "codex: selection.log written" \
|
|
1556
|
+
|| t_fail "codex selection.log" "no selections logged"
|
|
1557
|
+
|
|
1558
|
+
# ---- C10. CLI: list / import / remove / dedupe --------------------------------------
|
|
1559
|
+
out="$(codex-accounts list 2>&1)"
|
|
1560
|
+
check "codex-accounts list shows accounts" "a@cx" "$out"
|
|
1561
|
+
check "codex-accounts list shows auth kind" "auth=chatgpt" "$out"
|
|
1562
|
+
mk_cx_auth "$WORK/cx-import.json" c@cx "$FUTURE_EXP"
|
|
1563
|
+
out="$(codex-accounts import c@cx --id acct-03 --auth "$WORK/cx-import.json" --no-sync 2>&1)"
|
|
1564
|
+
check "codex-accounts import" "Imported acct-03" "$out"
|
|
1565
|
+
[ -f "$CX/acct-03/auth.json" ] && t_ok "codex: imported auth.json in place" || t_fail "codex import" "auth.json missing"
|
|
1566
|
+
perm="$(ls -l "$CX/acct-03/auth.json" | cut -c1-10)"
|
|
1567
|
+
[ "$perm" = "-rw-------" ] && t_ok "codex: imported auth.json is 0600" || t_fail "codex import perms" "$perm"
|
|
1568
|
+
# duplicate email refused without --force / --id
|
|
1569
|
+
out="$(codex-accounts import c@cx --no-sync 2>&1)"
|
|
1570
|
+
rc=$?
|
|
1571
|
+
check "codex: duplicate import refused" "already registered as acct-03" "$out"
|
|
1572
|
+
[ "$rc" != "0" ] && t_ok "codex: duplicate import exits nonzero" || t_fail "codex dup import rc" "rc=0"
|
|
1573
|
+
# dedupe removes a forced duplicate, keeping the authed one
|
|
1574
|
+
codex-accounts import c@cx --id acct-04 --force --no-sync >/dev/null 2>&1
|
|
1575
|
+
out="$(codex-accounts dedupe --yes 2>&1)"
|
|
1576
|
+
check "codex: dedupe removes the duplicate" "Removed 1 duplicate" "$out"
|
|
1577
|
+
codex-accounts list 2>&1 | grep -q "acct-04" \
|
|
1578
|
+
&& t_fail "codex dedupe" "acct-04 still listed" || t_ok "codex: dedupe kept one entry per email"
|
|
1579
|
+
out="$(codex-accounts remove acct-03 --yes 2>&1)"
|
|
1580
|
+
check "codex-accounts remove" "Removed acct-03" "$out"
|
|
1581
|
+
[ ! -d "$CX/acct-03" ] && t_ok "codex: removed account dir deleted" || t_fail "codex remove" "dir still present"
|
|
1582
|
+
|
|
1583
|
+
# ---- C11. login-first add ceremony --------------------------------------------------
|
|
1584
|
+
# derived email: no argument, identity read back from the sign-in
|
|
1585
|
+
out="$(FAKE_EMAIL=new@cx codex-accounts add 2>&1)"
|
|
1586
|
+
check "codex: add with no email derives it from the sign-in" "Registered acct-03 for new@cx" "$out"
|
|
1587
|
+
grep -q '"email": "new@cx"' "$CX/accounts.json" && t_ok "codex: derived email registered" \
|
|
1588
|
+
|| t_fail "codex add derive" "manifest lacks new@cx"
|
|
1589
|
+
# signed in as a different account than named: register who actually authenticated
|
|
1590
|
+
out="$(FAKE_EMAIL=other@cx codex-accounts add named@cx 2>&1)"
|
|
1591
|
+
check "codex: mismatched sign-in registers the real account" "registering the account that actually authenticated" "$out"
|
|
1592
|
+
grep -q '"email": "other@cx"' "$CX/accounts.json" && t_ok "codex: actual identity registered" \
|
|
1593
|
+
|| t_fail "codex add mismatch" "manifest lacks other@cx"
|
|
1594
|
+
codex-accounts remove acct-04 --yes >/dev/null 2>&1
|
|
1595
|
+
# aborted login leaves zero traces
|
|
1596
|
+
before="$(ls "$CX" | sort)"
|
|
1597
|
+
out="$(FAKE_LOGIN_FAIL=1 codex-accounts add gone@cx 2>&1)"
|
|
1598
|
+
rc=$?
|
|
1599
|
+
[ "$rc" != "0" ] && t_ok "codex: aborted login exits nonzero" || t_fail "codex abort rc" "rc=0"
|
|
1600
|
+
after="$(ls "$CX" | sort)"
|
|
1601
|
+
[ "$before" = "$after" ] && t_ok "codex: aborted login leaves zero traces" \
|
|
1602
|
+
|| t_fail "codex abort traces" "pool dir changed: $after"
|
|
1603
|
+
grep -q "gone@cx" "$CX/accounts.json" && t_fail "codex abort manifest" "gone@cx registered" \
|
|
1604
|
+
|| t_ok "codex: aborted login registered nothing"
|
|
1605
|
+
# adding an already-registered email skips before sign-in
|
|
1606
|
+
out="$(codex-accounts add a@cx 2>&1)"
|
|
1607
|
+
rc=$?
|
|
1608
|
+
check "codex: add of registered email skips gracefully" "already added as acct-01" "$out"
|
|
1609
|
+
[ "$rc" = "0" ] && t_ok "codex: duplicate add exits 0 (no-op)" || t_fail "codex dup add rc" "rc=$rc"
|
|
1610
|
+
# signing in AS an already-registered email (unnamed) also refuses a second entry
|
|
1611
|
+
out="$(FAKE_EMAIL=a@cx codex-accounts add 2>&1)"
|
|
1612
|
+
check "codex: sign-in as registered email refused" "already added as acct-01" "$out"
|
|
1613
|
+
n="$(grep -c '"email": "a@cx"' "$CX/accounts.json")"
|
|
1614
|
+
[ "$n" = "1" ] && t_ok "codex: no second entry for a re-signed-in email" || t_fail "codex dup signin" "count=$n"
|
|
1615
|
+
|
|
1616
|
+
# ---- C12. login command + expired worklist + relogin --------------------------------
|
|
1617
|
+
# an account with no auth on this machine: login completes it
|
|
1618
|
+
codex-accounts import d@cx --id acct-04 --no-sync >/dev/null 2>&1
|
|
1619
|
+
out="$(codex-accounts expired 2>&1)"
|
|
1620
|
+
rc=$?
|
|
1621
|
+
check "codex: expired lists the auth-less account" "acct-04" "$out"
|
|
1622
|
+
[ "$rc" != "0" ] && t_ok "codex: expired exits 1 when a human is needed" || t_fail "codex expired rc" "rc=0"
|
|
1623
|
+
out="$(FAKE_EMAIL=d@cx codex-accounts login acct-04 2>&1)"
|
|
1624
|
+
check "codex: login completes an auth-less account" "login saved" "$out"
|
|
1625
|
+
[ -s "$CX/acct-04/auth.json" ] && t_ok "codex: login wrote auth.json" || t_fail "codex login" "no auth.json"
|
|
1626
|
+
# wrong sign-in identity is refused AND the prior credential is restored — the wrong
|
|
1627
|
+
# account's auth.json must never stay installed under this id (codex-review finding)
|
|
1628
|
+
out="$(FAKE_EMAIL=wrong@cx codex-accounts login acct-04 2>&1)"
|
|
1629
|
+
rc=$?
|
|
1630
|
+
check "codex: login refuses a wrong-account sign-in" "you signed in as wrong@cx" "$out"
|
|
1631
|
+
[ "$rc" != "0" ] && t_ok "codex: wrong-identity login exits nonzero" || t_fail "codex login identity rc" "rc=0"
|
|
1632
|
+
got_email="$(python3 - "$CX/acct-04/auth.json" <<'EOF'
|
|
1633
|
+
import base64, json, sys
|
|
1634
|
+
t = json.load(open(sys.argv[1]))['tokens']['id_token']
|
|
1635
|
+
p = t.split('.')[1]; p += '=' * (-len(p) % 4)
|
|
1636
|
+
print(json.loads(base64.urlsafe_b64decode(p)).get('email', ''))
|
|
1637
|
+
EOF
|
|
1638
|
+
)"
|
|
1639
|
+
[ "$got_email" = "d@cx" ] && t_ok "codex: refused login restores the prior credential" \
|
|
1640
|
+
|| t_fail "codex login restore" "auth.json now belongs to: $got_email"
|
|
1641
|
+
# the sign-in ceremony is DEVICE-CODE by default (the localhost browser callback
|
|
1642
|
+
# cannot reach a remote/SSH machine); --browser opts into the callback flow
|
|
1643
|
+
export FAKE_LOGIN_ARGS="$WORK/cx-login-args"
|
|
1644
|
+
: > "$FAKE_LOGIN_ARGS"
|
|
1645
|
+
FAKE_EMAIL=d@cx codex-accounts login acct-04 >/dev/null 2>&1
|
|
1646
|
+
rc=$?
|
|
1647
|
+
{ [ "$rc" = "0" ] && [ "$(cat "$FAKE_LOGIN_ARGS")" = "--device-auth" ]; } \
|
|
1648
|
+
&& t_ok "codex: sign-in uses the device-code flow by default" \
|
|
1649
|
+
|| t_fail "codex device default" "rc=$rc login args: '$(cat "$FAKE_LOGIN_ARGS")'"
|
|
1650
|
+
: > "$FAKE_LOGIN_ARGS"
|
|
1651
|
+
FAKE_EMAIL=d@cx codex-accounts login acct-04 --browser >/dev/null 2>&1
|
|
1652
|
+
rc=$?
|
|
1653
|
+
{ [ "$rc" = "0" ] && [ "$(cat "$FAKE_LOGIN_ARGS")" = "" ]; } \
|
|
1654
|
+
&& t_ok "codex: --browser opts into the localhost callback flow" \
|
|
1655
|
+
|| t_fail "codex --browser opt-out" "rc=$rc login args: '$(cat "$FAKE_LOGIN_ARGS")'"
|
|
1656
|
+
unset FAKE_LOGIN_ARGS
|
|
1657
|
+
|
|
1658
|
+
# a parked account shows in expired and relogin fixes exactly that
|
|
1659
|
+
printf '%s\nreason=refresh-denied-http-400 marked_at=t detail=x\n' "$now" > "$CX/acct-04/.expired"
|
|
1660
|
+
out="$(codex-accounts expired 2>&1)"
|
|
1661
|
+
check "codex: expired shows the parked account" "acct-04" "$out"
|
|
1662
|
+
out="$(FAKE_EMAIL=d@cx codex-accounts relogin --yes 2>&1)"
|
|
1663
|
+
check "codex: relogin re-authenticates the worklist" "re-authenticated 1 of 1" "$out"
|
|
1664
|
+
[ ! -f "$CX/acct-04/.expired" ] && t_ok "codex: relogin cleared the park" || t_fail "codex relogin" "park survived"
|
|
1665
|
+
# a relogin whose ceremony fails is NOT reported fixed: the dead credential stays
|
|
1666
|
+
# on disk, and success is judged by whether the credential can AUTHENTICATE.
|
|
1667
|
+
mk_cx_auth "$CX/acct-04/auth.json" d@cx 1000 norefresh
|
|
1668
|
+
out="$(FAKE_LOGIN_FAIL=1 FAKE_EMAIL=d@cx codex-accounts relogin --yes 2>&1)"
|
|
1669
|
+
rc=$?
|
|
1670
|
+
check "codex: failed relogin says still failing" "still failing" "$out"
|
|
1671
|
+
[ "$rc" != "0" ] && t_ok "codex: failed relogin exits nonzero" || t_fail "codex failed relogin rc" "rc=0"
|
|
1672
|
+
out="$(codex-accounts expired 2>&1)"
|
|
1673
|
+
check "codex: the un-fixed account is still on the worklist" "acct-04" "$out"
|
|
1674
|
+
# api-key-only auth is not subscription auth: audited as unusable, never selected
|
|
1675
|
+
mk_cx_auth "$CX/acct-04/auth.json" d@cx "$FUTURE_EXP" apikey
|
|
1676
|
+
out="$(codex-accounts expired 2>&1)"
|
|
1677
|
+
check "codex: api-key auth is called out as unsupported" "API key" "$out"
|
|
1678
|
+
out="$(CODEX_SHIM_SELECT=random codex 2>&1)"
|
|
1679
|
+
case "$out" in *CFG=acct-04*) t_fail "codex apikey selection" "api-key account was selected" ;;
|
|
1680
|
+
*) t_ok "codex: api-key account is never selected" ;; esac
|
|
1681
|
+
codex-accounts remove acct-04 --yes >/dev/null 2>&1
|
|
1682
|
+
|
|
1683
|
+
# ---- C13. limits: fixture endpoint, marking, classification -------------------------
|
|
1684
|
+
: > "$CX/.limits-kick" # re-arm the shim-kick throttle: no background limits racing these
|
|
1685
|
+
cat > "$WORK/cx-usage-high.json" <<EOF
|
|
1686
|
+
{"email":"a@cx","plan_type":"pro",
|
|
1687
|
+
"rate_limit":{"allowed":true,"limit_reached":false,
|
|
1688
|
+
"primary_window":{"used_percent":42,"limit_window_seconds":18000,"reset_at":$((now+3600))},
|
|
1689
|
+
"secondary_window":{"used_percent":57,"limit_window_seconds":604800,"reset_at":$((now+90000))}},
|
|
1690
|
+
"code_review_rate_limit":null,
|
|
1691
|
+
"additional_rate_limits":[
|
|
1692
|
+
{"limit_name":"GPT-5.3-Codex-Spark","rate_limit":{"allowed":true,"limit_reached":false,
|
|
1693
|
+
"primary_window":{"used_percent":12,"limit_window_seconds":18000,"reset_at":$((now+3600))},
|
|
1694
|
+
"secondary_window":{"used_percent":95,"limit_window_seconds":604800,"reset_at":$((now+90000))}}}]}
|
|
1695
|
+
EOF
|
|
1696
|
+
out="$(CODEX_MULTIACC_USAGE_URL="file://$WORK/cx-usage-high.json" codex-accounts limits 2>&1)"
|
|
1697
|
+
check "codex: >=90% model bucket marks the account" "LIMITED GPT-5.3-Codex-Spark:7d at 95%" "$out"
|
|
1698
|
+
[ -f "$CX/acct-01/.limited" ] && t_ok "codex: .limited marker written" || t_fail "codex limits marker" "missing"
|
|
1699
|
+
IFS= read -r first < "$CX/acct-01/.limited"
|
|
1700
|
+
[ "$first" = "$((now+90000))" ] && t_ok "codex: marker carries the offender's reset epoch" \
|
|
1701
|
+
|| t_fail "codex marker reset" "first line: $first"
|
|
1702
|
+
python3 - "$CX/acct-01/limits.json" <<'EOF'
|
|
1703
|
+
import json, sys
|
|
1704
|
+
d = json.load(open(sys.argv[1]))
|
|
1705
|
+
assert d['max_percent'] == 95, d['max_percent']
|
|
1706
|
+
assert d['weekly_percent'] == 95, d['weekly_percent']
|
|
1707
|
+
assert d['session_percent'] == 42, d['session_percent']
|
|
1708
|
+
names = {b['name']: (b['group'], b['percent']) for b in d['buckets']}
|
|
1709
|
+
assert names['5h'] == ('session', 42), names
|
|
1710
|
+
assert names['7d'] == ('weekly', 57), names
|
|
1711
|
+
assert names['GPT-5.3-Codex-Spark:5h'] == ('session', 12), names
|
|
1712
|
+
assert names['GPT-5.3-Codex-Spark:7d'] == ('weekly', 95), names
|
|
1713
|
+
EOF
|
|
1714
|
+
[ $? -eq 0 ] && t_ok "codex: buckets classified session/weekly with per-model names" \
|
|
1715
|
+
|| t_fail "codex bucket classification" "see assertions"
|
|
1716
|
+
# below-threshold refresh clears the marker
|
|
1717
|
+
cat > "$WORK/cx-usage-low.json" <<EOF
|
|
1718
|
+
{"email":"a@cx","plan_type":"pro",
|
|
1719
|
+
"rate_limit":{"allowed":true,"limit_reached":false,
|
|
1720
|
+
"primary_window":{"used_percent":5,"limit_window_seconds":18000,"reset_at":$((now+3600))},
|
|
1721
|
+
"secondary_window":{"used_percent":9,"limit_window_seconds":604800,"reset_at":$((now+90000))}},
|
|
1722
|
+
"additional_rate_limits":[]}
|
|
1723
|
+
EOF
|
|
1724
|
+
out="$(CODEX_MULTIACC_USAGE_URL="file://$WORK/cx-usage-low.json" codex-accounts limits 2>&1)"
|
|
1725
|
+
check "codex: below-threshold refresh clears the marker" "marker cleared" "$out"
|
|
1726
|
+
[ ! -f "$CX/acct-01/.limited" ] && t_ok "codex: marker gone after clean pass" || t_fail "codex marker clear" "still present"
|
|
1727
|
+
# a hard limit_reached verdict excludes even when no window is >= threshold
|
|
1728
|
+
cat > "$WORK/cx-usage-blocked.json" <<EOF
|
|
1729
|
+
{"email":"a@cx","plan_type":"pro",
|
|
1730
|
+
"rate_limit":{"allowed":false,"limit_reached":true,
|
|
1731
|
+
"primary_window":{"used_percent":50,"limit_window_seconds":18000,"reset_at":$((now+3600))}}}
|
|
1732
|
+
EOF
|
|
1733
|
+
out="$(CODEX_MULTIACC_USAGE_URL="file://$WORK/cx-usage-blocked.json" codex-accounts limits 2>&1)"
|
|
1734
|
+
check "codex: limit_reached without a >=90% window still marks" "LIMITED limit_reached at 100%" "$out"
|
|
1735
|
+
rm -f "$CX"/acct-*/.limited
|
|
1736
|
+
# reshaped payload: windows are still found by the recursive fallback
|
|
1737
|
+
cat > "$WORK/cx-usage-reshaped.json" <<EOF
|
|
1738
|
+
{"totally":{"new":{"shape":{"used_percent":97,"limit_window_seconds":604800,"reset_at":$((now+90000))}}}}
|
|
1739
|
+
EOF
|
|
1740
|
+
out="$(CODEX_MULTIACC_USAGE_URL="file://$WORK/cx-usage-reshaped.json" codex-accounts limits 2>&1)"
|
|
1741
|
+
check "codex: reshaped payload still tracked (fallback walk)" "LIMITED" "$out"
|
|
1742
|
+
rm -f "$CX"/acct-*/.limited
|
|
1743
|
+
# garbage payload degrades that account only, never the run
|
|
1744
|
+
printf 'this is not json' > "$WORK/cx-usage-garbage.json"
|
|
1745
|
+
out="$(CODEX_MULTIACC_USAGE_URL="file://$WORK/cx-usage-garbage.json" codex-accounts limits 2>&1)"
|
|
1746
|
+
rc=$?
|
|
1747
|
+
[ "$rc" = "0" ] && t_ok "codex: garbage payload fails open (rc=0)" || t_fail "codex garbage rc" "rc=$rc"
|
|
1748
|
+
check "codex: garbage payload logged as failure" "usage fetch failed" "$out"
|
|
1749
|
+
# fetch throttle: fresh data is not re-fetched
|
|
1750
|
+
out="$(CODEX_MULTIACC_MIN_FETCH=9999 CODEX_MULTIACC_USAGE_URL="file://$WORK/cx-usage-low.json" codex-accounts limits 2>&1)"
|
|
1751
|
+
printf '%s' "$out" | grep -q "acct-01" \
|
|
1752
|
+
&& t_fail "codex fetch throttle" "fresh account was re-fetched: $out" \
|
|
1753
|
+
|| t_ok "codex: fresh account skipped by the fetch throttle"
|
|
1754
|
+
# 429 backoff honored
|
|
1755
|
+
python3 - "$CX/acct-02/limits.json" "$now" <<'EOF'
|
|
1756
|
+
import json, sys
|
|
1757
|
+
json.dump({'fetched_at': 0, 'retry_after': int(sys.argv[2]) + 300, 'backoff': 120},
|
|
1758
|
+
open(sys.argv[1], 'w'))
|
|
1759
|
+
EOF
|
|
1760
|
+
out="$(CODEX_MULTIACC_USAGE_URL="file://$WORK/cx-usage-low.json" codex-accounts limits 2>&1)"
|
|
1761
|
+
check "codex: 429 backoff honored" "acct-02: backing off after 429" "$out"
|
|
1762
|
+
rm -f "$CX"/acct-*/limits.json
|
|
1763
|
+
|
|
1764
|
+
# ---- C14. oauth refresh via the token endpoint --------------------------------------
|
|
1765
|
+
# expired bearer, missing endpoint: fail open with backoff
|
|
1766
|
+
mk_cx_auth "$CX/acct-01/auth.json" a@cx 1000
|
|
1767
|
+
out="$(CODEX_MULTIACC_USAGE_URL="file://$WORK/cx-usage-low.json" codex-accounts limits 2>&1)"
|
|
1768
|
+
rc=$?
|
|
1769
|
+
check "codex: failed oauth refresh logged with backoff" "acct-01: oauth refresh failed" "$out"
|
|
1770
|
+
check "codex: expired bearer logged, not fatal" "acct-01: no fresh bearer" "$out"
|
|
1771
|
+
[ "$rc" = "0" ] && t_ok "codex: limits exits 0 with an expired-bearer account" || t_fail "codex refresh rc" "rc=$rc"
|
|
1772
|
+
[ -f "$CX/acct-01/.oauth-refresh.json" ] && t_ok "codex: refresh failure recorded for backoff" \
|
|
1773
|
+
|| t_fail "codex refresh backoff file" "missing"
|
|
1774
|
+
# a now-working endpoint is not retried inside the backoff window
|
|
1775
|
+
python3 - "$WORK/cx-token-ok.json" "$((now + 864000))" <<'EOF'
|
|
1776
|
+
import base64, json, sys
|
|
1777
|
+
def jwt(claims):
|
|
1778
|
+
enc = lambda o: base64.urlsafe_b64encode(json.dumps(o).encode()).rstrip(b'=').decode()
|
|
1779
|
+
return f"{enc({'alg':'RS256'})}.{enc(claims)}.sig"
|
|
1780
|
+
exp = float(sys.argv[2])
|
|
1781
|
+
auth_claim = {"chatgpt_plan_type": "pro", "chatgpt_account_id": "acct-uuid"}
|
|
1782
|
+
json.dump({"access_token": jwt({"exp": exp, "https://api.openai.com/auth": auth_claim, "marker": "REFRESHED"}),
|
|
1783
|
+
"refresh_token": "rt-rotated-new",
|
|
1784
|
+
"id_token": jwt({"email": "a@cx", "exp": exp, "https://api.openai.com/auth": auth_claim})},
|
|
1785
|
+
open(sys.argv[1], 'w'))
|
|
1786
|
+
EOF
|
|
1787
|
+
out="$(CODEX_MULTIACC_TOKEN_URL="file://$WORK/cx-token-ok.json" CODEX_MULTIACC_USAGE_URL="file://$WORK/cx-usage-low.json" codex-accounts limits 2>&1)"
|
|
1788
|
+
check "codex: refresh backoff honored (no early retry)" "acct-01: no fresh bearer" "$out"
|
|
1789
|
+
grep -q "rt-rotated-new" "$CX/acct-01/auth.json" \
|
|
1790
|
+
&& t_fail "codex backoff" "auth.json rewritten inside the backoff window" \
|
|
1791
|
+
|| t_ok "codex: no refresh inside the backoff window"
|
|
1792
|
+
# --force refreshes: rotated tokens persisted 0600, telemetry follows
|
|
1793
|
+
out="$(CODEX_MULTIACC_TOKEN_URL="file://$WORK/cx-token-ok.json" CODEX_MULTIACC_USAGE_URL="file://$WORK/cx-usage-low.json" codex-accounts limits --force 2>&1)"
|
|
1794
|
+
check "codex: --force refreshes the expired token" "acct-01: access token refreshed" "$out"
|
|
1795
|
+
check "codex: refreshed account fetches telemetry" "acct-01: ok" "$out"
|
|
1796
|
+
python3 - "$CX/acct-01/auth.json" <<'EOF'
|
|
1797
|
+
import base64, json, os, stat, sys
|
|
1798
|
+
doc = json.load(open(sys.argv[1]))
|
|
1799
|
+
t = doc['tokens']
|
|
1800
|
+
payload = t['access_token'].split('.')[1]
|
|
1801
|
+
payload += '=' * (-len(payload) % 4)
|
|
1802
|
+
claims = json.loads(base64.urlsafe_b64decode(payload))
|
|
1803
|
+
assert claims.get('marker') == 'REFRESHED', 'access token not replaced'
|
|
1804
|
+
assert t['refresh_token'] == 'rt-rotated-new', 'refresh token not rotated'
|
|
1805
|
+
assert doc.get('last_refresh', '').startswith('20'), 'last_refresh not stamped'
|
|
1806
|
+
mode = stat.S_IMODE(os.stat(sys.argv[1]).st_mode)
|
|
1807
|
+
assert mode == 0o600, oct(mode)
|
|
1808
|
+
EOF
|
|
1809
|
+
[ $? -eq 0 ] && t_ok "codex: rotated credential persisted with 0600" || t_fail "codex rotation" "see assertions"
|
|
1810
|
+
[ ! -f "$CX/acct-01/.oauth-refresh.json" ] && t_ok "codex: successful refresh clears the backoff file" \
|
|
1811
|
+
|| t_fail "codex refresh clear" "backoff file still present"
|
|
1812
|
+
grep -q "rt-rotated-new" "$CX/limits.log" \
|
|
1813
|
+
&& t_fail "codex token leak" "a refresh token leaked into limits.log" \
|
|
1814
|
+
|| t_ok "codex: limits.log leaks no tokens"
|
|
1815
|
+
# recently-expired token is left alone (a live codex session may own it) — even --force
|
|
1816
|
+
mk_cx_auth "$CX/acct-01/auth.json" a@cx "$((now - 100))"
|
|
1817
|
+
cp "$CX/acct-01/auth.json" "$WORK/cx-recent.bak"
|
|
1818
|
+
rm -f "$CX/acct-01/limits.json" "$CX/acct-01/.oauth-refresh.json"
|
|
1819
|
+
out="$(CODEX_MULTIACC_TOKEN_URL="file://$WORK/cx-token-ok.json" CODEX_MULTIACC_USAGE_URL="file://$WORK/cx-usage-low.json" codex-accounts limits --force 2>&1)"
|
|
1820
|
+
check "codex: recently-expired token not refreshed (even --force)" "acct-01: no fresh bearer" "$out"
|
|
1821
|
+
cmp -s "$CX/acct-01/auth.json" "$WORK/cx-recent.bak" \
|
|
1822
|
+
&& t_ok "codex: recently-expired credential left untouched" \
|
|
1823
|
+
|| t_fail "codex refresh gate" "credential rewritten inside the 5-min grace window"
|
|
1824
|
+
# no refresh token at all: parked with a clear reason
|
|
1825
|
+
mk_cx_auth "$CX/acct-01/auth.json" a@cx 1000 norefresh
|
|
1826
|
+
rm -f "$CX/acct-01/limits.json" "$CX/acct-01/.oauth-refresh.json"
|
|
1827
|
+
out="$(CODEX_MULTIACC_TOKEN_URL="file://$WORK/cx-token-ok.json" CODEX_MULTIACC_USAGE_URL="file://$WORK/cx-usage-low.json" codex-accounts limits --force 2>&1)"
|
|
1828
|
+
grep -q "reason=no-refresh-token" "$CX/acct-01/.expired" 2>/dev/null \
|
|
1829
|
+
&& t_ok "codex: refreshless expired credential is parked" \
|
|
1830
|
+
|| t_fail "codex no-refresh park" "no marker written"
|
|
1831
|
+
# a later successful authenticated fetch unparks it
|
|
1832
|
+
mk_cx_auth "$CX/acct-01/auth.json" a@cx "$FUTURE_EXP"
|
|
1833
|
+
rm -f "$CX/acct-01/limits.json"
|
|
1834
|
+
out="$(CODEX_MULTIACC_USAGE_URL="file://$WORK/cx-usage-low.json" codex-accounts limits --force 2>&1)"
|
|
1835
|
+
[ ! -f "$CX/acct-01/.expired" ] && t_ok "codex: a successful usage fetch clears the dead-auth marker" \
|
|
1836
|
+
|| t_fail "codex unpark" ".expired survived an authenticated fetch"
|
|
1837
|
+
# ...but never an org-blocked park (telemetry proves nothing about workspace policy)
|
|
1838
|
+
printf '%s\nreason=org-blocked marked_at=t detail=x\n' "$now" > "$CX/acct-01/.expired"
|
|
1839
|
+
rm -f "$CX/acct-01/limits.json"
|
|
1840
|
+
CODEX_MULTIACC_USAGE_URL="file://$WORK/cx-usage-low.json" codex-accounts limits --force >/dev/null 2>&1
|
|
1841
|
+
[ -f "$CX/acct-01/.expired" ] && t_ok "codex: a usage fetch does not unpark an org-blocked account" \
|
|
1842
|
+
|| t_fail "codex org unpark" "telemetry cleared an org block it cannot observe"
|
|
1843
|
+
rm -f "$CX/acct-01/.expired"
|
|
1844
|
+
# a 4xx from the token endpoint must not park the pool; invalid_grant is proof
|
|
1845
|
+
port=""
|
|
1846
|
+
python3 "$srv_script" > "$WORK/cx-token-port" 2>/dev/null &
|
|
1847
|
+
cx_srv_pid=$!
|
|
1848
|
+
for _ in $(seq 1 20); do
|
|
1849
|
+
port="$(head -1 "$WORK/cx-token-port" 2>/dev/null)"
|
|
1850
|
+
case "$port" in ''|*[!0-9]*) port=""; sleep 0.2 ;; *) break ;; esac
|
|
1851
|
+
done
|
|
1852
|
+
if [ -z "$port" ]; then
|
|
1853
|
+
kill "$cx_srv_pid" 2>/dev/null; wait "$cx_srv_pid" 2>/dev/null || true
|
|
1854
|
+
t_ok "codex: token-endpoint 4xx tests skipped (cannot bind a loopback port here)"
|
|
1855
|
+
else
|
|
1856
|
+
mk_cx_stale() {
|
|
1857
|
+
mk_cx_auth "$CX/acct-01/auth.json" a@cx 1000
|
|
1858
|
+
rm -f "$CX/acct-01/limits.json" "$CX/acct-01/.oauth-refresh.json" "$CX/acct-01/.expired"
|
|
1859
|
+
}
|
|
1860
|
+
mk_cx_stale
|
|
1861
|
+
CODEX_MULTIACC_TOKEN_URL="http://127.0.0.1:$port/boom" CODEX_MULTIACC_USAGE_URL="file://$WORK/cx-usage-low.json" codex-accounts limits --force >/dev/null 2>&1
|
|
1862
|
+
[ ! -f "$CX/acct-01/.expired" ] && t_ok "codex: one opaque 4xx does not park an account" \
|
|
1863
|
+
|| t_fail "codex 4xx park" "a single non-invalid_grant 400 parked the account"
|
|
1864
|
+
CODEX_MULTIACC_TOKEN_URL="http://127.0.0.1:$port/boom" CODEX_MULTIACC_USAGE_URL="file://$WORK/cx-usage-low.json" codex-accounts limits --force >/dev/null 2>&1
|
|
1865
|
+
CODEX_MULTIACC_TOKEN_URL="http://127.0.0.1:$port/boom" CODEX_MULTIACC_USAGE_URL="file://$WORK/cx-usage-low.json" codex-accounts limits --force >/dev/null 2>&1
|
|
1866
|
+
[ -f "$CX/acct-01/.expired" ] && t_ok "codex: a repeatedly-refused grant is parked on the third strike" \
|
|
1867
|
+
|| t_fail "codex 4xx strikes" "still not parked after three refusals"
|
|
1868
|
+
mk_cx_stale
|
|
1869
|
+
CODEX_MULTIACC_TOKEN_URL="http://127.0.0.1:$port/invalid-grant" CODEX_MULTIACC_USAGE_URL="file://$WORK/cx-usage-low.json" codex-accounts limits --force >/dev/null 2>&1
|
|
1870
|
+
grep -q "invalid_grant" "$CX/acct-01/.expired" 2>/dev/null \
|
|
1871
|
+
&& t_ok "codex: invalid_grant parks the account immediately" \
|
|
1872
|
+
|| t_fail "codex invalid_grant" "no marker for an explicit invalid_grant"
|
|
1873
|
+
kill "$cx_srv_pid" 2>/dev/null; wait "$cx_srv_pid" 2>/dev/null || true
|
|
1874
|
+
fi
|
|
1875
|
+
mk_cx_auth "$CX/acct-01/auth.json" a@cx "$FUTURE_EXP"
|
|
1876
|
+
rm -f "$CX/acct-01/.expired" "$CX/acct-01/.oauth-refresh.json" "$CX"/acct-*/limits.json
|
|
1877
|
+
# malformed tokens object (null) degrades that account only
|
|
1878
|
+
cp "$CX/acct-01/auth.json" "$WORK/cx-auth01.bak"
|
|
1879
|
+
printf '{"auth_mode":"chatgpt","tokens": null}' > "$CX/acct-01/auth.json"
|
|
1880
|
+
out="$(CODEX_MULTIACC_TOKEN_URL="file://$WORK/cx-token-ok.json" CODEX_MULTIACC_USAGE_URL="file://$WORK/cx-usage-low.json" codex-accounts limits --force 2>&1)"
|
|
1881
|
+
rc=$?
|
|
1882
|
+
[ "$rc" = "0" ] && t_ok "codex: null tokens object fails open (rc=0)" || t_fail "codex null tokens rc" "rc=$rc"
|
|
1883
|
+
check "codex: null tokens degrades only that account" "acct-01: no fresh bearer" "$out"
|
|
1884
|
+
[ -f "$CX/acct-02/limits.json" ] && t_ok "codex: accounts after a malformed one still refresh" \
|
|
1885
|
+
|| t_fail "codex fail-open loop" "acct-02 starved by acct-01's malformed auth"
|
|
1886
|
+
cp "$WORK/cx-auth01.bak" "$CX/acct-01/auth.json"
|
|
1887
|
+
rm -f "$CX"/acct-*/limits.json
|
|
1888
|
+
|
|
1889
|
+
# refresh is not attempted at all when the rotated credential could not be persisted
|
|
1890
|
+
# (the grant rotates the refresh token — consuming it and then failing to write
|
|
1891
|
+
# auth.json would strand the account; codex-review finding)
|
|
1892
|
+
mk_cx_auth "$CX/acct-01/auth.json" a@cx 1000
|
|
1893
|
+
rm -f "$CX/acct-01/limits.json" "$CX/acct-01/.oauth-refresh.json"
|
|
1894
|
+
chmod 500 "$CX/acct-01"
|
|
1895
|
+
out="$(CODEX_MULTIACC_TOKEN_URL="file://$WORK/cx-token-ok.json" CODEX_MULTIACC_USAGE_URL="file://$WORK/cx-usage-low.json" codex-accounts limits --force 2>&1)"
|
|
1896
|
+
chmod 700 "$CX/acct-01"
|
|
1897
|
+
check "codex: unpersistable credential skips the refresh grant" "refresh not attempted" "$out"
|
|
1898
|
+
grep -q "rt-rotated-new" "$CX/acct-01/auth.json" \
|
|
1899
|
+
&& t_fail "codex refresh preflight" "the grant was consumed despite an unwritable dir" \
|
|
1900
|
+
|| t_ok "codex: refresh grant not consumed when persistence would fail"
|
|
1901
|
+
mk_cx_auth "$CX/acct-01/auth.json" a@cx "$FUTURE_EXP"
|
|
1902
|
+
rm -f "$CX/acct-01/.oauth-refresh.json"
|
|
1903
|
+
|
|
1904
|
+
# two parallel imports get distinct ids and both land in the manifest (lock coverage;
|
|
1905
|
+
# codex-review finding)
|
|
1906
|
+
codex-accounts import p1@cx --no-sync >/dev/null 2>&1 &
|
|
1907
|
+
imp1=$!
|
|
1908
|
+
codex-accounts import p2@cx --no-sync >/dev/null 2>&1 &
|
|
1909
|
+
imp2=$!
|
|
1910
|
+
wait "$imp1"; wait "$imp2"
|
|
1911
|
+
n1="$(grep -c '"email": "p1@cx"' "$CX/accounts.json")"
|
|
1912
|
+
n2="$(grep -c '"email": "p2@cx"' "$CX/accounts.json")"
|
|
1913
|
+
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'), ''))")"
|
|
1914
|
+
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'), ''))")"
|
|
1915
|
+
{ [ "$n1" = "1" ] && [ "$n2" = "1" ] && [ -n "$id1" ] && [ -n "$id2" ] && [ "$id1" != "$id2" ]; } \
|
|
1916
|
+
&& t_ok "codex: parallel imports get distinct ids ($id1, $id2)" \
|
|
1917
|
+
|| t_fail "codex parallel import" "p1=$n1($id1) p2=$n2($id2)"
|
|
1918
|
+
codex-accounts remove "$id1" --yes >/dev/null 2>&1
|
|
1919
|
+
codex-accounts remove "$id2" --yes >/dev/null 2>&1
|
|
1920
|
+
|
|
1921
|
+
# ---- C15. verify --------------------------------------------------------------------
|
|
1922
|
+
out="$(codex-accounts verify --quick 2>&1)"
|
|
1923
|
+
check "codex: verify --quick passes chatgpt accounts" "acct-01 a@cx: OK" "$out"
|
|
1924
|
+
check "codex: verify --quick counts" "0 failure(s)" "$out"
|
|
1925
|
+
out="$(codex-accounts verify 2>&1)"
|
|
1926
|
+
check "codex: full verify PASS via the real-call matrix" "acct-01 a@cx: PASS" "$out"
|
|
1927
|
+
check "codex: full verify zero failures" "0 failure(s)" "$out"
|
|
1928
|
+
# verify parks a dead login it discovers, and a PASS clears an existing park
|
|
1929
|
+
echo "authfail:acct-02" > "$FAKE_CTL2"
|
|
1930
|
+
printf '%s\nreason=auth-error marked_at=t detail=x\n' "$now" > "$CX/acct-01/.expired"
|
|
1931
|
+
out="$(codex-accounts verify 2>&1)"
|
|
1932
|
+
check "codex: verify flags the dead login" "acct-02 b@cx: FAIL" "$out"
|
|
1933
|
+
check "codex: verify says what fixes it" "codex-accounts relogin acct-02" "$out"
|
|
1934
|
+
grep -q "reason=auth-error" "$CX/acct-02/.expired" 2>/dev/null \
|
|
1935
|
+
&& t_ok "codex: verify parks the dead login" || t_fail "codex verify park" "no marker"
|
|
1936
|
+
[ ! -f "$CX/acct-01/.expired" ] && t_ok "codex: a verify PASS clears an existing park" \
|
|
1937
|
+
|| t_fail "codex verify unpark" "PASS left the marker in place"
|
|
1938
|
+
rm -f "$FAKE_CTL2" "$CX/acct-02/.expired"
|
|
1939
|
+
# an org-disabled account is parked as org-blocked
|
|
1940
|
+
echo "orgfail:acct-02" > "$FAKE_CTL2"
|
|
1941
|
+
out="$(codex-accounts verify 2>&1)"
|
|
1942
|
+
check "codex: verify calls out the workspace block" "ORG BLOCKED" "$out"
|
|
1943
|
+
grep -q "reason=org-blocked" "$CX/acct-02/.expired" 2>/dev/null \
|
|
1944
|
+
&& t_ok "codex: verify parks the org-blocked account" || t_fail "codex verify org park" "no marker"
|
|
1945
|
+
rm -f "$FAKE_CTL2" "$CX/acct-02/.expired"
|
|
1946
|
+
|
|
1947
|
+
# ---- C16. audit vocabulary + empty pool ---------------------------------------------
|
|
1948
|
+
mkdir -p "$CX/acct-06"
|
|
1949
|
+
python3 - "$CX/accounts.json" <<'EOF'
|
|
1950
|
+
import json, sys
|
|
1951
|
+
doc = json.load(open(sys.argv[1]))
|
|
1952
|
+
doc['accounts'] = [a for a in doc['accounts'] if a['id'] != 'acct-06']
|
|
1953
|
+
doc['accounts'].append({'id': 'acct-06', 'email': 'srv@cx', 'home': 'server'})
|
|
1954
|
+
doc['accounts'].sort(key=lambda a: a['id'])
|
|
1955
|
+
json.dump(doc, open(sys.argv[1], 'w'), indent=2)
|
|
1956
|
+
EOF
|
|
1957
|
+
out="$(python3 "$REPO_DIR/lib/codex_audit.py" "$CX" linux | awk -F'\t' '$1=="acct-06"{print $4}')"
|
|
1958
|
+
[ "$out" = "missing" ] && t_ok "codex: home=server on the server means NO LOGIN, not 'elsewhere'" \
|
|
1959
|
+
|| t_fail "codex home vocabulary" "expected missing on linux, got: $out"
|
|
1960
|
+
out="$(python3 "$REPO_DIR/lib/codex_audit.py" "$CX" mac | awk -F'\t' '$1=="acct-06"{print $4}')"
|
|
1961
|
+
[ "$out" = "remote" ] && t_ok "codex: home=server on the Mac is correctly 'elsewhere'" \
|
|
1962
|
+
|| t_fail "codex home vocabulary" "expected remote on mac, got: $out"
|
|
1963
|
+
python3 - "$CX/accounts.json" <<'EOF'
|
|
1964
|
+
import json, sys
|
|
1965
|
+
doc = json.load(open(sys.argv[1]))
|
|
1966
|
+
doc['accounts'] = [a for a in doc['accounts'] if a['id'] != 'acct-06']
|
|
1967
|
+
json.dump(doc, open(sys.argv[1], 'w'), indent=2)
|
|
1968
|
+
EOF
|
|
1969
|
+
rm -rf "$CX/acct-06"
|
|
1970
|
+
cxemptypool="$WORK/cx-emptypool"
|
|
1971
|
+
mkdir -p "$cxemptypool/tmp"
|
|
1972
|
+
printf '{"version":1,"threshold":90,"accounts":[]}' > "$cxemptypool/accounts.json"
|
|
1973
|
+
out="$(CODEX_ACCOUNTS_DIR="$cxemptypool" codex-accounts expired 2>&1)"
|
|
1974
|
+
rc=$?
|
|
1975
|
+
check "codex: expired on an empty pool says so" "No accounts registered yet" "$out"
|
|
1976
|
+
[ "$rc" = "0" ] && t_ok "codex: empty pool exits 0" || t_fail "codex empty pool rc" "rc=$rc"
|
|
1977
|
+
|
|
1978
|
+
# ---- C17. security hardening --------------------------------------------------------
|
|
1979
|
+
mkdir -p "$WORK/cx-canary" && : > "$WORK/cx-canary/DO_NOT_DELETE"
|
|
1980
|
+
cp "$CX/accounts.json" "$WORK/cx-manifest.bak"
|
|
1981
|
+
python3 - "$CX/accounts.json" <<'EOF'
|
|
1982
|
+
import json, os, sys
|
|
1983
|
+
d = json.load(open(sys.argv[1]))
|
|
1984
|
+
d['accounts'].append({'id': '../cx-canary', 'email': 'evil@cx', 'home': 'mac'})
|
|
1985
|
+
json.dump(d, open(sys.argv[1] + '.tmp', 'w'), indent=2); os.replace(sys.argv[1] + '.tmp', sys.argv[1])
|
|
1986
|
+
EOF
|
|
1987
|
+
out="$(codex-accounts remove ../cx-canary --yes 2>&1)"
|
|
1988
|
+
rc=$?
|
|
1989
|
+
[ -f "$WORK/cx-canary/DO_NOT_DELETE" ] && t_ok "codex: remove refuses path-traversal id" \
|
|
1990
|
+
|| t_fail "codex path traversal" "remove ../cx-canary DELETED files outside the pool"
|
|
1991
|
+
[ "$rc" != "0" ] && t_ok "codex: traversal id rejected nonzero" || t_fail "codex traversal rc" "rc=0"
|
|
1992
|
+
codex-accounts list 2>&1 | grep -q "\.\./cx-canary" \
|
|
1993
|
+
&& t_fail "codex id filter" "traversal id surfaced" \
|
|
1994
|
+
|| t_ok "codex: invalid manifest ids are filtered out"
|
|
1995
|
+
cp "$WORK/cx-manifest.bak" "$CX/accounts.json"
|
|
1996
|
+
if [ "$(uname -s)" = "Darwin" ]; then
|
|
1997
|
+
python3 - "$CX/accounts.json" <<'EOF'
|
|
1998
|
+
import json, os, sys
|
|
1999
|
+
d = json.load(open(sys.argv[1]))
|
|
2000
|
+
d['server_root'] = "/tmp/x'; touch /tmp/cx_multiacc_PWNED; #"
|
|
2001
|
+
json.dump(d, open(sys.argv[1] + '.tmp', 'w'), indent=2); os.replace(sys.argv[1] + '.tmp', sys.argv[1])
|
|
2002
|
+
EOF
|
|
2003
|
+
rm -f /tmp/cx_multiacc_PWNED
|
|
2004
|
+
out="$(CODEX_MULTIACC_NO_SYNC=0 codex-accounts sync 2>&1)"
|
|
2005
|
+
[ ! -f /tmp/cx_multiacc_PWNED ] && t_ok "codex: sync rejects injected server_root" \
|
|
2006
|
+
|| { t_fail "codex command injection" "server_root injection EXECUTED"; rm -f /tmp/cx_multiacc_PWNED; }
|
|
2007
|
+
check "codex: injected server_root refused" "not a plain absolute path" "$out"
|
|
2008
|
+
cp "$WORK/cx-manifest.bak" "$CX/accounts.json"
|
|
2009
|
+
python3 - "$CX/accounts.json" <<'EOF'
|
|
2010
|
+
import json, os, sys
|
|
2011
|
+
d = json.load(open(sys.argv[1])); d['accounts'] = []
|
|
2012
|
+
json.dump(d, open(sys.argv[1] + '.tmp', 'w'), indent=2); os.replace(sys.argv[1] + '.tmp', sys.argv[1])
|
|
2013
|
+
EOF
|
|
2014
|
+
out="$(CODEX_MULTIACC_NO_SYNC=0 codex-accounts sync 2>&1)"
|
|
2015
|
+
rc=$?
|
|
2016
|
+
check "codex: empty manifest refuses to sync" "refusing to blank the server pool" "$out"
|
|
2017
|
+
[ "$rc" != "0" ] && t_ok "codex: empty-manifest sync exits nonzero" || t_fail "codex empty sync rc" "rc=0"
|
|
2018
|
+
cp "$WORK/cx-manifest.bak" "$CX/accounts.json"
|
|
2019
|
+
else
|
|
2020
|
+
t_ok "codex: sync validation tests skipped (Mac-only feature)"
|
|
2021
|
+
fi
|
|
2022
|
+
|
|
2023
|
+
# ---- C18. status + npm layer --------------------------------------------------------
|
|
2024
|
+
out="$(codex-accounts status 2>&1)"
|
|
2025
|
+
check "codex: status shows threshold" "90%" "$out"
|
|
2026
|
+
check "codex: status shows account" "a@cx" "$out"
|
|
2027
|
+
check "codex: status shows plan" "plan pro" "$out"
|
|
2028
|
+
if command -v node >/dev/null 2>&1; then
|
|
2029
|
+
out="$(node "$REPO_DIR/bin/cli.mjs" codex list 2>&1)"
|
|
2030
|
+
check "cli.mjs codex passthrough (list)" "a@cx" "$out"
|
|
2031
|
+
out="$(node "$REPO_DIR/bin/cli.mjs" --help 2>&1)"
|
|
2032
|
+
check "cli --help documents the codex pool" "codex-accounts" "$out"
|
|
2033
|
+
else
|
|
2034
|
+
t_ok "codex npm-layer tests skipped (node not installed)"
|
|
2035
|
+
fi
|
|
2036
|
+
|
|
1248
2037
|
# ---- summary ---------------------------------------------------------------------
|
|
1249
2038
|
echo
|
|
1250
2039
|
echo "passed: $PASS failed: $FAIL"
|