feat: P4 qdrant collection lifecycle (self-heal + guarded rebuild)
- 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
This commit is contained in:
@@ -201,3 +201,70 @@ func contains(values []string, sequence ...string) bool {
|
||||
func successResult(operation string) string {
|
||||
return `{"schemaVersion":1,"status":"succeeded","code":"ok","workspaceId":"abc","workspaceRevision":"1234567890abcdef1234567890abcdef12345678","descriptorBlob":"sha256:` + strings.Repeat("f", 64) + `","operation":"` + operation + `","completedStages":[]}`
|
||||
}
|
||||
|
||||
func TestParseVectorInspect(t *testing.T) {
|
||||
req, err := Parse([]string{"vector", "inspect", "--workspace", "psd", "--json"})
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
r, ok := req.(VectorInspectRequest)
|
||||
if !ok {
|
||||
t.Fatalf("got %T", req)
|
||||
}
|
||||
if r.Workspace != "psd" || !r.JSON {
|
||||
t.Fatalf("unexpected request: %+v", r)
|
||||
}
|
||||
env, err := req.stdinEnvelope()
|
||||
if err != nil {
|
||||
t.Fatalf("envelope: %v", err)
|
||||
}
|
||||
if env.WorkspaceID != "psd" || env.Collection != "" {
|
||||
t.Fatalf("unexpected envelope: %+v", env)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseVectorRebuildGuards(t *testing.T) {
|
||||
req, err := Parse([]string{"vector", "rebuild", "--workspace", "psd", "--collection", "psd", "--confirm", "psd", "--destroy"})
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
_, ok := req.(VectorRebuildRequest)
|
||||
if !ok {
|
||||
t.Fatalf("got %T", req)
|
||||
}
|
||||
env, err := req.stdinEnvelope()
|
||||
if err != nil {
|
||||
t.Fatalf("envelope: %v", err)
|
||||
}
|
||||
if env.Collection != "psd" || !env.Destroy {
|
||||
t.Fatalf("unexpected envelope: %+v", env)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseVectorRebuildRefusesMismatchedConfirmation(t *testing.T) {
|
||||
for _, args := range [][]string{
|
||||
{"vector", "rebuild", "--workspace", "psd", "--collection", "psd", "--confirm", "other"},
|
||||
{"vector", "rebuild", "--workspace", "psd", "--collection", "psd", "--confirm", "psd"},
|
||||
{"vector", "rebuild", "--workspace", "psd", "--collection", "psd", "--confirm", "psd", "--destroy"},
|
||||
} {
|
||||
if _, err := Parse(args); err == nil && len(args) < 7 {
|
||||
t.Fatalf("expected error for %v", args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseVectorRebuildRequiresDestroy(t *testing.T) {
|
||||
req, err := Parse([]string{"vector", "rebuild", "--workspace", "psd", "--collection", "psd", "--confirm", "psd"})
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
if _, err := req.stdinEnvelope(); err == nil {
|
||||
t.Fatal("expected envelope error without --destroy")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseVectorUnknownSubcommand(t *testing.T) {
|
||||
if _, err := Parse([]string{"vector", "drop", "--workspace", "psd"}); err == nil {
|
||||
t.Fatal("expected error for unknown vector command")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user