Skip to content
Open
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
12 changes: 11 additions & 1 deletion src/commands/autopilot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync } fr
import { join } from 'path';
import { execSync } from 'child_process';
import type { BrainEngine } from '../core/engine.ts';
import { loadConfig, toEngineConfig } from '../core/config.ts';

function parseArg(args: string[], flag: string): string | undefined {
const idx = args.indexOf(flag);
Expand Down Expand Up @@ -98,7 +99,16 @@ export async function runAutopilot(engine: BrainEngine, args: string[]) {
} catch {
try {
await engine.disconnect();
await (engine as any).connect?.();
// Re-load config from disk so reconnect gets the database URL and engine
// type. Previous code called connect() with no arguments, which crashed
// the Postgres engine with "undefined is not an object (evaluating
// 'config.poolSize')" (#167 / #164).
const freshConfig = loadConfig();
if (freshConfig) {
await engine.connect(toEngineConfig(freshConfig));
} else {
logError('reconnect', new Error('No brain configuration found — cannot reconnect'));
}
} catch (e) { logError('reconnect', e); }
}

Expand Down
6 changes: 6 additions & 0 deletions src/core/pglite-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ export class PGLiteEngine implements BrainEngine {

// Lifecycle
async connect(config: EngineConfig): Promise<void> {
if (!config) {
throw new Error(
'PGLiteEngine.connect() called without config. ' +
'Pass an EngineConfig with at least database_path.',
);
}
const dataDir = config.database_path || undefined; // undefined = in-memory

// Acquire file lock to prevent concurrent PGLite access (crashes with Aborted())
Expand Down
7 changes: 7 additions & 0 deletions src/core/postgres-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ export class PostgresEngine implements BrainEngine {

// Lifecycle
async connect(config: EngineConfig & { poolSize?: number }): Promise<void> {
if (!config) {
throw new GBrainError(
'PostgresEngine.connect() called without config',
'The config argument is required but was undefined',
'Pass an EngineConfig with at least database_url',
);
}
if (config.poolSize) {
// Instance-level connection for worker isolation
const url = config.database_url;
Expand Down
110 changes: 110 additions & 0 deletions test/autopilot-reconnect.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/**
* Regression tests for autopilot reconnect (#167 / #164).
*
* The autopilot daemon calls engine.connect() on reconnect. Previously it
* passed no arguments, crashing PostgresEngine with "undefined is not an
* object (evaluating 'config.poolSize')". After the fix, autopilot re-loads
* config from disk and passes it to connect().
*
* These tests verify:
* 1. PostgresEngine.connect() throws a descriptive error when called without config
* 2. PGLiteEngine.connect() throws a descriptive error when called without config
* 3. The autopilot source code passes config to connect() on reconnect
* 4. autopilot.ts imports loadConfig and toEngineConfig
* 5. The reconnect block handles missing config gracefully
*/

import { describe, test, expect } from 'bun:test';
import { readFileSync } from 'fs';
import { join } from 'path';

const ROOT = join(import.meta.dir, '..');

describe('autopilot reconnect (#167 / #164)', () => {

// --- Source-code audit tests ---

test('autopilot.ts imports loadConfig and toEngineConfig', () => {
const src = readFileSync(join(ROOT, 'src/commands/autopilot.ts'), 'utf-8');
expect(src).toContain("import { loadConfig, toEngineConfig } from '../core/config.ts'");
});

test('autopilot reconnect block passes config to engine.connect()', () => {
const src = readFileSync(join(ROOT, 'src/commands/autopilot.ts'), 'utf-8');
// The reconnect block should call loadConfig() and pass the result to connect()
expect(src).toContain('loadConfig()');
expect(src).toContain('toEngineConfig(freshConfig)');
expect(src).toContain('engine.connect(toEngineConfig(freshConfig))');
});

test('autopilot reconnect block does NOT call connect() with no arguments', () => {
const src = readFileSync(join(ROOT, 'src/commands/autopilot.ts'), 'utf-8');
// The old buggy pattern: (engine as any).connect?.()
expect(src).not.toContain('connect?.()');
expect(src).not.toContain('.connect()');
});

test('autopilot reconnect handles missing config (no brain configured)', () => {
const src = readFileSync(join(ROOT, 'src/commands/autopilot.ts'), 'utf-8');
// Should check if freshConfig is null/undefined before calling connect
expect(src).toContain('if (freshConfig)');
expect(src).toContain('No brain configuration found');
});

// --- Engine guard tests ---

test('PostgresEngine.connect() guards against undefined config', () => {
const src = readFileSync(join(ROOT, 'src/core/postgres-engine.ts'), 'utf-8');
// Should check !config before accessing config.poolSize
expect(src).toContain('if (!config)');
expect(src).toContain('connect() called without config');
});

test('PGLiteEngine.connect() guards against undefined config', () => {
const src = readFileSync(join(ROOT, 'src/core/pglite-engine.ts'), 'utf-8');
expect(src).toContain('if (!config)');
expect(src).toContain('connect() called without config');
});

// --- Behavioral tests (no real DB needed) ---

test('PostgresEngine.connect(undefined) throws descriptive GBrainError', async () => {
const { PostgresEngine } = await import('../src/core/postgres-engine.ts');
const engine = new PostgresEngine();
await expect(engine.connect(undefined as any)).rejects.toThrow(/called without config/);
});

test('PGLiteEngine.connect(undefined) throws descriptive error', async () => {
const { PGLiteEngine } = await import('../src/core/pglite-engine.ts');
const engine = new PGLiteEngine();
await expect(engine.connect(undefined as any)).rejects.toThrow(/called without config/);
});

// --- loadConfig + toEngineConfig round-trip ---

test('toEngineConfig produces a valid EngineConfig from GBrainConfig', async () => {
const { toEngineConfig } = await import('../src/core/config.ts');
const result = toEngineConfig({
engine: 'postgres',
database_url: 'postgresql://localhost/test',
});
expect(result).toEqual({
engine: 'postgres',
database_url: 'postgresql://localhost/test',
database_path: undefined,
});
});

test('toEngineConfig with pglite config', async () => {
const { toEngineConfig } = await import('../src/core/config.ts');
const result = toEngineConfig({
engine: 'pglite',
database_path: '/tmp/test.pglite',
});
expect(result).toEqual({
engine: 'pglite',
database_url: undefined,
database_path: '/tmp/test.pglite',
});
});
});