Files
template-app/modules/knowledge/__tests__/procedure-kinds.test.ts
T
2026-11-14 09:56:45 +00:00

145 lines
6.1 KiB
TypeScript

// Every knowledge procedure a page calls, run as the kernel runs it: `createCaller` hands a handler the
// container of its kind, and that container refuses a port above the kind. `knowledge.status` backs the
// knowledge page and `knowledge.settings.get` the organization's settings page, so a handler that reaches
// for an effect port from a read is a 500 `kind_violation` on those pages on every real stack. The ports
// stay effects: what changes is what each handler asks for.
import { Container, type ProcedureKind, type TenantScope } from "@conusai-kit/core"
import { createMemoryDataSource, DATA_SOURCE, OBJECT_STORE } from "@conusai-kit/data"
import { files } from "@conusai-kit/files"
import { fakeObjectStore } from "@conusai-kit/testing"
import { describe, expect, it, vi } from "vitest"
import {
knowledgeStatusImpl,
settingsDisableDocumentAiImpl,
settingsEnableDocumentAiImpl,
settingsGetImpl,
} from "../adapters/procedures.js"
import { platformKnowledgeReadiness } from "../adapters/readiness.js"
import { KNOWLEDGE_ENTITIES } from "../contract/index.js"
import {
type DocumentAiPlatform,
KNOWLEDGE_ENABLEMENT,
KNOWLEDGE_READINESS,
KNOWLEDGE_SEARCH,
type KnowledgeScope,
} from "../core/ports.js"
const scope: TenantScope & KnowledgeScope = {
level: "project",
actor: { userId: "01900000-0000-7000-0000-000000000009" },
orgId: "01900000-0000-7000-0000-000000000001",
workspaceId: "01900000-0000-7000-0000-000000000002",
projectId: "01900000-0000-7000-0000-000000000003",
}
const projectInput = { org: "acme", workspace: "general", project: "demo" }
const orgInput = { org: "acme" }
const processContainer = () => {
const container = new Container()
container.value(DATA_SOURCE, createMemoryDataSource({ entities: [...KNOWLEDGE_ENTITIES, files] }))
container.value(OBJECT_STORE, fakeObjectStore())
return container
}
/** A handler as the kernel calls it (`createCaller`): with the container of its procedure's kind. */
const run = (
impl: { readonly kind: ProcedureKind; readonly handler: (call: never) => unknown },
input: unknown,
container: Container,
) => impl.handler({ input, ctx: { container: container.restrict(impl.kind), scope } } as never)
const neverBuilt = () =>
vi.fn((): never => {
throw new Error("a port above this procedure's kind was built")
})
const platform: DocumentAiPlatform = {
checkEligibility: async () => ({ ok: true }),
indexingQuota: async () => 1000,
}
describe("the ports that leave the process", () => {
it("stay refused to a read and a write", () => {
const container = processContainer()
container.value(KNOWLEDGE_ENABLEMENT, platform)
container.register(KNOWLEDGE_SEARCH, neverBuilt())
for (const kind of ["read", "write"] as const) {
expect(() => container.restrict(kind).resolve(KNOWLEDGE_SEARCH)).toThrow(/cannot resolve knowledge.search/)
expect(() => container.restrict(kind).resolve(KNOWLEDGE_ENABLEMENT)).toThrow(
/cannot resolve knowledge.enablement/,
)
}
})
})
describe("the knowledge page's knowledge.status", () => {
it("answers through a read handler's container, and never builds the search service", async () => {
const container = processContainer()
const search = neverBuilt()
container.register(KNOWLEDGE_SEARCH, search)
await expect(run(knowledgeStatusImpl, projectInput, container)).resolves.toMatchObject({
documents: { queued: 0, indexing: 0, indexed: 0, failed: 0, cancelled: 0 },
})
expect(search).not.toHaveBeenCalled()
})
})
describe("the settings page's knowledge.settings", () => {
it("get answers through a read handler's container, without the enablement platform", async () => {
const container = processContainer()
const enablement = neverBuilt()
container.register(KNOWLEDGE_ENABLEMENT, enablement)
await expect(run(settingsGetImpl, orgInput, container)).resolves.toEqual({
enabled: false,
backend: null,
})
expect(enablement).not.toHaveBeenCalled()
})
it("enable reaches the platform and stores the configuration", async () => {
const container = processContainer()
container.value(KNOWLEDGE_ENABLEMENT, platform)
await expect(run(settingsEnableDocumentAiImpl, orgInput, container)).resolves.toEqual({
enabled: true,
backend: "platform",
})
await expect(run(settingsGetImpl, orgInput, container)).resolves.toEqual({ enabled: true, backend: "platform" })
})
it("disable only flips the stored flag, without the enablement platform", async () => {
const container = processContainer()
const enablement = neverBuilt()
container.register(KNOWLEDGE_ENABLEMENT, enablement)
await expect(run(settingsDisableDocumentAiImpl, orgInput, container)).resolves.toEqual({ enabled: false })
expect(enablement).not.toHaveBeenCalled()
})
})
describe("what knowledge.status says of the backend", () => {
it("says what the bound readiness says, and asks it with the request's scope", async () => {
const container = processContainer()
const readiness = vi.fn(async () => ({ state: "degraded" as const, reasons: ["the index is behind"] }))
container.value(KNOWLEDGE_READINESS, { readiness })
await expect(run(knowledgeStatusImpl, projectInput, container)).resolves.toMatchObject({
service: { state: "degraded", reasons: ["the index is behind"] },
})
expect(readiness).toHaveBeenCalledWith(scope)
})
it("says the backend is not bound in a process that has none", async () => {
await expect(run(knowledgeStatusImpl, projectInput, processContainer())).resolves.toMatchObject({
service: { state: "unavailable", reasons: ["the knowledge search service is not bound in this process"] },
})
})
it("reports the managed service failing to answer as unavailable, not as an error page", async () => {
const container = processContainer()
const client = { readiness: vi.fn(async () => Promise.reject(new Error("no answer\nstack"))) }
container.value(KNOWLEDGE_READINESS, platformKnowledgeReadiness(client as never))
await expect(run(knowledgeStatusImpl, projectInput, container)).resolves.toMatchObject({
service: { state: "unavailable", reasons: [], refusal: "runtime_unavailable" },
})
})
})