WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s

This commit is contained in:
yawan
2026-09-22 16:19:06 +08:00
commit f7fc992f57
3929 changed files with 1006607 additions and 0 deletions
+172
View File
@@ -0,0 +1,172 @@
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")
}
})
}
}