Files
triggerdotdev--trigger.dev/apps/webapp/test/sessionsReplicationService.test.ts
James Ritchie a90a495542 feat(webapp,database): show a Test column for agent sessions (#4011)
## Summary

Sessions started from the agent Test playground were tagged with a
`"playground"` tag that rendered in the Sessions table's Tags column.
They are now flagged with a real `Session.isTest` boolean (mirroring
`TaskRun.isTest`) and surfaced as a dedicated **Test** column with a
check icon, to the left of Tags, on both the Sessions page and the Agent
landing page, plus a matching **Test** property on the session detail
page. This mirrors how Standard and Scheduled task runs already indicate
test runs.

## Design

`isTest` is a new `Session` column (Postgres) replicated into ClickHouse
`sessions_v1` alongside the existing fields. The Sessions list reads
`isTest` from Postgres for display (ClickHouse only supplies the ordered
session IDs), so the column renders correctly without a ClickHouse
backfill.

The playground action now sets `isTest: true` on session create instead
of writing the `"playground"` tag. The triggered run still carries
`playground:true` in its own tags (unchanged). A migration backfills
existing sessions, setting `isTest = true` and stripping the
now-redundant `"playground"` tag where it is present, so the list and
detail views render consistently without read-time tag filtering.
2026-06-22 15:30:34 +01:00

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