Files
Agent-System-Backend/internal/surfacesvc/surfacesvc_test.go
T
weichen f1302ed5b3 Enable canonical versioned Go module consumption
Why: Allow consumers to download an immutable module revision without nested source checkouts or local path replacements.

Evidence: SELF package tests and the prior canonical-path rejection are recorded in the Dev dependency-workspace task card; remote consumption is replayed against the committed candidate.

Impact: Change the module identity and its Go imports while preserving API behavior. Existing historical submodule revisions remain buildable.

Refs: Versioned dependencies U1/U3; feature/dependency-workspace
2026-10-13 15:47:16 +00:00

149 lines
5.4 KiB
Go

package surfacesvc
import (
"context"
"encoding/json"
"errors"
"path/filepath"
"testing"
"git-ssh.u1680832.nyat.app/weichen/Agent-System-Backend.git/internal/store"
core "git-ssh.u1680832.nyat.app/weichen/Agent-System-Core.git"
)
func openSvcStore(t *testing.T) *store.Store {
t.Helper()
st, err := store.OpenSQLite(filepath.Join(t.TempDir(), "test.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { st.Close() })
return st
}
func validSpecJSON() json.RawMessage {
return json.RawMessage(`{"title":"选型对比","nodes":[
{"type":"section","id":"n1","title":"概览","children":[
{"type":"markdown","id":"n1a","source":["doc:a"],"markdown":"结论先行。"}]},
{"type":"table","id":"n2","source":["doc:a","doc:b"],"table":{
"columns":[{"key":"k","label":"维度"}],
"rows":[{"cells":[["k","价格"]]}]}}]}`)
}
func newSvc(t *testing.T) (*Service, *[]core.StreamEventType) {
st := openSvcStore(t)
var events []core.StreamEventType
return New(st, func(conversationID, runID string, typ core.StreamEventType, payload any) error {
events = append(events, typ)
return nil
}), &events
}
func TestSubmitGuards(t *testing.T) {
ctx := context.Background()
svc, _ := newSvc(t)
// 默认关闭:新插件无 activation 行 → 拒绝
if _, err := svc.Submit(ctx, "alice", "genui", SubmitRequest{Spec: validSpecJSON()}); !errors.Is(err, ErrDisabled) {
t.Fatalf("default-disabled should reject, got %v", err)
}
// 启用后提交成功
if _, err := svc.SetActivation(ctx, "alice", "genui", true); err != nil {
t.Fatal(err)
}
snap, err := svc.Submit(ctx, "alice", "genui", SubmitRequest{ConversationID: "c1", Spec: validSpecJSON()})
if err != nil || snap.Revision != 1 || snap.Identity.Owner != "alice" || snap.Identity.PluginID != "genui" {
t.Fatalf("submit: %+v err=%v", snap, err)
}
// epoch 失配:观察值与当前不一致(禁用/重启用后的在途结果)→ 拒绝
if _, err := svc.Submit(ctx, "alice", "genui", SubmitRequest{
SurfaceID: snap.Identity.SurfaceID, BaseRevision: 1, ObservedEpoch: 99, Spec: validSpecJSON(),
}); !errors.Is(err, ErrStaleEpoch) {
t.Fatalf("stale epoch should reject, got %v", err)
}
// 规格违规:未知类型 + 字段级清单
bad := json.RawMessage(`{"title":"x","nodes":[{"type":"hologram"}]}`)
_, err = svc.Submit(ctx, "alice", "genui", SubmitRequest{Spec: bad})
var inv *ErrInvalidSpec
if !errors.As(err, &inv) || len(inv.Errs) == 0 {
t.Fatalf("invalid spec should carry field errors, got %v", err)
}
// 严格解码:未知字段拒绝(目录纪律)
extra := json.RawMessage(`{"title":"x","nodes":[{"type":"list","items":["a"],"color":"red"}]}`)
if _, err := svc.Submit(ctx, "alice", "genui", SubmitRequest{Spec: extra}); err == nil {
t.Fatal("unknown field should be rejected")
}
// 禁用后:提交拒绝、读取保留(KTD7 历史页面不丢)
if _, err := svc.SetActivation(ctx, "alice", "genui", false); err != nil {
t.Fatal(err)
}
if _, err := svc.Submit(ctx, "alice", "genui", SubmitRequest{Spec: validSpecJSON()}); !errors.Is(err, ErrDisabled) {
t.Fatalf("disabled submit should reject, got %v", err)
}
if _, err := svc.Get(ctx, "alice", snap.Identity.SurfaceID); err != nil {
t.Fatalf("disabled should not block reads, got %v", err)
}
// owner 隔离
if _, err := svc.Get(ctx, "bob", snap.Identity.SurfaceID); !errors.Is(err, ErrNotFound) {
t.Fatalf("cross-owner get should be NotFound, got %v", err)
}
}
func TestSubmitConflictSurfacesCurrent(t *testing.T) {
ctx := context.Background()
svc, _ := newSvc(t)
svc.SetActivation(ctx, "alice", "genui", true)
s1, err := svc.Submit(ctx, "alice", "genui", SubmitRequest{ConversationID: "c1", Spec: validSpecJSON()})
if err != nil {
t.Fatal(err)
}
s2, err := svc.Submit(ctx, "alice", "genui", SubmitRequest{
SurfaceID: s1.Identity.SurfaceID, BaseRevision: 1, ConversationID: "c1", Spec: validSpecJSON(),
})
if err != nil || s2.Revision != 2 {
t.Fatalf("concurrent second writer: %+v err=%v", s2, err)
}
// 用过期 base 的第三方写入 → 冲突
_, err = svc.Submit(ctx, "alice", "genui", SubmitRequest{
SurfaceID: s1.Identity.SurfaceID, BaseRevision: 1, ConversationID: "c1", Spec: validSpecJSON(),
})
var conflict *store.ErrRevisionConflict
if !errors.As(err, &conflict) || conflict.Current != 2 {
t.Fatalf("conflict should surface current=2, got %v", err)
}
}
func TestEventPublishFailureRecovery(t *testing.T) {
ctx := context.Background()
st := openSvcStore(t)
fail := true
var emitted int
svc := New(st, func(conversationID, runID string, typ core.StreamEventType, payload any) error {
if fail {
return errors.New("bus down")
}
emitted++
return nil
})
svc.SetActivation(ctx, "alice", "genui", true)
// 快照保存成功、事件发布失败 → 待恢复标记
snap, err := svc.Submit(ctx, "alice", "genui", SubmitRequest{ConversationID: "c1", Spec: validSpecJSON()})
if err != nil {
t.Fatalf("snapshot must survive event failure, got %v", err)
}
pend, _ := st.ListSurfaceEventsPending(10)
if len(pend) != 1 {
t.Fatalf("event should be pending, got %d", len(pend))
}
// 恢复:事件通道恢复后重发并清除
fail = false
n, err := svc.RecoverPendingEvents(ctx)
if err != nil || n != 1 || emitted != 1 {
t.Fatalf("recovery: n=%d emitted=%d err=%v", n, emitted, err)
}
if pend, _ := st.ListSurfaceEventsPending(10); len(pend) != 0 {
t.Fatalf("pending should clear, got %d", len(pend))
}
_ = snap
}