ConusAI vendor v0.1.0-beta.114
ci / gate (push) Waiting to run

This commit is contained in:
ConusAI vendor v0.1.0-beta.114
2026-11-14 09:56:45 +00:00
commit 1ce8d23c35
867 changed files with 122104 additions and 0 deletions
@@ -0,0 +1,62 @@
import { Container, type TenantScope } from "@conusai-kit/core"
import { createMemoryDataSource, DATA_SOURCE, SYSTEM_DATA } from "@conusai-kit/data"
import { accountData, projects } from "@conusai-kit/tenancy"
import { expect, it } from "vitest"
import { custodyModule } from "../../custody.js"
import { knowledgeAccountData } from "../adapters/account.js"
import { autoIndex, KNOWLEDGE_ENTITIES } from "../contract/index.js"
import { knowledgeModule } from "../module.js"
const subject = "01900000-0000-7000-0000-000000000009"
async function fixture() {
const data = createMemoryDataSource({ entities: [projects, ...KNOWLEDGE_ENTITIES] })
const container = new Container()
container.value(DATA_SOURCE, data)
container.value(SYSTEM_DATA, data)
const scopes: TenantScope[] = []
for (const n of [1, 2, 3]) {
const orgId = `01900000-0000-7000-0000-00000000000${n}`
const workspaceId = `01900000-0000-7000-0000-00000000001${n}`
const actor = { userId: subject }
const project = await data
.forScope({ level: "workspace", orgId, workspaceId, actor })
.of(projects)
.insert({ slug: "site", name: "Site", moduleKey: "notes", archivedAt: null })
const scope: TenantScope = { level: "project", orgId, workspaceId, projectId: project.id, actor }
scopes.push(scope)
await data
.forScope(scope)
.of(autoIndex)
.insert({
collectionName: "contracts",
enabledBy: n === 3 ? "another-person" : subject,
enabledAt: "2026-11-11T00:00:00.000Z",
})
}
return { data, container, scopes }
}
it("exports and erases only that administrator’s opt-ins across projects, idempotently", async () => {
const { data, container, scopes } = await fixture()
const exported = await knowledgeAccountData.export?.({ subject, container })
expect(exported?.automaticIndexing).toHaveLength(2)
await knowledgeAccountData.erase?.({ subject, container })
expect(await knowledgeAccountData.export?.({ subject, container })).toEqual({ automaticIndexing: [] })
await knowledgeAccountData.erase?.({ subject, container })
const otherScope = scopes[2]
if (!otherScope) throw new Error("Missing unrelated project fixture")
expect((await data.forScope(otherScope).of(autoIndex).list()).items).toHaveLength(1)
})
it("retains the account contribution when processing is deselected", () => {
const retained = custodyModule(knowledgeModule)
expect(retained.extends?.find((entry) => entry.point.id === accountData.id)?.values).toEqual([knowledgeAccountData])
})
it("an aborted account operation changes no opt-in", async () => {
const { data, container, scopes } = await fixture()
const controller = new AbortController()
controller.abort()
await expect(knowledgeAccountData.erase?.({ subject, container, signal: controller.signal })).rejects.toThrow()
for (const scope of scopes) expect((await data.forScope(scope).of(autoIndex).list()).items).toHaveLength(1)
})
@@ -0,0 +1,309 @@
// The automatic PDF intake (template lesson core-11): an uploaded PDF of an opted-in project is read by
// the platform's document reader and indexed passage by passage into the opted-in collection, for the
// administrator who turned it on, whose permission is checked again before each step that leaves the
// process. A project that is not opted in, or has knowledge off, reads nothing; removing the document
// tombstones every passage it indexed, even once the file itself is gone.
import type { TenantScope } from "@conusai-kit/core"
import { createMemoryDataSource } from "@conusai-kit/data"
import { DEFAULT_FILES_POLICY, files, filesProcessors, sha256Hex } from "@conusai-kit/files"
import { fakeObjectStore } from "@conusai-kit/testing"
import { describe, expect, it } from "vitest"
import {
AUTO_INDEX_JOB,
AUTO_INDEX_PROCESSOR_KEY,
autoIndex,
collections,
documents,
KNOWLEDGE_ENTITIES,
passageSets,
} from "../contract/index.js"
import { autoIndexUpload } from "../core/auto-index-job.js"
import { removeDocument } from "../core/index-job.js"
import type {
DocumentAiPlatform,
KnowledgeDeps,
KnowledgePassage,
KnowledgePassageIndex,
KnowledgeReader,
} from "../core/ports.js"
import { knowledgeModule } from "../module.js"
const ADMIN = "01900000-0000-7000-0000-000000000009"
const UPLOADER = "01900000-0000-7000-0000-000000000010"
const scope: TenantScope = {
level: "project",
actor: { userId: "system:knowledge.autoindex", system: true },
orgId: "01900000-0000-7000-0000-000000000001",
workspaceId: "01900000-0000-7000-0000-000000000002",
projectId: "01900000-0000-7000-0000-000000000003",
}
const pdf = new TextEncoder().encode("%PDF-1.7 synthetic")
const read: readonly KnowledgePassage[] = [
{ page: 1, index: 0, blockIds: ["b1", "b2"], text: "First page, first passage" },
{ page: 1, index: 1, blockIds: ["b3"], text: "First page, second passage" },
{ page: 3, index: 0, blockIds: [], text: "Third page" },
]
function makeDeps(
options: { enabled?: boolean; reader?: KnowledgeReader; indexError?: Error; enablement?: DocumentAiPlatform } = {},
) {
const data = createMemoryDataSource({ entities: [...KNOWLEDGE_ENTITIES, files] })
const store = fakeObjectStore()
const calls = {
reads: [] as Array<{ filename: string; idempotencyKey: string }>,
indexed: [] as Array<{ collectionName: string; fileId: string; passages: readonly KnowledgePassage[] }>,
removed: [] as Array<{ documentRef: string; passages: ReadonlyArray<{ page: number; index: number }> }>,
admitted: [] as string[],
}
const reader: KnowledgeReader = options.reader ?? {
async read(bytes, input) {
expect(bytes).toEqual(pdf)
calls.reads.push({ filename: input.filename, idempotencyKey: input.idempotencyKey })
return read
},
}
const passages: KnowledgePassageIndex = {
async reference(_scope, fileId) {
return `org/${scope.orgId}/${fileRow.storageKey}?${fileId}`
},
async index(_scope, input) {
expect(input.documentRef).toBe(`org/${scope.orgId}/${fileRow.storageKey}?${input.fileId}`)
calls.indexed.push({ collectionName: input.collectionName, fileId: input.fileId, passages: input.passages })
if (options.indexError) throw options.indexError
},
async remove(_scope, input) {
calls.removed.push({ documentRef: input.documentRef, passages: input.passages })
},
}
const deps: KnowledgeDeps = {
data,
files: { data, store, policy: DEFAULT_FILES_POLICY },
flags: { enabled: async () => options.enabled ?? true },
reader,
passages,
enablement: options.enablement ?? {
checkEligibility: async () => ({ ok: true }),
indexingQuota: async () => 1000,
},
beforeIndex: async (subject) => {
calls.admitted.push(subject)
},
}
return { data, store, deps, calls }
}
const fileRow = {
status: "stored" as const,
storageKey: `files/${scope.workspaceId}/${scope.projectId}/01900000-0000-7000-0000-000000000100`,
filename: "contract.pdf",
contentType: "application/pdf",
size: pdf.length,
sha256: "",
uploadedBy: UPLOADER,
storedAt: "2026-11-09T19:00:00.000Z",
uploadId: null,
declaredSize: null,
partSize: null,
deletingAt: null,
}
async function seed(fixture: ReturnType<typeof makeDeps>, options: { optIn?: boolean } = {}) {
const sha256 = await sha256Hex(pdf)
await fixture.data.forScope(scope).of(collections).insert({ name: "contracts", createdBy: ADMIN })
if (options.optIn ?? true)
await fixture.data
.forScope(scope)
.of(autoIndex)
.insert({ collectionName: "contracts", enabledBy: ADMIN, enabledAt: "2026-11-09T18:00:00.000Z" })
const file = await fixture.data
.forScope(scope)
.of(files)
.insert({ ...fileRow, sha256 })
await fixture.store.put(scope, { key: fileRow.storageKey, body: pdf, contentType: "application/pdf" })
return { fileId: file.id, storageKey: fileRow.storageKey, contentType: "application/pdf", sha256 }
}
/** The run's document, which these cases expect there to be. */
async function indexedView<T>(view: T | null): Promise<T> {
if (view === null) throw new Error("the upload made no document")
return view
}
describe("the automatic PDF intake", () => {
it("reads an opted-in project's PDF and indexes its passages into the collection, for the administrator who opted in", async () => {
const fixture = makeDeps()
const upload = await seed(fixture)
const done = await autoIndexUpload(fixture.deps, scope, upload)
expect(done).toMatchObject({ collectionName: "contracts", fileId: upload.fileId, status: "indexed" })
expect(fixture.calls.reads).toEqual([
{ filename: "contract.pdf", idempotencyKey: `knowledge.autoindex.${upload.fileId}.${upload.sha256}` },
])
expect(fixture.calls.indexed).toEqual([{ collectionName: "contracts", fileId: upload.fileId, passages: read }])
// The job runs for nobody else: the administrator's permission is what each step leaving the process checks.
expect(fixture.calls.admitted.length).toBeGreaterThan(0)
expect(new Set(fixture.calls.admitted)).toEqual(new Set([ADMIN]))
const [row] = (await fixture.data.forScope(scope).of(documents).list({ limit: 10 })).items
expect(row).toMatchObject({ filename: "contract.pdf", requestedBy: ADMIN, status: "indexed", error: null })
// What removal needs, kept beside the document: the file's reference and the passages per page.
const [set] = (await fixture.data.forScope(scope).of(passageSets).list({ limit: 10 })).items
expect(set).toMatchObject({
collectionName: "contracts",
fileId: upload.fileId,
documentRef: `org/${scope.orgId}/${fileRow.storageKey}?${upload.fileId}`,
pages: "2,0,1",
})
})
it("is the same work once: a redelivered job reads and indexes nothing again", async () => {
const fixture = makeDeps()
const upload = await seed(fixture)
await autoIndexUpload(fixture.deps, scope, upload)
expect(await autoIndexUpload(fixture.deps, scope, upload)).toMatchObject({ status: "indexed" })
expect(fixture.calls.reads).toHaveLength(1)
expect(fixture.calls.indexed).toHaveLength(1)
})
it("reads nothing for a project that is not opted in, has knowledge off, or a file that is not a PDF", async () => {
const notOptedIn = makeDeps()
expect(await autoIndexUpload(notOptedIn.deps, scope, await seed(notOptedIn, { optIn: false }))).toBeNull()
const off = makeDeps({ enabled: false })
expect(await autoIndexUpload(off.deps, scope, await seed(off))).toBeNull()
const text = makeDeps()
const upload = await seed(text)
expect(await autoIndexUpload(text.deps, scope, { ...upload, contentType: "text/plain" })).toBeNull()
for (const fixture of [notOptedIn, off, text]) {
expect(fixture.calls.reads).toEqual([])
expect((await fixture.data.forScope(scope).of(documents).list({ limit: 10 })).items).toEqual([])
}
})
it("reads nothing once the administrator's permission is gone: the refusal is the job's", async () => {
const fixture = makeDeps()
const upload = await seed(fixture)
const refused = Object.assign(new Error("permission_missing"), { status: 403 })
const deps: KnowledgeDeps = {
...fixture.deps,
beforeIndex: async () => {
throw refused
},
}
await expect(autoIndexUpload(deps, scope, upload)).rejects.toBe(refused)
expect(fixture.calls.reads).toEqual([])
expect(fixture.calls.indexed).toEqual([])
})
it("records a PDF the reader cannot make passages of as failed, with a stable code", async () => {
const fixture = makeDeps({
reader: {
read: async () => {
throw Object.assign(new Error("block id over 256 characters"), { permanent: "not_indexable" })
},
},
})
const upload = await seed(fixture)
expect(await autoIndexUpload(fixture.deps, scope, upload)).toMatchObject({
status: "failed",
error: "not_indexable",
})
expect(fixture.calls.indexed).toEqual([])
})
it("keeps the passage layout of an index the service refused partway, so removal still tombstones it", async () => {
const fixture = makeDeps({ indexError: Object.assign(new Error("quota"), { permanent: "quota_exhausted" }) })
const upload = await seed(fixture)
const failed = await autoIndexUpload(fixture.deps, scope, upload)
expect(failed).toMatchObject({ status: "failed", error: "quota_exhausted" })
await removeDocument(fixture.deps, scope, (await indexedView(failed)).documentId)
expect(fixture.calls.removed.map((call) => call.passages.length)).toEqual([3])
expect((await fixture.data.forScope(scope).of(passageSets).list({ limit: 10 })).items).toEqual([])
})
it("reads nothing once the organization is no longer eligible or its indexing quota is spent", async () => {
const ineligible = makeDeps({
enablement: {
checkEligibility: async () => ({
ok: false,
reason: "feature_disabled",
detail: "the agent_document_tools flag is off",
}),
indexingQuota: async () => 1000,
},
})
expect(await autoIndexUpload(ineligible.deps, scope, await seed(ineligible))).toMatchObject({
status: "failed",
error: "feature_disabled",
})
const spent = makeDeps({
enablement: { checkEligibility: async () => ({ ok: true }), indexingQuota: async () => 0 },
})
expect(await autoIndexUpload(spent.deps, scope, await seed(spent))).toMatchObject({
status: "failed",
error: "quota_exhausted",
})
for (const fixture of [ineligible, spent]) {
expect(fixture.calls.reads).toEqual([])
expect(fixture.calls.indexed).toEqual([])
}
})
it("refuses to run where the enablement platform is not bound, rather than skip its checks", async () => {
const fixture = makeDeps()
const upload = await seed(fixture)
const { enablement: _unbound, ...deps } = fixture.deps
await expect(autoIndexUpload(deps, scope, upload)).rejects.toThrow("enablement")
expect(fixture.calls.reads).toEqual([])
})
it("does not read a file whose bytes changed since the upload", async () => {
const fixture = makeDeps()
const upload = await seed(fixture)
expect(await autoIndexUpload(fixture.deps, scope, { ...upload, sha256: "0".repeat(64) })).toBeNull()
expect(fixture.calls.reads).toEqual([])
})
it("tombstones every passage it indexed when the document is removed, even after the file is gone", async () => {
const fixture = makeDeps()
const upload = await seed(fixture)
const done = await autoIndexUpload(fixture.deps, scope, upload)
await fixture.data.forScope(scope).of(files).delete(upload.fileId)
await removeDocument(fixture.deps, scope, (await indexedView(done)).documentId)
expect(fixture.calls.removed).toEqual([
{
documentRef: `org/${scope.orgId}/${fileRow.storageKey}?${upload.fileId}`,
passages: [
{ page: 1, index: 0 },
{ page: 1, index: 1 },
{ page: 3, index: 0 },
],
},
])
expect((await fixture.data.forScope(scope).of(documents).list({ limit: 10 })).items).toEqual([])
expect((await fixture.data.forScope(scope).of(passageSets).list({ limit: 10 })).items).toEqual([])
})
})
describe("the intake's place in the manifest", () => {
it("contributes a files processor for PDFs whose job the module runs", () => {
const contributions = (knowledgeModule.extends ?? []).filter((entry) => entry.point.id === filesProcessors.id)
expect(contributions.flatMap((entry) => entry.values)).toEqual([
{ key: AUTO_INDEX_PROCESSOR_KEY, contentTypes: ["application/pdf"], job: AUTO_INDEX_JOB },
])
expect(Object.keys(knowledgeModule.jobs ?? {})).toContain(AUTO_INDEX_JOB)
})
it("declares the platform operations the job uses: the knowledge service's and the document reader's", () => {
const operations = (service: string) =>
knowledgeModule.serviceRequirements?.find((requirement) => requirement.service === service)?.operations
expect(operations("knowledge")?.[AUTO_INDEX_JOB]).toEqual(["knowledge.collections:use"])
expect(operations("ocr")?.[AUTO_INDEX_JOB]).toEqual(["ocr:use"])
})
})
@@ -0,0 +1,73 @@
// A project's automatic PDF indexing (template lesson core-11): an administrator turns it on into an
// existing collection, and is recorded as the one whose permission each indexing job re-checks; off
// is no row. Another project's collection is not found, and a project with knowledge off refuses.
import type { TenantScope } from "@conusai-kit/core"
import { createMemoryDataSource } from "@conusai-kit/data"
import { DEFAULT_FILES_POLICY, files } from "@conusai-kit/files"
import { fakeObjectStore } from "@conusai-kit/testing"
import { describe, expect, it } from "vitest"
import { autoIndex, collections, KNOWLEDGE_ENTITIES } from "../contract/index.js"
import { getAutoIndex, setAutoIndex } from "../core/auto-index.js"
import type { KnowledgeDeps } from "../core/ports.js"
const ADMIN = "01900000-0000-7000-0000-000000000009"
const scope: TenantScope = {
level: "project",
actor: { userId: ADMIN },
orgId: "01900000-0000-7000-0000-000000000001",
workspaceId: "01900000-0000-7000-0000-000000000002",
projectId: "01900000-0000-7000-0000-000000000003",
}
const otherProject: TenantScope = { ...scope, projectId: "01900000-0000-7000-0000-000000000004" }
function makeDeps(enabled = true) {
const data = createMemoryDataSource({ entities: [...KNOWLEDGE_ENTITIES, files] })
const deps: KnowledgeDeps & { now: () => number } = {
data,
files: { data, store: fakeObjectStore(), policy: DEFAULT_FILES_POLICY },
flags: { enabled: async () => enabled },
now: () => Date.parse("2026-11-09T19:00:00.000Z"),
}
return { data, deps }
}
describe("automatic PDF indexing of a project", () => {
it("is off until an administrator names a collection, and records who turned it on", async () => {
const { data, deps } = makeDeps()
await data.forScope(scope).of(collections).insert({ name: "contracts", createdBy: ADMIN })
expect(await getAutoIndex(deps, scope)).toEqual({ collectionName: null })
expect(await setAutoIndex(deps, scope, "contracts")).toEqual({ collectionName: "contracts" })
expect(await getAutoIndex(deps, scope)).toEqual({ collectionName: "contracts" })
const [row] = (await data.forScope(scope).of(autoIndex).list({ limit: 10 })).items
expect(row).toMatchObject({ collectionName: "contracts", enabledBy: ADMIN, enabledAt: "2026-11-09T19:00:00.000Z" })
// Another project of the same organization is not opted in.
expect(await getAutoIndex(deps, otherProject)).toEqual({ collectionName: null })
})
it("keeps one row per project: naming another collection moves it, and off removes it", async () => {
const { data, deps } = makeDeps()
for (const name of ["contracts", "invoices"])
await data.forScope(scope).of(collections).insert({ name, createdBy: ADMIN })
await setAutoIndex(deps, scope, "contracts")
await setAutoIndex(deps, scope, "invoices")
expect(
(await data.forScope(scope).of(autoIndex).list({ limit: 10 })).items.map((row) => row.collectionName),
).toEqual(["invoices"])
expect(await setAutoIndex(deps, scope, null)).toEqual({ collectionName: null })
expect((await data.forScope(scope).of(autoIndex).list({ limit: 10 })).items).toEqual([])
// Off when already off writes nothing and answers the same.
expect(await setAutoIndex(deps, scope, null)).toEqual({ collectionName: null })
})
it("answers another project's collection as not found, and refuses while knowledge is off", async () => {
const { data, deps } = makeDeps()
await data.forScope(otherProject).of(collections).insert({ name: "theirs", createdBy: ADMIN })
await expect(setAutoIndex(deps, scope, "theirs")).rejects.toMatchObject({ status: 404 })
const off = makeDeps(false)
await off.data.forScope(scope).of(collections).insert({ name: "contracts", createdBy: ADMIN })
await expect(setAutoIndex(off.deps, scope, "contracts")).rejects.toMatchObject({ status: 403 })
expect((await off.data.forScope(scope).of(autoIndex).list({ limit: 10 })).items).toEqual([])
})
})
+134
View File
@@ -0,0 +1,134 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
import {
buildRegistry,
Container,
contribute, // kit:guides
type ModuleManifest,
type TenantScope,
} from "@conusai-kit/core"
import { createMemoryDataSource, DATA_SOURCE, entitiesOf, JOB_QUEUE, OBJECT_STORE } from "@conusai-kit/data"
import * as guidesKit from "@conusai-kit/guides" // kit:guides
import { projects, workspaces } from "@conusai-kit/tenancy"
import { fakeObjectStore } from "@conusai-kit/testing"
import { expect, it } from "vitest"
import { custodyModule } from "../../custody.js"
import { OPTIONAL_MODULE_IDS } from "../../inclusion.js"
import { appModules } from "../../index.js"
import { documents, KNOWLEDGE_ENTITIES, KNOWLEDGE_SCOPE, KNOWLEDGE_SEARCH, knowledgeModule } from "../index.js"
const actor = { userId: "01900000-0000-7000-0000-000000000009" }
const orgId = "01900000-0000-7000-0000-000000000001"
/**
* A module as the app composes it without the opt-in guides module: its contributions to the guides extension
* points go with it, whichever module made them (an app's own module may bring code guides too).
*/
function withoutGuideContributions(entry: ModuleManifest): ModuleManifest {
return entry.extends
? { ...entry, extends: entry.extends.filter((item) => !item.point.id.startsWith("guides.")) }
: entry
}
// kit:guides:begin
it("drops guides contributions from every module, not only notes", () => {
// An app's own module with a code guide: knowledge stands in for it.
const guided: ModuleManifest = {
...knowledgeModule,
extends: [...(knowledgeModule.extends ?? []), contribute(guidesKit.guidesCode, [])],
}
expect(withoutGuideContributions(guided).extends?.filter((item) => item.point.id.startsWith("guides."))).toEqual([])
})
// kit:guides:end
it("off retains the original Knowledge rows/schema and executes only source cleanup", async () => {
const base = appModules.filter(
(entry) => !OPTIONAL_MODULE_IDS.some((id) => id === entry.id) && entry.id !== "assistant",
)
const independent = base.map(withoutGuideContributions)
const active = buildRegistry([...independent, knowledgeModule])
const retained = buildRegistry([...independent, custodyModule(knowledgeModule)])
expect(retained.migrations.postgres).toEqual(active.migrations.postgres)
expect(entitiesOf(retained, "postgres").map((entity) => entity.table)).toEqual(
entitiesOf(active, "postgres").map((entity) => entity.table),
)
expect([...retained.procedures.keys()].filter((key) => key.startsWith("knowledge."))).toEqual([])
expect(Object.keys(retained.jobs).filter((key) => key.startsWith("knowledge."))).toEqual(["knowledge.remove"])
// The project the deleted file was in, as the organization has it: a delivery acts only in a project that exists.
const data = createMemoryDataSource({ entities: [workspaces, projects, ...KNOWLEDGE_ENTITIES] })
const workspace = await data
.forScope({ level: "org", actor, orgId })
.of(workspaces)
.insert({ slug: "main", name: "Main", isDefault: true, visibility: "org" })
const project = await data
.forScope({ level: "workspace", actor, orgId, workspaceId: workspace.id })
.of(projects)
.insert({ slug: "site", name: "Site", moduleKey: "notes", archivedAt: null })
const scope: TenantScope = { level: "project", actor, orgId, workspaceId: workspace.id, projectId: project.id }
const row = await data.forScope(scope).of(documents).insert({
collectionName: "synthetic",
fileId: "01900000-0000-7000-0000-000000000004",
filename: "synthetic.txt",
status: "indexed",
error: null,
indexedAt: "2026-11-03T00:00:00Z",
requestedBy: "synthetic",
})
const module = custodyModule(knowledgeModule)
expect(await data.forScope(scope).of(documents).get(row.id)).toEqual(row)
const container = new Container()
container.value(DATA_SOURCE, data)
container.value(OBJECT_STORE, fakeObjectStore())
container.value(KNOWLEDGE_SCOPE, scope)
const enqueued: string[] = []
container.value(JOB_QUEUE, {
enqueue: async (_scope, job) => {
enqueued.push(job)
return "synthetic-job"
},
})
const deletion = module.events?.subscribes?.find((entry) => entry.event.name === "files.FileDeleted")
if (!deletion) throw new Error("source cleanup subscription missing")
await deletion.handler(
{
name: "files.FileDeleted",
// As kit-files publishes it: the deleted file's storage key comes with the record.
payload: {
orgId: scope.orgId,
workspaceId: scope.workspaceId,
projectId: scope.projectId,
fileId: row.fileId,
storageKey: "synthetic/key",
},
},
{ container } as never,
)
expect((await data.forScope(scope).of(documents).get(row.id))?.status).toBe("cancelled")
const event = (await data.outbox.pending(10))[0]
if (!event) throw new Error("cleanup event missing")
const cleanup = module.events?.subscribes?.find((entry) => entry.event.name === "knowledge.documentRemoveRequested")
if (!cleanup) throw new Error("cleanup subscription missing")
await cleanup.handler({ name: event.name, payload: event.payload }, { container } as never)
expect(enqueued).toEqual(["knowledge.remove"])
expect(module.events?.subscribes?.some((entry) => entry.event.name === "knowledge.documentIndexRequested")).toBe(
false,
)
const removed: string[] = []
container.value(KNOWLEDGE_SEARCH, {
remove: async (_scope: unknown, input: { fileId: string }) => {
removed.push(input.fileId)
},
index: async () => {
throw new Error("processing must be fenced")
},
search: async () => {
throw new Error("interactive search must be fenced")
},
} as never)
// The removal job resolves the original repositories and ports; no alternate store is created.
const remove = module.jobs?.["knowledge.remove"]
if (!remove) throw new Error("cleanup job missing")
await remove.handler({ documentId: row.id, collectionName: row.collectionName, fileId: row.fileId }, {
container,
} as never)
expect(removed).toEqual([row.fileId])
expect(await data.forScope(scope).of(documents).get(row.id)).toBeNull()
})
@@ -0,0 +1,78 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// A delivery's project comes from the record, inside the delivery's organization (adapters/jobs.ts). The record's
// workspace and project must exist in that organization before knowledge acts on them; one that names another
// workspace's project, or one the organization does not have, is refused by name and nothing is enqueued.
import { Container, type EventContext, newId, type OrgScope } from "@conusai-kit/core"
import { createMemoryDataSource, DATA_SOURCE, JOB_QUEUE } from "@conusai-kit/data"
import { projects, workspaces } from "@conusai-kit/tenancy"
import { fakeJobQueue } from "@conusai-kit/testing"
import { describe, expect, it } from "vitest"
import { KNOWLEDGE_SUBSCRIPTIONS } from "../adapters/jobs.js"
import { KNOWLEDGE_ENTITIES, KNOWLEDGE_SCOPE } from "../index.js"
async function delivery(archivedAt: string | null = null) {
const data = createMemoryDataSource({ entities: [workspaces, projects, ...KNOWLEDGE_ENTITIES] })
const orgId = newId()
const org: OrgScope = { level: "org", actor: { userId: newId() }, orgId }
const workspace = await data
.forScope(org)
.of(workspaces)
.insert({ slug: "main", name: "Main", isDefault: true, visibility: "org" })
const other = await data
.forScope(org)
.of(workspaces)
.insert({ slug: "other", name: "Other", isDefault: false, visibility: "org" })
const project = await data
.forScope({ level: "workspace", actor: org.actor, orgId, workspaceId: workspace.id })
.of(projects)
.insert({ slug: "site", name: "Site", moduleKey: "notes", archivedAt })
const queue = fakeJobQueue()
const container = new Container()
container.value(DATA_SOURCE, data)
container.value(JOB_QUEUE, queue)
// The relay's delivery: the organization's scope, with a system actor.
container.value(KNOWLEDGE_SCOPE, org)
const ctx = { container } as unknown as EventContext
return { ctx, queue, orgId, workspaceId: workspace.id, otherWorkspaceId: other.id, projectId: project.id }
}
const indexRequested = KNOWLEDGE_SUBSCRIPTIONS[1]
const request = (ids: { orgId: string; workspaceId: string; projectId: string }) => ({
name: "knowledge.DocumentIndexRequested",
payload: { ...ids, documentId: newId(), collectionName: "contracts", fileId: newId() },
})
describe("a knowledge delivery acts only in a project of its organization", () => {
it("enqueues the job in the record's project when it exists there", async () => {
const { ctx, queue, orgId, workspaceId, projectId } = await delivery()
await indexRequested?.handler(request({ orgId, workspaceId, projectId }) as never, ctx)
expect(queue.of("knowledge.index")).toHaveLength(1)
})
it("refuses, by name, a workspace the organization does not have", async () => {
const { ctx, queue, orgId, projectId } = await delivery()
const workspaceId = newId()
await expect(indexRequested?.handler(request({ orgId, workspaceId, projectId }) as never, ctx)).rejects.toThrow(
new RegExp(`workspace ${workspaceId}`),
)
expect(queue.of("knowledge.index")).toEqual([])
})
it("refuses, by name, a project that is not in the record's workspace", async () => {
const { ctx, queue, orgId, otherWorkspaceId, projectId } = await delivery()
await expect(
indexRequested?.handler(request({ orgId, workspaceId: otherWorkspaceId, projectId }) as never, ctx),
).rejects.toThrow(new RegExp(`project ${projectId}`))
expect(queue.of("knowledge.index")).toEqual([])
})
it("acts on a record of an archived project: an archive hides a project and does not end its work", async () => {
// The kit's one rule (kit-tenancy suspension.ts): an archived project refuses a person's write, not work with
// no person behind it. A deleted file's text must still leave the index, and an index request a member made
// before the archive still completes.
const { ctx, queue, orgId, workspaceId, projectId } = await delivery(new Date("2026-11-09T00:00:00Z").toISOString())
await indexRequested?.handler(request({ orgId, workspaceId, projectId }) as never, ctx)
expect(queue.of("knowledge.index")).toHaveLength(1)
})
})
@@ -0,0 +1,349 @@
// The document requests and the index job's orchestration: idempotent (collection, file) pairs,
// the flag gate, the content-type and size admission, the row's progress states, and which
// failures are permanent.
import type { TenantScope } from "@conusai-kit/core"
import { createMemoryDataSource } from "@conusai-kit/data"
import { DEFAULT_FILES_POLICY, files, sha256Hex } from "@conusai-kit/files"
import { fakeObjectStore } from "@conusai-kit/testing"
import { describe, expect, it } from "vitest"
import { collections, documents, KNOWLEDGE_ENTITIES } from "../contract/index.js"
import { listDocuments, requestIndex, requestRemove, requestSourceRemoval } from "../core/documents.js"
import { indexDocument, removeDocument } from "../core/index-job.js"
import type { KnowledgeDeps } from "../core/ports.js"
import { searchKnowledge } from "../core/search.js"
const USER = "01900000-0000-7000-0000-000000000009"
const scope: TenantScope = {
level: "project",
actor: { userId: USER },
orgId: "01900000-0000-7000-0000-000000000001",
workspaceId: "01900000-0000-7000-0000-000000000002",
projectId: "01900000-0000-7000-0000-000000000003",
}
const fileRow = {
status: "stored" as const,
storageKey: "k/seed",
filename: "a.txt",
contentType: "text/plain",
size: 12,
sha256: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
uploadedBy: USER,
storedAt: "2026-10-23T00:00:00.000Z",
uploadId: null,
declaredSize: null,
partSize: null,
deletingAt: null,
}
const makeDeps = (options: { flag?: boolean; search?: KnowledgeDeps["search"] } = {}) => {
const data = createMemoryDataSource({ entities: [...KNOWLEDGE_ENTITIES, files] })
const store = fakeObjectStore()
const search = options.search ?? {
readiness: async () => ({ state: "ready" as const, reasons: [] }),
index: async () => undefined,
search: async () => [],
remove: async () => undefined,
}
const deps: KnowledgeDeps = {
data,
files: { data, store, policy: DEFAULT_FILES_POLICY },
search,
flags: { enabled: async () => options.flag ?? true },
}
void deps
return { data, deps, store }
}
const seed = async (data: ReturnType<typeof createMemoryDataSource>, contentType = "text/plain") => {
await data.forScope(scope).of(collections).insert({ name: "contracts", createdBy: USER })
const row = await data
.forScope(scope)
.of(files)
.insert({ ...fileRow, ...(contentType === "text/plain" ? {} : { contentType }) })
return row.id
}
/** Seeds the file row AND the object the row names, the way a completed upload leaves both. */
const seedWithBytes = async (
data: ReturnType<typeof createMemoryDataSource>,
store: ReturnType<typeof fakeObjectStore>,
bytes: Uint8Array,
) => {
const fileId = await seed(data)
await data
.forScope(scope)
.of(files)
.update(fileId, { size: bytes.length, sha256: await sha256Hex(bytes) }, 1)
await store.put(scope, { key: fileRow.storageKey, body: bytes, contentType: "text/plain" })
return fileId
}
describe("requesting a document's indexing", () => {
it("creates the row, publishes the event, and a repeat request changes nothing", async () => {
const { data, deps } = makeDeps()
const fileId = await seed(data)
const first = await requestIndex(deps, scope, { collectionName: "contracts", fileId })
expect(first.status).toBe("queued")
const second = await requestIndex(deps, scope, { collectionName: "contracts", fileId: first.fileId })
expect(second.documentId).toBe(first.documentId)
expect(await listDocuments(deps, scope, "contracts")).toEqual({
documents: [expect.objectContaining({ documentId: first.documentId, status: "queued" })],
})
// One outbox event: the subscriber's enqueue is the exactly-once path.
const outbox = data.outbox.pending(10)
expect((await outbox).filter((record) => record.name === "knowledge.documentIndexRequested")).toHaveLength(1)
})
it("re-queues an indexed pair instead of duplicating it", async () => {
const { data, deps } = makeDeps()
const fileId = await seed(data)
const first = await requestIndex(deps, scope, { collectionName: "contracts", fileId })
await data.forScope(scope).of(documents).update(first.documentId, { status: "indexed" }, 1)
const again = await requestIndex(deps, scope, { collectionName: "contracts", fileId })
expect(again.documentId).toBe(first.documentId)
expect(again.status).toBe("queued")
})
it("refuses when the flag is off, and admits text files only, within the size cap", async () => {
const { data, deps } = makeDeps({ flag: false })
const fileId = await seed(data)
const off = await requestIndex(deps, scope, { collectionName: "contracts", fileId }).catch((e: unknown) => e)
expect((off as { status?: number }).status).toBe(403)
const refusing = makeDeps()
const pdfFile = await seed(refusing.data, "application/pdf")
const pdf = await requestIndex(refusing.deps, scope, { collectionName: "contracts", fileId: pdfFile }).catch(
(e: unknown) => e,
)
expect((pdf as { status?: number }).status).toBe(422)
const big = makeDeps()
const bigFile = await seed(big.data)
await big.data.forScope(scope).of(files).update(bigFile, { size: 2_000_000 }, 1)
const tooBig = await requestIndex(big.deps, scope, { collectionName: "contracts", fileId: bigFile }).catch(
(e: unknown) => e,
)
expect((tooBig as { status?: number }).status).toBe(422)
})
})
describe("current retrieval authority", () => {
it("withholds cancelled and unindexed documents even while remote deletion retries", async () => {
const { data, deps } = makeDeps()
const fileId = await seed(data)
const row = await requestIndex(deps, scope, { collectionName: "contracts", fileId })
const searching = {
...deps,
search: {
...deps.search,
readiness: async () => ({ state: "ready" as const, reasons: [] }),
index: async () => undefined,
remove: async () => undefined,
search: async () => [
{ fileId, filename: "a.txt", collectionName: "contracts", snippet: "private source", score: 1, chunk: 1 },
],
},
}
expect((await searchKnowledge(searching, scope, { collectionName: "contracts", query: "source" })).results).toEqual(
[],
)
await data.forScope(scope).of(documents).update(row.documentId, { status: "indexed" }, 1)
expect(
(await searchKnowledge(searching, scope, { collectionName: "contracts", query: "source" })).results,
).toHaveLength(1)
await requestRemove(deps, scope, { collectionName: "contracts", fileId })
expect((await searchKnowledge(searching, scope, { collectionName: "contracts", query: "source" })).results).toEqual(
[],
)
// A removal a person asks for names the stored file's object, read when it is asked for: if the file is deleted
// before the job runs, the job still removes the right text (F407, review R1).
const asked = (await data.outbox.pending(100)).filter((event) => event.name === "knowledge.documentRemoveRequested")
expect(asked).toHaveLength(1)
expect(asked[0]?.payload).toMatchObject({ storageKey: "k/seed" })
})
it("refuses a person's removal of a file being deleted, which leaves the document to the deletion's own removal", async () => {
const { data, deps } = makeDeps()
const fileId = await seed(data)
const row = await requestIndex(deps, scope, { collectionName: "contracts", fileId })
await data.forScope(scope).of(documents).update(row.documentId, { status: "indexed" }, 1)
const file = await data.forScope(scope).of(files).require(fileId)
await data
.forScope(scope)
.of(files)
.update(fileId, { status: "deleting", deletingAt: new Date().toISOString() }, file.version)
await expect(requestRemove(deps, scope, { collectionName: "contracts", fileId })).rejects.toMatchObject({
status: 409,
})
// Still active, so `FileDeleted` (which carries the key) cancels it and asks for the removal.
expect((await data.forScope(scope).of(documents).require(row.documentId)).status).toBe("indexed")
expect(
(await data.outbox.pending(100)).filter((event) => event.name === "knowledge.documentRemoveRequested"),
).toEqual([])
})
})
describe("retained remote citations after source deletion", () => {
it("withholds a deleting or missing file even when its index row still says indexed", async () => {
const { data, deps } = makeDeps()
const fileId = await seed(data)
const row = await requestIndex(deps, scope, { collectionName: "contracts", fileId })
await data.forScope(scope).of(documents).update(row.documentId, { status: "indexed" }, 1)
if (!deps.search) throw new Error("the retained citation fixture must configure search")
const searching = {
...deps,
search: {
...deps.search,
search: async () => [
{ fileId, filename: "a.txt", collectionName: "contracts", snippet: "retained source", score: 1, chunk: 1 },
],
},
}
expect(
(await searchKnowledge(searching, scope, { collectionName: "contracts", query: "source" })).results,
).toHaveLength(1)
await data.forScope(scope).of(files).update(fileId, { status: "deleting" }, 1)
expect((await searchKnowledge(searching, scope, { collectionName: "contracts", query: "source" })).results).toEqual(
[],
)
const { storageKey } = (await data.forScope(scope).of(files).get(fileId)) ?? { storageKey: "" }
await data.forScope(scope).of(files).delete(fileId)
await requestSourceRemoval({ ...deps, flags: { enabled: () => false } }, scope, fileId, storageKey)
await requestSourceRemoval(deps, scope, fileId, storageKey)
expect((await listDocuments(deps, scope, "contracts")).documents[0]?.status).toBe("cancelled")
const cleanup = (await data.outbox.pending(100)).filter(
(event) => event.name === "knowledge.documentRemoveRequested",
)
expect(cleanup).toHaveLength(1)
// The deleted file's key travels with the request: its row is gone, so the removal cannot look it up (F407).
expect(cleanup[0]?.payload).toMatchObject({ storageKey })
expect((await searchKnowledge(searching, scope, { collectionName: "contracts", query: "source" })).results).toEqual(
[],
)
})
})
describe("the index job's orchestration", () => {
it("moves the row queued -> indexing -> indexed and calls the search service once", async () => {
const indexed: string[] = []
const { data, deps, store } = makeDeps({
search: {
readiness: async () => ({ state: "ready", reasons: [] }),
index: async (_scope, input) => {
indexed.push(input.fileId)
},
search: async () => [],
remove: async () => undefined,
},
})
const fileId = await seedWithBytes(data, store, new TextEncoder().encode("hello knowledge"))
const requested = await requestIndex(deps, scope, { collectionName: "contracts", fileId })
const done = await indexDocument(deps, scope, requested.documentId)
expect(done.status).toBe("indexed")
expect(indexed).toEqual([fileId])
// A redelivered job finds the row indexed and does the external work again never.
const again = await indexDocument(deps, scope, requested.documentId)
expect(again.status).toBe("indexed")
expect(indexed).toEqual([fileId])
})
it("records a permanent failure on the row; a transient one throws for the queue to retry", async () => {
const permanent = makeDeps({
search: {
readiness: async () => ({ state: "ready", reasons: [] }),
index: async () => {
throw Object.assign(new Error("not text"), { permanent: "not_text" })
},
search: async () => [],
remove: async () => undefined,
},
})
const permanentFile = await seedWithBytes(
permanent.data,
permanent.store,
new TextEncoder().encode("not text at all"),
)
const requested = await requestIndex(permanent.deps, scope, { collectionName: "contracts", fileId: permanentFile })
const failed = await indexDocument(permanent.deps, scope, requested.documentId)
expect(failed.status).toBe("failed")
expect(failed.error).toBe("not_text")
const transient = makeDeps({
search: {
readiness: async () => ({ state: "ready", reasons: [] }),
index: async () => {
throw new Error("the store hiccuped")
},
search: async () => [],
remove: async () => undefined,
},
})
const transientFile = await seedWithBytes(transient.data, transient.store, new TextEncoder().encode("hello"))
const requested2 = await requestIndex(transient.deps, scope, { collectionName: "contracts", fileId: transientFile })
await expect(indexDocument(transient.deps, scope, requested2.documentId)).rejects.toThrow("hiccuped")
let resumed = 0
const retry = {
...transient.deps,
search: {
...transient.deps.search,
readiness: async () => ({ state: "ready" as const, reasons: [] }),
search: async () => [],
remove: async () => undefined,
index: async () => {
resumed++
},
},
}
expect((await indexDocument(retry, scope, requested2.documentId)).status).toBe("indexed")
expect(resumed).toBe(1)
})
it("removal deletes the row and asks the service to drop the vectors", async () => {
const removed: string[] = []
const { data, deps } = makeDeps({
search: {
readiness: async () => ({ state: "ready", reasons: [] }),
index: async () => undefined,
search: async () => [],
remove: async (_scope, input) => {
removed.push(input.fileId)
},
},
})
const fileId = await seed(data)
const requested = await requestIndex(deps, scope, { collectionName: "contracts", fileId })
const cancelled = await requestRemove(deps, scope, { collectionName: "contracts", fileId })
expect(cancelled.status).toBe("cancelled")
expect((await indexDocument(deps, scope, requested.documentId)).status).toBe("cancelled")
// The remove job's body: delete the vectors, then the row.
await removeDocument(deps, scope, requested.documentId)
expect(removed).toEqual([fileId])
expect((await listDocuments(deps, scope, "contracts")).documents).toEqual([])
})
})
it("persists a final transient indexing failure but never overwrites cancellation on a lost claim", async () => {
const transient = makeDeps({
search: {
readiness: async () => ({ state: "ready", reasons: [] }),
index: async () => {
throw new Error("temporary provider outage")
},
search: async () => [],
remove: async () => {},
},
})
const fileId = await seedWithBytes(transient.data, transient.store, new TextEncoder().encode("synthetic source"))
const requested = await requestIndex(transient.deps, scope, { collectionName: "contracts", fileId })
const outcome = await indexDocument(transient.deps, scope, requested.documentId, { finalAttempt: true })
expect(outcome).toMatchObject({ status: "failed", error: "processing_exhausted" })
const controller = new AbortController()
controller.abort()
const next = await requestIndex(transient.deps, scope, { collectionName: "contracts", fileId })
await expect(
indexDocument(transient.deps, scope, next.documentId, { finalAttempt: true, signal: controller.signal }),
).rejects.toBeDefined()
expect((await transient.data.forScope(scope).of(documents).get(next.documentId))?.status).not.toBe("failed")
})
+30
View File
@@ -0,0 +1,30 @@
// The module's messages: en and lt declare the same keys, or a page in one language breaks.
import { buildRegistry } from "@conusai-kit/core"
import { filesModule } from "@conusai-kit/files"
import { createI18n } from "@conusai-kit/i18n"
import { tenancyModule } from "@conusai-kit/tenancy"
import { describe, expect, it } from "vitest"
import { knowledgeModule } from "../index.js"
describe("the knowledge module's messages", () => {
const catalogues = buildRegistry([tenancyModule, filesModule, knowledgeModule]).catalogues
it("declares en and lt, and every en key resolves in lt", () => {
const en = catalogues.en?.knowledge ?? {}
const lt = catalogues.lt?.knowledge ?? {}
const flatten = (tree: Record<string, unknown>, prefix = ""): string[] =>
Object.entries(tree).flatMap(([key, value]) =>
typeof value === "string" ? [`${prefix}${key}`] : flatten(value as Record<string, unknown>, `${prefix}${key}.`),
)
const enKeys = flatten(en)
const ltKeys = new Set(flatten(lt))
expect(enKeys.length).toBeGreaterThan(0)
for (const key of enKeys) expect(ltKeys.has(key), `knowledge.${key} has no Lithuanian message`).toBe(true)
})
it("formats a message with values in both locales", () => {
const i18n = createI18n("lt", { lt: { knowledge: catalogues.lt?.knowledge ?? {} } })
expect(i18n.message("knowledge.status.documents", { queued: 1, indexing: 2, indexed: 3, failed: 0 })).toContain("1")
})
})
+59
View File
@@ -0,0 +1,59 @@
// What the generated tenant-isolation suite (test/isolation.test.ts) needs of this module: rows it
// seeds in every tenant, and one fixture per tenant-scoped procedure. A fixture is built from the
// OTHER tenant's ids: the call is made as a member of organization A with organization B's slugs
// and ids, and has to be refused or answered empty.
import type { TransportCaller } from "@conusai-kit/core"
import { type IsolationFixture, type IsolationTenant, uploadViaProcedures } from "@conusai-kit/testing"
/** What a module's isolation file exports: the shape the suite reads. */
export interface ModuleIsolation {
seed(
call: TransportCaller,
inProject: { org: string; workspace: string; project: string },
): Promise<Record<string, string>>
fixtures: Record<string, IsolationFixture>
}
const NOWHERE = "00000000-0000-0000-0000-000000000000"
const inProjectOf = (t: IsolationTenant) => ({ org: t.orgSlug, workspace: t.workspaceSlug, project: t.projectSlug })
const collectionOf = (t: IsolationTenant) => t.extra.knowledgeCollection ?? "contracts"
const fileOf = (t: IsolationTenant) => t.extra.knowledgeFileId ?? NOWHERE
/** A knowledge_document row the suite seeds in each tenant (test/isolation.test.ts): indexing needs the module on. */
const documentOf = (t: IsolationTenant) => t.extra.knowledgeDocumentId ?? NOWHERE
export const isolation: ModuleIsolation = {
async seed(call, inProject) {
const file = await uploadViaProcedures(call, inProject, {
filename: "knowledge-seed.txt",
contentType: "text/plain",
bytes: new TextEncoder().encode("seed knowledge"),
})
// The seed needs a collection row to exist; collection.create needs the admin role, which the
// isolation harness does not grant the seeding caller, so the fixture names a collection and
// the write fixtures create it through the manage permission where a denial is the assertion.
return { knowledgeCollection: "contracts", knowledgeFileId: file.fileId }
},
fixtures: {
"knowledge.collection.list": { input: (t) => inProjectOf(t) },
"knowledge.collection.create": { input: (t) => ({ ...inProjectOf(t), name: "intruders" }) },
"knowledge.document.list": {
input: (t) => ({ ...inProjectOf(t), collectionName: collectionOf(t) }),
},
// The live collection's read-back: another tenant's document id answers nothing.
"knowledge.document.getMany": { input: (t) => ({ ...inProjectOf(t), ids: [documentOf(t)] }) },
"knowledge.document.index": {
input: (t) => ({ ...inProjectOf(t), collectionName: collectionOf(t), fileId: fileOf(t) }),
},
"knowledge.document.remove": {
input: (t) => ({ ...inProjectOf(t), collectionName: collectionOf(t), fileId: fileOf(t) }),
},
"knowledge.search": { input: (t) => ({ ...inProjectOf(t), collectionName: collectionOf(t), query: "seed" }) },
"knowledge.status": { input: (t) => inProjectOf(t) },
"knowledge.settings.get": { input: (t) => ({ org: t.orgSlug }) },
"knowledge.settings.enableDocumentAi": { input: (t) => ({ org: t.orgSlug }) },
"knowledge.settings.disableDocumentAi": { input: (t) => ({ org: t.orgSlug }) },
"knowledge.autoindex.get": { input: (t) => inProjectOf(t) },
"knowledge.autoindex.set": { input: (t) => ({ ...inProjectOf(t), collectionName: collectionOf(t) }) },
},
}
@@ -0,0 +1,56 @@
// The module's migrations are its own files, each exactly `schemaStatements()` over the entities it adds
// (decision AT-D20: the entity definitions are the one source of a column). The module ships the
// Postgres profile; the Convex binding is provided for the integration owner and has no SQL.
import { readFileSync } from "node:fs"
import { schemaStatements } from "@conusai-kit/data-kysely"
import { describe, expect, it } from "vitest"
import { autoIndex, KNOWLEDGE_ENTITIES, KNOWLEDGE_ENTITIES_V1, knowledgeModule, passageSets } from "../index.js"
/** The statements for these tables alone: `schemaStatements` leads with the kernel's own two. */
function tablesOf(entities: Parameters<typeof schemaStatements>[0]): string {
const kernel = schemaStatements([]).length
return `${schemaStatements(entities)
.slice(kernel)
.map((statement) => `${statement};`)
.join("\n\n")}\n`
}
describe("the migration of the knowledge module", () => {
it("is its own file, exactly its three tables as first shipped", async () => {
await expect(
`-- SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0\n${tablesOf(KNOWLEDGE_ENTITIES_V1)}`,
).toMatchFileSnapshot("../migrations/0001_knowledge.sql")
})
it("adds the auto-index opt-in in the second file, exactly its table", async () => {
await expect(
`-- SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0\n${tablesOf([autoIndex])}`,
).toMatchFileSnapshot("../migrations/0002_knowledge_auto_index.sql")
})
it("adds the PDF intake's passage record in the third file, exactly its table", async () => {
await expect(
`-- SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0\n${tablesOf([passageSets])}`,
).toMatchFileSnapshot("../migrations/0003_knowledge_passage_set.sql")
expect(KNOWLEDGE_ENTITIES).toEqual([...KNOWLEDGE_ENTITIES_V1, autoIndex, passageSets])
})
it("is declared by the manifest, so the migrate job finds it, and every table is row-level secured", () => {
expect(knowledgeModule.migrations).toEqual({ postgres: "migrations" })
expect(knowledgeModule.profiles).toEqual(["postgres"])
const sql = ["0001_knowledge.sql", "0002_knowledge_auto_index.sql", "0003_knowledge_passage_set.sql"]
.map((file) => readFileSync(new URL(`../migrations/${file}`, import.meta.url), "utf8"))
.join("\n")
for (const table of [
"knowledge_collection",
"knowledge_document",
"knowledge_config",
"knowledge_auto_index",
"knowledge_passage_set",
]) {
expect(sql).toContain(`create table "${table}"`)
expect(sql).toContain(`alter table "${table}" force row level security`)
}
})
})
+208
View File
@@ -0,0 +1,208 @@
// 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(<UiLabelsProvider labels={kitStrings}>{node}</UiLabelsProvider>)
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<string, unknown>, 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<string, unknown>, 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<string, unknown>) => {
const data = await load([], answers)
expect(JSON.parse(JSON.stringify(data))).toStrictEqual(data)
const { View } = knowledgePage
return renderToStaticMarkup(
<DataProvider client={createDataClient({ fetch: async () => new Response(null, { status: 501 }) })}>
<View slugs={slugs} segments={[]} locale="en" data={JSON.parse(JSON.stringify(data))} />
</DataProvider>,
)
}
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("<h1 ")
expect(html).toContain("<li>the search index is behind</li>")
expect(html).toContain(">contracts</h2>")
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("<dl>")
})
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("'", "&#x27;"))
})
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&#x27;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"] },
}))
@@ -0,0 +1,304 @@
// The PDF intake's two adapters (template lesson core-11). The reader hands a PDF to the platform's
// document reader and answers its passages (`@conusai-sdk/ocr/passages`); the passage index sends one
// inline item per passage, under the file's Blob reference plus `#p{page}.{index}` and with its page,
// blocks and file in metadata, and tombstones exactly those references. A search hit on a passage
// cites back into the file it came from.
import { createMemoryDataSource } from "@conusai-kit/data"
import { DEFAULT_FILES_POLICY, files } from "@conusai-kit/files"
import {
KNOWLEDGE_LIMITS,
type Knowledge,
type KnowledgeClient,
KnowledgeError,
type KnowledgeInlineItem,
} from "@conusai-kit/platform"
import { fakeObjectStore } from "@conusai-kit/testing"
import { OcrError } from "@conusai-sdk/ocr"
import { describe, expect, it } from "vitest"
import { platformKnowledgeSearch, platformPassageIndex } from "../adapters/platform-knowledge.js"
import { sdkKnowledgeReader } from "../adapters/reader.js"
import type { KnowledgePassage, KnowledgeScope } from "../core/ports.js"
const scope: KnowledgeScope = {
level: "project",
actor: { userId: "system:knowledge.autoindex", system: true },
orgId: "01900000-0000-7000-0000-000000000001",
workspaceId: "01900000-0000-7000-0000-000000000002",
projectId: "01900000-0000-7000-0000-000000000003",
}
const storageKey = `files/${scope.workspaceId}/${scope.projectId}/01900000-0000-7000-0000-000000000100`
const pathname = `org/${scope.orgId}/${storageKey}`
async function storedFile() {
const data = createMemoryDataSource({ entities: [files] })
const deps = { data, store: fakeObjectStore(), policy: DEFAULT_FILES_POLICY }
const row = await data
.forScope(scope)
.of(files)
.insert({
status: "stored",
storageKey,
filename: "contract.pdf",
contentType: "application/pdf",
size: 10,
sha256: "0".repeat(64),
uploadedBy: "01900000-0000-7000-0000-000000000010",
storedAt: "2026-11-09T19:00:00.000Z",
uploadId: null,
declaredSize: null,
partSize: null,
deletingAt: null,
})
return { deps, fileId: row.id }
}
type PassageClient = Pick<KnowledgeClient, "createCollection" | "indexInline" | "job" | "remove">
function passageClient(jobError: string | null = null) {
const calls = {
collections: [] as string[],
batches: [] as KnowledgeInlineItem[][],
polled: [] as string[],
removed: [] as string[],
}
const client: PassageClient = {
createCollection: async (input) => {
calls.collections.push(`${input.collection}@${input.environment}`)
return input
},
indexInline: async (input) => {
calls.batches.push([...input.items])
return input.items.map((item, at) => ({
documentRef: item.documentRef,
jobId: `job-${calls.batches.length}-${at}`,
}))
},
job: async (jobId) => {
calls.polled.push(jobId)
return jobError === null
? { jobId, status: "succeeded", progress: 1, generation: 1, error: null }
: { jobId, status: "failed", progress: null, generation: null, error: jobError }
},
remove: async (documentRef) => {
calls.removed.push(documentRef)
},
}
return { client, calls }
}
const passage = (page: number, index: number, blockIds: string[] = []): KnowledgePassage => ({
page,
index,
blockIds,
text: `page ${page}, passage ${index}`,
})
describe("the passage index", () => {
it("indexes one tenant item per passage under the file's reference, with its page, blocks and file in metadata", async () => {
const { deps, fileId } = await storedFile()
const { client, calls } = passageClient()
const index = platformPassageIndex(client, deps, { environment: "production", intervalMs: 1 })
const documentRef = await index.reference(scope, fileId)
expect(documentRef).toBe(pathname)
await index.index(scope, {
collectionName: "contracts",
documentRef,
fileId,
passages: [passage(1, 0, ["b1", "b2"]), passage(2, 0)],
})
expect(calls.collections).toEqual(["contracts@production"])
const [batch] = calls.batches
expect(batch?.map((item) => item.documentRef)).toEqual([`${pathname}#p1.0`, `${pathname}#p2.0`])
expect(batch?.[0]).toMatchObject({
text: "page 1, passage 0",
visibility: "tenant",
metadata: { page: "1", block_ids: "b1,b2", file_id: fileId },
})
// A passage on a page with no blocks carries no block ids rather than an empty one.
expect(batch?.[1]?.metadata).toEqual({ page: "2", file_id: fileId })
// The tenant partition is the project's, the same one a search of the project asks for.
expect(new Set(batch?.map((item) => item.tenant))).toEqual(new Set([batch?.[0]?.tenant]))
expect(batch?.[0]?.tenant).toMatch(/^[0-9a-f]{64}$/)
// Every item's durable job is waited for.
expect([...calls.polled].sort()).toEqual(["job-1-0", "job-1-1"])
})
it("sends at most the service's item count per call", async () => {
const { deps, fileId } = await storedFile()
const { client, calls } = passageClient()
const many = Array.from({ length: KNOWLEDGE_LIMITS.inlineItems + 1 }, (_, at) => passage(1, at))
await platformPassageIndex(client, deps, { environment: "production", intervalMs: 1 }).index(scope, {
collectionName: "contracts",
documentRef: pathname,
fileId,
passages: many,
})
expect(calls.batches.map((batch) => batch.length)).toEqual([KNOWLEDGE_LIMITS.inlineItems, 1])
})
it("records a quota refusal as the document's, not as something to retry", async () => {
const { deps, fileId } = await storedFile()
const { client } = passageClient("quota-exhausted")
await expect(
platformPassageIndex(client, deps, { environment: "production", intervalMs: 1 }).index(scope, {
collectionName: "contracts",
documentRef: pathname,
fileId,
passages: [passage(1, 0)],
}),
).rejects.toMatchObject({ reason: "quota_exhausted", permanent: "quota_exhausted" })
})
it("tombstones exactly the references it indexed, without the file", async () => {
const { deps } = await storedFile()
const { client, calls } = passageClient()
await platformPassageIndex(client, deps, { environment: "production" }).remove(scope, {
documentRef: pathname,
passages: [
{ page: 1, index: 0 },
{ page: 1, index: 1 },
{ page: 3, index: 0 },
],
})
expect(calls.removed).toEqual([`${pathname}#p1.0`, `${pathname}#p1.1`, `${pathname}#p3.0`])
})
it("takes a passage the service never indexed as already removed", async () => {
const { deps } = await storedFile()
const { client, calls } = passageClient()
const index = platformPassageIndex(
{
...client,
remove: async (documentRef) => {
calls.removed.push(documentRef)
if (documentRef.endsWith("#p1.1")) throw new KnowledgeError("unavailable_resource", "no such document")
},
},
deps,
{ environment: "production" },
)
await index.remove(scope, {
documentRef: pathname,
passages: [
{ page: 1, index: 0 },
{ page: 1, index: 1 },
{ page: 2, index: 0 },
],
})
expect(calls.removed).toEqual([`${pathname}#p1.0`, `${pathname}#p1.1`, `${pathname}#p2.0`])
})
})
describe("a search hit on a passage", () => {
it("cites the file it came from, with the passage's page and first block", async () => {
const { deps, fileId } = await storedFile()
const client = {
search: async () => [
{
documentRef: `${pathname}#p2.0`,
snippet: "page 2",
score: 0.9,
citations: [],
chunkId: "chunk-1",
metadata: { page: "2", block_ids: "b3,b4", file_id: fileId },
blockIds: [],
},
],
} as unknown as Knowledge
const hits = await platformKnowledgeSearch(client, deps, { environment: "production" }).search(scope, {
collectionName: "contracts",
query: "page",
limit: 5,
actorId: "01900000-0000-7000-0000-000000000010",
})
expect(hits).toEqual([
{
fileId,
filename: "contract.pdf",
collectionName: "contracts",
score: 0.9,
chunk: 1,
snippet: "page 2",
page: 2,
blockId: "b3",
chunkId: "chunk-1",
},
])
})
})
describe("the reader", () => {
const document = {
schema_version: "conusai.ocr.document/v1",
pages: [
{
index: 0,
lines: [
{ text: "Agreement", order: 0, repeated: false },
{ text: "Signed", order: 1, repeated: false },
],
blocks: [{ id: "p1-b1", order: 0, lines: [0, 1] }],
},
],
}
it("submits the PDF once under its key and answers the read document's passages", async () => {
const submitted: Array<{ filename?: string; idempotencyKey?: string }> = []
const reader = sdkKnowledgeReader(
{
submit: (async (_bytes: unknown, options: { filename?: string; idempotencyKey?: string }) => {
submitted.push(options)
return { id: 7 }
}) as never,
wait: (async (job: unknown) => {
expect(job).toBe(7)
return document
}) as never,
},
{ intervalMs: 1 },
)
const read = await reader.read(new Uint8Array([1]), { filename: "contract.pdf", idempotencyKey: "key-1" })
expect(submitted).toEqual([{ filename: "contract.pdf", idempotencyKey: "key-1" }])
expect(read).toEqual([{ page: 1, index: 0, blockIds: ["p1-b1"], text: "Agreement\nSigned" }])
})
it("answers a job the platform failed, or a document it cannot cite, as not indexable", async () => {
const failing = sdkKnowledgeReader({
submit: (async () => ({ id: 1 })) as never,
wait: (async () => {
throw new OcrError("failed", { type: "job-failed", status: 200 })
}) as never,
})
await expect(failing.read(new Uint8Array([1]), { filename: "a.pdf", idempotencyKey: "k" })).rejects.toMatchObject({
permanent: "not_indexable",
})
const longId = {
...document,
pages: [{ ...document.pages[0], blocks: [{ id: "x".repeat(257), order: 0, lines: [0, 1] }] }],
}
const uncitable = sdkKnowledgeReader({
submit: (async () => ({ id: 1 })) as never,
wait: (async () => longId) as never,
})
await expect(uncitable.read(new Uint8Array([1]), { filename: "a.pdf", idempotencyKey: "k" })).rejects.toMatchObject(
{
permanent: "not_indexable",
},
)
})
it("leaves a transient failure to the queue", async () => {
const transient = sdkKnowledgeReader({
submit: (async () => {
throw new OcrError("unavailable", { status: 503 })
}) as never,
wait: (async () => document) as never,
})
const error = await transient.read(new Uint8Array([1]), { filename: "a.pdf", idempotencyKey: "k" }).catch((e) => e)
expect(error).toBeInstanceOf(OcrError)
expect((error as { permanent?: unknown }).permanent).toBeUndefined()
})
})
@@ -0,0 +1,325 @@
import { createMemoryDataSource } from "@conusai-kit/data"
import { DEFAULT_FILES_POLICY, type FilesDeps, files } from "@conusai-kit/files"
import { type Knowledge, KnowledgeError, type KnowledgeSearchHit } from "@conusai-kit/platform"
import { fakeObjectStore } from "@conusai-kit/testing"
import { describe, expect, it } from "vitest"
import { platformKnowledgeSearch } from "../adapters/platform-knowledge.js"
import type { KnowledgeScope } from "../core/ports.js"
const scope: 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 otherScope: KnowledgeScope = {
...scope,
projectId: "01900000-0000-7000-0000-000000000004",
}
const storageKey = `files/${scope.workspaceId}/${scope.projectId}/01900000-0000-7000-0000-000000000100`
const otherStorageKey = `files/${otherScope.workspaceId}/${otherScope.projectId}/01900000-0000-7000-0000-000000000101`
const pathname = `org/${scope.orgId}/${storageKey}`
interface ClientStub {
indexed: string[]
removed: string[]
searches: Array<Parameters<Knowledge["search"]>[0]>
results: readonly KnowledgeSearchHit[]
}
function knowledgeClient(stub: ClientStub): Knowledge {
return {
readiness: async () => ({ state: "ready", reasons: [], checkedAt: new Date().toISOString() }),
createCollection: async (input) => input,
search: async (input) => {
stub.searches.push(input)
return stub.results
},
index: async (input) => {
stub.indexed.push(input.documentRef)
return { jobId: "job-1" }
},
job: async (jobId) => ({ jobId, status: "succeeded", progress: 1, generation: 1, error: null }),
cancel: async () => undefined,
remove: async (documentRef) => {
stub.removed.push(documentRef)
},
}
}
function stub(results: readonly KnowledgeSearchHit[] = []): ClientStub {
return { indexed: [], removed: [], searches: [], results }
}
function makeFiles() {
const data = createMemoryDataSource({ entities: [files] })
const deps: FilesDeps = { data, store: fakeObjectStore(), policy: DEFAULT_FILES_POLICY }
return { data, deps }
}
async function seedFile(
data: ReturnType<typeof createMemoryDataSource>,
atScope: KnowledgeScope,
key: string,
filename = "contract.txt",
): Promise<string> {
const row = await data.forScope(atScope).of(files).insert({
status: "stored",
storageKey: key,
filename,
contentType: "text/plain",
size: 17,
sha256: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
uploadedBy: atScope.actor.userId,
storedAt: "2026-10-24T00:00:00.000Z",
uploadId: null,
declaredSize: null,
partSize: null,
deletingAt: null,
})
return row.id
}
describe("the managed knowledge Blob reference", () => {
it("indexes and removes by Blob pathname rather than opaque file id", async () => {
const { data, deps } = makeFiles()
const fileId = await seedFile(data, scope, storageKey)
const client = stub()
const service = platformKnowledgeSearch(knowledgeClient(client), deps, { environment: "preview" })
await service.index(scope, {
collectionName: "contracts",
fileId,
filename: "contract.txt",
bytes: new TextEncoder().encode("managed knowledge"),
})
await service.remove(scope, { collectionName: "contracts", fileId })
expect(client.indexed).toEqual([pathname])
expect(client.removed).toEqual([pathname])
})
it("removes a deleted source by the key it had, whose files row is gone (F407)", async () => {
const { data, deps } = makeFiles()
const fileId = await seedFile(data, scope, storageKey)
await data.forScope(scope).of(files).delete(fileId)
const client = stub()
const service = platformKnowledgeSearch(knowledgeClient(client), deps, { environment: "preview" })
// Without the key the file can no longer be found, so the removal refuses rather than guessing.
await expect(service.remove(scope, { collectionName: "contracts", fileId })).rejects.toThrow()
await service.remove(scope, { collectionName: "contracts", fileId, storageKey })
expect(client.removed).toEqual([pathname])
})
it("maps citations through scoped file rows and drops foreign or stale references", async () => {
const { data, deps } = makeFiles()
const fileId = await seedFile(data, scope, storageKey)
await seedFile(data, otherScope, otherStorageKey, "other-project.txt")
const client = stub([
{ documentRef: pathname, snippet: "own snippet", score: 0.9, citations: [{ page: 1 }] },
{
documentRef: `org/${scope.orgId}/${otherStorageKey}`,
snippet: "other project snippet",
score: 0.8,
citations: [],
},
{
documentRef: `org/01900000-0000-7000-0000-000000000777/${storageKey}`,
snippet: "other organization snippet",
score: 0.7,
citations: [],
},
{
documentRef: `org/${scope.orgId}/files/${scope.workspaceId}/${scope.projectId}/01900000-0000-7000-0000-000000000200`,
snippet: "stale snippet",
score: 0.6,
citations: [],
},
])
const service = platformKnowledgeSearch(knowledgeClient(client), deps, { environment: "preview" })
const hits = await service.search(scope, {
collectionName: "contracts",
query: "contract",
limit: 10,
actorId: scope.actor.userId,
})
expect(hits).toEqual([
{
fileId,
filename: "contract.txt",
collectionName: "contracts",
score: 0.9,
chunk: 1,
snippet: "own snippet",
page: 1,
},
])
expect(client.searches).toHaveLength(1)
expect(client.searches[0]).toMatchObject({ collection: "contracts", query: "contract", limit: 10 })
expect(client.searches[0]?.tenant).toMatch(/^[0-9a-f]{64}$/)
expect(client.searches[0]).not.toHaveProperty("ownerRef")
})
it("refuses a foreign file id before any managed index call", async () => {
const { data, deps } = makeFiles()
const foreignFileId = await seedFile(data, otherScope, otherStorageKey)
const client = stub()
const service = platformKnowledgeSearch(knowledgeClient(client), deps, { environment: "preview" })
await expect(
service.index(scope, {
collectionName: "contracts",
fileId: foreignFileId,
filename: "other-project.txt",
bytes: new TextEncoder().encode("foreign content"),
}),
).rejects.toThrow()
expect(client.indexed).toEqual([])
})
})
describe("the managed service's collections", () => {
it("creates the collection in the deployment's environment before it indexes: the service answers 404 otherwise", async () => {
const { data, deps } = makeFiles()
const fileId = await seedFile(data, scope, storageKey)
const order: string[] = []
const client: Knowledge = {
...knowledgeClient(stub()),
createCollection: async (input) => {
order.push(`collection ${input.collection} in ${input.environment}`)
return input
},
index: async () => {
order.push("index")
return { jobId: "job-1" }
},
}
const input = { collectionName: "contracts", fileId, filename: "a.txt", bytes: new TextEncoder().encode("text") }
await platformKnowledgeSearch(client, deps, { environment: "preview" }).index(scope, input)
expect(order).toEqual(["collection contracts in preview", "index"])
order.length = 0
const missing = platformKnowledgeSearch(client, deps, { environment: undefined })
expect(await missing.readiness(scope)).toMatchObject({ state: "unavailable", refusal: "configuration_missing" })
await expect(missing.index(scope, input)).rejects.toMatchObject({
reason: "configuration_missing",
permanent: "configuration_missing",
})
expect(order).toEqual([])
})
it("answers no results for a collection nothing was indexed into yet", async () => {
const client: Knowledge = {
...knowledgeClient(stub()),
search: async () => {
throw new KnowledgeError("unavailable_resource", "no such collection")
},
}
const hits = await platformKnowledgeSearch(client, makeFiles().deps, { environment: "preview" }).search(scope, {
collectionName: "empty",
query: "anything",
limit: 5,
actorId: scope.actor.userId,
})
expect(hits).toEqual([])
})
})
describe("the managed service's refusals", () => {
const answering = (over: Partial<Knowledge>) => ({ ...knowledgeClient(stub()), ...over })
it.each([
["plan_required", "disabled", "plan_required"],
["feature_disabled", "disabled", "feature_disabled"],
["forbidden", "unavailable", "scope_missing"],
["unauthenticated", "unavailable", "runtime_unavailable"],
["runtime_unavailable", "unavailable", "runtime_unavailable"],
["indexing", "indexing", "indexing"],
] as const)("readiness refused with %s is state %s and the typed refusal %s", async (reason, state, refusal) => {
const client = answering({
readiness: async () => {
throw new KnowledgeError(reason, "the service's own words")
},
})
const answer = await platformKnowledgeSearch(client, makeFiles().deps, { environment: "preview" }).readiness(scope)
expect(answer).toEqual({ state, reasons: [], refusal })
})
it("records a refusal waiting cannot fix on the document, and leaves a transient failure to the queue", async () => {
const { data, deps } = makeFiles()
const fileId = await seedFile(data, scope, storageKey)
const bytes = new TextEncoder().encode("managed knowledge")
const input = { collectionName: "contracts", fileId, filename: "contract.txt", bytes }
const quota = platformKnowledgeSearch(
answering({
job: async (jobId) => ({ jobId, status: "failed", progress: null, generation: null, error: "quota-exhausted" }),
}),
deps,
{ environment: "preview" },
)
await expect(quota.index(scope, input)).rejects.toMatchObject({ permanent: "quota_exhausted" })
const scope_missing = platformKnowledgeSearch(
answering({
index: async () => {
throw new KnowledgeError("forbidden", "the token lacks knowledge.collections:use")
},
}),
deps,
{ environment: "preview" },
)
await expect(scope_missing.index(scope, input)).rejects.toMatchObject({ permanent: "scope_missing" })
const down = platformKnowledgeSearch(
answering({
index: async () => {
throw new KnowledgeError("runtime_unavailable", "try later", true)
},
}),
deps,
{ environment: "preview" },
)
const transient = await down.index(scope, input).catch((error: unknown) => error)
expect(transient).toBeInstanceOf(KnowledgeError)
expect(transient).not.toHaveProperty("permanent")
})
})
describe("bounded durable index polling", () => {
it("stops a queued wait on cancellation without cancelling the durable platform job", async () => {
const { data, deps } = makeFiles()
const fileId = await seedFile(data, scope, storageKey)
const controller = new AbortController()
let polls = 0
let cancellations = 0
const client: Knowledge = {
...knowledgeClient(stub()),
job: async (jobId) => {
polls++
controller.abort()
return { jobId, status: "queued", progress: null, generation: null, error: null }
},
cancel: async () => {
cancellations++
},
}
await expect(
platformKnowledgeSearch(client, deps, { environment: "preview", intervalMs: 1 }).index(scope, {
collectionName: "contracts",
fileId,
filename: "a.txt",
bytes: new Uint8Array(),
signal: controller.signal,
}),
).rejects.toBeDefined()
expect(polls).toBe(1)
expect(cancellations).toBe(0)
})
})
@@ -0,0 +1,144 @@
// 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" },
})
})
})
@@ -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)
}
})
})
@@ -0,0 +1,121 @@
// The enablement flow's semantics: idempotent enable, typed denials from the platform checks, nothing
// provisioned on the app's side, and disable that preserves every row and every indexed document.
import type { TenantScope } from "@conusai-kit/core"
import { createMemoryDataSource } from "@conusai-kit/data"
import { DEFAULT_FILES_POLICY } from "@conusai-kit/files"
import { fakeObjectStore } from "@conusai-kit/testing"
import { describe, expect, it } from "vitest"
import { collections, config, documents, KNOWLEDGE_ENTITIES } from "../contract/index.js"
import { createCollection } from "../core/collections.js"
import type { DocumentAiPlatform } from "../core/ports.js"
import { disableDocumentAi, enableDocumentAi, getSettings } from "../core/settings.js"
const ORG = "01900000-0000-7000-0000-000000000001"
const scope: TenantScope = { level: "org", actor: { userId: "01900000-0000-7000-0000-000000000009" }, orgId: ORG }
const platform = (over: Partial<DocumentAiPlatform> = {}): DocumentAiPlatform => ({
checkEligibility: async () => ({ ok: true }),
indexingQuota: async () => 1000,
...over,
})
const deps = (data: ReturnType<typeof createMemoryDataSource>, p: DocumentAiPlatform) => ({
data,
platform: p,
now: () => Date.UTC(2026, 9, 23),
})
describe("the enablement flow", () => {
it("enables idempotently: the second call writes nothing and answers the same configuration", async () => {
const data = createMemoryDataSource({ entities: [...KNOWLEDGE_ENTITIES] })
const first = await enableDocumentAi(deps(data, platform()), scope)
expect(first).toEqual({ enabled: true, backend: "platform" })
const second = await enableDocumentAi(deps(data, platform()), scope)
expect(second).toEqual(first)
expect((await getSettings(deps(data, platform()), scope)).enabled).toBe(true)
})
it("surfaces the platform's denials as typed reason codes, retirable", async () => {
const data = createMemoryDataSource({ entities: [...KNOWLEDGE_ENTITIES] })
const denied = await enableDocumentAi(
deps(data, platform({ checkEligibility: async () => ({ ok: false, reason: "plan_required" }) })),
scope,
).catch((error: unknown) => error)
expect((denied as { code?: string }).code).toBe("plan_required")
// Nothing was persisted by the denied attempt; the retry path runs the same checks again.
expect((await getSettings(deps(data, platform()), scope)).enabled).toBe(false)
const quota = await enableDocumentAi(deps(data, platform({ indexingQuota: async () => 0 })), scope).catch(
(error: unknown) => error,
)
expect((quota as { code?: string }).code).toBe("quota_exhausted")
})
it("provisions nothing of the app's own: enabling records that the platform serves this organization", async () => {
const data = createMemoryDataSource({ entities: [...KNOWLEDGE_ENTITIES] })
const asked: string[] = []
const watched: DocumentAiPlatform = new Proxy(platform(), {
get: (target, name, receiver) => {
asked.push(String(name))
return Reflect.get(target, name, receiver)
},
})
await enableDocumentAi(deps(data, watched), scope)
expect(asked).toEqual(["checkEligibility", "indexingQuota"])
const rows = await data.forScope(scope).of(config).list({ limit: 1 })
expect(rows.items[0]).toMatchObject({ enabled: true, backend: "platform", serviceId: null, url: null })
expect(await getSettings(deps(data, platform()), scope)).toEqual({ enabled: true, backend: "platform" })
})
it("disable preserves collections, documents and the recorded configuration; re-enable resumes without duplication", async () => {
const data = createMemoryDataSource({ entities: [...KNOWLEDGE_ENTITIES] })
const projectScope: TenantScope = {
level: "project",
actor: { userId: "01900000-0000-7000-0000-000000000009" },
orgId: ORG,
workspaceId: "01900000-0000-7000-0000-000000000002",
projectId: "01900000-0000-7000-0000-000000000003",
}
const knowledgeDepsForProject = {
data,
files: { data, store: fakeObjectStore(), policy: DEFAULT_FILES_POLICY },
flags: { enabled: async () => true },
}
await enableDocumentAi(deps(data, platform()), scope)
const collection = await createCollection(knowledgeDepsForProject, projectScope as never, "contracts")
expect(collection.name).toBe("contracts")
// Seed a document row directly (the upload half is the files module's own suite).
await data.forScope(projectScope).of(documents).insert({
collectionName: "contracts",
fileId: "01900000-0000-7000-0000-000000000099",
filename: "a.txt",
status: "indexed",
error: null,
indexedAt: "2026-10-23T00:00:00.000Z",
requestedBy: "01900000-0000-7000-0000-000000000009",
})
const disabled = await disableDocumentAi(deps(data, platform()), scope)
expect(disabled.enabled).toBe(false)
// Everything is still there.
expect((await getSettings(deps(data, platform()), scope)).backend).toBe("platform")
const collectionsAfter = await data
.forScope(projectScope)
.of(collections)
.list({ where: [{ field: "name", op: "eq", value: "contracts" }], limit: 1 })
expect(collectionsAfter.items).toHaveLength(1)
const documentsAfter = await data
.forScope(projectScope)
.of(documents)
.list({ where: [{ field: "collectionName", op: "eq", value: "contracts" }], limit: 1 })
expect(documentsAfter.items[0]?.status).toBe("indexed")
// Re-enable: idempotent, same row, no second collection.
await enableDocumentAi(deps(data, platform()), scope)
const again = await data
.forScope(projectScope)
.of(collections)
.list({ where: [{ field: "name", op: "eq", value: "contracts" }], limit: 1 })
expect(again.items).toHaveLength(1)
})
})
+86
View File
@@ -0,0 +1,86 @@
// The components' markup, rendered on the server the way a page renders it: what a screen reader
// and a keyboard user get is in the HTML — a name for every control, a status that says what a
// colour would, and the AI disclosure where AI output appears.
import { buildRegistry } 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 } from "react-dom/server"
import { describe, expect, it, vi } from "vitest"
import { appModules } from "../../index.js"
import { searchFormLabels } from "../ui/labels.js"
import { SearchForm } from "../ui/search-form.js"
import { SettingsSection } from "../ui/settings-section.js"
const catalogues = buildRegistry([...appModules]).catalogues
const i18n = createI18n("en", { en: { knowledge: catalogues.en?.knowledge ?? {} } })
const t = (key: string, values?: Record<string, string | number>) => i18n.message(`knowledge.${key}`, values)
const failed = "That did not work: {detail}"
const slugs = { org: "acme", workspace: "ws", project: "p" }
const kitStrings = uiLabels(createI18n("en", builtInCatalogues))
const inProvider = (node: React.ReactNode) =>
renderToStaticMarkup(
<UiLabelsProvider labels={kitStrings}>
<DataProvider client={createDataClient({ fetch: async () => new Response(null, { status: 501 }) })}>
{node}
</DataProvider>
</UiLabelsProvider>,
)
describe("the search form", () => {
const html = inProvider(
<SearchForm slugs={slugs} collectionName="contracts" labels={searchFormLabels(t)} locale="en" />,
)
it("names its controls and carries the AI disclosure", () => {
expect(html).toMatch(/<label[^>]*for=/)
expect(html).toContain('type="search"')
expect(html).toContain("kit-ai-disclosure")
expect(html).toContain(t("search.disclosure"))
})
})
describe("the settings section", () => {
const labels = {
heading: t("settings.heading"),
state: t("settings.state"),
stateOn: t("settings.stateOn"),
stateOff: t("settings.stateOff"),
backend: t("settings.backend"),
backendPlatform: t("settings.backendPlatform"),
enable: t("settings.enable"),
enabling: t("settings.enabling"),
disable: t("settings.disable"),
disabling: t("settings.disabling"),
enabledDone: t("settings.enabledDone"),
disabledDone: t("settings.disabledDone"),
planRequired: t("settings.planRequired"),
featureDisabled: t("settings.featureDisabled"),
quotaExhausted: t("settings.quotaExhausted"),
upgrade: t("settings.upgrade"),
hint: t("settings.hint"),
failed,
}
it("renders the state and the enable action when disabled", () => {
const html = inProvider(<SettingsSection org="acme" initial={{ enabled: false, backend: null }} labels={labels} />)
expect(html).toContain(t("settings.stateOff"))
expect(html).toContain(`>${t("settings.enable")}<`)
})
it("says the platform serves the organization, with the disable action, when enabled", () => {
const html = inProvider(
<SettingsSection org="acme" initial={{ enabled: true, backend: "platform" }} labels={labels} />,
)
expect(html).toContain(t("settings.backendPlatform").replaceAll("'", "&#x27;"))
expect(html).toContain(`>${t("settings.disable")}<`)
})
})
// Explicit synthetic admission: module-local fixtures exercise the selected module.
vi.mock("../../inclusion.json", () => ({
default: { schemaVersion: 1, selected: ["knowledge"], everAdopted: ["knowledge"] },
}))