// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0 // A module event delivered through the transactional outbox, end to end: `notes.NoteCreated` is written by // the transaction that writes the note, the relay hands it to the notes module's subscriber in the relay's // own transaction (`NOTIFICATIONS_REPOS`), and the subscriber tells the project's other members through the // notifications kit. What is proven here: one inbox row per record and recipient, also when a delivery is // repeated (the record-derived key), never for the author, and none when the note's transaction rolls back. // The relay's own exactly-once claim (Postgres-family profiles) is proven in kit-jobs-pgboss, not here. import { type NoteView, notesModule } from "@app/modules/notes" import type { EventContext, TenantScope } from "@conusai-kit/core" import { systemActor } from "@conusai-kit/core/worker" import type { DataSource } from "@conusai-kit/data" import { FILES_SCOPE } from "@conusai-kit/files" import { NOTIFICATIONS_REPOS } from "@conusai-kit/notifications" import { describe, expect, it } from "vitest" import { notesDeps } from "../modules/notes/adapters/procedures.js" import { createNote } from "../modules/notes/core/notes.js" import { makeNotesApp, type NotesApp, type SeededProject } from "./support/notes-app.js" interface Inbox { items: Array<{ code: string; category: string; key: string; title: string | null; href: string | null }> } /** A restricted project: its creator is its first member, and the second person is added as an editor. */ async function restrictedProject(app: NotesApp) { const seeded = await app.seedProject("acme", "owner@acme.test") await seeded.call.callByName("tenancy.project.create", { org: seeded.slugs.org, workspace: seeded.slugs.workspace, slug: "launch", name: "Launch", moduleKey: "notes", visibility: "restricted", }) const slugs = { ...seeded.slugs, project: "launch" } const member = await app.addMember(seeded, "member@acme.test", "member") await seeded.call.callByName("tenancy.project.set-member", { ...slugs, userId: member.actor.userId, roleKey: "editor", }) return { seeded, slugs, member } } /** The notes module's entries of a person's inbox; the kit's own (an organization ready, an invitation accepted) are not. */ const inboxOf = async (call: SeededProject["call"], org: string) => { const inbox = (await call.callByName("notifications.inbox.list", { org })) as Inbox return { items: inbox.items.filter((item) => item.key.startsWith("notes:")) } } describe("notes.NoteCreated, through the outbox, to the project's other members", () => { it("gives the project's other member one inbox row with a link to the note, and the author none", async () => { const app = makeNotesApp() const { seeded, slugs, member } = await restrictedProject(app) const note = (await member.call.callByName("notes.note.create", { ...slugs, title: "Kick-off", body: "", })) as NoteView // Nothing until the relay runs: the event is a record of the note's transaction, not a side effect of the request. expect((await inboxOf(seeded.call, slugs.org)).items).toEqual([]) await app.deliver() expect((await inboxOf(seeded.call, slugs.org)).items).toEqual([ expect.objectContaining({ category: "notice", code: "custom", key: `notes:${note.noteId}:created`, title: "New note in Launch", href: `/${slugs.org}/${slugs.workspace}/${slugs.project}/notes/${note.noteId}`, }), ]) expect((await inboxOf(member.call, slugs.org)).items).toEqual([]) // The record is published: a second pass delivers nothing, and the inbox still holds one row. expect(await app.deliver()).toBe(0) expect((await inboxOf(seeded.call, slugs.org)).items).toHaveLength(1) }) it("is the same notification when the relay delivers a record again after a crash", async () => { const app = makeNotesApp() const { seeded, slugs, member } = await restrictedProject(app) await member.call.callByName("notes.note.create", { ...slugs, title: "Kick-off", body: "" }) const record = (await app.data.outbox.pending(10)).find((entry) => entry.name === "notes.NoteCreated") if (!record) throw new Error("expected the note's outbox record") const scope: TenantScope = { level: "org", actor: systemActor("app.relay"), orgId: record.orgId } const subscribers = app.registry.events["notes.NoteCreated"]?.subscribers ?? [] const deliverOnce = async () => { for (const subscriber of subscribers) { const container = app.root.child() container.value(FILES_SCOPE, scope) container.value(NOTIFICATIONS_REPOS, app.data.forScope(scope)) const ctx: EventContext = { container, correlationId: record.id } await subscriber.handler({ name: record.name, payload: record.payload }, ctx) } } await deliverOnce() await deliverOnce() expect((await inboxOf(seeded.call, slugs.org)).items).toHaveLength(1) }) it("tells the other member, not the author, when the project's creator writes the note", async () => { const app = makeNotesApp() const { seeded, slugs, member } = await restrictedProject(app) await seeded.call.callByName("notes.note.create", { ...slugs, title: "Plan", body: "" }) await app.deliver() expect((await inboxOf(seeded.call, slugs.org)).items).toEqual([]) expect((await inboxOf(member.call, slugs.org)).items).toHaveLength(1) }) it("writes the title in the recipient's own language", async () => { const app = makeNotesApp() const { seeded, slugs, member } = await restrictedProject(app) await seeded.call.callByName("tenancy.me.update", { locale: "lt" }) await member.call.callByName("notes.note.create", { ...slugs, title: "Planas", body: "" }) await app.deliver() expect((await inboxOf(seeded.call, slugs.org)).items.map((item) => item.title)).toEqual([ "Naujas užrašas projekte „Launch“", ]) }) it("tells nobody in a project without named members, where the whole workspace would be noise", async () => { const app = makeNotesApp() const seeded = await app.seedProject("acme", "owner@acme.test") const member = await app.addMember(seeded, "member@acme.test", "member") await member.call.callByName("notes.note.create", { ...seeded.slugs, title: "Open", body: "" }) await app.deliver() expect((await inboxOf(seeded.call, seeded.slugs.org)).items).toEqual([]) }) it("publishes nothing when the note's transaction rolls back, so nobody is told about a note that is not there", async () => { const app = makeNotesApp() const { seeded, slugs, member } = await restrictedProject(app) await member.call.callByName("notes.note.create", { ...slugs, title: "Kept", body: "" }) const [kept] = await app.data.outbox.pending(500).then((all) => all.filter((r) => r.name === "notes.NoteCreated")) if (!kept) throw new Error("expected the note's outbox record") await app.deliver() const { noteId, orgId, workspaceId, projectId } = kept.payload as { noteId: string orgId: string workspaceId: string projectId: string } const scope = { level: "project" as const, actor: member.actor, orgId, workspaceId, projectId } // The commit fails after the use case wrote the row and the record: both go, or neither does. const failing: DataSource = { ...app.data, unitOfWork: { run: (s, fn) => app.data.unitOfWork.run(s, async (tx) => { await fn(tx) throw new Error("the commit failed") }), }, } await expect( createNote({ ...notesDeps(app.root), data: failing }, scope, { title: "Ghost", body: "" }), ).rejects.toThrow("the commit failed") expect(await app.data.outbox.pending(500)).toEqual([]) expect(await app.deliver()).toBe(0) expect((await inboxOf(seeded.call, slugs.org)).items.map((item) => item.key)).toEqual([`notes:${noteId}:created`]) const titles = ((await member.call.callByName("notes.note.list", slugs)) as { notes: NoteView[] }).notes expect(titles.map((n) => n.title)).toEqual(["Kept"]) }) it("has one subscription per module and event, because the relay claims a delivery per record and module", () => { // A second subscription of one module to one event would be claimed by the first and never run. const app = makeNotesApp() for (const [name, entry] of Object.entries(app.registry.events)) { const modules = entry.subscribers.map((subscriber) => subscriber.module) expect(modules, name).toEqual([...new Set(modules)]) } expect(notesModule.requires).toContain("notifications") }) })