Why: EXT-9 is the other step-4 hard prerequisite; the surface (Options.DisableUploadIndexing passthrough, Host.KnowledgePublish idempotent replace, Host.KnowledgeDelete prefix-enforced revoke, SurfaceVersion 2.2.0-experimental) must land with the acceptance reds pinned before implementation. Evidence: app red = publish not implemented; server red = switch-on still indexes (hits=1); rows recorded on the task card. Impact: pkg/extension additive surface (surface.txt updated); internal Options passthrough; Server.DisableUploadIndexing field; host method stubs. Behavior unchanged. Refs: EXT-9 proposal; task card.
82 lines
2.8 KiB
Go
82 lines
2.8 KiB
Go
package app
|
||
|
||
import (
|
||
"context"
|
||
"path/filepath"
|
||
"testing"
|
||
|
||
"agent-system-backend/internal/rag"
|
||
"agent-system-backend/internal/settings"
|
||
"agent-system-backend/internal/store"
|
||
core "agent-system-core"
|
||
)
|
||
|
||
// newExt9Host EXT-9 测试装配:真实 store + rag 管线 + inprocHost(无 embedder,
|
||
// embed=false 走全文档路径)。
|
||
func newExt9Host(t *testing.T) (interface {
|
||
KnowledgePublish(ctx context.Context, docs []core.KnowledgeDoc, embed bool) error
|
||
KnowledgeDelete(ctx context.Context, sourcePrefix string) error
|
||
}, *store.Store, *rag.Pipeline) {
|
||
t.Helper()
|
||
st, err := store.OpenSQLite(filepath.Join(t.TempDir(), "test.db"))
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
t.Cleanup(func() { st.Close() })
|
||
set := settings.NewService(st)
|
||
pipe := rag.NewPipeline(st, set, nil, nil)
|
||
h := inprocHost{caps: hostcaps{st: st, pipe: pipe, set: set}, pluginID: "rdm", owner: "alice"}
|
||
return h, st, pipe
|
||
}
|
||
|
||
func searchAll(t *testing.T, pipe *rag.Pipeline, owner, text string) []core.RetrievalHit {
|
||
t.Helper()
|
||
hits, err := pipe.Retrieve(context.Background(), core.ID(owner), core.RetrievalQuery{Text: text, TopK: 50})
|
||
if err != nil {
|
||
t.Fatalf("retrieve: %v", err)
|
||
}
|
||
return hits
|
||
}
|
||
|
||
// EXT-9 验收:publish 按 ref 幂等(重试不重复);delete 后检索即停且幂等;
|
||
// manual 来源不经 host 可删(前缀强制 plugin:<自身>)。
|
||
func TestKnowledgePublishIdempotentAndRevoke(t *testing.T) {
|
||
h, st, pipe := newExt9Host(t)
|
||
ctx := context.Background()
|
||
docs := []core.KnowledgeDoc{
|
||
{Ref: "batch-001", Chunk: "EXT9甲 独占令牌之一陈述"},
|
||
{Ref: "batch-001", Chunk: "EXT9乙 独占令牌之二陈述"},
|
||
}
|
||
if err := h.KnowledgePublish(ctx, docs, false); err != nil {
|
||
t.Fatalf("publish#1: %v", err)
|
||
}
|
||
if err := h.KnowledgePublish(ctx, docs, false); err != nil { // 重试幂等
|
||
t.Fatalf("publish#2: %v", err)
|
||
}
|
||
hits := searchAll(t, pipe, "alice", "独占令牌")
|
||
if len(hits) != 2 {
|
||
t.Fatalf("after double publish: hits=%d, want 2(幂等不重复;got %+v)", len(hits), hits)
|
||
}
|
||
|
||
if err := h.KnowledgeDelete(ctx, "plugin:rdm"); err != nil {
|
||
t.Fatalf("delete: %v", err)
|
||
}
|
||
if hits = searchAll(t, pipe, "alice", "独占令牌"); len(hits) != 0 {
|
||
t.Fatalf("after delete: hits=%d, want 0(撤销即停)", len(hits))
|
||
}
|
||
if err := h.KnowledgeDelete(ctx, "plugin:rdm"); err != nil { // 幂等
|
||
t.Fatalf("delete#2: %v", err)
|
||
}
|
||
|
||
// manual 来源:host 前缀强制拒绝,且内容不受插件删除影响。
|
||
if err := st.AddKnowledge(core.KnowledgeDoc{Owner: "alice", Source: "manual", Ref: "m-1", Chunk: "manual 手册独占词"}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := h.KnowledgeDelete(ctx, "manual"); err == nil {
|
||
t.Fatal("delete(manual) must be rejected: plugins may only revoke their own prefix")
|
||
}
|
||
if hits = searchAll(t, pipe, "alice", "手册独占词"); len(hits) != 1 {
|
||
t.Fatalf("manual row must survive: hits=%d", len(hits))
|
||
}
|
||
}
|