ci / vet-and-test (pull_request) Successful in 40s
Why: Generative UI proposal U2 needs the host side of the contract: owner-isolated surface storage with revision CAS, an execution-level enable/disable ledger (default-off, epoch bumped on every transition so in-flight submits after a disable are rejected), a single-choke-point service that validates specs (strict decode + catalog rules) before saving, event publish with pending recovery, the optional SurfaceHost extension capability, and direct API endpoints that share the same guard (no bypass by entrance). Evidence: go build/vet clean; full go test ./... passes (SQLite); surface store tests also pass under AGENT_PG_TEST_DSN (PostgreSQL); surface.txt regenerated via the AST snapshot test. Impact: Additive migration v22 (new tables only); existing endpoints and plugin_grants semantics untouched; old consumers unaffected. U3+ (data binding, actions, frontend) deliberately out of scope. Refs: Documents proposal 2026-10-10-generative-ui-plugin.md (r2 U2); Core PR #25; Dev task feature/generative-ui-u2
90 lines
3.3 KiB
Go
90 lines
3.3 KiB
Go
package server
|
||
|
||
// Generative UI U2 红测(提案 KTD7):直接 API 与插件命令共用 surfacesvc
|
||
// 单一咽喉——禁用状态下 POST /api/surfaces 必须被拒(不能因入口绕过执行级
|
||
// 启禁),读取与启禁端点行为符合「历史保留 + epoch 纪元」语义。
|
||
|
||
import (
|
||
"bytes"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"testing"
|
||
|
||
"agent-system-backend/internal/surfacesvc"
|
||
)
|
||
|
||
func TestSurfaceDirectAPIGuards(t *testing.T) {
|
||
srv, st := newAuthTestServer(t)
|
||
h := srv.Handler()
|
||
key, err := st.CreateAPIKey("alice", "e2e")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
svc := surfacesvc.New(st, nil)
|
||
srv.Surfaces = svc
|
||
|
||
post := func(path, body string) *httptest.ResponseRecorder {
|
||
req := httptest.NewRequest(http.MethodPost, path, bytes.NewBufferString(body))
|
||
req.Header.Set("Authorization", "Bearer "+key)
|
||
req.Header.Set("Content-Type", "application/json")
|
||
rec := httptest.NewRecorder()
|
||
h.ServeHTTP(rec, req)
|
||
return rec
|
||
}
|
||
get := func(path string) *httptest.ResponseRecorder {
|
||
req := httptest.NewRequest(http.MethodGet, path, nil)
|
||
req.Header.Set("Authorization", "Bearer "+key)
|
||
rec := httptest.NewRecorder()
|
||
h.ServeHTTP(rec, req)
|
||
return rec
|
||
}
|
||
spec := `"spec":{"title":"t","nodes":[{"type":"markdown","id":"n1","markdown":"x"}]}`
|
||
|
||
// 未启用:提交被拒(403),提示能力存在但插件未启用
|
||
if rec := post("/api/surfaces", `{"pluginId":"genui","conversationId":"c1",`+spec+`}`); rec.Code != http.StatusForbidden {
|
||
t.Fatalf("disabled submit should 403, got %d: %s", rec.Code, rec.Body.String())
|
||
}
|
||
// 启用(activation 端点走服务层,epoch=1)
|
||
if rec := post("/api/plugins/genui/activation", `{"enabled":true}`); rec.Code != http.StatusOK {
|
||
t.Fatalf("enable failed: %d %s", rec.Code, rec.Body.String())
|
||
}
|
||
// 提交成功 → 201
|
||
rec := post("/api/surfaces", `{"pluginId":"genui","conversationId":"c1",`+spec+`}`)
|
||
if rec.Code != http.StatusCreated {
|
||
t.Fatalf("enabled submit should 201, got %d: %s", rec.Code, rec.Body.String())
|
||
}
|
||
var snap struct {
|
||
Identity struct {
|
||
SurfaceID string `json:"surfaceId"`
|
||
} `json:"identity"`
|
||
Revision int `json:"revision"`
|
||
}
|
||
readJSON(t, rec.Body.Bytes(), &snap)
|
||
|
||
// 读取可用;列表可用
|
||
if rec := get("/api/surfaces/" + snap.Identity.SurfaceID); rec.Code != http.StatusOK {
|
||
t.Fatalf("get failed: %d", rec.Code)
|
||
}
|
||
if rec := get("/api/conversations/c1/surfaces"); rec.Code != http.StatusOK {
|
||
t.Fatalf("list failed: %d", rec.Code)
|
||
}
|
||
// 禁用后:提交 403;读取仍 200(历史保留)
|
||
if rec := post("/api/plugins/genui/activation", `{"enabled":false}`); rec.Code != http.StatusOK {
|
||
t.Fatalf("disable failed: %d", rec.Code)
|
||
}
|
||
if rec := post("/api/surfaces", `{"pluginId":"genui","conversationId":"c1",`+spec+`}`); rec.Code != http.StatusForbidden {
|
||
t.Fatalf("post-disable submit should 403, got %d", rec.Code)
|
||
}
|
||
if rec := get("/api/surfaces/" + snap.Identity.SurfaceID); rec.Code != http.StatusOK {
|
||
t.Fatalf("post-disable read should stay 200, got %d", rec.Code)
|
||
}
|
||
// 未带凭据:401(认证面同现有守卫)
|
||
if rec := httptest.NewRecorder(); true {
|
||
req := httptest.NewRequest(http.MethodPost, "/api/surfaces", bytes.NewBufferString(`{"pluginId":"genui"}`))
|
||
h.ServeHTTP(rec, req)
|
||
if rec.Code != http.StatusUnauthorized {
|
||
t.Fatalf("unauthenticated should 401, got %d", rec.Code)
|
||
}
|
||
}
|
||
}
|