Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,8 +225,6 @@ Future feature ideas:
- **Spotlight integration** - Check status or pause builds from Spotlight.
- **Artifact inspector** - Browse uploaded artifacts without leaving the app.
- **Disk space monitoring** - Warn or pause when disk is low, auto-clean old work dirs.
- **Runner handoff** - Transfer a running job to GitHub-hosted if you need to leave.
- **Reactive state management** - Unify disk state, React state, and state machine into a single reactive store to prevent synchronization bugs.
- **Linux and Windows host support** - Run self-hosted runners on non-Mac machines for projects that need them.
- **Higher parallelism cap** - Parallelize proxy registration to support 16+ concurrent runners (currently capped at 8 due to serial registration time).
- **Ephemeral VM isolation** - Run each job in a fresh lightweight VM for stronger isolation between jobs.
Expand Down
90 changes: 90 additions & 0 deletions src/cli/env.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { jest, describe, it, expect, beforeEach } from '@jest/globals';

Check warning on line 1 in src/cli/env.test.ts

View workflow job for this annotation

GitHub Actions / build

'beforeEach' is defined but never used

Check warning on line 1 in src/cli/env.test.ts

View workflow job for this annotation

GitHub Actions / build

'jest' is defined but never used
import { parseEnvArgs, EnvOptions } from './env';

Check warning on line 2 in src/cli/env.test.ts

View workflow job for this annotation

GitHub Actions / build

'EnvOptions' is defined but never used

describe('CLI env command', () => {
describe('parseEnvArgs', () => {
it('returns empty options for no args', () => {
const result = parseEnvArgs([]);
expect(result).toEqual({});
});

it('parses --compare option', () => {
const result = parseEnvArgs(['--compare', 'macos-14']);
expect(result.compare).toBe('macos-14');
});

it('parses -c short flag for compare', () => {
const result = parseEnvArgs(['-c', 'macos-15']);
expect(result.compare).toBe('macos-15');
});

it('parses --list option', () => {
const result = parseEnvArgs(['--list']);
expect(result.list).toBe(true);
});

it('parses -l short flag for list', () => {
const result = parseEnvArgs(['-l']);
expect(result.list).toBe(true);
});

it('handles both options together', () => {
const result = parseEnvArgs(['--list', '--compare', 'macos-13']);
expect(result.list).toBe(true);
expect(result.compare).toBe('macos-13');
});

it('ignores unknown flags', () => {
const result = parseEnvArgs(['--unknown', '--list']);
expect(result.list).toBe(true);
});
});

describe('environment detection', () => {
// These tests would require mocking os module calls

it('detects macOS version', () => {
// Would test detectLocalEnvironment
});

it('detects Xcode version', () => {
// Would test detectLocalEnvironment via xcodebuild -version
});

it('detects architecture', () => {
// Would test detectLocalEnvironment via process.arch
});
});

describe('environment comparison', () => {
// Tests for environment diff logic

it('identifies matching versions', () => {
// Would test compareEnvironments
});

it('identifies version mismatches', () => {
// Would test compareEnvironments
});

it('identifies missing tools', () => {
// Would test compareEnvironments
});
});
});

describe('GitHub runner environments', () => {
// Test that known runner labels are defined

it('defines macos-latest', () => {
// Would import and check GITHUB_RUNNER_ENVIRONMENTS
});

it('defines macos-14', () => {
// Would import and check GITHUB_RUNNER_ENVIRONMENTS
});

it('defines macos-15', () => {
// Would import and check GITHUB_RUNNER_ENVIRONMENTS
});
});
178 changes: 178 additions & 0 deletions src/cli/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { jest, describe, it, expect, beforeEach, afterEach } from '@jest/globals';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';

// Store original process.argv
const originalArgv = process.argv;

// Mock fs
jest.mock('fs');
const mockFs = jest.mocked(fs);

// Mock child_process
jest.mock('child_process', () => ({
spawn: jest.fn(),
}));

// Mock the shared paths module
const testSocketPath = '/tmp/test-localmost.sock';
jest.mock('../shared/paths', () => ({
getCliSocketPath: () => testSocketPath,
}));

// Import after mocks are set up
import { spawn } from 'child_process';

Check warning on line 25 in src/cli/index.test.ts

View workflow job for this annotation

GitHub Actions / build

'spawn' is defined but never used

describe('CLI index', () => {
beforeEach(() => {
jest.clearAllMocks();
process.argv = ['node', 'cli'];
});

afterEach(() => {
process.argv = originalArgv;
});

describe('formatDuration', () => {
// We need to test the formatDuration function
// Since it's not exported, we test it through printJobs indirectly
// or we can extract and export it for testing
it('formats seconds correctly', () => {
// This would require exporting formatDuration or testing through integration
});
});

describe('formatTimestamp', () => {
it('formats ISO timestamps to locale string', () => {
// This would require exporting formatTimestamp or testing through integration
});
});

describe('getStatusIcon', () => {
it('returns correct icon for each status', () => {
// This would require exporting getStatusIcon or testing through integration
});
});

describe('isAppRunning', () => {
it('returns true when socket file exists', () => {
mockFs.existsSync.mockReturnValue(true);
// Would need to export isAppRunning or test through main()
});

it('returns false when socket file does not exist', () => {
mockFs.existsSync.mockReturnValue(false);
// Would need to export isAppRunning or test through main()
});
});

describe('findAppPath', () => {
it('returns app path when found in /Applications', () => {
mockFs.existsSync.mockImplementation((p) => {
return p === '/Applications/localmost.app';
});
mockFs.realpathSync.mockReturnValue('/usr/local/bin/localmost');
// Would need to export findAppPath or test through startApp
});

it('returns app path when found in ~/Applications', () => {
const homeApp = path.join(os.homedir(), 'Applications', 'localmost.app');
mockFs.existsSync.mockImplementation((p) => {
return p === homeApp;
});
// Would need to export findAppPath or test through startApp
});

it('returns null when app not found', () => {
mockFs.existsSync.mockReturnValue(false);
// Would need to export findAppPath or test through startApp
});
});

describe('command parsing', () => {
it('recognizes help command', () => {
process.argv = ['node', 'cli', 'help'];
// Test that help text is printed
});

it('recognizes --help flag', () => {
process.argv = ['node', 'cli', '--help'];
// Test that help text is printed
});

it('recognizes version command', () => {
process.argv = ['node', 'cli', '--version'];
// Test that version is printed
});

it('recognizes unknown commands', () => {
process.argv = ['node', 'cli', 'unknown-command'];
// Test that error is printed
});
});
});

describe('CLI utility functions', () => {
describe('duration formatting', () => {
// Test the internal formatDuration logic
function formatDuration(seconds: number): string {
if (seconds < 60) return `${seconds}s`;
const minutes = Math.floor(seconds / 60);
const secs = seconds % 60;
if (minutes < 60) return `${minutes}m ${secs}s`;
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
return `${hours}h ${mins}m`;
}

it('formats seconds only', () => {
expect(formatDuration(45)).toBe('45s');
});

it('formats minutes and seconds', () => {
expect(formatDuration(125)).toBe('2m 5s');
});

it('formats hours and minutes', () => {
expect(formatDuration(3725)).toBe('1h 2m');
});
});

describe('status icons', () => {
function getStatusIcon(status: string): string {
switch (status) {
case 'listening': return '\u2713';
case 'busy': return '\u25CF';
case 'starting': return '\u25CB';
case 'offline': return '\u25CB';
case 'shutting_down': return '\u25CB';
case 'error': return '\u2717';
case 'completed': return '\u2713';
case 'failed': return '\u2717';
case 'cancelled': return '-';
default: return '?';
}
}

it('returns checkmark for listening', () => {
expect(getStatusIcon('listening')).toBe('\u2713');
});

it('returns filled circle for busy', () => {
expect(getStatusIcon('busy')).toBe('\u25CF');
});

it('returns empty circle for starting', () => {
expect(getStatusIcon('starting')).toBe('\u25CB');
});

it('returns x mark for error', () => {
expect(getStatusIcon('error')).toBe('\u2717');
});

it('returns question mark for unknown status', () => {
expect(getStatusIcon('unknown')).toBe('?');
});
});
});
84 changes: 84 additions & 0 deletions src/cli/policy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { jest, describe, it, expect, beforeEach } from '@jest/globals';

Check warning on line 1 in src/cli/policy.test.ts

View workflow job for this annotation

GitHub Actions / build

'beforeEach' is defined but never used

Check warning on line 1 in src/cli/policy.test.ts

View workflow job for this annotation

GitHub Actions / build

'jest' is defined but never used
import { parsePolicyArgs, PolicyOptions } from './policy';

Check warning on line 2 in src/cli/policy.test.ts

View workflow job for this annotation

GitHub Actions / build

'PolicyOptions' is defined but never used

describe('CLI policy command', () => {
describe('parsePolicyArgs', () => {
it('returns show as default subcommand', () => {
const result = parsePolicyArgs([]);
expect(result.subcommand).toBe('show');
expect(result.options).toEqual({});
});

it('parses show subcommand explicitly', () => {
const result = parsePolicyArgs(['show']);
expect(result.subcommand).toBe('show');
});

it('parses diff subcommand', () => {
const result = parsePolicyArgs(['diff']);
expect(result.subcommand).toBe('diff');
});

it('parses validate subcommand', () => {
const result = parsePolicyArgs(['validate']);
expect(result.subcommand).toBe('validate');
});

it('parses init subcommand', () => {
const result = parsePolicyArgs(['init']);
expect(result.subcommand).toBe('init');
});

it('parses --workflow option', () => {
const result = parsePolicyArgs(['show', '--workflow', 'build']);
expect(result.subcommand).toBe('show');
expect(result.options.workflow).toBe('build');
});

it('parses -w short flag for workflow', () => {
const result = parsePolicyArgs(['-w', 'deploy']);
expect(result.options.workflow).toBe('deploy');
});

it('parses --force option', () => {
const result = parsePolicyArgs(['init', '--force']);
expect(result.subcommand).toBe('init');
expect(result.options.force).toBe(true);
});

it('parses -f short flag for force', () => {
const result = parsePolicyArgs(['init', '-f']);
expect(result.options.force).toBe(true);
});

it('handles options before subcommand', () => {
const result = parsePolicyArgs(['-w', 'ci', 'show']);
expect(result.subcommand).toBe('show');
expect(result.options.workflow).toBe('ci');
});

it('handles multiple options', () => {
const result = parsePolicyArgs(['show', '--workflow', 'build', '--force']);
expect(result.subcommand).toBe('show');
expect(result.options.workflow).toBe('build');
expect(result.options.force).toBe(true);
});
});

describe('policy validation', () => {
// Tests for policy format validation would go here
// These would test the validation logic from localmostrc module

it('validates version field is required', () => {
// Would test parseLocalmostrc validation
});

it('validates network.allow is array of strings', () => {
// Would test parseLocalmostrc validation
});

it('validates filesystem paths are valid', () => {
// Would test parseLocalmostrc validation
});
});
});
Loading