Files
zopu-code/packages/backend/convex/conversationMessages.test.ts
2026-07-29 08:19:17 +05:30

172 lines
4.9 KiB
TypeScript

import { convexTest } from "convex-test";
import { anyApi, makeFunctionReference } from "convex/server";
import { describe, expect, test } from "vitest";
import schema from "./schema";
declare global {
interface ImportMeta {
readonly glob: (pattern: string) => Record<string, () => Promise<unknown>>;
}
}
const modules = import.meta.glob("./**/*.ts");
const api = anyApi;
const identityA = { tokenIdentifier: "https://convex.test|user-a" };
const identityB = { tokenIdentifier: "https://convex.test|user-b" };
const newTest = () => convexTest({ modules, schema });
const markProcessingRef = makeFunctionReference<
"mutation",
{ turnId: string; attempt: number; leaseOwner: string },
boolean
>("conversationMessages:markProcessing");
const failTurnRef = makeFunctionReference<
"mutation",
{
turnId: string;
attempt: number;
leaseOwner: string;
error: string;
retry: boolean;
},
boolean
>("conversationMessages:failTurn");
const ensureOrg = async (
t: ReturnType<typeof newTest>,
identity: { readonly tokenIdentifier: string }
) =>
await t
.withIdentity(identity)
.mutation(api.organizations.ensurePersonalOrganization, {});
describe("conversationMessages", () => {
test("queues one durable user turn and one assistant placeholder", async () => {
const t = newTest();
const organization = await ensureOrg(t, identityA);
await t.withIdentity(identityA).mutation(api.conversationMessages.send, {
clientRequestId: "request-1",
images: [],
organizationId: organization._id,
rawText: " Keep this exact text. ",
});
const rows = await t
.withIdentity(identityA)
.query(api.conversationMessages.listForCurrentOrganization, {
organizationId: organization._id,
});
expect(rows).toHaveLength(2);
expect(rows[0]).toMatchObject({
rawText: " Keep this exact text. ",
role: "user",
status: "queued",
});
expect(rows[1]).toMatchObject({
rawText: "",
role: "assistant",
status: "queued",
});
});
test("is idempotent by client request id", async () => {
const t = newTest();
const organization = await ensureOrg(t, identityA);
const input = {
clientRequestId: "request-duplicate",
images: [],
organizationId: organization._id,
rawText: "First payload wins",
};
const first = await t
.withIdentity(identityA)
.mutation(api.conversationMessages.send, input);
const second = await t
.withIdentity(identityA)
.mutation(api.conversationMessages.send, {
...input,
rawText: "Ignored duplicate payload",
});
expect(second).toEqual(first);
const rows = await t
.withIdentity(identityA)
.query(api.conversationMessages.listForCurrentOrganization, {
organizationId: organization._id,
});
expect(rows).toHaveLength(2);
expect(rows[0]?.rawText).toBe("First payload wins");
});
test("rejects unauthenticated and cross-organization access", async () => {
const t = newTest();
const organization = await ensureOrg(t, identityA);
await ensureOrg(t, identityB);
const input = {
clientRequestId: "request-private",
images: [],
organizationId: organization._id,
rawText: "Private",
};
await expect(
t.mutation(api.conversationMessages.send, input)
).rejects.toThrow(/Authentication required/u);
await expect(
t.withIdentity(identityB).mutation(api.conversationMessages.send, input)
).rejects.toThrow(/Organization membership required/u);
await expect(
t
.withIdentity(identityB)
.query(api.conversationMessages.listForCurrentOrganization, {
organizationId: organization._id,
})
).rejects.toThrow(/Organization membership required/u);
});
test("fences stale dispatch failures after admission", async () => {
const t = newTest();
const organization = await ensureOrg(t, identityA);
const sent = await t
.withIdentity(identityA)
.mutation(api.conversationMessages.send, {
clientRequestId: "request-fenced",
images: [],
organizationId: organization._id,
rawText: "Keep only the admitted submission",
});
expect(
await t.mutation(markProcessingRef, {
attempt: 1,
leaseOwner: "worker-1",
turnId: sent.turnId,
})
).toBe(true);
await t.run(async (ctx) => {
await ctx.db.patch(sent.turnId, {
leaseExpiresAt: undefined,
leaseOwner: undefined,
status: "running",
submissionId: "submission-1",
});
});
expect(
await t.mutation(failTurnRef, {
attempt: 1,
error: "lost 202",
leaseOwner: "worker-1",
retry: true,
turnId: sent.turnId,
})
).toBe(false);
expect(await t.run((ctx) => ctx.db.get(sent.turnId))).toMatchObject({
status: "running",
submissionId: "submission-1",
});
});
});