Ctrl K
ComunidadGuías…

test/claude-trust.test.js

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

Archivo original ↗Preguntar a KiBot
// Pre-seeding Claude Code's workspace trust for board workers (server/claude-trust.js).
// Every test runs against a temp HOME: the real ~/.claude.json is never touched.
import { describe, it, expect, beforeEach } from 'vitest';
import { mkdtempSync, mkdirSync, readFileSync, writeFileSync, readdirSync, statSync, realpathSync, symlinkSync, lstatSync, existsSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { autoTrusts, trustClaudeFolder, codexTrustArgs } from '../server/claude-trust.js';
import { createSessionFromConfig } from '../server/pty.js';

let home, file, wt;
beforeEach(() => {
  home = realpathSync(mkdtempSync(join(tmpdir(), 'claude-trust-')));
  file = join(home, '.claude.json');
  wt = join(home, 'worktrees', 'Falcon');
  mkdirSync(wt, { recursive: true });
});

describe('trustClaudeFolder', () => {
  it('adds only the one entry and keeps everything else, key order and mode included', () => {
    const before = {
      numStartups: 9, theme: 'light',
      projects: { '/elsewhere': { hasTrustDialogAccepted: false, allowedTools: ['Bash'] } },
      userID: 'u1',
    };
    writeFileSync(file, JSON.stringify(before, null, 2), { mode: 0o600 });
    expect(trustClaudeFolder(wt, { home, env: {} })).toBe(true);
    const after = JSON.parse(readFileSync(file, 'utf8'));
    expect(after).toEqual({ ...before, projects: { ...before.projects, [wt]: { hasTrustDialogAccepted: true } } });
    expect(Object.keys(after)).toEqual(Object.keys(before));
    if (process.platform !== 'win32') expect(statSync(file).mode & 0o777).toBe(0o600);   // Windows has no POSIX modes
    expect(readdirSync(home).filter(f => f.includes('.tmp'))).toEqual([]);
  });

  it('keeps the fields of an entry that already exists for the path', () => {
    writeFileSync(file, JSON.stringify({ projects: { [wt]: { allowedTools: ['Read'] } } }));
    trustClaudeFolder(wt, { home, env: {} });
    expect(JSON.parse(readFileSync(file, 'utf8')).projects[wt]).toEqual({ allowedTools: ['Read'], hasTrustDialogAccepted: true });
  });

  it('records the real path, as claude sees its cwd', () => {
    writeFileSync(file, '{}');
    trustClaudeFolder(join(wt, '..', 'Falcon'), { home, env: {} });
    expect(Object.keys(JSON.parse(readFileSync(file, 'utf8')).projects)).toEqual([wt]);
  });

  it('does not rewrite a file that already trusts the path', () => {
    const text = JSON.stringify({ projects: { [wt]: { hasTrustDialogAccepted: true } } });
    writeFileSync(file, text);
    expect(trustClaudeFolder(wt, { home, env: {} })).toBe(true);
    expect(readFileSync(file, 'utf8')).toBe(text);
  });

  it('leaves a malformed file exactly as it was', () => {
    for (const text of ['{"projects": {', '[1,2]', 'null', '{"projects": [1]}', '{"projects": "x"}', `{"projects": {${JSON.stringify(wt)}: "x"}}`]) {
      writeFileSync(file, text);
      expect(trustClaudeFolder(wt, { home, env: {} })).toBe(false);
      expect(readFileSync(file, 'utf8')).toBe(text);
    }
    expect(readdirSync(home).filter(f => f.includes('.tmp'))).toEqual([]);
  });

  it('writes through a symlinked file and leaves the link in place', () => {
    const real = join(home, 'dotfiles.json');
    writeFileSync(real, '{"theme":"dark"}');
    symlinkSync(real, file);
    expect(trustClaudeFolder(wt, { home, env: {} })).toBe(true);
    expect(lstatSync(file).isSymbolicLink()).toBe(true);
    expect(JSON.parse(readFileSync(real, 'utf8'))).toEqual({ theme: 'dark', projects: { [wt]: { hasTrustDialogAccepted: true } } });
  });

  it('uses CLAUDE_CONFIG_DIR when set, as claude does', () => {
    const dir = join(home, 'profile');
    mkdirSync(dir);
    writeFileSync(join(dir, '.claude.json'), '{}');
    writeFileSync(file, '{}');
    expect(trustClaudeFolder(wt, { home, env: { CLAUDE_CONFIG_DIR: dir } })).toBe(true);
    expect(JSON.parse(readFileSync(join(dir, '.claude.json'), 'utf8')).projects[wt]).toEqual({ hasTrustDialogAccepted: true });
    expect(readFileSync(file, 'utf8')).toBe('{}');
  });

  it('replaces a temp file left by a crash instead of writing through it', () => {
    writeFileSync(file, '{}', { mode: 0o600 });
    const stale = `${file}.agent007-${process.pid}.tmp`;
    writeFileSync(stale, 'junk', { mode: 0o644 });
    expect(trustClaudeFolder(wt, { home, env: {} })).toBe(true);
    if (process.platform !== 'win32') expect(statSync(file).mode & 0o777).toBe(0o600);   // Windows has no POSIX modes
    expect(readdirSync(home).filter(f => f.includes('.tmp'))).toEqual([]);
  });

  it('does not create the file when claude has none', () => {
    expect(trustClaudeFolder(wt, { home, env: {} })).toBe(false);
    expect(readdirSync(home)).toEqual(['worktrees']);
  });
});

describe('autoTrusts', () => {
  const board = { spawnedBy: 'board', worktreePath: '/wt', command: "claude --permission-mode auto 'do it'" };
  it('covers board-dispatched Claude Code workers by default', () => {
    expect(autoTrusts(board, {})).toBe(true);
  });
  it('is off when TRUST_BOARD_WORKTREES opts out', () => {
    for (const v of ['0', 'false', 'OFF', 'no', ' 0 ']) expect(autoTrusts(board, { TRUST_BOARD_WORKTREES: v })).toBe(false);
    expect(autoTrusts(board, { TRUST_BOARD_WORKTREES: '1' })).toBe(true);
  });
  it('covers board-dispatched Codex workers too, under the same opt-out', () => {
    const codex = { ...board, command: "codex 'do it'" };
    expect(autoTrusts(codex, {})).toBe(true);
    expect(autoTrusts(codex, { TRUST_BOARD_WORKTREES: '0' })).toBe(false);
    expect(autoTrusts({ ...codex, spawnedBy: 'user' }, {})).toBe(false);
  });
  it('leaves hand-started agents, other CLIs and repo-less sessions alone', () => {
    expect(autoTrusts({ ...board, spawnedBy: 'user' }, {})).toBe(false);
    expect(autoTrusts({ ...board, command: 'gemini hi' }, {})).toBe(false);
    expect(autoTrusts({ ...board, worktreePath: null }, {})).toBe(false);
  });
});

describe('codexTrustArgs', () => {
  it('trusts only the worktree, by its real path, as a TOML inline table', () => {
    const dotted = join(home, '.agent-007', 'wt.1');
    mkdirSync(dotted, { recursive: true });
    expect(codexTrustArgs(dotted)).toEqual(['-c', `projects={${JSON.stringify(dotted)}={trust_level="trusted"}}`]);
    if (process.platform !== 'win32') {   // symlinks need privileges there
      symlinkSync(dotted, join(home, 'link'));
      expect(codexTrustArgs(join(home, 'link'))[1]).toContain(JSON.stringify(dotted));
    }
  });
  it('writes nothing to CODEX_HOME', () => {
    codexTrustArgs(wt);
    expect(readdirSync(home)).toEqual(['worktrees']);
  });
});

// A stand-in `codex` that records the arguments it was started with.
describe.skipIf(process.platform === 'win32')('a Codex spawn', () => {
  const spawnCodex = (extra) => {
    const bin = join(home, 'bin');
    mkdirSync(bin, { recursive: true });
    writeFileSync(join(bin, 'codex'), `#!/bin/sh\nprintf '%s\\n' "$@" > "${home}/args"\n`, { mode: 0o755 });
    const PATH = process.env.PATH;
    process.env.PATH = `${bin}:${PATH}`;
    try {
      return createSessionFromConfig({ sessionId: `ct-${Math.random()}`, name: 'T', color: '#000', command: "codex 'do it'", worktreePath: wt, spawnedBy: 'board', ...extra }, () => {});
    } finally { process.env.PATH = PATH; }
  };
  const argsSeen = async () => {
    for (let i = 0; i < 100 && !existsSync(join(home, 'args')); i++) await new Promise(r => setTimeout(r, 20));
    return readFileSync(join(home, 'args'), 'utf8');
  };
  const done = (r) => { try { r.session.pty.kill(); } catch {} clearInterval(r.session.stateCheckInterval); };

  it('gets the trust flag and no PTY answering when auto-trusted', async () => {
    const r = spawnCodex({ autoTrust: true });
    try {
      expect(await argsSeen()).toContain(`projects={${JSON.stringify(wt)}={trust_level="trusted"}}`);
      expect(r.session.answersTrust).toBe(false);
    } finally { done(r); }
  });
  it('gets no trust flag otherwise', async () => {
    const r = spawnCodex({ spawnedBy: 'user' });
    try {
      const seen = await argsSeen();
      expect(seen).toContain('do it');
      expect(seen).not.toContain('trust_level');
    } finally { done(r); }
  });
});

describe('trustClaudeFolder on a fresh install', () => {
  it('says nothing when Claude Code has no ~/.claude.json yet', () => {
    const errors = [];
    const saved = console.error;
    console.error = (...a) => errors.push(a.join(' '));
    try { expect(trustClaudeFolder(wt, { home, env: {} })).toBe(false); } finally { console.error = saved; }
    expect(errors).toEqual([]);
    expect(existsSync(file)).toBe(false);
  });
});