Skip to content

Commit 4c4a89b

Browse files
committed
Add e2e test
1 parent 247bf1a commit 4c4a89b

File tree

6 files changed

+427
-1
lines changed

6 files changed

+427
-1
lines changed

.github/workflows/e2e.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: E2E Tests
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
branches:
9+
- main
10+
11+
jobs:
12+
e2e:
13+
name: E2E (Node ${{ matrix.node-version }}, ${{ matrix.os }})
14+
runs-on: ${{ matrix.os }}
15+
strategy:
16+
fail-fast: false
17+
matrix:
18+
node-version: [20, 22, 24]
19+
os: [ubuntu-latest, macos-latest]
20+
21+
steps:
22+
- name: Checkout
23+
uses: actions/checkout@v4
24+
with:
25+
fetch-depth: 0
26+
27+
- name: Setup Node.js ${{ matrix.node-version }}
28+
uses: actions/setup-node@v4
29+
with:
30+
node-version: ${{ matrix.node-version }}
31+
cache: 'npm'
32+
33+
- name: Install dependencies
34+
run: npm ci
35+
36+
- name: Build
37+
run: npm run build
38+
39+
- name: Run E2E tests
40+
run: npx jest --config e2e/jest.config.js --verbose

e2e/cli.e2e.ts

Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
import { existsSync, readFileSync } from 'fs';
2+
import { join } from 'path';
3+
import { run, createTempProject, cleanupTempProject, writeConfigFile } from './helpers';
4+
5+
describe('CLI basics', () => {
6+
it('should print version', () => {
7+
const result = run('--version');
8+
expect(result.exitCode).toBe(0);
9+
expect(result.stdout.trim()).toMatch(/^\d+\.\d+\.\d+$/);
10+
});
11+
12+
it('should print help', () => {
13+
const result = run('--help');
14+
expect(result.exitCode).toBe(0);
15+
expect(result.stdout).toContain('ai-devkit');
16+
expect(result.stdout).toContain('init');
17+
expect(result.stdout).toContain('lint');
18+
expect(result.stdout).toContain('memory');
19+
expect(result.stdout).toContain('skill');
20+
expect(result.stdout).toContain('phase');
21+
});
22+
23+
it('should exit with error for unknown command', () => {
24+
const result = run('nonexistent-command');
25+
expect(result.exitCode).not.toBe(0);
26+
});
27+
});
28+
29+
describe('init command', () => {
30+
let projectDir: string;
31+
32+
beforeEach(() => {
33+
projectDir = createTempProject();
34+
});
35+
36+
afterEach(() => {
37+
cleanupTempProject(projectDir);
38+
});
39+
40+
it('should initialize with environment and all phases', () => {
41+
const result = run('init -e claude --all', { cwd: projectDir });
42+
expect(result.exitCode).toBe(0);
43+
expect(result.stdout).toContain('AI DevKit initialized successfully');
44+
45+
// Config file should exist
46+
const configPath = join(projectDir, '.ai-devkit.json');
47+
expect(existsSync(configPath)).toBe(true);
48+
49+
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
50+
expect(config.environments).toContain('claude');
51+
expect(config.phases).toEqual(
52+
expect.arrayContaining(['requirements', 'design', 'planning', 'implementation', 'testing', 'deployment', 'monitoring'])
53+
);
54+
});
55+
56+
it('should initialize with specific phases', () => {
57+
const result = run('init -e cursor -p requirements,design', { cwd: projectDir });
58+
expect(result.exitCode).toBe(0);
59+
60+
const config = JSON.parse(readFileSync(join(projectDir, '.ai-devkit.json'), 'utf-8'));
61+
expect(config.environments).toContain('cursor');
62+
expect(config.phases).toContain('requirements');
63+
expect(config.phases).toContain('design');
64+
expect(config.phases).not.toContain('monitoring');
65+
});
66+
67+
it('should create phase template files in docs/ai', () => {
68+
run('init -e claude -p requirements,planning', { cwd: projectDir });
69+
70+
expect(existsSync(join(projectDir, 'docs', 'ai', 'requirements', 'README.md'))).toBe(true);
71+
expect(existsSync(join(projectDir, 'docs', 'ai', 'planning', 'README.md'))).toBe(true);
72+
});
73+
74+
it('should support custom docs directory', () => {
75+
run('init -e claude -p requirements -d custom/docs', { cwd: projectDir });
76+
77+
expect(existsSync(join(projectDir, 'custom', 'docs', 'requirements', 'README.md'))).toBe(true);
78+
});
79+
80+
it('should create environment config files', () => {
81+
run('init -e claude --all', { cwd: projectDir });
82+
83+
// Claude environment creates .claude/commands/ directory
84+
expect(existsSync(join(projectDir, '.claude', 'commands'))).toBe(true);
85+
});
86+
87+
it('should initialize with template file', () => {
88+
const templatePath = join(projectDir, 'template.yaml');
89+
const templateContent = `environments:
90+
- claude
91+
phases:
92+
- requirements
93+
- design
94+
paths:
95+
docs: docs/ai
96+
`;
97+
require('fs').writeFileSync(templatePath, templateContent);
98+
99+
const result = run(`init -t "${templatePath}"`, { cwd: projectDir });
100+
expect(result.exitCode).toBe(0);
101+
expect(result.stdout).toContain('AI DevKit initialized successfully');
102+
});
103+
});
104+
105+
describe('lint command', () => {
106+
let projectDir: string;
107+
108+
beforeEach(() => {
109+
projectDir = createTempProject();
110+
});
111+
112+
afterEach(() => {
113+
cleanupTempProject(projectDir);
114+
});
115+
116+
it('should run lint on uninitialized project', () => {
117+
const result = run('lint', { cwd: projectDir });
118+
// Should complete (may have failures but shouldn't crash)
119+
expect(result.stdout).toBeDefined();
120+
});
121+
122+
it('should run lint with --json flag', () => {
123+
const result = run('lint --json', { cwd: projectDir });
124+
const output = result.stdout.trim();
125+
const json = JSON.parse(output);
126+
expect(json).toHaveProperty('checks');
127+
expect(json).toHaveProperty('summary');
128+
expect(json).toHaveProperty('pass');
129+
});
130+
131+
it('should lint initialized project', () => {
132+
run('init -e claude --all', { cwd: projectDir });
133+
const result = run('lint --json', { cwd: projectDir });
134+
const json = JSON.parse(result.stdout.trim());
135+
expect(json).toHaveProperty('checks');
136+
expect(Array.isArray(json.checks)).toBe(true);
137+
});
138+
139+
it('should lint with feature flag', () => {
140+
run('init -e claude --all', { cwd: projectDir });
141+
const result = run('lint -f my-feature --json', { cwd: projectDir });
142+
const json = JSON.parse(result.stdout.trim());
143+
expect(json).toHaveProperty('feature');
144+
expect(json.feature.normalizedName).toBe('my-feature');
145+
});
146+
});
147+
148+
describe('memory commands', () => {
149+
let projectDir: string;
150+
let uid: string;
151+
152+
beforeEach(() => {
153+
projectDir = createTempProject();
154+
uid = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
155+
});
156+
157+
afterEach(() => {
158+
cleanupTempProject(projectDir);
159+
});
160+
161+
it('should store and search knowledge', () => {
162+
const title = `E2E API Design Practices ${uid}`;
163+
const storeResult = run(
164+
`memory store -t "${title}" -c "When building REST APIs always use Response DTOs instead of returning domain entities directly ref ${uid}."`,
165+
{ cwd: projectDir }
166+
);
167+
expect(storeResult.exitCode).toBe(0);
168+
const stored = JSON.parse(storeResult.stdout.trim());
169+
expect(stored.success).toBe(true);
170+
expect(stored.id).toBeDefined();
171+
172+
const searchResult = run(`memory search -q "${title}"`, { cwd: projectDir });
173+
expect(searchResult.exitCode).toBe(0);
174+
const searched = JSON.parse(searchResult.stdout.trim());
175+
expect(searched.results).toBeDefined();
176+
expect(searched.results.length).toBeGreaterThan(0);
177+
});
178+
179+
it('should store with tags and scope', () => {
180+
const result = run(
181+
`memory store -t "E2E Backend Testing Strategy ${uid}" -c "Integration tests should always hit a real database rather than mocks ensuring migration issues are caught ref ${uid}." --tags "testing,backend" -s "project:e2e-${uid}"`,
182+
{ cwd: projectDir }
183+
);
184+
expect(result.exitCode).toBe(0);
185+
const stored = JSON.parse(result.stdout.trim());
186+
expect(stored.success).toBe(true);
187+
});
188+
189+
it('should update stored knowledge', () => {
190+
const storeResult = run(
191+
`memory store -t "E2E Deployment Checklist ${uid}" -c "Before deploying to production ensure all tests pass and database migrations are reviewed and documented ref ${uid}."`,
192+
{ cwd: projectDir }
193+
);
194+
expect(storeResult.exitCode).toBe(0);
195+
const stored = JSON.parse(storeResult.stdout.trim());
196+
197+
const updateResult = run(
198+
`memory update --id ${stored.id} -t "E2E Updated Deployment Checklist ${uid}"`,
199+
{ cwd: projectDir }
200+
);
201+
expect(updateResult.exitCode).toBe(0);
202+
const updated = JSON.parse(updateResult.stdout.trim());
203+
expect(updated.success).toBe(true);
204+
});
205+
206+
it('should search with --table flag', () => {
207+
run(
208+
`memory store -t "E2E Component Architecture ${uid}" -c "Use compound components pattern for complex UI elements providing better composition and reducing prop drilling ref ${uid}."`,
209+
{ cwd: projectDir }
210+
);
211+
212+
const result = run(`memory search -q "E2E Component Architecture ${uid}" --table`, { cwd: projectDir });
213+
expect(result.exitCode).toBe(0);
214+
expect(result.stdout).toContain('id');
215+
expect(result.stdout).toContain('title');
216+
expect(result.stdout).toContain('scope');
217+
});
218+
219+
it('should reject invalid store input', () => {
220+
const result = run('memory store -t "Short" -c "Too short"', { cwd: projectDir });
221+
expect(result.exitCode).not.toBe(0);
222+
});
223+
224+
it('should search with limit', () => {
225+
for (let i = 1; i <= 3; i++) {
226+
run(
227+
`memory store -t "E2E Knowledge item ${i} ${uid}" -c "This is detailed content for knowledge item number ${i} with unique identifier ${uid} to meet the minimum length."`,
228+
{ cwd: projectDir }
229+
);
230+
}
231+
232+
const result = run(`memory search -q "E2E Knowledge item ${uid}" -l 2`, { cwd: projectDir });
233+
expect(result.exitCode).toBe(0);
234+
const searched = JSON.parse(result.stdout.trim());
235+
expect(searched.results.length).toBeLessThanOrEqual(2);
236+
});
237+
});
238+
239+
describe('phase command', () => {
240+
let projectDir: string;
241+
242+
beforeEach(() => {
243+
projectDir = createTempProject();
244+
// Initialize first
245+
run('init -e claude -p requirements', { cwd: projectDir });
246+
});
247+
248+
afterEach(() => {
249+
cleanupTempProject(projectDir);
250+
});
251+
252+
it('should add a new phase', () => {
253+
const result = run('phase testing', { cwd: projectDir });
254+
expect(result.exitCode).toBe(0);
255+
256+
expect(existsSync(join(projectDir, 'docs', 'ai', 'testing', 'README.md'))).toBe(true);
257+
258+
const config = JSON.parse(readFileSync(join(projectDir, '.ai-devkit.json'), 'utf-8'));
259+
expect(config.phases).toContain('testing');
260+
});
261+
});
262+
263+
describe('install command', () => {
264+
let projectDir: string;
265+
266+
beforeEach(() => {
267+
projectDir = createTempProject();
268+
});
269+
270+
afterEach(() => {
271+
cleanupTempProject(projectDir);
272+
});
273+
274+
it('should install from config file', () => {
275+
writeConfigFile(projectDir, {
276+
version: '1.0.0',
277+
environments: ['claude'],
278+
phases: ['requirements', 'design'],
279+
createdAt: new Date().toISOString(),
280+
updatedAt: new Date().toISOString()
281+
});
282+
283+
const result = run('install', { cwd: projectDir });
284+
expect(result.exitCode).toBe(0);
285+
});
286+
287+
it('should fail with missing config file', () => {
288+
const result = run('install -c nonexistent.json', { cwd: projectDir });
289+
expect(result.exitCode).not.toBe(0);
290+
});
291+
});
292+
293+
describe('skill command', () => {
294+
it('should list skills (empty)', () => {
295+
const projectDir = createTempProject();
296+
run('init -e claude -p requirements', { cwd: projectDir });
297+
298+
const result = run('skill list', { cwd: projectDir });
299+
expect(result.exitCode).toBe(0);
300+
301+
cleanupTempProject(projectDir);
302+
});
303+
});
304+
305+
describe('Node.js compatibility', () => {
306+
it('should report correct Node.js version range support', () => {
307+
const nodeVersion = process.version;
308+
const major = parseInt(nodeVersion.slice(1).split('.')[0], 10);
309+
expect(major).toBeGreaterThanOrEqual(20);
310+
311+
// CLI should work on this Node version
312+
const result = run('--version');
313+
expect(result.exitCode).toBe(0);
314+
});
315+
});

0 commit comments

Comments
 (0)