Files
2025-08-07 00:42:31 +10:00

183 lines
6.4 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { promises as fs } from 'fs';
import path from 'path';
import os from 'os';
import { InitCommand } from '../../src/core/init.js';
import * as prompts from '@inquirer/prompts';
vi.mock('@inquirer/prompts', () => ({
select: vi.fn()
}));
describe('InitCommand', () => {
let testDir: string;
let initCommand: InitCommand;
beforeEach(async () => {
testDir = path.join(os.tmpdir(), `openspec-init-test-${Date.now()}`);
await fs.mkdir(testDir, { recursive: true });
initCommand = new InitCommand();
// Mock console.log to suppress output during tests
vi.spyOn(console, 'log').mockImplementation(() => {});
});
afterEach(async () => {
await fs.rm(testDir, { recursive: true, force: true });
vi.restoreAllMocks();
});
describe('execute', () => {
it('should create OpenSpec directory structure', async () => {
vi.mocked(prompts.select).mockResolvedValue('claude');
await initCommand.execute(testDir);
const openspecPath = path.join(testDir, 'openspec');
expect(await directoryExists(openspecPath)).toBe(true);
expect(await directoryExists(path.join(openspecPath, 'specs'))).toBe(true);
expect(await directoryExists(path.join(openspecPath, 'changes'))).toBe(true);
expect(await directoryExists(path.join(openspecPath, 'changes', 'archive'))).toBe(true);
});
it('should create README.md and project.md', async () => {
vi.mocked(prompts.select).mockResolvedValue('claude');
await initCommand.execute(testDir);
const openspecPath = path.join(testDir, 'openspec');
expect(await fileExists(path.join(openspecPath, 'README.md'))).toBe(true);
expect(await fileExists(path.join(openspecPath, 'project.md'))).toBe(true);
const readmeContent = await fs.readFile(path.join(openspecPath, 'README.md'), 'utf-8');
expect(readmeContent).toContain('OpenSpec Instructions');
const projectContent = await fs.readFile(path.join(openspecPath, 'project.md'), 'utf-8');
expect(projectContent).toContain('Project Context');
});
it('should create CLAUDE.md when Claude Code is selected', async () => {
vi.mocked(prompts.select).mockResolvedValue('claude');
await initCommand.execute(testDir);
const claudePath = path.join(testDir, 'CLAUDE.md');
expect(await fileExists(claudePath)).toBe(true);
const content = await fs.readFile(claudePath, 'utf-8');
expect(content).toContain('<!-- OPENSPEC:START -->');
expect(content).toContain('OpenSpec Project');
expect(content).toContain('<!-- OPENSPEC:END -->');
});
it('should update existing CLAUDE.md with markers', async () => {
vi.mocked(prompts.select).mockResolvedValue('claude');
const claudePath = path.join(testDir, 'CLAUDE.md');
const existingContent = '# My Project Instructions\nCustom instructions here';
await fs.writeFile(claudePath, existingContent);
await initCommand.execute(testDir);
const updatedContent = await fs.readFile(claudePath, 'utf-8');
expect(updatedContent).toContain('<!-- OPENSPEC:START -->');
expect(updatedContent).toContain('OpenSpec Project');
expect(updatedContent).toContain('<!-- OPENSPEC:END -->');
expect(updatedContent).toContain('Custom instructions here');
});
it('should throw error if OpenSpec already exists', async () => {
const openspecPath = path.join(testDir, 'openspec');
await fs.mkdir(openspecPath, { recursive: true });
await expect(initCommand.execute(testDir)).rejects.toThrow(
/OpenSpec seems to already be initialized/
);
});
it('should handle non-existent target directory', async () => {
vi.mocked(prompts.select).mockResolvedValue('claude');
const newDir = path.join(testDir, 'new-project');
await initCommand.execute(newDir);
const openspecPath = path.join(newDir, 'openspec');
expect(await directoryExists(openspecPath)).toBe(true);
});
it('should display success message with selected tool name', async () => {
vi.mocked(prompts.select).mockResolvedValue('claude');
const logSpy = vi.spyOn(console, 'log');
await initCommand.execute(testDir);
const calls = logSpy.mock.calls.flat().join('\n');
expect(calls).toContain('Copy these prompts to Claude Code');
});
});
describe('AI tool selection', () => {
it('should prompt for AI tool selection', async () => {
const selectMock = vi.mocked(prompts.select);
selectMock.mockResolvedValue('claude');
await initCommand.execute(testDir);
expect(selectMock).toHaveBeenCalledWith(
expect.objectContaining({
message: 'Which AI tool do you use?'
})
);
});
it('should handle different AI tool selections', async () => {
// For now, only Claude is available, but test the structure
vi.mocked(prompts.select).mockResolvedValue('claude');
await initCommand.execute(testDir);
// When other tools are added, we'd test their specific configurations here
const claudePath = path.join(testDir, 'CLAUDE.md');
expect(await fileExists(claudePath)).toBe(true);
});
});
describe('error handling', () => {
it('should provide helpful error for insufficient permissions', async () => {
// This is tricky to test cross-platform, but we can test the error message
const readOnlyDir = path.join(testDir, 'readonly');
await fs.mkdir(readOnlyDir);
// Mock the permission check to fail
const originalCheck = fs.writeFile;
vi.spyOn(fs, 'writeFile').mockImplementation(async (filePath: any, ...args: any[]) => {
if (typeof filePath === 'string' && filePath.includes('.openspec-test-')) {
throw new Error('EACCES: permission denied');
}
return originalCheck.call(fs, filePath, ...args);
});
await expect(initCommand.execute(readOnlyDir)).rejects.toThrow(
/Insufficient permissions/
);
});
});
});
async function fileExists(filePath: string): Promise<boolean> {
try {
await fs.access(filePath);
return true;
} catch {
return false;
}
}
async function directoryExists(dirPath: string): Promise<boolean> {
try {
const stats = await fs.stat(dirPath);
return stats.isDirectory();
} catch {
return false;
}
}