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.
113 lines
3.4 KiB
TypeScript
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);
|
|
});
|
|
});
|