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
171 lines
5.7 KiB
Go
171 lines
5.7 KiB
Go
package runtime
|
|
|
|
import (
|
|
"context"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
agentllm "git-ssh.u1680832.nyat.app/weichen/Agent-Native.git/runtime/llm"
|
|
agentsession "git-ssh.u1680832.nyat.app/weichen/Agent-Native.git/runtime/session"
|
|
core "git-ssh.u1680832.nyat.app/weichen/Agent-System-Core.git"
|
|
)
|
|
|
|
// ---- N3 WP2:产品会话路径持久化(验收① 产品层机器证据)----
|
|
|
|
func textTurns(texts ...string) [][]agentllm.Event {
|
|
out := make([][]agentllm.Event, 0, len(texts))
|
|
for _, t := range texts {
|
|
out = append(out, []agentllm.Event{
|
|
{Kind: agentllm.KindTextDelta, Text: t},
|
|
{Kind: agentllm.KindMessageEnd, Message: agentllm.Message{Role: agentllm.RoleAssistant, Content: t, StopReason: agentllm.StopEnd}},
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// 杀窗口形态的种子转录:新 run 前配对修复成立,且 run 正常产出。
|
|
func TestDirectRunPairsAfterKillSeed(t *testing.T) {
|
|
sandbox := t.TempDir()
|
|
sess, err := agentsession.Open(filepath.Join(sandbox, ".agentx", "transcript.jsonl"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, m := range []agentllm.Message{
|
|
{Role: agentllm.RoleSystem, Content: "sys"},
|
|
{Role: agentllm.RoleUser, Content: "早前"},
|
|
{Role: agentllm.RoleAssistant, StopReason: agentllm.StopToolCalls, ToolCalls: []agentllm.ToolCall{{ID: "c1", Name: "run_python", Arguments: `{}`}}},
|
|
} {
|
|
if err := sess.Append(m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
rc := &fakeRunCtx{
|
|
ctx: context.Background(), sandbox: sandbox,
|
|
prof: core.AgentProfile{Model: core.ModelRoute{Chat: "test-model"}},
|
|
history: []core.Message{
|
|
{ID: "m1", Role: core.RoleUser, Parts: []core.Part{{Type: core.PartText, Text: "还在吗"}}},
|
|
},
|
|
}
|
|
r := &LLMRunner{Stream: scriptStream(textTurns("还在"), nil)}
|
|
if err := r.Execute(rc); err != nil {
|
|
t.Fatalf("Execute: %v", err)
|
|
}
|
|
loaded, err := sess.Load()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var paired bool
|
|
for _, m := range loaded {
|
|
if m.Role == agentllm.RoleTool && m.ToolCallID == "c1" {
|
|
paired = true
|
|
if !m.IsError || !strings.Contains(m.Content, "进程中断") {
|
|
t.Fatalf("兜底配对形态不对:%+v", m)
|
|
}
|
|
}
|
|
}
|
|
if !paired {
|
|
t.Fatalf("杀窗口调用未配对:%#v", loaded)
|
|
}
|
|
}
|
|
|
|
// 转录权威语义(质量 P2-I2):首次播种把组装历史持久进转录且恰好一次;
|
|
// 后续 run 由转录承载,不重复播种。
|
|
func TestDirectRunSeedsStoreHistoryOnce(t *testing.T) {
|
|
sandbox := t.TempDir()
|
|
rc := &fakeRunCtx{
|
|
ctx: context.Background(), sandbox: sandbox,
|
|
prof: core.AgentProfile{Model: core.ModelRoute{Chat: "test-model"}},
|
|
history: []core.Message{
|
|
{ID: "m1", Role: core.RoleUser, Parts: []core.Part{{Type: core.PartText, Text: "历史一"}}},
|
|
{ID: "m2", Role: core.RoleAssistant, Parts: []core.Part{{Type: core.PartText, Text: "回复一"}}},
|
|
{ID: "m3", Role: core.RoleUser, Parts: []core.Part{{Type: core.PartText, Text: "最新"}}},
|
|
},
|
|
}
|
|
var requests [][]agentllm.Message
|
|
base := scriptStream(textTurns("一答", "二答"), nil)
|
|
r := &LLMRunner{Stream: func(ctx context.Context, model string, msgs []agentllm.Message, tools []agentllm.ToolDecl, opts agentllm.StreamOptions) <-chan agentllm.Event {
|
|
requests = append(requests, append([]agentllm.Message{}, msgs...))
|
|
return base(ctx, model, msgs, tools, opts)
|
|
}}
|
|
if err := r.Execute(rc); err != nil {
|
|
t.Fatalf("first Execute: %v", err)
|
|
}
|
|
// 第二跑:同一 rc(新 user 消息由 history 尾部提供)。
|
|
rc.history = append(rc.history, core.Message{ID: "m4", Role: core.RoleUser, Parts: []core.Part{{Type: core.PartText, Text: "再来"}}})
|
|
if err := r.Execute(rc); err != nil {
|
|
t.Fatalf("second Execute: %v", err)
|
|
}
|
|
if len(requests) < 2 {
|
|
t.Fatalf("stream 调用数 = %d", len(requests))
|
|
}
|
|
for i, req := range requests {
|
|
count := 0
|
|
for _, m := range req {
|
|
if m.Content == "历史一" {
|
|
count++
|
|
}
|
|
}
|
|
if count != 1 {
|
|
t.Fatalf("第 %d 跑历史中「历史一」必须恰好一次,得到 %d", i+1, count)
|
|
}
|
|
}
|
|
sess, err := agentsession.Open(filepath.Join(sandbox, ".agentx", "transcript.jsonl"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
loaded, err := sess.Load()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
count := 0
|
|
for _, m := range loaded {
|
|
if m.Content == "历史一" {
|
|
count++
|
|
}
|
|
}
|
|
if count != 1 {
|
|
t.Fatalf("转录应恰好一次包含种子历史「历史一」,得到 %d:%#v", count, loaded)
|
|
}
|
|
}
|
|
|
|
// ---- N3 WP5:记忆端口迁移对照(注入文案同一形态)----
|
|
|
|
// 端口注入的 [记忆] 块到达请求(迁移前经组装侧 InjectMemory 同文案形态)。
|
|
func TestDirectRunInjectsMemoryBlock(t *testing.T) {
|
|
var gotReq []agentllm.Message
|
|
base := scriptStream(textTurns("收到"), nil)
|
|
rc := &fakeRunCtx{
|
|
ctx: context.Background(),
|
|
prof: core.AgentProfile{Model: core.ModelRoute{Chat: "test-model"}},
|
|
memEntries: []memEntry{
|
|
{typ: "fact", content: "项目用 Go 1.27"},
|
|
{typ: "preference", content: "喜欢简洁回复"},
|
|
},
|
|
}
|
|
r := &LLMRunner{Stream: func(ctx context.Context, model string, msgs []agentllm.Message, tools []agentllm.ToolDecl, opts agentllm.StreamOptions) <-chan agentllm.Event {
|
|
gotReq = append([]agentllm.Message{}, msgs...)
|
|
return base(ctx, model, msgs, tools, opts)
|
|
}}
|
|
if err := r.Execute(rc); err != nil {
|
|
t.Fatalf("Execute: %v", err)
|
|
}
|
|
var block string
|
|
for _, m := range gotReq {
|
|
if strings.Contains(m.Content, "[记忆]") {
|
|
block = m.Content
|
|
}
|
|
}
|
|
if block == "" {
|
|
t.Fatalf("记忆块缺失:%#v", gotReq)
|
|
}
|
|
// 形态对照(与 ctxasm.InjectMemory 同文):[记忆]\n- (typ) content,确定性排序。
|
|
if !strings.Contains(block, "- (fact) 项目用 Go 1.27") ||
|
|
!strings.Contains(block, "- (preference) 喜欢简洁回复") {
|
|
t.Fatalf("记忆块形态不对:%q", block)
|
|
}
|
|
if strings.Index(block, "(fact)") > strings.Index(block, "(preference)") {
|
|
t.Fatalf("排序不确定:%q", block)
|
|
}
|
|
}
|