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