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
180 lines
6.8 KiB
Go
180 lines
6.8 KiB
Go
package ctxasm
|
||
|
||
import (
|
||
"strings"
|
||
"testing"
|
||
|
||
agentllm "git-ssh.u1680832.nyat.app/weichen/Agent-Native.git/runtime/llm"
|
||
core "git-ssh.u1680832.nyat.app/weichen/Agent-System-Core.git"
|
||
)
|
||
|
||
func TestVisibleHistoryDropsExcludedAndUnselectedVariants(t *testing.T) {
|
||
hist := []core.Message{
|
||
{ID: "u", Role: core.RoleUser, Parts: []core.Part{{Type: core.PartText, Text: "问"}}},
|
||
{ID: "a1", Role: core.RoleAssistant, VariantGroup: "g", VariantIndex: 0, Excluded: true, Parts: []core.Part{{Type: core.PartText, Text: "旧"}}},
|
||
{ID: "a2", Role: core.RoleAssistant, VariantGroup: "g", VariantIndex: 1, Parts: []core.Part{{Type: core.PartText, Text: "新"}}},
|
||
}
|
||
msgs, _ := AssembleInput(core.AgentProfile{}, core.EffectiveSettings{}, hist, core.Message{ID: "cur", Role: core.RoleUser}, nil, nil, nil, nil, nil, nil, AssembleOptions{})
|
||
blob := ""
|
||
for _, m := range msgs {
|
||
blob += m.Content
|
||
}
|
||
if strings.Contains(blob, "旧") || !strings.Contains(blob, "新") {
|
||
t.Fatalf("history = %q", blob)
|
||
}
|
||
}
|
||
|
||
func TestFinishAssemblyAttachesNoteWithoutTouchingSystem(t *testing.T) {
|
||
msgs := []agentllm.Message{
|
||
{Role: agentllm.RoleSystem, Content: "前缀"},
|
||
{Role: agentllm.RoleUser, Content: "你好"},
|
||
}
|
||
out, rep := FinishAssembly(msgs, &Report{Items: []string{"system"}}, Tail{
|
||
Note: "用短句",
|
||
Entries: []Injection{{
|
||
Keys: []string{"你好"}, Content: "提到问候时补充天气", ScanDepth: 2, BudgetChars: 100,
|
||
}},
|
||
History: []core.Message{{Role: core.RoleUser, Parts: []core.Part{{Type: core.PartText, Text: "你好"}}}},
|
||
Window: 32000,
|
||
})
|
||
if out[0].Content != "前缀" {
|
||
t.Fatal("system prefix changed")
|
||
}
|
||
if !strings.Contains(out[len(out)-1].Content, "本会话要求") || !strings.Contains(out[len(out)-1].Content, "天气") {
|
||
t.Fatalf("tail = %q", out[len(out)-1].Content)
|
||
}
|
||
if rep.PrefixHash == "" || rep.EstChars <= 0 || len(rep.Sections) != 3 {
|
||
t.Fatalf("report = %#v", rep)
|
||
}
|
||
}
|
||
|
||
func TestTrimDropsOldestHistory(t *testing.T) {
|
||
msgs := []agentllm.Message{
|
||
{Role: agentllm.RoleSystem, Content: "s"},
|
||
{Role: agentllm.RoleUser, Content: strings.Repeat("旧", 400)},
|
||
{Role: agentllm.RoleUser, Content: "现在"},
|
||
}
|
||
out, rep := FinishAssembly(msgs, &Report{}, Tail{Window: 30})
|
||
if len(out) != 2 {
|
||
t.Fatalf("kept %d", len(out))
|
||
}
|
||
if len(rep.Truncations) == 0 {
|
||
t.Fatal("expected truncation")
|
||
}
|
||
}
|
||
|
||
// ---- N2 WP1:裁切工具组对齐(验收② 产品侧机器证据)----
|
||
|
||
// assertNoOrphanToolPairs 双向断言:每个 tool_call 有结果,每个结果有调用。
|
||
func assertNoOrphanToolPairs(t *testing.T, msgs []agentllm.Message) {
|
||
t.Helper()
|
||
calls := map[string]bool{}
|
||
results := map[string]bool{}
|
||
for _, m := range msgs {
|
||
for _, c := range m.ToolCalls {
|
||
calls[c.ID] = true
|
||
}
|
||
if m.Role == agentllm.RoleTool && m.ToolCallID != "" {
|
||
results[m.ToolCallID] = true
|
||
}
|
||
}
|
||
for id := range calls {
|
||
if !results[id] {
|
||
t.Fatalf("孤立 tool_call:%s(无结果)", id)
|
||
}
|
||
}
|
||
for id := range results {
|
||
if !calls[id] {
|
||
t.Fatalf("悬空 tool 结果:%s(无调用)", id)
|
||
}
|
||
}
|
||
}
|
||
|
||
func toolMsgs() []agentllm.Message {
|
||
call := func(id, name string) agentllm.ToolCall {
|
||
return agentllm.ToolCall{ID: id, Name: name, Arguments: "{}"}
|
||
}
|
||
return []agentllm.Message{
|
||
{Role: agentllm.RoleSystem, Content: "s"},
|
||
{Role: agentllm.RoleUser, Content: strings.Repeat("旧一", 300)},
|
||
{Role: agentllm.RoleAssistant, ToolCalls: []agentllm.ToolCall{call("tc1", "read")}},
|
||
{Role: agentllm.RoleTool, ToolCallID: "tc1", Name: "read", Content: strings.Repeat("结果一", 200)},
|
||
{Role: agentllm.RoleUser, Content: strings.Repeat("旧二", 300)},
|
||
{Role: agentllm.RoleAssistant, ToolCalls: []agentllm.ToolCall{call("tc2", "grep"), call("tc3", "read")}},
|
||
{Role: agentllm.RoleTool, ToolCallID: "tc2", Name: "grep", Content: strings.Repeat("结果二", 200)},
|
||
{Role: agentllm.RoleTool, ToolCallID: "tc3", Name: "read", Content: strings.Repeat("结果三", 200)},
|
||
{Role: agentllm.RoleUser, Content: "现在"},
|
||
}
|
||
}
|
||
|
||
// 连续多轮裁切后配对仍成立:assistant 与其结果同退,不逐条拆散。
|
||
func TestTrimOldestKeepsToolPairs(t *testing.T) {
|
||
out, rep := FinishAssembly(toolMsgs(), &Report{}, Tail{Window: 1600})
|
||
assertNoOrphanToolPairs(t, out)
|
||
if len(rep.Truncations) == 0 {
|
||
t.Fatal("expected truncation")
|
||
}
|
||
if out[len(out)-1].Content != "现在" {
|
||
t.Fatal("当前轮必须保留")
|
||
}
|
||
// 窗口再压:旧二组也须整组同退,仍不出现孤立配对。
|
||
out2, _ := FinishAssembly(toolMsgs(), &Report{}, Tail{Window: 900})
|
||
assertNoOrphanToolPairs(t, out2)
|
||
}
|
||
|
||
// 开头即孤立 tool 结果组:整组同退(其 assistant 已不存在)。
|
||
func TestTrimOldestDropsLeadingOrphanToolRun(t *testing.T) {
|
||
msgs := []agentllm.Message{
|
||
{Role: agentllm.RoleSystem, Content: "s"},
|
||
{Role: agentllm.RoleTool, ToolCallID: "ghost", Name: "read", Content: strings.Repeat("孤", 500)},
|
||
{Role: agentllm.RoleUser, Content: strings.Repeat("旧", 500)},
|
||
{Role: agentllm.RoleUser, Content: "现在"},
|
||
}
|
||
out, _ := FinishAssembly(msgs, &Report{}, Tail{Window: 700})
|
||
assertNoOrphanToolPairs(t, out)
|
||
for _, m := range out {
|
||
if m.ToolCallID == "ghost" {
|
||
t.Fatal("孤立结果组应整组丢弃")
|
||
}
|
||
}
|
||
}
|
||
|
||
// 工具组越过当前轮时不再丢弃:窗口可略超,配对不可破坏。
|
||
func TestTrimOldestStopsWhenGroupCrossesCurrentTurn(t *testing.T) {
|
||
msgs := []agentllm.Message{
|
||
{Role: agentllm.RoleSystem, Content: "s"},
|
||
{Role: agentllm.RoleAssistant, ToolCalls: []agentllm.ToolCall{{ID: "tc9", Name: "read", Arguments: "{}"}}},
|
||
{Role: agentllm.RoleTool, ToolCallID: "tc9", Name: "read", Content: strings.Repeat("结果", 900)},
|
||
}
|
||
start, end := oldestDropRange(msgs)
|
||
if start != -1 || end != -1 {
|
||
t.Fatalf("组越过当前轮应返回 -1,-1,得到 %d,%d", start, end)
|
||
}
|
||
}
|
||
|
||
// ---- N2 WP1:PrefixHash 稳定契约(同一输入 → 同一前缀哈希)----
|
||
|
||
func TestPrefixHashStableForIdenticalInput(t *testing.T) {
|
||
build := func() []agentllm.Message {
|
||
return []agentllm.Message{
|
||
{Role: agentllm.RoleSystem, Content: "系统前缀A"},
|
||
{Role: agentllm.RoleSystem, Content: "系统前缀B"},
|
||
{Role: agentllm.RoleUser, Content: "历史一"},
|
||
{Role: agentllm.RoleAssistant, Content: "回复一"},
|
||
{Role: agentllm.RoleUser, Content: "现在"},
|
||
}
|
||
}
|
||
_, rep1 := FinishAssembly(build(), &Report{}, Tail{Instructions: "个人指令"})
|
||
_, rep2 := FinishAssembly(build(), &Report{}, Tail{Instructions: "个人指令"})
|
||
if rep1.PrefixHash == "" || rep1.PrefixHash != rep2.PrefixHash {
|
||
t.Fatalf("同一输入的前缀哈希必须一致:%q vs %q", rep1.PrefixHash, rep2.PrefixHash)
|
||
}
|
||
// 前缀变化必须改变哈希(钉住哈希语义,防退化为常量)。
|
||
other := build()
|
||
other[0].Content = "系统前缀X"
|
||
_, rep3 := FinishAssembly(other, &Report{}, Tail{Instructions: "个人指令"})
|
||
if rep3.PrefixHash == rep1.PrefixHash {
|
||
t.Fatal("前缀变化后哈希必须不同")
|
||
}
|
||
}
|