145 lines
6.1 KiB
TypeScript
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" },
|
|
})
|
|
})
|
|
})
|