Ctrl K
ComunidadGuías…

test/agent-accounts.test.js

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

Archivo original ↗Preguntar a KiBot
import { describe, it, expect, beforeEach } from 'vitest';
import { mkdtempSync, mkdirSync, writeFileSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { scanAgents } from '../server/agent-accounts.js';

// Synthetic files in a temp HOME and an injected exec: nothing here reads the
// real ~/.claude, ~/.codex or ~/.gemini, or runs a real CLI.
let home;
const put = (rel, text = '{}') => { mkdirSync(join(home, rel, '..'), { recursive: true }); writeFileSync(join(home, rel), text); };
const fakeJwt = (payload) => ['e30', Buffer.from(JSON.stringify(payload)).toString('base64url'), 'sig'].join('.');

beforeEach(() => { home = mkdtempSync(join(tmpdir(), 'a007-accounts-')); });

const scan = (opts = {}) => scanAgents({
  env: {}, home, platform: 'linux', timeoutMs: 200,
  which: (c) => (['claude', 'codex', 'gemini', 'aider'].includes(c) ? `/bin/${c}` : null),
  run: async (file, args) => (args[0] === '--version' ? { code: 0, stdout: `${file} 1.0\n`, stderr: '' } : { code: 1, stdout: '', stderr: '' }),
  ...opts,
});

describe('scanAgents', () => {
  it('lists only the CLIs that were found, each with version and path', async () => {
    const agents = await scan();
    expect(agents.map(a => a.cli)).toEqual(['claude', 'codex', 'gemini', 'aider']);
    expect(agents.find(a => a.cli === 'aider')).toEqual({ cli: 'aider', version: '/bin/aider 1.0', path: '/bin/aider', accounts: [] });
  });

  it('reads each Claude config folder with CLAUDE_CONFIG_DIR, unset for the default', async () => {
    put('.claude/settings.json');
    put('.claude-work/.claude.json');
    put('.claude_junk/notes.txt');   // not a config dir
    put('.claude-file');             // not a dir
    const seen = [];
    const agents = await scan({
      run: async (file, args, { env }) => {
        if (args[0] !== 'auth') return { code: 0, stdout: '2.0 (Claude Code)', stderr: '' };
        seen.push(env.CLAUDE_CONFIG_DIR);
        const work = env.CLAUDE_CONFIG_DIR?.endsWith('.claude-work');
        return { code: 0, stdout: JSON.stringify({ loggedIn: !work, email: work ? null : 'a@example.com', subscriptionType: 'max', orgName: 'Org', accessToken: 'SECRET' }), stderr: 'Warning: {not json}' };
      },
    });
    const claude = agents.find(a => a.cli === 'claude');
    expect(seen.sort()).toEqual([join(home, '.claude-work'), undefined].sort());
    expect(claude.accounts).toEqual([
      { folder: join(home, '.claude'), isDefault: true, email: 'a@example.com', plan: 'max', org: 'Org', loggedIn: true },
      { folder: join(home, '.claude-work'), isDefault: false, email: null, plan: 'max', org: 'Org', loggedIn: false },
    ]);
    expect(JSON.stringify(agents)).not.toContain('SECRET');
  });

  it('marks CLAUDE_CONFIG_DIR as the default when it is set', async () => {
    put('.claude/settings.json');
    put('elsewhere/.claude.json');
    const agents = await scan({ env: { CLAUDE_CONFIG_DIR: join(home, 'elsewhere') + '/' } });
    expect(agents[0].accounts.map(a => [a.folder, a.isDefault])).toEqual([[join(home, 'elsewhere'), true], [join(home, '.claude'), false]]);
  });

  it('takes the Codex email from the id_token payload and nothing else from auth.json', async () => {
    put('.codex/auth.json', JSON.stringify({ OPENAI_API_KEY: 'sk-SECRET', tokens: { id_token: fakeJwt({ email: 'c@example.com', sub: 'SECRET' }), access_token: 'SECRET', refresh_token: 'SECRET' } }));
    put('.codex-two/auth.json', '{"tokens":{"id_token":"not-a-jwt"}}');
    put('.codex-empty/config.toml');   // no auth.json: not an account
    const agents = await scan({
      env: { CODEX_HOME: undefined },
      run: async (file, args, { env }) => (args[0] === 'login'
        ? (env.CODEX_HOME.endsWith('.codex') ? { code: 0, stdout: '', stderr: 'Logged in using ChatGPT' } : { code: 1, stdout: 'Not logged in', stderr: '' })
        : { code: 0, stdout: 'codex-cli 0.1', stderr: '' }),
    });
    const codex = agents.find(a => a.cli === 'codex');
    expect(codex.accounts).toEqual([
      { folder: join(home, '.codex'), isDefault: true, email: 'c@example.com', plan: 'ChatGPT', org: null, loggedIn: true },
      { folder: join(home, '.codex-two'), isDefault: false, email: null, plan: null, org: null, loggedIn: false },
    ]);
    expect(JSON.stringify(agents)).not.toContain('SECRET');
  });

  it('reads the active Gemini account email only', async () => {
    put('.gemini/google_accounts.json', JSON.stringify({ active: 'g@example.com', old: ['x@example.com'] }));
    const gemini = (await scan()).find(a => a.cli === 'gemini');
    expect(gemini.accounts).toEqual([{ folder: join(home, '.gemini'), isDefault: true, email: 'g@example.com', plan: null, org: null, loggedIn: true }]);
  });

  it('still lists a CLI whose --version fails or cannot start', async () => {
    const agents = await scan({ run: async (file, args) => (file.endsWith('claude') ? { code: 2, stdout: 'boom', stderr: '' } : null) });
    expect(agents.find(a => a.cli === 'claude').version).toBeNull();
    expect(agents.find(a => a.cli === 'codex').version).toBeNull();
  });

  it('does not let a hanging CLI hold up the others', async () => {
    put('.claude/settings.json');
    const t = Date.now();
    const agents = await scan({
      run: (file, args) => (file.endsWith('claude') ? new Promise(() => {}) : Promise.resolve({ code: 0, stdout: 'ok 1', stderr: '' })),
    });
    expect(Date.now() - t).toBeLessThan(2000);
    const claude = agents.find(a => a.cli === 'claude');
    expect(claude.version).toBeNull();
    expect(claude.accounts[0].loggedIn).toBeNull();
    expect(agents.find(a => a.cli === 'codex').version).toBe('ok 1');
  });
});

describe('renderClis (Settings → Accounts, one line per CLI)', () => {
  it('escapes what it shows and shortens paths under the home dir, for a CLI with no login too', async () => {
    const { renderClis } = await import('../public/modules/settings.js');
    const html = renderClis([
      { cli: 'claude', version: '1.0', path: '/h/.local/bin/claude', accounts: [{ folder: '/h/.claude', isDefault: true, email: 'a@x', plan: null, org: null, loggedIn: true }] },
      { cli: 'aider', version: '<b>0.86</b>', path: '/hx/bin/aider', accounts: [] },
    ]);
    expect(html).toContain('Claude Code</span> 1.0');
    expect(html).toContain('~/.local/bin/claude');
    expect(html).toContain('aider</span> &lt;b&gt;0.86&lt;/b&gt;');
    expect(html).toContain('>/hx/bin/aider<');
    expect(html).not.toContain('a@x');
  });

  it('names claude and codex when missing, with where to get them', async () => {
    const { renderClis } = await import('../public/modules/settings.js');
    const none = renderClis([]);
    expect(none).toContain('No agent CLIs found on the PATH.');
    expect(none).toMatch(/Claude Code<\/span> · <span class="settings-status out">not installed/);
    expect(none).toContain('Install Codex: npm install -g @openai/codex, then restart Agent 007');
    const some = renderClis([{ cli: 'claude', version: '1.0', path: '/h/bin/claude', accounts: [] }]);
    expect(some).not.toMatch(/Claude Code<\/span> · <span class="settings-status out">not installed/);
    expect(some).toMatch(/Codex<\/span> · <span class="settings-status out">not installed/);
  });
});