// The knowledge page on F10: its loader reads through the caller and returns plain data with every label // translated, and its view renders what reached the browser. The service's health, the indexing indicator // and the search stay three separate things: "indexing" is not "down". import { AppError, buildRegistry, Container, notFound, type TransportCaller } from "@conusai-kit/core" import { builtInCatalogues, createI18n, uiLabels } from "@conusai-kit/i18n" import { createDataClient, DataProvider } from "@conusai-kit/ui/data" import { UiLabelsProvider } from "@conusai-kit/ui/primitives" import { renderToStaticMarkup as renderMarkup } from "react-dom/server" import { describe, expect, it, vi } from "vitest" import { appModules } from "../../index.js" import { modulePages } from "../../pages.js" import { knowledgePage } from "../ui/pages.js" const kitStrings = uiLabels(createI18n("en", builtInCatalogues)) const renderToStaticMarkup = (node: React.ReactNode) => renderMarkup({node}) const catalogues = buildRegistry([...appModules]).catalogues const i18n = createI18n("en", { en: { knowledge: catalogues.en?.knowledge ?? {}, app: { failed: "That did not work: {detail}" } }, }) const slugs = { org: "acme", workspace: "ws", project: "p" } const callerOf = (answers: Record, asked: unknown[] = []): TransportCaller => ({ call: async (contract: { name: string }, input: unknown) => { asked.push([contract.name, input]) const answer = answers[contract.name] if (answer instanceof Error) throw answer return answer }, }) as unknown as TransportCaller const load = (segments: string[], answers: Record, asked?: unknown[]) => knowledgePage.load({ slugs, segments, search: {}, caller: callerOf(answers, asked), i18n, container: new Container(), }) /** The page as the shell renders it: the loader's data through JSON, as the browser receives it, then the view. */ const render = async (answers: Record) => { const data = await load([], answers) expect(JSON.parse(JSON.stringify(data))).toStrictEqual(data) const { View } = knowledgePage return renderToStaticMarkup( new Response(null, { status: 501 }) })}> , ) } const document = (documentId: string, status: string, error: string | null) => ({ documentId, collectionName: "contracts", fileId: `file-${documentId}`, filename: `${documentId}.pdf`, status, error, indexedAt: null, requestedBy: "u1", }) const status = (enabled: boolean, state: string, reasons: readonly string[] = [], refusal?: string) => ({ enabled, service: { state, reasons, ...(refusal === undefined ? {} : { refusal }) }, documents: { queued: 1, indexing: 0, indexed: 2, failed: 0, cancelled: 0 }, }) const answers = { "knowledge.collection.list": { collections: [{ name: "contracts", documentCount: 2, indexedCount: 1, createdBy: "u1" }], }, "knowledge.status": status(true, "degraded", ["the search index is behind"]), "knowledge.document.list": { documents: [document("d1", "indexed", null), document("d2", "failed", "the file could not be read")], }, "knowledge.autoindex.get": { collectionName: null }, } describe("the knowledge page", () => { it("is listed for the shell", async () => { await expect(modulePages.knowledge?.()).resolves.toBe(knowledgePage) }) it("shows the service's health, the collection and its documents in words", async () => { const html = await render(answers) expect(html).toContain("

the search index is behind") expect(html).toContain(">contracts

") expect(html).toContain("d1.pdf") expect(html).toContain("the file could not be read") expect(html).toContain('role="grid"') expect(html.match(/role="columnheader"/g)).toHaveLength(3) }) // core-11: the project's automatic PDF indexing, off or naming its collection, with the project's slugs. it("offers automatic PDF indexing into one of the collections, showing the current choice", async () => { const asked: unknown[] = [] const off = await load([], answers, asked) expect(asked).toContainEqual(["knowledge.autoindex.get", slugs]) expect(off.autoIndex.current).toBeNull() const html = await render(answers) expect(html).toContain("Index uploaded PDFs automatically") const on = await load([], { ...answers, "knowledge.autoindex.get": { collectionName: "contracts" } }) expect(on.autoIndex.current).toBe("contracts") }) it("does not offer automatic indexing while the module is off", async () => { const html = await render({ ...answers, "knowledge.status": status(false, "disabled") }) expect(html).not.toContain("Index uploaded PDFs automatically") }) it("reads each collection's documents with the project's slugs", async () => { const asked: unknown[] = [] await load([], answers, asked) expect(asked).toContainEqual(["knowledge.document.list", { ...slugs, collectionName: "contracts" }]) }) it("says the module is off in place of its collections", async () => { const html = await render({ ...answers, "knowledge.status": status(false, "disabled") }) expect(html).toContain('role="status"') expect(html).not.toContain('role="grid"') expect(html).not.toContain("
") }) it.each([ ["plan_required", "The organization's plan does not include document search."], ["feature_disabled", "Document search is switched off for this organization by platform policy."], ["quota_exhausted", "Today's indexing allowance is spent. Try again tomorrow."], ["budget_exhausted", "The organization's AI budget is spent. An administrator can raise it on the platform."], [ "scope_missing", "This deployment's token does not hold knowledge.collections:use. Deploy the app again, then have an owner or admin approve it.", ], ["indexing", "The service is building its index. Try again in a few minutes."], ["approval_required", "An administrator must approve this on the platform first."], ["runtime_unavailable", "The knowledge service did not answer. Try again later."], ])("shows the refusal %s as the page's own state", async (refusal, text) => { const html = await render({ ...answers, "knowledge.status": status(true, "unavailable", [], refusal), }) expect(html).toContain(`data-state="${refusal}"`) expect(html).toContain('role="status"') expect(html).toContain(text.replaceAll("'", "'")) }) it("shows a refusal in Lithuanian to a Lithuanian reader", async () => { const lt = createI18n("lt", { lt: { knowledge: catalogues.lt?.knowledge ?? {}, app: { failed: "Nepavyko: {detail}" } }, }) const data = await knowledgePage.load({ slugs, segments: [], search: {}, caller: callerOf({ ...answers, "knowledge.status": status(true, "disabled", [], "plan_required") }), i18n: lt, container: new Container(), }) expect(data.status.refusal).toEqual({ code: "plan_required", text: "Organizacijos planas dokumentų paieškos neapima.", }) }) it("says a document's failure in words, and keeps a code it does not know", async () => { const html = await render({ ...answers, "knowledge.document.list": { documents: [document("d1", "failed", "quota_exhausted"), document("d2", "failed", "something_new")], }, }) // Worded in the browser from the failure labels the loader handed over: the list is live. expect(html).toContain(">Today's indexing allowance is spent. Try again tomorrow.<") expect(html).toContain(">something_new<") }) it("answers a path it does not know as not found, without reading", async () => { const call = vi.fn() await expect( knowledgePage.load({ slugs, segments: ["extra"], search: {}, caller: { call } as unknown as TransportCaller, i18n, container: new Container(), }), ).rejects.toMatchObject({ status: 404 }) expect(call).not.toHaveBeenCalled() }) it("hands a procedure's refusal on, as the AppError the shell answers", async () => { const refused = load([], { ...answers, "knowledge.collection.list": notFound("no such project") }) await expect(refused).rejects.toBeInstanceOf(AppError) await expect(refused).rejects.toMatchObject({ status: 404 }) }) }) // Explicit synthetic admission: module-local fixtures exercise the selected module. vi.mock("../../inclusion.json", () => ({ default: { schemaVersion: 1, selected: ["knowledge"], everAdopted: ["knowledge"] }, }))