This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
// 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)
|
||||
}
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user