docs-v2/scripts/docs-cli/__tests__/cli-integration.test.js

392 lines
12 KiB
JavaScript

#!/usr/bin/env node
/**
* Integration tests for docs CLI commands
*
* These tests verify that commands can be loaded and executed without
* import/module resolution errors. They catch issues like:
* - Bad relative import paths (e.g., ./lib/ vs ../../lib/)
* - Missing dependencies
* - Syntax errors
*
* Run with: node scripts/docs-cli/__tests__/cli-integration.test.js
*/
import { spawn } from 'child_process';
import { writeFileSync, unlinkSync, mkdirSync, rmdirSync } from 'fs';
import { join } from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Colors for output
const colors = {
green: (s) => `\x1b[32m${s}\x1b[0m`,
red: (s) => `\x1b[31m${s}\x1b[0m`,
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
};
let passed = 0;
let failed = 0;
/**
* Run a CLI command and check for success/failure
*/
function runCommand(args, options = {}) {
return new Promise((resolve) => {
const cliPath = join(__dirname, '..', 'docs-cli.js');
const proc = spawn('node', [cliPath, ...args], {
cwd: join(__dirname, '..', '..', '..'), // repo root
timeout: options.timeout || 10000,
env: { ...process.env, ...options.env },
});
let stdout = '';
let stderr = '';
proc.stdout.on('data', (data) => {
stdout += data.toString();
});
proc.stderr.on('data', (data) => {
stderr += data.toString();
});
proc.on('close', (code) => {
resolve({ code, stdout, stderr });
});
proc.on('error', (err) => {
resolve({ code: -1, stdout, stderr: err.message });
});
// Handle timeout
setTimeout(() => {
proc.kill();
resolve({ code: -1, stdout, stderr: 'Timeout' });
}, options.timeout || 10000);
});
}
function test(name, assertion) {
if (assertion) {
console.log(colors.green(`${name}`));
passed++;
} else {
console.log(colors.red(`${name}`));
failed++;
}
}
async function testCommandHelp(command, description) {
const result = await runCommand([command, '--help']);
const success = result.code === 0 && !result.stderr.includes('Error');
test(`${command} --help: ${description}`, success);
if (!success) {
console.log(colors.yellow(` stdout: ${result.stdout.slice(0, 200)}`));
console.log(colors.red(` stderr: ${result.stderr.slice(0, 500)}`));
}
return success;
}
async function testCommandExecution(command, args, description, validator) {
const result = await runCommand([command, ...args]);
const success = validator(result);
test(`${command} execution: ${description}`, success);
if (!success) {
console.log(colors.yellow(` exit code: ${result.code}`));
console.log(colors.yellow(` stdout: ${result.stdout.slice(0, 200)}`));
console.log(colors.red(` stderr: ${result.stderr.slice(0, 500)}`));
}
return success;
}
async function runTests() {
console.log(colors.cyan('\n🧪 CLI Integration Tests\n'));
console.log('Testing that all commands load without import errors...\n');
// Test 1: Main help loads
await testCommandHelp('--help', 'main CLI help loads');
// Test 2-6: Each command's help loads (catches import errors)
await testCommandHelp('create', 'loads without import errors');
await testCommandHelp('edit', 'loads without import errors');
await testCommandHelp('placeholders', 'loads without import errors');
await testCommandHelp('audit', 'loads without import errors');
await testCommandHelp('release-notes', 'loads without import errors');
// Test 7: Alias works
await testCommandHelp('add-placeholders', 'alias resolves correctly');
// Test 8: Unknown command gives proper error
const unknownResult = await runCommand(['nonexistent-command']);
test(
'unknown command: returns error message',
unknownResult.code !== 0 && unknownResult.stderr.includes('Unknown command')
);
// Test 9: Create command actually executes (not just --help)
// Create a temp draft file
const tempDir = join(__dirname, '..', '..', '..', '.tmp-test');
const tempDraft = join(tempDir, 'test-draft.md');
try {
mkdirSync(tempDir, { recursive: true });
writeFileSync(tempDraft, '# Test Draft\n\nThis is test content.');
// Run create with --context-only to avoid interactive prompts
// This exercises the actual code paths, not just argument parsing
await testCommandExecution(
'create',
[tempDraft, '--context-only'],
'executes with draft file (context preparation)',
(result) => {
// Must succeed (exit code 0)
if (result.code !== 0) {
console.log(
colors.red(` Command failed with exit code ${result.code}`)
);
return false;
}
// Check for actual errors (not progress messages which may go to stderr)
const hasError =
result.stderr.includes('Error:') ||
result.stderr.includes('ERR_') ||
result.stderr.includes('Cannot find module');
if (hasError) {
console.log(colors.red(` Error detected in output`));
return false;
}
return true;
}
);
} finally {
// Cleanup
try {
unlinkSync(tempDraft);
} catch {}
try {
rmdirSync(tempDir);
} catch {}
}
// Test 10: Edit command actually executes
await testCommandExecution(
'edit',
['/influxdb3/core/get-started/', '--list'],
'executes with URL (list mode)',
(result) => {
// Must succeed (exit code 0) with no errors
if (result.code !== 0) {
console.log(
colors.red(` Command failed with exit code ${result.code}`)
);
return false;
}
if (result.stderr && result.stderr.trim()) {
console.log(colors.red(` Unexpected stderr output`));
return false;
}
return true;
}
);
// Test 11: Placeholders command actually executes
// Create a temp file with a code block to test
const tempPlaceholderFile = join(tempDir, 'test-placeholders.md');
try {
mkdirSync(tempDir, { recursive: true });
writeFileSync(
tempPlaceholderFile,
'# Test\n\n```sql\nSELECT * FROM MY_TABLE\n```\n'
);
await testCommandExecution(
'placeholders',
[tempPlaceholderFile, '--dry'],
'executes with dry-run on test file',
(result) => {
// Must succeed (exit code 0) with no errors
if (result.code !== 0) {
console.log(
colors.red(` Command failed with exit code ${result.code}`)
);
return false;
}
if (result.stderr && result.stderr.trim()) {
console.log(colors.red(` Unexpected stderr output`));
return false;
}
return true;
}
);
} finally {
try {
unlinkSync(tempPlaceholderFile);
} catch {}
try {
rmdirSync(tempDir);
} catch {}
}
// Test 12: Create command errors helpfully when piping without --products
// Simulates piping by checking for the error message in stderr
await testCommandExecution(
'create',
['content/influxdb3/core/_index.md'],
'errors helpfully when piping without --products',
(result) => {
// When run in this test harness, stdout is not a TTY (similar to piping)
// Should exit with error about needing --products
const hasProductError =
result.stderr.includes('Cannot show interactive product selection') ||
result.stderr.includes('--products');
return result.code !== 0 && hasProductError;
}
);
// Test 13: Create command works when --products is provided (even when piping)
await testCommandExecution(
'create',
['content/influxdb3/core/_index.md', '--products', 'influxdb3_core'],
'succeeds with --products flag when piping',
(result) => {
// Should succeed and output prompt text
if (result.code !== 0) {
console.log(
colors.red(` Command failed with exit code ${result.code}`)
);
return false;
}
// Should contain prompt text in stdout
const hasPrompt = result.stdout.includes(
'expert InfluxData documentation'
);
if (!hasPrompt) {
console.log(colors.red(` Expected prompt text in stdout`));
return false;
}
return true;
}
);
// Test 14: release-notes --products flag parsing
await testCommandExecution(
'release-notes',
['v3.7.0', 'v3.8.0', '--products', 'influxdb3_core', '--no-fetch'],
'--products flag parses correctly',
(result) => {
// Should attempt to use the product (may fail due to missing repo, but parsing worked)
const parsedCorrectly =
result.stderr.includes('influxdb3_core') ||
result.stderr.includes('Using cached clone') ||
result.stderr.includes('Cloning');
return parsedCorrectly;
}
);
// Test 15: release-notes --repos flag parsing
await testCommandExecution(
'release-notes',
['v1.0.0', 'v1.1.0', '--repos', '/nonexistent/path', '--no-fetch'],
'--repos flag parses correctly',
(result) => {
// Should error about path not found (parsing worked, validation caught it)
return (
result.code !== 0 && result.stderr.includes('Repository path not found')
);
}
);
// Test 16: release-notes error when no repos specified
await testCommandExecution(
'release-notes',
['v3.7.0', 'v3.8.0'],
'errors when no --products or --repos specified',
(result) => {
return (
result.code !== 0 &&
result.stderr.includes('No repositories specified') &&
result.stderr.includes('--products') &&
result.stderr.includes('--repos')
);
}
);
// Test 17: audit command with --products flag (version defaults to main)
await testCommandExecution(
'audit',
['--products', 'influxdb3_core'],
'--products flag parses correctly',
(result) => {
// Should attempt to run audit (may fail due to missing auditor module)
const parsedCorrectly =
result.stderr.includes('influxdb3_core') ||
result.stderr.includes('Resolved products') ||
result.stderr.includes('Using cached clone') ||
result.stderr.includes('Running CLI audit') ||
result.stderr.includes('Cannot find module'); // Expected - auditor not in test env
return parsedCorrectly;
}
);
// Test 18: audit --repos flag parsing
await testCommandExecution(
'audit',
['--repos', '/nonexistent/path'],
'--repos flag parses correctly',
(result) => {
// Should error about path not found (parsing worked, validation caught it)
return (
result.code !== 0 && result.stderr.includes('Repository path not found')
);
}
);
// Test 19: audit error when no products or repos specified
await testCommandExecution(
'audit',
[],
'errors when no --products or --repos specified',
(result) => {
return (
result.code !== 0 &&
result.stderr.includes('No products or repositories specified') &&
result.stderr.includes('--products') &&
result.stderr.includes('--repos')
);
}
);
// Test 20: audit with invalid product key
await testCommandExecution(
'audit',
['--products', 'invalid_product'],
'errors on invalid product key',
(result) => {
return (
result.code !== 0 &&
result.stderr.includes('Could not resolve product identifier')
);
}
);
// Summary
console.log(colors.cyan(`\n📊 Results: ${passed}/${passed + failed} passed`));
if (failed > 0) {
console.log(colors.red(`\n${failed} test(s) failed\n`));
process.exit(1);
} else {
console.log(colors.green('\n✅ All integration tests passed!\n'));
process.exit(0);
}
}
runTests().catch((err) => {
console.error(colors.red('Test runner error:'), err);
process.exit(1);
});