- shared TS collection manager: self-heal creates missing collection (1024/cosine) and missing keyword payload indexes; never mutates incompatible contracts (semantic_index_incompatible); async index visibility polled with bounded deadline - session admission (qdrantEnsure) uses the manager in self-heal mode; operator path keeps require_existing semantics - runtime lease exposes semanticQdrantUrl to the operator - operator commands vector-inspect/vector-rebuild with exact confirmation guards - thothctl workspace vector inspect|rebuild (Go) with --collection/--confirm/--destroy - p4 acceptance runner: real Qdrant (v1.18.2) lifecycle checks, 11/11 PASS - docs: CLI contract, manual walkthrough P4 (PENDING), PROJECT_STATE
115 lines
5.7 KiB
TypeScript
115 lines
5.7 KiB
TypeScript
import { expect, test, vi } from "vitest";
|
|
import {
|
|
runWorkspaceMaintenanceCli,
|
|
type WorkspaceMaintenanceIo,
|
|
} from "../src/workspace-maintenance.js";
|
|
import type { WorkspaceOperationResult } from "../src/workspaces/preprocessing-service.js";
|
|
|
|
function io(stdin: string): WorkspaceMaintenanceIo & { stdout: string[]; stderr: string[] } {
|
|
const stdout: string[] = [];
|
|
const stderr: string[] = [];
|
|
return {
|
|
stdin,
|
|
stdout,
|
|
stderr,
|
|
writeStdout: (value) => void stdout.push(value),
|
|
writeStderr: (value) => void stderr.push(value),
|
|
};
|
|
}
|
|
|
|
function ok(operation: string): WorkspaceOperationResult {
|
|
return {
|
|
schemaVersion: 1,
|
|
status: "succeeded",
|
|
code: "ok",
|
|
workspaceId: "psd-clinical",
|
|
workspaceRevision: "a".repeat(40),
|
|
descriptorBlob: "b".repeat(40),
|
|
operation,
|
|
completedStages: [],
|
|
};
|
|
}
|
|
|
|
test("entrypoint emits exactly one pristine JSON document and maps success/block/failure exits", async () => {
|
|
const service = {
|
|
inspect: vi.fn(async () => ok("inspect")),
|
|
preprocessDwh: vi.fn(async () => ({ ...ok("preprocess dwh"), status: "blocked", code: "manual_review_required" as const })),
|
|
run: vi.fn(async () => ({ ...ok("preprocess run"), status: "failed", code: "semantic_index_incompatible" as const })),
|
|
} as any;
|
|
|
|
const inspectIo = io(JSON.stringify({ schemaVersion: 1, workspaceId: "psd-clinical" }));
|
|
expect(await runWorkspaceMaintenanceCli(["node", "workspace-maintenance", "inspect"], service, inspectIo)).toBe(0);
|
|
expect(JSON.parse(inspectIo.stdout.join(""))).toMatchObject({ operation: "inspect", code: "ok" });
|
|
expect(inspectIo.stderr.join("")).toBe("");
|
|
|
|
const blockedIo = io(JSON.stringify({ schemaVersion: 1, workspaceId: "psd-clinical" }));
|
|
expect(await runWorkspaceMaintenanceCli(["node", "workspace-maintenance", "preprocess-dwh"], service, blockedIo)).toBe(3);
|
|
expect(JSON.parse(blockedIo.stdout.join(""))).toMatchObject({ code: "manual_review_required" });
|
|
|
|
const failedIo = io(JSON.stringify({ schemaVersion: 1, workspaceId: "psd-clinical" }));
|
|
expect(await runWorkspaceMaintenanceCli(["node", "workspace-maintenance", "preprocess-run"], service, failedIo)).toBe(1);
|
|
expect(JSON.parse(failedIo.stdout.join(""))).toMatchObject({ code: "semantic_index_incompatible" });
|
|
});
|
|
|
|
test("malformed stdin, unknown commands, and extra fields fail with exit 2 but still return bounded JSON", async () => {
|
|
const service = {} as any;
|
|
|
|
const malformedIo = io("not-json");
|
|
expect(await runWorkspaceMaintenanceCli(["node", "workspace-maintenance", "inspect"], service, malformedIo)).toBe(2);
|
|
expect(JSON.parse(malformedIo.stdout.join(""))).toMatchObject({ status: "failed", operation: "inspect" });
|
|
|
|
const extraFieldIo = io(JSON.stringify({ schemaVersion: 1, workspaceId: "psd-clinical", unexpected: true }));
|
|
expect(await runWorkspaceMaintenanceCli(["node", "workspace-maintenance", "inspect"], service, extraFieldIo)).toBe(2);
|
|
expect(JSON.parse(extraFieldIo.stdout.join(""))).toMatchObject({ status: "failed", operation: "inspect" });
|
|
|
|
const unknownIo = io(JSON.stringify({ schemaVersion: 1, workspaceId: "psd-clinical" }));
|
|
expect(await runWorkspaceMaintenanceCli(["node", "workspace-maintenance", "explode"], service, unknownIo)).toBe(2);
|
|
expect(JSON.parse(unknownIo.stdout.join(""))).toMatchObject({ status: "failed", operation: "explode" });
|
|
});
|
|
|
|
test("raw exception text is redacted from stderr and stdout remains within the public result contract", async () => {
|
|
const service = {
|
|
inspect: vi.fn(async () => {
|
|
throw new Error("https://secret.example.invalid?q=token SELECT * FROM sensitive_table");
|
|
}),
|
|
} as any;
|
|
const captured = io(JSON.stringify({ schemaVersion: 1, workspaceId: "psd-clinical" }));
|
|
|
|
expect(await runWorkspaceMaintenanceCli(["node", "workspace-maintenance", "inspect"], service, captured)).toBe(1);
|
|
expect(JSON.parse(captured.stdout.join(""))).toMatchObject({
|
|
status: "failed",
|
|
operation: "inspect",
|
|
});
|
|
expect(captured.stderr.join("")).not.toContain("secret.example.invalid");
|
|
expect(captured.stderr.join("")).not.toContain("SELECT *");
|
|
});
|
|
|
|
test("vector-inspect and vector-rebuild dispatch to the service with the exact envelope", async () => {
|
|
const service = {
|
|
vectorInspect: vi.fn(async () => ok("vector inspect")),
|
|
vectorRebuild: vi.fn(async () => ok("vector rebuild")),
|
|
} as any;
|
|
|
|
const inspectIo = io(JSON.stringify({ schemaVersion: 1, workspaceId: "psd-clinical" }));
|
|
expect(await runWorkspaceMaintenanceCli(["node", "workspace-maintenance", "vector-inspect"], service, inspectIo)).toBe(0);
|
|
expect(service.vectorInspect).toHaveBeenCalledWith({ workspaceId: "psd-clinical" });
|
|
expect(JSON.parse(inspectIo.stdout.join(""))).toMatchObject({ operation: "vector inspect", code: "ok" });
|
|
|
|
const rebuildIo = io(JSON.stringify({ schemaVersion: 1, workspaceId: "psd-clinical", collection: "psd-clinical", confirm: "psd-clinical", destroy: true }));
|
|
expect(await runWorkspaceMaintenanceCli(["node", "workspace-maintenance", "vector-rebuild"], service, rebuildIo)).toBe(0);
|
|
expect(service.vectorRebuild).toHaveBeenCalledWith({
|
|
workspaceId: "psd-clinical",
|
|
collection: "psd-clinical",
|
|
confirm: "psd-clinical",
|
|
destroy: true,
|
|
});
|
|
});
|
|
|
|
test("vector-rebuild without exact confirmation is refused by the service", async () => {
|
|
const service = {
|
|
vectorRebuild: vi.fn(async () => ({ ...ok("vector rebuild"), status: "failed", code: "semantic_index_incompatible" as const })),
|
|
} as any;
|
|
const rebuildIo = io(JSON.stringify({ schemaVersion: 1, workspaceId: "psd-clinical", collection: "other", confirm: "other", destroy: true }));
|
|
expect(await runWorkspaceMaintenanceCli(["node", "workspace-maintenance", "vector-rebuild"], service, rebuildIo)).toBe(1);
|
|
});
|