// 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() })