Files
triggerdotdev--trigger.dev/apps/webapp/test/realtime/shadowCompare.test.ts
T
2026-07-13 13:32:57 +08:00

224 lines
6.9 KiB
TypeScript

import {
type RealtimeRunRow,
serializeRunRow,
} from "~/services/realtime/electricStreamProtocol.server";
import { type RunListFilter } from "~/services/realtime/runReader.server";
import { RealtimeShadowComparator } from "~/services/realtime/shadowCompare.server";
import { describe, expect, it } from "vitest";
function sampleRow(overrides: Partial<RealtimeRunRow> = {}): RealtimeRunRow {
return {
id: "run_a",
taskIdentifier: "my-task",
createdAt: new Date("2026-06-07T09:00:00.000Z"),
updatedAt: new Date("2026-06-07T10:05:30.123Z"),
startedAt: null,
delayUntil: null,
queuedAt: null,
expiredAt: null,
completedAt: null,
friendlyId: "run_friendly_a",
number: 7,
isTest: true,
status: "EXECUTING",
usageDurationMs: 1234,
costInCents: 0.55,
baseCostInCents: 0.25,
ttl: "1h",
payload: '{"hello":"world"}',
payloadType: "application/json",
metadata: null,
metadataType: "application/json",
output: null,
outputType: "application/json",
runTags: ["a", "b"],
error: null,
realtimeStreams: [],
...overrides,
};
}
const UP_TO_DATE = { headers: { control: "up-to-date" } };
function insert(value: Record<string, string | null>) {
return { key: `"public"."TaskRun"/"${value.id}"`, value, headers: { operation: "insert" } };
}
function makeComparator(
rowsById: Record<string, RealtimeRunRow | null>,
resolvedIds: string[] = []
) {
return new RealtimeShadowComparator({
runReader: {
getRunById: async (_env: string, id: string) => rowsById[id] ?? null,
hydrateByIds: async (_env: string, ids: string[]) =>
ids.map((id) => rowsById[id]).filter((row): row is RealtimeRunRow => Boolean(row)),
} as any,
runListResolver: { resolveMatchingRunIds: async (_f: RunListFilter) => resolvedIds } as any,
});
}
describe("RealtimeShadowComparator serialization", () => {
it("counts a faithful re-serialization as a match", async () => {
const row = sampleRow();
const body = JSON.stringify([insert(serializeRunRow(row)), UP_TO_DATE]);
const cmp = makeComparator({ run_a: row });
const out = await cmp.compare({
feed: "run",
electricBody: body,
environment: { id: "env_1" },
skipColumns: [],
isInitialSnapshot: true,
});
expect(out.serializationMatched).toBe(1);
expect(out.serializationDiverged).toBe(0);
expect(out.serializationSkew).toBe(0);
expect(out.diffs).toEqual([]);
});
it("does not flag semantically-equivalent but differently-encoded values", async () => {
const row = sampleRow();
// Electric encodes bool as "true" (native uses "t"), a number with a trailing
// zero, and a timestamp without millis — all equal after decoding.
const value = {
...serializeRunRow(row),
isTest: "true",
costInCents: "0.5500",
createdAt: "2026-06-07T09:00:00",
};
const body = JSON.stringify([insert(value), UP_TO_DATE]);
const cmp = makeComparator({ run_a: row });
const out = await cmp.compare({
feed: "run",
electricBody: body,
environment: { id: "env_1" },
skipColumns: [],
isInitialSnapshot: true,
});
expect(out.serializationMatched).toBe(1);
expect(out.serializationDiverged).toBe(0);
});
it("flags a genuine column divergence (same version)", async () => {
const row = sampleRow();
const value = { ...serializeRunRow(row), payload: '{"hello":"TAMPERED"}' };
const body = JSON.stringify([insert(value), UP_TO_DATE]);
const cmp = makeComparator({ run_a: row });
const out = await cmp.compare({
feed: "run",
electricBody: body,
environment: { id: "env_1" },
skipColumns: [],
isInitialSnapshot: true,
});
expect(out.serializationDiverged).toBe(1);
expect(out.serializationMatched).toBe(0);
expect(out.diffs).toEqual([
{
runId: "run_a",
column: "payload",
electric: '{"hello":"TAMPERED"}',
native: '{"hello":"world"}',
},
]);
});
it("treats DEQUEUED/EXECUTING as equivalent (legacy status rewrite)", async () => {
const row = sampleRow({ status: "EXECUTING" });
const value = { ...serializeRunRow(row), status: "DEQUEUED" };
const body = JSON.stringify([insert(value), UP_TO_DATE]);
const cmp = makeComparator({ run_a: row });
const out = await cmp.compare({
feed: "run",
electricBody: body,
environment: { id: "env_1" },
skipColumns: [],
isInitialSnapshot: true,
});
expect(out.serializationDiverged).toBe(0);
expect(out.serializationMatched).toBe(1);
});
it("records skew when the row advanced between emit and refetch", async () => {
const row = sampleRow();
// Electric emitted an older version; the refetched row is newer.
const value = {
...serializeRunRow(sampleRow({ updatedAt: new Date("2026-06-07T10:00:00.000Z") })),
};
const body = JSON.stringify([insert(value), UP_TO_DATE]);
const cmp = makeComparator({ run_a: row });
const out = await cmp.compare({
feed: "run",
electricBody: body,
environment: { id: "env_1" },
skipColumns: [],
isInitialSnapshot: true,
});
expect(out.serializationSkew).toBe(1);
expect(out.serializationMatched).toBe(0);
expect(out.serializationDiverged).toBe(0);
});
});
describe("RealtimeShadowComparator membership", () => {
const filter: RunListFilter = {
organizationId: "org_1",
projectId: "proj_1",
environmentId: "env_1",
tags: ["t"],
createdAtAfter: new Date("2026-06-06T00:00:00.000Z"),
limit: 1000,
};
function bodyFor(ids: string[]) {
const msgs = ids.map((id) => insert(serializeRunRow(sampleRow({ id }))));
return JSON.stringify([...msgs, UP_TO_DATE]);
}
it("matches when Electric's set equals the native resolver's set", async () => {
const cmp = makeComparator({ a: sampleRow({ id: "a" }), b: sampleRow({ id: "b" }) }, [
"a",
"b",
]);
const out = await cmp.compare({
feed: "runs",
electricBody: bodyFor(["a", "b"]),
environment: { id: "env_1" },
skipColumns: [],
isInitialSnapshot: true,
membershipFilter: filter,
});
expect(out.membershipMatch).toBe(true);
expect(out.missingInNative).toEqual([]);
expect(out.extraInNative).toEqual([]);
});
it("reports rows missing from / extra in the native resolution", async () => {
const cmp = makeComparator(
{ a: sampleRow({ id: "a" }), b: sampleRow({ id: "b" }) },
["a", "c"] // native missing b, has extra c
);
const out = await cmp.compare({
feed: "runs",
electricBody: bodyFor(["a", "b"]),
environment: { id: "env_1" },
skipColumns: [],
isInitialSnapshot: true,
membershipFilter: filter,
});
expect(out.membershipMatch).toBe(false);
expect(out.missingInNative).toEqual(["b"]);
expect(out.extraInNative).toEqual(["c"]);
});
});