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
+190
View File
@@ -0,0 +1,190 @@
package workbench
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/Tencent/WeKnora/internal/models/vlm"
"github.com/Tencent/WeKnora/internal/types"
"github.com/Tencent/WeKnora/internal/types/interfaces"
"github.com/gin-gonic/gin"
)
func updateWorkbenchResource(s *Service, ctx context.Context, r *Resource, request resourceRequest) *httptest.ResponseRecorder {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
data, _ := json.Marshal(request)
c.Request = httptest.NewRequest(http.MethodPut, "/", strings.NewReader(string(data))).WithContext(ctx)
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)
return w
}
func TestImportedResourceRenameConfigurationActivationAndSpoofProtection(t *testing.T) {
s := sqliteWorkbench(t)
s.agents = generationAgentStub{}
ctx := workbenchContext(1, "alice", types.TenantRoleContributor)
r := seedResource(t, s, 1, "alice", "flows")
r.Status = "needs_configuration"
r.Data = JSON{"legacy_import": JSON{"read_only": true, "needs_configuration": true}, "legacy_history": JSON{"original": "preserved"}, "nodes": []FlowNode{}, "edges": []FlowEdge{}}
if err := s.db.Save(r).Error; err != nil {
t.Fatal(err)
}
original := compact(r.Data)
if w := updateWorkbenchResource(s, ctx, r, resourceRequest{Name: "renamed", Version: 1, Data: r.Data}); w.Code != 200 {
t.Fatal(w.Code, w.Body.String())
}
r, _ = s.get(ctx, r.Kind, r.ID)
if requireExecutable(r) == nil {
t.Fatal("rename activated incomplete import")
}
forged := resourceData(r, r.Data)
forged["legacy_activation"] = JSON{"status": "active"}
if w := updateWorkbenchResource(s, ctx, r, resourceRequest{Name: r.Name, Version: r.Version, Data: forged, Configure: true}); w.Code != 400 {
t.Fatal("forged activation accepted")
}
if w := updateWorkbenchResource(s, ctx, r, resourceRequest{Name: r.Name, Version: r.Version, Data: r.Data, Configure: true}); w.Code != 400 {
t.Fatal("empty canvas activated")
}
configured := resourceData(r, FlowData{Nodes: []FlowNode{{ID: "n1", AgentID: "author"}}})
if w := updateWorkbenchResource(s, ctx, r, resourceRequest{Name: r.Name, Version: r.Version, Data: configured, Configure: true}); w.Code != 200 {
t.Fatal(w.Code, w.Body.String())
}
r, _ = s.get(ctx, r.Kind, r.ID)
if err := requireExecutable(r); err != nil {
t.Fatal(err)
}
if compact(r.Data["legacy_history"]) != `{"original":"preserved"}` {
t.Fatal("history lost")
}
var revision Revision
if err := s.db.First(&revision, "resource_id = ? AND version = 1", r.ID).Error; err != nil || compact(revision.Data) != original {
t.Fatal("preedit import lost")
}
}
func TestTaskConfigurationKeepsHistoricalOutput(t *testing.T) {
d := TaskData{FlowID: "old", SourceText: "original", Segments: []Segment{{ID: "s", Content: "historical", AgentID: "a", Status: "accepted", Review: "segment review"}}, Review: "review", Composed: "final"}
r := &Resource{Kind: "content-tasks", Data: asJSON(d), Status: "completed"}
d.FlowID = "new"
if err := applyManualUpdate(r, resourceRequest{Name: "task", Data: asJSON(d)}); err != nil {
t.Fatal(err)
}
got, _ := decode[TaskData](r.Data)
if len(got.Segments) != 1 || got.Segments[0].Content != "historical" || got.Segments[0].Status != "needs_review" || got.Review != "review" || got.Composed != "final" || r.Status != "needs_review" {
t.Fatalf("output lost or stale approval retained: %+v", got)
}
}
func TestTextTaskInheritsAgentKnowledgeAndIgnoresLegacyCanvasScope(t *testing.T) {
s := sqliteWorkbench(t)
s.agents = generationAgentStub{config: types.CustomAgentConfig{KnowledgeBases: []string{"agent-kb"}, KBSelectionMode: "selected"}}
ctx := workbenchContext(1, "alice", types.TenantRoleContributor)
f := seedResource(t, s, 1, "alice", "flows")
f.Data = asJSON(FlowData{Nodes: []FlowNode{{ID: "n", AgentID: "canvas-agent"}}, KnowledgeIDs: []string{"canvas-file"}, KnowledgeFolders: map[string][]string{"kb": {"folder"}}})
if err := s.db.Save(f).Error; err != nil {
t.Fatal(err)
}
r := &Resource{Kind: "content-tasks", Name: "task", Data: asJSON(TaskData{SourceText: "manual framework", FlowID: f.ID, AgentID: "injected", KBIDs: []string{"injected-kb"}})}
if err := s.validate(ctx, r); err != nil {
t.Fatal(err)
}
d, _ := decode[TaskData](r.Data)
if d.AgentID != "canvas-agent" || len(d.KBIDs) != 1 || d.KBIDs[0] != "agent-kb" || len(d.KnowledgeIDs) != 0 || len(d.KnowledgeFolders) != 0 || len(d.KnowledgeTags) != 0 {
t.Fatalf("task did not inherit: %+v", d)
}
if err := s.inheritFlow(ctx, &d, FlowData{Nodes: []FlowNode{{ID: "n", AgentID: "canvas-agent"}}}); err != nil || len(d.KBIDs) != 1 || d.KBIDs[0] != "agent-kb" {
t.Fatalf("agent scope not inherited: %+v %v", d, err)
}
}
func TestAIRecommendsTrainingDays(t *testing.T) {
s, ctx, job, model := generationHarness(t)
job.Action = "generate"
model.output = `{"days":[{"day":1,"title":"first","hook":"next","nodes":[{"id":"n1","label":"foundation"}]},{"day":2,"title":"second","prerequisite":"first","hook":"apply","nodes":[{"id":"n2","label":"practice"}]}]}`
r := &Resource{Kind: "training-maps", Data: asJSON(MapData{Topic: "topic", AgentID: "author"})}
if err := s.executeMap(ctx, job, r, ActionRequest{}); err != nil {
t.Fatal(err)
}
d, _ := decode[MapData](r.Data)
if d.DaysCount != 2 || !d.DaysAuto || !strings.Contains(model.messages[1].Content, "推荐合理天数") {
t.Fatal("AI day recommendation not persisted")
}
if validateDays(nil, 0) == nil {
t.Fatal("zero recommended days accepted")
}
}
func TestNodeRevisionOnlyReplacesRequestedSubtree(t *testing.T) {
s, ctx, job, model := generationHarness(t)
job.Action = "revise"
model.output = `{"days":[{"day":1,"title":"unrequested change","hook":"unrequested","nodes":[{"id":"a","label":"changed","children":[{"id":"a1","label":"child"}]},{"id":"b","label":"unrequested sibling change"}]}]}`
r := &Resource{Data: asJSON(MapData{AgentID: "author", DaysCount: 1, Days: []Day{{Day: 1, Title: "original title", Hook: "original hook", Nodes: []Node{{ID: "a", Label: "before"}, {ID: "b", Label: "sibling"}}}}})}
if err := s.executeMap(ctx, job, r, ActionRequest{Day: 1, NodeID: "a", Advice: "fix"}); err != nil {
t.Fatal(err)
}
d, _ := decode[MapData](r.Data)
if d.Days[0].Title != "original title" || d.Days[0].Hook != "original hook" || d.Days[0].Nodes[1].Label != "sibling" || d.Days[0].Nodes[0].Label != "changed" {
t.Fatalf("unrelated content changed: %+v", d.Days[0])
}
if err := s.executeMap(ctx, job, r, ActionRequest{Day: 1, NodeID: "missing", Advice: "fix"}); !errors.Is(err, ErrInvalid) {
t.Fatal("unknown node accepted")
}
}
type imageModelStub struct {
interfaces.ModelService
model vlm.VLM
id string
}
func (s *imageModelStub) GetVLMModel(_ context.Context, id string) (vlm.VLM, error) {
s.id = id
return s.model, nil
}
func (s *imageModelStub) ListModels(context.Context) ([]*types.Model, error) {
return []*types.Model{{ID: "workspace-vision", Type: types.ModelTypeVLLM, Status: types.ModelStatusActive}}, nil
}
type imageVLMStub struct {
vlm.VLM
seen bool
}
func (v *imageVLMStub) Predict(_ context.Context, data [][]byte, prompt string) (string, error) {
v.seen = len(data) == 1 && strings.Contains(prompt, "节点")
return "# recognized framework", nil
}
func TestImageOCRUsesAgentModelAndWorkspaceFallback(t *testing.T) {
image := "data:image/png;base64," + base64.StdEncoding.EncodeToString([]byte("\x89PNG\r\n\x1a\nimage"))
for _, configured := range []string{"agent-vision", ""} {
s := NewService(nil)
v := &imageVLMStub{}
m := &imageModelStub{model: v}
s.models, s.agents = m, generationAgentStub{config: types.CustomAgentConfig{VLMModelID: configured}}
text, err := s.recognizeSourceImage(workbenchContext(1, "alice", types.TenantRoleContributor), "agent", image)
if err != nil || text != "# recognized framework" || !v.seen {
t.Fatal(text, err)
}
want := configured
if want == "" {
want = "workspace-vision"
}
if m.id != want {
t.Fatal("wrong VLM", m.id)
}
}
for _, invalid := range []string{"https://external/image.png", "data:image/png;base64,not-base64", "data:image/png;base64," + base64.StdEncoding.EncodeToString([]byte("text"))} {
if _, err := decodeSourceImage(invalid); err == nil {
t.Fatal("invalid image accepted")
}
}
}