Ctrl K
ComunidadGuías…

test.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
'use strict';
// One runnable check for the whole project: `node test.js`.
// Runs each module's own self-check, then asserts the cross-module invariants that
// matter - no token ever leaves the process, and the swap never eats a config key.
const assert = require('node:assert');
const { execFileSync } = require('node:child_process');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');

let failures = 0;
function check(name, fn) {
  try {
    fn();
    console.log(`  ok   ${name}`);
  } catch (err) {
    failures++;
    console.error(`  FAIL ${name}\n       ${err.message}`);
  }
}

console.log('\nLLMSwapper self-check\n');

for (const mod of ['lib/paths.js', 'lib/store.js', 'lib/usage.js', 'lib/swap.js', 'lib/credentials.js', 'lib/targets.js', 'lib/terminal.js', 'lib/auto.js',
  'lib/codex/auth.js', 'lib/codex/store.js', 'lib/codex/targets.js', 'lib/codex/usage.js', 'lib/codex/swap.js', 'lib/codex/routes.js', 'lib/codex/paseo.js', 'lib/codex/daemon.js']) {
  check(`${mod} module self-check`, () => {
    execFileSync(process.execPath, [path.join(__dirname, mod)], { stdio: 'pipe', timeout: 30000 });
  });
}

const P = require('./lib/paths');
const usage = require('./lib/usage');
const swapLib = require('./lib/swap');
const oauth = require('./lib/oauth');

// The usage cache is written to disk, so without this the suite would trample the real
// data/usage-cache.json - including, now that it is persisted, the rate-limit cooldown.
const SANDBOX = fs.mkdtempSync(path.join(os.tmpdir(), 'swapper-data-'));
P.dataDir = () => SANDBOX;
P.backupsDir = () => path.join(SANDBOX, 'backups');
P.accountsPath = () => path.join(SANDBOX, 'accounts.json');

check('atomic write survives a corrupt-target refusal', () => {
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'swapper-t-'));
  const f = path.join(tmp, 'x.json');
  P.writeJsonAtomic(f, { a: 1 });
  assert.deepStrictEqual(P.readJsonFile(f), { a: 1 });
  fs.writeFileSync(f, '{ broken');
  assert.throws(() => P.readJsonFile(f), /valid JSON/);
  assert.strictEqual(fs.readdirSync(tmp).filter((n) => n.endsWith('.tmp')).length, 0);
  fs.rmSync(tmp, { recursive: true, force: true });
});

check('un ~/.claude.json montado como fichero en Docker (rename = EBUSY) se reescribe en el sitio', () => {
  // docker-compose monta ~/.claude.json como bind mount de UN fichero: es un punto de montaje y
  // rename() encima devuelve EBUSY siempre. Antes eso tumbaba el swap Y su rollback.
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'swapper-ebusy-'));
  const f = path.join(tmp, 'claude.json');
  fs.writeFileSync(f, JSON.stringify({ userID: 'KEEP', oauthAccount: { emailAddress: 'old@x' } }));
  const realRename = fs.renameSync;
  let renames = 0;
  fs.renameSync = () => { renames++; throw Object.assign(new Error('EBUSY: resource busy or locked'), { code: 'EBUSY' }); };
  try {
    P.writeJsonAtomic(f, { userID: 'KEEP', oauthAccount: { emailAddress: 'new@x' } });
  } finally {
    fs.renameSync = realRename;
  }
  assert.ok(renames > 1, 'primero reintenta el rename');
  assert.deepStrictEqual(P.readJsonFile(f), { userID: 'KEEP', oauthAccount: { emailAddress: 'new@x' } });
  assert.strictEqual(fs.readdirSync(tmp).filter((n) => n.endsWith('.tmp')).length, 0, 'sin restos .tmp');

  // Cualquier otro fallo del rename sigue siendo fatal: no se pisa el fichero a ciegas.
  fs.renameSync = () => { throw Object.assign(new Error('ENOSPC'), { code: 'ENOSPC' }); };
  try {
    assert.throws(() => P.writeJsonAtomic(f, { userID: 'OTHER' }), /ENOSPC/);
  } finally {
    fs.renameSync = realRename;
  }
  assert.strictEqual(P.readJsonFile(f).userID, 'KEEP');
  fs.rmSync(tmp, { recursive: true, force: true });
});

check('un fichero montado (otro device que su directorio) se escribe en el sitio, sin rename', () => {
  // Es lo que ve el servidor dentro de Docker: /home/node/.claude.json en un device y
  // /home/node en otro. Ahí el rename nunca va a funcionar, así que ni se intenta.
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'swapper-mount-'));
  const f = path.join(tmp, 'claude.json');
  fs.writeFileSync(f, JSON.stringify({ userID: 'KEEP', oauthAccount: { emailAddress: 'old@x' } }));
  const realStat = fs.statSync;
  const realRename = fs.renameSync;
  let renames = 0;
  const fakeMount = (nlink) => (p, ...rest) => {
    const st = realStat(p, ...rest);
    if (path.resolve(p) === path.resolve(f)) return Object.assign(st, { dev: st.dev + 1, nlink });
    return st;
  };
  fs.renameSync = (...a) => { renames++; return realRename(...a); };
  try {
    fs.statSync = fakeMount(1);
    assert.strictEqual(P.isMountedFile(f), true);
    assert.strictEqual(P.isDetachedMount(f), false);
    P.writeJsonAtomic(f, { userID: 'KEEP', oauthAccount: { emailAddress: 'new@x' } });
    assert.strictEqual(renames, 0, 'sin rename');
    fs.statSync = realStat;
    assert.deepStrictEqual(P.readJsonFile(f), { userID: 'KEEP', oauthAccount: { emailAddress: 'new@x' } });
    assert.strictEqual(fs.readdirSync(tmp).filter((n) => n.endsWith('.tmp')).length, 0, 'sin restos .tmp');

    // Host Linux: Claude Code renombró un fichero nuevo encima y el contenedor se quedó con el
    // inode viejo (nlink 0). Escribir ahí sería un swap "correcto" que el host nunca vería.
    fs.statSync = fakeMount(0);
    assert.strictEqual(P.isDetachedMount(f), true);
    assert.throws(() => P.writeJsonAtomic(f, { userID: 'LOST' }), /reinicia el contenedor/);
    fs.statSync = realStat;
    assert.strictEqual(P.readJsonFile(f).userID, 'KEEP', 'no se toca el fichero');
    assert.strictEqual(renames, 0);
  } finally {
    fs.statSync = realStat;
    fs.renameSync = realRename;
  }
  assert.strictEqual(P.isMountedFile(f), false, 'un fichero normal no es un punto de montaje');
  fs.rmSync(tmp, { recursive: true, force: true });
});

check('writeJsonAtomic refuses to write a non-object as a whole config', () => {
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'swapper-t-'));
  assert.throws(() => P.writeJsonAtomic(path.join(tmp, 'y.json'), undefined), /empty JSON/);
  fs.rmSync(tmp, { recursive: true, force: true });
});

check('scrub() removes tokens from anything headed for a log or a response', () => {
  // Assembled at runtime so this fixture cannot trip the "no hardcoded token" scan below.
  const dirty = `error: token sk-ant-${'oat'}01-AbC_dEf_1234567890 rejected`;
  assert.ok(!oauth.scrub(dirty).includes('AbC_dEf'));
  assert.ok(oauth.scrub(dirty).includes('sk-ant-***'));
  assert.strictEqual(oauth.scrub(null), '');
});

check('scrub() also redacts OpenAI JWTs and refresh tokens', () => {
  const b64 = (o) => Buffer.from(JSON.stringify(o)).toString('base64url');
  const jwt = `${b64({ alg: 'RS256' })}.${b64({ email: 'x@y.z' })}.${'s'.repeat(40)}`;
  const rt = `rt.A.${'q'.repeat(120)}`;
  const out = oauth.scrub(`bad ${jwt} and ${rt} end`);
  assert.ok(!out.includes(jwt) && !out.includes(rt), out);
  assert.match(out, /eyJ\*\*\*/);
  assert.strictEqual(oauth.scrub('sk-ant-oat01-abc'), 'sk-ant-***', 'Claude behaviour unchanged');
});

check('lib/usage y lib/targets exportan lo que reutiliza lib/codex', () => {
  const targets = require('./lib/targets');
  assert.deepStrictEqual(usage.meter(12, 'T'), { percent: 12, resetsAt: 'T', severity: 'normal' });
  assert.strictEqual(usage.num('7.25'), 7.3);
  for (const fn of ['runWsl', 'wslPath', 'uncBaseCandidates']) assert.strictEqual(typeof targets[fn], 'function', fn);
});

check('expires_in seconds becomes an absolute ms epoch', () => {
  const before = Date.now();
  const stored = oauth.toStoredOauth({ access_token: 'a', refresh_token: 'r', expires_in: 3600, scope: 'x y' });
  assert.ok(stored.expiresAt > before + 3500 * 1000, 'expiresAt must be ms in the future');
  assert.ok(stored.expiresAt < before + 3700 * 1000, 'expiresAt must not be seconds-as-ms');
  assert.deepStrictEqual(stored.scopes, ['x', 'y']);
});

check('normalize() handles the verified payload, legacy-only, and all-null', () => {
  const live = usage.normalize({
    limits: [
      { kind: 'session', percent: 90, resets_at: 'A' },
      { kind: 'weekly_all', percent: 28, resets_at: 'B' },
    ],
  }, 'i');
  assert.strictEqual(live.session.percent, 90);
  assert.strictEqual(live.weekly.percent, 28);

  const legacy = usage.normalize({ five_hour: { utilization: 12 }, seven_day: { utilization: 99 }, limits: [] }, 'i');
  assert.strictEqual(legacy.session.percent, 12);
  assert.strictEqual(legacy.weekly.severity, 'critical');

  const nothing = usage.normalize({}, 'i');
  assert.strictEqual(nothing.session.percent, 0);
  assert.ok(!Number.isNaN(nothing.weekly.percent));
});

check('swap preserves every unrelated key in a realistic .claude.json', () => {
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'swapper-cfg-'));
  const cj = path.join(tmp, '.claude.json');
  const original = {
    numStartups: 7,
    projects: { '/x': { allowedTools: [], history: ['a', 'b'] } },
    mcpServers: { s: { command: 'c', args: ['--x'] } },
    tipsHistory: { t: 3 },
    plugins: { installed: ['p1'] },
    userID: 'INSTALL-ID',
    machineID: 'MACHINE',
    oauthAccount: { emailAddress: 'old@x', accountUuid: 'u-old' },
    modelAccessCache: [1],
    hasAvailableSubscription: true,
  };
  fs.writeFileSync(cj, JSON.stringify(original, null, 2));

  swapLib.writeClaudeJson(cj, { accountUuid: 'u-new', emailAddress: 'new@x', displayName: 'N' });
  const after = JSON.parse(fs.readFileSync(cj, 'utf8'));

  for (const key of ['numStartups', 'projects', 'mcpServers', 'tipsHistory', 'plugins', 'userID', 'machineID']) {
    assert.deepStrictEqual(after[key], original[key], `${key} must survive the swap untouched`);
  }
  assert.strictEqual(after.oauthAccount.emailAddress, 'new@x');
  assert.ok(!('modelAccessCache' in after), 'stale cache must be dropped');
  assert.ok(!('hasAvailableSubscription' in after), 'stale cache must be dropped');
  fs.rmSync(tmp, { recursive: true, force: true });
});

check('swap refuses to touch a config it cannot parse', () => {
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'swapper-bad-'));
  const bad = path.join(tmp, '.claude.json');
  fs.writeFileSync(bad, 'not json at all');
  assert.throws(() => swapLib.writeClaudeJson(bad, { accountUuid: 'x' }));
  assert.strictEqual(fs.readFileSync(bad, 'utf8'), 'not json at all', 'the bad file must be left alone');
  fs.rmSync(tmp, { recursive: true, force: true });
});

check('credentials round-trip through the file backend', () => {
  // CLAUDE_CONFIG_DIR redirects paths.credentialsPath() and, on macOS, names a Keychain item
  // that cannot exist (lib/credentials suffixes the service with a hash of the directory, as
  // Claude Code does), so this never touches the real credentials on any platform.
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'swapper-cred-'));
  const previous = process.env.CLAUDE_CONFIG_DIR;
  process.env.CLAUDE_CONFIG_DIR = tmp;
  try {
    delete require.cache[require.resolve('./lib/credentials')];
    const credentials = require('./lib/credentials');

    assert.strictEqual(credentials.read(), null, 'nothing stored yet');

    const blob = {
      mcpOAuth: { 'srv|1': { serverName: 'srv', accessToken: 'keep' } },
      claudeAiOauth: { accessToken: 'A', refreshToken: 'R', expiresAt: 1, scopes: [] },
    };
    credentials.write(blob);

    const back = credentials.read();
    assert.deepStrictEqual(back, blob, 'what goes in must come out');
    assert.ok(back.mcpOAuth, 'mcpOAuth must survive a round-trip');

    // The swap mutation must preserve mcpOAuth when going through the backend.
    swapLib.writeCredentials(null, { accessToken: 'B', refreshToken: 'R2', expiresAt: 2, scopes: ['s'] });
    const after = credentials.read();
    assert.strictEqual(after.claudeAiOauth.accessToken, 'B');
    assert.strictEqual(after.mcpOAuth['srv|1'].accessToken, 'keep', 'mcpOAuth must not be clobbered');

    const backend = credentials.describeBackend();
    assert.ok(backend.kind === 'file' || backend.kind === 'keychain');
  } finally {
    if (previous === undefined) delete process.env.CLAUDE_CONFIG_DIR;
    else process.env.CLAUDE_CONFIG_DIR = previous;
    delete require.cache[require.resolve('./lib/credentials')];
    fs.rmSync(tmp, { recursive: true, force: true });
  }
});

check('the macOS branch degrades to the file backend, and a throwaway CLAUDE_CONFIG_DIR keeps it off the real Keychain', () => {
  // Forces the darwin path on whatever this really is. Where `security` does not exist
  // (or holds no item) the Keychain read must fail soft and fall back to the file -
  // this is the closest thing to macOS coverage without a Mac.
  //
  // Regresión con víctima (PR #1): el Keychain es global y write() lo prefería en cuanto
  // encontraba el item, así que en un Mac esta suite pisó las credenciales reales con tokens
  // de fixture. Claude Code nombra el item por CLAUDE_CONFIG_DIR (sufijo sha256) y ahora
  // nosotros también, de modo que un directorio de usar y tirar apunta a un item que no
  // existe. No basta con mirar qué backend sale elegido: se interceptan las llamadas a
  // `security` y se exige que todas lleven el sufijo, la cuenta de Claude Code, y que
  // ninguna escriba.
  const cp = require('node:child_process');
  const realExec = cp.execFileSync;
  const invoked = [];
  cp.execFileSync = (file, args, ...rest) => { invoked.push({ file, args }); return realExec(file, args, ...rest); };

  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'swapper-darwin-'));
  const realPlatform = process.platform;
  const previousDir = process.env.CLAUDE_CONFIG_DIR;
  process.env.CLAUDE_CONFIG_DIR = tmp;
  Object.defineProperty(process, 'platform', { value: 'darwin', configurable: true });
  try {
    delete require.cache[require.resolve('./lib/credentials')];
    const credentials = require('./lib/credentials');
    const suffix = require('node:crypto').createHash('sha256').update(tmp).digest('hex').slice(0, 8);
    assert.strictEqual(credentials.SERVICE, `Claude Code-credentials-${suffix}`, 'el item lleva el sufijo del config dir');
    assert.strictEqual(credentials.ACCOUNTS[0], 'claude-code-user', 'la cuenta que usa Claude Code 2.1+');

    assert.strictEqual(credentials.isMac(), true, 'must take the mac branch');
    assert.strictEqual(credentials.read(), null, 'no keychain and no file -> null, not a throw');

    const blob = { mcpOAuth: { a: 1 }, claudeAiOauth: { accessToken: 'X' } };
    assert.strictEqual(credentials.write(blob).kind, 'file', 'must fall back to the file');
    assert.deepStrictEqual(credentials.read(), blob);
    assert.ok(['file', 'keychain'].includes(credentials.describeBackend().kind));

    const sec = invoked.filter((c) => c.file === 'security');
    assert.ok(sec.length > 0, 'the mac branch must have asked the Keychain');
    for (const c of sec) {
      assert.strictEqual(c.args[0], 'find-generic-password', 'nunca escribe en el Keychain con un CLAUDE_CONFIG_DIR de usar y tirar');
      assert.ok(c.args.includes(`Claude Code-credentials-${suffix}`), 'cada lectura apunta al item con sufijo, nunca al real');
    }
  } finally {
    cp.execFileSync = realExec;
    Object.defineProperty(process, 'platform', { value: realPlatform, configurable: true });
    if (previousDir === undefined) delete process.env.CLAUDE_CONFIG_DIR;
    else process.env.CLAUDE_CONFIG_DIR = previousDir;
    delete require.cache[require.resolve('./lib/credentials')];
    fs.rmSync(tmp, { recursive: true, force: true });
  }
});

check('el keep-alive solo sincroniza la sesión viva si sigue siendo de esa cuenta', () => {
  // Regresión: el keep-alive escribía por RUTA (saltándose el Keychain en macOS) y decidía
  // por activeId, que durante un swap va por detrás de la realidad varios segundos.
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'swapper-sync-'));
  const previous = process.env.CLAUDE_CONFIG_DIR;
  process.env.CLAUDE_CONFIG_DIR = tmp;
  try {
    delete require.cache[require.resolve('./lib/credentials')];
    const credentials = require('./lib/credentials');
    credentials.write({
      mcpOAuth: { 'srv|1': { accessToken: 'keep' } },
      claudeAiOauth: { accessToken: 'A-acc', refreshToken: 'A-ref', expiresAt: 1, scopes: [] },
    });

    // Otra cuenta se ha adueñado de la sesión viva (un swap, o un /login a mano).
    const foreign = swapLib.syncLiveCredentials('B-ref', { accessToken: 'B2', refreshToken: 'B2r', expiresAt: 2 });
    assert.strictEqual(foreign, false, 'no debe escribir sobre una sesión que ya no es suya');
    assert.strictEqual(credentials.read().claudeAiOauth.accessToken, 'A-acc');

    // La sesión viva sigue siendo de A: el par rotado sí tiene que entrar, o el refresh
    // token que acaba de morir se queda como el único que conoce Claude Code.
    const own = swapLib.syncLiveCredentials('A-ref', {
      accessToken: 'A2-acc', refreshToken: 'A2-ref', expiresAt: 2, scopes: [],
    });
    assert.strictEqual(own, true);
    const after = credentials.read();
    assert.strictEqual(after.claudeAiOauth.accessToken, 'A2-acc');
    assert.strictEqual(after.claudeAiOauth.refreshToken, 'A2-ref');
    assert.strictEqual(after.mcpOAuth['srv|1'].accessToken, 'keep', 'mcpOAuth debe sobrevivir');
  } finally {
    if (previous === undefined) delete process.env.CLAUDE_CONFIG_DIR;
    else process.env.CLAUDE_CONFIG_DIR = previous;
    delete require.cache[require.resolve('./lib/credentials')];
    fs.rmSync(tmp, { recursive: true, force: true });
  }
});

check('el store adopta el par que Claude Code rotó por su cuenta, y solo si sabe de quién es', () => {
  // Claude Code renueva su propia sesión y el refresh rota: el par vivo avanza y la copia
  // del store muere. Sin esto, semanas después el keep-alive fallaba con invalid_grant y
  // la cuenta solo se recuperaba con un login, que es justo lo que la app evita.
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'swapper-adopt-'));
  const previous = process.env.CLAUDE_CONFIG_DIR;
  process.env.CLAUDE_CONFIG_DIR = tmp;
  const store = require('./lib/store');
  try {
    const profile = { accountUuid: 'uuid-live', emailAddress: 'live@x.com', displayName: 'Live' };
    const account = store.add({
      email: 'live@x.com', profile,
      oauth: { accessToken: 'VIEJO', refreshToken: 'R-VIEJO', expiresAt: 1, subscriptionType: 'max' },
    });
    store.setActive(account.id);
    const writeLive = (refreshToken) => P.writeJsonAtomic(P.credentialsPath(), {
      claudeAiOauth: { accessToken: 'NUEVO', refreshToken, expiresAt: 2, scopes: [] },
    }, 0o600);
    const claudeJson = (accountUuid) => P.writeJsonAtomic(P.claudeJsonPath(), { oauthAccount: { accountUuid } });

    // Sin deriva: el par vivo es el que ya tiene guardado, no hay nada que adoptar.
    writeLive('R-VIEJO');
    claudeJson('uuid-live');
    assert.strictEqual(swapLib.adoptLiveTokens(store), null, 'sin deriva no debe tocar nada');

    // Deriva, pero ~/.claude.json dice que la sesión es de OTRA cuenta: no se sabe de quién
    // es el par, así que no se escribe. Adoptarlo metería tokens ajenos en esta cuenta.
    writeLive('R-NUEVO');
    claudeJson('uuid-de-otro');
    assert.strictEqual(swapLib.adoptLiveTokens(store), null, 'identidad no corroborada: no adoptar');
    assert.strictEqual(store.get(account.id).oauth.refreshToken, 'R-VIEJO');

    // Deriva y las dos fuentes coinciden: solo se movieron los tokens. Se adopta.
    claudeJson('uuid-live');
    assert.strictEqual(swapLib.adoptLiveTokens(store), account.id);
    const after = store.get(account.id).oauth;
    assert.strictEqual(after.refreshToken, 'R-NUEVO');
    assert.strictEqual(after.accessToken, 'NUEVO');
    assert.strictEqual(after.subscriptionType, 'max', 'lo que el store sabía de más debe sobrevivir');

    // Mount desprendido (Linux, ver paths.js): ~/.claude.json es una copia congelada que dirá
    // "uuid-live" para siempre, mientras el host puede haber hecho login con OTRA cuenta. No se
    // adopta nada: injertaría el par de esa otra cuenta en esta y perdería el suyo.
    writeLive('R-DE-OTRO-LOGIN');
    const realStat = fs.statSync;
    fs.statSync = (p, ...rest) => {
      const st = realStat(p, ...rest);
      return path.resolve(p) === path.resolve(P.claudeJsonPath()) ? Object.assign(st, { nlink: 0 }) : st;
    };
    try {
      assert.strictEqual(swapLib.adoptLiveTokens(store), null, 'copia congelada: no adoptar');
    } finally {
      fs.statSync = realStat;
    }
    assert.strictEqual(store.get(account.id).oauth.refreshToken, 'R-NUEVO', 'el par guardado no se toca');

    // Un par vivo MÁS VIEJO que el guardado (una renovación cuya escritura en el fichero falló) no
    // se adopta: su refresh token es el muerto. Se cura el fichero con el par del store.
    P.writeJsonAtomic(P.credentialsPath(), { claudeAiOauth: { accessToken: 'VIEJO-2', refreshToken: 'R-MUERTO', expiresAt: 1, scopes: [] } }, 0o600);
    assert.strictEqual(swapLib.adoptLiveTokens(store), null, 'un par más viejo no se adopta');
    assert.strictEqual(store.get(account.id).oauth.refreshToken, 'R-NUEVO', 'el par guardado sigue');
    assert.strictEqual(JSON.parse(fs.readFileSync(P.credentialsPath(), 'utf8')).claudeAiOauth.refreshToken, 'R-NUEVO', 'y el fichero lo recupera');
  } finally {
    if (previous === undefined) delete process.env.CLAUDE_CONFIG_DIR;
    else process.env.CLAUDE_CONFIG_DIR = previous;
    fs.rmSync(P.accountsPath(), { force: true });
    fs.rmSync(tmp, { recursive: true, force: true });
  }
});

check('la rotación automática persiste su estado y clampa el umbral', () => {
  const auto = require('./lib/auto');
  try {
    auto.set({ enabled: true, threshold: 85 });
    let s = auto.load();
    assert.strictEqual(s.enabled, true);
    assert.strictEqual(s.threshold, 85);
    auto.set({ threshold: 999 });          // fuera de rango -> clamp a 100
    assert.strictEqual(auto.load().threshold, 100);
    auto.set({ enabled: false });
    assert.strictEqual(auto.load().enabled, false, 'apagar debe persistir');
  } finally {
    auto.set({ enabled: false, threshold: 90 }); // no dejar el sandbox con auto encendido
  }
});

check('un swap a un target de fichero (WSL) escribe en SUS ficheros, no en los del host', () => {
  // Un target WSL es exactamente esto: fileBackend + dos rutas propias. Simulado con un
  // directorio temporal - misma mecánica que las rutas UNC \\wsl.localhost\... reales.
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'swapper-wsl-'));
  const target = {
    id: 'wsl:test', kind: 'wsl', label: 'WSL · test',
    claudeJsonPath: path.join(tmp, '.claude.json'),
    credentialsPath: path.join(tmp, '.claude', '.credentials.json'),
    fileBackend: true,
  };
  fs.mkdirSync(path.join(tmp, '.claude'));
  fs.writeFileSync(target.credentialsPath, JSON.stringify({
    mcpOAuth: { 'srv|1': { accessToken: 'keep' } },
    claudeAiOauth: { accessToken: 'OLD', refreshToken: 'OLDR', expiresAt: 1, scopes: [] },
  }, null, 2));
  fs.writeFileSync(target.claudeJsonPath, JSON.stringify({
    userID: 'WSL-INSTALL-ID', projects: { '/x': { history: [1] } },
    oauthAccount: { emailAddress: 'old@wsl', accountUuid: 'old-uuid' },
    modelAccessCache: [1],
  }, null, 2));

  swapLib.writeCredentials(target, { accessToken: 'NEW', refreshToken: 'NEWR', expiresAt: 2, scopes: ['s'], subscriptionType: 'max' });
  swapLib.writeClaudeJson(target.claudeJsonPath, { accountUuid: 'new-uuid', emailAddress: 'new@wsl', displayName: 'N' });

  const cred = JSON.parse(fs.readFileSync(target.credentialsPath, 'utf8'));
  assert.strictEqual(cred.claudeAiOauth.accessToken, 'NEW', 'la credencial del target se actualiza');
  assert.strictEqual(cred.mcpOAuth['srv|1'].accessToken, 'keep', 'mcpOAuth del target sobrevive');
  const cj = JSON.parse(fs.readFileSync(target.claudeJsonPath, 'utf8'));
  assert.strictEqual(cj.oauthAccount.emailAddress, 'new@wsl');
  assert.strictEqual(cj.userID, 'WSL-INSTALL-ID', 'userID del target intacto');
  assert.ok(!('modelAccessCache' in cj), 'caché stale del target descartada');

  // Backup + restore contra ese mismo target vuelve a dejarlo como estaba.
  const backup = swapLib.backupNow('acc_x', target);
  swapLib.writeClaudeJson(target.claudeJsonPath, { accountUuid: 'z', emailAddress: 'z@z', displayName: 'Z' });
  swapLib.restoreFrom(backup.dir, target);
  const back = JSON.parse(fs.readFileSync(target.claudeJsonPath, 'utf8'));
  assert.strictEqual(back.oauthAccount.emailAddress, 'new@wsl', 'restore del target vuelve al estado respaldado');

  fs.rmSync(tmp, { recursive: true, force: true });
  fs.rmSync(backup.dir, { recursive: true, force: true });
});

check('un target que no resuelve no cae en silencio al host', () => {
  // swap.js tomaba `targets.resolve(t) || hostTarget()`: un distro parado, o un id mal escrito
  // desde una skill, reescribía las credenciales del HOST y devolvía ok.
  assert.throws(() => swapLib.asTarget('wsl:no-existe'), /Target desconocido/);
  assert.strictEqual(swapLib.asTarget('host').id, 'host');
  assert.strictEqual(swapLib.asTarget(undefined).id, 'host');
});

check('el rollback restaura ~/.claude.json con escritura atómica, no copyFileSync', () => {
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'swapper-rb-'));
  const previous = process.env.CLAUDE_CONFIG_DIR;
  process.env.CLAUDE_CONFIG_DIR = tmp;
  try {
    const backupDir = path.join(tmp, 'backup');
    fs.mkdirSync(backupDir);
    const original = { userID: 'KEEP', projects: { '/x': { history: [1] } }, oauthAccount: { emailAddress: 'old@x' } };
    fs.writeFileSync(path.join(backupDir, 'claude.json'), JSON.stringify(original, null, 2));
    fs.writeFileSync(P.claudeJsonPath(), '{"oauthAccount":{"emailAddress":"new@x"}}');

    const restored = swapLib.restoreFrom(backupDir);
    assert.ok(restored.includes(P.claudeJsonPath()));
    assert.deepStrictEqual(P.readJsonFile(P.claudeJsonPath()), original);
    assert.strictEqual(fs.readdirSync(tmp).filter((n) => n.endsWith('.tmp')).length, 0, 'sin restos .tmp');

    // Si la config viva sigue siendo byte a byte el backup (la escritura se rechazó antes de
    // tocarla), no hay nada que restaurar: ni se escribe ni se dice que falló la restauración.
    fs.copyFileSync(path.join(backupDir, 'claude.json'), P.claudeJsonPath());
    const realWrite = P.writeJsonAtomic;
    P.writeJsonAtomic = (p) => { throw new Error(`no debería escribir ${p}`); };
    try {
      assert.deepStrictEqual(swapLib.restoreFrom(backupDir), []);
    } finally {
      P.writeJsonAtomic = realWrite;
    }

    // Y un backup corrupto no puede llevarse por delante la configuración viva: la ruta
    // atómica lo rechaza antes de abrir el destino. copyFileSync lo habría copiado encima,
    // que es la misma ventana por la que un fallo a mitad de copia dejaba un fragmento.
    const live = fs.readFileSync(P.claudeJsonPath(), 'utf8');
    fs.writeFileSync(path.join(backupDir, 'claude.json'), '{ roto');
    assert.throws(() => swapLib.restoreFrom(backupDir), /JSON/);
    assert.strictEqual(fs.readFileSync(P.claudeJsonPath(), 'utf8'), live, 'la config viva debe quedar intacta');
  } finally {
    if (previous === undefined) delete process.env.CLAUDE_CONFIG_DIR;
    else process.env.CLAUDE_CONFIG_DIR = previous;
    fs.rmSync(tmp, { recursive: true, force: true });
  }
});

check('el suelo de ritmo deja como mucho 4 peticiones en la ventana de 300 s', () => {
  // Lo que importa no es la tasa media sino cuántas caben en la ventana del endpoint:
  // con un hueco g son floor(300/g)+1, y la quinta es la que devuelve 429.
  const perWindow = Math.floor((300 * 1000) / usage.MIN_GAP_MS) + 1;
  assert.ok(perWindow <= 4, `MIN_GAP_MS=${usage.MIN_GAP_MS}ms permite ${perWindow} peticiones por ventana`);
});

check('hardenDataDir deja constancia aunque icacls falle', () => {
  // ponytail: fuera de Windows la función no hace nada, así que no hay nada que probar.
  if (process.platform !== 'win32') return;
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'swapper-acl-'));
  const realRoot = process.env.SystemRoot;
  process.env.SystemRoot = path.join(tmp, 'no-such-windows');
  try {
    P.hardenDataDir(tmp);
    const marker = path.join(tmp, '.acl-applied');
    assert.ok(fs.existsSync(marker), 'sin marcador, ensureDirs() relanza icacls en cada lectura');
    assert.match(fs.readFileSync(marker, 'utf8'), /NOT applied/, 'el fallo debe quedar escrito');
  } finally {
    if (realRoot === undefined) delete process.env.SystemRoot;
    else process.env.SystemRoot = realRoot;
    fs.rmSync(tmp, { recursive: true, force: true });
  }
});

check('detectClaudeProcesses never throws', () => {
  const r = swapLib.detectClaudeProcesses();
  assert.strictEqual(typeof r.running, 'boolean');
  assert.ok(Array.isArray(r.pids));
});

check('[hidden] beats any class rule that sets display', () => {
  // Regression: a .modal-backdrop{display:grid} rule outranked the UA [hidden] rule,
  // leaving the empty state and the banners permanently visible.
  const css = fs.readFileSync(path.join(__dirname, 'public', 'style.css'), 'utf8');
  assert.match(css, /\[hidden\]\s*\{\s*display:\s*none\s*!important/, 'style.css must force [hidden] to none');
});

check('no source file hardcodes a token', () => {
  const walk = (dir) => fs.readdirSync(dir, { withFileTypes: true }).flatMap((d) => {
    if (d.name === 'node_modules' || d.name === 'data' || d.name === '.git') return [];
    const full = path.join(dir, d.name);
    return d.isDirectory() ? walk(full) : /\.(js|mjs|html|css|md)$/.test(d.name) ? [full] : [];
  });
  for (const file of walk(__dirname)) {
    const text = fs.readFileSync(file, 'utf8');
    const hit = text.match(/sk-ant-(oat|ort)01-[A-Za-z0-9_-]{10,}/)
      // An OpenAI access or id token (Codex). Fixtures build theirs at runtime.
      || text.match(/eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}/);
    assert.ok(!hit, `${path.relative(__dirname, file)} contains what looks like a real token`);
  }
});

check('las tres skills llevan el mismo swapper.mjs', () => {
  // Se editan en bloque (seis de los últimos quince commits tocaron las tres) y nada lo garantizaba.
  const [a, b, c] = ['swapper', 'swapper-usage', 'swapper-auto']
    .map((sk) => fs.readFileSync(path.join(__dirname, 'skills', sk, 'swapper.mjs'), 'utf8'));
  assert.strictEqual(a, b, 'swapper-usage/swapper.mjs difiere de swapper/swapper.mjs');
  assert.strictEqual(a, c, 'swapper-auto/swapper.mjs difiere de swapper/swapper.mjs');
});

check('un login de Claude tiene un final fijo: la fila lo avisa y lo marca cuando llega', () => {
  // Medido: refresh_expires_in cuenta atrás hasta el mismo instante en cada renovación.
  const store = require('./lib/store');
  const now = Date.now();
  const mk = (refreshTokenExpiresAt) => store.add({
    email: `s${refreshTokenExpiresAt}@x.y`, profile: { accountUuid: `uuid-s-${refreshTokenExpiresAt}` },
    oauth: { accessToken: 'A', refreshToken: 'R', expiresAt: now + 3600e3, refreshTokenExpiresAt, scopes: ['user:profile'] },
  });
  const over = mk(now - 1000);
  const soon = mk(now + 86400e3);
  try {
    const view = store.publicView('host').accounts;
    const vo = view.find((a) => a.id === over.id), vs = view.find((a) => a.id === soon.id);
    assert.strictEqual(vo.tokenExpired, true, 'past its end, the login is over even with a live access token');
    assert.strictEqual(vs.tokenExpired, false);
    assert.strictEqual(vs.sessionEndsAt, now + 86400e3, 'the end travels to the row, for the warning');
    assert.ok(!JSON.stringify(view).includes('"R"'), 'still no token in the view');
  } finally {
    store.remove(over.id);
    store.remove(soon.id);
  }
});

async function checkAsync(name, fn) {
  try {
    await fn();
    console.log(`  ok   ${name}`);
  } catch (err) {
    failures++;
    console.error(`  FAIL ${name}\n       ${err.message}`);
  }
}

(async () => {
  await checkAsync('con el mount desprendido, swapTo rechaza ANTES de hacer backup o tocar credenciales', async () => {
    const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'swapper-detached-'));
    const previous = process.env.CLAUDE_CONFIG_DIR;
    process.env.CLAUDE_CONFIG_DIR = tmp;
    delete require.cache[require.resolve('./lib/credentials')];
    const store = require('./lib/store');
    try {
      fs.writeFileSync(P.credentialsPath(), '{"claudeAiOauth":{"accessToken":"HOST","refreshToken":"R-HOST"}}');
      fs.writeFileSync(P.claudeJsonPath(), '{"oauthAccount":{"accountUuid":"uuid-host"}}');
      const account = store.add({
        email: 'next@x.com', profile: { accountUuid: 'uuid-next', emailAddress: 'next@x.com' },
        oauth: { accessToken: 'NEXT', refreshToken: 'R-NEXT', expiresAt: Date.now() + 3600e3 },
      });
      const backupsBefore = fs.existsSync(P.backupsDir()) ? fs.readdirSync(P.backupsDir()).length : 0;
      const credsBefore = fs.readFileSync(P.credentialsPath());
      const realStat = fs.statSync;
      fs.statSync = (p, ...rest) => {
        const st = realStat(p, ...rest);
        return path.resolve(p) === path.resolve(P.claudeJsonPath()) ? Object.assign(st, { nlink: 0 }) : st;
      };
      try {
        await assert.rejects(swapLib.swapTo(account.id, { store, oauth: {}, usage: {} }), (err) => {
          assert.match(err.message, /reinicia el contenedor/);
          assert.doesNotMatch(err.message, /RESTAURACI/, 'no hubo nada que restaurar, y no debe decir lo contrario');
          return true;
        });
      } finally {
        fs.statSync = realStat;
      }
      const backupsAfter = fs.existsSync(P.backupsDir()) ? fs.readdirSync(P.backupsDir()).length : 0;
      assert.strictEqual(backupsAfter, backupsBefore, 'sin backup nuevo');
      assert.ok(fs.readFileSync(P.credentialsPath()).equals(credsBefore), 'credenciales intactas');
    } finally {
      if (previous === undefined) delete process.env.CLAUDE_CONFIG_DIR;
      else process.env.CLAUDE_CONFIG_DIR = previous;
      delete require.cache[require.resolve('./lib/credentials')];
      fs.rmSync(tmp, { recursive: true, force: true });
    }
  });

  await checkAsync('dos swaps a la vez: el segundo se rechaza en vez de entrelazarse con el primero', async () => {
    // El monitor de rotación y un swap manual pueden coincidir; sin esto ambos hacían
    // backup/escritura/verificación/rollback sobre los mismos dos ficheros.
    const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'swapper-lock-'));
    const target = {
      id: 'host', kind: 'host', label: 'test',
      claudeJsonPath: path.join(tmp, '.claude.json'),
      credentialsPath: path.join(tmp, '.credentials.json'),
      fileBackend: true,
    };
    fs.writeFileSync(target.credentialsPath, JSON.stringify({ claudeAiOauth: { accessToken: 'OLD', refreshToken: 'R', expiresAt: 1, scopes: ['s'] } }));
    fs.writeFileSync(target.claudeJsonPath, JSON.stringify({ oauthAccount: { accountUuid: 'old' } }));
    const account = {
      id: 'acc_lock', label: 'Lock', email: null,
      oauth: { accessToken: 'NEW', refreshToken: null, expiresAt: Date.now() + 1e9, scopes: ['user:inference'] },
    };
    const store = { get: (id) => (id === account.id ? account : null), setActive() {}, canReadUsage: () => false, publicAccount: (x) => ({ id: x.id, label: x.label }) };
    let release;
    const gate = new Promise((r) => { release = r; });
    const deps = {
      store,
      usage: { invalidate() {}, prime: (_, v) => v, normalize: (raw) => raw, fetchRaw: async () => ({}) },
      oauth: { probeToken: async () => { await gate; return { kind: 'inference' }; } },
    };
    try {
      const first = swapLib.swapTo(account.id, deps, target);
      // The lock is taken synchronously, so the second call is refused even with the gate
      // already open - and it has to be open: without the lock the second swap would park on
      // it and the suite would hang instead of failing.
      release();
      await assert.rejects(swapLib.swapTo(account.id, deps, target), /en curso/);
      assert.strictEqual((await first).ok, true);
      // Y una vez terminado, el siguiente vuelve a entrar.
      assert.strictEqual((await swapLib.swapTo(account.id, deps, target)).ok, true);
    } finally {
      fs.rmSync(tmp, { recursive: true, force: true });
    }
  });

  await checkAsync('la rotación automática excluye un token rechazado y no reintenta cada tick un swap fallido', async () => {
    const auto = require('./lib/auto');
    auto.save({ enabled: true, target: 'host', threshold: 90, lastSwapAt: 0 });
    const live = { accessToken: 't', expiresAt: Date.now() + 1e6 };
    const accounts = [
      { id: 'cur', label: 'Cur', oauth: { ...live } },
      { id: 'dead', label: 'Dead', oauth: { ...live } },
      { id: 'sana', label: 'Sana', oauth: { ...live } },
    ];
    const readings = {
      cur: { ok: true, session: { percent: 95 }, weekly: { percent: 10 } },
      // 401 al leer su uso: credencial muerta. Antes contaba como "uso desconocido" y era
      // elegible, así que el monitor rotaba a ella, el verify fallaba, y vuelta a empezar.
      dead: { ok: false, status: 401, needsRelogin: true },
      sana: { ok: true, session: { percent: 5 }, weekly: { percent: 5 } },
    };
    const store = { list: () => accounts, get: (id) => accounts.find((a) => a.id === id) || null, activeFor: () => 'cur' };
    const usage = {
      fetchFor: async (a) => readings[a.id],
      fetchAll: async (list) => Object.fromEntries(list.map((a) => [a.id, readings[a.id]])),
      cachedFor: (id) => readings[id],
    };
    const calls = [];
    const swap = {
      asTarget: () => ({ claudeJsonPath: path.join(SANDBOX, 'no-existe.json') }),
      swapTo: async (id) => { calls.push(id); throw new Error('verify falló'); },
    };
    try {
      // Only a dead account to rotate into: nothing is tried, swapTo is not even called.
      const onlyDead = { ...store, list: () => accounts.filter((a) => a.id !== 'sana') };
      const r = await auto.tick({ store: onlyDead, usage, swap });
      assert.strictEqual(r && r.rotated, false, 'un token rechazado no es candidato');
      assert.deepStrictEqual(calls, [], 'ni siquiera se intenta el swap');

      await assert.rejects(auto.tick({ store, usage, swap }), /verify falló/);
      assert.deepStrictEqual(calls, ['sana'], 'rota a la cuenta sana, nunca a la del token rechazado');
      assert.strictEqual(await auto.tick({ store, usage, swap }), null, 'un swap fallido arma el cooldown igual');
      assert.strictEqual(calls.length, 1, 'no se reintenta en el tick siguiente');
      assert.ok(auto.load().lastSwapAt > 0);
    } finally {
      auto.save({ enabled: false, target: 'host', threshold: 90, lastSwapAt: 0 });
    }
  });

  await checkAsync('codex: con el puerto del login ocupado o reservado, se detecta y el alta pasa al código de dispositivo', async () => {
    // Windows puede reservar el 1455 (rango dinámico que empieza en 1024 + Hyper-V/WSL): codex login
    // falla entonces con os error 10013. El panel lo comprueba antes de abrir la terminal.
    const terminal = require('./lib/terminal');
    const holder = require('node:net').createServer();
    await new Promise((r) => holder.listen({ host: '127.0.0.1', port: 0 }, r));
    const { port } = holder.address();
    try {
      assert.strictEqual(await terminal.loginPortFree(port), false, 'un puerto ocupado no está libre');
    } finally {
      await new Promise((r) => holder.close(r));
    }
    assert.strictEqual(await terminal.loginPortFree(port), true, 'liberado, vuelve a estar libre');
  });

  await checkAsync('un login terminado no se intenta renovar: el swap dice qué hacer sin llamar a Anthropic', async () => {
    const store = require('./lib/store');
    const acc = store.add({
      email: 'ended@x.y', profile: { accountUuid: 'uuid-ended' },
      oauth: { accessToken: 'A', refreshToken: 'R', expiresAt: Date.now() - 1000, refreshTokenExpiresAt: Date.now() - 1000, scopes: ['user:profile'] },
    });
    let called = false;
    const oauthLib = { refresh: async () => { called = true; throw new Error('invalid_grant'); }, toStoredOauth: (x) => x };
    try {
      await assert.rejects(swapLib.ensureFreshToken(store.get(acc.id), { store, oauth: oauthLib }), /terminó[\s\S]*30 días[\s\S]*token de un año/);
      assert.strictEqual(called, false, 'no call to the token endpoint for a login that is over');
    } finally {
      store.remove(acc.id);
    }
  });

  await checkAsync('claude: una cuenta importada (token que rota) solo puede estar activa en un entorno; un token de un año, en varios', async () => {
    const store = require('./lib/store');
    const claudeTargets = require('./lib/targets');
    const realList = claudeTargets.list;
    const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'swapper-one-env-'));
    const mk = (name) => {
      const dir = path.join(tmp, name.replace(/[:]/g, '_'));
      fs.mkdirSync(path.join(dir, '.claude'), { recursive: true });
      return { id: name, kind: name === 'host' ? 'host' : 'wsl', label: name, claudeJsonPath: path.join(dir, '.claude.json'), credentialsPath: path.join(dir, '.claude', '.credentials.json'), fileBackend: true };
    };
    const win = mk('host'), ubuntu = mk('wsl:Ubuntu');
    claudeTargets.list = () => [win, ubuntu];
    const imported = store.add({ email: 'rot@x.y', profile: { accountUuid: 'uuid-rot' }, oauth: { accessToken: 'A1', refreshToken: 'R1', expiresAt: Date.now() + 3600e3, refreshTokenExpiresAt: Date.now() + 9e8, scopes: ['user:profile'] } });
    const pasted = store.add({ oauth: { accessToken: 'TOKEN-YEAR', refreshToken: null, expiresAt: Date.now() + 9e9, scopes: ['user:inference'] }, label: 'Year' });
    try {
      // Ubuntu has the imported account live: its config names it.
      fs.writeFileSync(ubuntu.claudeJsonPath, JSON.stringify({ oauthAccount: { accountUuid: 'uuid-rot' } }));
      await assert.rejects(swapLib.swapTo(imported.id, { store, oauth: {}, usage: {} }, win),
        (err) => err.status === 409 && /ya está activa en wsl:Ubuntu[\s\S]*token de un año/.test(err.message));
      assert.ok(!fs.existsSync(win.credentialsPath), 'refused before anything was written');
      store.setActive(imported.id, 'wsl:Ubuntu');
      assert.deepStrictEqual(store.publicView('host').accounts.find((a) => a.id === imported.id).activeIn, ['wsl:Ubuntu'], 'the row says where, before the click');
      store.setActive(pasted.id, 'wsl:Ubuntu');
      assert.deepStrictEqual(store.publicView('host').accounts.find((a) => a.id === pasted.id).activeIn, [], 'a year-long token is never held back');

      // Ubuntu stops: it drops off the list, and still holds its copy on disk. Still refused.
      store.setActive(imported.id, 'wsl:Ubuntu');
      claudeTargets.list = () => [win];
      await assert.rejects(swapLib.swapTo(imported.id, { store, oauth: {}, usage: {} }, win),
        (err) => err.status === 409 && /Ubuntu \(parada/.test(err.message), 'a stopped distro still holds it');
      assert.ok(!fs.existsSync(win.credentialsPath), 'refused before anything was written');

      // A renewal reaches a WSL copy that still holds the old pair, and only such a copy.
      fs.writeFileSync(ubuntu.credentialsPath, JSON.stringify({ claudeAiOauth: { accessToken: 'A1', refreshToken: 'R1', expiresAt: 1 } }));
      assert.strictEqual(swapLib.syncLiveCredentials('R1', { accessToken: 'A2', refreshToken: 'R2', expiresAt: 2 }, ubuntu), true);
      assert.strictEqual(JSON.parse(fs.readFileSync(ubuntu.credentialsPath, 'utf8')).claudeAiOauth.refreshToken, 'R2');
      assert.strictEqual(swapLib.syncLiveCredentials('R1', { accessToken: 'A3', refreshToken: 'R3', expiresAt: 3 }, ubuntu), false, 'not a copy of the old pair: left alone');
    } finally {
      claudeTargets.list = realList;
      store.remove(imported.id);
      store.remove(pasted.id);
      fs.rmSync(tmp, { recursive: true, force: true });
    }
  });

  await checkAsync('la cabecera X-Swapper es obligatoria en toda la API, GET incluido', async () => {
    // Un <img src="http://127.0.0.1:7373/api/health"> desde cualquier web pasaba las tres
    // guardas: sin Origin, método GET, y Host correcto. Cada llamada lanza un tasklist.
    const server = require('./server');
    const PORT = 7999;
    const s = server.createServer(PORT);
    await new Promise((resolve, reject) => {
      s.once('error', reject);
      s.listen(PORT, '127.0.0.1', resolve);
    });
    try {
      const base = `http://127.0.0.1:${PORT}`;
      assert.strictEqual((await fetch(`${base}/api/health`)).status, 403, 'GET a la API sin cabecera');
      assert.strictEqual((await fetch(`${base}/api/accounts`)).status, 403, 'GET a la API sin cabecera');
      assert.strictEqual((await fetch(`${base}/api/health`, { headers: { 'X-Swapper': '1' } })).status, 200);
      assert.strictEqual((await fetch(`${base}/style.css`)).status, 200, 'los estáticos no pueden exigirla');
    } finally {
      await new Promise((resolve) => s.close(resolve));
    }
  });

  await checkAsync('el cooldown de un 429 sobrevive a reiniciar el servidor', async () => {
    const realFetch = global.fetch;
    const account = { id: 'rst1', oauth: { accessToken: 'tok' } };
    try {
      usage.invalidate();
      usage.resetCooldown();
      global.fetch = async () => ({
        ok: false, status: 429,
        headers: { get: (h) => (h === 'retry-after' ? '300' : null) },
        text: async () => '{"error":{"type":"rate_limit_error"}}',
      });
      await usage.fetchFor(account, { force: true });
      assert.ok(usage.cooldownRemainingMs() > 0, 'el 429 arma el cooldown');

      // Reiniciar el proceso = cargar el módulo desde cero. Antes, eso lo olvidaba todo y
      // el arranque siguiente volvía derecho al endpoint que seguía castigando.
      delete require.cache[require.resolve('./lib/usage')];
      const restarted = require('./lib/usage');
      assert.ok(restarted.cooldownRemainingMs() > 0, 'el cooldown debe sobrevivir al reinicio');

      let called = false;
      global.fetch = async () => { called = true; throw new Error('must not be called'); };
      await restarted.fetchFor(account, { force: true });
      assert.strictEqual(called, false, 'tras reiniciar no debe volver a golpear el endpoint');
      restarted.resetCooldown();
    } finally {
      global.fetch = realFetch;
      delete require.cache[require.resolve('./lib/usage')];
      usage.resetCooldown();
      usage.invalidate();
    }
  });

  await checkAsync('una cuenta con el token muerto no monopoliza el turno', async () => {
    // cache.at solo avanza al triunfar, así que una cuenta con 401 se quedaba en 0 y ganaba
    // el orden "más desactualizada primero" en TODOS los barridos, para siempre.
    const accounts = ['broken', 'good1', 'good2'].map((n) => ({ id: `st-${n}`, oauth: { accessToken: n } }));
    const realFetch = global.fetch;
    const asked = [];
    try {
      usage.invalidate();
      usage.resetCooldown();
      global.fetch = async (url, opts) => {
        const who = String(opts.headers.Authorization).replace('Bearer ', '');
        asked.push(who);
        if (who === 'broken') {
          return { ok: false, status: 401, headers: { get: () => null }, text: async () => 'unauthorized' };
        }
        return {
          ok: true, status: 200, headers: { get: () => null },
          text: async () => JSON.stringify({ limits: [{ kind: 'session', percent: 5 }] }),
        };
      };
      // Tres barridos. resetCooldown entre ellos solo levanta el suelo de 80 s, que si no
      // haría del test una espera de cuatro minutos; el orden de turnos no se toca.
      for (let i = 0; i < 3; i++) {
        await usage.fetchAll(accounts, { force: true });
        usage.resetCooldown();
      }
      assert.strictEqual(asked.length, 3, `un barrido, una petición (${asked.join(', ')})`);
      assert.strictEqual(asked[0], 'broken', 'la primera vez sí gana la más desactualizada');
      assert.ok(!asked.slice(1).includes('broken'), `la cuenta muerta repitió turno: ${asked.join(', ')}`);
    } finally {
      global.fetch = realFetch;
      usage.invalidate();
      usage.resetCooldown();
    }
  });

  await checkAsync('un refresh forzado encadena hasta refrescar TODAS las cuentas, y solo una vez cada una', async () => {
    // Regresión: el tope de 80 s deja pasar UNA llamada por barrido. Las demás volvían como
    // stale sin retryInS, el frontend no reintentaba, y con 4 cuentas tardaban 40 min en
    // estar frescas. Ahora vuelven `queued` con retryInS y quedan pendientes: el siguiente
    // barrido (sin force) las refresca saltando la caché, y la cadena acaba cuando no queda
    // ninguna pendiente — sin volver a pedir una cuenta recién refrescada.
    const accounts = ['a', 'b', 'c'].map((n) => ({ id: `chain-${n}`, oauth: { accessToken: n, scopes: ['user:profile'] } }));
    const realFetch = global.fetch;
    const asked = [];
    const body = JSON.stringify({ limits: [{ kind: 'session', percent: 5 }, { kind: 'weekly_all', percent: 5 }] });
    try {
      usage.invalidate();
      usage.resetCooldown();
      global.fetch = async (url, opts) => {
        asked.push(String(opts.headers.Authorization).replace('Bearer ', ''));
        return { ok: true, status: 200, headers: { get: () => null }, text: async () => body };
      };
      // Precalentar: las tres con caché (cada una en su turno)… y luego envejecerla más allá
      // del suelo de 80 s, que es el estado real de un usuario pulsando refresh: caché vieja
      // pero no caducada. Recién pedidas, un force las devolvería tal cual sin llamar.
      for (let i = 0; i < 3; i++) { await usage.fetchAll(accounts, { force: true }); usage.resetCooldown(); }
      for (const a of accounts) usage.prime(a.id, usage.cachedFor(a.id), Date.now() - 2 * usage.MIN_GAP_MS);
      asked.length = 0;

      // Barrido FORZADO: una entra, dos quedan en cola con retryInS.
      const r1 = await usage.fetchAll(accounts, { force: true });
      const queued = Object.values(r1).filter((u) => u.queued);
      assert.strictEqual(asked.length, 1, 'el tope deja pasar una sola llamada');
      assert.strictEqual(queued.length, 2, 'las otras dos vuelven en cola');
      assert.ok(queued.every((u) => Number.isFinite(u.retryInS) && u.retryInS > 0), 'con retryInS para encadenar');

      // Cadena: barridos SIN force (como hace el frontend) hasta que no quede nada en cola.
      let rounds = 0;
      let last = r1;
      while (Object.values(last).some((u) => u.queued) && rounds < 5) {
        usage.resetCooldown(); // levanta el suelo de 80 s; no es un test de esperar
        last = await usage.fetchAll(accounts, {});
        rounds++;
      }
      assert.strictEqual(asked.length, 3, `tres cuentas, tres llamadas en total (${asked.join(',')})`);
      assert.strictEqual(new Set(asked).size, 3, 'cada cuenta exactamente una vez, ninguna repetida');
      assert.ok(!Object.values(last).some((u) => u.queued), 'la cadena termina sin nada en cola');

      // Y un barrido más SIN force no gasta nada: todas frescas.
      usage.resetCooldown();
      await usage.fetchAll(accounts, {});
      assert.strictEqual(asked.length, 3, 'nada pendiente, nada que pedir');
    } finally {
      global.fetch = realFetch;
      usage.invalidate();
      usage.resetCooldown();
    }
  });

  await checkAsync('a 429 serves the last good reading instead of blanking the row', async () => {
    const account = { id: 'rl1', oauth: { accessToken: 'tok' } };
    const realFetch = global.fetch;
    const body = JSON.stringify({ limits: [
      { kind: 'session', percent: 42, resets_at: 'A' },
      { kind: 'weekly_all', percent: 11, resets_at: 'B' }] });

    try {
      global.fetch = async () => ({ ok: true, status: 200, headers: { get: () => null }, text: async () => body });
      const good = await usage.fetchFor(account, { force: true });
      assert.strictEqual(good.session.percent, 42);
      assert.ok(!good.stale);

      // Clear the rate floor: otherwise the next call is throttled locally and never
      // reaches the API, so no 429 could come back. And age the cache past the floor: a
      // reading fetched seconds ago is handed back as-is on a forced refresh (nothing to
      // learn from re-asking), which would also keep the 429 from ever happening.
      usage.resetCooldown();
      usage.prime(account.id, good, Date.now() - 2 * usage.MIN_GAP_MS);
      global.fetch = async () => ({
        ok: false, status: 429,
        headers: { get: (h) => (h === 'retry-after' ? '5' : null) },
        text: async () => '{"error":{"type":"rate_limit_error"}}',
      });
      const stale = await usage.fetchFor(account, { force: true });
      assert.strictEqual(stale.ok, true, 'a 429 must not blank the row');
      assert.strictEqual(stale.stale, true, 'the reading must be flagged stale');
      assert.strictEqual(stale.session.percent, 42, 'last good numbers are kept');
      assert.ok(usage.cooldownRemainingMs() > 0, 'retry-after must arm the cooldown');

      let called = false;
      global.fetch = async () => { called = true; throw new Error('must not be called'); };
      await usage.fetchFor(account, { force: true });
      assert.strictEqual(called, false, 'the cooldown must suppress the API call entirely');

      // A genuinely dead token must never hide behind stale numbers.
      usage.resetCooldown();
      global.fetch = async () => ({ ok: false, status: 401, headers: { get: () => null }, text: async () => 'unauthorized' });
      const dead = await usage.fetchFor(account, { force: true });
      assert.strictEqual(dead.ok, false, '401 must surface, not be masked by stale data');
      assert.strictEqual(dead.needsRelogin, true);
    } finally {
      global.fetch = realFetch;
      usage.resetCooldown();
      usage.invalidate();
    }
  });

  await checkAsync('a primed reading spares the API a second call', async () => {
    // The swap fetches usage to verify the new token and donates it via prime(); the UI
    // must then read it from cache. Request amplification is what trips the 429.
    const account = { id: 'pr1', oauth: { accessToken: 'tok' } };
    const realFetch = global.fetch;
    try {
      usage.invalidate();
      usage.resetCooldown();
      usage.prime(account.id, usage.normalize({ limits: [
        { kind: 'session', percent: 7, resets_at: 'A' },
        { kind: 'weekly_all', percent: 8, resets_at: 'B' }] }, account.id));

      let called = false;
      global.fetch = async () => { called = true; throw new Error('must not be called'); };
      const got = await usage.fetchFor(account);
      assert.strictEqual(called, false, 'a primed reading must not hit the network');
      assert.strictEqual(got.session.percent, 7);

      // A failed reading is worthless as a cache entry and must be refused.
      assert.strictEqual(usage.prime('pr2', { ok: false, error: 'x' }).ok, false);
    } finally {
      global.fetch = realFetch;
      usage.invalidate();
    }
  });

  await checkAsync('the rate floor caps outbound calls no matter how hard the UI pushes', async () => {
    // Measured against the live endpoint: the 5th rapid request returns 429 with
    // Retry-After 300, escalating on repeat. So the floor, not the poll interval, is
    // what has to hold - a user mashing refresh must not be able to spend the budget.
    const accounts = [1, 2, 3, 4].map((n) => ({ id: `gap${n}`, oauth: { accessToken: 't' } }));
    const realFetch = global.fetch;
    let calls = 0;
    try {
      usage.invalidate();
      usage.resetCooldown();
      global.fetch = async () => {
        calls++;
        return {
          ok: true, status: 200, headers: { get: () => null },
          text: async () => JSON.stringify({ limits: [{ kind: 'session', percent: 3 }] }),
        };
      };
      // Four accounts swept three times over, every call forced.
      for (let i = 0; i < 3; i++) await usage.fetchAll(accounts, { force: true });
      assert.strictEqual(calls, 1, `the floor should allow exactly 1 call, saw ${calls}`);

      // The starved accounts must still render something rather than break.
      const out = await usage.fetchAll(accounts, { force: true });
      assert.strictEqual(Object.keys(out).length, 4);
      const throttled = Object.values(out).filter((v) => v.throttled);
      assert.ok(throttled.length >= 1, 'starved accounts report throttled, not a hard error');
      assert.ok(throttled.every((v) => v.needsRelogin === false), 'throttling is not a token problem');
    } finally {
      global.fetch = realFetch;
      usage.invalidate();
      usage.resetCooldown();
    }
  });

  await checkAsync('fetchAll queries accounts one at a time, not as a burst', async () => {
    const accounts = [1, 2, 3].map((n) => ({ id: `seq${n}`, oauth: { accessToken: 't' } }));
    const realFetch = global.fetch;
    let inFlight = 0;
    let maxInFlight = 0;
    try {
      usage.invalidate();
      usage.resetCooldown();
      global.fetch = async () => {
        inFlight++;
        maxInFlight = Math.max(maxInFlight, inFlight);
        await new Promise((r) => setTimeout(r, 5));
        inFlight--;
        return {
          ok: true, status: 200, headers: { get: () => null },
          text: async () => JSON.stringify({ limits: [{ kind: 'session', percent: 1 }] }),
        };
      };
      const out = await usage.fetchAll(accounts, { force: true });
      assert.strictEqual(Object.keys(out).length, 3);
      assert.strictEqual(maxInFlight, 1, `expected serial requests, saw ${maxInFlight} at once`);
    } finally {
      global.fetch = realFetch;
      usage.invalidate();
    }
  });

  /* ---------------- tokens de larga duración (claude setup-token) ---------------- */

  // Ensamblado en tiempo de ejecución a propósito: escrito entero dispararía el propio check
  // "no source file hardcodes a token" de este mismo fichero.
  const FAKE_TOKEN = ['sk', 'ant', 'oat01', 'A'.repeat(24)].join('-');
  const store = require('./lib/store');

  await checkAsync('un token con forma inválida se rechaza sin tocar la red', async () => {
    const realFetch = global.fetch;
    let called = false;
    try {
      global.fetch = async () => { called = true; throw new Error('no debería llamarse'); };
      await assert.rejects(() => oauth.probeToken('esto-no-es-un-token'), (e) => e.malformed === true);
      await assert.rejects(() => oauth.probeToken(''), (e) => e.malformed === true);
      await assert.rejects(() => oauth.probeToken(null), (e) => e.malformed === true);
      assert.strictEqual(called, false, 'la validación de forma es local: no debe gastar una petición');
    } finally { global.fetch = realFetch; }
  });

  await checkAsync('un 403 por scope IDENTIFICA un token de solo inferencia, no lo rechaza', async () => {
    const realFetch = global.fetch;
    try {
      global.fetch = async () => ({
        ok: false, status: 403, headers: { get: () => null },
        text: async () => JSON.stringify({ error: { message: 'OAuth token does not meet scope requirement user:profile' } }),
      });
      // Anthropic solo comprueba scopes en un token que YA autenticó, así que este 403 prueba que
      // el token es real. Tratarlo como fallo dejaría fuera justo el caso que buscamos.
      assert.strictEqual((await oauth.probeToken(FAKE_TOKEN)).kind, 'inference');
    } finally { global.fetch = realFetch; }
  });

  await checkAsync('un 401 rechaza el token', async () => {
    const realFetch = global.fetch;
    try {
      global.fetch = async () => ({
        ok: false, status: 401, headers: { get: () => null },
        text: async () => JSON.stringify({ error: { message: 'OAuth access token is invalid.' } }),
      });
      await assert.rejects(() => oauth.probeToken(FAKE_TOKEN), (e) => e.status === 401);
    } finally { global.fetch = realFetch; }
  });

  await checkAsync('un 200 identifica un token de scope completo', async () => {
    const realFetch = global.fetch;
    try {
      global.fetch = async () => ({ ok: true, status: 200, headers: { get: () => null }, text: async () => '{}' });
      assert.strictEqual((await oauth.probeToken(FAKE_TOKEN)).kind, 'full');
    } finally { global.fetch = realFetch; }
  });

  // Cabeceras como las que devuelve /v1/messages, para las sondas de cuota.
  const cabeceras = (map) => ({ get: (k) => (k.toLowerCase() in map ? map[k.toLowerCase()] : null) });
  const RESET_5H = Math.floor(Date.now() / 1000) + 3600;
  const RESET_7D = Math.floor(Date.now() / 1000) + 86400;
  const CABECERAS_OK = {
    'anthropic-ratelimit-unified-status': 'allowed',
    'anthropic-ratelimit-unified-representative-claim': '5h',
    'anthropic-ratelimit-unified-5h-utilization': '0.42',
    'anthropic-ratelimit-unified-5h-reset': String(RESET_5H),
    'anthropic-ratelimit-unified-7d-utilization': '0.87',
    'anthropic-ratelimit-unified-7d-reset': String(RESET_7D),
  };

  await checkAsync('una cuenta de solo inferencia saca la cuota de las cabeceras, sin tocar el endpoint de uso', async () => {
    const realFetch = global.fetch;
    const urls = [];
    try {
      usage.invalidate(); usage.resetCooldown();
      global.fetch = async (url) => {
        urls.push(String(url));
        return { ok: true, status: 200, headers: cabeceras(CABECERAS_OK), text: async () => '{}' };
      };
      const r = await usage.fetchFor({ id: 'inf1', oauth: { accessToken: 't', scopes: ['user:inference'] } });

      // Lo que NO puede pasar: /api/oauth/usage responde 403 permanente a este token, y el cupo
      // es de ~5 peticiones por 5 minutos para TODA la app. Gastarlo ahí sería un fallo seguro
      // que además deja sin datos a las cuentas que sí pueden responder.
      assert.ok(!urls.some((u) => u.includes('/api/oauth/usage')), 'no debe tocar el endpoint de uso');
      assert.ok(urls.some((u) => u === usage.PROBE_URL), 'debe sondear /v1/messages');

      assert.strictEqual(r.ok, true);
      assert.strictEqual(r.viaProbe, true, 'debe marcar de dónde salió el número');
      // utilization llega como fracción (0.42) y sale como porcentaje.
      assert.strictEqual(r.session.percent, 42);
      assert.strictEqual(r.weekly.percent, 87);
      assert.strictEqual(r.weekly.severity, 'high');
      // reset llega en segundos epoch y sale en ISO, que es lo que sabe leer la cuenta atrás.
      assert.strictEqual(r.session.resetsAt, new Date(RESET_5H * 1000).toISOString());
    } finally { global.fetch = realFetch; usage.invalidate(); usage.resetCooldown(); }
  });

  await checkAsync('la sonda envía lo mínimo: Haiku, max_tokens 1 y un carácter', async () => {
    const realFetch = global.fetch;
    let enviado = null;
    let cabecerasEnviadas = null;
    try {
      usage.invalidate(); usage.resetCooldown();
      global.fetch = async (url, opts) => {
        enviado = JSON.parse(opts.body);
        cabecerasEnviadas = opts.headers;
        return { ok: true, status: 200, headers: cabeceras(CABECERAS_OK), text: async () => '{}' };
      };
      await usage.fetchFor({ id: 'inf2', oauth: { accessToken: 'tok', scopes: ['user:inference'] } });
      // Los endpoints gratuitos (count_tokens, /v1/models) no traen cabeceras de límite, así que
      // hay que gastar algo. Esto es el suelo medido: 8 tokens de entrada y 1 de salida.
      assert.strictEqual(enviado.model, usage.PROBE_MODEL);
      assert.strictEqual(enviado.max_tokens, 1);
      assert.strictEqual(enviado.messages[0].content, '.');
      assert.strictEqual(cabecerasEnviadas['anthropic-beta'], 'oauth-2025-04-20',
        'un token OAuth de suscripción necesita esta beta');
    } finally { global.fetch = realFetch; usage.invalidate(); usage.resetCooldown(); }
  });

  await checkAsync('un token rechazado en la sonda es una credencial muerta, no una cuenta sin cuota', async () => {
    const realFetch = global.fetch;
    try {
      usage.invalidate(); usage.resetCooldown();
      global.fetch = async () => ({ ok: false, status: 401, headers: cabeceras({}), text: async () => 'nope' });
      const r = await usage.fetchFor({ id: 'inf3', oauth: { accessToken: 't', scopes: ['user:inference'] } });
      // Confundir las dos cosas es el fallo que el panel del servidor documentaba: una cuenta
      // apartada hora y media por "cuota" teniendo la ventana al 0%.
      assert.strictEqual(r.ok, false);
      assert.strictEqual(r.needsRelogin, true);
      assert.strictEqual(r.session, undefined, 'una credencial muerta no debe traer medidores inventados');
      assert.ok(!JSON.stringify(r).includes('sk-ant-'), 'el error no debe llevar el token');
    } finally { global.fetch = realFetch; usage.invalidate(); usage.resetCooldown(); }
  });

  check('writeCredentials no escribe el centinela de token muerto ni deja los scopes vacíos', () => {
    const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'swapper-tok-'));
    try {
      const f = path.join(tmp, '.credentials.json');
      fs.writeFileSync(f, JSON.stringify({ mcpOAuth: { keep: 1 } }));

      swapLib.writeCredentials(f, { accessToken: 'A', expiresAt: 1, scopes: ['user:inference'] });
      const c = JSON.parse(fs.readFileSync(f, 'utf8'));
      // Claude Code lee refreshToken === "" como "este token ya está muerto" y ni lo intenta.
      assert.strictEqual(c.claudeAiOauth.refreshToken, null);
      assert.notStrictEqual(c.claudeAiOauth.refreshToken, '');
      assert.deepStrictEqual(c.claudeAiOauth.scopes, ['user:inference']);
      assert.deepStrictEqual(c.mcpOAuth, { keep: 1 }, 'mcpOAuth debe sobrevivir');

      // Sin scopes, Claude Code imprime "Not logged in - Please run /login" y sale con 1.
      swapLib.writeCredentials(f, { accessToken: 'B', expiresAt: 1 });
      const c2 = JSON.parse(fs.readFileSync(f, 'utf8'));
      assert.ok(c2.claudeAiOauth.scopes.includes('user:inference'), 'unos scopes vacíos dejan la sesión sin login');
    } finally { fs.rmSync(tmp, { recursive: true, force: true }); }
  });

  check('clearClaudeJsonIdentity borra la identidad anterior y respeta todo lo demás', () => {
    const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'swapper-cid-'));
    try {
      const f = path.join(tmp, '.claude.json');
      fs.writeFileSync(f, JSON.stringify({
        numStartups: 7, userID: 'INSTALL-ID', projects: { '/a': { history: [1] } },
        mcpServers: { x: { command: 'y' } },
        oauthAccount: { emailAddress: 'anterior@x.com', accountUuid: 'u1' },
        modelAccessCache: [], somethingElse: { deep: true },
      }));
      const r = swapLib.clearClaudeJsonIdentity(f);
      const cj = JSON.parse(fs.readFileSync(f, 'utf8'));
      // Dejarla haría que Claude Code siguiera nombrando la cuenta de la que acabas de salir.
      assert.ok(!('oauthAccount' in cj));
      assert.strictEqual(r.clearedIdentity, true);
      assert.ok(!('modelAccessCache' in cj), 'las cachés de la cuenta anterior también se van');
      assert.strictEqual(cj.userID, 'INSTALL-ID', 'userID es del instalador, no de la cuenta');
      assert.strictEqual(cj.numStartups, 7);
      assert.deepStrictEqual(cj.projects, { '/a': { history: [1] } });
      assert.deepStrictEqual(cj.mcpServers, { x: { command: 'y' } });
      assert.deepStrictEqual(cj.somethingElse, { deep: true });
    } finally { fs.rmSync(tmp, { recursive: true, force: true }); }
  });

  check('una cuenta creada desde un token pegado se identifica sola, se deduplica y no filtra nada', () => {
    const before = store.list().length;
    const blob = { accessToken: FAKE_TOKEN, refreshToken: null, expiresAt: Date.now() + 60000, scopes: ['user:inference'] };
    const a = store.add({ label: null, email: null, profile: null, oauth: blob });
    try {
      // Sin accountUuid ni email, el propio token es la única identidad estable disponible.
      assert.strictEqual(store.idForToken(FAKE_TOKEN), a.id);
      assert.strictEqual(store.add({ label: null, email: null, profile: null, oauth: blob }).id, a.id,
        'pegar el mismo token dos veces debe actualizar, no duplicar');
      assert.strictEqual(store.list().length, before + 1);

      const view = store.publicView().accounts.find((x) => x.id === a.id);
      assert.strictEqual(view.canReadUsage, false);
      assert.strictEqual(view.renewable, false);
      assert.strictEqual(view.plan, null, 'no debe inventar un plan que nunca ha visto');
      assert.ok(!JSON.stringify(store.publicView()).includes('sk-ant-'), 'publicView filtró un token');
    } finally { store.remove(a.id); }
  });

  check('volver a pegar un token aplica el nombre nuevo, pero un import sin nombre no lo pisa', () => {
    const blob = { accessToken: FAKE_TOKEN, refreshToken: null, expiresAt: Date.now() + 60000, scopes: ['user:inference'] };
    const a = store.add({ label: null, email: null, profile: null, oauth: blob });
    try {
      // Sin nombre, la etiqueta es el propio id: legible, pero no dice nada.
      assert.match(a.label, /^token /, 'sin nombre debe caer en la etiqueta derivada del id');

      // Volver a pegar el MISMO token con nombre es la única forma de renombrar desde el panel,
      // y además es la vía por la que se sustituye un token de un año que no se puede refrescar.
      const renamed = store.add({ label: 'Trabajo', email: null, profile: null, oauth: blob });
      assert.strictEqual(renamed.id, a.id, 'debe seguir siendo la misma cuenta');
      assert.strictEqual(store.get(a.id).label, 'Trabajo', 'el nombre tecleado no puede descartarse en silencio');

      // Pero un caller que no opina sobre el nombre (el import lo pasa null) no debe pisarlo.
      store.add({ label: null, email: null, profile: null, oauth: blob });
      assert.strictEqual(store.get(a.id).label, 'Trabajo', 'un import sin nombre debe respetar el que puso el usuario');
    } finally { store.remove(a.id); }
  });

  check('una cuenta sin perfil escribe su nombre como identidad, en el campo que /status muestra', () => {
    const blob = { accessToken: FAKE_TOKEN, refreshToken: null, expiresAt: Date.now() + 60000, scopes: ['user:inference'] };
    const a = store.add({ label: 'Equipo', email: null, profile: null, oauth: blob });
    try {
      const ident = swapLib.identityFromLabel(store.get(a.id));
      // /status renderiza Email y Organization; displayName lo ignora por completo (verificado
      // contra la TUI real). Poner el nombre solo en displayName dejaría /status en blanco.
      assert.strictEqual(ident.emailAddress, 'Equipo', 'el nombre debe ir donde /status mira');
      assert.strictEqual(ident.displayName, 'Equipo');
      // Lo que NO sabemos de un token de solo inferencia se queda en null: nada inventado.
      assert.strictEqual(ident.accountUuid, null, 'no se puede inventar un accountUuid');
      // Nombrar el panel, en vez de dejarlo vacío: si se deja null, Claude Code se inventa
      // "<nombre>'s Organization", que se lee como una organización real a la que perteneces.
      assert.strictEqual(ident.organizationName, 'LLMSwapper');
      assert.strictEqual(ident.organizationUuid, null, 'no se puede inventar un uuid de organización');
    } finally { store.remove(a.id); }
  });

  check('el swap escribe esa identidad en ~/.claude.json y respeta el resto del fichero', () => {
    const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'swapper-ident-'));
    try {
      const f = path.join(tmp, '.claude.json');
      fs.writeFileSync(f, JSON.stringify({
        numStartups: 3, userID: 'INSTALL-ID', projects: { '/x': { history: [7] } },
        oauthAccount: { emailAddress: 'anterior@x.com', accountUuid: 'viejo' },
        modelAccessCache: [],
      }));
      swapLib.writeClaudeJson(f, swapLib.identityFromLabel({ label: 'Trabajo' }));
      const cj = JSON.parse(fs.readFileSync(f, 'utf8'));
      // Sustituye a la anterior, no la deja: mostrar la cuenta de la que saliste es el peor caso.
      assert.strictEqual(cj.oauthAccount.emailAddress, 'Trabajo');
      assert.strictEqual(cj.oauthAccount.accountUuid, null);
      assert.ok(cj.oauthAccount.profileFetchedAt, 'writeClaudeJson sella la marca de tiempo');
      assert.ok(!('modelAccessCache' in cj), 'las cachés de la cuenta anterior se van');
      assert.strictEqual(cj.userID, 'INSTALL-ID', 'userID es del instalador, no de la cuenta');
      assert.deepStrictEqual(cj.projects, { '/x': { history: [7] } });
    } finally { fs.rmSync(tmp, { recursive: true, force: true }); }
  });

  check('en un contenedor, "no veo los procesos" no se reporta como "no hay procesos"', () => {
    const antes = process.env.SWAPPER_IN_CONTAINER;
    try {
      process.env.SWAPPER_IN_CONTAINER = '1';
      assert.strictEqual(P.inContainer(), true);
      const r = swapLib.detectClaudeProcesses();
      // running:false a secas sería una mentira con cara de certeza: el panel diría que Claude
      // Code está cerrado mientras corre en el host, al otro lado de la frontera del contenedor.
      assert.strictEqual(r.unknown, true, 'debe admitir que no puede saberlo');
      assert.deepStrictEqual(r.pids, []);
    } finally {
      if (antes === undefined) delete process.env.SWAPPER_IN_CONTAINER;
      else process.env.SWAPPER_IN_CONTAINER = antes;
    }
  });

  check('fuera de un contenedor la detección sigue siendo real', () => {
    const antes = process.env.SWAPPER_IN_CONTAINER;
    try {
      delete process.env.SWAPPER_IN_CONTAINER;
      assert.strictEqual(P.inContainer(), false, 'sin la variable y sin /.dockerenv');
      const r = swapLib.detectClaudeProcesses();
      assert.strictEqual(r.unknown, undefined, 'aquí sí se puede mirar, así que no hay excusa');
      assert.ok(Array.isArray(r.pids));
    } finally {
      if (antes !== undefined) process.env.SWAPPER_IN_CONTAINER = antes;
    }
  });

  await checkAsync('el guard valida el HOSTNAME, no el puerto: rebinding fuera, contenedor dentro', async () => {
    const realFetch = global.fetch;
    const server = require('./server').createServer(7996);
    try {
      await new Promise((r) => server.listen(7996, '127.0.0.1', r));
      // http.request y no fetch: Host es un "forbidden header name", así que fetch lo ignora en
      // silencio y el test pasaría sin haber probado nada. Esto lo descubrió el propio test
      // fallando al revés - pedía un 403 y recibía un 200 porque su Host nunca salió.
      const http = require('node:http');
      const pedir = (headers) => new Promise((resolve, reject) => {
        const req = http.request({
          host: '127.0.0.1', port: 7996, path: '/api/health', method: 'GET',
          headers: { 'X-Swapper': '1', ...headers },
        }, (res) => { res.resume(); res.on('end', () => resolve(res.statusCode)); });
        req.on('error', reject);
        req.end();
      });

      // Un dominio que resuelve a 127.0.0.1 hace que el navegador SÍ conecte con este socket;
      // lo que le delata es que la cabecera Host lleva su dominio, no el loopback.
      assert.strictEqual(await pedir({ Host: 'evil.example' }), 403, 'DNS rebinding debe caer');
      assert.strictEqual(await pedir({ Host: 'llmswapper.local' }), 403);

      // El puerto NO se valida: al publicar el contenedor con -p el navegador manda el puerto
      // externo, que este proceso no puede conocer. Exigirlo rechazaba todo uso en Docker.
      assert.strictEqual(await pedir({ Host: '127.0.0.1:27387' }), 200, 'otro puerto es legítimo');
      assert.strictEqual(await pedir({ Host: 'localhost:9999' }), 200);

      // Un Origin de otro sitio sigue fuera, aunque el Host esté bien.
      assert.strictEqual(await pedir({ Origin: 'http://evil.example' }), 403, 'origen ajeno fuera');
      assert.strictEqual(await pedir({ Origin: 'http://127.0.0.1:27387' }), 200);
    } finally {
      global.fetch = realFetch;
      await new Promise((r) => server.close(r));
    }
  });

  await checkAsync('SWAPPER_ALLOWED_HOSTS abre el guard a una IP de LAN, y solo a esa', async () => {
    // La lista se calcula al cargar el modulo, asi que no basta con poner la variable: hay que
    // sacar server.js de la cache y volver a pedirlo. Es justo lo que hace un arranque real.
    const http = require('node:http');
    const antes = process.env.SWAPPER_ALLOWED_HOSTS;
    const realFetch = global.fetch;
    let server;
    try {
      process.env.SWAPPER_ALLOWED_HOSTS = '192.168.9.9, MI-PC';
      delete require.cache[require.resolve('./server')];
      server = require('./server').createServer(7994);
      await new Promise((r) => server.listen(7994, '127.0.0.1', r));

      // http.request y no fetch: Host es un "forbidden header name" y fetch lo descarta callando.
      const pedir = (headers) => new Promise((resolve, reject) => {
        const req = http.request({
          host: '127.0.0.1', port: 7994, path: '/api/health', method: 'GET',
          headers: { 'X-Swapper': '1', ...headers },
        }, (res) => { res.resume(); res.on('end', () => resolve(res.statusCode)); });
        req.on('error', reject);
        req.end();
      });

      assert.strictEqual(await pedir({ Host: '192.168.9.9:7373' }), 200, 'la IP permitida entra');
      assert.strictEqual(await pedir({ Host: 'mi-pc' }), 200, 'los nombres de host no distinguen mayúsculas');
      assert.strictEqual(await pedir({ Host: '127.0.0.1' }), 200, 'el loopback nunca se pierde');

      // Lo que importa: ensanchar la lista NO la abre a cualquiera. Una IP vecina de la misma
      // red sigue fuera, y el rebinding tambien, que era el ataque que el guard existe para parar.
      assert.strictEqual(await pedir({ Host: '192.168.9.10:7373' }), 403, 'otra IP de la misma red, fuera');
      assert.strictEqual(await pedir({ Host: 'evil.example' }), 403, 'DNS rebinding sigue cayendo');
      assert.strictEqual(await pedir({ Origin: 'http://evil.example' }), 403, 'origen ajeno sigue fuera');
      assert.strictEqual(await pedir({ Origin: 'http://192.168.9.9:7373' }), 200, 'origen permitido entra');
    } finally {
      if (server) await new Promise((r) => server.close(r));
      if (antes === undefined) delete process.env.SWAPPER_ALLOWED_HOSTS;
      else process.env.SWAPPER_ALLOWED_HOSTS = antes;
      // Devolver el modulo a su estado normal para los tests que vengan detras.
      delete require.cache[require.resolve('./server')];
      require('./server');
      global.fetch = realFetch;
    }
  });

  await checkAsync('POST /api/accounts/token rechaza un token inválido sin crear nada', async () => {
    const realFetch = global.fetch;
    const server = require('./server').createServer(7998);
    const before = store.list().length;
    try {
      await new Promise((r) => server.listen(7998, '127.0.0.1', r));
      const post = async (body) => {
        const res = await realFetch('http://127.0.0.1:7998/api/accounts/token', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json', 'X-Swapper': '1' },
          body: JSON.stringify(body),
        });
        return { status: res.status, body: await res.json() };
      };

      // Forma inválida: se corta antes de salir a la red.
      global.fetch = async () => { throw new Error('no debería llamarse'); };
      assert.strictEqual((await post({ token: 'no-es-un-token' })).status, 400);

      global.fetch = async () => ({
        ok: false, status: 401, headers: { get: () => null },
        text: async () => JSON.stringify({ error: { message: 'OAuth access token is invalid.' } }),
      });
      const rejected = await post({ token: FAKE_TOKEN });
      assert.strictEqual(rejected.status, 401);
      assert.ok(!JSON.stringify(rejected.body).includes(FAKE_TOKEN), 'el error no debe devolver el token');
      assert.strictEqual(store.list().length, before, 'un token rechazado no debe dejar una cuenta a medias');
    } finally {
      global.fetch = realFetch;
      await new Promise((r) => server.close(r));
    }
  });

  await checkAsync('en contenedor, "abre una terminal" se niega ANTES de lanzar nada', async () => {
    const realFetch = global.fetch;
    const server = require('./server').createServer(7995);
    const antes = process.env.SWAPPER_IN_CONTAINER;
    // Si el endpoint llegase a spawn, la suite abriría ventanas en la máquina de quien la corre.
    // Envolver child_process aquí es lo que convierte "creo que no lo llama" en "no lo llama".
    const cp = require('node:child_process');
    const spawnReal = cp.spawn;
    let lanzo = false;
    try {
      process.env.SWAPPER_IN_CONTAINER = '1';
      cp.spawn = (...a) => { lanzo = true; return spawnReal(...a); };
      await new Promise((r) => server.listen(7995, '127.0.0.1', r));

      const res = await realFetch('http://127.0.0.1:7995/api/token/terminal', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'X-Swapper': '1' },
        body: '{}',
      });
      const body = await res.json();

      assert.strictEqual(res.status, 409, 'no es un fallo del servidor: es que ahí no aplica');
      assert.strictEqual(lanzo, false, 'no debe intentar abrir nada dentro de un contenedor');
      assert.match(body.error, /contenedor/i, 'el motivo debe decir por qué, no solo que no');
    } finally {
      cp.spawn = spawnReal;
      if (antes === undefined) delete process.env.SWAPPER_IN_CONTAINER;
      else process.env.SWAPPER_IN_CONTAINER = antes;
      await new Promise((r) => server.close(r));
    }
  });

  check('el comando de la terminal es constante: nada de fuera entra en el argv', () => {
    const src = fs.readFileSync(path.join(__dirname, 'lib', 'terminal.js'), 'utf8');
    // La inyeccion solo es posible si algo de la peticion llega hasta aquí. No hay parametros:
    // openSetupToken no los acepta, y eso es lo que hace irrelevante el resto de la discusión.
    assert.strictEqual(require('./lib/terminal').openSetupToken.length, 0,
      'openSetupToken no debe aceptar argumentos');
    assert.ok(!/req\.|request|body|query|params/.test(src),
      'lib/terminal.js no debe saber nada de HTTP');
    // Y el comando viaja como argv, no como una cadena para que un shell la reinterprete.
    assert.ok(src.includes("const CLI = 'claude'") && src.includes("const ARG = 'setup-token'"),
      'el comando debe estar en constantes');
  });

  check('el login de Codex lleva el directorio una sola vez, y bien entrecomillado en cada plataforma', () => {
    const terminal = require('./lib/terminal');
    assert.strictEqual(typeof terminal.codexInstalled(), 'boolean');
    const dir = "/tmp/panel data/it's here";
    // Windows: through the environment, so there is nothing for cmd.exe to re-parse.
    const win = terminal.codexLoginCommand('win32', dir);
    assert.ok(win.args.every((a) => typeof a === 'string'));
    assert.strictEqual(win.env.CODEX_HOME, dir);
    assert.ok(!win.args.some((a) => a.includes(dir)), 'the directory never reaches the command line');
    assert.ok(win.args.includes('codex login'));
    // macOS and Linux: one shell line, the directory single-quoted exactly once.
    for (const platform of ['darwin', 'linux']) {
      const cmd = terminal.codexLoginCommand(platform, dir);
      assert.ok(cmd.args.every((a) => typeof a === 'string'), `${platform}: argv of strings`);
      assert.strictEqual(cmd.sh.split(terminal.shQuote(dir)).length - 1, 1, `${platform}: the directory exactly once`);
      assert.match(cmd.sh, /^CODEX_HOME='.*' codex login/);
    }
    // AppleScript gets that same line back once its own escaping is undone.
    const mac = terminal.codexLoginCommand('darwin', dir);
    const script = mac.args[1].match(/do script "((?:[^"\\]|\\.)*)"$/);
    assert.ok(script, mac.args[1]);
    assert.strictEqual(script[1].replace(/\\(.)/g, '$1'), mac.sh);
    // The shell quoting round-trips, where there is a shell to ask.
    if (terminal.existeEnPath('sh')) {
      const out = execFileSync('sh', ['-c', `printf %s ${terminal.shQuote(dir)}`], { encoding: 'utf8' });
      assert.strictEqual(out, dir);
    }
  });

  await checkAsync('PATCH /api/accounts/:id renombra, y rechaza lo que no es un nombre', async () => {
    const realFetch = global.fetch;
    const server = require('./server').createServer(7997);
    const blob = { accessToken: FAKE_TOKEN, refreshToken: null, expiresAt: Date.now() + 60000, scopes: ['user:inference'] };
    const a = store.add({ label: null, email: null, profile: null, oauth: blob });
    try {
      await new Promise((r) => server.listen(7997, '127.0.0.1', r));
      const patch = async (id, body) => {
        const res = await realFetch(`http://127.0.0.1:7997/api/accounts/${id}`, {
          method: 'PATCH',
          headers: { 'Content-Type': 'application/json', 'X-Swapper': '1' },
          body: JSON.stringify(body),
        });
        return { status: res.status, body: await res.json() };
      };

      const ok = await patch(a.id, { label: '  Trabajo  ' });
      assert.strictEqual(ok.status, 200);
      assert.strictEqual(store.get(a.id).label, 'Trabajo', 'debe recortar los espacios');
      assert.strictEqual(ok.body.account.label, 'Trabajo');
      // La respuesta viaja por publicAccount, así que sigue sin llevar el token.
      assert.ok(!JSON.stringify(ok.body).includes('sk-ant-'), 'la respuesta del PATCH filtró un token');

      // Un nombre en blanco no es un nombre: debe rechazarse, no borrar el que había.
      assert.strictEqual((await patch(a.id, { label: '   ' })).status, 400);
      assert.strictEqual(store.get(a.id).label, 'Trabajo', 'un nombre vacío no debe pisar el bueno');

      assert.strictEqual((await patch('acc_nolaexiste', { label: 'X' })).status, 404);
    } finally {
      global.fetch = realFetch;
      store.remove(a.id);
      await new Promise((r) => server.close(r));
    }
  });

  /* ---------------- tier: el plan de la cuenta (Free, Pro, Max, Ultra) ---------------- */

  // Desde el 2026-10-30 "Max 5x" se llama Max y "Max 20x" Ultra, que en una cuenta verificada no
  // tiene ventana de 5 horas: solo la semanal. Un setup-token no deja leer el plan: lo dice el usuario.
  const tokenDe = (c) => ['sk', 'ant', 'oat01', c.repeat(24)].join('-');
  const scope403 = async () => ({
    ok: false, status: 403, headers: { get: () => null },
    text: async () => JSON.stringify({ error: { message: 'OAuth token does not meet scope requirement user:profile' } }),
  });
  /** Un servidor de verdad en un puerto libre, y un fetch a él que no pasa por el stub de red. */
  const conServidor = async (fn) => {
    const realFetch = global.fetch;
    const server = require('./server').createServer(0);
    await new Promise((r) => server.listen(0, '127.0.0.1', r));
    const base = `http://127.0.0.1:${server.address().port}`;
    const call = async (method, p, body) => {
      const res = await realFetch(base + p, {
        method, headers: { 'Content-Type': 'application/json', 'X-Swapper': '1' },
        body: body === undefined ? undefined : JSON.stringify(body),
      });
      const text = await res.text();
      assert.ok(!/sk-ant-/.test(text), `una respuesta filtró un token: ${text.slice(0, 120)}`);
      return { status: res.status, body: JSON.parse(text) };
    };
    try { await fn(call); } finally {
      global.fetch = realFetch;
      await new Promise((r) => server.close(r));
    }
  };

  check('tier: el declarado manda en planLabel, y sin él se deduce del rate-limit con los nombres nuevos', () => {
    const { planLabel, tierFromRateLimit, effectiveTier } = store;
    for (const [tier, label] of [['free', 'Free'], ['pro', 'Pro'], ['max', 'Max'], ['ultra', 'Ultra']]) {
      assert.strictEqual(planLabel({ tier }), label, `${tier} -> ${label}`);
    }
    const prof = (t) => ({ profile: { organizationRateLimitTier: t } });
    // Lo que el usuario eligió gana a lo que diga el perfil.
    assert.strictEqual(planLabel({ tier: 'pro', ...prof('default_claude_max_20x') }), 'Pro');
    assert.strictEqual(planLabel(prof('default_claude_max_20x')), 'Ultra', 'max_20x es Ultra');
    assert.strictEqual(planLabel(prof('default_claude_max_5x')), 'Max', 'max_5x es Max');
    assert.strictEqual(planLabel(prof('default_claude_pro')), 'Pro');
    assert.strictEqual(planLabel({ oauth: { subscriptionType: 'max', rateLimitTier: 'default_claude_max_20x' } }), 'Ultra',
      'también desde el rateLimitTier guardado junto a los tokens');
    assert.strictEqual(planLabel({ profile: null, oauth: { scopes: ['user:inference'] } }), null, 'sin nada que leer, no inventa');

    assert.strictEqual(tierFromRateLimit(prof('default_claude_max_20x')), 'ultra');
    assert.strictEqual(tierFromRateLimit(prof('default_claude_max_5x')), 'max');
    assert.strictEqual(tierFromRateLimit({ oauth: { rateLimitTier: 'default_claude_pro' } }), 'pro');
    assert.strictEqual(tierFromRateLimit(prof(null)), null);
    assert.strictEqual(tierFromRateLimit({}), null);
    assert.strictEqual(tierFromRateLimit(null), null);
    assert.strictEqual(effectiveTier({ tier: null, ...prof('default_claude_max_20x') }), 'ultra', 'sin declarar: lo que reporta Anthropic');
    assert.strictEqual(effectiveTier({ tier: 'max', ...prof('default_claude_max_20x') }), 'max');
    assert.strictEqual(effectiveTier({ tier: 'oro' }), null, 'un valor corrupto en disco no cuenta');
  });

  check('tier: add() lo guarda, volver a pegar sin tier no lo borra, update() lo cambia o lo limpia, publicView lo expone', () => {
    const blob = { accessToken: tokenDe('T'), refreshToken: null, expiresAt: Date.now() + 60000, scopes: ['user:inference'] };
    const a = store.add({ label: 'Grande', email: null, profile: null, oauth: blob, tier: 'ultra' });
    const view = () => store.publicAccount(a.id);
    try {
      assert.strictEqual(store.get(a.id).tier, 'ultra');
      assert.strictEqual(view().tier, 'ultra');
      assert.strictEqual(view().plan, 'Ultra', 'el plan aparece aunque el token no tenga perfil');

      store.add({ label: null, email: null, profile: null, oauth: blob });
      assert.strictEqual(store.get(a.id).tier, 'ultra', 'volver a pegar sin tier no lo borra');
      store.add({ label: null, email: null, profile: null, oauth: blob, tier: 'oro' });
      assert.strictEqual(store.get(a.id).tier, 'ultra', 'ni con uno que no es de los cuatro');
      store.add({ label: null, email: null, profile: null, oauth: blob, tier: 'max' });
      assert.strictEqual(store.get(a.id).tier, 'max', 'con tier, lo sustituye');

      store.update(a.id, { tier: 'free' });
      assert.strictEqual(view().tier, 'free');
      store.update(a.id, { tier: 'oro' });
      assert.strictEqual(view().tier, 'free', 'update ignora lo que no es uno de los cuatro');
      store.update(a.id, { tier: null });
      assert.strictEqual(store.get(a.id).tier, null);
      assert.strictEqual(view().tier, null, 'null es "desconocido"');
      assert.strictEqual(view().plan, null);
      assert.ok(!JSON.stringify(store.publicView()).includes('sk-ant-'), 'publicView filtró un token');
    } finally { store.remove(a.id); }

    // Nueva y sin tier: null explícito, no el campo ausente.
    const b = store.add({ label: null, email: null, profile: null, oauth: { ...blob, accessToken: tokenDe('W') } });
    try {
      assert.strictEqual(store.get(b.id).tier, null);
      assert.strictEqual(store.publicAccount(b.id).tier, null);
      assert.strictEqual(store.publicAccount(b.id).tierDeclared, null);
    } finally { store.remove(b.id); }

    // Deducido de Anthropic: tier dice Ultra, tierDeclared null; declararlo lo distingue.
    const c = store.add({ label: null, email: null, profile: { organizationRateLimitTier: 'default_claude_max_20x' }, oauth: { ...blob, accessToken: tokenDe('X') } });
    try {
      assert.strictEqual(store.publicAccount(c.id).tier, 'ultra');
      assert.strictEqual(store.publicAccount(c.id).tierDeclared, null, 'solo deducido, no declarado');
      store.update(c.id, { tier: 'max' });
      assert.strictEqual(store.publicAccount(c.id).tier, 'max');
      assert.strictEqual(store.publicAccount(c.id).tierDeclared, 'max');
    } finally { store.remove(c.id); }
  });

  await checkAsync('POST /api/accounts/token: tier válido se guarda, inválido es 400 sin tocar la red, y volver a pegar sin él no lo borra', async () => {
    const token = tokenDe('V');
    const id = store.idForToken(token);
    try {
      await conServidor(async (call) => {
        let probes = 0;
        global.fetch = async () => { probes++; return scope403(); };
        for (const tier of ['oro', '', 'ULTRA', 5, {}]) {
          const bad = await call('POST', '/api/accounts/token', { token, tier });
          assert.strictEqual(bad.status, 400, `tier ${JSON.stringify(tier)}`);
          assert.match(bad.body.error, /Tipo de cuenta no válido/);
        }
        assert.strictEqual(probes, 0, 'un tier inválido se corta antes de la sonda');
        assert.strictEqual(store.get(id), null, 'y no escribe nada');

        const ok = await call('POST', '/api/accounts/token', { token, label: 'Grande', tier: 'ultra' });
        assert.strictEqual(ok.status, 200);
        assert.strictEqual(ok.body.account.tier, 'ultra');
        assert.strictEqual(ok.body.account.plan, 'Ultra');
        assert.strictEqual(store.get(id).tier, 'ultra');

        // Un cliente antiguo no manda tier; el importador de Paseo manda null si el export no lo traía.
        assert.strictEqual((await call('POST', '/api/accounts/token', { token })).body.account.tier, 'ultra', 'sin tier no se borra');
        assert.strictEqual((await call('POST', '/api/accounts/token', { token, tier: null })).body.account.tier, 'ultra', 'null tampoco');
        assert.strictEqual((await call('POST', '/api/accounts/token', { token, tier: 'max' })).body.account.tier, 'max', 'otro tier lo cambia');
        assert.strictEqual(store.get(id).label, 'Grande');
      });
    } finally { store.remove(id); }
  });

  await checkAsync('POST /api/accounts/token sin tier: null en un setup-token, y el de su perfil en uno de scope completo', async () => {
    const inference = tokenDe('X');
    const full = tokenDe('Y');
    const fullId = store.idFor({ accountUuid: 'uuid-tier-full' });
    try {
      await conServidor(async (call) => {
        global.fetch = scope403;
        const a = await call('POST', '/api/accounts/token', { token: inference });
        assert.strictEqual(a.status, 200);
        assert.strictEqual(a.body.account.tier, null, 'sin tier ni perfil: desconocido');
        assert.strictEqual(store.get(a.body.account.id).tier, null);

        // probeToken y fetchProfile preguntan al mismo endpoint: 200 con el perfil para los dos.
        global.fetch = async () => ({
          ok: true, status: 200, headers: { get: () => null },
          text: async () => JSON.stringify({
            account: { uuid: 'uuid-tier-full', email: 'full@x.y' },
            organization: { rate_limit_tier: 'default_claude_max_5x', organization_type: 'claude_max' },
          }),
        });
        const b = await call('POST', '/api/accounts/token', { token: full });
        assert.strictEqual(b.status, 200);
        assert.strictEqual(b.body.kind, 'full');
        assert.strictEqual(b.body.account.id, fullId);
        assert.strictEqual(b.body.account.tier, 'max', 'max_5x del perfil: Max, deducido al leer');
        assert.strictEqual(b.body.account.plan, 'Max');
        assert.strictEqual(store.get(fullId).tier, null, 'no declarado: se guarda null y se deduce al leer');
      });
    } finally {
      store.remove(store.idForToken(inference));
      store.remove(fullId);
    }
  });

  await checkAsync('PATCH /api/accounts/:id cambia el tier, null lo limpia y uno inválido es 400 sin tocar nada', async () => {
    const blob = { accessToken: tokenDe('Z'), refreshToken: null, expiresAt: Date.now() + 60000, scopes: ['user:inference'] };
    const a = store.add({ label: 'Plan', email: null, profile: null, oauth: blob, tier: 'pro' });
    try {
      await conServidor(async (call) => {
        const up = await call('PATCH', `/api/accounts/${a.id}`, { tier: 'ultra' });
        assert.strictEqual(up.status, 200);
        assert.strictEqual(up.body.account.tier, 'ultra');
        assert.strictEqual(up.body.account.plan, 'Ultra');

        for (const tier of ['diamante', '', 'Max', 1]) {
          const bad = await call('PATCH', `/api/accounts/${a.id}`, { tier, label: 'Otro' });
          assert.strictEqual(bad.status, 400, `tier ${JSON.stringify(tier)}`);
          assert.match(bad.body.error, /Tipo de cuenta no válido/);
        }
        assert.strictEqual(store.get(a.id).tier, 'ultra', 'un tier inválido no cambia nada');
        assert.strictEqual(store.get(a.id).label, 'Plan', 'ni el nombre que venía con él');

        const both = await call('PATCH', `/api/accounts/${a.id}`, { tier: 'max', color: '#112233' });
        assert.strictEqual(both.body.account.tier, 'max');
        assert.strictEqual(both.body.account.color, '#112233');

        const cleared = await call('PATCH', `/api/accounts/${a.id}`, { tier: null });
        assert.strictEqual(cleared.status, 200);
        assert.strictEqual(cleared.body.account.tier, null, 'null lo devuelve a desconocido');
        assert.strictEqual(store.get(a.id).tier, null);

        assert.strictEqual((await call('PATCH', '/api/accounts/acc_nolaexiste', { tier: 'pro' })).status, 404);
      });
    } finally { store.remove(a.id); }
  });

  await checkAsync('import: el tier sale del perfil o del rateLimitTier, y si no dicen nada se respeta el que había', async () => {
    const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'swapper-tier-import-'));
    const id = store.idFor({ accountUuid: 'uuid-tier-import' });
    const sesion = (rateLimitTier) => {
      fs.writeFileSync(path.join(tmp, '.credentials.json'), JSON.stringify({ claudeAiOauth: {
        accessToken: 'IMPORT-TIER', refreshToken: 'R-IMPORT-TIER', expiresAt: Date.now() + 3600e3,
        scopes: ['user:inference', 'user:profile'], subscriptionType: 'max', rateLimitTier,
      } }));
      fs.writeFileSync(path.join(tmp, '.claude.json'), '{}');
    };
    const perfil = (rateLimitTier) => async () => ({
      ok: true, status: 200, headers: { get: () => null },
      text: async () => JSON.stringify({ account: { uuid: 'uuid-tier-import', email: 'imp@x.y' }, organization: { rate_limit_tier: rateLimitTier } }),
    });
    try {
      await conServidor(async (call) => {
        sesion(null);
        global.fetch = perfil('default_claude_max_20x');
        const a = await call('POST', '/api/accounts/import', { configDir: tmp });
        assert.strictEqual(a.status, 200);
        assert.strictEqual(a.body.account.id, id);
        assert.strictEqual(a.body.account.tier, 'ultra');
        assert.strictEqual(store.get(id).tier, 'ultra', 'un login completo guarda el plan que dice');

        // Ni el perfil ni los tokens lo dicen: el que eligió el usuario se queda.
        store.update(id, { tier: 'pro' });
        global.fetch = perfil(null);
        assert.strictEqual((await call('POST', '/api/accounts/import', { configDir: tmp })).body.account.tier, 'pro');
        assert.strictEqual(store.get(id).tier, 'pro');

        // Sin rate_limit_tier en el perfil, el rateLimitTier que Claude Code guardó con los tokens.
        sesion('default_claude_max_5x');
        assert.strictEqual((await call('POST', '/api/accounts/import', { configDir: tmp })).body.account.tier, 'max');
        assert.strictEqual(store.get(id).tier, 'max');
      });
    } finally {
      store.remove(id);
      fs.rmSync(tmp, { recursive: true, force: true });
    }
  });

  await checkAsync('rotación con una cuenta Ultra: dispara por la semana, ordena por la semana y una sesión presente aún excluye', async () => {
    const auto = require('./lib/auto');
    auto.save({ enabled: true, target: 'host', threshold: 90, lastSwapAt: 0 });
    const live = { accessToken: 't', expiresAt: Date.now() + 1e6 };
    const accounts = [
      { id: 'cur', label: 'Cur', tier: 'ultra', oauth: { ...live } },
      { id: 'pro', label: 'Pro', tier: 'pro', oauth: { ...live } },
      // Ultra sin declarar, deducido del perfil: cuenta igual.
      { id: 'ult', label: 'Ult', tier: null, profile: { organizationRateLimitTier: 'default_claude_max_20x' }, oauth: { ...live } },
      // Ultra todavía sin verificar: tiene un límite temporal de 5 h, y lleno.
      { id: 'temp', label: 'Temp', tier: 'ultra', oauth: { ...live } },
      { id: 'semana', label: 'Semana', tier: 'ultra', oauth: { ...live } },
    ];
    // Una ventana ausente sale de usage.meter como 0% con severity 'unknown'.
    const absent = { percent: 0, resetsAt: null, severity: 'unknown' };
    const readings = {
      cur: { ok: true, session: { percent: 99 }, weekly: { percent: 50 } },
      pro: { ok: true, session: { percent: 30 }, weekly: { percent: 5 } },
      ult: { ok: true, session: absent, weekly: { percent: 20 } },
      temp: { ok: true, session: { percent: 95 }, weekly: { percent: 1 } },
      semana: { ok: true, session: absent, weekly: { percent: 93 } },
    };
    let liveId = 'cur';
    const fakeStore = { list: () => accounts, get: (id) => accounts.find((a) => a.id === id) || null, activeFor: () => liveId };
    const fakeUsage = {
      fetchFor: async (a) => readings[a.id],
      fetchAll: async (list) => Object.fromEntries(list.map((a) => [a.id, readings[a.id]])),
      cachedFor: (id) => readings[id],
    };
    const calls = [];
    const swap = {
      asTarget: () => ({ claudeJsonPath: path.join(SANDBOX, 'no-existe.json') }),
      swapTo: async (id) => { calls.push(id); },
    };
    const deps = { store: fakeStore, usage: fakeUsage, swap };
    try {
      // Una Ultra verificada no tiene ventana de 5 h: con la semana al 50% no rota.
      readings.cur = { ok: true, session: absent, weekly: { percent: 50 } };
      assert.strictEqual(await auto.tick(deps), null, 'Ultra con la semana bajo el umbral: no rota');
      assert.deepStrictEqual(calls, []);

      // La cola: elegibles por su clave (Ultra la semana, las demás la sesión), luego las excluidas.
      const st = await auto.status(deps);
      assert.deepStrictEqual(st.queue.map((q) => [q.id, q.eligible, q.reason]), [
        ['ult', true, null], ['pro', true, null],
        ['temp', false, 'sesión 95% ≥ 90%'], ['semana', false, 'semana 93% ≥ 90%'],
      ]);
      assert.strictEqual(st.next.id, 'ult', 'la Ultra con la semana al 20% va antes que la Pro con la sesión al 30%');
      assert.strictEqual(st.current.tier, 'ultra', 'el brief lleva el tier');
      assert.deepStrictEqual(st.queue.map((q) => q.tier), ['ultra', 'pro', 'ultra', 'ultra'], 'también el deducido');
      for (const k of ['id', 'label', 'email', 'sessionPercent', 'weeklyPercent', 'sessionResetsAt', 'weeklyResetsAt']) assert.ok(k in st.current, k);

      // Una Ultra sin verificar que SÍ informa una ventana de 5 h llena rota por ella, igual que esa
      // misma lectura la excluye como candidata.
      readings.cur = { ok: true, session: { percent: 99 }, weekly: { percent: 50 } };
      const rs = await auto.tick(deps);
      assert.strictEqual(rs.window, 'session');
      assert.strictEqual(rs.at, 99);
      assert.deepStrictEqual(calls, ['ult']);
      auto.save({ ...auto.load(), lastSwapAt: 0 });

      readings.cur = { ok: true, session: absent, weekly: { percent: 92 } };
      const r = await auto.tick(deps);
      assert.deepStrictEqual(calls, ['ult', 'ult']);
      assert.strictEqual(r.rotated, true);
      assert.strictEqual(r.window, 'weekly');
      assert.strictEqual(r.at, 92);
      assert.strictEqual(await auto.tick(deps), null, 'el cooldown de 5 minutos sigue igual');
      assert.strictEqual(calls.length, 2);

      // Una cuenta que no es Ultra sigue rotando por su sesión, como siempre.
      auto.save({ ...auto.load(), lastSwapAt: 0 });
      liveId = 'pro';
      readings.pro = { ok: true, session: { percent: 91 }, weekly: { percent: 5 } };
      const r2 = await auto.tick(deps);
      assert.strictEqual(r2.window, 'session');
      assert.strictEqual(r2.at, 91);
      assert.deepStrictEqual(calls, ['ult', 'ult', 'ult']);

      // Semana llena y ninguna otra cuenta con margen: un barrido, y luego espera en vez de gastar
      // la cuota de todas las demás cada 3 minutos durante días.
      auto.save({ ...auto.load(), lastSwapAt: 0 });
      liveId = 'cur';
      readings.cur = { ok: true, session: absent, weekly: { percent: 97 } };
      for (const id of ['pro', 'ult', 'temp', 'semana']) readings[id] = { ok: true, session: absent, weekly: { percent: 96 } };
      let sweeps = 0;
      const fetchAll = fakeUsage.fetchAll;
      fakeUsage.fetchAll = async (list) => { sweeps++; return fetchAll(list); };
      const none = await auto.tick(deps);
      assert.strictEqual(none.rotated, false);
      assert.strictEqual(sweeps, 1);
      assert.strictEqual(await auto.tick(deps), null, 'dentro de la espera no vuelve a barrer');
      assert.strictEqual(sweeps, 1);
      auto.set({ threshold: 90 }); // cambiar un ajuste levanta la espera
      await auto.tick(deps);
      assert.strictEqual(sweeps, 2);
      assert.ok(auto.WEEKLY_IDLE_MS >= 15 * 60 * 1000);
      assert.deepStrictEqual(calls, ['ult', 'ult', 'ult'], 'nadie con margen: no cambia');
    } finally {
      auto.save({ enabled: false, target: 'host', threshold: 90, lastSwapAt: 0 });
    }
  });

  await checkAsync('skills: una cuenta Ultra muestra "—" en la columna 5h y "más libre" se decide por su semana', async () => {
    const http = require('node:http');
    const { execFile } = require('node:child_process');
    const accounts = [
      { id: 'acc_a', label: 'Activa', email: null, tier: 'pro' },
      { id: 'acc_p', label: 'ProMedia', email: null, tier: 'pro' },
      { id: 'acc_u', label: 'UltraLibre', email: null, tier: 'ultra' },
      { id: 'acc_t', label: 'UltraTemp', email: null, tier: 'ultra' },
      { id: 'acc_n', label: 'Antigua', email: null }, // un panel sin tier: como siempre
    ];
    const absent = { percent: 0, resetsAt: null, severity: 'unknown' };
    const usageAll = {
      acc_a: { ok: true, session: { percent: 70 }, weekly: { percent: 40 } },
      acc_p: { ok: true, session: { percent: 30 }, weekly: { percent: 10 } },
      acc_u: { ok: true, session: absent, weekly: { percent: 20 } },
      acc_t: { ok: true, session: { percent: 95 }, weekly: { percent: 2 } },
      acc_n: { ok: true, session: { percent: 50 }, weekly: { percent: 60 } },
    };
    const brief = (a, s, w, extra) => ({ id: a.id, label: a.label, email: null, tier: a.tier || null, sessionPercent: s, weeklyPercent: w, sessionResetsAt: null, weeklyResetsAt: null, ...extra });
    const autoStatus = {
      enabled: true, target: 'host', threshold: 90,
      current: brief(accounts[2], 0, 20),
      next: brief(accounts[1], 30, 10),
      queue: [
        brief(accounts[1], 30, 10, { eligible: true, reason: null, role: 'next' }),
        brief(accounts[3], 95, 2, { eligible: false, reason: 'sesión 95% ≥ 90%', role: '' }),
      ],
    };
    const fake = http.createServer((req, res) => {
      const p = new URL(req.url, 'http://x').pathname;
      const body = p === '/api/accounts' ? { activeId: 'acc_a', accounts } : p === '/api/usage/all' ? usageAll : p === '/api/auto' ? autoStatus : null;
      res.writeHead(body ? 200 : 404, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify(body || { error: 'no' }));
    });
    await new Promise((r) => fake.listen(0, '127.0.0.1', r));
    const skill = path.join(__dirname, 'skills', 'swapper', 'swapper.mjs');
    const run = (args) => new Promise((resolve, reject) => {
      execFile(process.execPath, [skill, ...args], { env: { ...process.env, PORT: String(fake.address().port) }, timeout: 20000 },
        (err, stdout, stderr) => (err ? reject(new Error(stderr || err.message)) : resolve(stdout)));
    });
    try {
      const out = await run(['usage']);
      const row = (text, name) => text.split(/\r?\n/).find((l) => l.includes(name)) || '';
      assert.match(row(out, 'UltraLibre'), /UltraLibre\s+—\s+20%/, 'Ultra: "—" en 5h, la semana en 7d');
      assert.match(row(out, 'UltraLibre'), /^ {2}→ .*más libre/, 'la Ultra con la semana al 20% gana a la Pro con la sesión al 30%');
      assert.match(row(out, 'UltraTemp'), /UltraTemp\s+—\s+2%/, 'el "—" aunque traiga una sesión temporal');
      assert.doesNotMatch(row(out, 'UltraTemp'), /más libre/, 'esa sesión temporal llena la deja fuera');
      assert.match(row(out, 'ProMedia'), /ProMedia\s+30%\s+10%/);
      assert.match(row(out, 'Antigua'), /Antigua\s+50%\s+60%/, 'sin tier, igual que antes');

      const st = await run(['auto', 'status']);
      assert.match(row(st, 'UltraLibre'), /^ {2}● UltraLibre\s+—\s+20%\s+en uso/);
      assert.match(row(st, 'UltraTemp'), /UltraTemp\s+—\s+2%\s+excluida · sesión 95%/);
      assert.match(row(st, 'ProMedia'), /ProMedia\s+30%\s+10%\s+siguiente/);
    } finally {
      await new Promise((r) => fake.close(r));
    }
  });

  /* ---------------- Codex ---------------- */

  // Nothing here may read or write the real ~/.codex or a WSL one: CODEX_HOME points at a temp
  // dir, and target discovery is replaced by that host alone (the real list() would find the
  // WSL distros). Every JWT is built at runtime, and every network call is a stub.
  const CODEX_TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'swapper-codex-'));
  const realFetchForTests = global.fetch;
  process.env.CODEX_HOME = path.join(CODEX_TMP, 'host-home');
  const codexAuth = require('./lib/codex/auth');
  const codexStore = require('./lib/codex/store');
  const codexTargets = require('./lib/codex/targets');
  const codexUsage = require('./lib/codex/usage');
  let codexTargetList = [codexTargets.hostTarget()];
  codexTargets.list = () => codexTargetList;

  const b64 = (o) => Buffer.from(JSON.stringify(o)).toString('base64url');
  const fakeJwt = (payload) => `${b64({ alg: 'RS256', typ: 'JWT' })}.${b64(payload)}.${'s'.repeat(43)}`;
  const AUTH_CLAIM = 'https://api.openai.com/auth';
  let rtSeq = 0;
  /** An auth.json `tokens` block for a ChatGPT workspace, as Codex would write it. */
  const codexTokens = (accountId, { email = `${accountId}@x.test`, expSecs = 240 * 3600, rt } = {}) => {
    const claim = { chatgpt_account_id: accountId, chatgpt_plan_type: 'plus', chatgpt_user_id: `user-${accountId}` };
    return {
      id_token: fakeJwt({ email, [AUTH_CLAIM]: claim }),
      access_token: fakeJwt({ exp: Math.floor(Date.now() / 1000) + expSecs, n: ++rtSeq, [AUTH_CLAIM]: claim }),
      refresh_token: rt || `rt.a.${accountId}-${rtSeq}`,
      account_id: accountId,
    };
  };
  const addCodexAccount = (accountId, opts) => {
    const tokens = codexTokens(accountId, opts);
    const who = codexAuth.identity(tokens);
    return codexStore.add({ accountId, email: who.email, plan: who.plan, oauth: codexAuth.toStored(tokens, new Date().toISOString()) });
  };
  const codexTarget = (name) => {
    const home = path.join(CODEX_TMP, name);
    return { id: `dir:${name}`, kind: 'dir', label: name, home, authPath: path.join(home, 'auth.json') };
  };
  const res = (status, body, headers = {}) => ({
    ok: status >= 200 && status < 300, status,
    headers: { get: (h) => headers[h.toLowerCase()] ?? null },
    text: async () => (typeof body === 'string' ? body : JSON.stringify(body)),
  });
  const nowS = () => Math.floor(Date.now() / 1000);
  const FIVE_H = { used_percent: 12, limit_window_seconds: 18000, reset_after_seconds: 3600, reset_at: nowS() + 3600 };
  const WEEK = { used_percent: 2, limit_window_seconds: 604800, reset_after_seconds: 86400, reset_at: nowS() + 86400 };
  const whamBody = (primary, secondary) => ({ plan_type: 'plus', rate_limit: { allowed: true, limit_reached: false, primary_window: primary, secondary_window: secondary } });

  await checkAsync('codex: el uso se mapea por la longitud de la ventana, 401 pide login y un 429 sirve la última lectura', async () => {
    const realFetch = global.fetch;
    const calls = [];
    try {
      codexUsage.invalidate();
      codexUsage.resetCooldown();
      const acc = addCodexAccount('acct-usage');
      global.fetch = async (url, opts) => { calls.push({ url: String(url), headers: opts.headers }); return res(200, whamBody(FIVE_H, WEEK)); };
      const r = await codexUsage.fetchFor(acc, { force: true });
      assert.strictEqual(r.ok, true);
      assert.strictEqual(r.session.percent, 12);
      assert.strictEqual(r.weekly.percent, 2);
      assert.strictEqual(r.session.resetsAt, new Date(FIVE_H.reset_at * 1000).toISOString());
      assert.strictEqual(calls[0].url, codexUsage.USAGE_URL);
      assert.strictEqual(calls[0].headers.Authorization, `Bearer ${acc.oauth.accessToken}`);
      assert.strictEqual(calls[0].headers['ChatGPT-Account-ID'], 'acct-usage');
      assert.strictEqual(calls[0].headers.originator, 'codex_cli_rs');

      // Order does not decide: a weekly window reported first is still the weekly meter.
      const swapped = codexUsage.normalize(whamBody(WEEK, FIVE_H), 'x');
      assert.strictEqual(swapped.session.percent, 12);
      assert.strictEqual(swapped.weekly.percent, 2);
      // With no window lengths at all, fall back to position.
      const bare = codexUsage.normalize(whamBody({ used_percent: 5 }, { used_percent: 6 }), 'x');
      assert.deepStrictEqual([bare.session.percent, bare.weekly.percent], [5, 6]);

      // A cached reading is served without a call.
      assert.strictEqual((await codexUsage.fetchFor(acc)).session.percent, 12);
      assert.strictEqual(calls.length, 1);

      // 429 after a good read: the good numbers, flagged stale, and a backoff that holds.
      codexUsage.resetCooldown();
      global.fetch = async () => { calls.push(1); return res(429, 'slow down'); };
      const stale = await codexUsage.fetchFor(acc, { force: true });
      assert.strictEqual(stale.ok, true);
      assert.strictEqual(stale.stale, true);
      assert.strictEqual(stale.staleReason, 'rate-limited');
      assert.strictEqual(stale.session.percent, 12);
      const before = calls.length;
      const again = await codexUsage.fetchFor(acc, { force: true });
      assert.strictEqual(calls.length, before, 'no call inside the backoff');
      assert.strictEqual(again.stale, true);

      // 401: the token is dead, never hidden behind stale numbers.
      codexUsage.resetCooldown();
      global.fetch = async () => res(401, { error: { message: `bad token ${acc.oauth.accessToken}` } });
      const dead = await codexUsage.fetchFor(acc, { force: true });
      assert.strictEqual(dead.ok, false);
      assert.strictEqual(dead.needsRelogin, true);
      assert.ok(!JSON.stringify(dead).includes(acc.oauth.accessToken), 'the error must not carry the token');
      assert.match(dead.error, /eyJ\*\*\*/, 'scrubbed, not dropped');

      // A Cloudflare challenge is a 403 with an HTML page: about the client, not the token. It must
      // not mark the account "sign in again".
      codexUsage.resetCooldown();
      global.fetch = async () => res(403, '<!DOCTYPE html><html><title>Just a moment...</title></html>');
      const challenged = await codexUsage.fetchFor(acc, { force: true });
      assert.notStrictEqual(challenged.needsRelogin, true, 'a bot challenge is not a dead token');
      assert.strictEqual(challenged.stale, true, 'the last good reading is served, flagged stale');
      assert.match(challenged.error || challenged.staleReason || '', /Cloudflare/);
    } finally {
      global.fetch = realFetch;
      codexUsage.invalidate();
      codexUsage.resetCooldown();
    }
  });

  const codexSwap = require('./lib/codex/swap');
  // The usage endpoint answering `status` (default 200) and the token endpoint failing loudly: a
  // swap test that reaches auth.openai.com without asking for it is a bug in the test.
  const stubCodexNet = (status = 200) => async (url) => {
    if (String(url) === codexUsage.USAGE_URL) return status === 200 ? res(200, whamBody(FIVE_H, WEEK)) : res(status, 'nope');
    throw new Error(`unexpected call to ${url}`);
  };
  /** Runs fn with a clean store, fresh targets and stubbed network, then puts everything back. */
  const withCodex = async (targetNames, fn, net = stubCodexNet()) => {
    const realFetch = global.fetch;
    const realDetect = codexTargets.detectRunning;
    const codexPaseo = require('./lib/codex/paseo');
    const realAfterSwap = codexPaseo.afterSwap;
    codexPaseo.afterSwap = async () => ({ paseo: false, reloading: 0, waiting: 0 });
    const codexDaemon = require('./lib/codex/daemon');
    const realDaemonAfterSwap = codexDaemon.afterSwap;
    codexDaemon.afterSwap = async () => ({ state: 'none' });
    const tgs = targetNames.map(codexTarget);
    codexTargetList = tgs;
    fs.rmSync(path.join(SANDBOX, 'codex'), { recursive: true, force: true });
    for (const t of tgs) fs.rmSync(t.home, { recursive: true, force: true });
    codexUsage.invalidate();
    codexUsage.resetCooldown();
    global.fetch = net;
    try {
      await fn(...tgs);
    } finally {
      global.fetch = realFetch;
      codexTargets.detectRunning = realDetect;
      codexPaseo.afterSwap = realAfterSwap;
      codexDaemon.afterSwap = realDaemonAfterSwap;
      codexTargetList = [codexTargets.hostTarget()];
    }
  };
  const writeLive = (t, tokens, extra = {}) => {
    fs.mkdirSync(t.home, { recursive: true });
    fs.writeFileSync(t.authPath, JSON.stringify({ auth_mode: 'chatgpt', OPENAI_API_KEY: null, tokens, last_refresh: new Date().toISOString(), ...extra }, null, 2));
  };
  const readLive = (t) => JSON.parse(fs.readFileSync(t.authPath, 'utf8'));

  await checkAsync('codex swap: crea auth.json en un entorno vacío y conserva las claves ajenas', async () => {
    await withCodex(['empty'], async (t) => {
      const a = addCodexAccount('acct-a');
      const b = addCodexAccount('acct-b');
      const r = await codexSwap.swapTo(a.id, t);
      assert.strictEqual(r.ok, true);
      assert.strictEqual(r.verified, true);
      assert.strictEqual(r.target, t.id);
      let live = readLive(t);
      assert.strictEqual(live.auth_mode, 'chatgpt');
      assert.strictEqual(live.tokens.account_id, 'acct-a');
      assert.strictEqual(live.tokens.access_token, a.oauth.accessToken);
      assert.ok(live.last_refresh, 'last_refresh is mandatory');
      assert.strictEqual(codexStore.activeFor(t.id), a.id);
      assert.ok(!JSON.stringify(r).includes(a.oauth.accessToken) && !JSON.stringify(r).includes(a.oauth.refreshToken), 'no token in the answer');
      assert.strictEqual(codexUsage.cachedFor(a.id).session.percent, 12, 'the verify reading primes the cache');

      fs.writeFileSync(t.authPath, JSON.stringify({ ...live, agent_identity: { keep: true } }));
      await codexSwap.swapTo(b.id, t);
      live = readLive(t);
      assert.strictEqual(live.tokens.account_id, 'acct-b');
      assert.deepStrictEqual(live.agent_identity, { keep: true }, 'a key the swap does not own survives');
      assert.strictEqual(codexStore.activeFor(t.id), b.id);
    });
  });

  await checkAsync('codex swap: un 401 al verificar deja auth.json como estaba, byte a byte, o sin crear', async () => {
    await withCodex(['had', 'none'], async (had, none) => {
      const a = addCodexAccount('acct-a');
      const b = addCodexAccount('acct-b');
      // The stored pair is what is live: same access token, so nothing to adopt.
      writeLive(had, codexAuth.toTokens(a.oauth), { extra: 1 });
      const before = fs.readFileSync(had.authPath);
      await assert.rejects(codexSwap.swapTo(b.id, had), /401[\s\S]*restaurado/);
      assert.ok(fs.readFileSync(had.authPath).equals(before), 'previous bytes restored');
      assert.strictEqual(codexStore.activeFor(had.id), a.id, 'active is what the restored file holds, not b');

      await assert.rejects(codexSwap.swapTo(b.id, none), /restaurado/);
      assert.strictEqual(fs.existsSync(none.authPath), false, 'a target that had no auth.json is left with none');
    }, stubCodexNet(401));
  });

  await checkAsync('codex: un token revocado (logout en Codex) marca la cuenta, en la lectura de uso y en el swap', async () => {
    // Medido en vivo: el logout de Codex revoca los tokens y chatgpt.com responde 401 con
    // x-openai-ide-error-code: token_revoked. La fila no puede seguir enseñando el 0% de antes.
    const revoked = async (url) => {
      if (String(url) !== codexUsage.USAGE_URL) throw new Error(`unexpected call to ${url}`);
      return { ok: false, status: 401, headers: { get: (h) => (h.toLowerCase() === 'x-openai-ide-error-code' ? 'token_revoked' : null) }, text: async () => '{"error":{"code":"token_revoked"}}' };
    };
    await withCodex(['win'], async (win) => {
      const a = addCodexAccount('acct-live');
      const b = addCodexAccount('acct-revoked');
      writeLive(win, codexAuth.toTokens(a.oauth));
      const before = fs.readFileSync(win.authPath);

      await assert.rejects(codexSwap.swapTo(b.id, win), /revocado[\s\S]*añadir cuenta[\s\S]*restaurado/);
      assert.ok(fs.readFileSync(win.authPath).equals(before), 'rolled back');
      assert.ok(codexStore.get(b.id).dead, 'the revoked account is marked, not left looking healthy');

      const c = addCodexAccount('acct-read');
      const r = await codexUsage.fetchFor(codexStore.get(c.id), { force: true });
      assert.strictEqual(r.needsRelogin, true);
      assert.match(r.error, /añadir cuenta/);
      assert.ok(codexStore.get(c.id).dead, 'a usage read that meets token_revoked marks it too');
      await assert.rejects(codexSwap.swapTo(c.id, win), (e) => e.status === 409, 'a marked account is refused before any write');
      assert.ok(fs.readFileSync(win.authPath).equals(before));
    }, revoked);
  });

  await checkAsync('codex swap: con Codex abierto avisa, y el store se queda el par rotado de la cuenta saliente', async () => {
    await withCodex(['live'], async (t) => {
      const a = addCodexAccount('acct-a');
      const b = addCodexAccount('acct-b');
      // Codex refreshed on its own: same account, new pair.
      const rotated = codexTokens('acct-a');
      writeLive(t, rotated);
      codexTargets.detectRunning = () => ({ running: true, pids: [4242] });
      const r = await codexSwap.swapTo(b.id, t);
      assert.strictEqual(r.ok, true, 'an open Codex never blocks the swap');
      assert.ok(r.warnings.some((w) => /abierto/.test(w) && /reinicie/.test(w)), r.warnings.join(' | '));
      assert.strictEqual(codexStore.get(a.id).oauth.refreshToken, rotated.refresh_token, 'outgoing pair adopted');
      assert.strictEqual(codexStore.get(a.id).oauth.accessToken, rotated.access_token);
    });
  });

  await checkAsync('codex swap: el servidor en segundo plano toma la cuenta al momento, y una al límite se avisa', async () => {
    const locked = async (url) => {
      if (String(url) !== codexUsage.USAGE_URL) throw new Error(`unexpected call to ${url}`);
      return res(200, { plan_type: 'plus', rate_limit: { allowed: false, limit_reached: true, primary_window: { ...FIVE_H, used_percent: 100 }, secondary_window: WEEK } });
    };
    await withCodex(['host'], async (t) => {
      const host = { ...t, id: 'host', kind: 'host' };
      codexTargetList = [host];
      require('./lib/codex/daemon').afterSwap = async () => ({ state: 'switched' });
      codexTargets.detectRunning = () => ({ running: true, pids: [4242] });
      const a = addCodexAccount('acct-a');
      const r = await codexSwap.swapTo(a.id, host);
      assert.ok(r.warnings.some((w) => /terminal usa ya/.test(w)), r.warnings.join(' | '));
      assert.ok(!r.warnings.some((w) => /reinicie/.test(w)), 'sessions that already switched are not told to restart');
      assert.ok(r.warnings.some((w) => /al límite de uso hasta el/.test(w)), r.warnings.join(' | '));
    }, locked);
  });

  await checkAsync('codex swap: una sesión viva que el store no conoce se importa antes de sobrescribirla', async () => {
    await withCodex(['unknown'], async (t) => {
      const b = addCodexAccount('acct-b');
      writeLive(t, codexTokens('acct-stranger', { email: 'stranger@x.test' }));
      const r = await codexSwap.swapTo(b.id, t);
      const imported = codexStore.get(codexStore.idFor('acct-stranger'));
      assert.ok(imported, 'the live session must not survive only in a backup');
      assert.strictEqual(imported.email, 'stranger@x.test');
      assert.ok(r.warnings.some((w) => /stranger@x\.test/.test(w)), 'and the answer says so');
    });
  });

  await checkAsync('codex swap: una cuenta activa en un entorno se niega en otro (409)', async () => {
    await withCodex(['x', 'y'], async (x, y) => {
      const a = addCodexAccount('acct-a');
      await codexSwap.swapTo(a.id, x);
      await assert.rejects(codexSwap.swapTo(a.id, y), (e) => e.status === 409 && /x/.test(e.message));
      assert.strictEqual(fs.existsSync(y.authPath), false, 'nothing written');
      // Stale store evidence is not a conflict: x now holds another account.
      writeLive(x, codexTokens('acct-other'));
      assert.strictEqual((await codexSwap.swapTo(a.id, y)).ok, true);
    });
  });

  await checkAsync('codex swap: almacenamiento en llavero se niega (409) sin escribir nada', async () => {
    await withCodex(['kr'], async (t) => {
      const a = addCodexAccount('acct-a');
      fs.mkdirSync(t.home, { recursive: true });
      fs.writeFileSync(path.join(t.home, 'config.toml'), 'cli_auth_credentials_store = "keyring"\n');
      await assert.rejects(codexSwap.swapTo(a.id, t), (e) => e.status === 409 && /file/.test(e.message));
      assert.strictEqual(fs.existsSync(t.authPath), false);
      assert.strictEqual(fs.existsSync(path.join(SANDBOX, 'codex', 'backups')), false, 'not even a backup');
    });
  });

  await checkAsync('codex swap: un auth.json a medio escribir se niega y no se pisa', async () => {
    await withCodex(['half'], async (t) => {
      const a = addCodexAccount('acct-a');
      fs.mkdirSync(t.home, { recursive: true });
      fs.writeFileSync(t.authPath, '{"auth_mode":"chatgpt","tok');
      await assert.rejects(codexSwap.swapTo(a.id, t), /ilegible/);
      assert.strictEqual(fs.readFileSync(t.authPath, 'utf8'), '{"auth_mode":"chatgpt","tok');
    });
  });

  await checkAsync('codex swap: 400 entorno desconocido, 404 cuenta desconocida, 409 si ya hay uno en curso', async () => {
    await withCodex(['lock'], async (t) => {
      const a = addCodexAccount('acct-a');
      await assert.rejects(codexSwap.swapTo(a.id, 'wsl:no-existe'), (e) => e.status === 400);
      await assert.rejects(codexSwap.swapTo('cdx_000000', t), (e) => e.status === 404);
      const first = codexSwap.swapTo(a.id, t);
      await assert.rejects(codexSwap.swapTo(a.id, t), (e) => e.status === 409 && /en curso/.test(e.message));
      assert.strictEqual((await first).ok, true);
      assert.strictEqual((await codexSwap.swapTo(a.id, t)).ok, true, 'the lock is released afterwards');
    });
  });

  await checkAsync('codex swap: los backups van a data/codex/backups, se podan a 20 y nunca tocan data/backups', async () => {
    await withCodex(['bk'], async (t) => {
      const a = addCodexAccount('acct-a');
      const b = addCodexAccount('acct-b');
      const claudeBackups = path.join(SANDBOX, 'backups');
      fs.mkdirSync(claudeBackups, { recursive: true });
      // More than MAX_BACKUPS of them: a pruner that also pruned data/backups would show here.
      for (let i = 0; i < 25; i++) fs.mkdirSync(path.join(claudeBackups, `2020-01-01-claude-${String(i).padStart(2, '0')}`), { recursive: true });
      const claudeBefore = fs.readdirSync(claudeBackups).sort();
      for (let i = 0; i < 25; i++) await codexSwap.swapTo(i % 2 ? a.id : b.id, t);
      const codexBackups = fs.readdirSync(path.join(SANDBOX, 'codex', 'backups'));
      assert.strictEqual(codexBackups.length, 20);
      assert.deepStrictEqual(fs.readdirSync(claudeBackups).sort(), claudeBefore, 'Claude backups untouched');
      assert.ok(fs.existsSync(path.join(SANDBOX, 'codex', 'backups', codexBackups.sort()[19], 'auth.json')));
    });
  });

  await checkAsync('codex import: desde staging guarda la cuenta y borra el auth.json de staging; una clave de API se rechaza', async () => {
    await withCodex([], async () => {
      const staging = codexSwap.stagingDir();
      assert.strictEqual(staging, path.join(SANDBOX, 'codex', 'login'));
      await assert.rejects(codexSwap.importFrom({ staging: true }), (e) => e.status === 404);
      writeLive({ home: staging, authPath: path.join(staging, 'auth.json') }, codexTokens('acct-new', { email: 'new@x.test' }));
      const acc = await codexSwap.importFrom({ staging: true });
      assert.strictEqual(acc.email, 'new@x.test');
      assert.strictEqual(acc.plan, 'plus');
      assert.strictEqual(fs.existsSync(path.join(staging, 'auth.json')), false, 'staging file deleted');
      assert.strictEqual(codexStore.activeTargetsOf(acc.id).length, 0, 'a staging import is not live anywhere');

      const other = path.join(CODEX_TMP, 'apikey-home');
      fs.mkdirSync(other, { recursive: true });
      fs.writeFileSync(path.join(other, 'auth.json'), JSON.stringify({ auth_mode: 'apikey', OPENAI_API_KEY: 'x' }));
      await assert.rejects(codexSwap.importFrom({ configDir: other }), (e) => e.status === 400 && /clave de API/.test(e.message));
    });
  });

  await checkAsync('codex import: la sesión viva de un entorno se importa y queda activa ahí', async () => {
    await withCodex(['imp'], async (t) => {
      writeLive(t, codexTokens('acct-live'));
      const acc = await codexSwap.importFrom({ target: t });
      assert.strictEqual(codexStore.activeFor(t.id), acc.id);
      assert.strictEqual(codexSwap.detectActiveId(t.id), acc.id);
      assert.ok(fs.existsSync(t.authPath), 'a live import never deletes the live file');
    });
  });

  await checkAsync('codex refresh: un token caducado se renueva, se guarda y se devuelve al auth.json que tenía el viejo', async () => {
    const posted = [];
    let fresh;
    const net = async (url, opts) => {
      if (String(url) === codexAuth.TOKEN_URL) {
        posted.push(JSON.parse(opts.body));
        fresh = codexTokens('acct-a');
        return res(200, { id_token: fresh.id_token, access_token: fresh.access_token, refresh_token: fresh.refresh_token });
      }
      return stubCodexNet()(url);
    };
    await withCodex(['holder'], async (t) => {
      // 60 s left and live in t: the Codex there refreshes at this same margin, so the panel
      // must not race it for the refresh token.
      const soon = addCodexAccount('acct-soon', { expSecs: 60 });
      writeLive(t, codexAuth.toTokens(soon.oauth));
      assert.strictEqual((await codexSwap.ensureFresh(codexStore.get(soon.id))).oauth.refreshToken, soon.oauth.refreshToken);
      assert.strictEqual(posted.length, 0, 'a live account with time left is not refreshed');

      // Past exp: that Codex is not refreshing it, so the panel does and hands the pair back.
      const a = addCodexAccount('acct-a', { expSecs: -60 });
      writeLive(t, codexAuth.toTokens(a.oauth), { agent_identity: { keep: 1 } });
      const out = await codexSwap.ensureFresh(codexStore.get(a.id));
      assert.deepStrictEqual(posted, [{ client_id: codexAuth.CLIENT_ID, grant_type: 'refresh_token', refresh_token: a.oauth.refreshToken }]);
      assert.strictEqual(out.oauth.refreshToken, fresh.refresh_token);
      assert.strictEqual(codexStore.get(a.id).oauth.refreshToken, fresh.refresh_token, 'stored before anything else');
      const live = readLive(t);
      assert.strictEqual(live.tokens.refresh_token, fresh.refresh_token, 'handed back to the file that held the old one');
      assert.strictEqual(live.tokens.account_id, 'acct-a', 'account_id kept although the endpoint does not return it');
      assert.deepStrictEqual(live.agent_identity, { keep: 1 });
      // Not due: no call at all.
      await codexSwap.ensureFresh(codexStore.get(a.id));
      assert.strictEqual(posted.length, 1);
    }, net);
  });

  await checkAsync('codex refresh: un refresh token rechazado marca la cuenta como muerta y el keep-alive no la reintenta', async () => {
    let calls = 0;
    const net = async (url) => {
      if (String(url) === codexAuth.TOKEN_URL) { calls++; return res(400, { error: { code: 'refresh_token_reused' } }); }
      return stubCodexNet()(url);
    };
    await withCodex(['kd'], async (t) => {
      const a = addCodexAccount('acct-a', { expSecs: 60 });
      await assert.rejects(codexSwap.ensureFresh(a), (e) => e.permanent === true);
      assert.ok(codexStore.get(a.id).dead, 'marked dead');
      assert.strictEqual(codexStore.publicAccount(a.id).tokenExpired, true);
      const u = await codexUsage.fetchFor(codexStore.get(a.id), { force: true });
      assert.strictEqual(u.needsRelogin, true);

      // Dead with days of access token left (the keep-alive marks them under 2 days), and a
      // good reading cached: the row still says "sign in again", and it cannot be swapped in.
      const d = addCodexAccount('acct-d');
      codexUsage.prime(d.id, codexUsage.normalize(whamBody(FIVE_H, WEEK), d.id));
      codexStore.update(d.id, { dead: 'refresh_token_reused' });
      const du = await codexUsage.fetchFor(codexStore.get(d.id));
      assert.strictEqual(du.ok, false);
      assert.strictEqual(du.needsRelogin, true, 'not the cached reading');
      await assert.rejects(codexSwap.swapTo(d.id, t), (e) => e.status === 409);
      assert.strictEqual(fs.existsSync(t.authPath), false, 'nothing written');
      const r = await codexSwap.keepAliveTick();
      assert.strictEqual(calls, 1, 'a dead account is never retried');
      assert.deepStrictEqual(r.refreshed, []);
      assert.deepStrictEqual(r.failed, [], 'skipped, not tried and failed');
    }, net);
  });

  await checkAsync('codex keep-alive: adopta el par vivo, renueva solo las cuentas ociosas cerca de caducar', async () => {
    const refreshedWith = [];
    const net = async (url, opts) => {
      if (String(url) === codexAuth.TOKEN_URL) {
        refreshedWith.push(JSON.parse(opts.body).refresh_token);
        const f = codexTokens('acct-idle');
        return res(200, { id_token: f.id_token, access_token: f.access_token, refresh_token: f.refresh_token });
      }
      return stubCodexNet()(url);
    };
    await withCodex(['ka'], async (t) => {
      const live = addCodexAccount('acct-live', { expSecs: 3600 });
      const idle = addCodexAccount('acct-idle', { expSecs: 3600 });
      const healthy = addCodexAccount('acct-healthy');
      const rotated = codexTokens('acct-live', { expSecs: 3600 });
      writeLive(t, rotated);
      const r = await codexSwap.keepAliveTick();
      assert.deepStrictEqual(refreshedWith, [idle.oauth.refreshToken], 'only the idle, near-expiry account');
      assert.strictEqual(codexStore.get(live.id).oauth.refreshToken, rotated.refresh_token, 'live pair adopted, not refreshed');
      assert.strictEqual(r.refreshed.length, 1);
      assert.strictEqual(codexStore.get(healthy.id).oauth.refreshToken, healthy.oauth.refreshToken);
    }, net);
  });

  // A token endpoint that answers a fresh pair for `accountId`, recording the refresh tokens spent.
  const tokenEndpoint = (accountId, spent, before) => async (url, opts) => {
    if (String(url) === codexAuth.TOKEN_URL) {
      spent.push(JSON.parse(opts.body).refresh_token);
      if (before) await before();
      const f = codexTokens(accountId);
      return res(200, { id_token: f.id_token, access_token: f.access_token, refresh_token: f.refresh_token });
    }
    return stubCodexNet()(url);
  };

  await checkAsync('codex refresh: lecturas de uso y keep-alive a la vez gastan el refresh token una sola vez', async () => {
    const spent = [];
    await withCodex([], async () => {
      const a = addCodexAccount('acct-a', { expSecs: 60 });
      const [r1, r2, ka] = await Promise.all([
        codexUsage.fetchFor(a, { force: true }), codexUsage.fetchFor(a, { force: true }), codexSwap.keepAliveTick(),
      ]);
      assert.deepStrictEqual(spent, [a.oauth.refreshToken], 'one POST, one refresh token');
      assert.ok(r1.ok && r2.ok, `${r1.error || ''} ${r2.error || ''}`);
      assert.deepStrictEqual(ka.failed, []);
      assert.strictEqual(codexStore.get(a.id).dead, null);
    }, tokenEndpoint('acct-a', spent, () => new Promise((r) => setTimeout(r, 20))));
  });

  await checkAsync('codex: una cuenta adoptada en un WSL que luego se para sigue contando como activa ahí (409)', async () => {
    await withCodex(['wsl', 'hostx'], async (wsl, host) => {
      const a = addCodexAccount('acct-a');
      // The user logged in natively inside that distro: a newer pair of the same account.
      writeLive(wsl, codexTokens('acct-a', { expSecs: 241 * 3600 }));
      await codexSwap.keepAliveTick();
      assert.deepStrictEqual(codexStore.activeTargetsOf(a.id), [wsl.id], 'adopting records where it is live');
      codexTargetList = [host]; // the distro stopped when idle: off the list, its file unseen
      await assert.rejects(codexSwap.swapTo(a.id, host), (e) => e.status === 409 && e.message.includes(wsl.id));
      assert.strictEqual(fs.existsSync(host.authPath), false, 'nothing written');
      // Back, and holding another account: the record follows the file.
      codexTargetList = [wsl, host];
      writeLive(wsl, codexTokens('acct-other'));
      assert.strictEqual(codexSwap.detectActiveId(wsl.id), null);
      assert.strictEqual((await codexSwap.swapTo(a.id, host)).ok, true);
    });
  });

  await checkAsync('codex swap: si Codex rota el par saliente mientras se renueva el entrante, el store se queda el nuevo', async () => {
    const spent = [];
    let t;
    let rotated;
    // What the Codex open there does during the incoming account's refresh: rotate the outgoing one.
    const codexRotates = async () => { rotated = codexTokens('acct-a', { expSecs: 241 * 3600 }); writeLive(t, rotated); };
    await withCodex(['rot'], async (tg) => {
      t = tg;
      const a = addCodexAccount('acct-a');
      const b = addCodexAccount('acct-b', { expSecs: 60 });
      writeLive(t, codexAuth.toTokens(a.oauth));
      assert.strictEqual((await codexSwap.swapTo(b.id, t)).ok, true);
      assert.deepStrictEqual(spent, [b.oauth.refreshToken]);
      assert.strictEqual(codexStore.get(a.id).oauth.refreshToken, rotated.refresh_token, 'not only in the backup');
      assert.strictEqual(readLive(t).tokens.account_id, 'acct-b');
    }, tokenEndpoint('acct-b', spent, codexRotates));
  });

  await checkAsync('codex: un auth.json que se quedó atrás tras un refresh recibe el par del store, salvo si la cuenta vive en otro sitio', async () => {
    await withCodex(['lag', 'other'], async (t, other) => {
      const a = addCodexAccount('acct-a', { expSecs: 200 * 3600 });
      writeLive(t, codexAuth.toTokens(a.oauth), { agent_identity: { keep: 1 } });
      // The panel refreshed, and handing the pair back to t failed.
      const newer = codexTokens('acct-a');
      codexStore.update(a.id, { oauth: codexAuth.toStored(newer, 'LR') });
      await codexSwap.keepAliveTick();
      const live = readLive(t);
      assert.strictEqual(live.tokens.refresh_token, newer.refresh_token, 'caught up');
      assert.deepStrictEqual(live.agent_identity, { keep: 1 });

      // Live in another environment too: a second holder of that pair would be worse than a lag.
      writeLive(t, codexAuth.toTokens(a.oauth));
      writeLive(other, newer);
      await codexSwap.keepAliveTick();
      assert.strictEqual(readLive(t).tokens.refresh_token, a.oauth.refreshToken);
    });
  });

  await checkAsync('codex: un par guardado que OpenAI rechazó no protege contra uno más antiguo pero válido', async () => {
    await withCodex(['old'], async (t) => {
      const a = addCodexAccount('acct-a');
      codexStore.update(a.id, { dead: 'refresh_token_reused' });
      const older = codexTokens('acct-a', { expSecs: 100 * 3600 });
      writeLive(t, older);
      codexSwap.adoptLive(t);
      assert.strictEqual(codexStore.get(a.id).oauth.refreshToken, older.refresh_token, 'adopted');
      assert.strictEqual(codexStore.get(a.id).dead, null);

      codexStore.update(a.id, { dead: 'refresh_token_reused', oauth: codexAuth.toStored(codexTokens('acct-a'), 'LR') });
      const dir = path.join(CODEX_TMP, 'older-home');
      fs.rmSync(dir, { recursive: true, force: true });
      const older2 = codexTokens('acct-a', { expSecs: 50 * 3600 });
      writeLive({ home: dir, authPath: path.join(dir, 'auth.json') }, older2);
      await codexSwap.importFrom({ configDir: dir });
      assert.strictEqual(codexStore.get(a.id).oauth.refreshToken, older2.refresh_token, 'imported');

      // A pair that still works stays protected from an older copy.
      const newest = codexTokens('acct-a', { expSecs: 300 * 3600 });
      codexStore.update(a.id, { oauth: codexAuth.toStored(newest, 'LR') });
      await codexSwap.importFrom({ configDir: dir });
      assert.strictEqual(codexStore.get(a.id).oauth.refreshToken, newest.refresh_token, 'kept');
    });
  });

  await checkAsync('codex swap: con backups de fecha futura el rollback restaura igual, y sin marca de ausencia no borra', async () => {
    await withCodex(['future'], async (t) => {
      const a = addCodexAccount('acct-a');
      const b = addCodexAccount('acct-b');
      // A clock that ran ahead once: 20 backups that sort after anything taken today.
      for (let i = 0; i < 20; i++) fs.mkdirSync(path.join(codexSwap.backupsDir(), `2099-01-01T00-00-00-000Z-${String(i).padStart(4, '0')}-cdx_ffffff`), { recursive: true });
      writeLive(t, codexAuth.toTokens(a.oauth), { extra: 1 });
      const before = fs.readFileSync(t.authPath);
      await assert.rejects(codexSwap.swapTo(b.id, t), /restaurado/);
      assert.ok(fs.existsSync(t.authPath) && fs.readFileSync(t.authPath).equals(before), 'restored, not deleted');
      assert.strictEqual(fs.readdirSync(codexSwap.backupsDir()).length, 20);

      const hollow = path.join(CODEX_TMP, 'hollow-backup');
      fs.mkdirSync(hollow, { recursive: true });
      assert.throws(() => codexSwap.restoreFrom(hollow, t), /ausencia/);
      assert.ok(fs.readFileSync(t.authPath).equals(before), 'no auth.json and no marker is no proof of absence');
    }, stubCodexNet(401));
  });

  await checkAsync('codex rollback: reintenta un EPERM pasajero y no deja el .restore.tmp con tokens si falla', async () => {
    await withCodex(['perm'], async (t) => {
      const a = addCodexAccount('acct-a');
      writeLive(t, codexAuth.toTokens(a.oauth));
      const backup = codexSwap.backupNow(a.id, t);
      fs.writeFileSync(t.authPath, '{}');
      const realRename = fs.renameSync;
      const eperm = () => Object.assign(new Error('EPERM: locked'), { code: 'EPERM' });
      let fails = 1;
      fs.renameSync = (...x) => { if (fails-- > 0) throw eperm(); return realRename(...x); };
      try {
        codexSwap.restoreFrom(backup.dir, t);
        assert.strictEqual(readLive(t).tokens.account_id, 'acct-a', 'a transient lock is retried');
        fs.writeFileSync(t.authPath, '{}');
        fs.renameSync = () => { throw eperm(); };
        assert.throws(() => codexSwap.restoreFrom(backup.dir, t), /EPERM/);
      } finally {
        fs.renameSync = realRename;
      }
      assert.deepStrictEqual(fs.readdirSync(t.home).filter((n) => n.endsWith('.tmp')), [], 'no token copy left behind');
    });
  });

  await checkAsync('/api/codex/*: X-Swapper obligatoria, 400/404 donde toca, y ninguna respuesta lleva un token', async () => {
    const server = require('./server').createServer(7993);
    const cp = require('node:child_process');
    const spawnReal = cp.spawn;
    const antes = process.env.SWAPPER_IN_CONTAINER;
    let lanzo = false;
    await withCodex(['http'], async (t) => {
      try {
        await new Promise((r) => server.listen(7993, '127.0.0.1', r));
        const bodies = [];
        const call = async (method, p, body, headers = { 'X-Swapper': '1' }) => {
          const r = await fetch(`http://127.0.0.1:7993${p}`, {
            method, headers: { 'Content-Type': 'application/json', ...headers },
            body: body === undefined ? undefined : JSON.stringify(body),
          });
          const text = await r.text();
          bodies.push(text);
          return { status: r.status, body: JSON.parse(text) };
        };
        // The server's own fetch goes to the stubbed network; the test's goes to the socket.
        const stub = global.fetch;
        const real = realFetchForTests;
        global.fetch = (url, opts) => (String(url).startsWith('http://127.0.0.1:7993') ? real(url, opts) : stub(url, opts));

        codexTargetList = [t, codexTargets.hostTarget()];
        const a = addCodexAccount('acct-http');
        const b = addCodexAccount('acct-http-b');
        assert.strictEqual((await call('GET', '/api/codex/accounts', undefined, {})).status, 403, 'no X-Swapper, no answer');
        const list = await call('GET', `/api/codex/accounts?target=${encodeURIComponent(t.id)}`);
        assert.strictEqual(list.status, 200);
        assert.ok('activeId' in list.body && Array.isArray(list.body.accounts));
        assert.strictEqual(list.body.accounts.length, 2);

        assert.strictEqual((await call('POST', '/api/codex/swap', { id: a.id, target: 'wsl:no-existe' })).status, 400);
        assert.strictEqual((await call('POST', '/api/codex/swap', { target: t.id })).status, 400, 'no id');
        assert.strictEqual((await call('POST', '/api/codex/swap', { id: 'cdx_000000', target: t.id })).status, 404);
        assert.strictEqual((await call('GET', '/api/codex/nope')).status, 404);
        assert.strictEqual((await call('DELETE', '/api/codex/accounts/acc_123456')).status, 404, 'a Claude id is not a Codex route');

        const swapped = await call('POST', '/api/codex/swap', { id: a.id, target: t.id });
        assert.strictEqual(swapped.status, 200);
        assert.strictEqual(swapped.body.target, t.id);
        assert.strictEqual(swapped.body.account.isActive, true);
        const targetsRes = await call('GET', '/api/codex/targets');
        assert.strictEqual(targetsRes.body.targets[0].activeId, a.id);
        assert.strictEqual(targetsRes.body.targets[0].storeMode, 'file');
        const all = await call('GET', '/api/codex/usage/all');
        assert.strictEqual(all.body[a.id].session.percent, 12, 'served from the reading the swap primed');
        assert.strictEqual((await call('GET', `/api/codex/usage?id=${a.id}`)).body.ok, true);
        assert.strictEqual((await call('GET', '/api/codex/usage?id=cdx_000000')).status, 404);
        assert.strictEqual((await call('POST', '/api/codex/swap', { id: a.id, target: 'host' })).status, 409, 'live in another environment');

        const renamed = await call('PATCH', `/api/codex/accounts/${b.id}`, { label: '  Equipo  ' });
        assert.strictEqual(renamed.body.account.label, 'Equipo');
        assert.strictEqual((await call('PATCH', `/api/codex/accounts/${b.id}`, { label: ' ' })).status, 400);

        const health = await call('GET', '/api/codex/health');
        assert.strictEqual(health.status, 200);
        for (const k of ['installed', 'running', 'pids', 'overridingEnv', 'softEnv', 'storeMode', 'container', 'paths']) assert.ok(k in health.body, k);
        assert.strictEqual(health.body.paths.home, process.env.CODEX_HOME);

        writeLive({ home: codexSwap.stagingDir(), authPath: path.join(codexSwap.stagingDir(), 'auth.json') }, codexTokens('acct-staged'));
        const imp = await call('POST', '/api/codex/accounts/import', { staging: true });
        assert.strictEqual(imp.status, 200);
        assert.strictEqual(imp.body.account.id, codexStore.idFor('acct-staged'));
        assert.strictEqual((await call('POST', '/api/codex/accounts/import', { staging: true })).status, 404, 'consumed');
        assert.deepStrictEqual(imp.body.warnings, []);
        // Another CODEX_HOME keeps its copy of the rotating refresh token: the answer says so.
        const otherHome = path.join(CODEX_TMP, 'http-other-home');
        writeLive({ home: otherHome, authPath: path.join(otherHome, 'auth.json') }, codexTokens('acct-dir'));
        const fromDir = await call('POST', '/api/codex/accounts/import', { configDir: otherHome });
        assert.strictEqual(fromDir.status, 200);
        assert.strictEqual(fromDir.body.warnings.length, 1);
        assert.match(fromDir.body.warnings[0], /refresh token/);

        process.env.SWAPPER_IN_CONTAINER = '1';
        cp.spawn = (...x) => { lanzo = true; return spawnReal(...x); };
        const term = await call('POST', '/api/codex/login/terminal', {});
        assert.strictEqual(term.status, 409);
        assert.strictEqual(lanzo, false, 'nothing opened inside a container');

        assert.strictEqual((await call('DELETE', `/api/codex/accounts/${b.id}`)).status, 200);
        assert.strictEqual(codexStore.get(b.id), null);

        for (const text of bodies) {
          assert.ok(!/eyJ[A-Za-z0-9_-]{10,}/.test(text) && !/rt\.a\./.test(text), `a response leaked a token: ${text.slice(0, 120)}`);
        }
      } finally {
        cp.spawn = spawnReal;
        if (antes === undefined) delete process.env.SWAPPER_IN_CONTAINER; else process.env.SWAPPER_IN_CONTAINER = antes;
        await new Promise((r) => server.close(r));
      }
    });
  });

  fs.rmSync(CODEX_TMP, { recursive: true, force: true });
  console.log(failures === 0 ? '\nAll checks passed.\n' : `\n${failures} check(s) failed.\n`);
  process.exit(failures === 0 ? 0 : 1);
})();