fix(cli): make embedded-Postgres tests survive runner contention (#12466)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The CLI test suite uses embedded Postgres for worktree checks.
> - Several tests start a cluster and run migrations on each test.
> - Runner contention can make this cost class exceed small hand-written
time budgets.
> - A timed-out test can also leave the cluster alive while cleanup
removes its data directory.
> - This pull request gives the cost class one measured timeout and
registers cleanup for the timeout path.
> - The benefit is stable tests and fewer orphaned embedded Postgres
processes.

## Linked Issues or Issue Description

No public GitHub issue tracks this defect. Related PRs #10987 and #10961
address wider embedded-Postgres test setup issues. This pull request
addresses the CLI timeout and cleanup path described below.

**What happened?**
Five CLI tests used different hand-written time budgets while they
started embedded Postgres and ran migrations. Runner contention made one
test exceed its budget. A timeout also skipped the `finally` cleanup
path and left a Postgres process alive.

**Expected behavior**
Each embedded-Postgres test gets a budget that covers measured runner
contention. Cleanup stops the cluster before the test removes its data
directory, including after a timeout.

**Steps to reproduce**
1. Run `npx vitest run cli/src/__tests__/worktree.test.ts` on a
contended runner.
2. Observe the embedded-Postgres tests take longer than their small
hand-written budgets.
3. Inspect the process list after a timeout and observe an orphaned
Postgres process.

**Paperclip version or commit**
The defect reproduces on `master` before this pull request.

**Deployment mode**
Built from source with Vitest.

**Installation method**
Built from source with pnpm.

## What Changed

- Export `EMBEDDED_POSTGRES_TEST_TIMEOUT_MS` from the embedded-Postgres
test helper.
- Apply the shared timeout to every test defined by
`itEmbeddedPostgres`.
- Replace the five hand-written timeout values in `worktree.test.ts`.
- Register temporary-directory removal and cluster stop with
`onTestFinished` in the correct order.
- Restore the working directory during timeout cleanup.

## Verification

- `npx vitest run cli/src/__tests__/worktree.test.ts` passes 63 tests.
- `npx vitest run cli/src` passes 424 tests across 59 files.
- `tsc --noEmit` in `cli/` adds no new error in the touched files.
- CI must pass on the pull request head.
- Greptile must report 5/5 with no unresolved blocking thread.

## Risks

- Low risk. The change affects test helpers and test cleanup only.
- The shared budget can lengthen a failing test before Vitest reports
the failure.
- The cleanup order depends on Vitest callback order, which the tests
now use explicitly.

## Model Used

OpenAI Codex, GPT-5. The model used tool calls and code review support.
The context window size and internal reasoning mode were not exposed in
this run.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` /
`Closes: #` / `Refs: #` OR (b) described the issue in-PR following the
relevant issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Nicky LeachandPaperclip authored and GitHub committed 2026-08-28 13:51:54 -07:00
1 parent 1da6b37fc5
commit a3b78f9d34
4 files changed
+211 -207

No files matched your search

@@ -1,6 +1,7 @@
export {
getEmbeddedPostgresTestSupport,
startEmbeddedPostgresTestDatabase,
EMBEDDED_POSTGRES_TEST_TIMEOUT_MS,
type EmbeddedPostgresTestDatabase,
type EmbeddedPostgresTestSupport,
} from "@paperclipai/db";
+201 -207
View File
@@ -5,7 +5,7 @@ import { execFileSync } from "node:child_process";
import { randomUUID } from "node:crypto";
import { createServer } from "node:net";
import { eq } from "drizzle-orm";
import { afterEach, describe, expect, it, vi } from "vitest";
import { afterEach, describe, expect, it, onTestFinished, vi } from "vitest";
import {
agents,
authAccounts,
@@ -64,6 +64,7 @@ import {
} from "../commands/worktree-lib.js";
import type { PaperclipConfig } from "../config/schema.js";
import {
EMBEDDED_POSTGRES_TEST_TIMEOUT_MS,
getEmbeddedPostgresTestSupport,
startEmbeddedPostgresTestDatabase,
} from "./helpers/embedded-postgres.js";
@@ -71,7 +72,16 @@ import {
const ORIGINAL_CWD = process.cwd();
const ORIGINAL_ENV = { ...process.env };
const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport();
const itEmbeddedPostgres = embeddedPostgresSupport.supported ? it : it.skip;
// Every test in the embedded-Postgres cost class shares one time budget
// (see EMBEDDED_POSTGRES_TEST_TIMEOUT_MS) instead of a hand-written number
// per call site.
function itEmbeddedPostgres(name: string, fn: () => Promise<void>): void {
if (!embeddedPostgresSupport.supported) {
it.skip(name, fn);
return;
}
it(name, fn, EMBEDDED_POSTGRES_TEST_TIMEOUT_MS);
}
const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip;
function mockVerifiedSeedResult() {
@@ -593,6 +603,7 @@ describe("worktree helpers", () => {
itEmbeddedPostgres("recognizes positive legacy database schema evidence", async () => {
const tempDb = await startEmbeddedPostgresTestDatabase("paperclip-worktree-legacy-evidence-");
onTestFinished(() => tempDb.cleanup());
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-legacy-config-"));
try {
const configPath = path.join(tempRoot, "config.json");
@@ -623,9 +634,8 @@ describe("worktree helpers", () => {
});
} finally {
fs.rmSync(tempRoot, { recursive: true, force: true });
await tempDb.cleanup();
}
}, 30000);
});
it("ensure-seeded seeds once and fast-exits on the verified manifest", async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-ensure-seeded-"));
@@ -1160,6 +1170,7 @@ describe("worktree helpers", () => {
itEmbeddedPostgres("quarantines copied live execution state in seeded worktree databases", async () => {
const tempDb = await startEmbeddedPostgresTestDatabase("paperclip-worktree-quarantine-");
onTestFinished(() => tempDb.cleanup());
const db = createDb(tempDb.connectionString);
const companyId = randomUUID();
const agentId = randomUUID();
@@ -1392,9 +1403,8 @@ describe("worktree helpers", () => {
expect(runtimeService?.stoppedAt).toBeInstanceOf(Date);
} finally {
await db.$client?.end?.({ timeout: 5 }).catch(() => undefined);
await tempDb.cleanup();
}
}, 20_000);
});
it("copies the source local_encrypted secrets key into the seeded worktree instance", () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-secrets-"));
@@ -1532,99 +1542,93 @@ describe("worktree helpers", () => {
"seeds a local-trusted implicit board user without a credential account",
async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-local-board-seed-"));
const originalCwd = process.cwd();
onTestFinished(() => {
process.chdir(originalCwd);
fs.rmSync(tempRoot, { recursive: true, force: true });
});
const worktreeRoot = path.join(tempRoot, "PAP-17696-local-board-seed");
const sourceConfigDir = path.join(tempRoot, "source");
const sourceConfigPath = path.join(sourceConfigDir, "config.json");
const sourceKeyPath = path.join(sourceConfigDir, "secrets", "master.key");
const worktreeHome = path.join(tempRoot, ".paperclip-worktrees");
const originalCwd = process.cwd();
const sourceDb = await startEmbeddedPostgresTestDatabase("paperclip-worktree-local-board-source-");
onTestFinished(() => sourceDb.cleanup());
try {
await seedValidWorktreeSource(sourceDb.connectionString, {
includeCredentialAccount: false,
userId: "local-board",
});
fs.mkdirSync(path.dirname(sourceKeyPath), { recursive: true });
fs.mkdirSync(worktreeRoot, { recursive: true });
await seedValidWorktreeSource(sourceDb.connectionString, {
includeCredentialAccount: false,
userId: "local-board",
});
fs.mkdirSync(path.dirname(sourceKeyPath), { recursive: true });
fs.mkdirSync(worktreeRoot, { recursive: true });
const sourceConfig = buildSourceConfig();
sourceConfig.database = {
...sourceConfig.database,
mode: "postgres",
connectionString: sourceDb.connectionString,
};
sourceConfig.server.deploymentMode = "local_trusted";
sourceConfig.server.exposure = "private";
sourceConfig.auth.baseUrlMode = "auto";
delete sourceConfig.auth.publicBaseUrl;
sourceConfig.secrets.localEncrypted.keyFilePath = sourceKeyPath;
const sourceConfig = buildSourceConfig();
sourceConfig.database = {
...sourceConfig.database,
mode: "postgres",
connectionString: sourceDb.connectionString,
};
sourceConfig.server.deploymentMode = "local_trusted";
sourceConfig.server.exposure = "private";
sourceConfig.auth.baseUrlMode = "auto";
delete sourceConfig.auth.publicBaseUrl;
sourceConfig.secrets.localEncrypted.keyFilePath = sourceKeyPath;
fs.writeFileSync(sourceConfigPath, `${JSON.stringify(sourceConfig, null, 2)}\n`, "utf8");
fs.writeFileSync(sourceKeyPath, "source-master-key", "utf8");
fs.writeFileSync(sourceConfigPath, `${JSON.stringify(sourceConfig, null, 2)}\n`, "utf8");
fs.writeFileSync(sourceKeyPath, "source-master-key", "utf8");
process.chdir(worktreeRoot);
await worktreeInitCommand({
name: "PAP-17696-local-board-seed",
home: worktreeHome,
fromConfig: sourceConfigPath,
force: true,
});
process.chdir(worktreeRoot);
await worktreeInitCommand({
name: "PAP-17696-local-board-seed",
home: worktreeHome,
fromConfig: sourceConfigPath,
force: true,
});
const targetConfigPath = path.join(worktreeRoot, ".paperclip", "config.json");
const targetConfig = JSON.parse(fs.readFileSync(targetConfigPath, "utf8")) as PaperclipConfig;
expect(readWorktreeSeedManifest(targetConfigPath)).toMatchObject({
state: "verified",
phase: "complete",
});
const targetConfigPath = path.join(worktreeRoot, ".paperclip", "config.json");
const targetConfig = JSON.parse(fs.readFileSync(targetConfigPath, "utf8")) as PaperclipConfig;
expect(readWorktreeSeedManifest(targetConfigPath)).toMatchObject({
state: "verified",
phase: "complete",
});
const { default: EmbeddedPostgres } = await import("embedded-postgres");
const targetPg = new EmbeddedPostgres({
databaseDir: targetConfig.database.embeddedPostgresDataDir,
user: "paperclip",
password: "paperclip",
port: targetConfig.database.embeddedPostgresPort,
persistent: true,
initdbFlags: ["--encoding=UTF8", "--locale=C", "--lc-messages=C"],
onLog: () => {},
onError: () => {},
});
const { default: EmbeddedPostgres } = await import("embedded-postgres");
const targetPg = new EmbeddedPostgres({
databaseDir: targetConfig.database.embeddedPostgresDataDir,
user: "paperclip",
password: "paperclip",
port: targetConfig.database.embeddedPostgresPort,
persistent: true,
initdbFlags: ["--encoding=UTF8", "--locale=C", "--lc-messages=C"],
onLog: () => {},
onError: () => {},
});
await targetPg.start();
try {
const targetDb = createDb(
`postgres://paperclip:paperclip@127.0.0.1:${targetConfig.database.embeddedPostgresPort}/paperclip`,
);
const [seededLocalBoard] = await targetDb
.select({ id: authUsers.id })
.from(authUsers)
.where(eq(authUsers.id, "local-board"));
const seededAccounts = await targetDb.select().from(authAccounts);
expect(seededLocalBoard?.id).toBe("local-board");
expect(seededAccounts).toHaveLength(0);
await targetDb.$client.end({ timeout: 5 });
} finally {
await targetPg.stop();
}
} finally {
process.chdir(originalCwd);
await sourceDb.cleanup();
fs.rmSync(tempRoot, { recursive: true, force: true });
}
await targetPg.start();
onTestFinished(() => targetPg.stop());
const targetDb = createDb(
`postgres://paperclip:paperclip@127.0.0.1:${targetConfig.database.embeddedPostgresPort}/paperclip`,
);
const [seededLocalBoard] = await targetDb
.select({ id: authUsers.id })
.from(authUsers)
.where(eq(authUsers.id, "local-board"));
const seededAccounts = await targetDb.select().from(authAccounts);
expect(seededLocalBoard?.id).toBe("local-board");
expect(seededAccounts).toHaveLength(0);
await targetDb.$client.end({ timeout: 5 });
},
// This test starts three separate embedded Postgres lifecycles (the
// source database, the worktree init's internal target database, and a
// third instance opened here to verify the seeded rows), so it needs
// more headroom than the other embedded-Postgres tests in this file.
// It normally finishes in well under 10s; the 60s budget absorbs CI
// runner contention without masking a real hang.
60_000,
);
itEmbeddedPostgres(
"seeds a lagging source whose migration application order differs from filename order",
async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-auth-seed-"));
const originalCwd = process.cwd();
onTestFinished(() => {
process.chdir(originalCwd);
fs.rmSync(tempRoot, { recursive: true, force: true });
});
const worktreeRoot = path.join(tempRoot, "PAP-999-auth-seed");
const sourceHome = path.join(tempRoot, "source-home");
const sourceConfigDir = path.join(sourceHome, "instances", "source");
@@ -1632,139 +1636,129 @@ describe("worktree helpers", () => {
const sourceEnvPath = path.join(sourceConfigDir, ".env");
const sourceKeyPath = path.join(sourceConfigDir, "secrets", "master.key");
const worktreeHome = path.join(tempRoot, ".paperclip-worktrees");
const originalCwd = process.cwd();
const sourceDb = await startEmbeddedPostgresTestDatabase("paperclip-worktree-auth-source-");
onTestFinished(() => sourceDb.cleanup());
try {
await seedValidWorktreeSource(sourceDb.connectionString);
const sourceDbClient = createDb(sourceDb.connectionString);
await sourceDbClient.$client.unsafe(`
DELETE FROM "drizzle"."__drizzle_migrations"
WHERE "id" = (
SELECT max("id") FROM "drizzle"."__drizzle_migrations"
);
WITH pair AS (
SELECT
array_agg("id" ORDER BY "id" DESC) AS ids,
array_agg("hash" ORDER BY "id" DESC) AS hashes
FROM (
SELECT "id", "hash"
FROM "drizzle"."__drizzle_migrations"
ORDER BY "id" DESC
LIMIT 2
) latest
)
UPDATE "drizzle"."__drizzle_migrations" migrations
SET "hash" = CASE
WHEN migrations."id" = pair.ids[1] THEN pair.hashes[2]
WHEN migrations."id" = pair.ids[2] THEN pair.hashes[1]
ELSE migrations."hash"
END
FROM pair
WHERE migrations."id" IN (pair.ids[1], pair.ids[2]);
INSERT INTO "drizzle"."__drizzle_migrations" ("hash", "created_at")
VALUES ('stale-unresolvable-migration-hash', 0)
`);
await sourceDbClient.$client.end({ timeout: 5 });
const laggingMigrationState = await inspectMigrations(sourceDb.connectionString);
expect(laggingMigrationState.status).toBe("needsMigrations");
if (laggingMigrationState.status !== "needsMigrations") {
throw new Error("Expected the source migration journal to lag the code journal");
}
expect(laggingMigrationState.pendingMigrations).toHaveLength(1);
const expectedAppliedPrefix = laggingMigrationState.availableMigrations.slice(
0,
laggingMigrationState.appliedMigrations.length,
await seedValidWorktreeSource(sourceDb.connectionString);
const sourceDbClient = createDb(sourceDb.connectionString);
await sourceDbClient.$client.unsafe(`
DELETE FROM "drizzle"."__drizzle_migrations"
WHERE "id" = (
SELECT max("id") FROM "drizzle"."__drizzle_migrations"
);
expect(laggingMigrationState.appliedMigrations).not.toEqual(expectedAppliedPrefix);
expect([...laggingMigrationState.appliedMigrations].sort()).toEqual(
[...expectedAppliedPrefix].sort(),
);
expect(laggingMigrationState.journalEntryCount).toBeGreaterThan(
laggingMigrationState.appliedMigrations.length,
);
const sourceMigrationRevision = expectedAppliedPrefix.at(-1);
expect(sourceMigrationRevision).toBeTruthy();
fs.mkdirSync(path.dirname(sourceKeyPath), { recursive: true });
fs.mkdirSync(worktreeRoot, { recursive: true });
WITH pair AS (
SELECT
array_agg("id" ORDER BY "id" DESC) AS ids,
array_agg("hash" ORDER BY "id" DESC) AS hashes
FROM (
SELECT "id", "hash"
FROM "drizzle"."__drizzle_migrations"
ORDER BY "id" DESC
LIMIT 2
) latest
)
UPDATE "drizzle"."__drizzle_migrations" migrations
SET "hash" = CASE
WHEN migrations."id" = pair.ids[1] THEN pair.hashes[2]
WHEN migrations."id" = pair.ids[2] THEN pair.hashes[1]
ELSE migrations."hash"
END
FROM pair
WHERE migrations."id" IN (pair.ids[1], pair.ids[2]);
const sourceConfig = buildSourceConfig();
sourceConfig.database = {
mode: "postgres",
embeddedPostgresDataDir: path.join(sourceConfigDir, "db"),
embeddedPostgresPort: 54329,
backup: {
enabled: true,
intervalMinutes: 60,
retentionDays: 30,
dir: path.join(sourceConfigDir, "backups"),
},
connectionString: sourceDb.connectionString,
};
sourceConfig.logging.logDir = path.join(sourceConfigDir, "logs");
sourceConfig.storage.localDisk.baseDir = path.join(sourceConfigDir, "storage");
sourceConfig.secrets.localEncrypted.keyFilePath = sourceKeyPath;
fs.writeFileSync(sourceConfigPath, JSON.stringify(sourceConfig, null, 2) + "\n", "utf8");
fs.writeFileSync(sourceEnvPath, "", "utf8");
fs.writeFileSync(sourceKeyPath, "source-master-key", "utf8");
process.chdir(worktreeRoot);
await worktreeInitCommand({
name: "PAP-999-auth-seed",
home: worktreeHome,
fromConfig: sourceConfigPath,
force: true,
});
const targetConfig = JSON.parse(
fs.readFileSync(path.join(worktreeRoot, ".paperclip", "config.json"), "utf8"),
) as PaperclipConfig;
const manifestText = fs.readFileSync(
path.join(worktreeRoot, ".paperclip", "seed-manifest.json"),
"utf8",
);
expect(JSON.parse(manifestText)).toMatchObject({
version: 2,
seedMode: "minimal",
state: "verified",
phase: "complete",
});
expect(manifestText).toContain(`Validated migration ${sourceMigrationRevision}`);
expect(manifestText).not.toContain("fixture-password-hash");
expect(manifestText).not.toContain("source-master-key");
const { default: EmbeddedPostgres } = await import("embedded-postgres");
const targetPg = new EmbeddedPostgres({
databaseDir: targetConfig.database.embeddedPostgresDataDir,
user: "paperclip",
password: "paperclip",
port: targetConfig.database.embeddedPostgresPort,
persistent: true,
initdbFlags: ["--encoding=UTF8", "--locale=C", "--lc-messages=C"],
onLog: () => {},
onError: () => {},
});
await targetPg.start();
try {
const targetDb = createDb(
`postgres://paperclip:paperclip@127.0.0.1:${targetConfig.database.embeddedPostgresPort}/paperclip`,
);
const seededUsers = await targetDb.select().from(authUsers);
expect(seededUsers.some((row) => row.email === "existing@paperclip.ing")).toBe(true);
} finally {
await targetPg.stop();
}
} finally {
process.chdir(originalCwd);
await sourceDb.cleanup();
fs.rmSync(tempRoot, { recursive: true, force: true });
INSERT INTO "drizzle"."__drizzle_migrations" ("hash", "created_at")
VALUES ('stale-unresolvable-migration-hash', 0)
`);
await sourceDbClient.$client.end({ timeout: 5 });
const laggingMigrationState = await inspectMigrations(sourceDb.connectionString);
expect(laggingMigrationState.status).toBe("needsMigrations");
if (laggingMigrationState.status !== "needsMigrations") {
throw new Error("Expected the source migration journal to lag the code journal");
}
expect(laggingMigrationState.pendingMigrations).toHaveLength(1);
const expectedAppliedPrefix = laggingMigrationState.availableMigrations.slice(
0,
laggingMigrationState.appliedMigrations.length,
);
expect(laggingMigrationState.appliedMigrations).not.toEqual(expectedAppliedPrefix);
expect([...laggingMigrationState.appliedMigrations].sort()).toEqual(
[...expectedAppliedPrefix].sort(),
);
expect(laggingMigrationState.journalEntryCount).toBeGreaterThan(
laggingMigrationState.appliedMigrations.length,
);
const sourceMigrationRevision = expectedAppliedPrefix.at(-1);
expect(sourceMigrationRevision).toBeTruthy();
fs.mkdirSync(path.dirname(sourceKeyPath), { recursive: true });
fs.mkdirSync(worktreeRoot, { recursive: true });
const sourceConfig = buildSourceConfig();
sourceConfig.database = {
mode: "postgres",
embeddedPostgresDataDir: path.join(sourceConfigDir, "db"),
embeddedPostgresPort: 54329,
backup: {
enabled: true,
intervalMinutes: 60,
retentionDays: 30,
dir: path.join(sourceConfigDir, "backups"),
},
connectionString: sourceDb.connectionString,
};
sourceConfig.logging.logDir = path.join(sourceConfigDir, "logs");
sourceConfig.storage.localDisk.baseDir = path.join(sourceConfigDir, "storage");
sourceConfig.secrets.localEncrypted.keyFilePath = sourceKeyPath;
fs.writeFileSync(sourceConfigPath, JSON.stringify(sourceConfig, null, 2) + "\n", "utf8");
fs.writeFileSync(sourceEnvPath, "", "utf8");
fs.writeFileSync(sourceKeyPath, "source-master-key", "utf8");
process.chdir(worktreeRoot);
await worktreeInitCommand({
name: "PAP-999-auth-seed",
home: worktreeHome,
fromConfig: sourceConfigPath,
force: true,
});
const targetConfig = JSON.parse(
fs.readFileSync(path.join(worktreeRoot, ".paperclip", "config.json"), "utf8"),
) as PaperclipConfig;
const manifestText = fs.readFileSync(
path.join(worktreeRoot, ".paperclip", "seed-manifest.json"),
"utf8",
);
expect(JSON.parse(manifestText)).toMatchObject({
version: 2,
seedMode: "minimal",
state: "verified",
phase: "complete",
});
expect(manifestText).toContain(`Validated migration ${sourceMigrationRevision}`);
expect(manifestText).not.toContain("fixture-password-hash");
expect(manifestText).not.toContain("source-master-key");
const { default: EmbeddedPostgres } = await import("embedded-postgres");
const targetPg = new EmbeddedPostgres({
databaseDir: targetConfig.database.embeddedPostgresDataDir,
user: "paperclip",
password: "paperclip",
port: targetConfig.database.embeddedPostgresPort,
persistent: true,
initdbFlags: ["--encoding=UTF8", "--locale=C", "--lc-messages=C"],
onLog: () => {},
onError: () => {},
});
await targetPg.start();
onTestFinished(() => targetPg.stop());
const targetDb = createDb(
`postgres://paperclip:paperclip@127.0.0.1:${targetConfig.database.embeddedPostgresPort}/paperclip`,
);
const seededUsers = await targetDb.select().from(authUsers);
expect(seededUsers.some((row) => row.email === "existing@paperclip.ing")).toBe(true);
},
30000,
);
it("avoids ports already claimed by sibling worktree instance configs", async () => {
@@ -2049,6 +2043,7 @@ describe("worktree helpers", () => {
const currentDatabaseReservation = await reserveTestPort();
const currentDatabasePort = currentDatabaseReservation.port;
const sourceDb = await startEmbeddedPostgresTestDatabase("paperclip-worktree-reseed-source-");
onTestFinished(() => sourceDb.cleanup());
try {
fs.mkdirSync(path.dirname(currentPaths.configPath), { recursive: true });
@@ -2122,7 +2117,6 @@ describe("worktree helpers", () => {
).toBe(true);
} finally {
await currentDatabaseReservation.release();
await sourceDb.cleanup();
process.chdir(originalCwd);
if (originalPaperclipConfig === undefined) {
delete process.env.PAPERCLIP_CONFIG;
@@ -2131,7 +2125,7 @@ describe("worktree helpers", () => {
}
fs.rmSync(tempRoot, { recursive: true, force: true });
}
}, 30_000);
});
it("restores the current worktree config and instance data if reseed fails", async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-reseed-rollback-"));
+1
View File
@@ -16,6 +16,7 @@ export {
export {
getEmbeddedPostgresTestSupport,
startEmbeddedPostgresTestDatabase,
EMBEDDED_POSTGRES_TEST_TIMEOUT_MS,
type EmbeddedPostgresTestDatabase,
type EmbeddedPostgresTestSupport,
} from "./test-embedded-postgres.js";
@@ -9,6 +9,14 @@ import {
} from "./embedded-postgres-error.js";
import { prepareEmbeddedPostgresNativeRuntime } from "./embedded-postgres-native.js";
// Time budget (ms) for a vitest test in the embedded-Postgres cost class: a
// test that starts an embedded Postgres cluster and runs migrations. Measured
// evidence: this cost class normally finishes in well under 10s. Under a
// contended CI runner the same test took up to 4.9x longer. This budget
// gives about 10x headroom over the clean time, so a contended run still
// passes while a genuine hang still fails fast.
export const EMBEDDED_POSTGRES_TEST_TIMEOUT_MS = 90_000;
type EmbeddedPostgresInstance = {
initialise(): Promise<void>;
start(): Promise<void>;