package workbench import ( "encoding/json" "net/http/httptest" "strings" "testing" "github.com/Tencent/WeKnora/internal/types" "github.com/gin-gonic/gin" ) func TestResourceSummaryPreservesDaysAndConfigurationWithoutHistory(t *testing.T) { resource := Resource{ID: "map", Kind: "training-maps", Name: "Plan", OwnerID: "alice", Version: 3, Data: asJSON(MapData{AgentID: "author", DaysCount: 2, Days: []Day{ {Day: 1, Title: "Foundation", Framework: strings.Repeat("full text", 10000), Nodes: []Node{{ID: "a", Label: "topic"}}}, {Day: 2, Title: "Practice", Hook: "Next lesson"}, }})} resource.Data["legacy_history"] = JSON{"old": strings.Repeat("history", 10000)} resource.Data["legacy_import"] = JSON{"source_id": 5, "needs_configuration": true} resource.Data["legacy_activation"] = JSON{"configured": true, "audit": "retained only in detail"} before := compact(resource.Data) summary := summarizeResource(resource) if !summary.Summary || summary.ID != resource.ID || summary.Version != resource.Version || summary.OwnerID != resource.OwnerID { t.Fatal("summary lost identity, ownership or version") } if len(compact(summary)) > 1000 || compact(summary.Data["days"]) != `[{"day":1,"title":"Foundation"},{"day":2,"title":"Practice"}]` { t.Fatal("summary included full content or lost day selection metadata") } if compact(summary.Data["legacy_activation"]) != `{"configured":true}` || summary.Data["legacy_import"] == nil { t.Fatal("summary lost configuration state") } summary.Data["agent_id"] = "changed" if compact(resource.Data) != before { t.Fatal("summary changed persisted detail data") } for _, kind := range []string{"content-tasks", "flows"} { resource.Kind = kind resource.Data["segments"] = []Segment{{Content: "full manuscript"}} resource.Data["composed"] = "final manuscript" resource.Data["flow_id"] = "canvas" if got := summarizeResource(resource); got.Data["flow_id"] != "canvas" || got.Data["segments"] != nil || got.Data["composed"] != nil || got.Data["days"] != nil { t.Fatal("non-map summary contains generated body or omits selector metadata") } } } func TestSummaryListRetainsPaginationAndTenantOwnerScope(t *testing.T) { s := sqliteWorkbench(t) owned := seedResource(t, s, 1, "alice", "training-maps") owned.Data = JSON{"legacy_history": JSON{"source": "complete history"}, "agent_id": "author", "days": []Day{{Day: 1, Title: "Start", Framework: "complete framework"}}} if err := s.db.Save(owned).Error; err != nil { t.Fatal(err) } before, err := s.get(workbenchContext(1, "alice", types.TenantRoleContributor), owned.Kind, owned.ID) if err != nil { t.Fatal(err) } beforeData := compact(before.Data) seedResource(t, s, 1, "bob", "training-maps") seedResource(t, s, 2, "alice", "training-maps") seedResource(t, s, 1, "alice", "content-tasks") for _, tc := range []struct { name, query string role types.TenantRole total, count int summary bool }{ {"private summary", "?view=summary", types.TenantRoleContributor, 1, 1, true}, {"full compatibility", "", types.TenantRoleContributor, 1, 1, false}, {"admin pagination", "?view=summary&page_size=1", types.TenantRoleAdmin, 2, 1, true}, {"past last page", "?view=summary&page=3&page_size=1", types.TenantRoleAdmin, 2, 0, true}, } { t.Run(tc.name, func(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("GET", "/"+tc.query, nil).WithContext(workbenchContext(1, "alice", tc.role)) c.Params = gin.Params{{Key: "kind", Value: "training-maps"}} NewHandler(s).List(c) var response struct { Data []resourceListSummary `json:"data"` Total int `json:"total"` } if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil || w.Code != 200 || response.Total != tc.total || len(response.Data) != tc.count { t.Fatalf("invalid response: %d %s", w.Code, w.Body.String()) } for _, row := range response.Data { if row.TenantID != 1 || row.Summary != tc.summary || (tc.role == types.TenantRoleContributor && row.ID != owned.ID) { t.Fatal("summary changed authorization scope") } if tc.summary && row.Data["legacy_history"] != nil { t.Fatal("history in summary") } if !tc.summary && row.Data["legacy_history"] == nil { t.Fatal("default list lost history") } } }) } stored, err := s.get(workbenchContext(1, "alice", types.TenantRoleContributor), owned.Kind, owned.ID) if err != nil || compact(stored.Data) != beforeData { t.Fatal("listing changed complete detail") } } func TestSummaryCannotOverwriteCompleteResource(t *testing.T) { s := sqliteWorkbench(t) r := seedResource(t, s, 1, "alice", "training-maps") r.Data = JSON{"days": []Day{{Day: 1, Title: "Start", Framework: "complete framework"}}} if err := s.db.Save(r).Error; err != nil { t.Fatal(err) } w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("PUT", "/", strings.NewReader(compact(summarizeResource(*r)))).WithContext(workbenchContext(1, "alice", types.TenantRoleContributor)) c.Request.Header.Set("Content-Type", "application/json") c.Params = gin.Params{{Key: "kind", Value: r.Kind}, {Key: "id", Value: r.ID}} NewHandler(s).Update(c) if w.Code != 400 || !strings.Contains(w.Body.String(), "完整详情") { t.Fatalf("summary update not rejected: %d %s", w.Code, w.Body.String()) } stored, err := s.get(c.Request.Context(), r.Kind, r.ID) if err != nil || stored.Version != r.Version || !strings.Contains(compact(stored.Data), "complete framework") { t.Fatal("rejected summary changed stored resource") } } func TestSummaryListCanIncludeOnlyAuthorizedInitialDetail(t *testing.T) { s := sqliteWorkbench(t) owned := seedResource(t, s, 1, "alice", "training-maps") owned.Data = JSON{"days": []Day{{Day: 1, Title: "Start", Framework: "private full framework"}}} if err := s.db.Save(owned).Error; err != nil { t.Fatal(err) } other := seedResource(t, s, 1, "bob", "training-maps") for _, tc := range []struct { name, user, selected string wantDetail bool wantID string }{ {"owner gets detail", "alice", owned.ID, true, owned.ID}, {"non-owner falls back in scope", "alice", other.ID, true, owned.ID}, {"owner cannot cross tenant", "alice", owned.ID, false, ""}, } { t.Run(tc.name, func(t *testing.T) { ctx := workbenchContext(1, tc.user, types.TenantRoleContributor) if tc.name == "owner cannot cross tenant" { ctx = workbenchContext(2, tc.user, types.TenantRoleAdmin) } w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("GET", "/?view=summary&include=initial_detail&selected_id="+tc.selected, nil).WithContext(ctx) c.Params = gin.Params{{Key: "kind", Value: "training-maps"}} NewHandler(s).List(c) var response struct { Initial *Resource `json:"initial_detail"` } if w.Code != 200 || json.Unmarshal(w.Body.Bytes(), &response) != nil { t.Fatalf("invalid response: %d %s", w.Code, w.Body.String()) } if (response.Initial != nil) != tc.wantDetail { t.Fatalf("initial detail presence = %v, want %v: %s", response.Initial != nil, tc.wantDetail, w.Body.String()) } if response.Initial != nil && response.Initial.ID != tc.wantID { t.Fatalf("initial detail id = %s, want %s", response.Initial.ID, tc.wantID) } if response.Initial != nil && response.Initial.Data["days"] == nil { t.Fatal("initial detail was reduced to summary data") } }) } }