Files
Agent-System-Backend/internal/app/ext9_publish_test.go
T
weichen 1ee32822c3 Add the EXT-9 publish/revoke surface and red acceptance tests
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.
2026-10-08 15:53:39 +00:00

82 lines
2.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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))
}
}