Details (original message): Add the context assembly observation to the run-completed payload (items, truncations, estimated size).
188 lines
7.7 KiB
Go
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,
|
|
})
|
|
}
|