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
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:
@@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user