Files
Agent-System-Core/conformance_test.go
weichen e5c9c540cd Add the avatar vocabulary to UINode (UI capability platform)
Why: plugins and the host need one shared avatar primitive for member/
owner displays in cards, messages, and admin surfaces (user decision
2026-09-30: avatars, cards, settings become base capabilities plugins
reuse directly).

Evidence: go test green with regenerated golden (single avatar with
sub/tone plus a stacked group); omitempty keeps existing wire samples
byte-stable.

Impact: additive optional UINode field + type; unknown-type consumers
ignore it (forward compat). Renderers gain 'avatar' via the ui repo.

Refs: Agent-System-Documents proposal 2026-09-30-ui-redesign-and-auth.md
2026-09-30 14:22:10 +00:00

265 lines
12 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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: "也可以直接输入其他格式要求",
})
// 阻断态 + 锚定(内联确认卡:anchorMessageId 内联于对话流;blocked 禁用确认并给出出路)。
golden(t, "hitl_card_blocked", HITLCard{
ID: "hitl_def456abc789", RunID: "run_1", ConversationID: "cnv_1",
AnchorMessageID: "msg_abc123def456",
Question: "批准变更 CO-2026-007?", Kind: HITLKindConfirm,
Choices: []HITLChoice{
{ID: "approve", Label: "确认批准", Description: "变更单进入执行态", Impact: "立即生效并留痕,不可裸文本撤回"},
{ID: "reject", Label: "驳回", Description: "关闭变更单并记录理由", Impact: "留痕可审计"},
},
Blocked: true, BlockReason: "存在未闭环高风险变更单,可先处理后重新检查",
})
}
func TestGoldenActivityCard(t *testing.T) {
golden(t, "activity_card", ActivityCard{
ID: "card_abc123def456", ProjectID: "prj_abc123def456", ConversationID: "cnv_abc123def456",
SourceRunID: "run_abc123def456", AnchorMessageID: "msg_abc123def456",
Kind: CardKindCompare, Title: "项目执行周期不一致 · 目标已基线",
Status: CardActionRequired, Tone: "danger",
Body: []UINode{
{Type: UICompare, Compare: &UICompareSpec{
Left: UICompareSide{Title: "基线 V1.2 · 已评审", Tone: "danger", Nodes: []UINode{{Type: UIMarkdown, Markdown: "12 个月(立项到注册)"}}},
Right: UICompareSide{Title: "新资料 · 待确认", Tone: "ok", Nodes: []UINode{{Type: UIMarkdown, Markdown: "18 个月(含临床试验)"}}},
}},
{Type: UIProgress, Title: "切片进度", Progress: &UIProgressSpec{Label: "切片 24/24", Value: 1, Tone: "ok"}},
},
Actions: []UIAction{
{ID: "escalate", Label: "发起变更流程", Style: "primary", Command: "change.escalate"},
{ID: "keep", Label: "维持原样", Style: "danger"},
},
Pinned: true,
})
}
func TestGoldenCardEvents(t *testing.T) {
golden(t, "event_card_created", StreamEvent{Seq: 45, RunID: "run_1", ConversationID: "cnv_1", TS: "2026-09-30T10:00:00Z", V: 1, Type: EvCardCreated,
Payload: json.RawMessage(`{"card":{"id":"card_abc123def456","projectId":"prj_1","kind":"progress","title":"竞品分析 PPTX 入库","status":"active","createdAt":"2026-09-30T10:00:00Z","updatedAt":"2026-09-30T10:00:00Z"}}`)})
golden(t, "event_card_resolved", StreamEvent{Seq: 46, RunID: "run_1", ConversationID: "cnv_1", TS: "2026-09-30T10:05:00Z", V: 1, Type: EvCardResolved,
Payload: json.RawMessage(`{"cardId":"card_abc123def456","actionId":"escalate"}`)})
}
func TestGoldenUIVocabularies(t *testing.T) {
golden(t, "ui_avatar", []UINode{
{Type: UIAvatar, Avatar: &UIAvatarSpec{Name: "weichen", Sub: "OWNER"}},
{Type: UIAvatar, Avatar: &UIAvatarSpec{
Stack: []UIAvatarSpec{{Name: "甲", Tone: "ok"}, {Name: "乙", Tone: "warn"}, {Name: "丙"}},
}},
})
golden(t, "ui_vocabularies", []UINode{
{Type: UIProgress, Progress: &UIProgressSpec{Label: "市场可行性 78%", Value: 0.78, Tone: "ok"}},
{Type: UICompare, Compare: &UICompareSpec{
Left: UICompareSide{Title: "基线", Tone: "danger"},
Right: UICompareSide{Title: "新内容", Tone: "ok", Nodes: []UINode{{Type: UIMarkdown, Markdown: "18 个月"}}},
}},
{Type: UITree, Tree: &UITreeSpec{Nodes: []UITreeNode{
{ID: "stage-1", Label: "商务与策划", Count: 24, Children: []UITreeNode{
{ID: "topic-1", Label: "市场调研", Tone: "ok", Badge: "多挂载", Count: 12, Selected: true},
}},
}}},
{Type: UITable, Table: &UITableSpec{
Columns: []UITableColumn{{Key: "id", Label: "编号"}, {Key: "title", Label: "标题"}},
Rows: []UITableRow{
{ID: "ur-005", Cells: [][2]string{{"id", "UR-005"}, {"title", "项目执行周期"}}, Actions: []UIAction{{ID: "trace", Label: "追溯"}}},
},
}},
{Type: UISteps, Steps: &UIStepsSpec{Current: 2, Steps: []UIStep{
{Label: "申请", Status: "done"}, {Label: "影响分析", Status: "done"}, {Label: "评审", Status: "active"}, {Label: "批准", Status: "pending"},
}}},
{Type: UITabs, Tabs: &UITabsSpec{Active: "view", Tabs: []UITab{
{ID: "view", Label: "查看", Nodes: []UINode{{Type: UIMarkdown, Markdown: "正文"}}},
{ID: "history", Label: "历史版本"},
}}},
{Type: UITimeline, Timeline: &UITimelineSpec{Items: []UITimelineItem{
{Time: "10:42", Title: "v0.6 填入注册分类界定", Tone: "ok", Markdown: "来源 S-121", Ref: "S-121"},
{Time: "09-28", Title: "v0.5 更新竞品定价段落", Ref: "陈磊确认"},
}}},
})
}
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"}, Commands: []string{"demo.refresh"}, Workspaces: []string{"demo-main"}},
})
}
// ---- 纯函数行为 ----
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,
})
}