98 lines
3.7 KiB
TypeScript
98 lines
3.7 KiB
TypeScript
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
|
// `ConusAI-Txid` (umbrella TS6.3): a write's answer carries the transaction id it committed under,
|
|
// which the browser's live collections await before they let go of an optimistic row; a read, a
|
|
// write that committed nothing and a refused write carry none. The kernel's caller is stubbed with
|
|
// one that commits the way kit-data-kysely does inside a capture (`recordTxid` after the commit);
|
|
// the capture itself is proven against a real Postgres in @conusai-kit/data-kysely's own gate.
|
|
|
|
import type { Principal } from "@conusai-kit/auth"
|
|
import { AppError } from "@conusai-kit/core"
|
|
import { recordTxid } from "@conusai-kit/data-kysely"
|
|
import { describe, expect, it, vi } from "vitest"
|
|
|
|
const route = (kind: "read" | "write", scope: "project" | "user" = "project") => ({
|
|
module: "notes",
|
|
kind,
|
|
scope,
|
|
permission: "notes.note.read",
|
|
idempotent: false,
|
|
tool: false,
|
|
download: false,
|
|
})
|
|
|
|
vi.mock("../apps/web/src/server/app", () => ({
|
|
convexCapabilityStatus: async () => ({ ready: true }),
|
|
webRuntime: () => ({
|
|
registry: {
|
|
router: {
|
|
"notes.note.list": route("read"),
|
|
"notes.note.create": route("write"),
|
|
"notes.note.noop": route("write"),
|
|
"notes.note.refused": route("write"),
|
|
},
|
|
},
|
|
platform: { audit: { log: async () => undefined }, usage: { track: () => undefined } },
|
|
observe: {
|
|
span: (_name: string, _attributes: unknown, run: () => unknown) => run(),
|
|
log: { info: () => undefined, error: () => undefined },
|
|
captureException: () => undefined,
|
|
},
|
|
}),
|
|
}))
|
|
|
|
const principal: Principal = { actor: { userId: "u-txid" }, method: "bearer" } as Principal
|
|
|
|
vi.mock("../apps/web/src/server/request", () => ({
|
|
requestContext: async () => ({
|
|
principal,
|
|
caller: {
|
|
callByName: async (name: string) => {
|
|
// Two commits, as a write with an outbox event in a second transaction makes: the higher wins.
|
|
if (name === "notes.note.create") {
|
|
await Promise.resolve()
|
|
recordTxid("7001")
|
|
recordTxid("7003")
|
|
}
|
|
if (name === "notes.note.refused") {
|
|
recordTxid("9999")
|
|
throw new AppError({ type: "/problems/conflict", title: "Conflict", status: 409, code: "conflict" })
|
|
}
|
|
return { ok: true }
|
|
},
|
|
},
|
|
}),
|
|
}))
|
|
|
|
async function post(name: string): Promise<Response> {
|
|
const { handle } = await import("../apps/web/src/server/router")
|
|
return handle(
|
|
new Request(`https://app.example.test/api/v1/${name}`, {
|
|
method: "POST",
|
|
headers: { "content-type": "application/json", authorization: "Bearer stub" },
|
|
body: JSON.stringify({ org: "o", workspace: "w", project: "p" }),
|
|
}),
|
|
)
|
|
}
|
|
|
|
describe("ConusAI-Txid", () => {
|
|
it("is the highest transaction id a write committed", async () => {
|
|
const answer = await post("notes.note.create")
|
|
expect(answer.status).toBe(200)
|
|
expect(answer.headers.get("conusai-txid")).toBe("7003")
|
|
})
|
|
|
|
it("is absent from a read, from a write that committed nothing and from a refused write", async () => {
|
|
expect((await post("notes.note.list")).headers.get("conusai-txid")).toBeNull()
|
|
expect((await post("notes.note.noop")).headers.get("conusai-txid")).toBeNull()
|
|
const refused = await post("notes.note.refused")
|
|
expect(refused.status).toBe(409)
|
|
expect(refused.headers.get("conusai-txid")).toBeNull()
|
|
})
|
|
|
|
it("does not leak one request's id into the next", async () => {
|
|
const [first, second] = await Promise.all([post("notes.note.create"), post("notes.note.noop")])
|
|
expect(first.headers.get("conusai-txid")).toBe("7003")
|
|
expect(second.headers.get("conusai-txid")).toBeNull()
|
|
})
|
|
})
|