Skip to content
Open
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
14 changes: 12 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion packages/cli/src/__tests__/commands/lint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ import { ui } from '../../util/terminal-ui';
import { lintCommand, renderLintReport } from '../../commands/lint';
import { LintReport, runLintChecks } from '../../services/lint/lint.service';

jest.mock('../../lib/Config', () => ({
ConfigManager: jest.fn(() => ({
getDocsDir: jest.fn<() => Promise<string>>().mockResolvedValue('docs/ai')
}))
}));

jest.mock('../../services/lint/lint.service', () => ({
runLintChecks: jest.fn()
}));
Expand Down Expand Up @@ -46,7 +52,7 @@ describe('lint command', () => {

await lintCommand({ feature: 'lint-command', json: true });

expect(mockedRunLintChecks).toHaveBeenCalledWith({ feature: 'lint-command', json: true });
expect(mockedRunLintChecks).toHaveBeenCalledWith({ feature: 'lint-command', json: true, docsDir: 'docs/ai' });
expect(mockedUi.text).toHaveBeenCalledWith(JSON.stringify(report, null, 2));
expect(process.exitCode).toBe(0);
});
Expand Down
66 changes: 66 additions & 0 deletions packages/cli/src/__tests__/lib/Config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,72 @@ describe('ConfigManager', () => {
});
});

describe('getDocsDir', () => {
it('should return custom docsDir when set in config', async () => {
const config: DevKitConfig = {
version: '1.0.0',
docsDir: '.ai-docs',
environments: [],
phases: [],
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z'
};

(mockFs.pathExists as any).mockResolvedValue(true);
(mockFs.readJson as any).mockResolvedValue(config);

const result = await configManager.getDocsDir();

expect(result).toBe('.ai-docs');
});

it('should return default docs/ai when docsDir is not set', async () => {
const config: DevKitConfig = {
version: '1.0.0',
environments: [],
phases: [],
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z'
};

(mockFs.pathExists as any).mockResolvedValue(true);
(mockFs.readJson as any).mockResolvedValue(config);

const result = await configManager.getDocsDir();

expect(result).toBe('docs/ai');
});

it('should return default docs/ai when config does not exist', async () => {
(mockFs.pathExists as any).mockResolvedValue(false);

const result = await configManager.getDocsDir();

expect(result).toBe('docs/ai');
});
});

describe('setDocsDir', () => {
it('should update docsDir in config', async () => {
const config: DevKitConfig = {
version: '1.0.0',
environments: [],
phases: [],
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z'
};

(mockFs.pathExists as any).mockResolvedValue(true);
(mockFs.readJson as any).mockResolvedValue(config);
(mockFs.writeJson as any).mockResolvedValue(undefined);

const result = await configManager.setDocsDir('.ai-docs');

expect(result.docsDir).toBe('.ai-docs');
expect(mockFs.writeJson).toHaveBeenCalled();
});
});

describe('getEnvironments', () => {
it('should return environments array when config exists', async () => {
const config: DevKitConfig = {
Expand Down
36 changes: 36 additions & 0 deletions packages/cli/src/__tests__/lib/InitTemplate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,42 @@ environments:
);
});

it('loads template with custom docsDir', async () => {
mockFs.pathExists.mockResolvedValue(true as never);
mockFs.readFile.mockResolvedValue(`
docsDir: .ai-docs
environments:
- claude
phases:
- requirements
` as never);

const result = await loadInitTemplate('/tmp/init.yaml');

expect(result.docsDir).toBe('.ai-docs');
expect(result.environments).toEqual(['claude']);
});

it('throws when docsDir is empty string', async () => {
mockFs.pathExists.mockResolvedValue(true as never);
mockFs.readFile.mockResolvedValue(`
docsDir: " "
` as never);

await expect(loadInitTemplate('/tmp/init.yaml')).rejects.toThrow(
'"docsDir" must be a non-empty string'
);
});

it('throws when docsDir is not a string', async () => {
mockFs.pathExists.mockResolvedValue(true as never);
mockFs.readFile.mockResolvedValue(JSON.stringify({ docsDir: 123 }) as never);

await expect(loadInitTemplate('/tmp/init.json')).rejects.toThrow(
'"docsDir" must be a non-empty string'
);
});

it('throws when unknown field exists', async () => {
mockFs.pathExists.mockResolvedValue(true as never);
mockFs.readFile.mockResolvedValue(`
Expand Down
88 changes: 76 additions & 12 deletions packages/cli/src/__tests__/lib/TemplateManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,12 @@ describe('TemplateManager', () => {
(mockFs.pathExists as any).mockResolvedValueOnce(true);

(mockFs.readdir as any).mockResolvedValue(['command1.md', 'command2.toml']);
(mockFs.readFile as any).mockResolvedValue('command content');
(mockFs.writeFile as any).mockResolvedValue(undefined);

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

expect(mockFs.copy).toHaveBeenCalledTimes(1);
expect(mockFs.writeFile).toHaveBeenCalledTimes(1);
expect(result).toEqual([path.join(templateManager['targetDir'], env.commandPath, 'command1.md')]);
});

Expand All @@ -56,6 +58,8 @@ describe('TemplateManager', () => {
(mockFs.pathExists as any).mockResolvedValueOnce(true);

(mockFs.readdir as any).mockResolvedValue(['command1.md']);
(mockFs.readFile as any).mockResolvedValue('command content');
(mockFs.writeFile as any).mockResolvedValue(undefined);

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

Expand All @@ -79,27 +83,52 @@ describe('TemplateManager', () => {
(mockFs.pathExists as any).mockResolvedValueOnce(true); // commands directory exists

(mockFs.readdir as any).mockResolvedValue(mockCommandFiles);
(mockFs.readFile as any).mockResolvedValue('command content');
(mockFs.writeFile as any).mockResolvedValue(undefined);

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')
// Should only write .md files (not .toml files)
expect(mockFs.writeFile).toHaveBeenCalledWith(
path.join(templateManager['targetDir'], env.commandPath, 'command1.md'),
'command content'
);
expect(mockFs.copy).toHaveBeenCalledWith(
path.join(templateManager['templatesDir'], 'commands', 'command3.md'),
path.join(templateManager['targetDir'], env.commandPath, 'command3.md')
expect(mockFs.writeFile).toHaveBeenCalledWith(
path.join(templateManager['targetDir'], env.commandPath, 'command3.md'),
'command content'
);

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

it('should replace docs/ai with custom docsDir in command content', async () => {
const customManager = new TemplateManager('/test/target', '.ai-docs');
const env: EnvironmentDefinition = {
code: 'test-env',
name: 'Test Environment',
contextFileName: '.test-context.md',
commandPath: '.test',
isCustomCommandPath: false
};

(mockFs.pathExists as any).mockResolvedValueOnce(true);
(mockFs.readdir as any).mockResolvedValue(['command1.md']);
(mockFs.readFile as any).mockResolvedValue('Review docs/ai/design/feature-{name}.md and docs/ai/requirements/.');
(mockFs.writeFile as any).mockResolvedValue(undefined);

await (customManager as any).setupSingleEnvironment(env);

expect(mockFs.writeFile).toHaveBeenCalledWith(
path.join(customManager['targetDir'], env.commandPath, 'command1.md'),
'Review .ai-docs/design/feature-{name}.md and .ai-docs/requirements/.'
);
});

it('should skip commands when isCustomCommandPath is true', async () => {
const env: EnvironmentDefinition = {
code: 'test-env',
Expand Down Expand Up @@ -235,6 +264,25 @@ This is the prompt content.`;
);
expect(result).toBe(path.join(templateManager['targetDir'], 'docs', 'ai', phase, 'README.md'));
});

it('should use custom docsDir when provided', async () => {
const customManager = new TemplateManager('/test/target', '.ai-docs');
const phase: Phase = 'design';

(mockFs.ensureDir as any).mockResolvedValue(undefined);
(mockFs.copy as any).mockResolvedValue(undefined);

const result = await customManager.copyPhaseTemplate(phase);

expect(mockFs.ensureDir).toHaveBeenCalledWith(
path.join(customManager['targetDir'], '.ai-docs', phase)
);
expect(mockFs.copy).toHaveBeenCalledWith(
path.join(customManager['templatesDir'], 'phases', `${phase}.md`),
path.join(customManager['targetDir'], '.ai-docs', phase, 'README.md')
);
expect(result).toBe(path.join(customManager['targetDir'], '.ai-docs', phase, 'README.md'));
});
});

describe('fileExists', () => {
Expand Down Expand Up @@ -263,6 +311,20 @@ This is the prompt content.`;
);
expect(result).toBe(false);
});

it('should check custom docsDir path when provided', async () => {
const customManager = new TemplateManager('/test/target', 'custom/docs');
const phase: Phase = 'testing';

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

const result = await customManager.fileExists(phase);

expect(mockFs.pathExists).toHaveBeenCalledWith(
path.join(customManager['targetDir'], 'custom/docs', phase, 'README.md')
);
expect(result).toBe(true);
});
});

describe('setupMultipleEnvironments', () => {
Expand Down Expand Up @@ -633,12 +695,13 @@ description: Test
mockGetEnvironment.mockReturnValue(envWithGlobal);
(mockFs.ensureDir as any).mockResolvedValue(undefined);
(mockFs.readdir as any).mockResolvedValue(mockCommandFiles);
(mockFs.copy as any).mockResolvedValue(undefined);
(mockFs.readFile as any).mockResolvedValue('command content');
(mockFs.writeFile as any).mockResolvedValue(undefined);

const result = await templateManager.copyCommandsToGlobal('antigravity');

expect(mockFs.ensureDir).toHaveBeenCalled();
expect(mockFs.copy).toHaveBeenCalledTimes(2); // Only .md files
expect(mockFs.writeFile).toHaveBeenCalledTimes(2); // Only .md files
expect(result).toHaveLength(2);
});

Expand All @@ -656,11 +719,12 @@ description: Test
mockGetEnvironment.mockReturnValue(envWithGlobal);
(mockFs.ensureDir as any).mockResolvedValue(undefined);
(mockFs.readdir as any).mockResolvedValue(mockCommandFiles);
(mockFs.copy as any).mockResolvedValue(undefined);
(mockFs.readFile as any).mockResolvedValue('command content');
(mockFs.writeFile as any).mockResolvedValue(undefined);

const result = await templateManager.copyCommandsToGlobal('codex');

expect(mockFs.copy).toHaveBeenCalledTimes(1);
expect(mockFs.writeFile).toHaveBeenCalledTimes(1);
expect(result).toHaveLength(1);
});

Expand Down
Loading