Skip to content

Commit 357a2f9

Browse files
authored
Merge pull request #50 from objectstack-ai/copilot/add-cli-command-tests
2 parents d45de1a + 7811b07 commit 357a2f9

File tree

5 files changed

+823
-3
lines changed

5 files changed

+823
-3
lines changed

packages/cli/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"dev": "node ./bin/cli.mjs dev ../../content/docs",
1919
"build": "node ./bin/cli.mjs build ../../content/docs",
2020
"translate": "node ./bin/cli.mjs translate",
21-
"test": "echo \"No test specified\" && exit 0"
21+
"test": "vitest run"
2222
},
2323
"dependencies": {
2424
"@objectdocs/site": "workspace:*",
@@ -27,5 +27,8 @@
2727
"dotenv": "^16.4.5",
2828
"openai": "^4.0.0",
2929
"typescript": "^5.9.3"
30+
},
31+
"devDependencies": {
32+
"vitest": "^4.0.18"
3033
}
3134
}
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
/**
2+
* ObjectDocs CLI Lifecycle Integration Tests
3+
*
4+
* Verifies the complete end-user workflow:
5+
* 1. Initialize a new project repository
6+
* 2. Run `objectdocs init` to scaffold the documentation site
7+
* 3. Create documentation content (MDX files, meta.json, config)
8+
* 4. Run `objectdocs build` to produce a production build
9+
*
10+
* These tests use a shared temporary directory and run sequentially
11+
* to mirror the real user experience.
12+
*/
13+
14+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
15+
import { execSync, execFileSync } from 'child_process';
16+
import path from 'path';
17+
import fs from 'fs';
18+
import os from 'os';
19+
import { fileURLToPath } from 'url';
20+
21+
const __filename = fileURLToPath(import.meta.url);
22+
const __dirname = path.dirname(__filename);
23+
24+
const CLI_PATH = path.resolve(__dirname, '../bin/cli.mjs');
25+
const MONOREPO_ROOT = path.resolve(__dirname, '../../..');
26+
27+
/** Shared temporary directory for all tests in this suite */
28+
let testDir;
29+
30+
/**
31+
* Helper: run a CLI command inside the test project directory.
32+
* Inherits stdio so build/install output is visible in CI logs.
33+
*/
34+
function runCli(args, options = {}) {
35+
const { cwd = testDir, env: extraEnv = {}, timeout = 300_000 } = options;
36+
return execFileSync(process.execPath, [CLI_PATH, ...args], {
37+
cwd,
38+
timeout,
39+
stdio: 'inherit',
40+
env: { ...process.env, ...extraEnv },
41+
});
42+
}
43+
44+
describe('CLI Lifecycle: init → content → build', () => {
45+
beforeAll(() => {
46+
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'objectdocs-test-'));
47+
});
48+
49+
afterAll(() => {
50+
if (testDir && fs.existsSync(testDir)) {
51+
fs.rmSync(testDir, { recursive: true, force: true });
52+
}
53+
});
54+
55+
// ------------------------------------------------------------------
56+
// Step 1: The CLI binary loads and prints help without errors
57+
// ------------------------------------------------------------------
58+
it('should display help information', () => {
59+
const output = execFileSync(process.execPath, [CLI_PATH, '--help'], {
60+
encoding: 'utf-8',
61+
timeout: 10_000,
62+
});
63+
64+
expect(output).toContain('init');
65+
expect(output).toContain('dev');
66+
expect(output).toContain('build');
67+
expect(output).toContain('start');
68+
});
69+
70+
// ------------------------------------------------------------------
71+
// Step 2: Initialize a fresh project with `objectdocs init`
72+
// ------------------------------------------------------------------
73+
it('should initialize a new project with objectdocs init', () => {
74+
// Create a minimal package.json (simulating `npm init -y`)
75+
fs.writeFileSync(
76+
path.join(testDir, 'package.json'),
77+
JSON.stringify({ name: 'test-project', version: '1.0.0' }, null, 2),
78+
);
79+
80+
// Run init
81+
runCli(['init']);
82+
83+
// --- Assertions ---
84+
85+
// .fumadocs directory must be created
86+
const fumadocsDir = path.join(testDir, 'content', '.fumadocs');
87+
expect(fs.existsSync(fumadocsDir)).toBe(true);
88+
89+
// .fumadocs should contain a package.json (the site engine)
90+
expect(fs.existsSync(path.join(fumadocsDir, 'package.json'))).toBe(true);
91+
92+
// content/package.json should be created with dev/build/start scripts
93+
const contentPkg = JSON.parse(
94+
fs.readFileSync(path.join(testDir, 'content', 'package.json'), 'utf-8'),
95+
);
96+
expect(contentPkg.scripts).toBeDefined();
97+
expect(contentPkg.scripts.dev).toContain('.fumadocs');
98+
expect(contentPkg.scripts.build).toContain('.fumadocs');
99+
expect(contentPkg.scripts.start).toContain('.fumadocs');
100+
101+
// .gitignore should include content/.fumadocs
102+
const gitignore = fs.readFileSync(path.join(testDir, '.gitignore'), 'utf-8');
103+
expect(gitignore).toContain('content/.fumadocs');
104+
105+
// node_modules should be installed inside .fumadocs
106+
expect(fs.existsSync(path.join(fumadocsDir, 'node_modules'))).toBe(true);
107+
});
108+
109+
// ------------------------------------------------------------------
110+
// Step 3: Running init again should detect existing installation
111+
// ------------------------------------------------------------------
112+
it('should detect already-initialized project on second init', () => {
113+
const output = execFileSync(process.execPath, [CLI_PATH, 'init'], {
114+
cwd: testDir,
115+
encoding: 'utf-8',
116+
timeout: 30_000,
117+
});
118+
119+
expect(output).toContain('already initialized');
120+
});
121+
122+
// ------------------------------------------------------------------
123+
// Step 4: Create documentation content
124+
// ------------------------------------------------------------------
125+
it('should allow creating documentation content', () => {
126+
const docsDir = path.join(testDir, 'content', 'docs');
127+
fs.mkdirSync(docsDir, { recursive: true });
128+
129+
// Site configuration
130+
const siteConfig = {
131+
branding: { name: 'Test Docs Site' },
132+
};
133+
fs.writeFileSync(
134+
path.join(testDir, 'content', 'docs.site.json'),
135+
JSON.stringify(siteConfig, null, 2),
136+
);
137+
138+
// Navigation meta
139+
const meta = { pages: ['index', 'getting-started'] };
140+
fs.writeFileSync(
141+
path.join(docsDir, 'meta.json'),
142+
JSON.stringify(meta, null, 2),
143+
);
144+
145+
// Index page
146+
fs.writeFileSync(
147+
path.join(docsDir, 'index.mdx'),
148+
[
149+
'---',
150+
'title: Welcome',
151+
'description: Welcome to the test documentation site',
152+
'---',
153+
'',
154+
'# Welcome',
155+
'',
156+
'This is the home page of the test documentation site.',
157+
].join('\n'),
158+
);
159+
160+
// Getting Started page
161+
fs.writeFileSync(
162+
path.join(docsDir, 'getting-started.mdx'),
163+
[
164+
'---',
165+
'title: Getting Started',
166+
'description: Quick start guide',
167+
'---',
168+
'',
169+
'# Getting Started',
170+
'',
171+
'Follow these steps to get up and running.',
172+
].join('\n'),
173+
);
174+
175+
// --- Assertions ---
176+
expect(fs.existsSync(path.join(docsDir, 'meta.json'))).toBe(true);
177+
expect(fs.existsSync(path.join(docsDir, 'index.mdx'))).toBe(true);
178+
expect(fs.existsSync(path.join(docsDir, 'getting-started.mdx'))).toBe(true);
179+
expect(fs.existsSync(path.join(testDir, 'content', 'docs.site.json'))).toBe(true);
180+
181+
const savedMeta = JSON.parse(
182+
fs.readFileSync(path.join(docsDir, 'meta.json'), 'utf-8'),
183+
);
184+
expect(savedMeta.pages).toEqual(['index', 'getting-started']);
185+
});
186+
187+
// ------------------------------------------------------------------
188+
// Step 5: Build the documentation site
189+
// ------------------------------------------------------------------
190+
it('should build the documentation site successfully', () => {
191+
// Run build
192+
runCli(['build']);
193+
194+
// The build should create a .next directory inside .fumadocs
195+
const nextDir = path.join(testDir, 'content', '.fumadocs', '.next');
196+
expect(fs.existsSync(nextDir)).toBe(true);
197+
198+
// The build command copies .next to the project root
199+
const rootNextDir = path.join(testDir, '.next');
200+
expect(fs.existsSync(rootNextDir)).toBe(true);
201+
});
202+
203+
// ------------------------------------------------------------------
204+
// Step 6: Migrate markdown files to MDX
205+
// ------------------------------------------------------------------
206+
it('should migrate markdown files to MDX format', () => {
207+
// Create a sample markdown file at the project root
208+
const mdContent = [
209+
'# My Guide',
210+
'',
211+
'This is a comprehensive guide for getting started.',
212+
'',
213+
'## Installation',
214+
'',
215+
'```bash',
216+
'npm install my-lib',
217+
'```',
218+
'',
219+
'## Usage',
220+
'',
221+
'Import and use the library.',
222+
].join('\n');
223+
fs.writeFileSync(path.join(testDir, 'GUIDE.md'), mdContent);
224+
225+
// Run migrate
226+
runCli(['migrate', 'GUIDE.md', '--output', 'content/docs']);
227+
228+
// Verify the MDX file was created
229+
const mdxPath = path.join(testDir, 'content', 'docs', 'guide.mdx');
230+
expect(fs.existsSync(mdxPath)).toBe(true);
231+
232+
// Verify frontmatter was added
233+
const mdxContent = fs.readFileSync(mdxPath, 'utf-8');
234+
expect(mdxContent).toContain('---');
235+
expect(mdxContent).toContain('title: "My Guide"');
236+
expect(mdxContent).toContain('description:');
237+
238+
// Verify the H1 heading is removed (it's in frontmatter now)
239+
expect(mdxContent).not.toMatch(/^# My Guide$/m);
240+
241+
// Verify content body is preserved (code blocks, sections)
242+
expect(mdxContent).toContain('## Installation');
243+
expect(mdxContent).toContain('npm install my-lib');
244+
expect(mdxContent).toContain('## Usage');
245+
246+
// Verify meta.json was updated
247+
const meta = JSON.parse(
248+
fs.readFileSync(path.join(testDir, 'content', 'docs', 'meta.json'), 'utf-8'),
249+
);
250+
expect(meta.pages).toContain('guide');
251+
});
252+
});

packages/cli/vitest.config.mjs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { defineConfig } from 'vitest/config';
2+
3+
export default defineConfig({
4+
test: {
5+
testTimeout: 600_000, // 10 minutes for integration tests
6+
hookTimeout: 600_000,
7+
teardownTimeout: 30_000,
8+
},
9+
});

packages/site/next.config.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
*/
88

99
import { createMDX } from 'fumadocs-mdx/next';
10+
import path from 'path';
11+
import { fileURLToPath } from 'url';
1012

13+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
1114
const withMDX = createMDX();
1215

1316
/** @type {import('next').NextConfig} */
@@ -16,6 +19,7 @@ const nextConfig = {
1619
distDir: '.next',
1720
images: { unoptimized: true },
1821
output: 'standalone',
22+
outputFileTracingRoot: path.resolve(__dirname, '../..'),
1923
transpilePackages: ['@objectdocs/site'],
2024
async rewrites() {
2125
return [

0 commit comments

Comments
 (0)