Ctrl K
ComunidadGuías…

test/remote-setup.test.js

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

Archivo original ↗Preguntar a KiBot
import { describe, it, expect } from 'vitest';
import { existsSync, mkdtempSync, readFileSync, statSync, writeFileSync, mkdirSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { setupRemote, addOrigin } from '../server/remote-setup.js';
import { tailscaleBin, serveTargets, MAC_APP_CLI } from '../server/tailscale.js';
import { originHost } from '../server/state.js';
import { runCommand } from '../server/service.js';
import { removeTempDir } from './temp-dir.js';

const NAME = 'mini.tail1.ts.net';
const PORT = 7123;
const RUNNING = { BackendState: 'Running', Self: { DNSName: `${NAME}.` } };
const served = (proxy, host = NAME, port = 443) => ({ TCP: { [port]: { HTTPS: true } }, Web: { [`${host}:${port}`]: { Handlers: { '/': { Proxy: proxy } } } } });

// Never a real tailscale, launchd or ~/.agent-007: a temp HOME and a fake CLI.
function fakeCtx({ status = RUNNING, serve = {}, serveRun, answers = [], running = false, ...over } = {}) {
  const home = mkdtempSync(join(tmpdir(), 'a007-remote-'));
  const cfg = join(home, '.agent-007');
  const out = [], calls = [], asked = [], restarts = [];
  const ctx = {
    home, cfg, out, calls, asked, restarts,
    platform: 'darwin', tty: true, port: PORT, root: home,
    env: { HOME: home, PATH: '/usr/bin', AGENT007_CONFIG_DIR: cfg },
    tailscaleBin: () => '/usr/local/bin/tailscale',
    run: async (cmd, args) => {
      calls.push(args.join(' '));
      if (args[0] === 'status') return status ? { code: 0, stdout: JSON.stringify(status), stderr: '' } : { code: 1, stdout: '', stderr: 'failed to connect to local tailscaled' };
      if (args.join(' ') === 'serve status --json') return { code: 0, stdout: JSON.stringify(serve), stderr: '' };
      return serveRun || { code: 0, stdout: '', stderr: '' };
    },
    write: (s) => out.push(`OUT ${s}`),
    stream: async (cmd, args, o) => {
      calls.push(args.join(' '));
      const r = serveRun || { code: 0, stdout: '', stderr: '' };
      if (r.stdout) o.onData(r.stdout);
      return r;
    },
    ask: async (q) => { asked.push(q); return answers.shift() ?? ''; },
    log: (s) => out.push(s), err: (s) => out.push(`ERR ${s}`),
    cmd: (s) => `agent007 ${s}`,
    ...over,
  };
  const server = { running: async () => running, restart: async () => { restarts.push(1); } };
  return { ctx, server };
}
const text = (c) => c.out.join('\n');
const envFile = (c) => join(c.cfg, '.env');
const changes = (c) => c.calls.filter(a => a.startsWith('serve --bg'));

describe('setupRemote', () => {
  it('serves the port, adds the ts.net name to ALLOWED_ORIGINS (0600), restarts a running server', async () => {
    const { ctx, server } = fakeCtx({ running: true });
    expect(await setupRemote(ctx, server)).toBe(0);
    expect(changes(ctx)).toEqual([`serve --bg ${PORT}`]);
    const env = readFileSync(envFile(ctx), 'utf8');
    expect(env).toContain(`\nALLOWED_ORIGINS=${NAME}\n`);
    expect(env).not.toMatch(/^# ALLOWED_ORIGINS=/m);
    if (process.platform !== 'win32') expect(statSync(envFile(ctx)).mode & 0o777).toBe(0o600);
    expect(ctx.restarts).toEqual([1]);
    expect(ctx.out.at(-1)).toBe(`Open https://${NAME} on any device in your tailnet.`);
    removeTempDir(ctx.home);
  });

  it('keeps a port already served and existing origins, and changes nothing when all is in place', async () => {
    const { ctx, server } = fakeCtx({ serve: served(`http://127.0.0.1:${PORT}`), running: true });
    mkdirSync(ctx.cfg, { recursive: true });
    writeFileSync(envFile(ctx), 'PORT=7123\nALLOWED_ORIGINS=other.example\n');
    expect(await setupRemote(ctx, server)).toBe(0);
    expect(changes(ctx)).toEqual([]);
    expect(readFileSync(envFile(ctx), 'utf8')).toBe(`PORT=7123\nALLOWED_ORIGINS=other.example,${NAME}\n`);
    expect(ctx.restarts).toEqual([1]);
    ctx.out.length = 0;
    expect(await setupRemote(ctx, server)).toBe(0);
    expect(text(ctx)).toMatch(/already lets mini\.tail1\.ts\.net in/);
    expect(ctx.restarts).toEqual([1]);
    removeTempDir(ctx.home);
  });

  it('stops without changing anything when Tailscale is missing, not running or not logged in', async () => {
    for (const [over, says] of [
      [{ tailscaleBin: () => null }, /not installed.*\n.*tailscale\.com\/download/],
      [{ status: null }, /`tailscale status` failed \(failed to connect.*\n.*tailscale up/],
      [{ status: { BackendState: 'NeedsLogin' } }, /Tailscale is NeedsLogin.*\n.*Open the Tailscale app and log in, or run: \/usr\/local\/bin\/tailscale up/],
    ]) {
      const { ctx, server } = fakeCtx(over);
      expect(await setupRemote(ctx, server)).toBe(1);
      expect(text(ctx)).toMatch(says);
      expect(text(ctx)).toContain('Nothing was changed');
      expect(changes(ctx)).toEqual([]);
      expect(existsSync(envFile(ctx))).toBe(false);
      removeTempDir(ctx.home);
    }
  });

  const busy = (...ports) => ports.reduce((c, p) => {
    const w = served('http://127.0.0.1:3000', NAME, p);
    return { TCP: { ...c.TCP, ...w.TCP }, Web: { ...c.Web, ...w.Web } };
  }, { TCP: {}, Web: {} });

  it('leaves another site on 443 alone and uses the next free HTTPS port, without asking', async () => {
    for (const [ports, p] of [[[443], 8443], [[443, 8443], 10000]]) {
      const { ctx, server } = fakeCtx({ serve: busy(...ports) });
      expect(await setupRemote(ctx, server)).toBe(0);
      expect(ctx.asked).toEqual([]);
      expect(changes(ctx)).toEqual([`serve --bg --https=${p} ${PORT}`]);
      expect(readFileSync(envFile(ctx), 'utf8')).toContain(`ALLOWED_ORIGINS=${NAME}:${p}\n`);
      expect(ctx.out.at(-1)).toBe(`Open https://${NAME}:${p} on any device in your tailnet.`);
      removeTempDir(ctx.home);
    }
  });

  it('stops without changes when every HTTPS port serves something else', async () => {
    const { ctx, server } = fakeCtx({ serve: busy(443, 8443, 10000) });
    expect(await setupRemote(ctx, server)).toBe(1);
    expect(text(ctx)).toMatch(/Every HTTPS port.*http:\/\/127\.0\.0\.1:3000.*Nothing was changed/s);
    expect(changes(ctx)).toEqual([]);
    expect(existsSync(envFile(ctx))).toBe(false);
    removeTempDir(ctx.home);
  });

  it('keeps this port when already served on a non-443 HTTPS port', async () => {
    const mine = served(`http://127.0.0.1:${PORT}`, NAME, 8443);
    const { ctx, server } = fakeCtx({ serve: { TCP: { ...busy(443).TCP, ...mine.TCP }, Web: { ...busy(443).Web, ...mine.Web } } });
    expect(await setupRemote(ctx, server)).toBe(0);
    expect(changes(ctx)).toEqual([]);
    expect(ctx.out.at(-1)).toBe(`Open https://${NAME}:8443 on any device in your tailnet.`);
    removeTempDir(ctx.home);
  });

  it('prints the link Tailscale gives when Serve is not enabled on the tailnet', async () => {
    const url = 'https://login.tailscale.com/f/serve?node=abc123';
    const { ctx, server } = fakeCtx({ serveRun: { code: -1, stdout: `Serve is not enabled on your tailnet.\nTo enable, visit:\n\n         ${url}\n`, stderr: '' } });
    expect(await setupRemote(ctx, server)).toBe(1);
    expect(text(ctx)).toContain(`Serve is not enabled on your tailnet. Enable it here, then run agent007 install --remote again:\n  ${url}`);
    expect(existsSync(envFile(ctx))).toBe(false);
    removeTempDir(ctx.home);
  });

  it('shows the enable link while tailscale is still waiting, and says which machine it is', async () => {
    const url = 'https://login.tailscale.com/f/serve?node=abc123';
    const { ctx, server } = fakeCtx();
    let seenBeforeExit = false;
    ctx.stream = async (cmd, args, o) => {
      o.onData(`To enable, visit:\n ${url}\n`);
      seenBeforeExit = ctx.out.some(l => l.includes(url));
      return { code: 0, stdout: url, stderr: '' };
    };
    expect(await setupRemote(ctx, server)).toBe(0);
    expect(seenBeforeExit).toBe(true);
    expect(text(ctx)).toContain(`This machine is ${NAME}.`);
    expect(text(ctx)).toMatch(/Tailscale prints a link to enable it/);
    removeTempDir(ctx.home);
  });

  it('on timeout prints the link and the command to re-run; on Ctrl-C stops cleanly', async () => {
    const url = 'https://login.tailscale.com/f/serve?node=abc123';
    const t = fakeCtx({ serveRun: { code: -1, stdout: url, stderr: '', timedOut: true } });
    expect(await setupRemote(t.ctx, t.server)).toBe(1);
    expect(text(t.ctx)).toContain('Gave up waiting');
    expect(text(t.ctx)).toContain(url);
    expect(text(t.ctx)).toContain('agent007 install --remote');
    const i = fakeCtx({ serveRun: { code: -1, stdout: '', stderr: '', interrupted: true } });
    expect(await setupRemote(i.ctx, i.server)).toBe(130);
    expect(existsSync(envFile(i.ctx))).toBe(false);
    removeTempDir(t.ctx.home); removeTempDir(i.ctx.home);
  });

  it('stream: echoes a real child live, kills it on timeout', async () => {
    const { stream } = await import('../server/service.js');
    const seen = [];
    const r = await stream(process.execPath, ['-e', "console.log('hello'); setInterval(()=>{},1000)"], { timeout: 700, onData: (d) => seen.push(d) });
    expect(seen.join('')).toContain('hello');
    expect(r.timedOut).toBe(true);
  });

  it('--dry-run reads status and prints what it would do, changing nothing', async () => {
    const { ctx, server } = fakeCtx({ 'dry-run': true, running: true });
    expect(await setupRemote(ctx, server)).toBe(0);
    expect(changes(ctx)).toEqual([]);
    expect(text(ctx)).toContain(`Would run: /usr/local/bin/tailscale serve --bg ${PORT}`);
    expect(text(ctx)).toContain(`Would set ALLOWED_ORIGINS=${NAME}`);
    expect(existsSync(envFile(ctx))).toBe(false);
    expect(ctx.restarts).toEqual([]);
    removeTempDir(ctx.home);
  });

  it('warns when the environment sets ALLOWED_ORIGINS without the name', async () => {
    const { ctx, server } = fakeCtx();
    ctx.env.ALLOWED_ORIGINS = 'other.example';
    expect(await setupRemote(ctx, server)).toBe(0);
    expect(text(ctx)).toMatch(/also set in your environment.*add mini\.tail1\.ts\.net there too/);
    removeTempDir(ctx.home);
  });

  it('is what `install --remote` runs, alone', async () => {
    const { ctx } = fakeCtx();
    ctx.callServer = async () => null;
    ctx.readServer = () => null;
    expect(await runCommand('install', { remote: true }, ctx)).toBe(0);
    expect(changes(ctx)).toEqual([`serve --bg ${PORT}`]);
    expect(await runCommand('install', { remote: true, voice: true }, ctx)).toBe(2);
    removeTempDir(ctx.home);
  });
});

describe('tailscale helpers', () => {
  it('finds the CLI on PATH, else the macOS app bundle', () => {
    const none = { PATH: '/nonexistent' };
    expect(tailscaleBin(none, { platform: 'darwin', exists: (p) => p === MAC_APP_CLI })).toBe(MAC_APP_CLI);
    expect(tailscaleBin(none, { platform: 'linux', exists: () => true })).toBe(null);
    expect(tailscaleBin(none, { platform: 'darwin', exists: () => false })).toBe(null);
  });

  it('serveTargets: names sent to this port, other targets, busy HTTPS ports', () => {
    const cfg = { ...served(`http://localhost:${PORT}`), Foreground: { x: served('http://127.0.0.1:3000', NAME, 8443) } };
    const t = serveTargets(cfg, PORT);
    expect([...t.names]).toEqual([NAME]);
    expect(t.targets).toEqual(['http://127.0.0.1:3000']);
    expect([...t.busyPorts]).toEqual(['8443']);
    expect([...serveTargets(served(`http://localhost:${PORT}`, NAME, 8443), PORT).names]).toEqual([`${NAME}:8443`]);
  });

  it('the server lets a host:port ALLOWED_ORIGINS entry in by hostname', () => {
    expect(originHost(`${NAME}:8443`)).toBe(NAME);
  });

  it('addOrigin merges and never removes', () => {
    expect(addOrigin(undefined, NAME)).toBe(NAME);
    expect(addOrigin('a.example, b.example', NAME)).toBe(`a.example,b.example,${NAME}`);
    expect(addOrigin(`https://${NAME}`, NAME)).toBe(null);
    expect(addOrigin('*', NAME)).toBe(null);
    expect(addOrigin('a.example', `${NAME}:8443`)).toBe(`a.example,${NAME}:8443`);
    expect(addOrigin(NAME, `${NAME}:8443`)).toBe(null);
  });
});