@@ -0,0 +1,517 @@
|
||||
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
||||
// Wall three (spec §3.3): the generated suite calls every tenant-scoped procedure of this app's
|
||||
// registry as a member of organization A with organization B's slugs and ids, and fails any
|
||||
// procedure that answers with B's data or writes anything at all.
|
||||
//
|
||||
// It runs on the in-memory adapter, which is the repository predicate alone: that is wall one.
|
||||
// Wall two, what Postgres refuses on its own, is proven against a real Postgres by
|
||||
// @conusai-kit/data-kysely's own gate, and wall three is proven here for every procedure this
|
||||
// app registers -- including the ones a module adds tomorrow, because the list comes from the
|
||||
// registry and a procedure without a fixture fails the suite by name.
|
||||
|
||||
import { join } from "node:path"
|
||||
import {
|
||||
type Actor,
|
||||
buildRegistry,
|
||||
type CallerOptions,
|
||||
Container,
|
||||
createCaller,
|
||||
type TransportCaller,
|
||||
} from "@conusai-kit/core"
|
||||
import {
|
||||
createMemoryDataSource,
|
||||
DATA_SOURCE,
|
||||
entitiesOf,
|
||||
JOB_QUEUE,
|
||||
OBJECT_STORE,
|
||||
SYSTEM_DATA,
|
||||
} from "@conusai-kit/data"
|
||||
import { GUIDES_PLATFORM, GUIDES_REGISTRY } from "@conusai-kit/guides" // kit:guides
|
||||
import { guidesIsolationExempt, guidesIsolationFixtures, recordingGuidesPlatform } from "@conusai-kit/guides/testing" // kit:guides
|
||||
import { PROPOSALS_REGISTRY } from "@conusai-kit/proposals"
|
||||
import { SUPPORT_PLATFORM } from "@conusai-kit/support" // kit:support
|
||||
import * as supportTesting from "@conusai-kit/support/testing" // kit:support
|
||||
import { createTenancyHooks, TENANCY, users } from "@conusai-kit/tenancy"
|
||||
import { TENANCY_ALSO_CHECK, tenancyIsolationExempt, tenancyIsolationFixtures } from "@conusai-kit/tenancy/testing"
|
||||
import {
|
||||
describeIsolation,
|
||||
fakeJobQueue,
|
||||
fakeObjectStore,
|
||||
type IsolationFixture,
|
||||
type IsolationTenant,
|
||||
runIsolation,
|
||||
snapshotOf,
|
||||
uploadViaProcedures,
|
||||
} from "@conusai-kit/testing"
|
||||
import { describe, expect, it, vi } from "vitest"
|
||||
import { accountIsolationExempt } from "../modules/account/__tests__/exempt.js"
|
||||
import { documents } from "../modules/documents/index.js" // kit:documents
|
||||
import { appModules } from "../modules/index.js"
|
||||
import * as knowledge from "../modules/knowledge/index.js" // kit:knowledge
|
||||
import { isolationFilesOf } from "./support/module-isolations.js"
|
||||
|
||||
const registry = buildRegistry([...appModules])
|
||||
|
||||
/** What a module's isolation file exports: the shape this suite reads. */
|
||||
interface ModuleIsolation {
|
||||
seed(
|
||||
call: TransportCaller,
|
||||
inProject: { org: string; workspace: string; project: string },
|
||||
): Promise<Record<string, string>>
|
||||
fixtures: Record<string, IsolationFixture>
|
||||
}
|
||||
|
||||
/**
|
||||
* The fixtures and the seed rows a module brings in `modules/<id>/__tests__/isolation.ts`, found by
|
||||
* their place: a module added with `pnpm new:module` is covered without a line here, and a
|
||||
* procedure it registers with no fixture fails the suite by name. Only registered modules count: a
|
||||
* module folder kept but not registered serves no procedure (`isolationFilesOf`). (The older
|
||||
* modules' fixtures are listed below; they move into their own folders as each is next touched.)
|
||||
*/
|
||||
const modulesDir = join(import.meta.dirname, "..", "modules")
|
||||
const moduleIsolations: ModuleIsolation[] = await Promise.all(
|
||||
isolationFilesOf(
|
||||
modulesDir,
|
||||
registry.modules.map((module) => module.id),
|
||||
).map(async (file) => {
|
||||
const loaded = (await import(/* @vite-ignore */ file)) as { isolation: ModuleIsolation }
|
||||
return loaded.isolation
|
||||
}),
|
||||
)
|
||||
const entities = entitiesOf(registry, "postgres")
|
||||
|
||||
/** The guides module's platform, recording: a refused cross-tenant call must never reach it. */ // kit:guides
|
||||
const guidesPlatform = recordingGuidesPlatform() // kit:guides
|
||||
/** The support module's platform, recording: a refused cross-tenant mint must never reach it. */ // kit:support
|
||||
const supportPlatform = supportTesting.recordingSupportPlatform() // kit:support
|
||||
/** The app wired the way apps/web wires it, with the in-memory adapter in place of Postgres. */
|
||||
function makeApp() {
|
||||
const data = createMemoryDataSource({ entities })
|
||||
const root = new Container()
|
||||
root.value(DATA_SOURCE, data)
|
||||
root.value(SYSTEM_DATA, data)
|
||||
// The files module's store: a seeded upload needs one, and a refused cross-tenant call never reaches it.
|
||||
root.value(OBJECT_STORE, fakeObjectStore())
|
||||
// A module whose writes enqueue work (a job started by a procedure) needs a queue to be driven at all.
|
||||
root.value(JOB_QUEUE, fakeJobQueue())
|
||||
root.value(PROPOSALS_REGISTRY, registry)
|
||||
root.value(GUIDES_PLATFORM, guidesPlatform) // kit:guides
|
||||
root.value(GUIDES_REGISTRY, registry) // kit:guides
|
||||
root.value(SUPPORT_PLATFORM, supportPlatform) // kit:support
|
||||
// kit:knowledge:begin
|
||||
// The knowledge module's ports, faked: the search answers nothing (the suite asserts a
|
||||
// cross-tenant call is refused or answered empty), and the enablement checks pass.
|
||||
root.value(knowledge.KNOWLEDGE_SEARCH, {
|
||||
readiness: async () => ({ state: "ready" as const, reasons: [] }),
|
||||
index: async () => undefined,
|
||||
search: async () => [],
|
||||
remove: async () => undefined,
|
||||
})
|
||||
root.value(knowledge.KNOWLEDGE_READINESS, { readiness: async () => ({ state: "ready" as const, reasons: [] }) })
|
||||
root.value(knowledge.KNOWLEDGE_ENABLEMENT, {
|
||||
checkEligibility: async () => ({ ok: true as const }),
|
||||
indexingQuota: async () => Number.MAX_SAFE_INTEGER,
|
||||
})
|
||||
// kit:knowledge:end
|
||||
const caller = (
|
||||
actor: Actor,
|
||||
admission: Pick<CallerOptions, "prepareServices" | "serviceReadiness"> = {},
|
||||
): TransportCaller => {
|
||||
const tenancy = createTenancyHooks({ data, registry })
|
||||
const container = root.child()
|
||||
container.value(TENANCY, tenancy)
|
||||
return createCaller({ registry, container, tenancy, ...admission }, { actor })
|
||||
}
|
||||
return { data, caller }
|
||||
}
|
||||
|
||||
const app = makeApp()
|
||||
|
||||
async function signUp(email: string): Promise<Actor> {
|
||||
const user = await app.data.global(users).insert({
|
||||
issuer: "https://id.example.test",
|
||||
subject: email,
|
||||
email,
|
||||
displayName: email.split("@")[0] ?? email,
|
||||
locale: "en",
|
||||
})
|
||||
return { userId: user.id }
|
||||
}
|
||||
|
||||
async function tenant(slug: string): Promise<IsolationTenant> {
|
||||
const owner = await signUp(`owner@${slug}.test`)
|
||||
const call = app.caller(owner)
|
||||
const org = (await call.callByName("tenancy.org.create", { slug, name: slug })) as {
|
||||
orgId: string
|
||||
slug: string
|
||||
workspaceSlug: 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: "tenancy",
|
||||
})) as { projectId: string; slug: string }
|
||||
const invite = (await call.callByName("tenancy.member.invite", {
|
||||
org: slug,
|
||||
email: `invited-${slug}@example.test`,
|
||||
role: "member",
|
||||
})) as { invitationId: string; token: string }
|
||||
const key = (await call.callByName("auth.apiKey.create", { org: slug, name: "ci" })) as { key: { id: string } }
|
||||
// A custom role of this tenant's own: the role procedures' fixtures name its key from the other side.
|
||||
const role = (await call.callByName("tenancy.role.create", {
|
||||
org: slug,
|
||||
name: `Auditor ${slug}`,
|
||||
level: "org",
|
||||
permissions: ["tenancy.org.read", "tenancy.member.read"],
|
||||
})) as { key: string }
|
||||
// One upload and one proposal in the project, so a fixture names rows that exist in the other
|
||||
// tenant: the call has to be refused for being another tenant's, not for naming nothing.
|
||||
const inProject = { org: slug, workspace: workspace.slug, project: project.slug }
|
||||
const file = await uploadViaProcedures(call, inProject, {
|
||||
filename: "seed.pdf",
|
||||
contentType: "application/pdf",
|
||||
bytes: new TextEncoder().encode("%PDF-1.4"),
|
||||
})
|
||||
// kit:documents:begin
|
||||
const proposal = (await call.callByName(
|
||||
"proposals.proposal.create",
|
||||
{
|
||||
...inProject,
|
||||
targetKey: "documents.line",
|
||||
change: {
|
||||
lineKey: "a".repeat(64),
|
||||
label: "Reference",
|
||||
value: "QR-1",
|
||||
page: 1,
|
||||
position: 0,
|
||||
quote: "Reference: QR-1",
|
||||
confidence: 0.9,
|
||||
verification: "verified",
|
||||
edited: false,
|
||||
},
|
||||
evidence: [{ fileId: file.fileId, page: 1, quote: "Reference: QR-1" }],
|
||||
model: "seed",
|
||||
promptVersion: "seed.v1",
|
||||
},
|
||||
{ idempotencyKey: `seed.${slug}` },
|
||||
)) as { proposalId: string }
|
||||
// A document row, as the OCR job makes one: the live collection's read-back names it by its id.
|
||||
const documentRow = await app.data
|
||||
.forScope({
|
||||
level: "project",
|
||||
actor: owner,
|
||||
orgId: org.orgId,
|
||||
workspaceId: workspace.workspaceId,
|
||||
projectId: project.projectId,
|
||||
})
|
||||
.of(documents)
|
||||
.insert({
|
||||
fileId: file.fileId,
|
||||
filename: "seed.pdf",
|
||||
status: "processing",
|
||||
ocrJobId: null,
|
||||
pageCount: null,
|
||||
proposedCount: null,
|
||||
failureCode: null,
|
||||
skippedPages: null,
|
||||
finishedAt: null,
|
||||
})
|
||||
// kit:documents:end
|
||||
// kit:knowledge:begin
|
||||
// A knowledge_document row, as an index request makes one (indexing needs the module switched on, which
|
||||
// the seeding caller cannot do): the live collection's read-back names it by its id.
|
||||
const knowledgeDocument = await app.data
|
||||
.forScope({
|
||||
level: "project",
|
||||
actor: owner,
|
||||
orgId: org.orgId,
|
||||
workspaceId: workspace.workspaceId,
|
||||
projectId: project.projectId,
|
||||
})
|
||||
.of(knowledge.documents)
|
||||
.insert({
|
||||
collectionName: "contracts",
|
||||
fileId: file.fileId,
|
||||
filename: "seed.pdf",
|
||||
status: "queued",
|
||||
error: null,
|
||||
indexedAt: null,
|
||||
requestedBy: owner.userId,
|
||||
})
|
||||
// kit:knowledge:end
|
||||
// What each module that brings fixtures seeds in this tenant: its ids are what the other tenant's fixtures name.
|
||||
const seeded: Record<string, string> = {}
|
||||
for (const isolation of moduleIsolations) Object.assign(seeded, await isolation.seed(call, inProject))
|
||||
return {
|
||||
orgSlug: org.slug,
|
||||
workspaceSlug: workspace.slug,
|
||||
projectSlug: project.slug,
|
||||
orgId: org.orgId,
|
||||
workspaceId: workspace.workspaceId,
|
||||
projectId: project.projectId,
|
||||
userId: owner.userId,
|
||||
email: `owner@${slug}.test`,
|
||||
extra: {
|
||||
invitationId: invite.invitationId,
|
||||
token: invite.token,
|
||||
roleKey: role.key,
|
||||
apiKeyId: key.key.id,
|
||||
fileId: file.fileId,
|
||||
proposalId: proposal.proposalId, // kit:documents
|
||||
documentId: documentRow.id, // kit:documents
|
||||
knowledgeDocumentId: knowledgeDocument.id, // kit:knowledge
|
||||
...seeded,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let seeded: Promise<{ a: IsolationTenant; b: IsolationTenant }> | undefined
|
||||
const tenants = () => {
|
||||
seeded ??= (async () => ({ a: await tenant("alpha"), b: await tenant("bravo") }))()
|
||||
return seeded
|
||||
}
|
||||
|
||||
/** The three slugs a project-scoped procedure's input carries, from the tenant it must not reach. */
|
||||
const inProjectOf = (t: IsolationTenant) => ({ org: t.orgSlug, workspace: t.workspaceSlug, project: t.projectSlug })
|
||||
|
||||
/** The other tenant's seeded rows; the ghost tenant has none, so its ids are well-formed and absent. */
|
||||
const NOWHERE = "00000000-0000-0000-0000-000000000000"
|
||||
const fileOf = (t: IsolationTenant) => t.extra.fileId ?? NOWHERE
|
||||
const proposalOf = (t: IsolationTenant) => t.extra.proposalId ?? NOWHERE
|
||||
const documentOf = (t: IsolationTenant) => t.extra.documentId ?? NOWHERE // kit:documents
|
||||
|
||||
/** One fixture per tenant-scoped procedure, built from the *other* tenant's ids. */
|
||||
const fixtures: Record<string, IsolationFixture> = {
|
||||
// The tenancy module's, which this suite cannot be run without (one list, in @conusai-kit/tenancy/testing).
|
||||
...tenancyIsolationFixtures(),
|
||||
...Object.assign({}, ...moduleIsolations.map((isolation) => isolation.fixtures)),
|
||||
...guidesIsolationFixtures, // kit:guides
|
||||
...supportTesting.supportIsolationFixtures, // kit:support
|
||||
"auth.apiKey.create": { input: (t) => ({ org: t.orgSlug, name: "intruder" }) },
|
||||
"auth.apiKey.list": { input: (t) => ({ org: t.orgSlug }) },
|
||||
"auth.apiKey.revoke": {
|
||||
input: (t) => ({ org: t.orgSlug, id: t.extra.apiKeyId ?? "00000000-0000-0000-0000-000000000000" }),
|
||||
},
|
||||
"files.file.beginUpload": {
|
||||
input: (t) => ({ ...inProjectOf(t), filename: "intruder.pdf", contentType: "application/pdf", size: 8 }),
|
||||
},
|
||||
"files.file.putPart": {
|
||||
input: (t) => ({ ...inProjectOf(t), fileId: fileOf(t), part: 1, body: new TextEncoder().encode("%PDF-1.4") }),
|
||||
},
|
||||
"files.file.completeUpload": {
|
||||
input: (t) => ({ ...inProjectOf(t), fileId: fileOf(t), parts: [{ number: 1, etag: "etag-intruder" }] }),
|
||||
},
|
||||
"files.file.get": { input: (t) => ({ ...inProjectOf(t), fileId: fileOf(t) }) },
|
||||
"files.file.download": { input: (t) => ({ ...inProjectOf(t), fileId: fileOf(t) }) },
|
||||
"files.file.list": { input: (t) => inProjectOf(t) },
|
||||
"files.file.delete": { input: (t) => ({ ...inProjectOf(t), fileId: fileOf(t) }) },
|
||||
"proposals.proposal.create": {
|
||||
input: (t) => ({
|
||||
...inProjectOf(t),
|
||||
targetKey: "documents.line",
|
||||
change: {
|
||||
lineKey: "b".repeat(64),
|
||||
label: "Intruder",
|
||||
value: "x",
|
||||
page: 1,
|
||||
position: 0,
|
||||
quote: "Intruder: x",
|
||||
confidence: 0.5,
|
||||
edited: false,
|
||||
},
|
||||
// Nothing of the other tenant's: `create` reads no id it is given, so the only cross-tenant
|
||||
// case it has is the slugs, which the resolver refuses. An id of B's here would be echoed
|
||||
// back in the answer, which is the caller's own input and not B's data.
|
||||
evidence: [{ fileId: NOWHERE, page: 1 }],
|
||||
model: "intruder",
|
||||
promptVersion: "intruder.v1",
|
||||
}),
|
||||
},
|
||||
"proposals.proposal.edit": {
|
||||
input: (t) => ({
|
||||
...inProjectOf(t),
|
||||
proposalId: proposalOf(t),
|
||||
expectedVersion: 1,
|
||||
change: {
|
||||
lineKey: "c".repeat(64),
|
||||
label: "Intruder",
|
||||
value: "x",
|
||||
page: 1,
|
||||
position: 0,
|
||||
quote: "Intruder: x",
|
||||
confidence: 0.5,
|
||||
edited: true,
|
||||
},
|
||||
}),
|
||||
},
|
||||
"proposals.proposal.get": { input: (t) => ({ ...inProjectOf(t), proposalId: proposalOf(t) }) },
|
||||
"proposals.proposal.list": { input: (t) => inProjectOf(t) },
|
||||
"proposals.proposal.approve": {
|
||||
input: (t) => ({ ...inProjectOf(t), proposalId: proposalOf(t), expectedVersion: 1 }),
|
||||
},
|
||||
"proposals.proposal.approveMany": {
|
||||
input: (t) => {
|
||||
const proposalId = proposalOf(t)
|
||||
return { ...inProjectOf(t), proposalIds: [proposalId], expectations: { [proposalId]: { expectedVersion: 1 } } }
|
||||
},
|
||||
},
|
||||
"proposals.proposal.reject": {
|
||||
input: (t) => ({ ...inProjectOf(t), proposalId: proposalOf(t), reason: "intruder" }),
|
||||
},
|
||||
// kit:documents:begin
|
||||
"documents.document.list": { input: (t) => inProjectOf(t) },
|
||||
"documents.document.get": { input: (t) => ({ ...inProjectOf(t), fileId: fileOf(t) }) },
|
||||
// The live collection's read-back: another tenant's document id answers nothing.
|
||||
"documents.document.getMany": { input: (t) => ({ ...inProjectOf(t), ids: [documentOf(t)] }) },
|
||||
"documents.line.edit": {
|
||||
input: (t) => ({ ...inProjectOf(t), lineId: proposalOf(t), expectedVersion: 1, label: "Intruder", value: "x" }),
|
||||
},
|
||||
"documents.export.create": { input: (t) => ({ ...inProjectOf(t), fileId: fileOf(t) }) },
|
||||
// kit:documents:end
|
||||
"branding.brand.get": { input: (t) => ({ org: t.orgSlug }) },
|
||||
"branding.brand.set": { input: (t) => ({ org: t.orgSlug, primary: "#000000", displayName: "Intruder" }) },
|
||||
"branding.logo.set": {
|
||||
input: (t) => ({ org: t.orgSlug, variant: "light", contentType: "image/png", body: "iVBORw0KGgoAAAAA" }),
|
||||
},
|
||||
"branding.logo.clear": { input: (t) => ({ org: t.orgSlug, variant: "light" }) },
|
||||
"branding.logo.get": { input: (t) => ({ org: t.orgSlug, variant: "light" }) },
|
||||
}
|
||||
|
||||
const alsoCheck = [...TENANCY_ALSO_CHECK]
|
||||
|
||||
const exempt = {
|
||||
...tenancyIsolationExempt,
|
||||
"auth.session.current": "it answers only about the signed-in user's own row",
|
||||
...accountIsolationExempt,
|
||||
...guidesIsolationExempt, // kit:guides
|
||||
...supportTesting.supportIsolationExempt, // kit:support
|
||||
}
|
||||
|
||||
const options = async () => {
|
||||
const { a, b } = await tenants()
|
||||
return {
|
||||
registry,
|
||||
seed: async () => ({ a, b }),
|
||||
caller: (actor: Actor) => app.caller(actor),
|
||||
snapshot: () => snapshotOf(app.data, entities, { outbox: app.data.outbox }),
|
||||
fixtures,
|
||||
alsoCheck,
|
||||
exempt,
|
||||
}
|
||||
}
|
||||
|
||||
describeIsolation(
|
||||
{
|
||||
registry,
|
||||
seed: async () => tenants(),
|
||||
caller: (actor) => ({
|
||||
call: async () => undefined as never,
|
||||
callByName: async (name, input) => app.caller(actor).callByName(name, input),
|
||||
}),
|
||||
snapshot: async () => snapshotOf(app.data, entities, { outbox: app.data.outbox }),
|
||||
fixtures,
|
||||
alsoCheck,
|
||||
exempt,
|
||||
},
|
||||
{ describe, it },
|
||||
)
|
||||
|
||||
describe("the generated isolation suite", () => {
|
||||
it("checks current membership before preparing or observing service admission", async () => {
|
||||
const { a, b } = await tenants()
|
||||
const prepareServices = vi.fn()
|
||||
const observe = vi.fn(async () => {
|
||||
throw new Error("synthetic service unavailable")
|
||||
})
|
||||
const admission = { prepareServices, serviceReadiness: { profile: "postgres" as const, observe } }
|
||||
const outsider = app.caller({ userId: a.userId }, admission)
|
||||
await expect(outsider.callByName("inventory.item.list", inProjectOf(b))).rejects.toMatchObject({ status: 404 })
|
||||
expect(prepareServices).not.toHaveBeenCalled()
|
||||
expect(observe).not.toHaveBeenCalled()
|
||||
|
||||
const member = await signUp("removed-member@isolation.test")
|
||||
const owner = app.caller({ userId: a.userId })
|
||||
const invite = (await owner.callByName("tenancy.member.invite", {
|
||||
org: a.orgSlug,
|
||||
email: "removed-member@isolation.test",
|
||||
role: "viewer",
|
||||
})) as { token: string }
|
||||
await app.caller(member).callByName("tenancy.invitation.accept", { org: a.orgSlug, token: invite.token })
|
||||
await expect(app.caller(member, admission).callByName("inventory.item.list", inProjectOf(a))).rejects.toThrow()
|
||||
expect(prepareServices).toHaveBeenCalledOnce()
|
||||
expect(observe).toHaveBeenCalledOnce()
|
||||
await owner.callByName("tenancy.member.remove", { org: a.orgSlug, userId: member.userId })
|
||||
prepareServices.mockClear()
|
||||
observe.mockClear()
|
||||
// A new request must resolve fresh membership, before consulting the unavailable service.
|
||||
await expect(app.caller(member, admission).callByName("inventory.item.list", inProjectOf(a))).rejects.toMatchObject(
|
||||
{ status: 404 },
|
||||
)
|
||||
expect(prepareServices).not.toHaveBeenCalled()
|
||||
expect(observe).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("covers every procedure this app registers", async () => {
|
||||
const result = await runIsolation(await options())
|
||||
expect(result.failures).toEqual([])
|
||||
// One case per live collection too (TS6.5): its read-back, by the collection's name.
|
||||
expect(result.passed.length).toBe(
|
||||
registry.isolationFixtures.length + alsoCheck.length + Object.keys(registry.collections).length,
|
||||
)
|
||||
expect(result.passed).toContain("live notes.note") // kit:notes
|
||||
})
|
||||
|
||||
// kit:notes:begin
|
||||
it("covers every procedure of the notes and files modules, by name", async () => {
|
||||
const result = await runIsolation(await options())
|
||||
const covered = new Set(result.passed)
|
||||
const names = [...registry.procedures.keys()].filter((name) => /^(notes|files)\./.test(name))
|
||||
expect(names.length).toBe(15)
|
||||
for (const name of names) expect(covered.has(name), name).toBe(true)
|
||||
})
|
||||
|
||||
it("fails, naming it, when a module's own fixture is missing", async () => {
|
||||
const base = await options()
|
||||
const { "notes.attachment.remove": _removed, ...rest } = fixtures
|
||||
const result = await runIsolation({ ...base, fixtures: rest })
|
||||
expect(result.failures.map((failure) => failure.procedure)).toEqual(["notes.attachment.remove"])
|
||||
})
|
||||
// kit:notes:end
|
||||
|
||||
// kit:guides:begin
|
||||
it("reaches the guides platform with no cross-tenant call", async () => {
|
||||
await runIsolation(await options())
|
||||
expect(guidesPlatform.calls).toEqual([])
|
||||
})
|
||||
|
||||
// kit:guides:end
|
||||
// kit:support:begin
|
||||
it("reaches the support platform with no cross-tenant call", async () => {
|
||||
await runIsolation(await options())
|
||||
expect(supportPlatform.calls).toEqual([])
|
||||
})
|
||||
|
||||
// kit:support:end
|
||||
it("fails, naming it, when a procedure has no fixture", async () => {
|
||||
const base = await options()
|
||||
const { "tenancy.member.list": _removed, ...rest } = fixtures
|
||||
const result = await runIsolation({ ...base, fixtures: rest })
|
||||
expect(result.failures.map((failure) => failure.procedure)).toEqual(["tenancy.member.list"])
|
||||
})
|
||||
})
|
||||
|
||||
// This journey requires explicit optional-module admission.
|
||||
vi.mock("../modules/inclusion.js", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("../modules/inclusion.js")>()),
|
||||
...(await import("./support/native-readiness.js")).selectedModules(
|
||||
(await importOriginal<typeof import("../modules/inclusion.js")>()).OPTIONAL_MODULE_IDS,
|
||||
),
|
||||
}))
|
||||
Reference in New Issue
Block a user