Some checks failed
CI/CD / detect-changes (pull_request) Successful in 23s
CI/CD / test-frontend (pull_request) Has been skipped
CI/CD / test-runners (pull_request) Has been skipped
CI/CD / e2e-smoke (pull_request) Has been skipped
CI/CD / test-go-pg (pull_request) Failing after 2m59s
CI/CD / test-sqlite (pull_request) Failing after 3m22s
CI/CD / build-and-deploy (pull_request) Has been skipped
Extensions declare environment requirements via capabilities.required and capabilities.optional in their manifest. The kernel validates at install time — required capabilities reject with HTTP 422 and rollback, optional capabilities log a warning. Runtime query via settings.has_capability(). Admin endpoint GET /api/v1/admin/capabilities re-probes live. Detected capabilities: postgres, pgvector, object_storage, s3, workspace. 18 new tests, all passing with -race. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
206 lines
5.8 KiB
Go
206 lines
5.8 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// ── ParseCapabilities ───────────────────────────────────────────────
|
|
|
|
func TestParseCapabilities_Full(t *testing.T) {
|
|
manifest := map[string]any{
|
|
"id": "test-pkg",
|
|
"capabilities": map[string]any{
|
|
"required": []any{"postgres", "pgvector"},
|
|
"optional": []any{"s3"},
|
|
},
|
|
}
|
|
caps, ok := ParseCapabilities(manifest)
|
|
if !ok {
|
|
t.Fatal("expected ok=true")
|
|
}
|
|
if len(caps.Required) != 2 || caps.Required[0] != "postgres" || caps.Required[1] != "pgvector" {
|
|
t.Errorf("required = %v, want [postgres pgvector]", caps.Required)
|
|
}
|
|
if len(caps.Optional) != 1 || caps.Optional[0] != "s3" {
|
|
t.Errorf("optional = %v, want [s3]", caps.Optional)
|
|
}
|
|
}
|
|
|
|
func TestParseCapabilities_Missing(t *testing.T) {
|
|
manifest := map[string]any{"id": "test-pkg"}
|
|
_, ok := ParseCapabilities(manifest)
|
|
if ok {
|
|
t.Error("expected ok=false when no capabilities block")
|
|
}
|
|
}
|
|
|
|
func TestParseCapabilities_EmptyArrays(t *testing.T) {
|
|
manifest := map[string]any{
|
|
"capabilities": map[string]any{
|
|
"required": []any{},
|
|
"optional": []any{},
|
|
},
|
|
}
|
|
caps, ok := ParseCapabilities(manifest)
|
|
if !ok {
|
|
t.Fatal("expected ok=true")
|
|
}
|
|
if len(caps.Required) != 0 {
|
|
t.Errorf("required should be empty, got %v", caps.Required)
|
|
}
|
|
if len(caps.Optional) != 0 {
|
|
t.Errorf("optional should be empty, got %v", caps.Optional)
|
|
}
|
|
}
|
|
|
|
func TestParseCapabilities_IgnoresEmpty(t *testing.T) {
|
|
manifest := map[string]any{
|
|
"capabilities": map[string]any{
|
|
"required": []any{"pgvector", "", "postgres"},
|
|
},
|
|
}
|
|
caps, _ := ParseCapabilities(manifest)
|
|
if len(caps.Required) != 2 {
|
|
t.Errorf("expected 2 entries (empty filtered), got %v", caps.Required)
|
|
}
|
|
}
|
|
|
|
// ── CheckRequiredCapabilities ───────────────────────────────────────
|
|
|
|
func TestCheckRequired_AllMet(t *testing.T) {
|
|
detected := map[string]bool{"postgres": true, "pgvector": true, "s3": true}
|
|
missing := CheckRequiredCapabilities([]string{"postgres", "pgvector"}, detected)
|
|
if len(missing) != 0 {
|
|
t.Errorf("expected no missing, got %v", missing)
|
|
}
|
|
}
|
|
|
|
func TestCheckRequired_SomeUnmet(t *testing.T) {
|
|
detected := map[string]bool{"postgres": true, "pgvector": false, "s3": false}
|
|
missing := CheckRequiredCapabilities([]string{"postgres", "pgvector", "s3"}, detected)
|
|
if len(missing) != 2 {
|
|
t.Errorf("expected 2 missing, got %v", missing)
|
|
}
|
|
}
|
|
|
|
func TestCheckRequired_EmptyRequired(t *testing.T) {
|
|
detected := map[string]bool{"postgres": true}
|
|
missing := CheckRequiredCapabilities(nil, detected)
|
|
if len(missing) != 0 {
|
|
t.Errorf("expected no missing for nil required, got %v", missing)
|
|
}
|
|
}
|
|
|
|
func TestCheckRequired_UnknownCap(t *testing.T) {
|
|
detected := map[string]bool{"postgres": true}
|
|
missing := CheckRequiredCapabilities([]string{"gpu"}, detected)
|
|
if len(missing) != 1 || missing[0] != "gpu" {
|
|
t.Errorf("expected [gpu], got %v", missing)
|
|
}
|
|
}
|
|
|
|
// ── capabilityHelpText ──────────────────────────────────────────────
|
|
|
|
func TestCapabilityHelpText_KnownCaps(t *testing.T) {
|
|
hints := capabilityHelpText([]string{"pgvector", "s3"})
|
|
if len(hints) != 2 {
|
|
t.Fatalf("expected 2 hints, got %d", len(hints))
|
|
}
|
|
if hints["pgvector"] == "" {
|
|
t.Error("pgvector hint is empty")
|
|
}
|
|
if hints["s3"] == "" {
|
|
t.Error("s3 hint is empty")
|
|
}
|
|
}
|
|
|
|
func TestCapabilityHelpText_UnknownCap(t *testing.T) {
|
|
hints := capabilityHelpText([]string{"gpu"})
|
|
if hints["gpu"] == "" {
|
|
t.Error("expected fallback hint for unknown cap")
|
|
}
|
|
}
|
|
|
|
// ── DetectCapabilities (SQLite path — no real DB) ───────────────────
|
|
|
|
func TestDetectCapabilities_SQLite_NilStore(t *testing.T) {
|
|
caps := DetectCapabilities(nil, false, nil, "")
|
|
if caps["postgres"] {
|
|
t.Error("postgres should be false on SQLite")
|
|
}
|
|
if caps["pgvector"] {
|
|
t.Error("pgvector should be false on SQLite")
|
|
}
|
|
if caps["object_storage"] {
|
|
t.Error("object_storage should be false with nil store")
|
|
}
|
|
if caps["s3"] {
|
|
t.Error("s3 should be false with nil store")
|
|
}
|
|
if caps["workspace"] {
|
|
t.Error("workspace should be false with empty root")
|
|
}
|
|
}
|
|
|
|
func TestDetectCapabilities_WorkspaceWritable(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
caps := DetectCapabilities(nil, false, nil, tmpDir)
|
|
if !caps["workspace"] {
|
|
t.Error("workspace should be true for writable temp dir")
|
|
}
|
|
}
|
|
|
|
func TestDetectCapabilities_WorkspaceUnwritable(t *testing.T) {
|
|
caps := DetectCapabilities(nil, false, nil, "/nonexistent/path/xyz")
|
|
if caps["workspace"] {
|
|
t.Error("workspace should be false for nonexistent path")
|
|
}
|
|
}
|
|
|
|
// ── Admin endpoint ──────────────────────────────────────────────────
|
|
|
|
func TestGetCapabilities_HTTP(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
h := NewCapabilitiesHandler(func() map[string]bool {
|
|
return map[string]bool{
|
|
"postgres": false,
|
|
"pgvector": false,
|
|
"object_storage": true,
|
|
"s3": false,
|
|
"workspace": true,
|
|
}
|
|
})
|
|
|
|
r := gin.New()
|
|
r.GET("/api/v1/admin/capabilities", h.GetCapabilities)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/capabilities", nil)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Fatalf("status = %d, want 200", w.Code)
|
|
}
|
|
|
|
var result map[string]bool
|
|
if err := json.Unmarshal(w.Body.Bytes(), &result); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
|
|
if result["postgres"] {
|
|
t.Error("postgres should be false")
|
|
}
|
|
if !result["object_storage"] {
|
|
t.Error("object_storage should be true")
|
|
}
|
|
if !result["workspace"] {
|
|
t.Error("workspace should be true")
|
|
}
|
|
}
|