Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
183 changes: 168 additions & 15 deletions src/cli.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { describe, it, expect, beforeEach, afterEach, afterAll } from 'vitest';
import { execSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
Expand All @@ -11,38 +11,52 @@ const testFile = path.join(testDir, 'captable.json');

describe('CLI Integration Tests', () => {
beforeEach(() => {
// Clean up any existing files first
fs.rmSync(testFile, { force: true });
if (!fs.existsSync(testDir)) {
fs.mkdirSync(testDir, { recursive: true });
}
process.chdir(testDir);
});

afterEach(() => {
if (fs.existsSync(testFile)) {
fs.unlinkSync(testFile);
}
// Switch back to the repo dir to release testDir handles (prevents EBUSY on Windows)
process.chdir(__dirname);
});

afterAll(() => {
// Clean up the test directory after all tests complete
// Be defensive: ensure we are not inside testDir before removing it
try {
process.chdir(__dirname);
} catch {}
if (fs.existsSync(testDir)) {
fs.rmSync(testDir, { recursive: true, force: true });
}
});

// Helper to filter out transient noise from command output
function stripNoise(output: string): string {
return output
.split('\n')
.filter((line: string) => !/^(npm WARN|npm notice|npx:)/i.test(line))
.join('\n')
.trim();
}

const runCLI = (args: string): string => {
try {
const output = execSync(`npx tsx ${cliPath} ${args}`, {
const output = execSync(`node --import tsx "${cliPath}" ${args}`, {
encoding: 'utf8',
cwd: testDir,
stdio: 'pipe',
});
// Filter out npm warnings
return output
.split('\n')
.filter((line) => !line.startsWith('npm warn'))
.join('\n');
// Filter out npm warnings/notices and npx chatter
return stripNoise(output);
} catch (error: any) {
const errorOutput = error.stdout || error.stderr || error.message;
// Filter out npm warnings from error output too
return errorOutput
.split('\n')
.filter((line) => !line.startsWith('npm warn'))
.join('\n');
// Filter out npm warnings/notices and npx chatter from error output too
return stripNoise(errorOutput);
}
};

Expand Down Expand Up @@ -298,4 +312,143 @@ describe('CLI Integration Tests', () => {
expect(output).toContain('Cannot issue');
});
});

describe('safes command', () => {
beforeEach(() => {
runCLI('init --name "Test Inc" --authorized 10000000 --state DE');
});

it('should list all SAFEs', () => {
runCLI('stakeholder --name "Investor 1"');
runCLI('stakeholder --name "Investor 2"');
const model = JSON.parse(fs.readFileSync(testFile, 'utf8'));
const investor1Id = model.stakeholders[0].id;
const investor2Id = model.stakeholders[1].id;

runCLI(`safe --holder ${investor1Id} --amount 100000 --post-money --cap 5000000`);
runCLI(`safe --holder ${investor2Id} --amount 250000`);

const output = runCLI('safes');

expect(output).toContain('Investor 1');
expect(output).toContain('100,000');
expect(output).toContain('Investor 2');
expect(output).toContain('250,000');
});

it('should handle no SAFEs gracefully', () => {
const output = runCLI('safes');
expect(output).toContain('No SAFEs');
});
});

describe('report command', () => {
beforeEach(() => {
runCLI('init --name "Test Inc" --authorized 10000000 --pool 1000000 --state DE');
});

it('should generate stakeholder report', () => {
runCLI('stakeholder --name "Alice" --email alice@test.com');
const model = JSON.parse(fs.readFileSync(testFile, 'utf8'));
const aliceId = model.stakeholders[0].id;
runCLI(`issue --holder ${aliceId} --qty 1000000`);

const output = runCLI(`report stakeholder ${aliceId}`);

expect(output).toContain('Alice');
expect(output).toContain('alice@test.com');
expect(output).toContain('1,000,000');
});

it('should generate security class report', () => {
const output = runCLI('report security sc_pool');

expect(output).toContain('Option Pool');
expect(output).toContain('OPTION_POOL');
});
});

describe('list command', () => {
beforeEach(() => {
runCLI('init --name "Test Inc" --authorized 10000000 --pool 1000000 --state DE');
});

it('should list stakeholders', () => {
runCLI('stakeholder --name "Person 1"');
runCLI('stakeholder --name "Company 1" --entity');

const output = runCLI('list stakeholders');

expect(output).toContain('Person 1');
expect(output).toContain('person');
expect(output).toContain('Company 1');
expect(output).toContain('entity');
});

it('should list securities', () => {
const output = runCLI('list securities');

expect(output).toContain('Common Stock');
expect(output).toContain('COMMON');
expect(output).toContain('Option Pool');
expect(output).toContain('OPTION_POOL');
});
});

describe('validate command', () => {
beforeEach(() => {
runCLI('init --name "Test Inc" --authorized 10000000 --state DE');
});

it('should validate a valid cap table', () => {
runCLI('stakeholder --name "Founder"');
const model = JSON.parse(fs.readFileSync(testFile, 'utf8'));
const founderId = model.stakeholders[0].id;
runCLI(`issue --holder ${founderId} --qty 1000000`);

const output = runCLI('validate');

expect(output).toContain('valid');
expect(output).not.toContain('error');
expect(output).not.toContain('warning');
});

it('should validate with extended validation', () => {
const output = runCLI('validate --extended');

expect(output).toContain('valid');
// Extended validation performs additional business rule checks
});
});

describe('schema command', () => {
it('should export schema to default file', () => {
const schemaFile = path.join(testDir, 'captable.schema.json');

// Remove schema file if it exists
fs.rmSync(schemaFile, { force: true });

const output = runCLI('schema');

expect(output).toContain('Schema exported');
expect(fs.existsSync(schemaFile)).toBe(true);

const schema = JSON.parse(fs.readFileSync(schemaFile, 'utf8'));
expect(schema).toHaveProperty('$schema');
expect(schema).toHaveProperty('definitions');
});

it('should export schema to custom file', () => {
const customFile = 'custom-schema.json';

// Remove custom file if it exists
fs.rmSync(path.join(testDir, customFile), { force: true });

const output = runCLI(`schema --output ${customFile}`);

expect(output).toContain('Schema exported');
expect(output).toContain('custom-schema.json');
expect(fs.existsSync(path.join(testDir, customFile))).toBe(true);
});
});
});
Loading
Loading