158 lines
7.0 KiB
TypeScript
158 lines
7.0 KiB
TypeScript
import { syntheticNativeReadiness } from "../../../test/support/native-readiness.js"
|
|
// A refusal of the platform's knowledge service reaches a person as one typed code: the procedures end to
|
|
// end, on the real module, with the service's client refusing underneath; and each code a form looks up.
|
|
|
|
import { AppError, buildRegistry, Container, createCaller, type TransportCaller } from "@conusai-kit/core"
|
|
import { createMemoryDataSource, DATA_SOURCE, entitiesOf, OBJECT_STORE, SYSTEM_DATA } from "@conusai-kit/data"
|
|
import { FILES_REGISTRY, filesModule } from "@conusai-kit/files"
|
|
import { createI18n } from "@conusai-kit/i18n"
|
|
import { KnowledgeError, type KnowledgeReasonCode } from "@conusai-kit/platform"
|
|
import { createTenancyHooks, TENANCY, tenancyModule, users } from "@conusai-kit/tenancy"
|
|
import { fakeObjectStore } from "@conusai-kit/testing"
|
|
import { describe, expect, it } from "vitest"
|
|
import { knowledgeRefusal } from "../adapters/refusals.js"
|
|
import { KNOWLEDGE_REFUSALS, type KnowledgeRefusal } from "../contract/index.js"
|
|
import {
|
|
KNOWLEDGE_ENABLEMENT,
|
|
KNOWLEDGE_FLAGS,
|
|
KNOWLEDGE_READINESS,
|
|
KNOWLEDGE_SEARCH,
|
|
type KnowledgeSearchService,
|
|
} from "../core/ports.js"
|
|
import { knowledgeModule } from "../index.js"
|
|
import { refusalLabels, refusalOf } from "../ui/labels.js"
|
|
|
|
async function app(
|
|
service: KnowledgeSearchService,
|
|
options: { flags?: boolean } = {},
|
|
): Promise<{ call: TransportCaller; slugs: Record<string, string> }> {
|
|
const registry = buildRegistry([tenancyModule, filesModule, knowledgeModule])
|
|
const data = createMemoryDataSource({ entities: entitiesOf(registry, "postgres") })
|
|
const root = new Container()
|
|
root.value(DATA_SOURCE, data)
|
|
root.value(SYSTEM_DATA, data)
|
|
root.value(OBJECT_STORE, fakeObjectStore())
|
|
root.value(FILES_REGISTRY, registry)
|
|
root.value(KNOWLEDGE_SEARCH, service)
|
|
root.value(KNOWLEDGE_READINESS, service)
|
|
root.value(KNOWLEDGE_ENABLEMENT, { checkEligibility: async () => ({ ok: true }), indexingQuota: async () => 1000 })
|
|
if (options.flags !== false) root.value(KNOWLEDGE_FLAGS, { enabled: () => true })
|
|
const user = await data.global(users).insert({
|
|
issuer: "https://id.example.test",
|
|
subject: "owner",
|
|
email: "owner@example.test",
|
|
displayName: "Owner",
|
|
locale: "en",
|
|
})
|
|
const container = root.child()
|
|
const tenancy = createTenancyHooks({ data, registry })
|
|
container.value(TENANCY, tenancy)
|
|
const call = createCaller(
|
|
{ registry, container, tenancy, serviceReadiness: syntheticNativeReadiness },
|
|
{ actor: { userId: user.id } },
|
|
)
|
|
await call.callByName("tenancy.org.create", { slug: "acme", name: "Acme" })
|
|
await call.callByName("tenancy.workspace.create", { org: "acme", slug: "ws", name: "WS", visibility: "org" })
|
|
await call.callByName("tenancy.project.create", {
|
|
org: "acme",
|
|
workspace: "ws",
|
|
slug: "p",
|
|
name: "P",
|
|
moduleKey: "knowledge",
|
|
})
|
|
return { call, slugs: { org: "acme", workspace: "ws", project: "p" } }
|
|
}
|
|
|
|
const refusing = (reason: KnowledgeReasonCode): KnowledgeSearchService => ({
|
|
readiness: async () => ({ state: "disabled", reasons: [], refusal: "plan_required" }),
|
|
index: async () => undefined,
|
|
search: async () => {
|
|
throw new KnowledgeError(reason, "the service's own words")
|
|
},
|
|
remove: async () => undefined,
|
|
})
|
|
|
|
describe("a refused search", () => {
|
|
it.each([
|
|
["plan_required", "plan_required", 403],
|
|
["feature_disabled", "feature_disabled", 403],
|
|
["quota_exhausted", "quota_exhausted", 429],
|
|
["budget_exhausted", "budget_exhausted", 429],
|
|
["forbidden", "scope_missing", 503],
|
|
["indexing", "indexing", 503],
|
|
["approval_required", "approval_required", 403],
|
|
["runtime_unavailable", "runtime_unavailable", 503],
|
|
["unauthenticated", "runtime_unavailable", 503],
|
|
] as const)("by %s answers %s", async (reason, code, status) => {
|
|
const { call, slugs } = await app(refusing(reason))
|
|
await call.callByName("knowledge.collection.create", { ...slugs, name: "contracts" })
|
|
const refused = await call
|
|
.callByName("knowledge.search", { ...slugs, collectionName: "contracts", query: "anything" })
|
|
.catch((error: unknown) => error)
|
|
expect(refused).toBeInstanceOf(AppError)
|
|
expect((refused as AppError).problem).toMatchObject({ code, status })
|
|
})
|
|
|
|
it("leaves a resource the service does not hold as not found, and a programming error as it is", () => {
|
|
const missing = knowledgeRefusal(new KnowledgeError("unavailable_resource", "no such collection"))
|
|
expect((missing as AppError).problem.status).toBe(404)
|
|
const bug = new TypeError("not a refusal")
|
|
expect(knowledgeRefusal(bug)).toBe(bug)
|
|
})
|
|
})
|
|
|
|
describe("the status a page reads", () => {
|
|
it("carries the service's refusal through the read procedure", async () => {
|
|
const { call, slugs } = await app(refusing("plan_required"))
|
|
expect(await call.callByName("knowledge.status", slugs)).toMatchObject({
|
|
enabled: true,
|
|
service: { state: "disabled", reasons: [], refusal: "plan_required" },
|
|
})
|
|
})
|
|
})
|
|
|
|
describe("enabling document AI", () => {
|
|
const ready: KnowledgeSearchService = {
|
|
readiness: async () => ({ state: "ready", reasons: [] }),
|
|
index: async () => undefined,
|
|
search: async () => [],
|
|
remove: async () => undefined,
|
|
}
|
|
|
|
it("turns knowledge on for the organization's projects, and disabling turns it off again", async () => {
|
|
const { call, slugs } = await app(ready, { flags: false })
|
|
const enabled = async () => ((await call.callByName("knowledge.status", slugs)) as { enabled: boolean }).enabled
|
|
expect(await enabled()).toBe(false)
|
|
await call.callByName("knowledge.settings.enableDocumentAi", { org: slugs.org })
|
|
expect(await enabled()).toBe(true)
|
|
await call.callByName("knowledge.settings.disableDocumentAi", { org: slugs.org })
|
|
expect(await enabled()).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe("a form's refusal", () => {
|
|
const t = (key: string) => key
|
|
it("names every typed refusal, and leaves any other error to the form's own text", () => {
|
|
const labels = refusalLabels(t)
|
|
expect(Object.keys(labels).sort()).toEqual([...KNOWLEDGE_REFUSALS].sort())
|
|
const refused = (code: KnowledgeRefusal) => ({ problem: { code } })
|
|
expect(refusalOf(labels, refused("scope_missing"))).toEqual({
|
|
code: "scope_missing",
|
|
text: "refusals.scope_missing",
|
|
})
|
|
expect(refusalOf(labels, { problem: { code: "not_found" } })).toBeUndefined()
|
|
expect(refusalOf(labels, new Error("boom"))).toBeUndefined()
|
|
})
|
|
|
|
it("has a message in both languages, the Lithuanian one its own", () => {
|
|
const catalogues = buildRegistry([tenancyModule, filesModule, knowledgeModule]).catalogues
|
|
const en = createI18n("en", { en: { knowledge: catalogues.en?.knowledge ?? {} } })
|
|
const lt = createI18n("lt", { lt: { knowledge: catalogues.lt?.knowledge ?? {} } })
|
|
for (const code of KNOWLEDGE_REFUSALS) {
|
|
const english = en.message(`knowledge.refusals.${code}`)
|
|
expect(english, code).not.toContain("refusals.")
|
|
expect(lt.message(`knowledge.refusals.${code}`), code).not.toBe(english)
|
|
}
|
|
})
|
|
})
|