package server // Generative UI U2 红测(提案 KTD7):直接 API 与插件命令共用 surfacesvc // 单一咽喉——禁用状态下 POST /api/surfaces 必须被拒(不能因入口绕过执行级 // 启禁),读取与启禁端点行为符合「历史保留 + epoch 纪元」语义。 import ( "bytes" "context" "fmt" "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) } } } // U5:动作端点红测——禁用 403、未知动作 400、端点不绑定 confirmed(伪造无入口)。 func TestSurfaceActionEndpoint(t *testing.T) { srv, st := newAuthTestServer(t) h := srv.Handler() key, _ := st.CreateAPIKey("alice", "e2e") svc := surfacesvc.New(st, nil) svc.SetDispatcher(func(ctx context.Context, d surfacesvc.ActionDispatch) (surfacesvc.ActionOutcome, error) { return surfacesvc.ActionOutcome{State: "committed", Receipt: map[string]any{"ok": true}}, nil }) srv.Surfaces = svc ctx := context.Background() svc.SetActivation(ctx, "alice", "generative-ui", true) spec := `"spec":{"title":"表单页","nodes":[{"type":"actions","id":"a1","actions":[{"id":"generative-ui.submit","label":"提交"}]}]}` post := func(body string) *httptest.ResponseRecorder { req := httptest.NewRequest(http.MethodPost, "/api/surfaces", bytes.NewBufferString(`{"pluginId":"generative-ui","conversationId":"c1",`+spec+`}`)) req.Header.Set("Authorization", "Bearer "+key) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) return rec } rec := post("") if rec.Code != http.StatusCreated { t.Fatalf("setup submit failed: %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) act := func(body string) *httptest.ResponseRecorder { req := httptest.NewRequest(http.MethodPost, "/api/surfaces/"+snap.Identity.SurfaceID+"/actions", 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 } // 未知动作 → 400 if r := act(`{"actionId":"shell.exec","revision":` + fmt.Sprint(snap.Revision) + `,"requestId":"r1","data":{}}`); r.Code != http.StatusBadRequest { t.Fatalf("unknown action should 400, got %d: %s", r.Code, r.Body.String()) } // 伪造 confirmed 字段:请求体里带 confirmed:true 不产生任何效果(端点不绑定 // 该字段;DangerHigh 确认只能经确认卡回道)。此处 submit 非 DangerHigh, // 断言 confirmed 被忽略(成功且不因它改变行为)。 if r := act(`{"actionId":"generative-ui.submit","revision":` + fmt.Sprint(snap.Revision) + `,"requestId":"r2","data":{"a":"1"},"confirmed":true}`); r.Code != http.StatusOK { t.Fatalf("submit with forged confirmed should still be normal submit, got %d: %s", r.Code, r.Body.String()) } // 禁用后 → 403 svc.SetActivation(ctx, "alice", "generative-ui", false) if r := act(`{"actionId":"generative-ui.submit","revision":` + fmt.Sprint(snap.Revision) + `,"requestId":"r3","data":{}}`); r.Code != http.StatusForbidden { t.Fatalf("post-disable action should 403, got %d", r.Code) } _ = fmt.Sprint }