package ctxasm import ( "strings" "testing" core "git-ssh.u1680832.nyat.app/weichen/Agent-System-Core.git" ) func fakeUser(text string) core.Message { return core.Message{ID: "m1", Role: core.RoleUser, Parts: []core.Part{{Type: core.PartText, Text: text}}} } func TestAssembleInputAttachmentLimit(t *testing.T) { prof := core.AgentProfile{SystemPrompt: "s"} long := strings.Repeat("会", 60000) // 60k 字符 > 默认 48k 上限 doc := core.ParsedDocument{ Meta: core.DocMeta{Filename: "纪要.docx"}, Blocks: []core.DocBlock{{Type: core.BlockPara, Text: long}}, } msgs, rep := AssembleInput(prof, core.EffectiveSettings{}, nil, fakeUser("请总结"), []core.ParsedDocument{doc}, nil, nil, nil, nil, nil, AssembleOptions{}) last := msgs[len(msgs)-1] if !strings.Contains(last.Content, "纪要.docx") { t.Fatal("attachment filename missing from model input") } if got := strings.Count(last.Content, "会"); got != DefaultMaxAttachmentChars { t.Errorf("injected chars = %d, want %d (default limit)", got, DefaultMaxAttachmentChars) } if !strings.Contains(last.Content, "附件超长截断") { t.Error("truncation marker missing in model input") } if len(rep.Truncations) != 1 || !strings.Contains(rep.Truncations[0], "纪要.docx") { t.Errorf("truncation not observable in report: %v", rep.Truncations) } } func TestAssembleInputAttachmentLimitConfigurable(t *testing.T) { prof := core.AgentProfile{SystemPrompt: "s"} doc := core.ParsedDocument{ Meta: core.DocMeta{Filename: "a.txt"}, Blocks: []core.DocBlock{{Type: core.BlockPara, Text: strings.Repeat("甲", 120)}}, } eff := core.EffectiveSettings{"context": map[string]any{"maxAttachmentChars": float64(100)}} msgs, rep := AssembleInput(prof, eff, nil, fakeUser("总结"), []core.ParsedDocument{doc}, nil, nil, nil, nil, nil, AssembleOptions{}) last := msgs[len(msgs)-1] if got := strings.Count(last.Content, "甲"); got != 100 { t.Errorf("injected chars = %d, want 100 (configured limit)", got) } if len(rep.Truncations) != 1 { t.Errorf("expected truncation record, got %v", rep.Truncations) } } func TestAssembleInputAttachmentNoTruncationUnderLimit(t *testing.T) { prof := core.AgentProfile{SystemPrompt: "s"} doc := core.ParsedDocument{ Meta: core.DocMeta{Filename: "b.txt"}, Blocks: []core.DocBlock{{Type: core.BlockPara, Text: strings.Repeat("乙", 100)}}, } msgs, rep := AssembleInput(prof, core.EffectiveSettings{}, nil, fakeUser("总结"), []core.ParsedDocument{doc}, nil, nil, nil, nil, nil, AssembleOptions{}) if len(rep.Truncations) != 0 { t.Errorf("under-limit doc should not truncate: %v", rep.Truncations) } if !strings.Contains(msgs[len(msgs)-1].Content, strings.Repeat("乙", 100)) { t.Error("full text should be injected") } } // TestAssembleInputCardBackfill 验证 card 引用部件回填产物卡全文(D35:卡片为产物 // 权威载体,下一轮模型不失忆),超限截断入 Truncations,构成入 Items 的 card(n)。 func TestAssembleInputCardBackfill(t *testing.T) { prof := core.AgentProfile{SystemPrompt: "s"} full := "全文第一段。\n" + strings.Repeat("丙", 50) card := core.ActivityCard{ID: "card_1", Title: "会议纪要", Body: []core.UINode{ {Type: "markdown", Markdown: full}, {Type: "kv", KV: [][2]string{{"摘要", "三项决议"}}}, }} hist := []core.Message{ fakeUser("生成纪要"), {ID: "m2", Role: core.RoleAssistant, Parts: []core.Part{ {Type: core.PartText, Text: "已完成,详见卡片。"}, {Type: core.PartCard, Card: &core.CardPart{Kind: "result", CardID: "card_1"}}, }}, } resolve := func(id core.ID) *core.ActivityCard { if string(id) == "card_1" { return &card } return nil } msgs, rep := AssembleInput(prof, core.EffectiveSettings{}, hist, fakeUser("把第二章改短"), nil, nil, nil, nil, resolve, nil, AssembleOptions{}) var assistantIn string for _, m := range msgs { if m.Role == "assistant" { assistantIn = m.Content } } if !strings.Contains(assistantIn, "[产物卡片:会议纪要]") || !strings.Contains(assistantIn, "全文第一段。") { t.Errorf("card body not backfilled into model input: %q", assistantIn) } if !strings.Contains(assistantIn, "摘要:三项决议") { t.Error("kv summary not backfilled") } found := false for _, it := range rep.Items { if it == "card(1)" { found = true } } if !found { t.Errorf("card(n) missing from report items: %v", rep.Items) } if len(rep.Truncations) != 0 { t.Errorf("under-limit card should not truncate: %v", rep.Truncations) } } // TestAssembleInputCardTruncationAndFallback 超限截断留痕;无 resolver 时留占位不静默丢弃。 func TestAssembleInputCardTruncationAndFallback(t *testing.T) { prof := core.AgentProfile{SystemPrompt: "s"} long := strings.Repeat("丁", 20000) card := core.ActivityCard{ID: "card_2", Title: "长报告", Body: []core.UINode{{Type: "markdown", Markdown: long}}} hist := []core.Message{ {ID: "m9", Role: core.RoleAssistant, Parts: []core.Part{ {Type: core.PartCard, Card: &core.CardPart{Kind: "result", CardID: "card_2"}}, }}, } eff := core.EffectiveSettings{"context": map[string]any{"maxCardChars": float64(100)}} msgs, rep := AssembleInput(prof, eff, hist, fakeUser("继续"), nil, nil, nil, nil, func(id core.ID) *core.ActivityCard { return &card }, nil, AssembleOptions{}) if strings.Count(msgs[1].Content, "丁") != 100 { t.Errorf("card chars = %d, want 100 (configured limit)", strings.Count(msgs[1].Content, "丁")) } if len(rep.Truncations) != 1 || !strings.Contains(rep.Truncations[0], "长报告") { t.Errorf("card truncation not observable: %v", rep.Truncations) } // 无 resolver(收件箱关闭/卡片缺失):占位可见。 msgs2, _ := AssembleInput(prof, core.EffectiveSettings{}, hist, fakeUser("继续"), nil, nil, nil, nil, nil, nil, AssembleOptions{}) if !strings.Contains(msgs2[1].Content, "内容见收件箱") { t.Errorf("fallback placeholder missing: %q", msgs2[1].Content) } } // TestAssembleInputArtifactBackfill 验证 D46:artifact_ref 与卡片回填对称—— // 文本工件回填正文(超 limit 截断留痕、报告 artifact(n));二进制留占位。 func TestAssembleInputArtifactBackfill(t *testing.T) { prof := core.AgentProfile{SystemPrompt: "s"} art := core.Artifact{ID: "art_1", Name: "检索汇总.md", Kind: "document"} hist := []core.Message{ {ID: "m2", Role: core.RoleAssistant, Parts: []core.Part{ {Type: core.PartArtifactRef, Artifact: &core.ArtifactRef{ArtifactID: "art_1", Name: art.Name, Kind: art.Kind}}, }}, } resolve := func(id core.ID) (core.Artifact, []byte, bool) { if id == "art_1" { return art, []byte("## 汇总\n关键结论一。"), true } return core.Artifact{}, nil, false } msgs, rep := AssembleInput(prof, core.EffectiveSettings{}, hist, fakeUser("继续"), nil, nil, nil, nil, nil, resolve, AssembleOptions{}) if !strings.Contains(msgs[1].Content, "此前已生成文件 检索汇总.md,内容如下:") || !strings.Contains(msgs[1].Content, "关键结论一。") { t.Errorf("artifact body not backfilled: %q", msgs[1].Content) } if !contains(rep.Items, "artifact(1)") { t.Errorf("artifact(n) missing from report items: %v", rep.Items) } // 超限截断留痕(复用 maxCardChars 上限口径)。 long := strings.Repeat("乙", 300) eff := core.EffectiveSettings{"context": map[string]any{"maxCardChars": float64(100)}} msgs2, rep2 := AssembleInput(prof, eff, hist, fakeUser("继续"), nil, nil, nil, nil, nil, func(core.ID) (core.Artifact, []byte, bool) { return art, []byte(long), true }, AssembleOptions{}) if strings.Count(msgs2[1].Content, "乙") != 100 { t.Errorf("artifact chars = %d, want 100", strings.Count(msgs2[1].Content, "乙")) } if len(rep2.Truncations) != 1 || !strings.Contains(rep2.Truncations[0], "检索汇总.md") { t.Errorf("artifact truncation not observable: %v", rep2.Truncations) } // 二进制内容:占位不回灌乱码。 msgs3, _ := AssembleInput(prof, core.EffectiveSettings{}, hist, fakeUser("继续"), nil, nil, nil, nil, nil, func(core.ID) (core.Artifact, []byte, bool) { return art, []byte{0xff, 0xfe, 0x00, 0x01}, true }, AssembleOptions{}) if !strings.Contains(msgs3[1].Content, "二进制内容") || strings.Contains(msgs3[1].Content, "关键结论") { t.Errorf("binary artifact should keep placeholder: %q", msgs3[1].Content) } // 无 resolver:占位可见。 msgs4, _ := AssembleInput(prof, core.EffectiveSettings{}, hist, fakeUser("继续"), nil, nil, nil, nil, nil, nil, AssembleOptions{}) if !strings.Contains(msgs4[1].Content, "此前已生成文件:检索汇总.md。") { t.Errorf("artifact placeholder missing without resolver: %q", msgs4[1].Content) } } func contains(items []string, want string) bool { for _, it := range items { if it == want { return true } } return false }