90 lines
3.9 KiB
TypeScript
90 lines
3.9 KiB
TypeScript
// The search service of a deployment with no vector store (no Qdrant linked, pgvector not chosen):
|
|
// readiness and the status page say `unavailable` with the actionable reason, an index job fails its
|
|
// row with the stable `vector_store_unavailable` code (permanent: the queue does not retry it), and a
|
|
// search or removal is a typed 503, never an empty answer that reads as "nothing indexed".
|
|
|
|
import { AppError, 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 { unavailableKnowledgeSearch } from "../adapters/unavailable.js"
|
|
import { collections, KNOWLEDGE_ENTITIES } from "../contract/index.js"
|
|
import { requestIndex } from "../core/documents.js"
|
|
import { indexDocument } from "../core/index-job.js"
|
|
import type { KnowledgeDeps } from "../core/ports.js"
|
|
import { knowledgeStatusOf } from "../core/status.js"
|
|
|
|
const USER = "01900000-0000-7000-0000-000000000009"
|
|
const scope: TenantScope & { workspaceId: string; projectId: string } = {
|
|
level: "project",
|
|
actor: { userId: USER },
|
|
orgId: "01900000-0000-7000-0000-000000000001",
|
|
workspaceId: "01900000-0000-7000-0000-000000000002",
|
|
projectId: "01900000-0000-7000-0000-000000000003",
|
|
}
|
|
|
|
const REASON = "no vector store is configured: link a Qdrant service"
|
|
|
|
const setup = () => {
|
|
const data = createMemoryDataSource({ entities: [...KNOWLEDGE_ENTITIES, files] })
|
|
const store = fakeObjectStore()
|
|
const deps: KnowledgeDeps = {
|
|
data,
|
|
files: { data, store, policy: DEFAULT_FILES_POLICY },
|
|
search: unavailableKnowledgeSearch(REASON),
|
|
flags: { enabled: async () => true },
|
|
}
|
|
return { data, deps, store }
|
|
}
|
|
|
|
describe("a deployment with no vector store", () => {
|
|
it("reports unavailable with the reason, on the port and on the status page", async () => {
|
|
const { deps } = setup()
|
|
expect(await deps.search?.readiness(scope)).toEqual({ state: "unavailable", reasons: [REASON] })
|
|
const status = await knowledgeStatusOf(deps, scope)
|
|
expect(status.service).toEqual({ state: "unavailable", reasons: [REASON] })
|
|
})
|
|
|
|
it("fails an index job permanently with vector_store_unavailable", async () => {
|
|
const { data, deps, store } = setup()
|
|
await data.forScope(scope).of(collections).insert({ name: "contracts", createdBy: USER })
|
|
const file = await data.forScope(scope).of(files).insert({
|
|
status: "stored",
|
|
storageKey: "k/seed",
|
|
filename: "a.txt",
|
|
contentType: "text/plain",
|
|
size: 5,
|
|
sha256: "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
|
|
uploadedBy: USER,
|
|
storedAt: "2026-10-25T00:00:00.000Z",
|
|
uploadId: null,
|
|
declaredSize: null,
|
|
partSize: null,
|
|
deletingAt: null,
|
|
})
|
|
await store.put(scope, { key: "k/seed", body: new TextEncoder().encode("hello"), contentType: "text/plain" })
|
|
const requested = await requestIndex(deps, scope, { collectionName: "contracts", fileId: file.id })
|
|
const failed = await indexDocument(deps, scope, requested.documentId)
|
|
expect(failed.status).toBe("failed")
|
|
expect(failed.error).toBe("vector_store_unavailable")
|
|
})
|
|
|
|
it("refuses search and removal with a typed 503 that names the reason", async () => {
|
|
const service = unavailableKnowledgeSearch(REASON)
|
|
for (const call of [
|
|
() => service.search(scope, { collectionName: "contracts", query: "q", limit: 5, actorId: USER }),
|
|
() => service.remove(scope, { collectionName: "contracts", fileId: "01900000-0000-7000-0000-0000000000aa" }),
|
|
]) {
|
|
const error = await call().then(
|
|
() => null,
|
|
(thrown: unknown) => thrown,
|
|
)
|
|
expect(error).toBeInstanceOf(AppError)
|
|
expect((error as AppError).status).toBe(503)
|
|
expect((error as AppError).code).toBe("runtime_unavailable")
|
|
expect((error as AppError).problem.detail).toBe(REASON)
|
|
}
|
|
})
|
|
})
|