216 lines
6.8 KiB
TypeScript
216 lines
6.8 KiB
TypeScript
import { ClickHouse } from "@internal/clickhouse";
|
|
import { replicationContainerTest } from "@internal/testcontainers";
|
|
import { setTimeout } from "node:timers/promises";
|
|
import { z } from "zod";
|
|
import { SessionsReplicationService } from "~/services/sessionsReplicationService.server";
|
|
import { TestReplicationClickhouseFactory } from "./utils/testReplicationClickhouseFactory";
|
|
|
|
vi.setConfig({ testTimeout: 60_000 });
|
|
|
|
describe("SessionsReplicationService", () => {
|
|
replicationContainerTest(
|
|
"replicates an insert from Postgres Session → ClickHouse sessions_v1",
|
|
async ({ clickhouseContainer, redisOptions, postgresContainer, prisma }) => {
|
|
// Logical replication needs full-row images for DELETE events.
|
|
await prisma.$executeRawUnsafe(`ALTER TABLE public."Session" REPLICA IDENTITY FULL;`);
|
|
|
|
const clickhouse = new ClickHouse({
|
|
url: clickhouseContainer.getConnectionUrl(),
|
|
name: "sessions-replication",
|
|
compression: { request: true },
|
|
logLevel: "warn",
|
|
});
|
|
|
|
const service = new SessionsReplicationService({
|
|
clickhouseFactory: new TestReplicationClickhouseFactory(clickhouse),
|
|
pgConnectionUrl: postgresContainer.getConnectionUri(),
|
|
serviceName: "sessions-replication",
|
|
slotName: "sessions_to_clickhouse_v1",
|
|
publicationName: "sessions_to_clickhouse_v1_publication",
|
|
redisOptions,
|
|
maxFlushConcurrency: 1,
|
|
flushIntervalMs: 100,
|
|
flushBatchSize: 1,
|
|
leaderLockTimeoutMs: 5000,
|
|
leaderLockExtendIntervalMs: 1000,
|
|
ackIntervalSeconds: 5,
|
|
logLevel: "warn",
|
|
});
|
|
|
|
await service.start();
|
|
|
|
const organization = await prisma.organization.create({
|
|
data: { title: "test", slug: "test" },
|
|
});
|
|
|
|
const project = await prisma.project.create({
|
|
data: {
|
|
name: "test",
|
|
slug: "test",
|
|
organizationId: organization.id,
|
|
externalRef: "test",
|
|
},
|
|
});
|
|
|
|
const environment = await prisma.runtimeEnvironment.create({
|
|
data: {
|
|
slug: "test",
|
|
type: "DEVELOPMENT",
|
|
projectId: project.id,
|
|
organizationId: organization.id,
|
|
apiKey: "test",
|
|
pkApiKey: "test",
|
|
shortcode: "test",
|
|
},
|
|
});
|
|
|
|
const session = await prisma.session.create({
|
|
data: {
|
|
id: "session_test_insert_1",
|
|
friendlyId: "session_abc123",
|
|
externalId: "my-test-session",
|
|
type: "chat.agent",
|
|
projectId: project.id,
|
|
runtimeEnvironmentId: environment.id,
|
|
environmentType: "DEVELOPMENT",
|
|
organizationId: organization.id,
|
|
taskIdentifier: "my-agent",
|
|
triggerConfig: {
|
|
basePayload: { messages: [], trigger: "preload" },
|
|
},
|
|
tags: ["user:42", "plan:pro"],
|
|
metadata: { plan: "pro", seats: 3 },
|
|
isTest: true,
|
|
},
|
|
});
|
|
|
|
// Allow the replication pipeline to flush
|
|
await setTimeout(2000);
|
|
|
|
const querySessions = clickhouse.reader.query({
|
|
name: "read-sessions",
|
|
query: "SELECT * FROM trigger_dev.sessions_v1 FINAL",
|
|
schema: z.any(),
|
|
});
|
|
|
|
const [queryError, result] = await querySessions({});
|
|
|
|
expect(queryError).toBeNull();
|
|
expect(result?.length).toBe(1);
|
|
expect(result?.[0]).toEqual(
|
|
expect.objectContaining({
|
|
session_id: session.id,
|
|
friendly_id: session.friendlyId,
|
|
external_id: "my-test-session",
|
|
type: "chat.agent",
|
|
project_id: project.id,
|
|
environment_id: environment.id,
|
|
organization_id: organization.id,
|
|
environment_type: "DEVELOPMENT",
|
|
task_identifier: "my-agent",
|
|
tags: ["user:42", "plan:pro"],
|
|
is_test: 1,
|
|
_is_deleted: 0,
|
|
})
|
|
);
|
|
|
|
await service.stop();
|
|
}
|
|
);
|
|
|
|
replicationContainerTest(
|
|
"replicates an update (close) from Postgres → ClickHouse",
|
|
async ({ clickhouseContainer, redisOptions, postgresContainer, prisma }) => {
|
|
await prisma.$executeRawUnsafe(`ALTER TABLE public."Session" REPLICA IDENTITY FULL;`);
|
|
|
|
const clickhouse = new ClickHouse({
|
|
url: clickhouseContainer.getConnectionUrl(),
|
|
name: "sessions-replication",
|
|
compression: { request: true },
|
|
logLevel: "warn",
|
|
});
|
|
|
|
const service = new SessionsReplicationService({
|
|
clickhouseFactory: new TestReplicationClickhouseFactory(clickhouse),
|
|
pgConnectionUrl: postgresContainer.getConnectionUri(),
|
|
serviceName: "sessions-replication",
|
|
slotName: "sessions_to_clickhouse_v1",
|
|
publicationName: "sessions_to_clickhouse_v1_publication",
|
|
redisOptions,
|
|
maxFlushConcurrency: 1,
|
|
flushIntervalMs: 100,
|
|
flushBatchSize: 1,
|
|
leaderLockTimeoutMs: 5000,
|
|
leaderLockExtendIntervalMs: 1000,
|
|
ackIntervalSeconds: 5,
|
|
logLevel: "warn",
|
|
});
|
|
|
|
await service.start();
|
|
|
|
const organization = await prisma.organization.create({
|
|
data: { title: "test", slug: "test" },
|
|
});
|
|
const project = await prisma.project.create({
|
|
data: {
|
|
name: "test",
|
|
slug: "test",
|
|
organizationId: organization.id,
|
|
externalRef: "test",
|
|
},
|
|
});
|
|
const environment = await prisma.runtimeEnvironment.create({
|
|
data: {
|
|
slug: "test",
|
|
type: "DEVELOPMENT",
|
|
projectId: project.id,
|
|
organizationId: organization.id,
|
|
apiKey: "test",
|
|
pkApiKey: "test",
|
|
shortcode: "test",
|
|
},
|
|
});
|
|
|
|
const created = await prisma.session.create({
|
|
data: {
|
|
id: "session_test_update_1",
|
|
friendlyId: "session_update1",
|
|
type: "chat.agent",
|
|
projectId: project.id,
|
|
runtimeEnvironmentId: environment.id,
|
|
environmentType: "DEVELOPMENT",
|
|
organizationId: organization.id,
|
|
taskIdentifier: "my-agent",
|
|
triggerConfig: {
|
|
basePayload: { messages: [], trigger: "preload" },
|
|
},
|
|
},
|
|
});
|
|
|
|
await setTimeout(1000);
|
|
|
|
await prisma.session.update({
|
|
where: { id: created.id },
|
|
data: { closedAt: new Date(), closedReason: "test-close" },
|
|
});
|
|
|
|
await setTimeout(2000);
|
|
|
|
const querySessions = clickhouse.reader.query({
|
|
name: "read-sessions-closed",
|
|
query: "SELECT closed_reason, closed_at FROM trigger_dev.sessions_v1 FINAL",
|
|
schema: z.any(),
|
|
});
|
|
|
|
const [queryError, result] = await querySessions({});
|
|
|
|
expect(queryError).toBeNull();
|
|
expect(result?.length).toBe(1);
|
|
expect(result?.[0].closed_reason).toBe("test-close");
|
|
expect(result?.[0].closed_at).toBeDefined();
|
|
|
|
await service.stop();
|
|
}
|
|
);
|
|
});
|