Files
claudemesh/apps/broker/tests/helpers.ts
Alejandro Gutiérrez e25115f1b0 test(broker): port test suite from claude-intercom to drizzle/postgres
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>
2026-04-04 22:09:06 +01:00

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),
),
);
}