Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased

- Fixed Gemini CLI integration https://github.com/codeaholicguy/ai-devkit/issues/3
- Added test for TemplateManager.ts

## [0.4.0] - 2025-10-31

### Added
Expand Down
6 changes: 3 additions & 3 deletions src/__tests__/lib/EnvironmentSelector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,16 +135,16 @@ describe('EnvironmentSelector', () => {
selector.displaySelectionSummary(['cursor', 'claude']);

expect(consoleSpy).toHaveBeenCalledWith('\nSelected environments:');
expect(consoleSpy).toHaveBeenCalledWith(' [OK] Cursor');
expect(consoleSpy).toHaveBeenCalledWith(' [OK] Claude Code');
expect(consoleSpy).toHaveBeenCalledWith(' Cursor');
expect(consoleSpy).toHaveBeenCalledWith(' Claude Code');
expect(consoleSpy).toHaveBeenCalledWith('');
});

it('should handle single environment selection', () => {
selector.displaySelectionSummary(['cursor']);

expect(consoleSpy).toHaveBeenCalledWith('\nSelected environments:');
expect(consoleSpy).toHaveBeenCalledWith(' [OK] Cursor');
expect(consoleSpy).toHaveBeenCalledWith(' Cursor');
expect(consoleSpy).toHaveBeenCalledWith('');
});
});
Expand Down
6 changes: 3 additions & 3 deletions src/__tests__/lib/PhaseSelector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,16 +82,16 @@ describe('PhaseSelector', () => {
selector.displaySelectionSummary(['requirements', 'design']);

expect(consoleSpy).toHaveBeenCalledWith('\nSelected phases:');
expect(consoleSpy).toHaveBeenCalledWith(' [OK] Requirements & Problem Understanding');
expect(consoleSpy).toHaveBeenCalledWith(' [OK] System Design & Architecture');
expect(consoleSpy).toHaveBeenCalledWith(' Requirements & Problem Understanding');
expect(consoleSpy).toHaveBeenCalledWith(' System Design & Architecture');
expect(consoleSpy).toHaveBeenCalledWith('');
});

it('should handle single phase selection', () => {
selector.displaySelectionSummary(['requirements']);

expect(consoleSpy).toHaveBeenCalledWith('\nSelected phases:');
expect(consoleSpy).toHaveBeenCalledWith(' [OK] Requirements & Problem Understanding');
expect(consoleSpy).toHaveBeenCalledWith(' Requirements & Problem Understanding');
expect(consoleSpy).toHaveBeenCalledWith('');
});
});
Expand Down
221 changes: 221 additions & 0 deletions src/__tests__/lib/TemplateManager.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import * as fs from 'fs-extra';
import * as path from 'path';
import { TemplateManager } from '../../lib/TemplateManager';
import { EnvironmentDefinition } from '../../types';

jest.mock('fs-extra');

describe('TemplateManager', () => {
let templateManager: TemplateManager;
let mockFs: jest.Mocked<typeof fs>;

beforeEach(() => {
mockFs = fs as jest.Mocked<typeof fs>;
templateManager = new TemplateManager('/test/target');

jest.clearAllMocks();
});

afterEach(() => {
jest.restoreAllMocks();
});

describe('setupSingleEnvironment', () => {
it('should copy context file when it exists', async () => {
const env: EnvironmentDefinition = {
code: 'test-env',
name: 'Test Environment',
contextFileName: '.test-context.md',
commandPath: '.test',
isCustomCommandPath: false
};

(mockFs.pathExists as any)
.mockResolvedValueOnce(true)
.mockResolvedValueOnce(true);

(mockFs.readdir as any).mockResolvedValue(['command1.md', 'command2.toml']);

const result = await (templateManager as any).setupSingleEnvironment(env);

expect(mockFs.copy).toHaveBeenCalledWith(
path.join(templateManager['templatesDir'], 'env', 'base.md'),
path.join(templateManager['targetDir'], env.contextFileName)
);
expect(result).toContain(path.join(templateManager['targetDir'], env.contextFileName));
});

it('should warn when context file does not exist', async () => {
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();

const env: EnvironmentDefinition = {
code: 'test-env',
name: 'Test Environment',
contextFileName: '.test-context.md',
commandPath: '.test',
isCustomCommandPath: false
};

(mockFs.pathExists as any)
.mockResolvedValueOnce(false)
.mockResolvedValueOnce(true);

(mockFs.readdir as any).mockResolvedValue(['command1.md']);

const result = await (templateManager as any).setupSingleEnvironment(env);

expect(consoleWarnSpy).toHaveBeenCalledWith(
expect.stringContaining('Warning: Context file not found')
);
expect(result).toEqual([path.join(templateManager['targetDir'], env.commandPath, 'command1.md')]);

consoleWarnSpy.mockRestore();
});

it('should copy commands when isCustomCommandPath is false', async () => {
const env: EnvironmentDefinition = {
code: 'test-env',
name: 'Test Environment',
contextFileName: '.test-context.md',
commandPath: '.test',
isCustomCommandPath: false
};

const mockCommandFiles = ['command1.md', 'command2.toml', 'command3.md'];

(mockFs.pathExists as any)
.mockResolvedValueOnce(true) // context file exists
.mockResolvedValueOnce(true); // commands directory exists

(mockFs.readdir as any).mockResolvedValue(mockCommandFiles);

const result = await (templateManager as any).setupSingleEnvironment(env);

expect(mockFs.ensureDir).toHaveBeenCalledWith(
path.join(templateManager['targetDir'], env.commandPath)
);

// Should only copy .md files (not .toml files)
expect(mockFs.copy).toHaveBeenCalledWith(
path.join(templateManager['templatesDir'], 'commands', 'command1.md'),
path.join(templateManager['targetDir'], env.commandPath, 'command1.md')
);
expect(mockFs.copy).toHaveBeenCalledWith(
path.join(templateManager['templatesDir'], 'commands', 'command3.md'),
path.join(templateManager['targetDir'], env.commandPath, 'command3.md')
);

expect(result).toContain(path.join(templateManager['targetDir'], env.commandPath, 'command1.md'));
expect(result).toContain(path.join(templateManager['targetDir'], env.commandPath, 'command3.md'));
});

it('should skip commands when isCustomCommandPath is true', async () => {
const env: EnvironmentDefinition = {
code: 'test-env',
name: 'Test Environment',
contextFileName: '.test-context.md',
commandPath: '.test',
isCustomCommandPath: true
};

(mockFs.pathExists as any).mockResolvedValueOnce(true);

const result = await (templateManager as any).setupSingleEnvironment(env);

expect(mockFs.ensureDir).not.toHaveBeenCalled();
expect(mockFs.copy).toHaveBeenCalledTimes(1);
expect(result).toContain(path.join(templateManager['targetDir'], env.contextFileName));
});

it('should handle cursor environment with special files', async () => {
const env: EnvironmentDefinition = {
code: 'cursor',
name: 'Cursor',
contextFileName: '.cursor.md',
commandPath: '.cursor',
isCustomCommandPath: false
};

const mockRuleFiles = ['rule1.md', 'rule2.toml'];

(mockFs.pathExists as any)
.mockResolvedValueOnce(true)
.mockResolvedValueOnce(true) .mockResolvedValueOnce(true);

(mockFs.readdir as any)
.mockResolvedValueOnce([]) .mockResolvedValueOnce(mockRuleFiles);
const result = await (templateManager as any).setupSingleEnvironment(env);

expect(mockFs.ensureDir).toHaveBeenCalledWith(
path.join(templateManager['targetDir'], '.cursor', 'rules')
);
expect(mockFs.copy).toHaveBeenCalledWith(
path.join(templateManager['templatesDir'], 'env', 'cursor', 'rules'),
path.join(templateManager['targetDir'], '.cursor', 'rules')
);

expect(result).toContain(path.join(templateManager['targetDir'], '.cursor', 'rules', 'rule1.md'));
expect(result).toContain(path.join(templateManager['targetDir'], '.cursor', 'rules', 'rule2.toml'));
});

it('should handle gemini environment with toml files', async () => {
const env: EnvironmentDefinition = {
code: 'gemini',
name: 'Gemini',
contextFileName: '.gemini.md',
commandPath: '.gemini',
isCustomCommandPath: false
};

const mockCommandFiles = ['command1.md', 'command2.toml', 'command3.toml'];

(mockFs.pathExists as any)
.mockResolvedValueOnce(true)
.mockResolvedValueOnce(true) .mockResolvedValueOnce(true); // gemini commands directory exists

(mockFs.readdir as any).mockResolvedValue(mockCommandFiles);

const result = await (templateManager as any).setupSingleEnvironment(env);

expect(mockFs.ensureDir).toHaveBeenCalledWith(
path.join(templateManager['targetDir'], '.gemini', 'commands')
);

expect(mockFs.copy).toHaveBeenCalledWith(
path.join(templateManager['templatesDir'], 'commands', 'command2.toml'),
path.join(templateManager['targetDir'], '.gemini', 'commands', 'command2.toml')
);
expect(mockFs.copy).toHaveBeenCalledWith(
path.join(templateManager['templatesDir'], 'commands', 'command3.toml'),
path.join(templateManager['targetDir'], '.gemini', 'commands', 'command3.toml')
);

expect(result).toContain(path.join(templateManager['targetDir'], '.gemini', 'commands', 'command2.toml'));
expect(result).toContain(path.join(templateManager['targetDir'], '.gemini', 'commands', 'command3.toml'));
});

it('should handle errors and rethrow them', async () => {
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();

const env: EnvironmentDefinition = {
code: 'test-env',
name: 'Test Environment',
contextFileName: '.test-context.md',
commandPath: '.test',
isCustomCommandPath: false
};

const testError = new Error('Test error');
(mockFs.pathExists as any).mockRejectedValue(testError);

await expect((templateManager as any).setupSingleEnvironment(env)).rejects.toThrow('Test error');

expect(consoleErrorSpy).toHaveBeenCalledWith(
'Error setting up environment Test Environment:',
testError
);

consoleErrorSpy.mockRestore();
});
});
});
2 changes: 1 addition & 1 deletion src/lib/EnvironmentSelector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export class EnvironmentSelector {

console.log('\nSelected environments:');
selected.forEach(envId => {
console.log(` [OK] ${getEnvironmentDisplayName(envId)}`);
console.log(` ${getEnvironmentDisplayName(envId)}`);
});
console.log('');
}
Expand Down
2 changes: 1 addition & 1 deletion src/lib/PhaseSelector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export class PhaseSelector {

console.log('\nSelected phases:');
selected.forEach(phase => {
console.log(` [OK] ${PHASE_DISPLAY_NAMES[phase]}`);
console.log(` ${PHASE_DISPLAY_NAMES[phase]}`);
});
console.log('');
}
Expand Down
Loading