Files
zopu-code/packages/backend/convex/conversationMessages.test.ts
-Puter a5414d83bd Convex-only Slice 1: clients talk to Convex, Flue is a private worker
Normalize the topology so web/desktop/mobile clients communicate only with
Convex. Convex admits durable commands, dispatches private Flue turns through
FLUE_URL, and stores the product-facing result before clients observe it.

- Normalized relational schema: organizations, projects, conversations/turns/
  messages/attachments, signals/sources/constraints, works/events/attachments.
- Conversation turns queued in Convex; the agent runAction dispatches to Flue
  and persists assistant response, signals, and proposed work back into Convex.
- Browser chat moved off the Flue transport: use-chat-agent now reads reactive
  Convex rows and sends through conversationMessages.send.
- Fixed signed-in blank screen: gate all product queries on useConvexAuth
  (isAuthenticated && !isRefreshing) plus personal-org bootstrap.
- Pinned react/react-dom to 19.2.8 via catalog to fix SSR dual-React crash.
- Removed ~16.6k net lines: daemon, AgentOS/Orb, project issues/runs/artifacts,
  todos, browser Flue transport, mobile execution views, smoke scripts.

Verified end-to-end on cheaptricks: connect repo, send actionable message,
Flue turn completes, Signal + proposed Work created with exact provenance,
persists across refresh.
2026-07-27 21:32:17 +05:30

113 lines
3.4 KiB
TypeScript

import { convexTest } from "convex-test";
import { anyApi } 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 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);
});
});