Files
xinmi/internal/workbench/legacy_test.go
T
yawan f7fc992f57
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
WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
2026-09-22 16:19:06 +08:00

233 lines
10 KiB
Go

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)
}
})
}
}