@@ -0,0 +1,157 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user