Files
story/internal/workflowbridge/service_test.go
T

163 lines
5.6 KiB
Go
Raw Normal View History

package workflowbridge
import (
"bytes"
"context"
"data-terminal-cloud/internal/dify"
"data-terminal-cloud/internal/extraction"
"data-terminal-cloud/internal/memoir"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"sync"
"sync/atomic"
"testing"
)
type fixtureEditor struct {
calls atomic.Int32
input string
mu sync.Mutex
}
func (e *fixtureEditor) GenerateJSON(_ context.Context, input, prompt string) (json.RawMessage, error) {
e.calls.Add(1)
raw, _ := json.Marshal(extraction.GenerationResult{Meaningful: true, StoryWorthy: true, DecisionReason: "有原话支持的历史事件", Summary: "进厂往事", Keywords: []string{"进厂"}, Confidence: .9, Stories: []extraction.EventStory{{Title: "进厂", Content: "1970年进厂。", FactContent: "1970年进厂", EventTime: "1970年", SourceIDs: []string{"turn-1"}}}})
var out map[string]any
_ = json.Unmarshal(raw, &out)
out["daily_events"] = []any{}
return json.Marshal(out)
}
func (e *fixtureEditor) GenerateJSONSchema(_ context.Context, input, prompt string, schema json.RawMessage) (json.RawMessage, error) {
e.calls.Add(1)
e.mu.Lock()
e.input = input
e.mu.Unlock()
var in struct {
Stories []memoir.Source `json:"stories"`
}
_ = json.Unmarshal([]byte(input), &in)
d := memoir.Document{Title: "测试回忆录"}
for _, s := range in.Stories {
d.Chapters = append(d.Chapters, memoir.Chapter{StoryID: s.ID, EventTime: s.EventTime, Title: s.Title, Paragraphs: []string{s.Content}, IllustrationPrompt: "水彩情景插画"})
}
return json.Marshal(d)
}
type overdueImages struct{}
func (overdueImages) Generate(context.Context, string) ([]byte, error) {
return nil, extraction.ErrModelAccountOverdue
}
func postStep(t *testing.T, base, step string, ticket dify.Ticket, index int) map[string]any {
t.Helper()
raw, _ := json.Marshal(stepInput{ID: ticket.ID, Token: ticket.Token, Index: index})
r, err := http.Post(base+"/steps/"+step, "application/json", bytes.NewReader(raw))
if err != nil {
t.Fatal(err)
}
defer r.Body.Close()
var out map[string]any
if json.NewDecoder(r.Body).Decode(&out) != nil {
t.Fatal("invalid step response")
}
return out
}
func TestStepsAreCachedAndNeverExposePrivateContent(t *testing.T) {
editor := &fixtureEditor{}
s := New("backend-secret", editor, editor, overdueImages{})
srv := httptest.NewServer(s)
defer srv.Close()
b := dify.NewBridge(srv.URL, "backend-secret")
ticket, err := b.Register(context.Background(), "extract", extraction.GenerationInput{Content: "[turn-1] 长者:1970年进厂。私密原话标记", PreviousStories: []extraction.PreviousStory{}})
if err != nil {
t.Fatal(err)
}
defer b.Forget(ticket)
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
out := postStep(t, srv.URL, "classify", ticket, 0)
raw, _ := json.Marshal(out)
if out["success"] != true || strings.Contains(string(raw), "私密原话") || strings.Contains(string(raw), "1970") {
t.Errorf("private content in metadata %s", raw)
}
}()
}
wg.Wait()
if editor.calls.Load() != 1 {
t.Fatalf("duplicate model calls %d", editor.calls.Load())
}
req, _ := http.NewRequest("GET", srv.URL+"/contexts/"+ticket.ID, nil)
req.Header.Set("Authorization", "Bearer "+ticket.Token)
resp, _ := http.DefaultClient.Do(req)
resp.Body.Close()
if resp.StatusCode != 403 {
t.Fatal("step token could read private results")
}
b.Forget(ticket)
if s.find(ticket.ID) != nil {
t.Fatal("deleted context survives")
}
}
func TestWholeCatalogAndBillingDuringImagesKeepText(t *testing.T) {
editor := &fixtureEditor{}
s := New("backend-secret", editor, editor, overdueImages{})
srv := httptest.NewServer(s)
defer srv.Close()
b := dify.NewBridge(srv.URL, "backend-secret")
sources := []memoir.Source{{ID: "one", Title: "进厂", Content: "1970年进厂。", EventTime: "1970年"}, {ID: "two", Title: "学徒", Content: "1971年学做手套。", EventTime: "1971年"}}
ticket, err := b.Register(context.Background(), "memoir", map[string]any{"stories": sources})
if err != nil {
t.Fatal(err)
}
defer b.Forget(ticket)
if postStep(t, srv.URL, "write-book", ticket, 0)["success"] != true {
t.Fatal("whole book failed")
}
if postStep(t, srv.URL, "validate-book", ticket, 0)["success"] != true {
t.Fatal("validation failed")
}
for i := range sources {
if postStep(t, srv.URL, "illustrate", ticket, i)["error_code"] != "MODEL_ACCOUNT_OVERDUE" {
t.Fatal("billing error not identified")
}
}
if postStep(t, srv.URL, "finish", ticket, 0)["success"] != true {
t.Fatal("image billing failure discarded text")
}
var result struct {
Document memoir.Document `json:"document"`
}
if b.Result(context.Background(), ticket, &result) != nil || len(result.Document.Chapters) != 2 || result.Document.IllustrationErrors != 2 {
t.Fatal("whole text unavailable")
}
if editor.calls.Load() != 1 || !strings.Contains(editor.input, "one") || !strings.Contains(editor.input, "two") {
t.Fatal("model did not receive whole catalog once")
}
}
func TestPrivateResultPreservesEmptyEventArrays(t *testing.T) {
s := New("backend-secret", nil, nil, nil)
srv := httptest.NewServer(s)
defer srv.Close()
b := dify.NewBridge(srv.URL, "backend-secret")
ticket, err := b.Register(context.Background(), "extract", extraction.GenerationInput{Content: "今天吃早饭。"})
if err != nil {
t.Fatal(err)
}
defer b.Forget(ticket)
var out struct {
Result extraction.GenerationResult `json:"result"`
}
if err = b.Result(context.Background(), ticket, &out); err != nil || out.Result.Stories == nil || out.Result.DailyEvents == nil {
t.Fatalf("empty arrays lost in private transport: %v", err)
}
}