Files
Profa 2253d05618 feat: flatten project structure — archive v2, promote v3 to root
Remove the nested v3/ subdirectory by moving all active source code,
tests, configs, and assets directly to the project root. Archive v2/
(dead code, 847MB) to v2-archive.tar.gz.

Key changes:
- Move v3/src/ → src/, v3/tests/ → tests/, v3/packages/ → packages/
- Move v3/tsconfig.json, v3/vitest.config.ts to root
- Merge v3/package.json into root package.json (ESM, scripts, deps, exports)
- Merge v3/scripts/ into scripts/, v3/implementation/ into docs/implementation/
- Update all CI workflows to remove "cd v3 &&" and v3/ path prefixes
- Fix all init installer path resolution (3-level-up → 2-level-up)
- Fix hooks to use "node ./dist/cli/bundle.js" instead of bare "aqe"
- Update settings.json ADR/DDD directories to docs/implementation/
- Fix helper scripts (statusline, adr-compliance, ddd-tracker, guidance-hooks)
- Update 58 agent definitions: aqe/v3/ → aqe/ memory namespaces
- Update 15 guidance shards: v3/src/ → src/ path references
- Fix sync interfaces: remove v3/ and v2/ path references
- Fix test path calculations (process.cwd() + '..' no longer needed)
- Fix flaky perf test threshold (500ms → 1000ms for CI environments)
- Fix complexity-analyzer test to match ADR-051 keyword-based eligibility

Build passes, 17,901 tests pass, CLI and MCP bundles verified.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 11:35:41 +00:00

587 lines
18 KiB
TypeScript

/**
* Tunnel Manager Unit Tests
*
* Tests for secure IAP tunnel management:
* - IAP tunnel start/stop lifecycle
* - Port connectivity checking
* - Connection info retrieval
* - Direct connection manager for local development
* - Factory function for appropriate manager selection
* - Error handling and timeouts
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { EventEmitter } from 'events';
import type { ChildProcess } from 'child_process';
// Create mock for child_process
const mockSpawn = vi.fn();
const mockCreateConnection = vi.fn();
vi.mock('child_process', () => ({
spawn: (...args: unknown[]) => mockSpawn(...args),
}));
vi.mock('net', () => ({
createConnection: (...args: unknown[]) => mockCreateConnection(...args),
}));
import {
IAPTunnelManager,
DirectConnectionManager,
createTunnelManager,
createConnectionManager,
type TunnelManager,
} from '../../../../src/sync/cloud/tunnel-manager.js';
import type { CloudConfig, TunnelConnection } from '../../../../src/sync/interfaces.js';
// Helper to create mock child process
function createMockProcess(): ChildProcess & EventEmitter & {
_mockStdout: EventEmitter;
_mockStderr: EventEmitter;
} {
const process = new EventEmitter() as ChildProcess & EventEmitter & {
_mockStdout: EventEmitter;
_mockStderr: EventEmitter;
kill: ReturnType<typeof vi.fn>;
pid: number;
};
process._mockStdout = new EventEmitter();
process._mockStderr = new EventEmitter();
process.kill = vi.fn();
process.pid = 12345;
Object.defineProperty(process, 'stdout', {
get: () => process._mockStdout,
});
Object.defineProperty(process, 'stderr', {
get: () => process._mockStderr,
});
return process;
}
// Helper to create mock socket
function createMockSocket() {
const socket = new EventEmitter() as EventEmitter & {
destroy: ReturnType<typeof vi.fn>;
};
socket.destroy = vi.fn();
return socket;
}
/**
* Set up createConnection mock that fails the pre-spawn port check
* (simulating no pre-existing tunnel) then succeeds on post-spawn calls.
* The start() method calls checkPort() before spawning gcloud to detect
* reusable tunnels — tests must fail that first check so spawn happens.
*/
function setupPortCheckMock(opts?: { succeedAfterCall?: number; failAll?: boolean }) {
let callCount = 0;
const succeedAfter = opts?.succeedAfterCall ?? 1;
mockCreateConnection.mockImplementation(() => {
callCount++;
const socket = createMockSocket();
if (opts?.failAll || callCount <= succeedAfter) {
setTimeout(() => socket.emit('error', new Error('ECONNREFUSED')), 5);
} else {
setTimeout(() => socket.emit('connect'), 5);
}
return socket;
});
}
describe('IAPTunnelManager', () => {
const defaultCloudConfig: CloudConfig = {
project: 'test-project',
zone: 'us-central1-a',
instance: 'test-instance',
database: 'test_db',
user: 'test_user',
tunnelPort: 15432,
};
let tunnelManager: IAPTunnelManager;
let mockProcess: ReturnType<typeof createMockProcess>;
let consoleLogSpy: ReturnType<typeof vi.spyOn>;
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
vi.clearAllMocks();
vi.useFakeTimers();
mockProcess = createMockProcess();
mockSpawn.mockReturnValue(mockProcess);
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
tunnelManager = new IAPTunnelManager(defaultCloudConfig);
});
afterEach(() => {
vi.useRealTimers();
consoleLogSpy.mockRestore();
consoleErrorSpy.mockRestore();
});
describe('constructor', () => {
it('should create tunnel manager with config', () => {
expect(tunnelManager).toBeDefined();
});
});
describe('start', () => {
it('should spawn gcloud process with correct arguments', async () => {
// Pre-spawn check fails (no tunnel), post-spawn checks succeed
setupPortCheckMock();
const startPromise = tunnelManager.start();
// Advance past pre-spawn checkPort
await vi.advanceTimersByTimeAsync(100);
// Emit tunnel ready message
mockProcess._mockStderr.emit('data', Buffer.from('Listening on port 15432'));
// Advance timers for port check delay and retry
await vi.advanceTimersByTimeAsync(5000);
const connection = await startPromise;
expect(mockSpawn).toHaveBeenCalledWith(
'gcloud',
expect.arrayContaining([
'compute',
'start-iap-tunnel',
'test-instance',
'5432',
'--local-host-port=localhost:15432',
'--zone=us-central1-a',
'--project=test-project',
]),
expect.any(Object)
);
expect(connection.host).toBe('localhost');
expect(connection.port).toBe(15432);
});
it('should return existing connection if already running', async () => {
setupPortCheckMock();
const startPromise1 = tunnelManager.start();
await vi.advanceTimersByTimeAsync(100);
mockProcess._mockStderr.emit('data', Buffer.from('Listening on port'));
await vi.advanceTimersByTimeAsync(5000);
const connection1 = await startPromise1;
const connection2 = await tunnelManager.start();
expect(connection1).toEqual(connection2);
expect(mockSpawn).toHaveBeenCalledTimes(1);
});
it('should handle "tunnel is running" message', async () => {
setupPortCheckMock();
const startPromise = tunnelManager.start();
await vi.advanceTimersByTimeAsync(100);
mockProcess._mockStderr.emit('data', Buffer.from('tunnel is running'));
await vi.advanceTimersByTimeAsync(5000);
const connection = await startPromise;
expect(connection).toBeDefined();
});
it('should handle "Testing if tunnel connection works" message', async () => {
setupPortCheckMock();
const startPromise = tunnelManager.start();
await vi.advanceTimersByTimeAsync(100);
mockProcess._mockStderr.emit(
'data',
Buffer.from('Testing if tunnel connection works')
);
await vi.advanceTimersByTimeAsync(5000);
const connection = await startPromise;
expect(connection).toBeDefined();
});
it('should reject on process error before connection', async () => {
// Pre-spawn check must fail so spawn happens
setupPortCheckMock({ failAll: true });
const startPromise = tunnelManager.start();
await vi.advanceTimersByTimeAsync(100);
mockProcess.emit('error', new Error('Spawn failed'));
await expect(startPromise).rejects.toThrow('Failed to start tunnel');
});
it('should reject on process close before connection', async () => {
setupPortCheckMock({ failAll: true });
const startPromise = tunnelManager.start();
await vi.advanceTimersByTimeAsync(100);
mockProcess.emit('close', 1);
await expect(startPromise).rejects.toThrow('Tunnel process exited');
});
it('should timeout after 60 seconds', async () => {
// Catch any unhandled rejections that may occur during timer cleanup
const unhandledRejections: Error[] = [];
const rejectionHandler = (err: Error) => unhandledRejections.push(err);
process.on('unhandledRejection', rejectionHandler);
try {
setupPortCheckMock({ failAll: true });
const startPromise = tunnelManager.start();
// Advance past timeout
await vi.advanceTimersByTimeAsync(61000);
await expect(startPromise).rejects.toThrow('Tunnel connection timeout');
// Clean up any pending timers
await vi.runAllTimersAsync();
} finally {
process.off('unhandledRejection', rejectionHandler);
}
});
it('should retry port connectivity check', async () => {
// Call 1 = pre-spawn check (fail), calls 2-3 = post-spawn retries (fail),
// call 4+ = succeed
setupPortCheckMock({ succeedAfterCall: 3 });
const startPromise = tunnelManager.start();
await vi.advanceTimersByTimeAsync(100);
mockProcess._mockStderr.emit('data', Buffer.from('Listening on port'));
// Advance through port check retries
await vi.advanceTimersByTimeAsync(15000);
const connection = await startPromise;
expect(connection).toBeDefined();
});
});
describe('stop', () => {
it('should kill process with SIGTERM', async () => {
setupPortCheckMock();
const startPromise = tunnelManager.start();
await vi.advanceTimersByTimeAsync(100);
mockProcess._mockStderr.emit('data', Buffer.from('Listening on port'));
await vi.advanceTimersByTimeAsync(5000);
await startPromise;
const stopPromise = tunnelManager.stop();
await vi.advanceTimersByTimeAsync(2000);
await stopPromise;
expect(mockProcess.kill).toHaveBeenCalledWith('SIGTERM');
});
it('should follow up with SIGKILL after delay', async () => {
setupPortCheckMock();
const startPromise = tunnelManager.start();
await vi.advanceTimersByTimeAsync(100);
mockProcess._mockStderr.emit('data', Buffer.from('Listening on port'));
await vi.advanceTimersByTimeAsync(5000);
await startPromise;
const stopPromise = tunnelManager.stop();
await vi.advanceTimersByTimeAsync(2000);
await stopPromise;
expect(mockProcess.kill).toHaveBeenCalledWith('SIGKILL');
});
it('should handle stop when not running', async () => {
await expect(tunnelManager.stop()).resolves.not.toThrow();
});
});
describe('isActive', () => {
it('should return false initially', () => {
expect(tunnelManager.isActive()).toBe(false);
});
it('should return true after successful start', async () => {
setupPortCheckMock();
const startPromise = tunnelManager.start();
await vi.advanceTimersByTimeAsync(100);
mockProcess._mockStderr.emit('data', Buffer.from('Listening on port'));
await vi.advanceTimersByTimeAsync(5000);
await startPromise;
expect(tunnelManager.isActive()).toBe(true);
});
it('should return false after stop', async () => {
setupPortCheckMock();
const startPromise = tunnelManager.start();
await vi.advanceTimersByTimeAsync(100);
mockProcess._mockStderr.emit('data', Buffer.from('Listening on port'));
await vi.advanceTimersByTimeAsync(5000);
await startPromise;
const stopPromise = tunnelManager.stop();
await vi.advanceTimersByTimeAsync(2000);
await stopPromise;
expect(tunnelManager.isActive()).toBe(false);
});
});
describe('getConnection', () => {
it('should return null initially', () => {
expect(tunnelManager.getConnection()).toBeNull();
});
it('should return connection info after start', async () => {
setupPortCheckMock();
const startPromise = tunnelManager.start();
await vi.advanceTimersByTimeAsync(100);
mockProcess._mockStderr.emit('data', Buffer.from('Listening on port'));
await vi.advanceTimersByTimeAsync(5000);
await startPromise;
const connection = tunnelManager.getConnection();
expect(connection).toEqual({
host: 'localhost',
port: 15432,
pid: 12345,
startedAt: expect.any(Date),
});
});
it('should return null after stop', async () => {
setupPortCheckMock();
const startPromise = tunnelManager.start();
await vi.advanceTimersByTimeAsync(100);
mockProcess._mockStderr.emit('data', Buffer.from('Listening on port'));
await vi.advanceTimersByTimeAsync(5000);
await startPromise;
const stopPromise = tunnelManager.stop();
await vi.advanceTimersByTimeAsync(2000);
await stopPromise;
expect(tunnelManager.getConnection()).toBeNull();
});
});
describe('getConnectionString', () => {
it('should throw if tunnel not active', () => {
expect(() => tunnelManager.getConnectionString()).toThrow('Tunnel not active');
});
it('should return PostgreSQL connection string when active', async () => {
setupPortCheckMock();
const startPromise = tunnelManager.start();
await vi.advanceTimersByTimeAsync(100);
mockProcess._mockStderr.emit('data', Buffer.from('Listening on port'));
await vi.advanceTimersByTimeAsync(5000);
await startPromise;
const connectionString = tunnelManager.getConnectionString();
expect(connectionString).toMatch(/^postgresql:\/\//);
expect(connectionString).toContain('localhost:15432');
expect(connectionString).toContain('test_db');
});
});
describe('port connectivity checking', () => {
it('should check port with timeout', async () => {
const unhandledRejections: Error[] = [];
const rejectionHandler = (err: Error) => unhandledRejections.push(err);
process.on('unhandledRejection', rejectionHandler);
try {
// All port checks time out (return socket that never emits)
mockCreateConnection.mockImplementation(() => {
return createMockSocket();
});
const startPromise = tunnelManager.start();
// Advance past the pre-spawn checkPort timeout (2000ms)
await vi.advanceTimersByTimeAsync(2500);
mockProcess._mockStderr.emit('data', Buffer.from('Listening on port'));
// Advance through all post-spawn port check attempts + overall timeout
await vi.advanceTimersByTimeAsync(60000);
await expect(startPromise).rejects.toThrow();
await vi.runAllTimersAsync();
} finally {
process.off('unhandledRejection', rejectionHandler);
}
});
it('should handle socket errors during port check', async () => {
// Call 1 = pre-spawn (fail), calls 2-5 = post-spawn retries (fail),
// call 6+ = succeed
setupPortCheckMock({ succeedAfterCall: 5 });
const startPromise = tunnelManager.start();
await vi.advanceTimersByTimeAsync(100);
mockProcess._mockStderr.emit('data', Buffer.from('Listening on port'));
await vi.advanceTimersByTimeAsync(15000);
const connection = await startPromise;
expect(connection).toBeDefined();
});
});
});
describe('DirectConnectionManager', () => {
let manager: DirectConnectionManager;
const connectionString = 'postgresql://user:pass@localhost:5432/testdb';
beforeEach(() => {
manager = new DirectConnectionManager(connectionString);
});
describe('constructor', () => {
it('should create manager with connection string', () => {
expect(manager).toBeDefined();
});
});
describe('start', () => {
it('should parse connection string and return connection info', async () => {
const connection = await manager.start();
expect(connection.host).toBe('localhost');
expect(connection.port).toBe(5432);
expect(connection.startedAt).toBeInstanceOf(Date);
});
it('should default port to 5432 if not specified', async () => {
const managerNoPort = new DirectConnectionManager(
'postgresql://user:pass@localhost/testdb'
);
const connection = await managerNoPort.start();
expect(connection.port).toBe(5432);
});
});
describe('stop', () => {
it('should clear connection', async () => {
await manager.start();
await manager.stop();
expect(manager.getConnection()).toBeNull();
});
});
describe('isActive', () => {
it('should return false initially', () => {
expect(manager.isActive()).toBe(false);
});
it('should return true after start', async () => {
await manager.start();
expect(manager.isActive()).toBe(true);
});
it('should return false after stop', async () => {
await manager.start();
await manager.stop();
expect(manager.isActive()).toBe(false);
});
});
describe('getConnection', () => {
it('should return null initially', () => {
expect(manager.getConnection()).toBeNull();
});
it('should return connection after start', async () => {
await manager.start();
const connection = manager.getConnection();
expect(connection).not.toBeNull();
expect(connection?.host).toBe('localhost');
});
});
describe('getConnectionString', () => {
it('should return original connection string', () => {
expect(manager.getConnectionString()).toBe(connectionString);
});
});
});
describe('Factory Functions', () => {
describe('createTunnelManager', () => {
it('should create IAPTunnelManager', () => {
const config: CloudConfig = {
project: 'test-project',
zone: 'us-central1-a',
instance: 'test-instance',
database: 'test_db',
user: 'test_user',
tunnelPort: 15432,
};
const manager = createTunnelManager(config);
expect(manager).toBeInstanceOf(IAPTunnelManager);
});
});
describe('createConnectionManager', () => {
it('should create DirectConnectionManager when connectionString provided', () => {
const config: CloudConfig = {
project: 'test-project',
zone: 'us-central1-a',
instance: 'test-instance',
database: 'test_db',
user: 'test_user',
tunnelPort: 15432,
connectionString: 'postgresql://localhost:5432/testdb',
};
const manager = createConnectionManager(config);
expect(manager).toBeInstanceOf(DirectConnectionManager);
});
it('should create IAPTunnelManager when no connectionString', () => {
const config: CloudConfig = {
project: 'test-project',
zone: 'us-central1-a',
instance: 'test-instance',
database: 'test_db',
user: 'test_user',
tunnelPort: 15432,
};
const manager = createConnectionManager(config);
expect(manager).toBeInstanceOf(IAPTunnelManager);
});
});
});