Files
gbrain/test/connection-manager.serial.test.ts
T
8901dc0f45 fix(backlog): x-to-brain health check, propose_takes deadlines, capture title truncation, extract_atoms backlog + pooler direct-URL (part4-6) (#3165)
* fix(recipes/x-to-brain): use /users/by/username for app-only bearer health check

Takeover of #2343. /users/me requires user-context OAuth and always fails
under the app-only bearer the recipe collects. Health check + setup curls
now use /users/by/username/$X_HANDLE, with X_HANDLE declared in secrets
so the installer prompts for it. Recipe version 0.8.1 -> 0.8.2.

Co-authored-by: ethanbeard <ethanbeard@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(cycle): bound propose_takes with per-call timeout + phase deadline

Takeover of #2262. The extractor's gateway.chat call had no abortSignal, so
one stalled provider socket could pin the phase for the 300s gateway default
per page; the nightly wrapper then SIGTERMed the whole phase mid-run. Each
extractor call is now bounded at 90s (per-page failure already logs a warning
and continues), and the page loop carries a 30-min wall-clock deadline that
breaks cleanly into a partial result with deadline_hit:true + warn status.

Unlike the original PR, the default pageLimit stays at 100 — shrinking it to
30 was an unrelated product-knob change that would permanently cut nightly
take coverage.

Co-authored-by: tschew72 <tschew72@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(capture): make fallback title truncation explicit and astral-safe

Takeover of #2310. deriveTitle's silent .slice(0, 80) could split an astral
surrogate pair mid-character and gave no signal the title was cut. Truncation
is now codepoint-aware and appends an ellipsis (still capped at 80 codepoints).

Unlike the original PR, this stays a three-line change: no whitespace
normalization of every derived title, no word-boundary heuristics.

Co-authored-by: xd-Neji <xd-Neji@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(doctor): clear extract_atoms raw source-holder backlog + normalize pooler direct-URL overrides

Takeover of #2242, split to the two concerns that survive review:

- extract_atoms: exclude source pages whose frontmatter declares a raw
  payload pointer from discovery AND the doctor backlog count (shared SQL
  fragment so they can't drift). Extraction on these yields zero atoms, so
  no atom row is ever written and they re-enter the backlog every cycle —
  a permanent no-progress doctor blocker.
- connection-manager: a direct-URL override (opts/env) that still points at
  the Supavisor TRANSACTION pooler (port 6543, usually a copy-paste of the
  primary URL) is normalized to the real direct host via deriveDirectUrl.
  Session-mode pooler overrides (port 5432) pass through — they are a
  legitimate direct-ish target, which the original PR would have nulled out.

Dropped from the original PR: orphan-reporting atom exclusions (master
already excludes atoms/ and raw/ first segments plus /raw/ segments in
src/commands/orphans.ts) and the drain dry-run status tweak.

Co-authored-by: benjonp <benjonp@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(cycle): record propose_takes deadline break as a halt in the extract rollup

A deadline-hit run breaks the page loop mid-list — same posture as budget
exhaustion — but the rollup still counted it as a completed round with no
halt, hiding chronic never-finishing nightly runs from extract-status/
doctor. Treat deadline_hit like budget_exhausted in the rollup deltas;
deadline test now pins halt=1 / completed=0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Garry Tan <garrytan@gmail.com>
Co-authored-by: ethanbeard <ethanbeard@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: tschew72 <tschew72@users.noreply.github.com>
Co-authored-by: xd-Neji <xd-Neji@users.noreply.github.com>
Co-authored-by: benjonp <benjonp@users.noreply.github.com>
2026-07-23 13:23:36 -07:00

367 lines
14 KiB
TypeScript

import { describe, expect, test, beforeEach, afterEach } from 'bun:test';
import {
isSupabasePoolerUrl,
deriveDirectUrl,
normalizeDirectUrl,
readKillSwitchEnv,
isNetworkUnreachableError,
resolveDirectPoolSize,
ConnectionManager,
DEFAULT_DIRECT_POOL_SIZE,
} from '../src/core/connection-manager.ts';
describe('isSupabasePoolerUrl', () => {
test('detects port 6543', () => {
expect(isSupabasePoolerUrl('postgresql://u:p@host:6543/db')).toBe(true);
});
test('detects pooler.supabase.com hostname', () => {
expect(
isSupabasePoolerUrl('postgresql://u:p@aws-0-us-east-1.pooler.supabase.com:5432/db')
).toBe(true);
});
test('rejects direct supabase host', () => {
expect(
isSupabasePoolerUrl('postgresql://u:p@db.abc.supabase.co:5432/postgres')
).toBe(false);
});
test('rejects self-hosted on standard port', () => {
expect(isSupabasePoolerUrl('postgresql://u:p@localhost:5432/gbrain_test')).toBe(false);
});
test('handles malformed URL gracefully', () => {
expect(isSupabasePoolerUrl('not a url')).toBe(false);
});
});
describe('deriveDirectUrl', () => {
test('swaps pooler hostname + port for known shape', () => {
const direct = deriveDirectUrl(
'postgresql://postgres.abcxyz:secret@aws-0-us-east-1.pooler.supabase.com:6543/postgres'
);
expect(direct).toBeTruthy();
expect(direct).toContain('db.abcxyz.supabase.co:5432');
expect(direct).toContain(':secret@'); // creds preserved
});
test('strips .<project-ref> suffix from username when going pooler→direct', () => {
// Supabase direct connections require bare `postgres`; the `postgres.<ref>`
// form is pooler-only (Supavisor uses the suffix for tenant routing).
// Without the strip, direct auth fails with "password authentication
// failed for user postgres.<ref>" even with the correct password.
const direct = deriveDirectUrl(
'postgresql://postgres.abcxyz:secret@aws-0-us-east-1.pooler.supabase.com:6543/postgres'
);
expect(direct).toContain('postgres:secret@'); // bare username
expect(direct).not.toContain('postgres.abcxyz:secret@'); // no pooler suffix
});
test('falls back to port-only swap when project-ref unparseable', () => {
const direct = deriveDirectUrl(
'postgresql://customuser:secret@some.pooler.supabase.com:6543/db'
);
expect(direct).toBeTruthy();
expect(direct).toContain(':5432');
expect(direct).toContain('some.pooler.supabase.com'); // host preserved
expect(direct).toContain('customuser:secret@'); // non-pooler username preserved
});
test('returns null for non-pooler URL', () => {
expect(deriveDirectUrl('postgresql://u:p@localhost:5432/db')).toBeNull();
});
test('preserves query string', () => {
const direct = deriveDirectUrl(
'postgresql://postgres.ref:p@aws.pooler.supabase.com:6543/db?prepare=false'
);
expect(direct).toContain('?prepare=false');
});
});
describe('normalizeDirectUrl', () => {
test('normalizes a transaction-pooler (6543) override to the real direct host', () => {
const direct = normalizeDirectUrl(
'postgresql://postgres.abcxyz:p@aws-0-us-west-2.pooler.supabase.com:6543/postgres',
'postgresql://postgres.abcxyz:p@aws-0-us-west-2.pooler.supabase.com:6543/postgres',
);
expect(direct).toBe('postgresql://postgres:p@db.abcxyz.supabase.co:5432/postgres');
});
test('keeps a non-pooler direct override', () => {
const direct = normalizeDirectUrl(
'postgresql://postgres.abc:p@aws.pooler.supabase.com:6543/db',
'postgresql://u:p@custom-direct.example.com:5432/db',
);
expect(direct).toBe('postgresql://u:p@custom-direct.example.com:5432/db');
});
test('keeps a session-mode pooler override (pooler host, port 5432)', () => {
const sessionUrl = 'postgresql://postgres.abc:p@aws.pooler.supabase.com:5432/db';
const direct = normalizeDirectUrl(
'postgresql://postgres.abc:p@aws.pooler.supabase.com:6543/db',
sessionUrl,
);
expect(direct).toBe(sessionUrl);
});
test('no override: derives from the primary as before', () => {
const direct = normalizeDirectUrl(
'postgresql://postgres.abc:p@aws.pooler.supabase.com:6543/db',
);
expect(direct).toContain('db.abc.supabase.co:5432');
});
test('no override, non-Supabase primary: null', () => {
expect(normalizeDirectUrl('postgresql://u:p@localhost:5432/db')).toBeNull();
});
});
describe('readKillSwitchEnv', () => {
let original: string | undefined;
beforeEach(() => { original = process.env.GBRAIN_DISABLE_DIRECT_POOL; });
afterEach(() => {
if (original === undefined) delete process.env.GBRAIN_DISABLE_DIRECT_POOL;
else process.env.GBRAIN_DISABLE_DIRECT_POOL = original;
});
test('false when unset', () => {
delete process.env.GBRAIN_DISABLE_DIRECT_POOL;
expect(readKillSwitchEnv()).toBe(false);
});
test('true when "1"', () => {
process.env.GBRAIN_DISABLE_DIRECT_POOL = '1';
expect(readKillSwitchEnv()).toBe(true);
});
test('true when "true"', () => {
process.env.GBRAIN_DISABLE_DIRECT_POOL = 'true';
expect(readKillSwitchEnv()).toBe(true);
});
test('false for any other value', () => {
process.env.GBRAIN_DISABLE_DIRECT_POOL = '0';
expect(readKillSwitchEnv()).toBe(false);
process.env.GBRAIN_DISABLE_DIRECT_POOL = 'false';
expect(readKillSwitchEnv()).toBe(false);
});
});
describe('resolveDirectPoolSize', () => {
let original: string | undefined;
beforeEach(() => { original = process.env.GBRAIN_DIRECT_POOL_SIZE; });
afterEach(() => {
if (original === undefined) delete process.env.GBRAIN_DIRECT_POOL_SIZE;
else process.env.GBRAIN_DIRECT_POOL_SIZE = original;
});
test('default to 3', () => {
delete process.env.GBRAIN_DIRECT_POOL_SIZE;
expect(resolveDirectPoolSize()).toBe(DEFAULT_DIRECT_POOL_SIZE);
expect(DEFAULT_DIRECT_POOL_SIZE).toBe(3);
});
test('explicit overrides env', () => {
process.env.GBRAIN_DIRECT_POOL_SIZE = '5';
expect(resolveDirectPoolSize(7)).toBe(7);
});
test('env overrides default', () => {
process.env.GBRAIN_DIRECT_POOL_SIZE = '5';
expect(resolveDirectPoolSize()).toBe(5);
});
test('rejects invalid env values', () => {
process.env.GBRAIN_DIRECT_POOL_SIZE = 'abc';
expect(resolveDirectPoolSize()).toBe(DEFAULT_DIRECT_POOL_SIZE);
process.env.GBRAIN_DIRECT_POOL_SIZE = '0';
expect(resolveDirectPoolSize()).toBe(DEFAULT_DIRECT_POOL_SIZE);
process.env.GBRAIN_DIRECT_POOL_SIZE = '999';
expect(resolveDirectPoolSize()).toBe(DEFAULT_DIRECT_POOL_SIZE);
});
});
describe('ConnectionManager — describeMode + dual-pool routing', () => {
let originalKillSwitch: string | undefined;
let originalDirectUrl: string | undefined;
beforeEach(() => {
originalKillSwitch = process.env.GBRAIN_DISABLE_DIRECT_POOL;
originalDirectUrl = process.env.GBRAIN_DIRECT_DATABASE_URL;
delete process.env.GBRAIN_DISABLE_DIRECT_POOL;
delete process.env.GBRAIN_DIRECT_DATABASE_URL;
});
afterEach(() => {
if (originalKillSwitch === undefined) delete process.env.GBRAIN_DISABLE_DIRECT_POOL;
else process.env.GBRAIN_DISABLE_DIRECT_POOL = originalKillSwitch;
if (originalDirectUrl === undefined) delete process.env.GBRAIN_DIRECT_DATABASE_URL;
else process.env.GBRAIN_DIRECT_DATABASE_URL = originalDirectUrl;
});
test('non-Supabase URL → single mode', () => {
const cm = new ConnectionManager({ url: 'postgresql://u:p@localhost:5432/db' });
expect(cm.isSupabase()).toBe(false);
expect(cm.isDualPoolActive()).toBe(false);
expect(cm.describeMode().mode).toBe('single (non-supabase)');
});
test('Supabase pooler URL → dual mode (without kill-switch)', () => {
const cm = new ConnectionManager({
url: 'postgresql://postgres.abc:p@aws.pooler.supabase.com:6543/db',
});
expect(cm.isSupabase()).toBe(true);
expect(cm.isDualPoolActive()).toBe(true);
expect(cm.describeMode().mode).toBe('split');
expect(cm.describeMode().direct_host).toContain('db.abc.supabase.co:5432');
});
test('kill-switch active → single mode (kill-switch)', () => {
process.env.GBRAIN_DISABLE_DIRECT_POOL = '1';
const cm = new ConnectionManager({
url: 'postgresql://postgres.abc:p@aws.pooler.supabase.com:6543/db',
});
expect(cm.isSupabase()).toBe(true);
expect(cm.isKillSwitchActive()).toBe(true);
expect(cm.isDualPoolActive()).toBe(false);
expect(cm.describeMode().mode).toBe('single (kill-switch)');
});
test('explicit directUrl override wins', () => {
const cm = new ConnectionManager({
url: 'postgresql://postgres.abc:p@aws.pooler.supabase.com:6543/db',
directUrl: 'postgresql://u:p@custom-direct.example.com:5432/db',
});
expect(cm.resolveDirectUrl()).toContain('custom-direct.example.com');
});
test('explicit transaction-pooler directUrl override is normalized to direct host', () => {
const cm = new ConnectionManager({
url: 'postgresql://postgres.abc:p@aws.pooler.supabase.com:6543/db',
directUrl: 'postgresql://postgres.abc:p@aws.pooler.supabase.com:6543/db',
});
expect(cm.resolveDirectUrl()).toContain('db.abc.supabase.co:5432');
expect(cm.resolveDirectUrl()).not.toContain(':6543');
});
test('env transaction-pooler directUrl override is normalized to direct host', () => {
process.env.GBRAIN_DIRECT_DATABASE_URL =
'postgresql://postgres.abc:p@aws.pooler.supabase.com:6543/db';
const cm = new ConnectionManager({
url: 'postgresql://postgres.abc:p@aws.pooler.supabase.com:6543/db',
});
expect(cm.resolveDirectUrl()).toContain('db.abc.supabase.co:5432');
expect(cm.resolveDirectUrl()).not.toContain(':6543');
});
test('host string contains creds neither in describeMode nor resolveDirectUrl logging', () => {
const cm = new ConnectionManager({
url: 'postgresql://postgres.abc:secret@aws.pooler.supabase.com:6543/db',
});
const desc = cm.describeMode();
expect(desc.direct_host ?? '').not.toContain('secret');
});
});
describe('ConnectionManager — parent inheritance (A2)', () => {
test('child inherits kill-switch from parent', () => {
const original = process.env.GBRAIN_DISABLE_DIRECT_POOL;
try {
process.env.GBRAIN_DISABLE_DIRECT_POOL = '1';
const parent = new ConnectionManager({
url: 'postgresql://postgres.abc:p@aws.pooler.supabase.com:6543/db',
});
// Child constructed AFTER env reset — parent's snapshot is what matters.
delete process.env.GBRAIN_DISABLE_DIRECT_POOL;
const child = new ConnectionManager({
url: 'postgresql://postgres.abc:p@aws.pooler.supabase.com:6543/db',
parent,
});
expect(child.isKillSwitchActive()).toBe(true);
expect(child.isDualPoolActive()).toBe(false);
} finally {
if (original === undefined) delete process.env.GBRAIN_DISABLE_DIRECT_POOL;
else process.env.GBRAIN_DISABLE_DIRECT_POOL = original;
}
});
test('child without parent reads env at construction', () => {
const original = process.env.GBRAIN_DISABLE_DIRECT_POOL;
try {
delete process.env.GBRAIN_DISABLE_DIRECT_POOL;
const cm = new ConnectionManager({
url: 'postgresql://postgres.abc:p@aws.pooler.supabase.com:6543/db',
});
expect(cm.isKillSwitchActive()).toBe(false);
// Mutating env after construction does NOT change the manager's state.
process.env.GBRAIN_DISABLE_DIRECT_POOL = '1';
expect(cm.isKillSwitchActive()).toBe(false); // snapshot semantics
} finally {
if (original === undefined) delete process.env.GBRAIN_DISABLE_DIRECT_POOL;
else process.env.GBRAIN_DISABLE_DIRECT_POOL = original;
}
});
});
describe('isNetworkUnreachableError (#1641)', () => {
test('classifies network codes as unreachable', () => {
for (const code of ['ENOTFOUND', 'ECONNREFUSED', 'ENETUNREACH', 'EHOSTUNREACH', 'ETIMEDOUT', 'CONNECT_TIMEOUT']) {
const err = Object.assign(new Error('connect failed'), { code });
expect(isNetworkUnreachableError(err)).toBe(true);
}
});
test('classifies by message when code absent', () => {
expect(isNetworkUnreachableError(new Error('getaddrinfo ENOTFOUND db.abc.supabase.co'))).toBe(true);
});
test('auth/SQL errors are NOT unreachable', () => {
expect(isNetworkUnreachableError(new Error('password authentication failed for user "postgres"'))).toBe(false);
expect(isNetworkUnreachableError(new Error('syntax error at or near "SELEC"'))).toBe(false);
expect(isNetworkUnreachableError(null)).toBe(false);
});
});
describe('ConnectionManager — direct-pool fallback on unreachable host (#1641)', () => {
let originalKillSwitch: string | undefined;
let originalError: typeof console.error;
let errLines: string[];
beforeEach(() => {
originalKillSwitch = process.env.GBRAIN_DISABLE_DIRECT_POOL;
delete process.env.GBRAIN_DISABLE_DIRECT_POOL;
originalError = console.error;
errLines = [];
console.error = (...args: unknown[]) => { errLines.push(args.join(' ')); };
});
afterEach(() => {
console.error = originalError;
if (originalKillSwitch === undefined) delete process.env.GBRAIN_DISABLE_DIRECT_POOL;
else process.env.GBRAIN_DISABLE_DIRECT_POOL = originalKillSwitch;
});
test('ddl() falls back to the read pool when the direct host is unreachable', async () => {
const cm = new ConnectionManager({
url: 'postgresql://postgres.abc:p@aws.pooler.supabase.com:6543/db',
// 127.0.0.1:9 (discard) → instant ECONNREFUSED, the IPv4-only-network shape.
directUrl: 'postgresql://postgres:p@127.0.0.1:9/db',
});
const fakeReadPool = {} as ReturnType<typeof ConnectionManager.prototype.read>;
cm.setReadPool(fakeReadPool);
expect(cm.isDualPoolActive()).toBe(true);
const pool = await cm.ddl(); // without the fix this throws ECONNREFUSED
expect(pool).toBe(fakeReadPool);
// Self-activating kill-switch: subsequent calls skip the direct pool.
expect(cm.isKillSwitchActive()).toBe(true);
expect(cm.isDualPoolActive()).toBe(false);
expect(cm.describeMode().mode).toBe('single (kill-switch)');
// One stderr line mentioning the power-user override.
const warning = errLines.filter(l => l.includes('GBRAIN_DIRECT_DATABASE_URL'));
expect(warning.length).toBe(1);
const again = await cm.ddl();
expect(again).toBe(fakeReadPool);
expect(errLines.filter(l => l.includes('GBRAIN_DIRECT_DATABASE_URL')).length).toBe(1);
}, 20000);
});