package runtime import ( "context" "path/filepath" "strings" "testing" agentllm "agent-native/runtime/llm" agentsession "agent-native/runtime/session" "agent-system-backend/internal/ctxasm" core "agent-system-core" ) func textPart(s string) core.Message { return core.Message{Role: core.RoleUser, Parts: []core.Part{{Type: core.PartText, Text: s}}} } // 指纹只随结构改写变化:正常追加不动,排除/变体切换必动。 func TestContextRevisionStableOnAppendAndChangesOnDivergence(t *testing.T) { base := []core.Message{ {ID: "m1"}, {ID: "m2"}, } r0 := contextRevision(base) if r0 != contextRevision(append(append([]core.Message{}, base...), core.Message{ID: "m3"})) { t.Fatal("append must not change revision") } excluded := append([]core.Message{}, base...) excluded[1].Excluded = true if contextRevision(excluded) == r0 { t.Fatal("exclude must change revision") } variant := append(append([]core.Message{}, base...), core.Message{ID: "m3", VariantGroup: "g1", VariantIndex: 1}) if contextRevision(variant) == r0 { t.Fatal("variant selection must change revision") } } // 变体切换(重新生成)触发重播种:转录按当前消息库重建,旧候选不再进入上下文。 func TestTranscriptReseedsOnVariantSwitch(t *testing.T) { sandbox := t.TempDir() hist := []core.Message{ {ID: "m1", Role: core.RoleUser, Parts: []core.Part{{Type: core.PartText, Text: "写纪要"}}}, {ID: "m2", Role: core.RoleAssistant, VariantGroup: "g1", VariantIndex: 0, 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) }} rc := &fakeRunCtx{ctx: context.Background(), sandbox: sandbox, prof: core.AgentProfile{Model: core.ModelRoute{Chat: "test-model"}}, history: hist} if err := r.Execute(rc); err != nil { t.Fatalf("first Execute: %v", err) } if !strings.Contains(joinReq(requests[0]), "旧候选") { t.Fatalf("seed run must include the then-current variant: %s", joinReq(requests[0])) } // 用户重新生成:新候选成为 g1 的选中变体(index 1),指纹变化 ⇒ 重播种。 rc.history = append(append([]core.Message{}, hist...), core.Message{ID: "m3", Role: core.RoleUser, Parts: []core.Part{{Type: core.PartText, Text: "换一版"}}}, core.Message{ID: "m4", Role: core.RoleAssistant, VariantGroup: "g1", VariantIndex: 1, Parts: []core.Part{{Type: core.PartText, Text: "新候选"}}}, ) if err := r.Execute(rc); err != nil { t.Fatalf("second Execute: %v", err) } 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) } joined := "" for _, m := range loaded { joined += m.Content + "\n" } if strings.Contains(joined, "旧候选") { t.Fatalf("reseed must drop the superseded variant: %s", joined) } if !strings.Contains(joined, "新候选") { t.Fatalf("reseed must keep the selected variant: %s", joined) } } // 非播种 run 的首个请求注入一次环境刷新与新命中模块;播种 run 不注入 // (系统段已含)。用 JSON 兼容方式探请求文本。 func joinReq(msgs []agentllm.Message) string { var b strings.Builder for _, m := range msgs { b.WriteString(m.Content) b.WriteString("\n") } return b.String() } func TestBuildCurrentUserCarriesAttachmentsAndKnowledge(t *testing.T) { doc := core.ParsedDocument{ Meta: core.DocMeta{Filename: "会议.docx"}, Blocks: []core.DocBlock{{Type: core.BlockPara, Text: "结论先入库。"}}, } kn := []core.RetrievalHit{{Doc: core.KnowledgeDoc{Title: "知识", Source: "upload", Chunk: "片段"}}} got := ctxasm.BuildCurrentUser(core.EffectiveSettings{}, core.Message{Parts: []core.Part{{Type: core.PartText, Text: "总结"}}}, []core.ParsedDocument{doc}, kn, &ctxasm.Report{}) for _, want := range []string{"总结", "会议.docx", "结论先入库。", "[相关知识]", "片段"} { if !strings.Contains(got.Content, want) { t.Fatalf("current user missing %q: %s", want, got.Content) } } } var _ = agentllm.Message{}