Define the core contracts: domain model
Details (original message): Define the core contracts: domain model, stream events, HITL, profiles, tools, plugins, UI vocabulary, ingest IR, retrieval and memory; golden-sample conformance tests; TS mirror.
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
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
|
||||
})()
|
||||
}
|
||||
Reference in New Issue
Block a user