Ctrl K
ComunidadGuías…

test/gh-account.test.js

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

Archivo original ↗Preguntar a KiBot
// Which gh account a spawned agent gets for its repo (server/jobs.js,
// ghAccountFor / ghAgentEnv). No real gh: every collaborator is a stub.

import { describe, it, expect, vi } from 'vitest';
import { readFileSync } from 'fs';
import { parseGithubRemote, ghAccountFor, ghAgentEnv, ghEnvForRepo } from '../server/jobs.js';

let n = 0;
// A fresh repo path per call: the per-repo memory is module state.
function setup({ remote = 'git@github.com:bill10/agent-007.git', accounts = ['bill-slung', 'bill10'], sees = {} } = {}) {
  const repo = `/repos/r${++n}`;
  const visible = vi.fn(async (slug, token) => (sees[token] || []).includes(slug));
  const opts = {
    remoteUrl: async () => remote,
    listAccounts: vi.fn(async () => accounts),
    tokenFor: async (login) => `tok-${login}`,
    visible,
  };
  return { repo, opts, visible };
}

describe('parseGithubRemote', () => {
  it('reads owner/name from https and ssh remotes, and nothing else', () => {
    expect(parseGithubRemote('https://github.com/Slung-AI-code/milo.git')).toEqual({ owner: 'Slung-AI-code', name: 'milo' });
    expect(parseGithubRemote('git@github.com:bill10/agent-007.git\n')).toEqual({ owner: 'bill10', name: 'agent-007' });
    expect(parseGithubRemote('https://gitlab.com/a/b.git')).toBeNull();
    expect(parseGithubRemote(null)).toBeNull();
  });
});

describe('ghAccountFor', () => {
  it('picks the account named like the repo owner, without probing', async () => {
    const { repo, opts, visible } = setup();
    expect(await ghAccountFor(repo, opts)).toEqual({ login: 'bill10', token: 'tok-bill10' });
    expect(visible).not.toHaveBeenCalled();
  });

  it('otherwise the first account that can see the repo', async () => {
    const { repo, opts } = setup({ remote: 'https://github.com/Slung-AI-code/milo', sees: { 'tok-bill-slung': ['Slung-AI-code/milo'] } });
    expect((await ghAccountFor(repo, opts)).login).toBe('bill-slung');
  });

  it('is null when no account can see it, or the repo is not on github.com', async () => {
    const unseen = setup({ remote: 'https://github.com/x/y' });
    expect(await ghAccountFor(unseen.repo, unseen.opts)).toBeNull();
    const local = setup({ remote: null });
    expect(await ghAccountFor(local.repo, local.opts)).toBeNull();
    expect(local.opts.listAccounts).not.toHaveBeenCalled();
  });

  it('remembers the answer per repo, re-checking it with one probe', async () => {
    const sees = { 'tok-bill-slung': ['Slung-AI-code/milo'] };
    const { repo, opts, visible } = setup({ remote: 'https://github.com/Slung-AI-code/milo', sees });
    await ghAccountFor(repo, opts);
    opts.listAccounts.mockClear(); visible.mockClear();
    expect((await ghAccountFor(repo, opts)).login).toBe('bill-slung');
    expect(opts.listAccounts).not.toHaveBeenCalled();
    expect(visible).toHaveBeenCalledTimes(1);
  });

  it('chooses again when the remembered account gets a 404', async () => {
    const sees = { 'tok-bill-slung': ['Slung-AI-code/milo'] };
    const { repo, opts } = setup({ remote: 'https://github.com/Slung-AI-code/milo', accounts: ['bill-slung', 'bill10', 'other'], sees });
    await ghAccountFor(repo, opts);
    sees['tok-bill-slung'] = [];
    sees['tok-other'] = ['Slung-AI-code/milo'];
    expect((await ghAccountFor(repo, opts)).login).toBe('other');
  });
});

describe('ghAgentEnv', () => {
  it('sets GH_TOKEN and points git at gh for github.com credentials', () => {
    const env = ghAgentEnv('tok', {});
    expect(env).toEqual({
      GH_TOKEN: 'tok', GIT_CONFIG_COUNT: '2',
      GIT_CONFIG_KEY_0: 'credential.https://github.com.helper', GIT_CONFIG_VALUE_0: '',
      GIT_CONFIG_KEY_1: 'credential.https://github.com.helper', GIT_CONFIG_VALUE_1: '!gh auth git-credential',
    });
  });

  it("appends after the owner's own GIT_CONFIG_* entries", () => {
    const env = ghAgentEnv('tok', { GIT_CONFIG_COUNT: '1' });
    expect(env.GIT_CONFIG_COUNT).toBe('3');
    expect(env.GIT_CONFIG_KEY_1).toBe('credential.https://github.com.helper');
    expect(env).not.toHaveProperty('GIT_CONFIG_KEY_0');
  });

  it('is empty without a token, so the agent keeps whatever gh is signed in as', async () => {
    expect(ghAgentEnv(null)).toEqual({});
    const { repo, opts } = setup({ remote: 'https://github.com/x/y' });
    expect(await ghEnvForRepo(repo, opts)).toEqual({});
    expect(await ghEnvForRepo(repo, { ...opts, remoteUrl: async () => { throw new Error('boom'); } })).toEqual({});
  });

  it('never runs gh auth switch or writes hosts.yml', () => {
    const src = readFileSync('server/jobs.js', 'utf8');
    expect(src).not.toMatch(/'auth',\s*'(switch|login|logout)'/);
    expect(src).not.toMatch(/hosts\.yml['"`]/);
  });
});