21 integration tests (14 broker behavior + 7 path encoding), all passing in ~1s against a real Postgres (claudemesh_test database on the dev container). Test infrastructure: - apps/broker/vitest.config.ts extends @turbostarter/vitest-config/base - tests/helpers.ts: setupTestMesh() creates a fresh mesh + 2 members per test with a unique slug, returns cleanup function that cascades the delete. cleanupAllTestMeshes() as an afterAll safety net. - Mesh isolation in broker logic means tests don't interfere even when they share a database — no per-test TRUNCATE needed. Ported behavior tests (broker.test.ts, 14 tests): - hook flips status + queued "next" messages unblock - "now"-priority bypasses the working gate - DND is sacred (hooks cannot unset it) - hook source stays fresh through jsonl refresh - source decays to jsonl when hook signal goes stale - isHookFresh freshness window + source-type rules - TTL sweep flips stuck "working" → idle - TTL sweep leaves DND alone - first-turn race: hook fired pre-connect stashed in pending_status - applyPendingHookStatus picks newest matching entry - expired pending entries are ignored on connect - broadcast targetSpec (*) reaches all members - pubkey mismatch → message not drained - mesh isolation: peer in mesh X doesn't drain from mesh Y Ported encoding tests (encoding.test.ts, 7 tests): - macOS, Linux, Windows path encoding first-candidate correctness - Roberto's H:\Claude → H--Claude regression test (2026-04-04) - Candidate dedup, drive-stripped fallback, leading-dash fallback How to run: from apps/broker, DATABASE_URL="postgresql://.../claudemesh_test" pnpm test Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
120 lines
3.3 KiB
TypeScript
120 lines
3.3 KiB
TypeScript
/**
|
|
* Test helpers for broker integration tests.
|
|
*
|
|
* Each test gets its own fresh mesh + members via `setupTestMesh`.
|
|
* Mesh isolation in the broker logic means tests don't interfere even
|
|
* when they share a database and run in the same process — we just
|
|
* need unique meshIds per test.
|
|
*/
|
|
|
|
import { eq, inArray } from "drizzle-orm";
|
|
import { db } from "../src/db";
|
|
import { mesh, meshMember } from "@turbostarter/db/schema/mesh";
|
|
import { user } from "@turbostarter/db/schema/auth";
|
|
import { randomBytes } from "node:crypto";
|
|
|
|
const TEST_USER_ID = "test-user-integration";
|
|
|
|
/**
|
|
* Shared test user. Created once, reused across tests.
|
|
* Uses a deterministic id so we can safely cascade-delete on cleanup.
|
|
*/
|
|
export async function ensureTestUser(): Promise<string> {
|
|
const [existing] = await db
|
|
.select({ id: user.id })
|
|
.from(user)
|
|
.where(eq(user.id, TEST_USER_ID));
|
|
if (!existing) {
|
|
await db.insert(user).values({
|
|
id: TEST_USER_ID,
|
|
name: "Broker Test User",
|
|
email: "broker-test@claudemesh.test",
|
|
emailVerified: true,
|
|
});
|
|
}
|
|
return TEST_USER_ID;
|
|
}
|
|
|
|
export interface TestMesh {
|
|
meshId: string;
|
|
peerA: { memberId: string; pubkey: string };
|
|
peerB: { memberId: string; pubkey: string };
|
|
cleanup: () => Promise<void>;
|
|
}
|
|
|
|
/**
|
|
* Create a test mesh + 2 members. Returns IDs + pubkeys and a
|
|
* cleanup function that cascade-deletes the mesh (and all presence,
|
|
* message_queue, member rows that reference it).
|
|
*/
|
|
export async function setupTestMesh(label: string): Promise<TestMesh> {
|
|
const userId = await ensureTestUser();
|
|
const slug = `t-${label}-${randomBytes(4).toString("hex")}`;
|
|
|
|
const [m] = await db
|
|
.insert(mesh)
|
|
.values({
|
|
name: `Test ${label}`,
|
|
slug,
|
|
ownerUserId: userId,
|
|
visibility: "private",
|
|
transport: "managed",
|
|
tier: "free",
|
|
})
|
|
.returning({ id: mesh.id });
|
|
if (!m) throw new Error("failed to insert test mesh");
|
|
|
|
const pubkeyA = "a".repeat(63) + randomBytes(1).toString("hex").slice(0, 1);
|
|
const pubkeyB = "b".repeat(63) + randomBytes(1).toString("hex").slice(0, 1);
|
|
|
|
const [mA] = await db
|
|
.insert(meshMember)
|
|
.values({
|
|
meshId: m.id,
|
|
userId,
|
|
peerPubkey: pubkeyA,
|
|
displayName: `peer-a-${label}`,
|
|
role: "admin",
|
|
})
|
|
.returning({ id: meshMember.id });
|
|
const [mB] = await db
|
|
.insert(meshMember)
|
|
.values({
|
|
meshId: m.id,
|
|
userId,
|
|
peerPubkey: pubkeyB,
|
|
displayName: `peer-b-${label}`,
|
|
role: "member",
|
|
})
|
|
.returning({ id: meshMember.id });
|
|
if (!mA || !mB) throw new Error("failed to insert test members");
|
|
|
|
return {
|
|
meshId: m.id,
|
|
peerA: { memberId: mA.id, pubkey: pubkeyA },
|
|
peerB: { memberId: mB.id, pubkey: pubkeyB },
|
|
cleanup: async () => {
|
|
// Cascade delete takes care of members, presences, message_queue.
|
|
await db.delete(mesh).where(eq(mesh.id, m.id));
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Delete all meshes with slugs starting with "t-" (test prefix).
|
|
* Used as a safety net in afterAll if individual cleanup() didn't run.
|
|
*/
|
|
export async function cleanupAllTestMeshes(): Promise<void> {
|
|
const testMeshes = await db
|
|
.select({ id: mesh.id })
|
|
.from(mesh)
|
|
.where(eq(mesh.ownerUserId, TEST_USER_ID));
|
|
if (testMeshes.length === 0) return;
|
|
await db.delete(mesh).where(
|
|
inArray(
|
|
mesh.id,
|
|
testMeshes.map((m) => m.id),
|
|
),
|
|
);
|
|
}
|