Files
template-app/modules/knowledge/__tests__/platform-knowledge.test.ts
T

170 lines
5.7 KiB
TypeScript

import { createMemoryDataSource } from "@conusai-kit/data"
import { DEFAULT_FILES_POLICY, type FilesDeps, files } from "@conusai-kit/files"
import type { Knowledge, 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)
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("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)
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",
},
])
expect(client.searches).toHaveLength(1)
expect(client.searches[0]).toMatchObject({ collection: "contracts", query: "contract", limit: 10 })
expect(client.searches[0]).not.toHaveProperty("tenant")
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)
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([])
})
})