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
+162
View File
@@ -0,0 +1,162 @@
package workbench
import (
"bytes"
"context"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/Tencent/WeKnora/internal/types"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
func TestMemoryApprovalIsAtomicAndOwnerScoped(t *testing.T) {
for _, mode := range []string{"accepted", "rejected", "write_failure", "status_failure", "nil_result", "pending_result", "other_owner", "other_tenant"} {
t.Run(mode, func(t *testing.T) {
s := sqliteWorkbench(t)
if err := s.db.AutoMigrate(&types.MemoryItem{}); err != nil {
t.Fatal(err)
}
row := PendingMemory{ID: "pending", TenantID: 1, OwnerID: "alice", Content: "每次直播先介绍主题", Status: "pending"}
if err := s.db.Create(&row).Error; err != nil {
t.Fatal(err)
}
s.SetTransactionalMemoryWriter(func(ctx context.Context, tx *gorm.DB, item types.MemoryItem) (*types.MemoryItem, error) {
item.ID, item.TenantID, item.SubjectID = "native", 1, "alice"
item.Status = types.MemoryStatusActive
if err := tx.Create(&item).Error; err != nil {
return nil, err
}
if mode == "write_failure" {
return nil, errors.New("write failed")
}
if mode == "nil_result" {
return nil, nil
}
if mode == "pending_result" {
item.Status = types.MemoryStatusPending
}
return &item, nil
})
if mode == "status_failure" {
if err := s.db.Exec("CREATE TRIGGER reject_approval BEFORE UPDATE ON workbench_pending_memories BEGIN SELECT RAISE(ABORT, 'status failed'); END").Error; err != nil {
t.Fatal(err)
}
}
ctx := workbenchContext(1, "alice", types.TenantRoleContributor)
if mode == "other_owner" {
ctx = workbenchContext(1, "bob", types.TenantRoleAdmin)
}
if mode == "other_tenant" {
ctx = workbenchContext(2, "alice", types.TenantRoleAdmin)
}
status := "accepted"
if mode == "rejected" {
status = "rejected"
}
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Params = gin.Params{{Key: "id", Value: row.ID}}
c.Request = httptest.NewRequest(http.MethodPost, "/", bytes.NewBufferString(`{"status":"`+status+`"}`)).WithContext(ctx)
c.Request.Header.Set("Content-Type", "application/json")
NewHandler(s).ReviewMemory(c)
var count int64
if err := s.db.Model(&types.MemoryItem{}).Count(&count).Error; err != nil {
t.Fatal(err)
}
if err := s.db.First(&row, "id = ?", row.ID).Error; err != nil {
t.Fatal(err)
}
if mode == "accepted" || mode == "rejected" {
if w.Code != 200 || row.Status != status {
t.Fatalf("approval failed: %d %s", w.Code, w.Body.String())
}
if (mode == "accepted" && count != 1) || (mode == "rejected" && count != 0) {
t.Fatalf("unexpected memories: %d", count)
}
} else if w.Code < 400 || row.Status != "pending" || count != 0 {
t.Fatalf("failed approval leaked state: http=%d pending=%s memories=%d", w.Code, row.Status, count)
}
})
}
}
func TestMemoryApprovalLengthAndRetry(t *testing.T) {
for _, mode := range []string{"long_accept", "long_reject", "retry", "sanitized"} {
t.Run(mode, func(t *testing.T) {
s := sqliteWorkbench(t)
if err := s.db.AutoMigrate(&types.MemoryItem{}); err != nil {
t.Fatal(err)
}
row := PendingMemory{ID: "pending", TenantID: 1, OwnerID: "alice", Content: "每次直播\n先介绍主题", Status: "pending"}
if strings.HasPrefix(mode, "long_") {
row.Content = strings.Repeat("字", types.MemoryContentMaxRunes+1)
}
if err := s.db.Create(&row).Error; err != nil {
t.Fatal(err)
}
calls := 0
s.SetTransactionalMemoryWriter(func(ctx context.Context, tx *gorm.DB, item types.MemoryItem) (*types.MemoryItem, error) {
calls++
if mode == "retry" && calls == 1 {
return nil, errors.New("temporary write failure")
}
item.ID, item.TenantID, item.SubjectID = "native", 1, "alice"
item.Status = types.MemoryStatusActive
item.Content = types.SanitizeMemoryContent(item.Content)
return &item, tx.Create(&item).Error
})
request := func(status string) *httptest.ResponseRecorder {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Params = gin.Params{{Key: "id", Value: row.ID}}
ctx := workbenchContext(1, "alice", types.TenantRoleContributor)
c.Request = httptest.NewRequest(http.MethodPost, "/", bytes.NewBufferString(`{"status":"`+status+`"}`)).WithContext(ctx)
c.Request.Header.Set("Content-Type", "application/json")
NewHandler(s).ReviewMemory(c)
return w
}
status := "accepted"
if mode == "long_reject" {
status = "rejected"
}
w := request(status)
if mode == "retry" {
if w.Code < 400 {
t.Fatal("failed write reported success")
}
if err := s.db.First(&row, "id = ?", row.ID).Error; err != nil || row.Status != "pending" {
t.Fatalf("retry lost pending row: %+v %v", row, err)
}
w = request(status)
}
if err := s.db.First(&row, "id = ?", row.ID).Error; err != nil {
t.Fatal(err)
}
var count int64
if err := s.db.Model(&types.MemoryItem{}).Count(&count).Error; err != nil {
t.Fatal(err)
}
if mode == "long_accept" {
if w.Code != http.StatusBadRequest || !strings.Contains(w.Body.String(), "300") || row.Status != "pending" || calls != 0 || count != 0 || len([]rune(row.Content)) != types.MemoryContentMaxRunes+1 {
t.Fatalf("long approval was truncated or lost: response=%d %s row=%+v", w.Code, w.Body.String(), row)
}
return
}
if w.Code != http.StatusOK || row.Status != status {
t.Fatalf("review failed: %d %s row=%+v", w.Code, w.Body.String(), row)
}
if mode == "long_reject" {
if calls != 0 || count != 0 {
t.Fatal("rejection created memory")
}
} else if count != 1 || row.Content != "每次直播 先介绍主题" {
t.Fatalf("approved history differs from stored memory: count=%d row=%+v", count, row)
}
})
}
}