Files
zopu-code/packages/backend/convex/conversationMessages.test.ts
-Puter a8d946b6a9 Slice 4 control-plane repairs, lint/catalog cleanup, Flue adapter fixes
Convex control plane (Slices 1-4 prerequisites for Slice 5):
- Versioned Definition/Design persistence: revisions preserve history
  instead of deleting prior slices/approvals
- Fenced conversation turn queue: attempt-numbered leases prevent stale
  worker overwrites; expired turns reconciled by cron
- Fenced Run/Attempt claiming: only queued attempts from running Runs can
  be claimed; lease expiry checks on every finish/checkpoint
- Multi-slice progression: successful slice marks next slice ready instead
  of completing the whole Work; completed Runs blocked from retry
- Typed AttemptClassification in schema and resolver decisions table
- Durable resolver decisions for normal outcomes, cancellation, and
  lease-expiry recovery
- Persistent artifacts and delivery metadata with provenance, source
  revision, verification status, and controlled delivery transitions
- High-impact open questions block definition approval
- Question submission gated to defining/awaiting-approval states only
- Delivery status updates validate JSON metadata before persisting
- Planner failures recorded as durable blocked events
- Approval identity uses canonical tokenIdentifier

Primitives:
- decodeDefinition separates decode from validate
- Design validation enforces 1-4 slices with unique IDs and valid deps
- Artifact and delivery draft schemas with verified-revision binding
- Delivery status transition table

Package management:
- Consolidated shared deps into single catalog (hono, valibot, streamdown,
  @types/react, @types/node, @tailwindcss/*, react-native)
- Removed cross-version Hono boundary: flue() exported as Fetchable
- Deleted bun.lock and regenerated from clean catalog resolution

Lint/format:
- Removed .eslintignore; oxc uses native ignorePatterns in oxlint.config.ts
- Deleted redundant packages/backend/.oxlintrc.json
- Added repos/** and scripts/** to ignore patterns
- Convex-specific rule overrides for ES2022 target constraints
- Fixed all oxc errors in fluePersistence.ts and agents/src/db.ts
- Fixed all formatting across changed files
2026-07-28 02:53:05 +05:30

202 lines
5.6 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 completeTurnRef = makeFunctionReference<
"mutation",
{
turnId: string;
attempt: number;
leaseOwner: string;
submissionId: string;
text: string;
},
boolean
>("conversationMessages:completeTurn");
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 turn attempts from overwriting a retry", 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 current response",
});
expect(
await t.mutation(markProcessingRef, {
attempt: 1,
leaseOwner: "worker-1",
turnId: sent.turnId,
})
).toBe(true);
expect(
await t.mutation(failTurnRef, {
attempt: 1,
error: "retry",
leaseOwner: "worker-1",
retry: true,
turnId: sent.turnId,
})
).toBe(true);
expect(
await t.mutation(completeTurnRef, {
attempt: 1,
leaseOwner: "worker-1",
submissionId: "stale",
text: "stale response",
turnId: sent.turnId,
})
).toBe(false);
expect(
await t.mutation(markProcessingRef, {
attempt: 2,
leaseOwner: "worker-2",
turnId: sent.turnId,
})
).toBe(true);
expect(
await t.mutation(completeTurnRef, {
attempt: 2,
leaseOwner: "worker-2",
submissionId: "current",
text: "current response",
turnId: sent.turnId,
})
).toBe(true);
const messages = await t
.withIdentity(identityA)
.query(api.conversationMessages.listForCurrentOrganization, {
organizationId: organization._id,
});
expect(messages[1]?.rawText).toBe("current response");
});
});