test(cli): cover full Codex setup flow

This commit is contained in:
Dmytro 2026-03-20 15:28:42 +01:00
parent a7b8c302d4
commit cbfdae0303
2 changed files with 141 additions and 0 deletions

View file

@ -9,6 +9,7 @@ describe('setupCommand skills integration', () => {
let tempHome: string;
const originalHome = process.env.HOME;
const originalUserProfile = process.env.USERPROFILE;
const originalPath = process.env.PATH;
const testId = `${Date.now()}-${process.pid}`;
const flatSkillName = `test-flat-skill-${testId}`;
const dirSkillName = `test-dir-skill-${testId}`;
@ -47,6 +48,7 @@ describe('setupCommand skills integration', () => {
await fs.rm(path.join(packageSkillsRoot, dirSkillName), { recursive: true, force: true });
process.env.HOME = originalHome;
process.env.USERPROFILE = originalUserProfile;
process.env.PATH = originalPath;
await fs.rm(tempHome, { recursive: true, force: true });
});
@ -85,4 +87,40 @@ describe('setupCommand skills integration', () => {
);
expect(nestedInstalled).toContain('Directory Nested File');
});
it('falls back to Codex config.toml and installs skills into ~/.agents/skills when codex CLI is unavailable', async () => {
await fs.mkdir(path.join(tempHome, '.codex'), { recursive: true });
process.env.PATH = '';
await setupCommand();
const codexConfig = await fs.readFile(
path.join(tempHome, '.codex', 'config.toml'),
'utf-8',
);
expect(codexConfig).toContain('[mcp_servers.gitnexus]');
expect(codexConfig).toContain('gitnexus@latest');
const codexSkill = await fs.readFile(
path.join(tempHome, '.agents', 'skills', 'gitnexus-cli', 'SKILL.md'),
'utf-8',
);
expect(codexSkill).toContain('GitNexus CLI Commands');
});
it('does not duplicate the Codex MCP section on repeated fallback setup runs', async () => {
await fs.mkdir(path.join(tempHome, '.codex'), { recursive: true });
process.env.PATH = '';
await setupCommand();
await setupCommand();
const codexConfig = await fs.readFile(
path.join(tempHome, '.codex', 'config.toml'),
'utf-8',
);
const sectionMatches = codexConfig.match(/\[mcp_servers\.gitnexus\]/g) ?? [];
expect(sectionMatches).toHaveLength(1);
});
});

View file

@ -0,0 +1,103 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import fs from 'fs/promises';
import os from 'os';
import path from 'path';
const execFileMock = vi.fn((...args: any[]) => {
const callback = args.at(-1);
if (typeof callback === 'function') {
callback(null, '', '');
}
});
vi.mock('child_process', () => ({
execFile: execFileMock,
}));
describe('setupCommand codex execution', () => {
let tempHome: string;
let originalHome: string | undefined;
let originalUserProfile: 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;
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;
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', 'gitnexus@latest', 'mcp'],
{ shell: 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', 'gitnexus@latest', 'mcp'],
{ shell: false },
expect.any(Function),
);
await expect(
fs.access(path.join(tempHome, '.codex', 'config.toml')),
).rejects.toThrow();
});
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();
});
});