Files
Agent-System-Backend/internal/server/surface_api_test.go
T
weichen adcd8fefd2 Fence U3 account writes and stabilize directory cursors
Why: Account disable could be followed by late Backend business writes, API key creation could implicitly create users, account bootstrap locks could invert, and directory cursors were process-local or leaked out-of-scope changes.

Evidence: Local go test ./... and isolated PostgreSQL package suites passed on the staged candidate; exact committed-head SELF replay and stage 6 human review remain pending.

Impact: Adds final account transactions and external-effect checks across HTTP, Host, Bus and run manager; tightens API key issuance and persists versioned cursor signing. Delegated RDM rename still needs the separately proposed real-operator contract, so A1 and stage 6 are not complete.

Refs: Documents U3 r1, approved A1-A4 amendment fb3a821, pending delegated-projection amendment 5783940, Dev feature/iam-u3-account-directory-recovery.
2026-10-14 07:33:54 +00:00

150 lines
6.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 := createLegacyAPIKeyFixture(t, st, "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, _ := createLegacyAPIKeyFixture(t, st, "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
}