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,232 @@
|
||||
package workbench
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func TestDetachPreservesNativeAndImportedHistory(t *testing.T) {
|
||||
for _, kind := range []string{"training-maps", "flows"} {
|
||||
for _, imported := range []bool{false, true} {
|
||||
t.Run(kind+map[bool]string{false: "/native", true: "/imported"}[imported], func(t *testing.T) {
|
||||
s := sqliteWorkbench(t)
|
||||
ctx := workbenchContext(1, "alice", types.TenantRoleContributor)
|
||||
parent := seedResource(t, s, 1, "alice", kind)
|
||||
task := seedResource(t, s, 1, "alice", "content-tasks")
|
||||
original := TaskData{MapID: "map", FlowID: "flow", Review: "historical review", Composed: "historical final", Segments: []Segment{{ID: "s1", Status: "accepted", Content: "historical paragraph", Review: "historical segment review", SourceIDs: []string{"source"}}}}
|
||||
if kind == "training-maps" {
|
||||
original.MapID = parent.ID
|
||||
} else {
|
||||
original.FlowID = parent.ID
|
||||
}
|
||||
task.Data = asJSON(original)
|
||||
if imported {
|
||||
task.Data["legacy_import"] = JSON{"read_only": true, "requires_review": true, "source_id": "old-task", "missing_configuration": []string{"day"}}
|
||||
task.Data["legacy_history"] = JSON{"qcreports": []JSON{{"result": "historical report", "passed": false}}}
|
||||
}
|
||||
metadata, history := compact(task.Data["legacy_import"]), compact(task.Data["legacy_history"])
|
||||
if err := s.db.Model(task).Update("data", task.Data).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if w := deleteWorkbenchResource(s, ctx, parent, true); w.Code != 200 {
|
||||
t.Fatalf("detach failed: status %d", w.Code)
|
||||
}
|
||||
stored, err := s.get(ctx, task.Kind, task.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data, err := decode[TaskData](stored.Data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stored.Status != "needs_configuration" || data.Review != original.Review || data.Composed != original.Composed || compact(data.Segments) != compact(original.Segments) || compact(stored.Data["legacy_import"]) != metadata || compact(stored.Data["legacy_history"]) != history {
|
||||
t.Fatal("detachment changed historical output or import metadata")
|
||||
}
|
||||
if (kind == "training-maps" && data.MapID != "") || (kind == "flows" && data.FlowID != "") {
|
||||
t.Fatal("deleted reference was retained")
|
||||
}
|
||||
for _, action := range []string{"decompose", "generate", "revise", "accept", "skip", "review", "compose"} {
|
||||
job, err := s.submit(ctx, task.Kind, task.ID, action, ActionRequest{Version: stored.Version, Advice: "advice", SegmentID: "s1"})
|
||||
if !imported && taskActionUsesOnlySavedContent(action) {
|
||||
if err != nil {
|
||||
t.Fatalf("saved prose action %s depends on detached sources: %v", action, err)
|
||||
}
|
||||
if err := s.db.Delete(job).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !errors.Is(err, ErrInvalid) {
|
||||
t.Fatalf("detached task accepted action %s: %v", action, err)
|
||||
}
|
||||
}
|
||||
var jobs int64
|
||||
if err := s.db.Model(&Job{}).Count(&jobs).Error; err != nil || jobs != 0 {
|
||||
t.Fatal("rejected actions were queued")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportGuardsRejectExecutionBeforeAIAndAllowRename(t *testing.T) {
|
||||
for _, metadata := range []any{JSON{"read_only": true}, JSON{"requires_review": true}, JSON{"needs_configuration": true}, JSON{"missing_configuration": []string{"agent_id"}}, JSON{"read_only": "false"}, nil, "invalid"} {
|
||||
for _, kind := range []string{"training-maps", "content-tasks", "flows"} {
|
||||
r := &Resource{Kind: kind, Status: "draft", Data: JSON{"legacy_import": metadata, "legacy_history": JSON{"preserved": true}}}
|
||||
original := compact(r.Data)
|
||||
s := NewService(nil)
|
||||
var err error
|
||||
if kind == "content-tasks" {
|
||||
err = s.executeTask(context.Background(), &Job{Action: "decompose"}, r, ActionRequest{})
|
||||
} else {
|
||||
err = s.executeMap(context.Background(), &Job{Action: "generate"}, r, ActionRequest{})
|
||||
}
|
||||
if !errors.Is(err, ErrInvalid) {
|
||||
t.Fatalf("invalid imported state executed: %v", err)
|
||||
}
|
||||
if err = applyManualUpdate(r, resourceRequest{Name: "renamed", Data: r.Data}); err != nil {
|
||||
t.Fatalf("owner cannot rename historical record: %v", err)
|
||||
}
|
||||
if compact(r.Data) != original {
|
||||
t.Fatal("rejected action changed historical data")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompatibleImportMetadataSurvivesActionsAndCannotBeRewritten(t *testing.T) {
|
||||
metadata := JSON{"source_id": "old-map", "read_only": false, "requires_review": false, "needs_configuration": false}
|
||||
history := JSON{"source_status": "reviewed", "source_text": "historical framework"}
|
||||
r := &Resource{Kind: "training-maps", Name: "map", Data: asJSON(MapData{Topic: "topic", AgentID: "author", DaysCount: 1, Days: []Day{{Day: 1, Title: "day", Framework: "framework"}}})}
|
||||
r.Data["legacy_import"], r.Data["legacy_history"] = metadata, history
|
||||
s := NewService(nil)
|
||||
if err := s.executeMap(context.Background(), &Job{Action: "confirm-day"}, r, ActionRequest{Day: 1}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data, _ := decode[MapData](r.Data)
|
||||
if !data.Days[0].Confirmed || compact(r.Data["legacy_import"]) != compact(metadata) || compact(r.Data["legacy_history"]) != compact(history) {
|
||||
t.Fatal("mapped resource action lost historical metadata")
|
||||
}
|
||||
if err := applyManualUpdate(r, resourceRequest{Name: "renamed", Data: asJSON(data)}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if compact(r.Data["legacy_import"]) != compact(metadata) || compact(r.Data["legacy_history"]) != compact(history) {
|
||||
t.Fatal("configuration update lost migration metadata")
|
||||
}
|
||||
for _, key := range legacyMetadataKeys {
|
||||
proposed := resourceData(r, data)
|
||||
proposed[key] = JSON{"changed": true}
|
||||
if err := applyManualUpdate(r, resourceRequest{Name: "renamed", Data: proposed}); !errors.Is(err, ErrInvalid) {
|
||||
t.Fatalf("reserved field %s can be rewritten: %v", key, err)
|
||||
}
|
||||
}
|
||||
if err := validateLegacyMetadataUpdate(nil, JSON{"legacy_import": metadata}); !errors.Is(err, ErrInvalid) {
|
||||
t.Fatal("client can create forged import metadata")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkerRejectsImportedTaskAlreadyInQueue(t *testing.T) {
|
||||
s := sqliteWorkbench(t)
|
||||
if err := s.db.AutoMigrate(&types.TenantMember{}, &types.Tenant{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.db.Create(&types.Tenant{ID: 1, Status: "active"}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
member := types.TenantMember{TenantID: 1, UserID: "alice", Role: types.TenantRoleContributor, Status: types.TenantMemberStatusActive}
|
||||
if err := s.db.Create(&member).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r := seedResource(t, s, 1, "alice", "content-tasks")
|
||||
r.Data = asJSON(TaskData{Review: "historical review", Composed: "historical final", Segments: []Segment{{ID: "s1", Content: "historical paragraph"}}})
|
||||
r.Data["legacy_import"] = JSON{"read_only": true, "requires_review": true, "needs_configuration": true}
|
||||
if err := s.db.Model(r).Update("data", r.Data).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
before := compact(r.Data)
|
||||
until := time.Now().Add(time.Minute)
|
||||
j := Job{ID: uuid.NewString(), TenantID: 1, OwnerID: "alice", ResourceID: r.ID, Kind: r.Kind, Action: "decompose", Status: "running", LeaseToken: "test-lease", LeaseUntil: &until, Request: asJSON(ActionRequest{Version: 1})}
|
||||
if err := s.db.Create(&j).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.run(context.Background(), &j)
|
||||
var job Job
|
||||
if err := s.db.First(&job, "id = ?", j.ID).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if job.Status != "failed" || !strings.Contains(job.Error, "只读历史资料") || len(job.Generation) != 0 {
|
||||
t.Fatal("queued legacy job was not rejected before model execution")
|
||||
}
|
||||
stored, err := s.get(workbenchContext(1, "alice", types.TenantRoleContributor), r.Kind, r.ID)
|
||||
if err != nil || compact(stored.Data) != before || stored.Version != 1 {
|
||||
t.Fatal("legacy worker rejection changed history")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskMissingConfigurationFailsBeforeQueueing(t *testing.T) {
|
||||
s := sqliteWorkbench(t)
|
||||
r := seedResource(t, s, 1, "alice", "content-tasks")
|
||||
ctx := workbenchContext(1, "alice", types.TenantRoleContributor)
|
||||
if _, err := s.submit(ctx, r.Kind, r.ID, "decompose", ActionRequest{Version: 1}); !errors.Is(err, ErrInvalid) || !strings.Contains(err.Error(), "任务缺少") {
|
||||
t.Fatalf("missing configuration was not rejected: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadOnlyImportCanBeDeletedWithOriginalArchived(t *testing.T) {
|
||||
s := sqliteWorkbench(t)
|
||||
ctx := workbenchContext(1, "alice", types.TenantRoleContributor)
|
||||
r := seedResource(t, s, 1, "alice", "content-tasks")
|
||||
r.Data["legacy_import"] = JSON{"read_only": true}
|
||||
if err := s.db.Model(r).Update("data", r.Data).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if response := deleteWorkbenchResource(s, ctx, r, true); response.Code != 200 {
|
||||
t.Fatalf("owner cannot delete imported resource: status %d", response.Code)
|
||||
}
|
||||
if _, err := s.get(ctx, r.Kind, r.ID); err == nil {
|
||||
t.Fatal("deleted resource remains visible")
|
||||
}
|
||||
var revision Revision
|
||||
if err := s.db.First(&revision, "resource_id = ?", r.ID).Error; err != nil || compact(revision.Data) != compact(r.Data) {
|
||||
t.Fatal("deleted import original was not archived")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskCannotRunUsingProtectedImportedParents(t *testing.T) {
|
||||
for _, parentKind := range []string{"training-maps", "flows"} {
|
||||
t.Run(parentKind, func(t *testing.T) {
|
||||
s := sqliteWorkbench(t)
|
||||
s.agents = generationAgentStub{}
|
||||
ctx := workbenchContext(1, "alice", types.TenantRoleContributor)
|
||||
m := seedResource(t, s, 1, "alice", "training-maps")
|
||||
m.Status = "confirmed"
|
||||
m.Data = asJSON(MapData{Days: []Day{{Day: 1, Title: "day"}}})
|
||||
flow := seedResource(t, s, 1, "alice", "flows")
|
||||
flow.Data = asJSON(FlowData{Nodes: []FlowNode{{ID: "n1", AgentID: "author"}}})
|
||||
parent := m
|
||||
if parentKind == "flows" {
|
||||
parent = flow
|
||||
}
|
||||
parent.Data["legacy_import"] = JSON{"read_only": true, "requires_review": true}
|
||||
for _, resource := range []*Resource{m, flow} {
|
||||
if err := s.db.Save(resource).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
task := seedResource(t, s, 1, "alice", "content-tasks")
|
||||
task.Data = asJSON(TaskData{MapID: m.ID, FlowID: flow.ID, Day: 1, AgentID: "author"})
|
||||
if err := s.db.Model(task).Update("data", task.Data).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := s.submit(ctx, task.Kind, task.ID, "decompose", ActionRequest{Version: 1}); !errors.Is(err, ErrInvalid) || !strings.Contains(err.Error(), "只读历史资料") {
|
||||
t.Fatalf("protected parent entered executable task: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user