Files
Agent-System-Core/conformance_test.go
T
weichen 95294268d8 Add the context assembly observation to the run-completed payload
Details (original message):
Add the context assembly observation to the run-completed payload (items, truncations, estimated size).
2026-09-29 18:26:44 +00:00

188 lines
7.7 KiB
Go

package core
import (
"encoding/json"
"flag"
"os"
"path/filepath"
"testing"
)
// 黄金样本一致性:Go 序列化(线格式契约)与 testdata/golden/*.json 逐字节一致。
// ts/types.ts 镜像同一契约;前端测试按样本做形状断言。变更契约须显式更新样本。
var update = flag.Bool("update", false, "rewrite golden files")
func golden(t *testing.T, name string, v any) {
t.Helper()
data, err := json.MarshalIndent(v, "", " ")
if err != nil {
t.Fatal(err)
}
path := filepath.Join("testdata", "golden", name+".json")
if *update {
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, append(data, '\n'), 0o644); err != nil {
t.Fatal(err)
}
return
}
want, err := os.ReadFile(path)
if err != nil {
t.Fatalf("golden missing (run go test ./... -update): %v", err)
}
if string(want) != string(append(data, '\n')) {
t.Errorf("%s: golden mismatch:\n got: %s\nwant: %s", name, data, want)
}
}
func sampleMessage() Message {
return Message{
ID: "msg_abc123def456", ConversationID: "cnv_abc123def456", RunID: "run_abc123def456",
Role: RoleAssistant,
Parts: []Part{
{Type: PartThinking, Text: "考虑先检索…", Process: true},
{Type: PartToolCall, Process: true, ToolCall: &ToolCallPart{ID: "call_1", Name: "history_search", Arguments: `{"q":"部署"}`, Duration: 12.5}},
{Type: PartToolResult, Process: true, ToolResult: &ToolResultPart{CallID: "call_1", Status: "ok", Summary: "3 条命中"}},
{Type: PartText, Text: "根据历史记录……"},
{Type: PartArtifactRef, Artifact: &ArtifactRef{ArtifactID: "art_abc123def456", Name: "报告.md", Kind: "document"}},
},
}
}
func sampleRun() Run {
return Run{
ID: "run_abc123def456", ConversationID: "cnv_abc123def456", ProjectID: "prj_abc123def456",
Kind: RunConversation, ProfileID: "default", Status: RunCompleted, Phase: PhaseDone,
Usage: &Usage{PromptTokens: 1200, CompletionTokens: 300, TotalTokens: 1500, DurationMs: 4200.5, ToolCalls: 2},
}
}
func TestGoldenDomain(t *testing.T) {
golden(t, "message", sampleMessage())
golden(t, "run", sampleRun())
golden(t, "project", Project{ID: "prj_abc123def456", Name: "示例项目"})
golden(t, "conversation", Conversation{ID: "cnv_abc123def456", ProjectID: "prj_abc123def456", Title: "新会话"})
golden(t, "artifact", Artifact{ID: "art_abc123def456", ProjectID: "prj_abc123def456", Name: "报告.md", Kind: "document", MIME: "text/markdown", Size: 128})
}
func TestGoldenEvents(t *testing.T) {
golden(t, "event_message_delta", StreamEvent{Seq: 42, RunID: "run_1", ConversationID: "cnv_1", TS: "2026-09-29T10:00:00Z", V: 1, Type: EvMessageDelta, Payload: json.RawMessage(`{"index":0,"text":"你"}`)})
golden(t, "event_agent_spawn", StreamEvent{Seq: 43, RunID: "run_1", ConversationID: "cnv_1", TS: "2026-09-29T10:00:01Z", V: 1, Type: EvAgentSpawn, Payload: json.RawMessage(`{"agentRun":"run_sub1","name":"检索员","task":"查法规"}`)})
golden(t, "event_run_completed", StreamEvent{Seq: 44, RunID: "run_1", ConversationID: "cnv_1", TS: "2026-09-29T10:00:02Z", V: 1, Type: EvRunCompleted, Payload: json.RawMessage(`{"usage":{"promptTokens":100,"completionTokens":20,"totalTokens":120,"durationMs":1500,"toolCalls":1}}`)})
}
func TestGoldenHITL(t *testing.T) {
golden(t, "hitl_card", HITLCard{
ID: "hitl_abc123def456", RunID: "run_1", ConversationID: "cnv_1",
Question: "采用哪种输出格式?", Kind: HITLKindChoice,
Choices: []HITLChoice{
{ID: "md", Label: "Markdown 文档", Description: "可版本管理的轻量文档", Impact: "写入 artifacts,可在右栏查看", Recommended: true},
{ID: "docx", Label: "Word 文档", Description: "正式交付格式", Impact: "生成 docx 工件"},
},
AllowFreeInput: true, FreeInputHint: "也可以直接输入其他格式要求",
})
}
func TestGoldenProfileAndTools(t *testing.T) {
maxT := 40
golden(t, "profile", AgentProfile{
ID: "default", Name: "通用助手", Builtin: true, Source: "builtin",
SystemPrompt: "你是一个通用智能体助手。",
Model: ModelRoute{Chat: "gpt-default", Lightweight: "gpt-lite"},
Tools: ToolPolicy{Allow: []string{"*"}, Deny: []string{"http.admin."}, Confirm: []string{"http."}},
Context: ContextPolicy{MaxHistoryMessages: 40, MaxToolResultChars: 20000},
HITL: HITLPolicy{AskOnAmbiguity: true},
Budget: RunBudget{MaxToolCalls: &maxT},
})
golden(t, "tool_http", ToolDesc{
Name: "http_translate", Description: "调用翻译 API", Source: SourceUser,
Parameters: json.RawMessage(`{"type":"object","properties":{"text":{"type":"string"}},"required":["text"]}`),
HTTP: &HTTPToolSpec{Method: "POST", URL: "https://api.example.com/translate", BodyTemplate: `{"q":"{{param.text}}"}`, TimeoutMs: 10000, ResultPath: "data.0"},
})
}
func TestGoldenIngest(t *testing.T) {
golden(t, "parsed_document", ParsedDocument{
Meta: DocMeta{Filename: "报告.docx", MIME: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", Parser: "builtin:docx", Confidence: 0.92},
Blocks: []DocBlock{
{Type: BlockHeading, Level: 1, Text: "概述"},
{Type: BlockPara, Text: "本文档说明……"},
{Type: BlockList, Items: []string{"要点一", "要点二"}},
{Type: BlockTable, Rows: [][]string{{"项", "值"}, {"版本", "1.0"}}},
},
})
}
func TestGoldenPlugin(t *testing.T) {
golden(t, "plugin_manifest", PluginManifest{
ID: "example", Name: "示例插件", Version: "0.1.0", APIVersion: PluginAPIVersion,
Requires: PluginRequires{Network: []string{"api.example.com"}},
Provides: PluginProvides{Tools: []string{"http_echo"}, Intents: []string{"demo"}, Panels: []string{"demo"}, Cards: []string{"note"}, Settings: []string{"greeting"}},
})
}
// ---- 纯函数行为 ----
func TestToolPolicy(t *testing.T) {
p := ToolPolicy{Allow: []string{"*"}, Deny: []string{"http.admin."}, Confirm: []string{"http."}}
if !p.ToolAllowed("history_search") {
t.Error("allow * should permit builtin tools")
}
if p.ToolAllowed("http.admin.reset") {
t.Error("deny prefix should win over allow *")
}
if !p.ToolAllowed("http.fetch") || !p.NeedsConfirm("http.fetch") {
t.Error("http prefix tools allowed but need confirm")
}
if p.NeedsConfirm("history_search") {
t.Error("builtin tool should not need confirm")
}
}
func TestPlainText(t *testing.T) {
d := ParsedDocument{Blocks: []DocBlock{
{Type: BlockHeading, Level: 2, Text: "标题"},
{Type: BlockPara, Text: "段落"},
{Type: BlockTable, Rows: [][]string{{"a", "b"}}},
}}
got := d.PlainText()
for _, want := range []string{"## 标题", "段落", "a\tb"} {
if !contains(got, want) {
t.Errorf("PlainText missing %q in %q", want, got)
}
}
}
func TestSettingsMergeAndPath(t *testing.T) {
base := map[string]any{"models": map[string]any{"chat": "a", "temperature": 0.7}, "ui": map[string]any{"theme": "light"}}
over := map[string]any{"models": map[string]any{"chat": "b"}, "ui": map[string]any{"theme": "dark"}}
eff := EffectiveSettings(MergeSettings(base, over))
if eff.GetString("models.chat", "") != "b" || eff.GetNumber("models.temperature", 0) != 0.7 {
t.Error("deep merge failed")
}
if eff.GetString("models.missing", "def") != "def" {
t.Error("missing path should return default")
}
}
func contains(s, sub string) bool {
return len(s) >= len(sub) && (func() bool {
for i := 0; i+len(sub) <= len(s); i++ {
if s[i:i+len(sub)] == sub {
return true
}
}
return false
})()
}
func TestGoldenContextObservation(t *testing.T) {
golden(t, "context_observation", ContextObservation{
Items: []string{"system(画像)", "history(12)", "attachments(1)", "knowledge(3)"},
Truncations: []string{"历史 47 条超窗口 40,仅保留最近部分", "工具结果超长,转工件引用"},
EstChars: 18234,
})
}