test: skip embedded postgres suites when initdb is unavailable
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
555f026c24
commit
c916626cef
10 changed files with 547 additions and 458 deletions
|
|
@ -6,33 +6,15 @@ import path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { promisify } from "node:util";
|
import { promisify } from "node:util";
|
||||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
getEmbeddedPostgresTestSupport,
|
||||||
|
startEmbeddedPostgresTestDatabase,
|
||||||
|
} from "./helpers/embedded-postgres.js";
|
||||||
import { createStoredZipArchive } from "./helpers/zip.js";
|
import { createStoredZipArchive } from "./helpers/zip.js";
|
||||||
|
|
||||||
type EmbeddedPostgresInstance = {
|
|
||||||
initialise(): Promise<void>;
|
|
||||||
start(): Promise<void>;
|
|
||||||
stop(): Promise<void>;
|
|
||||||
};
|
|
||||||
|
|
||||||
type EmbeddedPostgresCtor = new (opts: {
|
|
||||||
databaseDir: string;
|
|
||||||
user: string;
|
|
||||||
password: string;
|
|
||||||
port: number;
|
|
||||||
persistent: boolean;
|
|
||||||
initdbFlags?: string[];
|
|
||||||
onLog?: (message: unknown) => void;
|
|
||||||
onError?: (message: unknown) => void;
|
|
||||||
}) => EmbeddedPostgresInstance;
|
|
||||||
|
|
||||||
const execFileAsync = promisify(execFile);
|
const execFileAsync = promisify(execFile);
|
||||||
type ServerProcess = ReturnType<typeof spawn>;
|
type ServerProcess = ReturnType<typeof spawn>;
|
||||||
|
|
||||||
async function getEmbeddedPostgresCtor(): Promise<EmbeddedPostgresCtor> {
|
|
||||||
const mod = await import("embedded-postgres");
|
|
||||||
return mod.default as EmbeddedPostgresCtor;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getAvailablePort(): Promise<number> {
|
async function getAvailablePort(): Promise<number> {
|
||||||
return await new Promise((resolve, reject) => {
|
return await new Promise((resolve, reject) => {
|
||||||
const server = net.createServer();
|
const server = net.createServer();
|
||||||
|
|
@ -53,30 +35,13 @@ async function getAvailablePort(): Promise<number> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startTempDatabase() {
|
const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport();
|
||||||
const dataDir = mkdtempSync(path.join(os.tmpdir(), "paperclip-company-cli-db-"));
|
const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip;
|
||||||
const port = await getAvailablePort();
|
|
||||||
const EmbeddedPostgres = await getEmbeddedPostgresCtor();
|
|
||||||
const instance = new EmbeddedPostgres({
|
|
||||||
databaseDir: dataDir,
|
|
||||||
user: "paperclip",
|
|
||||||
password: "paperclip",
|
|
||||||
port,
|
|
||||||
persistent: true,
|
|
||||||
initdbFlags: ["--encoding=UTF8", "--locale=C", "--lc-messages=C"],
|
|
||||||
onLog: () => {},
|
|
||||||
onError: () => {},
|
|
||||||
});
|
|
||||||
await instance.initialise();
|
|
||||||
await instance.start();
|
|
||||||
|
|
||||||
const { applyPendingMigrations, ensurePostgresDatabase } = await import("@paperclipai/db");
|
if (!embeddedPostgresSupport.supported) {
|
||||||
const adminConnectionString = `postgres://paperclip:paperclip@127.0.0.1:${port}/postgres`;
|
console.warn(
|
||||||
await ensurePostgresDatabase(adminConnectionString, "paperclip");
|
`Skipping embedded Postgres company import/export e2e tests on this host: ${embeddedPostgresSupport.reason ?? "unsupported environment"}`,
|
||||||
const connectionString = `postgres://paperclip:paperclip@127.0.0.1:${port}/paperclip`;
|
);
|
||||||
await applyPendingMigrations(connectionString);
|
|
||||||
|
|
||||||
return { connectionString, dataDir, instance };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function writeTestConfig(configPath: string, tempRoot: string, port: number, connectionString: string) {
|
function writeTestConfig(configPath: string, tempRoot: string, port: number, connectionString: string) {
|
||||||
|
|
@ -265,26 +230,23 @@ async function waitForServer(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("paperclipai company import/export e2e", () => {
|
describeEmbeddedPostgres("paperclipai company import/export e2e", () => {
|
||||||
let tempRoot = "";
|
let tempRoot = "";
|
||||||
let configPath = "";
|
let configPath = "";
|
||||||
let exportDir = "";
|
let exportDir = "";
|
||||||
let apiBase = "";
|
let apiBase = "";
|
||||||
let serverProcess: ServerProcess | null = null;
|
let serverProcess: ServerProcess | null = null;
|
||||||
let dbDataDir = "";
|
let tempDb: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | null = null;
|
||||||
let dbInstance: EmbeddedPostgresInstance | null = null;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
tempRoot = mkdtempSync(path.join(os.tmpdir(), "paperclip-company-cli-e2e-"));
|
tempRoot = mkdtempSync(path.join(os.tmpdir(), "paperclip-company-cli-e2e-"));
|
||||||
configPath = path.join(tempRoot, "config", "config.json");
|
configPath = path.join(tempRoot, "config", "config.json");
|
||||||
exportDir = path.join(tempRoot, "exported-company");
|
exportDir = path.join(tempRoot, "exported-company");
|
||||||
|
|
||||||
const db = await startTempDatabase();
|
tempDb = await startEmbeddedPostgresTestDatabase("paperclip-company-cli-db-");
|
||||||
dbDataDir = db.dataDir;
|
|
||||||
dbInstance = db.instance;
|
|
||||||
|
|
||||||
const port = await getAvailablePort();
|
const port = await getAvailablePort();
|
||||||
writeTestConfig(configPath, tempRoot, port, db.connectionString);
|
writeTestConfig(configPath, tempRoot, port, tempDb.connectionString);
|
||||||
apiBase = `http://127.0.0.1:${port}`;
|
apiBase = `http://127.0.0.1:${port}`;
|
||||||
|
|
||||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
|
||||||
|
|
@ -294,7 +256,7 @@ describe("paperclipai company import/export e2e", () => {
|
||||||
["paperclipai", "run", "--config", configPath],
|
["paperclipai", "run", "--config", configPath],
|
||||||
{
|
{
|
||||||
cwd: repoRoot,
|
cwd: repoRoot,
|
||||||
env: createServerEnv(configPath, port, db.connectionString),
|
env: createServerEnv(configPath, port, tempDb.connectionString),
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
@ -311,10 +273,7 @@ describe("paperclipai company import/export e2e", () => {
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await stopServerProcess(serverProcess);
|
await stopServerProcess(serverProcess);
|
||||||
await dbInstance?.stop();
|
await tempDb?.cleanup();
|
||||||
if (dbDataDir) {
|
|
||||||
rmSync(dbDataDir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
if (tempRoot) {
|
if (tempRoot) {
|
||||||
rmSync(tempRoot, { recursive: true, force: true });
|
rmSync(tempRoot, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
148
cli/src/__tests__/helpers/embedded-postgres.ts
Normal file
148
cli/src/__tests__/helpers/embedded-postgres.ts
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
import fs from "node:fs";
|
||||||
|
import net from "node:net";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { applyPendingMigrations, ensurePostgresDatabase } from "@paperclipai/db";
|
||||||
|
|
||||||
|
type EmbeddedPostgresInstance = {
|
||||||
|
initialise(): Promise<void>;
|
||||||
|
start(): Promise<void>;
|
||||||
|
stop(): Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type EmbeddedPostgresCtor = new (opts: {
|
||||||
|
databaseDir: string;
|
||||||
|
user: string;
|
||||||
|
password: string;
|
||||||
|
port: number;
|
||||||
|
persistent: boolean;
|
||||||
|
initdbFlags?: string[];
|
||||||
|
onLog?: (message: unknown) => void;
|
||||||
|
onError?: (message: unknown) => void;
|
||||||
|
}) => EmbeddedPostgresInstance;
|
||||||
|
|
||||||
|
export type EmbeddedPostgresTestSupport = {
|
||||||
|
supported: boolean;
|
||||||
|
reason?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EmbeddedPostgresTestDatabase = {
|
||||||
|
connectionString: string;
|
||||||
|
cleanup(): Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
let embeddedPostgresSupportPromise: Promise<EmbeddedPostgresTestSupport> | null = null;
|
||||||
|
|
||||||
|
async function getEmbeddedPostgresCtor(): Promise<EmbeddedPostgresCtor> {
|
||||||
|
const mod = await import("embedded-postgres");
|
||||||
|
return mod.default as EmbeddedPostgresCtor;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAvailablePort(): Promise<number> {
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
const server = net.createServer();
|
||||||
|
server.unref();
|
||||||
|
server.on("error", reject);
|
||||||
|
server.listen(0, "127.0.0.1", () => {
|
||||||
|
const address = server.address();
|
||||||
|
if (!address || typeof address === "string") {
|
||||||
|
server.close(() => reject(new Error("Failed to allocate test port")));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { port } = address;
|
||||||
|
server.close((error) => {
|
||||||
|
if (error) reject(error);
|
||||||
|
else resolve(port);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatEmbeddedPostgresError(error: unknown): string {
|
||||||
|
if (error instanceof Error && error.message.length > 0) return error.message;
|
||||||
|
if (typeof error === "string" && error.length > 0) return error;
|
||||||
|
return "embedded Postgres startup failed";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function probeEmbeddedPostgresSupport(): Promise<EmbeddedPostgresTestSupport> {
|
||||||
|
if (process.platform !== "darwin") {
|
||||||
|
return { supported: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-embedded-postgres-probe-"));
|
||||||
|
const port = await getAvailablePort();
|
||||||
|
const EmbeddedPostgres = await getEmbeddedPostgresCtor();
|
||||||
|
const instance = new EmbeddedPostgres({
|
||||||
|
databaseDir: dataDir,
|
||||||
|
user: "paperclip",
|
||||||
|
password: "paperclip",
|
||||||
|
port,
|
||||||
|
persistent: true,
|
||||||
|
initdbFlags: ["--encoding=UTF8", "--locale=C", "--lc-messages=C"],
|
||||||
|
onLog: () => {},
|
||||||
|
onError: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await instance.initialise();
|
||||||
|
await instance.start();
|
||||||
|
return { supported: true };
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
supported: false,
|
||||||
|
reason: formatEmbeddedPostgresError(error),
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
await instance.stop().catch(() => {});
|
||||||
|
fs.rmSync(dataDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getEmbeddedPostgresTestSupport(): Promise<EmbeddedPostgresTestSupport> {
|
||||||
|
if (!embeddedPostgresSupportPromise) {
|
||||||
|
embeddedPostgresSupportPromise = probeEmbeddedPostgresSupport();
|
||||||
|
}
|
||||||
|
return await embeddedPostgresSupportPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function startEmbeddedPostgresTestDatabase(
|
||||||
|
tempDirPrefix: string,
|
||||||
|
): Promise<EmbeddedPostgresTestDatabase> {
|
||||||
|
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), tempDirPrefix));
|
||||||
|
const port = await getAvailablePort();
|
||||||
|
const EmbeddedPostgres = await getEmbeddedPostgresCtor();
|
||||||
|
const instance = new EmbeddedPostgres({
|
||||||
|
databaseDir: dataDir,
|
||||||
|
user: "paperclip",
|
||||||
|
password: "paperclip",
|
||||||
|
port,
|
||||||
|
persistent: true,
|
||||||
|
initdbFlags: ["--encoding=UTF8", "--locale=C", "--lc-messages=C"],
|
||||||
|
onLog: () => {},
|
||||||
|
onError: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await instance.initialise();
|
||||||
|
await instance.start();
|
||||||
|
|
||||||
|
const adminConnectionString = `postgres://paperclip:paperclip@127.0.0.1:${port}/postgres`;
|
||||||
|
await ensurePostgresDatabase(adminConnectionString, "paperclip");
|
||||||
|
const connectionString = `postgres://paperclip:paperclip@127.0.0.1:${port}/paperclip`;
|
||||||
|
await applyPendingMigrations(connectionString);
|
||||||
|
|
||||||
|
return {
|
||||||
|
connectionString,
|
||||||
|
cleanup: async () => {
|
||||||
|
await instance.stop().catch(() => {});
|
||||||
|
fs.rmSync(dataDir, { recursive: true, force: true });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
await instance.stop().catch(() => {});
|
||||||
|
fs.rmSync(dataDir, { recursive: true, force: true });
|
||||||
|
throw new Error(
|
||||||
|
`Failed to start embedded PostgreSQL test database: ${formatEmbeddedPostgresError(error)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,83 +1,24 @@
|
||||||
import { createHash } from "node:crypto";
|
import { createHash } from "node:crypto";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import net from "node:net";
|
|
||||||
import os from "node:os";
|
|
||||||
import path from "node:path";
|
|
||||||
import { afterEach, describe, expect, it } from "vitest";
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
import postgres from "postgres";
|
import postgres from "postgres";
|
||||||
import {
|
import {
|
||||||
applyPendingMigrations,
|
applyPendingMigrations,
|
||||||
ensurePostgresDatabase,
|
|
||||||
inspectMigrations,
|
inspectMigrations,
|
||||||
} from "./client.js";
|
} from "./client.js";
|
||||||
|
import {
|
||||||
|
getEmbeddedPostgresTestSupport,
|
||||||
|
startEmbeddedPostgresTestDatabase,
|
||||||
|
} from "./test-embedded-postgres.js";
|
||||||
|
|
||||||
type EmbeddedPostgresInstance = {
|
const cleanups: Array<() => Promise<void>> = [];
|
||||||
initialise(): Promise<void>;
|
const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport();
|
||||||
start(): Promise<void>;
|
const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip;
|
||||||
stop(): Promise<void>;
|
|
||||||
};
|
|
||||||
|
|
||||||
type EmbeddedPostgresCtor = new (opts: {
|
|
||||||
databaseDir: string;
|
|
||||||
user: string;
|
|
||||||
password: string;
|
|
||||||
port: number;
|
|
||||||
persistent: boolean;
|
|
||||||
initdbFlags?: string[];
|
|
||||||
onLog?: (message: unknown) => void;
|
|
||||||
onError?: (message: unknown) => void;
|
|
||||||
}) => EmbeddedPostgresInstance;
|
|
||||||
|
|
||||||
const tempPaths: string[] = [];
|
|
||||||
const runningInstances: EmbeddedPostgresInstance[] = [];
|
|
||||||
|
|
||||||
async function getEmbeddedPostgresCtor(): Promise<EmbeddedPostgresCtor> {
|
|
||||||
const mod = await import("embedded-postgres");
|
|
||||||
return mod.default as EmbeddedPostgresCtor;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getAvailablePort(): Promise<number> {
|
|
||||||
return await new Promise((resolve, reject) => {
|
|
||||||
const server = net.createServer();
|
|
||||||
server.unref();
|
|
||||||
server.on("error", reject);
|
|
||||||
server.listen(0, "127.0.0.1", () => {
|
|
||||||
const address = server.address();
|
|
||||||
if (!address || typeof address === "string") {
|
|
||||||
server.close(() => reject(new Error("Failed to allocate test port")));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { port } = address;
|
|
||||||
server.close((error) => {
|
|
||||||
if (error) reject(error);
|
|
||||||
else resolve(port);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createTempDatabase(): Promise<string> {
|
async function createTempDatabase(): Promise<string> {
|
||||||
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-db-client-"));
|
const db = await startEmbeddedPostgresTestDatabase("paperclip-db-client-");
|
||||||
tempPaths.push(dataDir);
|
cleanups.push(db.cleanup);
|
||||||
const port = await getAvailablePort();
|
return db.connectionString;
|
||||||
const EmbeddedPostgres = await getEmbeddedPostgresCtor();
|
|
||||||
const instance = new EmbeddedPostgres({
|
|
||||||
databaseDir: dataDir,
|
|
||||||
user: "paperclip",
|
|
||||||
password: "paperclip",
|
|
||||||
port,
|
|
||||||
persistent: true,
|
|
||||||
initdbFlags: ["--encoding=UTF8", "--locale=C", "--lc-messages=C"],
|
|
||||||
onLog: () => {},
|
|
||||||
onError: () => {},
|
|
||||||
});
|
|
||||||
await instance.initialise();
|
|
||||||
await instance.start();
|
|
||||||
runningInstances.push(instance);
|
|
||||||
|
|
||||||
const adminUrl = `postgres://paperclip:paperclip@127.0.0.1:${port}/postgres`;
|
|
||||||
await ensurePostgresDatabase(adminUrl, "paperclip");
|
|
||||||
return `postgres://paperclip:paperclip@127.0.0.1:${port}/paperclip`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function migrationHash(migrationFile: string): Promise<string> {
|
async function migrationHash(migrationFile: string): Promise<string> {
|
||||||
|
|
@ -89,19 +30,19 @@ async function migrationHash(migrationFile: string): Promise<string> {
|
||||||
}
|
}
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
while (runningInstances.length > 0) {
|
while (cleanups.length > 0) {
|
||||||
const instance = runningInstances.pop();
|
const cleanup = cleanups.pop();
|
||||||
if (!instance) continue;
|
await cleanup?.();
|
||||||
await instance.stop();
|
|
||||||
}
|
|
||||||
while (tempPaths.length > 0) {
|
|
||||||
const tempPath = tempPaths.pop();
|
|
||||||
if (!tempPath) continue;
|
|
||||||
fs.rmSync(tempPath, { recursive: true, force: true });
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("applyPendingMigrations", () => {
|
if (!embeddedPostgresSupport.supported) {
|
||||||
|
console.warn(
|
||||||
|
`Skipping embedded Postgres migration tests on this host: ${embeddedPostgresSupport.reason ?? "unsupported environment"}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describeEmbeddedPostgres("applyPendingMigrations", () => {
|
||||||
it(
|
it(
|
||||||
"applies an inserted earlier migration without replaying later legacy migrations",
|
"applies an inserted earlier migration without replaying later legacy migrations",
|
||||||
async () => {
|
async () => {
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,12 @@ export {
|
||||||
type MigrationBootstrapResult,
|
type MigrationBootstrapResult,
|
||||||
type Db,
|
type Db,
|
||||||
} from "./client.js";
|
} from "./client.js";
|
||||||
|
export {
|
||||||
|
getEmbeddedPostgresTestSupport,
|
||||||
|
startEmbeddedPostgresTestDatabase,
|
||||||
|
type EmbeddedPostgresTestDatabase,
|
||||||
|
type EmbeddedPostgresTestSupport,
|
||||||
|
} from "./test-embedded-postgres.js";
|
||||||
export {
|
export {
|
||||||
runDatabaseBackup,
|
runDatabaseBackup,
|
||||||
runDatabaseRestore,
|
runDatabaseRestore,
|
||||||
|
|
|
||||||
148
packages/db/src/test-embedded-postgres.ts
Normal file
148
packages/db/src/test-embedded-postgres.ts
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
import fs from "node:fs";
|
||||||
|
import net from "node:net";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { applyPendingMigrations, ensurePostgresDatabase } from "./client.js";
|
||||||
|
|
||||||
|
type EmbeddedPostgresInstance = {
|
||||||
|
initialise(): Promise<void>;
|
||||||
|
start(): Promise<void>;
|
||||||
|
stop(): Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type EmbeddedPostgresCtor = new (opts: {
|
||||||
|
databaseDir: string;
|
||||||
|
user: string;
|
||||||
|
password: string;
|
||||||
|
port: number;
|
||||||
|
persistent: boolean;
|
||||||
|
initdbFlags?: string[];
|
||||||
|
onLog?: (message: unknown) => void;
|
||||||
|
onError?: (message: unknown) => void;
|
||||||
|
}) => EmbeddedPostgresInstance;
|
||||||
|
|
||||||
|
export type EmbeddedPostgresTestSupport = {
|
||||||
|
supported: boolean;
|
||||||
|
reason?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EmbeddedPostgresTestDatabase = {
|
||||||
|
connectionString: string;
|
||||||
|
cleanup(): Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
let embeddedPostgresSupportPromise: Promise<EmbeddedPostgresTestSupport> | null = null;
|
||||||
|
|
||||||
|
async function getEmbeddedPostgresCtor(): Promise<EmbeddedPostgresCtor> {
|
||||||
|
const mod = await import("embedded-postgres");
|
||||||
|
return mod.default as EmbeddedPostgresCtor;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAvailablePort(): Promise<number> {
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
const server = net.createServer();
|
||||||
|
server.unref();
|
||||||
|
server.on("error", reject);
|
||||||
|
server.listen(0, "127.0.0.1", () => {
|
||||||
|
const address = server.address();
|
||||||
|
if (!address || typeof address === "string") {
|
||||||
|
server.close(() => reject(new Error("Failed to allocate test port")));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { port } = address;
|
||||||
|
server.close((error) => {
|
||||||
|
if (error) reject(error);
|
||||||
|
else resolve(port);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatEmbeddedPostgresError(error: unknown): string {
|
||||||
|
if (error instanceof Error && error.message.length > 0) return error.message;
|
||||||
|
if (typeof error === "string" && error.length > 0) return error;
|
||||||
|
return "embedded Postgres startup failed";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function probeEmbeddedPostgresSupport(): Promise<EmbeddedPostgresTestSupport> {
|
||||||
|
if (process.platform !== "darwin") {
|
||||||
|
return { supported: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-embedded-postgres-probe-"));
|
||||||
|
const port = await getAvailablePort();
|
||||||
|
const EmbeddedPostgres = await getEmbeddedPostgresCtor();
|
||||||
|
const instance = new EmbeddedPostgres({
|
||||||
|
databaseDir: dataDir,
|
||||||
|
user: "paperclip",
|
||||||
|
password: "paperclip",
|
||||||
|
port,
|
||||||
|
persistent: true,
|
||||||
|
initdbFlags: ["--encoding=UTF8", "--locale=C", "--lc-messages=C"],
|
||||||
|
onLog: () => {},
|
||||||
|
onError: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await instance.initialise();
|
||||||
|
await instance.start();
|
||||||
|
return { supported: true };
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
supported: false,
|
||||||
|
reason: formatEmbeddedPostgresError(error),
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
await instance.stop().catch(() => {});
|
||||||
|
fs.rmSync(dataDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getEmbeddedPostgresTestSupport(): Promise<EmbeddedPostgresTestSupport> {
|
||||||
|
if (!embeddedPostgresSupportPromise) {
|
||||||
|
embeddedPostgresSupportPromise = probeEmbeddedPostgresSupport();
|
||||||
|
}
|
||||||
|
return await embeddedPostgresSupportPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function startEmbeddedPostgresTestDatabase(
|
||||||
|
tempDirPrefix: string,
|
||||||
|
): Promise<EmbeddedPostgresTestDatabase> {
|
||||||
|
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), tempDirPrefix));
|
||||||
|
const port = await getAvailablePort();
|
||||||
|
const EmbeddedPostgres = await getEmbeddedPostgresCtor();
|
||||||
|
const instance = new EmbeddedPostgres({
|
||||||
|
databaseDir: dataDir,
|
||||||
|
user: "paperclip",
|
||||||
|
password: "paperclip",
|
||||||
|
port,
|
||||||
|
persistent: true,
|
||||||
|
initdbFlags: ["--encoding=UTF8", "--locale=C", "--lc-messages=C"],
|
||||||
|
onLog: () => {},
|
||||||
|
onError: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await instance.initialise();
|
||||||
|
await instance.start();
|
||||||
|
|
||||||
|
const adminConnectionString = `postgres://paperclip:paperclip@127.0.0.1:${port}/postgres`;
|
||||||
|
await ensurePostgresDatabase(adminConnectionString, "paperclip");
|
||||||
|
const connectionString = `postgres://paperclip:paperclip@127.0.0.1:${port}/paperclip`;
|
||||||
|
await applyPendingMigrations(connectionString);
|
||||||
|
|
||||||
|
return {
|
||||||
|
connectionString,
|
||||||
|
cleanup: async () => {
|
||||||
|
await instance.stop().catch(() => {});
|
||||||
|
fs.rmSync(dataDir, { recursive: true, force: true });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
await instance.stop().catch(() => {});
|
||||||
|
fs.rmSync(dataDir, { recursive: true, force: true });
|
||||||
|
throw new Error(
|
||||||
|
`Failed to start embedded PostgreSQL test database: ${formatEmbeddedPostgresError(error)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,89 +1,29 @@
|
||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import fs from "node:fs";
|
|
||||||
import net from "node:net";
|
|
||||||
import os from "node:os";
|
|
||||||
import path from "node:path";
|
|
||||||
import { spawn, type ChildProcess } from "node:child_process";
|
import { spawn, type ChildProcess } from "node:child_process";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
||||||
import {
|
import {
|
||||||
applyPendingMigrations,
|
|
||||||
createDb,
|
|
||||||
ensurePostgresDatabase,
|
|
||||||
agents,
|
agents,
|
||||||
agentWakeupRequests,
|
agentWakeupRequests,
|
||||||
companies,
|
companies,
|
||||||
|
createDb,
|
||||||
heartbeatRunEvents,
|
heartbeatRunEvents,
|
||||||
heartbeatRuns,
|
heartbeatRuns,
|
||||||
issues,
|
issues,
|
||||||
} from "@paperclipai/db";
|
} from "@paperclipai/db";
|
||||||
|
import {
|
||||||
|
getEmbeddedPostgresTestSupport,
|
||||||
|
startEmbeddedPostgresTestDatabase,
|
||||||
|
} from "./helpers/embedded-postgres.js";
|
||||||
import { runningProcesses } from "../adapters/index.ts";
|
import { runningProcesses } from "../adapters/index.ts";
|
||||||
import { heartbeatService } from "../services/heartbeat.ts";
|
import { heartbeatService } from "../services/heartbeat.ts";
|
||||||
|
const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport();
|
||||||
|
const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip;
|
||||||
|
|
||||||
type EmbeddedPostgresInstance = {
|
if (!embeddedPostgresSupport.supported) {
|
||||||
initialise(): Promise<void>;
|
console.warn(
|
||||||
start(): Promise<void>;
|
`Skipping embedded Postgres heartbeat recovery tests on this host: ${embeddedPostgresSupport.reason ?? "unsupported environment"}`,
|
||||||
stop(): Promise<void>;
|
);
|
||||||
};
|
|
||||||
|
|
||||||
type EmbeddedPostgresCtor = new (opts: {
|
|
||||||
databaseDir: string;
|
|
||||||
user: string;
|
|
||||||
password: string;
|
|
||||||
port: number;
|
|
||||||
persistent: boolean;
|
|
||||||
initdbFlags?: string[];
|
|
||||||
onLog?: (message: unknown) => void;
|
|
||||||
onError?: (message: unknown) => void;
|
|
||||||
}) => EmbeddedPostgresInstance;
|
|
||||||
|
|
||||||
async function getEmbeddedPostgresCtor(): Promise<EmbeddedPostgresCtor> {
|
|
||||||
const mod = await import("embedded-postgres");
|
|
||||||
return mod.default as EmbeddedPostgresCtor;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getAvailablePort(): Promise<number> {
|
|
||||||
return await new Promise((resolve, reject) => {
|
|
||||||
const server = net.createServer();
|
|
||||||
server.unref();
|
|
||||||
server.on("error", reject);
|
|
||||||
server.listen(0, "127.0.0.1", () => {
|
|
||||||
const address = server.address();
|
|
||||||
if (!address || typeof address === "string") {
|
|
||||||
server.close(() => reject(new Error("Failed to allocate test port")));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { port } = address;
|
|
||||||
server.close((error) => {
|
|
||||||
if (error) reject(error);
|
|
||||||
else resolve(port);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function startTempDatabase() {
|
|
||||||
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-heartbeat-recovery-"));
|
|
||||||
const port = await getAvailablePort();
|
|
||||||
const EmbeddedPostgres = await getEmbeddedPostgresCtor();
|
|
||||||
const instance = new EmbeddedPostgres({
|
|
||||||
databaseDir: dataDir,
|
|
||||||
user: "paperclip",
|
|
||||||
password: "paperclip",
|
|
||||||
port,
|
|
||||||
persistent: true,
|
|
||||||
initdbFlags: ["--encoding=UTF8", "--locale=C", "--lc-messages=C"],
|
|
||||||
onLog: () => {},
|
|
||||||
onError: () => {},
|
|
||||||
});
|
|
||||||
await instance.initialise();
|
|
||||||
await instance.start();
|
|
||||||
|
|
||||||
const adminConnectionString = `postgres://paperclip:paperclip@127.0.0.1:${port}/postgres`;
|
|
||||||
await ensurePostgresDatabase(adminConnectionString, "paperclip");
|
|
||||||
const connectionString = `postgres://paperclip:paperclip@127.0.0.1:${port}/paperclip`;
|
|
||||||
await applyPendingMigrations(connectionString);
|
|
||||||
return { connectionString, instance, dataDir };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function spawnAliveProcess() {
|
function spawnAliveProcess() {
|
||||||
|
|
@ -92,17 +32,14 @@ function spawnAliveProcess() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("heartbeat orphaned process recovery", () => {
|
describeEmbeddedPostgres("heartbeat orphaned process recovery", () => {
|
||||||
let db!: ReturnType<typeof createDb>;
|
let db!: ReturnType<typeof createDb>;
|
||||||
let instance: EmbeddedPostgresInstance | null = null;
|
let tempDb: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | null = null;
|
||||||
let dataDir = "";
|
|
||||||
const childProcesses = new Set<ChildProcess>();
|
const childProcesses = new Set<ChildProcess>();
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const started = await startTempDatabase();
|
tempDb = await startEmbeddedPostgresTestDatabase("paperclip-heartbeat-recovery-");
|
||||||
db = createDb(started.connectionString);
|
db = createDb(tempDb.connectionString);
|
||||||
instance = started.instance;
|
|
||||||
dataDir = started.dataDir;
|
|
||||||
}, 20_000);
|
}, 20_000);
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
|
|
@ -125,10 +62,7 @@ describe("heartbeat orphaned process recovery", () => {
|
||||||
}
|
}
|
||||||
childProcesses.clear();
|
childProcesses.clear();
|
||||||
runningProcesses.clear();
|
runningProcesses.clear();
|
||||||
await instance?.stop();
|
await tempDb?.cleanup();
|
||||||
if (dataDir) {
|
|
||||||
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
async function seedRunFixture(input?: {
|
async function seedRunFixture(input?: {
|
||||||
|
|
|
||||||
148
server/src/__tests__/helpers/embedded-postgres.ts
Normal file
148
server/src/__tests__/helpers/embedded-postgres.ts
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
import fs from "node:fs";
|
||||||
|
import net from "node:net";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { applyPendingMigrations, ensurePostgresDatabase } from "@paperclipai/db";
|
||||||
|
|
||||||
|
type EmbeddedPostgresInstance = {
|
||||||
|
initialise(): Promise<void>;
|
||||||
|
start(): Promise<void>;
|
||||||
|
stop(): Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type EmbeddedPostgresCtor = new (opts: {
|
||||||
|
databaseDir: string;
|
||||||
|
user: string;
|
||||||
|
password: string;
|
||||||
|
port: number;
|
||||||
|
persistent: boolean;
|
||||||
|
initdbFlags?: string[];
|
||||||
|
onLog?: (message: unknown) => void;
|
||||||
|
onError?: (message: unknown) => void;
|
||||||
|
}) => EmbeddedPostgresInstance;
|
||||||
|
|
||||||
|
export type EmbeddedPostgresTestSupport = {
|
||||||
|
supported: boolean;
|
||||||
|
reason?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EmbeddedPostgresTestDatabase = {
|
||||||
|
connectionString: string;
|
||||||
|
cleanup(): Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
let embeddedPostgresSupportPromise: Promise<EmbeddedPostgresTestSupport> | null = null;
|
||||||
|
|
||||||
|
async function getEmbeddedPostgresCtor(): Promise<EmbeddedPostgresCtor> {
|
||||||
|
const mod = await import("embedded-postgres");
|
||||||
|
return mod.default as EmbeddedPostgresCtor;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAvailablePort(): Promise<number> {
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
const server = net.createServer();
|
||||||
|
server.unref();
|
||||||
|
server.on("error", reject);
|
||||||
|
server.listen(0, "127.0.0.1", () => {
|
||||||
|
const address = server.address();
|
||||||
|
if (!address || typeof address === "string") {
|
||||||
|
server.close(() => reject(new Error("Failed to allocate test port")));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { port } = address;
|
||||||
|
server.close((error) => {
|
||||||
|
if (error) reject(error);
|
||||||
|
else resolve(port);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatEmbeddedPostgresError(error: unknown): string {
|
||||||
|
if (error instanceof Error && error.message.length > 0) return error.message;
|
||||||
|
if (typeof error === "string" && error.length > 0) return error;
|
||||||
|
return "embedded Postgres startup failed";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function probeEmbeddedPostgresSupport(): Promise<EmbeddedPostgresTestSupport> {
|
||||||
|
if (process.platform !== "darwin") {
|
||||||
|
return { supported: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-embedded-postgres-probe-"));
|
||||||
|
const port = await getAvailablePort();
|
||||||
|
const EmbeddedPostgres = await getEmbeddedPostgresCtor();
|
||||||
|
const instance = new EmbeddedPostgres({
|
||||||
|
databaseDir: dataDir,
|
||||||
|
user: "paperclip",
|
||||||
|
password: "paperclip",
|
||||||
|
port,
|
||||||
|
persistent: true,
|
||||||
|
initdbFlags: ["--encoding=UTF8", "--locale=C", "--lc-messages=C"],
|
||||||
|
onLog: () => {},
|
||||||
|
onError: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await instance.initialise();
|
||||||
|
await instance.start();
|
||||||
|
return { supported: true };
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
supported: false,
|
||||||
|
reason: formatEmbeddedPostgresError(error),
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
await instance.stop().catch(() => {});
|
||||||
|
fs.rmSync(dataDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getEmbeddedPostgresTestSupport(): Promise<EmbeddedPostgresTestSupport> {
|
||||||
|
if (!embeddedPostgresSupportPromise) {
|
||||||
|
embeddedPostgresSupportPromise = probeEmbeddedPostgresSupport();
|
||||||
|
}
|
||||||
|
return await embeddedPostgresSupportPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function startEmbeddedPostgresTestDatabase(
|
||||||
|
tempDirPrefix: string,
|
||||||
|
): Promise<EmbeddedPostgresTestDatabase> {
|
||||||
|
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), tempDirPrefix));
|
||||||
|
const port = await getAvailablePort();
|
||||||
|
const EmbeddedPostgres = await getEmbeddedPostgresCtor();
|
||||||
|
const instance = new EmbeddedPostgres({
|
||||||
|
databaseDir: dataDir,
|
||||||
|
user: "paperclip",
|
||||||
|
password: "paperclip",
|
||||||
|
port,
|
||||||
|
persistent: true,
|
||||||
|
initdbFlags: ["--encoding=UTF8", "--locale=C", "--lc-messages=C"],
|
||||||
|
onLog: () => {},
|
||||||
|
onError: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await instance.initialise();
|
||||||
|
await instance.start();
|
||||||
|
|
||||||
|
const adminConnectionString = `postgres://paperclip:paperclip@127.0.0.1:${port}/postgres`;
|
||||||
|
await ensurePostgresDatabase(adminConnectionString, "paperclip");
|
||||||
|
const connectionString = `postgres://paperclip:paperclip@127.0.0.1:${port}/paperclip`;
|
||||||
|
await applyPendingMigrations(connectionString);
|
||||||
|
|
||||||
|
return {
|
||||||
|
connectionString,
|
||||||
|
cleanup: async () => {
|
||||||
|
await instance.stop().catch(() => {});
|
||||||
|
fs.rmSync(dataDir, { recursive: true, force: true });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
await instance.stop().catch(() => {});
|
||||||
|
fs.rmSync(dataDir, { recursive: true, force: true });
|
||||||
|
throw new Error(
|
||||||
|
`Failed to start embedded PostgreSQL test database: ${formatEmbeddedPostgresError(error)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,99 +1,37 @@
|
||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import fs from "node:fs";
|
|
||||||
import net from "node:net";
|
|
||||||
import os from "node:os";
|
|
||||||
import path from "node:path";
|
|
||||||
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
||||||
import {
|
import {
|
||||||
activityLog,
|
activityLog,
|
||||||
agents,
|
agents,
|
||||||
applyPendingMigrations,
|
|
||||||
companies,
|
companies,
|
||||||
createDb,
|
createDb,
|
||||||
ensurePostgresDatabase,
|
|
||||||
issueComments,
|
issueComments,
|
||||||
issues,
|
issues,
|
||||||
} from "@paperclipai/db";
|
} from "@paperclipai/db";
|
||||||
|
import {
|
||||||
|
getEmbeddedPostgresTestSupport,
|
||||||
|
startEmbeddedPostgresTestDatabase,
|
||||||
|
} from "./helpers/embedded-postgres.js";
|
||||||
import { issueService } from "../services/issues.ts";
|
import { issueService } from "../services/issues.ts";
|
||||||
|
|
||||||
type EmbeddedPostgresInstance = {
|
const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport();
|
||||||
initialise(): Promise<void>;
|
const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip;
|
||||||
start(): Promise<void>;
|
|
||||||
stop(): Promise<void>;
|
|
||||||
};
|
|
||||||
|
|
||||||
type EmbeddedPostgresCtor = new (opts: {
|
if (!embeddedPostgresSupport.supported) {
|
||||||
databaseDir: string;
|
console.warn(
|
||||||
user: string;
|
`Skipping embedded Postgres issue service tests on this host: ${embeddedPostgresSupport.reason ?? "unsupported environment"}`,
|
||||||
password: string;
|
);
|
||||||
port: number;
|
|
||||||
persistent: boolean;
|
|
||||||
initdbFlags?: string[];
|
|
||||||
onLog?: (message: unknown) => void;
|
|
||||||
onError?: (message: unknown) => void;
|
|
||||||
}) => EmbeddedPostgresInstance;
|
|
||||||
|
|
||||||
async function getEmbeddedPostgresCtor(): Promise<EmbeddedPostgresCtor> {
|
|
||||||
const mod = await import("embedded-postgres");
|
|
||||||
return mod.default as EmbeddedPostgresCtor;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAvailablePort(): Promise<number> {
|
describeEmbeddedPostgres("issueService.list participantAgentId", () => {
|
||||||
return await new Promise((resolve, reject) => {
|
|
||||||
const server = net.createServer();
|
|
||||||
server.unref();
|
|
||||||
server.on("error", reject);
|
|
||||||
server.listen(0, "127.0.0.1", () => {
|
|
||||||
const address = server.address();
|
|
||||||
if (!address || typeof address === "string") {
|
|
||||||
server.close(() => reject(new Error("Failed to allocate test port")));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { port } = address;
|
|
||||||
server.close((error) => {
|
|
||||||
if (error) reject(error);
|
|
||||||
else resolve(port);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function startTempDatabase() {
|
|
||||||
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-issues-service-"));
|
|
||||||
const port = await getAvailablePort();
|
|
||||||
const EmbeddedPostgres = await getEmbeddedPostgresCtor();
|
|
||||||
const instance = new EmbeddedPostgres({
|
|
||||||
databaseDir: dataDir,
|
|
||||||
user: "paperclip",
|
|
||||||
password: "paperclip",
|
|
||||||
port,
|
|
||||||
persistent: true,
|
|
||||||
initdbFlags: ["--encoding=UTF8", "--locale=C", "--lc-messages=C"],
|
|
||||||
onLog: () => {},
|
|
||||||
onError: () => {},
|
|
||||||
});
|
|
||||||
await instance.initialise();
|
|
||||||
await instance.start();
|
|
||||||
|
|
||||||
const adminConnectionString = `postgres://paperclip:paperclip@127.0.0.1:${port}/postgres`;
|
|
||||||
await ensurePostgresDatabase(adminConnectionString, "paperclip");
|
|
||||||
const connectionString = `postgres://paperclip:paperclip@127.0.0.1:${port}/paperclip`;
|
|
||||||
await applyPendingMigrations(connectionString);
|
|
||||||
return { connectionString, dataDir, instance };
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("issueService.list participantAgentId", () => {
|
|
||||||
let db!: ReturnType<typeof createDb>;
|
let db!: ReturnType<typeof createDb>;
|
||||||
let svc!: ReturnType<typeof issueService>;
|
let svc!: ReturnType<typeof issueService>;
|
||||||
let instance: EmbeddedPostgresInstance | null = null;
|
let tempDb: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | null = null;
|
||||||
let dataDir = "";
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const started = await startTempDatabase();
|
tempDb = await startEmbeddedPostgresTestDatabase("paperclip-issues-service-");
|
||||||
db = createDb(started.connectionString);
|
db = createDb(tempDb.connectionString);
|
||||||
svc = issueService(db);
|
svc = issueService(db);
|
||||||
instance = started.instance;
|
|
||||||
dataDir = started.dataDir;
|
|
||||||
}, 20_000);
|
}, 20_000);
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
|
|
@ -105,10 +43,7 @@ describe("issueService.list participantAgentId", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await instance?.stop();
|
await tempDb?.cleanup();
|
||||||
if (dataDir) {
|
|
||||||
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns issues an agent participated in across the supported signals", async () => {
|
it("returns issues an agent participated in across the supported signals", async () => {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,4 @@
|
||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import fs from "node:fs";
|
|
||||||
import net from "node:net";
|
|
||||||
import os from "node:os";
|
|
||||||
import path from "node:path";
|
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import request from "supertest";
|
import request from "supertest";
|
||||||
|
|
@ -11,11 +7,9 @@ import {
|
||||||
activityLog,
|
activityLog,
|
||||||
agentWakeupRequests,
|
agentWakeupRequests,
|
||||||
agents,
|
agents,
|
||||||
applyPendingMigrations,
|
|
||||||
companies,
|
companies,
|
||||||
companyMemberships,
|
companyMemberships,
|
||||||
createDb,
|
createDb,
|
||||||
ensurePostgresDatabase,
|
|
||||||
heartbeatRunEvents,
|
heartbeatRunEvents,
|
||||||
heartbeatRuns,
|
heartbeatRuns,
|
||||||
instanceSettings,
|
instanceSettings,
|
||||||
|
|
@ -26,6 +20,10 @@ import {
|
||||||
routines,
|
routines,
|
||||||
routineTriggers,
|
routineTriggers,
|
||||||
} from "@paperclipai/db";
|
} from "@paperclipai/db";
|
||||||
|
import {
|
||||||
|
getEmbeddedPostgresTestSupport,
|
||||||
|
startEmbeddedPostgresTestDatabase,
|
||||||
|
} from "./helpers/embedded-postgres.js";
|
||||||
import { errorHandler } from "../middleware/index.js";
|
import { errorHandler } from "../middleware/index.js";
|
||||||
import { accessService } from "../services/access.js";
|
import { accessService } from "../services/access.js";
|
||||||
|
|
||||||
|
|
@ -78,82 +76,22 @@ vi.mock("../services/index.js", async () => {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
type EmbeddedPostgresInstance = {
|
const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport();
|
||||||
initialise(): Promise<void>;
|
const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip;
|
||||||
start(): Promise<void>;
|
|
||||||
stop(): Promise<void>;
|
|
||||||
};
|
|
||||||
|
|
||||||
type EmbeddedPostgresCtor = new (opts: {
|
if (!embeddedPostgresSupport.supported) {
|
||||||
databaseDir: string;
|
console.warn(
|
||||||
user: string;
|
`Skipping embedded Postgres routine route tests on this host: ${embeddedPostgresSupport.reason ?? "unsupported environment"}`,
|
||||||
password: string;
|
);
|
||||||
port: number;
|
|
||||||
persistent: boolean;
|
|
||||||
initdbFlags?: string[];
|
|
||||||
onLog?: (message: unknown) => void;
|
|
||||||
onError?: (message: unknown) => void;
|
|
||||||
}) => EmbeddedPostgresInstance;
|
|
||||||
|
|
||||||
async function getEmbeddedPostgresCtor(): Promise<EmbeddedPostgresCtor> {
|
|
||||||
const mod = await import("embedded-postgres");
|
|
||||||
return mod.default as EmbeddedPostgresCtor;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAvailablePort(): Promise<number> {
|
describeEmbeddedPostgres("routine routes end-to-end", () => {
|
||||||
return await new Promise((resolve, reject) => {
|
|
||||||
const server = net.createServer();
|
|
||||||
server.unref();
|
|
||||||
server.on("error", reject);
|
|
||||||
server.listen(0, "127.0.0.1", () => {
|
|
||||||
const address = server.address();
|
|
||||||
if (!address || typeof address === "string") {
|
|
||||||
server.close(() => reject(new Error("Failed to allocate test port")));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { port } = address;
|
|
||||||
server.close((error) => {
|
|
||||||
if (error) reject(error);
|
|
||||||
else resolve(port);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function startTempDatabase() {
|
|
||||||
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-routines-e2e-"));
|
|
||||||
const port = await getAvailablePort();
|
|
||||||
const EmbeddedPostgres = await getEmbeddedPostgresCtor();
|
|
||||||
const instance = new EmbeddedPostgres({
|
|
||||||
databaseDir: dataDir,
|
|
||||||
user: "paperclip",
|
|
||||||
password: "paperclip",
|
|
||||||
port,
|
|
||||||
persistent: true,
|
|
||||||
initdbFlags: ["--encoding=UTF8", "--locale=C", "--lc-messages=C"],
|
|
||||||
onLog: () => {},
|
|
||||||
onError: () => {},
|
|
||||||
});
|
|
||||||
await instance.initialise();
|
|
||||||
await instance.start();
|
|
||||||
|
|
||||||
const adminConnectionString = `postgres://paperclip:paperclip@127.0.0.1:${port}/postgres`;
|
|
||||||
await ensurePostgresDatabase(adminConnectionString, "paperclip");
|
|
||||||
const connectionString = `postgres://paperclip:paperclip@127.0.0.1:${port}/paperclip`;
|
|
||||||
await applyPendingMigrations(connectionString);
|
|
||||||
return { connectionString, dataDir, instance };
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("routine routes end-to-end", () => {
|
|
||||||
let db!: ReturnType<typeof createDb>;
|
let db!: ReturnType<typeof createDb>;
|
||||||
let instance: EmbeddedPostgresInstance | null = null;
|
let tempDb: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | null = null;
|
||||||
let dataDir = "";
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const started = await startTempDatabase();
|
tempDb = await startEmbeddedPostgresTestDatabase("paperclip-routines-e2e-");
|
||||||
db = createDb(started.connectionString);
|
db = createDb(tempDb.connectionString);
|
||||||
instance = started.instance;
|
|
||||||
dataDir = started.dataDir;
|
|
||||||
}, 20_000);
|
}, 20_000);
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
|
|
@ -174,10 +112,7 @@ describe("routine routes end-to-end", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await instance?.stop();
|
await tempDb?.cleanup();
|
||||||
if (dataDir) {
|
|
||||||
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
async function createApp(actor: Record<string, unknown>) {
|
async function createApp(actor: Record<string, unknown>) {
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,13 @@
|
||||||
import { createHmac, randomUUID } from "node:crypto";
|
import { createHmac, randomUUID } from "node:crypto";
|
||||||
import fs from "node:fs";
|
|
||||||
import net from "node:net";
|
|
||||||
import os from "node:os";
|
|
||||||
import path from "node:path";
|
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
||||||
import {
|
import {
|
||||||
activityLog,
|
activityLog,
|
||||||
agents,
|
agents,
|
||||||
applyPendingMigrations,
|
|
||||||
companies,
|
companies,
|
||||||
companySecrets,
|
companySecrets,
|
||||||
companySecretVersions,
|
companySecretVersions,
|
||||||
createDb,
|
createDb,
|
||||||
ensurePostgresDatabase,
|
|
||||||
heartbeatRuns,
|
heartbeatRuns,
|
||||||
issues,
|
issues,
|
||||||
projects,
|
projects,
|
||||||
|
|
@ -21,85 +15,29 @@ import {
|
||||||
routines,
|
routines,
|
||||||
routineTriggers,
|
routineTriggers,
|
||||||
} from "@paperclipai/db";
|
} from "@paperclipai/db";
|
||||||
|
import {
|
||||||
|
getEmbeddedPostgresTestSupport,
|
||||||
|
startEmbeddedPostgresTestDatabase,
|
||||||
|
} from "./helpers/embedded-postgres.js";
|
||||||
import { issueService } from "../services/issues.ts";
|
import { issueService } from "../services/issues.ts";
|
||||||
import { routineService } from "../services/routines.ts";
|
import { routineService } from "../services/routines.ts";
|
||||||
|
|
||||||
type EmbeddedPostgresInstance = {
|
const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport();
|
||||||
initialise(): Promise<void>;
|
const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip;
|
||||||
start(): Promise<void>;
|
|
||||||
stop(): Promise<void>;
|
|
||||||
};
|
|
||||||
|
|
||||||
type EmbeddedPostgresCtor = new (opts: {
|
if (!embeddedPostgresSupport.supported) {
|
||||||
databaseDir: string;
|
console.warn(
|
||||||
user: string;
|
`Skipping embedded Postgres routines service tests on this host: ${embeddedPostgresSupport.reason ?? "unsupported environment"}`,
|
||||||
password: string;
|
);
|
||||||
port: number;
|
|
||||||
persistent: boolean;
|
|
||||||
initdbFlags?: string[];
|
|
||||||
onLog?: (message: unknown) => void;
|
|
||||||
onError?: (message: unknown) => void;
|
|
||||||
}) => EmbeddedPostgresInstance;
|
|
||||||
|
|
||||||
async function getEmbeddedPostgresCtor(): Promise<EmbeddedPostgresCtor> {
|
|
||||||
const mod = await import("embedded-postgres");
|
|
||||||
return mod.default as EmbeddedPostgresCtor;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAvailablePort(): Promise<number> {
|
describeEmbeddedPostgres("routine service live-execution coalescing", () => {
|
||||||
return await new Promise((resolve, reject) => {
|
|
||||||
const server = net.createServer();
|
|
||||||
server.unref();
|
|
||||||
server.on("error", reject);
|
|
||||||
server.listen(0, "127.0.0.1", () => {
|
|
||||||
const address = server.address();
|
|
||||||
if (!address || typeof address === "string") {
|
|
||||||
server.close(() => reject(new Error("Failed to allocate test port")));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { port } = address;
|
|
||||||
server.close((error) => {
|
|
||||||
if (error) reject(error);
|
|
||||||
else resolve(port);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function startTempDatabase() {
|
|
||||||
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-routines-service-"));
|
|
||||||
const port = await getAvailablePort();
|
|
||||||
const EmbeddedPostgres = await getEmbeddedPostgresCtor();
|
|
||||||
const instance = new EmbeddedPostgres({
|
|
||||||
databaseDir: dataDir,
|
|
||||||
user: "paperclip",
|
|
||||||
password: "paperclip",
|
|
||||||
port,
|
|
||||||
persistent: true,
|
|
||||||
initdbFlags: ["--encoding=UTF8", "--locale=C", "--lc-messages=C"],
|
|
||||||
onLog: () => {},
|
|
||||||
onError: () => {},
|
|
||||||
});
|
|
||||||
await instance.initialise();
|
|
||||||
await instance.start();
|
|
||||||
|
|
||||||
const adminConnectionString = `postgres://paperclip:paperclip@127.0.0.1:${port}/postgres`;
|
|
||||||
await ensurePostgresDatabase(adminConnectionString, "paperclip");
|
|
||||||
const connectionString = `postgres://paperclip:paperclip@127.0.0.1:${port}/paperclip`;
|
|
||||||
await applyPendingMigrations(connectionString);
|
|
||||||
return { connectionString, dataDir, instance };
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("routine service live-execution coalescing", () => {
|
|
||||||
let db!: ReturnType<typeof createDb>;
|
let db!: ReturnType<typeof createDb>;
|
||||||
let instance: EmbeddedPostgresInstance | null = null;
|
let tempDb: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | null = null;
|
||||||
let dataDir = "";
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const started = await startTempDatabase();
|
tempDb = await startEmbeddedPostgresTestDatabase("paperclip-routines-service-");
|
||||||
db = createDb(started.connectionString);
|
db = createDb(tempDb.connectionString);
|
||||||
instance = started.instance;
|
|
||||||
dataDir = started.dataDir;
|
|
||||||
}, 20_000);
|
}, 20_000);
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
|
|
@ -117,10 +55,7 @@ describe("routine service live-execution coalescing", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await instance?.stop();
|
await tempDb?.cleanup();
|
||||||
if (dataDir) {
|
|
||||||
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
async function seedFixture(opts?: {
|
async function seedFixture(opts?: {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue