@@ -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()
|
||||
})
|
||||
Reference in New Issue
Block a user