Files
weichen bdb45c3c2c
ci / vet-and-test (pull_request) Successful in 49s
ci / docker-checkout-hostgateway (pull_request) Successful in 1s
Add the memory port with loop recall injection and the memory_write tool (N3 WP5)
Why: the completion doc assigns the Memory port to the library; today
memory recall is a product-side interface assertion with no library
surface, AgentSpec.MemoryScope has no consumer, and the model has no
write-back path.
Evidence: GOWORK=off go vet + go test -count=1 ./... green;
TestLoopInjectsMemoryAndRegistersWrite (recall block lands after the
system section, memory_write executes through the loop),
TestEmptyRecallInjectsNothing (empty recall changes nothing),
TestBlockRenderOrderingAndCaps (deterministic order, expiry/empty
pruning), TestWriteToolValidation (fail-closed scope/type/content).
Impact: new opt-in Config.Memory/MemoryScope; nil port keeps behavior
byte-identical; injection is request-scoped (never persisted, never
accumulates) and deterministic ([记忆] text shape preserved for the
product migration).
Refs: Agent-System-Documents#43 (N3 plan WP5)
2026-10-07 16:05:36 +00:00

92 lines
2.8 KiB
Go
Raw Permalink 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 memory
import (
"context"
"encoding/json"
"strings"
"testing"
"time"
)
// ---- N3 WP5:记忆端口 ----
type fakeStore struct {
entries []Entry
appended []Entry
recallQ RecallQuery
err error
}
func (f *fakeStore) Recall(_ context.Context, q RecallQuery) ([]Entry, error) {
f.recallQ = q
return f.entries, f.err
}
func (f *fakeStore) Append(_ context.Context, e Entry) error {
f.appended = append(f.appended, e)
return nil
}
func TestBlockRenderOrderingAndCaps(t *testing.T) {
future := time.Now().Add(24 * time.Hour)
past := time.Now().Add(-24 * time.Hour)
entries := []Entry{
{Type: "preference", Content: "喜欢简洁回复"},
{Type: "fact", Content: "项目用 Go 1.27"},
{Type: "fact", Content: " "},
{Type: "fact", Content: "过期了", ExpiresAt: &past},
{Type: "fact", Content: "没过期", ExpiresAt: &future},
}
out := Block(entries, time.Now())
if !strings.Contains(out, "[记忆]\n") {
t.Fatalf("缺块头:%q", out)
}
// 确定性排序:Type 字典序(fact 先于 preference);过期与空内容剔除。
firstFact := strings.Index(out, "fact")
firstPref := strings.Index(out, "preference")
if firstFact < 0 || firstPref < 0 || firstFact > firstPref {
t.Fatalf("排序不确定:%q", out)
}
if strings.Contains(out, "过期了") {
t.Fatalf("过期条目未剔除:%q", out)
}
if !strings.Contains(out, "没过期") {
t.Fatalf("未过期条目被误剔:%q", out)
}
if Block(nil, time.Now()) != "" {
t.Fatal("空结果应为空串")
}
}
func TestWriteToolValidation(t *testing.T) {
store := &fakeStore{}
tool := &writeTool{store: store}
cases := []struct {
name string
args string
wantErr string
}{
{"坏 scope", `{"scope":"global","type":"fact","content":"x"}`, "user 或 project"},
{"坏 type", `{"type":"todo","content":"x"}`, "fact|preference"},
{"空 content", `{"type":"fact","content":" "}`, "非空"},
{"超长 content", `{"type":"fact","content":"` + strings.Repeat("长", 2001) + `"}`, "上限"},
}
for _, tc := range cases {
res, err := tool.Execute(context.Background(), json.RawMessage(tc.args))
if err != nil || !res.IsError || !strings.Contains(res.Content, tc.wantErr) {
t.Fatalf("%s:%+v %v", tc.name, res, err)
}
}
if len(store.appended) != 0 {
t.Fatalf("校验失败不得落库:%+v", store.appended)
}
// 合法路径:默认 scope=user + 过期换算。
res, err := tool.Execute(context.Background(), json.RawMessage(`{"type":"preference","content":"喜欢简洁","expires_in_days":30}`))
if err != nil || res.IsError {
t.Fatalf("合法写入被拒:%+v %v", res, err)
}
if len(store.appended) != 1 || store.appended[0].Scope != "user" ||
store.appended[0].Type != "preference" || store.appended[0].ExpiresAt == nil {
t.Fatalf("写入形态:%+v", store.appended)
}
}