GitNexus/gitnexus/test/unit/setup-codex.test.ts

215 lines
7.6 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import fs from 'fs/promises';
import os from 'os';
import path from 'path';
import { packageVersion } from '../../src/core/package-version.js';
const PKG_VERSION = packageVersion();
const NPX_REF = `gitnexus@${PKG_VERSION}`;
const execFileMock = vi.fn((...args: any[]) => {
const callback = args.at(-1);
if (typeof callback === 'function') {
callback(null, '', '');
}
});
const execFileSyncMock = vi.fn((): string => {
throw new Error('not found');
});
vi.mock('child_process', () => ({
execFile: execFileMock,
execFileSync: execFileSyncMock,
}));
describe('setupCommand codex execution', () => {
let tempHome: string;
let originalHome: string | undefined;
let originalUserProfile: string | undefined;
let originalCodexHome: string | undefined;
let platformDescriptor: PropertyDescriptor | undefined;
const setPlatform = (value: NodeJS.Platform) => {
Object.defineProperty(process, 'platform', {
value,
configurable: true,
});
};
beforeEach(async () => {
vi.resetModules();
vi.clearAllMocks();
originalHome = process.env.HOME;
originalUserProfile = process.env.USERPROFILE;
originalCodexHome = process.env.CODEX_HOME;
delete process.env.CODEX_HOME;
tempHome = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-codex-setup-'));
process.env.HOME = tempHome;
process.env.USERPROFILE = tempHome;
await fs.mkdir(path.join(tempHome, '.codex'), { recursive: true });
platformDescriptor = Object.getOwnPropertyDescriptor(process, 'platform');
setPlatform('win32');
vi.spyOn(console, 'log').mockImplementation(() => {});
});
afterEach(async () => {
vi.restoreAllMocks();
if (platformDescriptor) {
Object.defineProperty(process, 'platform', platformDescriptor);
}
process.env.HOME = originalHome;
process.env.USERPROFILE = originalUserProfile;
if (originalCodexHome === undefined) delete process.env.CODEX_HOME;
else process.env.CODEX_HOME = originalCodexHome;
await fs.rm(tempHome, { recursive: true, force: true });
});
it('invokes codex mcp add with shell enabled on Windows', async () => {
const { setupCommand } = await import('../../src/cli/setup.js');
await setupCommand();
expect(execFileMock).toHaveBeenCalledWith(
'codex',
['mcp', 'add', 'gitnexus', '--', 'cmd', '/c', 'npx', '-y', NPX_REF, 'mcp'],
{ shell: true, windowsHide: true },
expect.any(Function),
);
});
it('uses Windows npx fallback arguments when where returns only a non-wrapper shim', async () => {
execFileSyncMock.mockReturnValueOnce('C:\\Users\\dev\\AppData\\Roaming\\npm\\gitnexus\n');
const { setupCommand } = await import('../../src/cli/setup.js');
await setupCommand();
expect(execFileMock).toHaveBeenCalledWith(
'codex',
['mcp', 'add', 'gitnexus', '--', 'cmd', '/c', 'npx', '-y', NPX_REF, 'mcp'],
{ shell: true, windowsHide: true },
expect.any(Function),
);
});
it('invokes codex mcp add without shell on non-Windows and does not write fallback config', async () => {
setPlatform('darwin');
const { setupCommand } = await import('../../src/cli/setup.js');
await setupCommand();
expect(execFileMock).toHaveBeenCalledWith(
'codex',
['mcp', 'add', 'gitnexus', '--', 'npx', '-y', NPX_REF, 'mcp'],
{ shell: false, windowsHide: true },
expect.any(Function),
);
await expect(fs.access(path.join(tempHome, '.codex', 'config.toml'))).rejects.toThrow();
});
it('keeps an existing HTTP entry and its bearer-token setting without invoking codex mcp add', async () => {
const configPath = path.join(tempHome, '.codex', 'config.toml');
const raw =
'[mcp_servers.gitnexus]\nurl = "http://127.0.0.1:4748/mcp"\nbearer_token_env_var = "GITNEXUS_TOKEN"\n';
await fs.writeFile(configPath, raw, 'utf-8');
const { setupCommand } = await import('../../src/cli/setup.js');
await setupCommand({ codingAgent: 'codex' });
expect(execFileMock).not.toHaveBeenCalled();
expect(await fs.readFile(configPath, 'utf-8')).toBe(raw);
expect(
await fs.stat(path.join(tempHome, '.agents', 'skills', 'gitnexus-guide', 'SKILL.md')),
).toBeDefined();
});
it('does not mistake an HTTP entry in the default home for the active CODEX_HOME entry', async () => {
const defaultConfig = path.join(tempHome, '.codex', 'config.toml');
const activeHome = path.join(tempHome, 'active-codex');
await fs.writeFile(defaultConfig, '[mcp_servers.gitnexus]\nurl = "https://example.test/mcp"\n');
await fs.mkdir(activeHome);
process.env.CODEX_HOME = activeHome;
const { setupCommand } = await import('../../src/cli/setup.js');
await setupCommand({ codingAgent: 'codex' });
expect(execFileMock).toHaveBeenCalledWith(
'codex',
['mcp', 'add', 'gitnexus', '--', 'cmd', '/c', 'npx', '-y', NPX_REF, 'mcp'],
{ shell: true, windowsHide: true },
expect.any(Function),
);
});
it('preserves an HTTP entry in the active CODEX_HOME even when the default home is empty', async () => {
const activeHome = path.join(tempHome, 'active-codex');
const activeConfig = path.join(activeHome, 'config.toml');
const raw = '[mcp_servers.gitnexus]\nurl = "https://example.test/mcp"\n';
await fs.mkdir(activeHome);
await fs.writeFile(activeConfig, raw);
process.env.CODEX_HOME = activeHome;
const { setupCommand } = await import('../../src/cli/setup.js');
await setupCommand({ codingAgent: 'codex' });
expect(execFileMock).not.toHaveBeenCalled();
expect(await fs.readFile(activeConfig, 'utf-8')).toBe(raw);
});
it('writes the fallback entry to the active CODEX_HOME when the Codex CLI is unavailable', async () => {
const activeHome = path.join(tempHome, 'active-codex');
process.env.CODEX_HOME = activeHome;
execFileMock.mockImplementationOnce((...args: unknown[]) => {
const callback = args.at(-1);
if (typeof callback === 'function') callback(new Error('codex unavailable'));
});
const { setupCommand } = await import('../../src/cli/setup.js');
await setupCommand({ codingAgent: 'codex' });
expect(await fs.readFile(path.join(activeHome, 'config.toml'), 'utf-8')).toContain(
'[mcp_servers.gitnexus]',
);
await expect(fs.access(path.join(tempHome, '.codex', 'config.toml'))).rejects.toThrow();
});
it('does not print TOML source or nearby credentials when the active config is malformed', async () => {
const activeHome = path.join(tempHome, 'active-codex');
const configPath = path.join(activeHome, 'config.toml');
const token = 'Bearer FAKE_PR3460_REVIEW_TOKEN';
await fs.mkdir(activeHome);
await fs.writeFile(
configPath,
`[mcp_servers.gitnexus]\nhttp_headers = { Authorization = "${token}" }\ninvalid =\n`,
);
process.env.CODEX_HOME = activeHome;
const { setupCommand } = await import('../../src/cli/setup.js');
await setupCommand({ codingAgent: 'codex' });
const output = JSON.stringify(vi.mocked(console.log).mock.calls);
expect(output).toMatch(/invalid config\.toml \(line 3, column \d+\)/);
expect(output).not.toContain(token);
expect(execFileMock).not.toHaveBeenCalled();
expect(await fs.readFile(configPath, 'utf-8')).toContain(token);
});
it('skips Codex setup entirely when ~/.codex is missing', async () => {
await fs.rm(path.join(tempHome, '.codex'), { recursive: true, force: true });
const { setupCommand } = await import('../../src/cli/setup.js');
await setupCommand();
expect(execFileMock).not.toHaveBeenCalled();
await expect(fs.access(path.join(tempHome, '.agents', 'skills'))).rejects.toThrow();
});
});