leetkick
Version:
A CLI tool for scaffolding LeetCode exercises with language-specific testing setups
247 lines (200 loc) • 7.63 kB
text/typescript
import test from 'node:test';
import assert from 'node:assert';
import {spawn} from 'node:child_process';
import {promises as fs} from 'fs';
import {join} from 'path';
import {tmpdir} from 'os';
const CLI_PATH = join(process.cwd(), 'build', 'src', 'index.js');
void test('CLI integration test suite', async t => {
const testWorkspace = join(tmpdir(), 'leetcode-cli-integration-test');
await t.test('should show help when no command provided', async () => {
const result = await runCLI([], {expectError: true});
// Help is output to stderr by commander.js
const output = result.stderr || result.stdout;
assert(output.includes('CLI tool for scaffolding LeetCode exercises'));
assert(output.includes('Commands:'));
assert(output.includes('fetch'));
assert(output.includes('init'));
assert(output.includes('add'));
});
await t.test('should show version information', async () => {
const result = await runCLI(['--version']);
assert(result.stdout.includes('0.1.0'));
});
await t.test('should show fetch command help', async () => {
const result = await runCLI(['fetch', '--help']);
assert(result.stdout.includes('Fetch a LeetCode problem'));
assert(result.stdout.includes('<problem-slug>'));
assert(result.stdout.includes('--language'));
});
await t.test('should show init command help', async () => {
const result = await runCLI(['init', '--help']);
assert(result.stdout.includes('Initialize an empty leetkick workspace'));
});
await t.test('should show add command help', async () => {
const result = await runCLI(['add', '--help']);
assert(result.stdout.includes('Add a language workspace'));
assert(result.stdout.includes('<language>'));
});
await t.test(
'should list available languages when no language specified',
async () => {
// Setup test workspace with initialized workspace
await fs.rm(testWorkspace, {recursive: true, force: true});
await fs.mkdir(testWorkspace, {recursive: true});
process.chdir(testWorkspace);
await runCLI(['init']);
await runCLI(['add', 'typescript']);
const result = await runCLI(['fetch', 'two-sum'], {expectError: true});
// Check both stdout and stderr for the error message
const combinedOutput = result.stdout + result.stderr;
assert(
combinedOutput.includes('Available languages:') ||
combinedOutput.includes('Please specify a language'),
);
assert(combinedOutput.includes('typescript'));
},
);
await t.test('should handle invalid language gracefully', async () => {
// Use the workspace from previous test
const result = await runCLI(
['fetch', 'two-sum', '--language', 'invalidlang'],
{expectError: true},
);
const combinedOutput = result.stdout + result.stderr;
assert(
combinedOutput.includes("Language 'invalidlang' not supported") ||
combinedOutput.includes('not supported'),
);
assert(combinedOutput.includes('Available languages:'));
});
await t.test('should handle invalid problem slug gracefully', async () => {
const result = await runCLI(
[
'fetch',
'invalid-problem-that-does-not-exist',
'--language',
'typescript',
],
{expectError: true},
);
assert(result.stderr.includes('Error:'));
// Should handle LeetCode API errors
});
await t.test('should initialize empty workspace successfully', async () => {
// Setup fresh test workspace
await fs.rm(testWorkspace, {recursive: true, force: true});
await fs.mkdir(testWorkspace, {recursive: true});
process.chdir(testWorkspace);
const result = await runCLI(['init']);
assert(result.stdout.includes('Creating leetkick workspace'));
assert(result.stdout.includes('Workspace initialized'));
// Verify workspace marker was created
const workspaceExists = await fs
.access(join(testWorkspace, '.leetkick.json'))
.then(() => true)
.catch(() => false);
assert(workspaceExists);
});
await t.test('should add language to workspace successfully', async () => {
// First initialize workspace
await runCLI(['init']);
// Then add typescript
const result = await runCLI(['add', 'typescript']);
assert(result.stdout.includes('Adding typescript workspace'));
assert(result.stdout.includes('Created typescript workspace'));
// Verify typescript workspace was created
const typescriptExists = await fs
.access(join(testWorkspace, 'typescript'))
.then(() => true)
.catch(() => false);
assert(typescriptExists);
});
await t.test(
'should add rust language to workspace successfully',
async () => {
// First initialize workspace
await runCLI(['init']);
// Then add rust
const result = await runCLI(['add', 'rust']);
assert(result.stdout.includes('Adding rust workspace'));
assert(result.stdout.includes('Created rust workspace'));
// Verify rust workspace was created
const rustExists = await fs
.access(join(testWorkspace, 'rust'))
.then(() => true)
.catch(() => false);
assert(rustExists);
// Verify Cargo.toml was created
const cargoExists = await fs
.access(join(testWorkspace, 'rust', 'Cargo.toml'))
.then(() => true)
.catch(() => false);
assert(cargoExists);
// Verify src directory was created
const srcExists = await fs
.access(join(testWorkspace, 'rust', 'src'))
.then(() => true)
.catch(() => false);
assert(srcExists);
},
);
await t.test('should require workspace for add command', async () => {
// Setup fresh directory without workspace
const noWorkspaceDir = join(tmpdir(), 'no-workspace-test');
await fs.rm(noWorkspaceDir, {recursive: true, force: true});
await fs.mkdir(noWorkspaceDir, {recursive: true});
process.chdir(noWorkspaceDir);
const result = await runCLI(['add', 'typescript'], {expectError: true});
const output = result.stdout || result.stderr;
assert(output.includes('No leetkick workspace found'));
assert(output.includes('Run "leetkick init" first'));
// Cleanup
await fs.rm(noWorkspaceDir, {recursive: true, force: true});
});
// Cleanup
await fs.rm(testWorkspace, {recursive: true, force: true});
});
// Helper function to run CLI commands
async function runCLI(
args: string[],
options: {expectError?: boolean; timeout?: number} = {},
): Promise<{stdout: string; stderr: string; exitCode: number}> {
return new Promise((resolve, reject) => {
const child = spawn('node', [CLI_PATH, ...args], {
stdio: ['pipe', 'pipe', 'pipe'],
});
let stdout = '';
let stderr = '';
child.stdout?.on('data', data => {
stdout += data.toString();
});
child.stderr?.on('data', data => {
stderr += data.toString();
});
const timeout = setTimeout(() => {
child.kill('SIGTERM');
reject(new Error('CLI command timed out'));
}, options.timeout || 10000);
child.on('close', code => {
clearTimeout(timeout);
if (!options.expectError && code !== 0) {
reject(
new Error(
`CLI command failed with exit code ${code}\nstdout: ${stdout}\nstderr: ${stderr}`,
),
);
} else {
resolve({
stdout,
stderr,
exitCode: code || 0,
});
}
});
child.on('error', error => {
clearTimeout(timeout);
reject(error);
});
});
}