ConusAI vendor v0.1.0-beta.114
ci / gate (push) Waiting to run

This commit is contained in:
ConusAI vendor v0.1.0-beta.114
2026-11-14 09:56:45 +00:00
commit 1ce8d23c35
867 changed files with 122104 additions and 0 deletions
+99
View File
@@ -0,0 +1,99 @@
// Owning pure/adapter consistency controls, not a Root approval or live service.
import { createHash } from "node:crypto"
import { canonicalActionJson } from "@conusai-kit/business"
import { installAppActionTransport } from "@conusai-kit/business/app-actions"
import { Container, type ProjectScope } from "@conusai-kit/core"
import { describe, expect, it, vi } from "vitest"
import { crmAppActionTargets } from "../adapters/app-actions.js"
import { crmServiceOf } from "../adapters/service.js"
import { partyCreate } from "../contract/index.js"
import { contactActionData, contactEncodedRequest, partyActionData, partyEncodedRequest } from "../core/action-plan.js"
import { CRM_SERVICE, type CrmService } from "../core/ports.js"
const binding = "11111111-1111-4111-8111-111111111111"
const scope: ProjectScope = {
level: "project",
orgId: "22222222-2222-4222-8222-222222222222",
workspaceId: "33333333-3333-4333-8333-333333333333",
projectId: "44444444-4444-4444-8444-444444444444",
actor: { userId: "55555555-5555-4555-8555-555555555555" },
}
const requestId = "66666666-6666-4666-8666-666666666666"
describe("actual CRM action plan is shared with its owning effect", () => {
it("preserves derived member/project scope and exact normalizations in the request", () => {
const value = partyActionData(scope, binding, {
kind: "person",
displayName: " Synthetic ",
legalName: " ",
country: "LT",
linkSelf: true,
requestId,
})
const plan = partyEncodedRequest("platform", value)
expect(plan.body.data.display_name).toBe("Synthetic")
expect(plan.body.data.legal_name).toBeUndefined()
expect(plan.body.data.identifiers?.[0]?.value).toBe(
`conusai://app-partitions/${binding}/members/${scope.actor.userId}`,
)
expect(plan.body.idempotency_key).toBe(`app-project:${scope.projectId}:${requestId}`)
expect(plan.arguments.body).toEqual(plan.body)
})
it("does not mask or renew the actual contact effective day across midnight", () => {
const input = {
partyId: "party",
kind: "email" as const,
value: " synthetic@example.test ",
purposes: ["billing" as const],
isPrimary: true,
requestId,
}
const before = contactEncodedRequest(
"platform",
contactActionData(scope, input, new Date("2026-11-05T23:59:59.999Z")),
)
const after = contactEncodedRequest("platform", contactActionData(scope, input, new Date("2026-11-06T00:00:00Z")))
expect(before.body.data.effective.from).toBe("2026-11-05")
expect(after.body.data.effective.from).toBe("2026-11-06")
expect(canonicalActionJson(before) === canonicalActionJson(after)).toBe(false)
expect(before.arguments.id).toBe("party")
})
it("refuses custom effect ports before inspection or guarded dispatch can call them", () => {
const container = new Container()
const custom = { createParty: vi.fn(), addContact: vi.fn() } as unknown as CrmService
container.value(CRM_SERVICE, custom)
const target = crmAppActionTargets.find((candidate) => candidate.procedure === partyCreate)
if (!target) throw new Error("missing declared CRM action target")
expect(() => target.encode({}, { scope, container, now: new Date() })).toThrow()
const request = partyEncodedRequest(
"platform",
partyActionData(scope, binding, { kind: "organization", displayName: "Synthetic", country: "LT", requestId }),
)
// Synthetic lower transport restriction ONLY, never a redeemed Root lease.
installAppActionTransport(
container,
{
bindingId: binding,
environmentUuid: "77777777-7777-4777-8777-777777777777",
appTenantId: scope.orgId,
actorId: scope.actor.userId,
orgSlug: "platform",
platformProjectId: 9,
},
{
action_id: requestId,
dispatch_lease_id: requestId,
source_id: requestId,
source_epoch: "1",
observation_sha256: "a".repeat(64),
encoded_request_sha256: createHash("sha256").update(canonicalActionJson(request)).digest("hex"),
expires_at_micros: String(BigInt(Date.now()) * 1000n + 20_000_000n),
execution_context: "synthetic_non_authoritative_context_".repeat(2),
},
request,
async () => {},
)
expect(() => crmServiceOf(container, "effect")).toThrow()
expect(custom.createParty).not.toHaveBeenCalled()
expect(custom.addContact).not.toHaveBeenCalled()
})
})
+106
View File
@@ -0,0 +1,106 @@
// Simulated adapter dispatch; these fixtures do not prove gateway or partition isolation.
// The module on its own, wired the way apps/web wires a request -- tenancy on the in-memory data
// source, the crm module, and the CRM port bound to the real generated client over the
// business-services fake of @conusai-kit/testing -- so its procedures are tested whether or not this
// app turned the module on.
import { businessClient } from "@conusai-kit/business"
import { type Actor, buildRegistry, Container, createCaller, type TransportCaller } from "@conusai-kit/core"
import { createMemoryDataSource, DATA_SOURCE, entitiesOf, SYSTEM_DATA } from "@conusai-kit/data"
import { createTenancyHooks, TENANCY, tenancyModule, users } from "@conusai-kit/tenancy"
import { type FakeBusinessServices, fakeBusinessServices } from "@conusai-kit/testing"
import { createClient } from "@conusai-sdk/crm/client"
import { simulatedBusinessAdmission } from "../../../test/helpers/business-admission.js"
import { sdkCrmService } from "../adapters/service.js"
import { CRM_SERVICE } from "../core/ports.js"
import { crmModule } from "../module.js"
/** The platform project of the fake deployment. */
export const BINDING_ID = "01900000-0000-7000-8000-000000000001"
export interface Tenant {
readonly owner: Actor
readonly call: TransportCaller
readonly orgSlug: string
readonly orgId: string
readonly workspaceSlug: string
readonly workspaceId: string
readonly projectSlug: string
readonly projectId: string
readonly slugs: { org: string; workspace: string; project: string }
}
export function crmApp(options: Parameters<typeof fakeBusinessServices>[0] = {}) {
const fake: FakeBusinessServices = fakeBusinessServices(options)
const client = businessClient({ apiUrl: fake.apiUrl, token: fake.token, orgSlug: fake.orgSlug }, createClient)
client.setConfig({ fetch: fake.fetch as typeof fetch })
const registry = buildRegistry([tenancyModule, crmModule])
const entities = entitiesOf(registry, "postgres")
const data = createMemoryDataSource({ entities })
const root = new Container()
root.value(DATA_SOURCE, data)
root.value(SYSTEM_DATA, data)
root.value(CRM_SERVICE, sdkCrmService({ client, orgSlug: fake.orgSlug, bindingId: BINDING_ID }))
const caller = (actor: Actor): TransportCaller => {
const tenancy = createTenancyHooks({ data, registry })
const container = root.child()
container.value(TENANCY, tenancy)
return createCaller({ registry, container, tenancy, serviceReadiness: simulatedBusinessAdmission }, { actor })
}
const signUp = async (email: string): Promise<Actor> => {
const user = await data.global(users).insert({
issuer: "https://id.example.test",
subject: email,
email,
displayName: email.split("@")[0] ?? email,
locale: "en",
})
return { userId: user.id }
}
/** An organization with one workspace and one project, owned by a new user. */
const tenant = async (slug: string): Promise<Tenant> => {
const owner = await signUp(`owner@${slug}.test`)
const call = caller(owner)
const org = (await call.callByName("tenancy.org.create", { slug, name: slug })) as { orgId: string; slug: string }
const workspace = (await call.callByName("tenancy.workspace.create", {
org: slug,
slug: `ws-${slug}`,
name: `Workspace ${slug}`,
visibility: "org",
})) as { workspaceId: string; slug: string }
const project = (await call.callByName("tenancy.project.create", {
org: slug,
workspace: workspace.slug,
slug: `p-${slug}`,
name: `Project ${slug}`,
moduleKey: "crm",
})) as { projectId: string; slug: string }
return {
owner,
call,
orgSlug: org.slug,
orgId: org.orgId,
workspaceSlug: workspace.slug,
workspaceId: workspace.workspaceId,
projectSlug: project.slug,
projectId: project.projectId,
slugs: { org: org.slug, workspace: workspace.slug, project: project.slug },
}
}
/** A second user who joins `t`'s organization with the org role `role`. */
const member = async (t: Tenant, email: string, role: "viewer" | "member"): Promise<TransportCaller> => {
const invite = (await t.call.callByName("tenancy.member.invite", { org: t.orgSlug, email, role })) as {
token: string
}
const actor = await signUp(email)
const call = caller(actor)
await call.callByName("tenancy.invitation.accept", { org: t.orgSlug, token: invite.token })
return call
}
return { fake, registry, data, caller, signUp, tenant, member }
}
+162
View File
@@ -0,0 +1,162 @@
import { dayOf, requestKey } from "../core/action-plan.js"
// The crm use cases over a recording port: what each asks the service, with no client and no network.
import { actorRef } from "@conusai-kit/business"
import type { ProjectScope } from "@conusai-kit/core"
import { describe, expect, it } from "vitest"
import type { PartyView } from "../contract/index.js"
import { addContact, createParty, listParties } from "../core/crm.js"
import { type CrmService, crmReadsOf } from "../core/ports.js"
const scope: ProjectScope = {
level: "project",
actor: { userId: "01900000-0000-7000-8000-000000000002" },
orgId: "o1",
workspaceId: "w1",
projectId: "p1",
}
const party: PartyView = {
partyId: "x",
kind: "person",
displayName: "Ada",
legalName: null,
country: "LT",
status: "active",
linked: false,
}
function recording() {
const asked: Array<[string, unknown]> = []
const service: CrmService = {
bindingId: "01900000-0000-7000-8000-000000000001",
listParties: async (query) => {
asked.push(["listParties", query])
return { rows: [] }
},
getParty: async () => party,
listContacts: async () => ({ rows: [] }),
createParty: async (input) => {
asked.push(["createParty", input])
return party
},
addContact: async (input) => {
asked.push(["addContact", input])
return {
contactId: "c",
kind: input.kind,
value: input.value,
purposes: input.purposes,
isPrimary: input.isPrimary,
erased: false,
}
},
}
return { service, asked }
}
describe("the crm use cases", () => {
it("list parties with a trimmed search and the module's page size", async () => {
const { service, asked } = recording()
await listParties(service, { q: " acme " })
expect(asked).toEqual([["listParties", { q: "acme", limit: 50 }]])
})
it("create a party with the project's request key and no identifier unless asked", async () => {
const { service, asked } = recording()
await createParty(service, scope, {
kind: "organization",
displayName: " Acme ",
legalName: " ",
country: "LT",
requestId: "r1",
})
expect(asked).toEqual([
[
"createParty",
{ kind: "organization", displayName: "Acme", country: "LT", identifiers: [], key: requestKey(scope, "r1") },
],
])
})
it("link a person to the member with the member's subject reference, and only a person, and only a member", async () => {
const { service, asked } = recording()
await createParty(service, scope, {
kind: "person",
displayName: "Ada",
country: "LT",
linkSelf: true,
requestId: "r2",
})
expect(asked[0]?.[1]).toMatchObject({
identifiers: [
{
scheme: "conusai.subject",
value: actorRef("01900000-0000-7000-8000-000000000001", "01900000-0000-7000-8000-000000000002"),
},
],
})
await expect(
createParty(service, scope, {
kind: "organization",
displayName: "Acme",
country: "LT",
linkSelf: true,
requestId: "r3",
}),
).rejects.toMatchObject({ problem: { status: 403 } })
const system: ProjectScope = { ...scope, actor: { userId: "system:sweep", system: true } }
await expect(
createParty(service, system, {
kind: "person",
displayName: "Ada",
country: "LT",
linkSelf: true,
requestId: "r4",
}),
).rejects.toMatchObject({ problem: { status: 403 } })
expect(asked).toHaveLength(1)
})
it("add a contact point from today, each purpose once", async () => {
const { service, asked } = recording()
const now = new Date("2026-11-02T23:30:00.000Z")
await addContact(
service,
scope,
{
partyId: "x",
kind: "phone",
value: " +370 600 00000 ",
purposes: ["billing", "billing"],
isPrimary: false,
requestId: "r5",
},
now,
)
expect(asked).toEqual([
[
"addContact",
{
partyId: "x",
kind: "phone",
value: "+370 600 00000",
purposes: ["billing"],
isPrimary: false,
effectiveFrom: "2026-11-02",
key: requestKey(scope, "r5"),
},
],
])
expect(dayOf(now)).toBe("2026-11-02")
})
it("hand a read handler a view without a single write", () => {
expect(Object.keys(crmReadsOf(recording().service)).sort()).toEqual([
"bindingId",
"getParty",
"listContacts",
"listParties",
])
})
})
+76
View File
@@ -0,0 +1,76 @@
// Every string a page of the module shows is a message in English and in Lithuanian, the Lithuanian
// plural forms are real ones, and no component of the module carries a string of its own.
import { readdirSync, readFileSync } from "node:fs"
import { join } from "node:path"
import { buildRegistry } from "@conusai-kit/core"
import { createI18n, lintUiStrings } from "@conusai-kit/i18n"
import { tenancyModule } from "@conusai-kit/tenancy"
import { describe, expect, it } from "vitest"
import { crmModule } from "../module.js"
import { contactFormLabels, contactsLabels, partyFormLabels, partyListLabels } from "../ui/labels.js"
// The messages as an app that turned the module on has them: from the manifest, flattened by the kit.
const catalogues = buildRegistry([tenancyModule, crmModule]).catalogues
const en = catalogues.en?.crm ?? {}
const lt = catalogues.lt?.crm ?? {}
function keysUsed(): string[] {
const asked: string[] = []
const t = (key: string) => {
asked.push(key)
return key
}
partyFormLabels(t, {})
partyListLabels(t)
contactsLabels(t, 2)
contactFormLabels(t, {})
return asked
}
describe("the crm catalogues", () => {
it("hold the same keys in English and in Lithuanian, none empty", () => {
expect(Object.keys(lt).sort()).toEqual(Object.keys(en).sort())
for (const [key, text] of [...Object.entries(en), ...Object.entries(lt)]) expect(text.trim(), key).not.toBe("")
})
it("hold every message the labels ask for, and none that nothing asks for", () => {
const asked = keysUsed()
for (const key of asked) {
expect(en[key as keyof typeof en], `en is missing crm.${key}`).toBeTruthy()
expect(lt[key as keyof typeof lt], `lt is missing crm.${key}`).toBeTruthy()
}
// The page and the navigation ask for these directly.
const direct = new Set(["title", "intro", "party.back", "party.linked"])
expect(Object.keys(en).filter((key) => !asked.includes(key) && !direct.has(key))).toEqual([])
})
it("answer the navigation label the manifest names", () => {
expect(crmModule.ui?.nav?.map((item) => item.label)).toEqual(["crm.title"])
})
it("count contact points with the Lithuanian one, few and other forms", () => {
const count = (locale: "en" | "lt", n: number) =>
createI18n(locale, { en: { crm: en }, lt: { crm: lt } }).message("crm.contacts.count", { count: n })
expect(count("lt", 1)).toBe("1 kontaktas")
expect(count("lt", 2)).toBe("2 kontaktai")
expect(count("lt", 10)).toBe("10 kontaktų")
expect(count("lt", 21)).toBe("21 kontaktas")
expect(count("en", 2)).toBe("2 contact points")
})
})
describe("the components", () => {
const dir = new URL("../ui/", import.meta.url).pathname
const sources = readdirSync(dir)
.filter((name) => name.endsWith(".tsx"))
.map((name) => ({ name, text: readFileSync(join(dir, name), "utf8") }))
it("carry no hard-coded user-facing string and no colour literal", () => {
expect(sources.length).toBeGreaterThanOrEqual(5)
for (const { name, text } of sources) {
expect(lintUiStrings(text, name), name).toEqual([])
expect(text, name).not.toMatch(/#[0-9a-fA-F]{3,8}\b|\brgb\(|\bhsl\(|\boklch\(/)
}
})
})
+53
View File
@@ -0,0 +1,53 @@
// What the generated tenant-isolation suite (test/isolation.test.ts) needs of this module, from this
// folder: one fixture per tenant-scoped procedure, each called as a member of one organization with
// another's slugs, which the resolver must refuse before the CRM service is reached.
//
// The module keeps no row of its own, so it seeds nothing: what a fixture names is the other tenant's
// project, and an id is any well-formed one (the service is never asked). The suite reads this file by
// its place whether or not the module is on, and checks only the procedures the app registers, so
// while the module is off (the template's default) it brings no fixture; `procedures.test.ts` runs the
// same fixtures against the module on its own, on or off.
import type { TransportCaller } from "@conusai-kit/core"
import type { IsolationFixture, IsolationTenant } from "@conusai-kit/testing"
import { businessModulesOn } from "../../index.js"
const SOMEWHERE = "00000000-0000-4000-8000-000000000001"
const inProjectOf = (t: IsolationTenant) => ({ org: t.orgSlug, workspace: t.workspaceSlug, project: t.projectSlug })
export const crmIsolationFixtures: Record<string, IsolationFixture> = {
"crm.party.list": { input: (t) => inProjectOf(t) },
"crm.party.get": { input: (t) => ({ ...inProjectOf(t), partyId: SOMEWHERE }) },
"crm.party.create": {
input: (t) => ({
...inProjectOf(t),
kind: "organization",
displayName: "Intruder",
country: "LT",
requestId: SOMEWHERE,
}),
},
"crm.contact.add": {
input: (t) => ({
...inProjectOf(t),
partyId: SOMEWHERE,
kind: "email",
value: "intruder@example.test",
purposes: [],
isPrimary: false,
requestId: SOMEWHERE,
}),
},
}
/** The shape the suite reads (`modules/notes/__tests__/isolation.ts` names it), so no other module is imported. */
export const isolation: {
seed(
call: TransportCaller,
inProject: { org: string; workspace: string; project: string },
): Promise<Record<string, string>>
fixtures: Record<string, IsolationFixture>
} = {
seed: async () => ({}),
fixtures: businessModulesOn.includes("crm") ? crmIsolationFixtures : {},
}
+183
View File
@@ -0,0 +1,183 @@
// The crm procedures end to end: the caller, the tenancy resolver, the module and the real generated
// client against the business-services fake -- what a member sees, what the service gets, and each
// refusal as the kit's app error (`businessAppError`) a page or a form shows.
import { actorRef } from "@conusai-kit/business"
import type { AppError } from "@conusai-kit/core"
import { entitiesOf } from "@conusai-kit/data"
import { TENANCY_ALSO_CHECK, tenancyIsolationExempt, tenancyIsolationFixtures } from "@conusai-kit/tenancy/testing"
import { type IsolationTenant, runIsolation, snapshotOf } from "@conusai-kit/testing"
import { describe, expect, it } from "vitest"
import { SUBJECT_SCHEME } from "../contract/index.js"
import { BINDING_ID, crmApp } from "./app.js"
import { crmIsolationFixtures } from "./isolation.js"
const refusal = async (promise: Promise<unknown>): Promise<AppError> => {
try {
await promise
} catch (error) {
return error as AppError
}
throw new Error("the call was not refused")
}
const acme = { kind: "organization", displayName: "Acme Supplies", country: "LT" }
describe("the crm procedures", () => {
it("create a party once per request, list it, and read it with its contact points", async () => {
const app = crmApp()
const t = await app.tenant("alpha")
const requestId = crypto.randomUUID()
const created = (await t.call.callByName("crm.party.create", { ...t.slugs, ...acme, requestId })) as {
partyId: string
}
expect(created).toMatchObject({
displayName: "Acme Supplies",
kind: "organization",
country: "LT",
status: "active",
linked: false,
})
expect(await t.call.callByName("crm.party.create", { ...t.slugs, ...acme, requestId })).toEqual(created)
expect(app.fake.crm.parties.size).toBe(1)
const listed = (await t.call.callByName("crm.party.list", t.slugs)) as { rows: unknown[] }
expect(listed.rows).toEqual([created])
const contact = await t.call.callByName("crm.contact.add", {
...t.slugs,
partyId: created.partyId,
kind: "email",
value: " orders@acme.example ",
purposes: ["invoice_email", "invoice_email"],
isPrimary: true,
requestId: crypto.randomUUID(),
})
expect(contact).toMatchObject({
kind: "email",
value: "orders@acme.example",
purposes: ["invoice_email"],
isPrimary: true,
erased: false,
})
const read = (await t.call.callByName("crm.party.get", { ...t.slugs, partyId: created.partyId })) as {
party: unknown
contacts: unknown[]
}
expect(read).toEqual({ party: created, contacts: [contact] })
const [stored] = [...app.fake.crm.contacts.values()]
if (!stored?.fields) throw new Error("the fake holds no contact point")
expect((stored.fields as { effective: { from: string } }).effective.from).toMatch(/^\d{4}-\d{2}-\d{2}$/)
})
it("link a person to the member who creates it with the member's subject reference", async () => {
const app = crmApp()
const t = await app.tenant("alpha")
const created = (await t.call.callByName("crm.party.create", {
...t.slugs,
kind: "person",
displayName: "Ada",
country: "LT",
linkSelf: true,
requestId: crypto.randomUUID(),
})) as { partyId: string; linked: boolean }
expect(created.linked).toBe(true)
expect(app.fake.crm.parties.get(created.partyId)?.identifiers).toEqual([
{ scheme: SUBJECT_SCHEME, value: actorRef(BINDING_ID, t.owner.userId) },
])
const organization = await refusal(
t.call.callByName("crm.party.create", { ...t.slugs, ...acme, linkSelf: true, requestId: crypto.randomUUID() }),
)
expect(organization.problem.status).toBe(403)
})
it("show the service's refusals in the service's words", async () => {
const app = crmApp()
const t = await app.tenant("alpha")
const missing = await refusal(t.call.callByName("crm.party.get", { ...t.slugs, partyId: "nobody" }))
expect(missing.problem).toMatchObject({ status: 404, code: "business_not_found" })
const { partyId } = (await t.call.callByName("crm.party.create", {
...t.slugs,
...acme,
requestId: crypto.randomUUID(),
})) as {
partyId: string
}
const bad = await refusal(
t.call.callByName("crm.contact.add", {
...t.slugs,
partyId,
kind: "email",
value: "not an address",
purposes: [],
isPrimary: false,
requestId: crypto.randomUUID(),
}),
)
expect(bad.problem).toMatchObject({ status: 400, code: "business_invalid" })
})
it("answer a disabled service, a missing scope and a silent service with the platform's own codes", async () => {
const app = crmApp({ crm: false })
const t = await app.tenant("alpha")
const disabled = await refusal(t.call.callByName("crm.party.list", t.slugs))
expect(disabled.problem).toMatchObject({ status: 404, code: "service_not_enabled" })
app.fake.enable("crm")
app.fake.revoke("crm.parties:write")
const scoped = await refusal(
t.call.callByName("crm.party.create", { ...t.slugs, ...acme, requestId: crypto.randomUUID() }),
)
expect(scoped.problem).toMatchObject({ status: 403, code: "service_permission_missing" })
expect(scoped.problem.detail).not.toContain("crm.parties:write")
app.fake.refuseNext(503)
const silent = await refusal(t.call.callByName("crm.party.list", t.slugs))
expect(silent.problem).toMatchObject({ status: 503, code: "business_unavailable" })
})
it("let a viewer read and refuse their writes before the service is called", async () => {
const app = crmApp()
const t = await app.tenant("alpha")
const viewer = await app.member(t, "viewer@alpha.test", "viewer")
expect(await viewer.callByName("crm.party.list", t.slugs)).toEqual({ rows: [] })
const before = app.fake.calls.length
// A member without the permission is answered as a non-member is (kit-core's caller, spec §3.3).
const refused = await refusal(
viewer.callByName("crm.party.create", { ...t.slugs, ...acme, requestId: crypto.randomUUID() }),
)
expect(refused.problem.status).toBe(404)
expect(app.fake.calls.length).toBe(before)
})
})
describe("the crm module's tenant isolation", () => {
it("refuses every procedure called with another organization's slugs, and never reaches the service", async () => {
const app = crmApp()
const tenant = async (slug: string): Promise<IsolationTenant> => {
const t = await app.tenant(slug)
return {
orgSlug: t.orgSlug,
workspaceSlug: t.workspaceSlug,
projectSlug: t.projectSlug,
orgId: t.orgId,
workspaceId: t.workspaceId,
projectId: t.projectId,
userId: t.owner.userId,
email: `owner@${slug}.test`,
extra: {},
}
}
const a = await tenant("alpha")
const b = await tenant("bravo")
const result = await runIsolation({
registry: app.registry,
seed: async () => ({ a, b }),
caller: (actor) => app.caller(actor),
snapshot: () => snapshotOf(app.data, entitiesOf(app.registry, "postgres"), { outbox: app.data.outbox }),
fixtures: { ...tenancyIsolationFixtures(), ...crmIsolationFixtures },
alsoCheck: [...TENANCY_ALSO_CHECK],
exempt: tenancyIsolationExempt,
})
expect(result.failures).toEqual([])
for (const name of Object.keys(crmIsolationFixtures)) expect(result.passed, name).toContain(name)
expect(app.fake.calls).toEqual([])
})
})
+178
View File
@@ -0,0 +1,178 @@
// The pages and components, rendered the way the shell renders them, from the loader's data: the platform's
// refusals are the page's own states, a control is offered to whoever may use it, every table has a
// caption and scoped headers.
import { AppError, buildRegistry, Container, type TransportCaller } from "@conusai-kit/core"
import { builtInCatalogues, createI18n, uiLabels } from "@conusai-kit/i18n"
import { tenancyModule } from "@conusai-kit/tenancy"
import { createDataClient, DataProvider, type PermissionKey, PermissionsProvider } from "@conusai-kit/ui/data"
import { UiLabelsProvider } from "@conusai-kit/ui/primitives"
import type { ReactNode } from "react"
import { renderToStaticMarkup as renderMarkup } from "react-dom/server"
import { describe, expect, it, vi } from "vitest"
import { modulePages } from "../../pages.js"
import { crmModule } from "../module.js"
import { crmPage } from "../ui/pages.js"
const catalogues = buildRegistry([tenancyModule, crmModule]).catalogues
const i18n = createI18n("en", { en: { crm: catalogues.en?.crm ?? {}, app: { failed: "That did not work: {detail}" } } })
const slugs = { org: "acme", workspace: "ws", project: "p" }
const EVERYTHING: readonly PermissionKey[] = ["crm.party.read", "crm.party.write"]
const kitStrings = uiLabels(createI18n("en", builtInCatalogues))
const renderToStaticMarkup = (node: React.ReactNode) =>
renderMarkup(<UiLabelsProvider labels={kitStrings}>{node}</UiLabelsProvider>)
const inProvider = (node: ReactNode, permissions: readonly PermissionKey[] = EVERYTHING) =>
renderToStaticMarkup(
<DataProvider client={createDataClient({ fetch: async () => new Response(null, { status: 501 }) })}>
<PermissionsProvider scope={slugs} seed={{ scope: slugs, permissions, revision: "1" }}>
{node}
</PermissionsProvider>
</DataProvider>,
)
const callerOf = (answers: Record<string, unknown>): TransportCaller =>
({
call: async (contract: { name: string }) => {
const answer = answers[contract.name]
if (answer instanceof Error) throw answer
return answer
},
}) as unknown as TransportCaller
const load = (segments: string[], answers: Record<string, unknown>) =>
crmPage.load({ slugs, segments, search: {}, caller: callerOf(answers), i18n, container: new Container() })
/** The page as the shell renders it: the loader's data through JSON, as the browser receives it, then the view. */
const render = async (segments: string[], answers: Record<string, unknown>, permissions?: readonly PermissionKey[]) => {
const data = await load(segments, answers)
expect(JSON.parse(JSON.stringify(data))).toEqual(data)
const { View } = crmPage
return inProvider(
<View slugs={slugs} segments={segments} locale="en" data={JSON.parse(JSON.stringify(data))} />,
permissions,
)
}
const party = {
partyId: "p-1",
kind: "organization",
displayName: "Acme Supplies",
legalName: "Acme Supplies UAB",
country: "LT",
status: "active",
linked: false,
}
describe("the crm page", () => {
it("lists the parties, with the form for whoever may add one", async () => {
const answers = { "crm.party.list": { rows: [party] } }
const html = await render([], answers)
expect(html).toContain(">Customers</h1>")
// The row's name is its link: the kit's row link (`kit-row-link`).
const link = html.match(/<a\b[^>]*>Acme Supplies<\/a>/)?.[0] ?? ""
expect(link).toContain('data-slot="row-link"')
expect(link.match(/class="([^"]*)"/)?.[1]?.split(/\s+/)).toContain("kit-row-link")
expect(link).toContain('href="/acme/ws/p/crm/p-1"')
expect(link).toContain('aria-label="Open Acme Supplies"')
expect(html).toContain('aria-label="Parties"')
expect(html).toContain("New party")
expect(await render([], answers, ["crm.party.read"])).not.toContain("New party")
})
it("shows a party's contact points and the form that adds one", async () => {
const html = await render(["p-1"], {
"crm.party.get": {
party: { ...party, linked: true },
contacts: [
{
contactId: "c1",
kind: "email",
value: "orders@acme.example",
purposes: ["invoice_email"],
isPrimary: true,
erased: false,
},
{ contactId: "c2", kind: "phone", value: "", purposes: [], isPrimary: false, erased: true },
],
},
})
expect(html).toContain(">Acme Supplies</h1>")
expect(html).toContain("Linked to a member&#x27;s account")
expect(html).toContain("2 contact points")
expect(html).toContain('aria-label="Contact points"')
for (const text of ["E-mail", "orders@acme.example", "Invoices by e-mail", "Yes"]) expect(html).toContain(text)
expect(html).toContain("Erased")
expect(html).toContain("Add a contact point")
})
it("shows the kit's not-enabled state when the organization has not enabled the service", async () => {
const refused = new AppError({
type: "/problems/service_not_enabled",
title: "Service prerequisite unavailable",
status: 404,
code: "service_not_enabled",
detail: "Service admission refused: not_enabled.",
})
const html = await render([], { "crm.party.list": refused })
expect(html).toContain('data-state="service_not_enabled"')
expect(html).toContain("This service is not enabled for your organization.")
expect(html).not.toContain('role="grid"')
})
it("tells a Lithuanian reader in the kit's Lithuanian sentences", async () => {
const lt = createI18n("lt", { lt: { crm: catalogues.lt?.crm ?? {} } })
const silent = new AppError({
type: "/problems/business_unavailable",
title: "Refused",
status: 503,
code: "business_unavailable",
})
const data = await crmPage.load({
slugs,
segments: [],
search: {},
caller: callerOf({ "crm.party.list": silent }),
i18n: lt,
container: new Container(),
})
expect(data).toMatchObject({
kind: "state",
notice: { code: "business_unavailable", text: "Paslauga neatsakė. Bandykite vėliau." },
})
const list = await crmPage.load({
slugs,
segments: [],
search: {},
caller: callerOf({ "crm.party.list": { rows: [] } }),
i18n: lt,
container: new Container(),
})
expect(list.kind === "list" && list.form.refusals.business_stale_revision).toBe(
"Kažkas tai pakeitė, kol jūs dirbote. Atnaujinkite ir bandykite dar kartą.",
)
})
it("leaves a party that is not there, and a path it does not have, to the shell", async () => {
const missing = new AppError({
type: "/problems/business_not_found",
title: "Not found",
status: 404,
code: "business_not_found",
})
await expect(render(["p-2"], { "crm.party.get": missing })).rejects.toMatchObject({
problem: { code: "business_not_found" },
})
await expect(render(["p-1", "x"], {})).rejects.toMatchObject({ problem: { status: 404 } })
await expect(render(["%2Fetc"], {})).rejects.toMatchObject({ problem: { status: 404 } })
await expect(render(["%E0%A4%A"], {})).rejects.toMatchObject({ problem: { status: 404 } })
})
it("is listed for the shell", async () => {
await expect(modulePages.crm?.()).resolves.toBe(crmPage)
})
})
// Explicit synthetic admission: module-local fixtures exercise the selected module.
vi.mock("../../inclusion.json", () => ({ default: { schemaVersion: 1, selected: ["crm"], everAdopted: ["crm"] } }))