- 缺口 d:状态在**思考前**读一次,同一份快照既喂思考轮又喂 Decide(读不到 = Known=false,绝不伪装成中性状态) - 缺口 c:新增只读工具 capability_list(owner-only、三重输出边界、读写隔离),她可自查当下真实可用面 - 缺口 b:内置允许集补 transcode / capability_list(此前 CAPACITY_ENABLED=true 时新工具会从模型可见面消失) - 缺口 e:guidancePrompt 旧文案承诺的「IoT/日历/提醒」在全仓根本没有对应工具(chat 侧也没有)⇒ 按诚实化改为「与主人对话同等工具面 + 指名自查入口」,不再有空头支票 - (a) 未做且**不自创**:那三把工具无任何设计规格(名字/schema/读写边界/输出上限)⇒ 已报回并登记进 design/20(裁决:走能力/插件/MCP 暴露路线) - 变异实测:允许集去掉新工具 ⇒ TestDecide_AllowedSets + 组合根跨层测试红;丢掉状态快照 ⇒ TestLoopThinker_Think_InjectsRoundStateBlock 红 - 已知弱项(如实登记):组合根那行 wireCapabilitySurface() 调用本身无测试覆盖(与既有 wireTranscode 同惯例)
242 lines
8.3 KiB
Go
242 lines
8.3 KiB
Go
package capacity
|
|
|
|
import (
|
|
"context"
|
|
"reflect"
|
|
"testing"
|
|
)
|
|
|
|
// allTools is a representative registration-order tool list for tests; it covers
|
|
// the basic/io/terminal plugins actually shipped by the repo, plus the two tools
|
|
// added to the built-in state sets by X1 (transcode / capability_list).
|
|
func allTools() []string {
|
|
return []string{
|
|
"terminal", "file", "knowledge", "text",
|
|
"web-search", "web-fetch", "http",
|
|
"calculator", "datetime", "random", "json", "markdown",
|
|
transcodeToolName, capabilityListToolName,
|
|
}
|
|
}
|
|
|
|
func activeConfig() Config {
|
|
return Config{Enabled: true, MinConfidence: 0.6}
|
|
}
|
|
|
|
func in(messages ...string) Input {
|
|
return Input{Messages: messages, CurrentState: StateQuiet, AllToolNames: allTools()}
|
|
}
|
|
|
|
func contains(s []string, want string) bool {
|
|
for _, v := range s {
|
|
if v == want {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func TestDecide_Disabled_ReturnsAllToolsNoEvents(t *testing.T) {
|
|
d := New(Config{}) // Enabled zero → disabled
|
|
dec := d.Decide(context.Background(), in("帮我查一下资料"))
|
|
if !reflect.DeepEqual(dec.AllowedTools, allTools()) {
|
|
t.Fatalf("disabled allowed = %v, want all %v", dec.AllowedTools, allTools())
|
|
}
|
|
if len(dec.Events) != 0 {
|
|
t.Fatalf("disabled should emit no events, got %+v", dec.Events)
|
|
}
|
|
}
|
|
|
|
func TestDecide_Chitchat_StaysQuiet(t *testing.T) {
|
|
d := New(activeConfig())
|
|
for _, msg := range []string{"你好", "抱抱", "嗯嗯", "晚安", "今天好累呀"} {
|
|
if got := d.Decide(context.Background(), in(msg)); got.State != StateQuiet {
|
|
t.Errorf("chitchat %q woke to %s, want quiet", msg, got.State)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDecide_Chitchat_NoAwakenEvent(t *testing.T) {
|
|
d := New(activeConfig())
|
|
dec := d.Decide(context.Background(), in("你好"))
|
|
if len(dec.Events) != 0 {
|
|
t.Fatalf("quiet chat should not emit capacity_awaken, got %+v", dec.Events)
|
|
}
|
|
}
|
|
|
|
func TestDecide_Query_Awakens(t *testing.T) {
|
|
d := New(activeConfig())
|
|
dec := d.Decide(context.Background(), in("帮我查一下今天的天气"))
|
|
if dec.State != StateAwake {
|
|
t.Fatalf("query state = %s, want awake", dec.State)
|
|
}
|
|
if dec.Confidence < 0.6 {
|
|
t.Fatalf("query confidence = %v, want >= 0.6", dec.Confidence)
|
|
}
|
|
if !contains(dec.AllowedTools, "web-search") {
|
|
t.Fatalf("awake allowed should include web-search, got %v", dec.AllowedTools)
|
|
}
|
|
if len(dec.Events) != 1 || dec.Events[0].FromState != StateQuiet || dec.Events[0].ToState != StateAwake {
|
|
t.Fatalf("expected a quiet→awake event, got %+v", dec.Events)
|
|
}
|
|
}
|
|
|
|
func TestDecide_EditFile_Awakens(t *testing.T) {
|
|
d := New(activeConfig())
|
|
dec := d.Decide(context.Background(), in("改一下 cx.go 这个函数"))
|
|
if dec.State != StateAwake {
|
|
t.Fatalf("edit state = %s, want awake", dec.State)
|
|
}
|
|
if !contains(dec.AllowedTools, "file") {
|
|
t.Fatalf("awake allowed should include file, got %v", dec.AllowedTools)
|
|
}
|
|
if len(dec.Events) != 1 || dec.Events[0].Tool != "file" {
|
|
t.Fatalf("edit event should light up file, got %+v", dec.Events)
|
|
}
|
|
}
|
|
|
|
func TestDecide_ComplexTask_Full(t *testing.T) {
|
|
d := New(activeConfig())
|
|
dec := d.Decide(context.Background(), in("帮我重构整个项目,迁移到新架构"))
|
|
if dec.State != StateFull {
|
|
t.Fatalf("complex state = %s, want full", dec.State)
|
|
}
|
|
if !reflect.DeepEqual(dec.AllowedTools, allTools()) {
|
|
t.Fatalf("full allowed = %v, want all %v", dec.AllowedTools, allTools())
|
|
}
|
|
}
|
|
|
|
func TestDecide_L1_UndecidedDeferstoL2_WakesOnTaskTrend(t *testing.T) {
|
|
d := New(activeConfig())
|
|
// Last turn (这个) is undecided by L1; the recent history is task-leaning.
|
|
dec := d.Decide(context.Background(), in("帮我查一下资料", "还有吗", "这个"))
|
|
if dec.State != StateAwake {
|
|
t.Fatalf("task-trend state = %s, want awake", dec.State)
|
|
}
|
|
}
|
|
|
|
func TestDecide_L2_CompanionTrend_Quiet(t *testing.T) {
|
|
d := New(activeConfig())
|
|
dec := d.Decide(context.Background(), in("你好", "抱抱", "嗯嗯", "这个"))
|
|
if dec.State != StateQuiet {
|
|
t.Fatalf("companion-trend state = %s, want quiet", dec.State)
|
|
}
|
|
}
|
|
|
|
func TestDecide_L2_Followup_Wakes(t *testing.T) {
|
|
d := New(activeConfig())
|
|
// A clear previous task plus a bare continuation cue (继续) stays awake —
|
|
// L2 picks up the follow-up on the previous round's task (design §2.2).
|
|
dec := d.Decide(context.Background(), in("帮我查一下资料", "继续"))
|
|
if dec.State != StateAwake {
|
|
t.Fatalf("followup state = %s, want awake", dec.State)
|
|
}
|
|
}
|
|
|
|
func TestDecide_Undecided_ConservativeQuiet(t *testing.T) {
|
|
d := New(activeConfig())
|
|
// Entirely neutral turns with no signal at all.
|
|
dec := d.Decide(context.Background(), in("嗯", "这个", "然后"))
|
|
if dec.State != StateQuiet {
|
|
t.Fatalf("undecided state = %s, want quiet (conservative)", dec.State)
|
|
}
|
|
}
|
|
|
|
func TestDecide_PersonaForceState(t *testing.T) {
|
|
d := New(activeConfig())
|
|
inp := in("你好")
|
|
inp.Awakening = Awakening{ForceState: StateAwake}
|
|
dec := d.Decide(context.Background(), inp)
|
|
if dec.State != StateAwake {
|
|
t.Fatalf("force_state state = %s, want awake", dec.State)
|
|
}
|
|
}
|
|
|
|
func TestDecide_PersonaDefaultState(t *testing.T) {
|
|
// An assistant-type persona rests at awake; an undecided round lands there.
|
|
d := New(Config{Enabled: true, MinConfidence: 0.6, Awakening: Awakening{DefaultState: StateAwake}})
|
|
dec := d.Decide(context.Background(), in("这个"))
|
|
if dec.State != StateAwake {
|
|
t.Fatalf("assistant default_state = %s, want awake", dec.State)
|
|
}
|
|
}
|
|
|
|
func TestDecide_PersonaMinConfidence_ForcesQuiet(t *testing.T) {
|
|
// A high threshold forces even a strong task signal to stay quiet. Low conf.
|
|
d := New(Config{Enabled: true, MinConfidence: 0.6})
|
|
inp := in("帮我查一下资料")
|
|
inp.Awakening = Awakening{MinConfidence: 0.99}
|
|
if dec := d.Decide(context.Background(), inp); dec.State != StateQuiet {
|
|
t.Fatalf("high min_confidence should force quiet, got %s", dec.State)
|
|
}
|
|
// The default threshold lets the same strong signal wake.
|
|
if dec := d.Decide(context.Background(), in("帮我查一下资料")); dec.State != StateAwake {
|
|
t.Fatalf("default min_confidence should wake, got %s", dec.State)
|
|
}
|
|
}
|
|
|
|
func TestDecide_AllowedSets(t *testing.T) {
|
|
d := New(activeConfig())
|
|
|
|
// 内置档位集里的「后加能力」也必须在允许集内(X1 / 裁决 15):登记了能力
|
|
// 却不在集合里 = CAPACITY_ENABLED=true 时它调不动。
|
|
extra := []string{transcodeToolName, capabilityListToolName}
|
|
|
|
quiet := d.Decide(context.Background(), in("你好")).AllowedTools
|
|
wantQuiet := append([]string{"terminal", "file", "knowledge", "text"}, extra...)
|
|
if !reflect.DeepEqual(quiet, wantQuiet) {
|
|
t.Fatalf("quiet allowed = %v, want %v", quiet, wantQuiet)
|
|
}
|
|
|
|
awake := d.Decide(context.Background(), in("帮我查一下资料")).AllowedTools
|
|
wantAwake := append([]string{"terminal", "file", "knowledge", "text", "web-search", "web-fetch", "http"}, extra...)
|
|
if !reflect.DeepEqual(awake, wantAwake) {
|
|
t.Fatalf("awake allowed = %v, want %v", awake, wantAwake)
|
|
}
|
|
if contains(awake, "json") || contains(awake, "markdown") || contains(awake, "calculator") {
|
|
t.Fatalf("awake must not expose heavy utility tools, got %v", awake)
|
|
}
|
|
|
|
full := d.Decide(context.Background(), in("帮我重构整个项目")).AllowedTools
|
|
if !reflect.DeepEqual(full, allTools()) {
|
|
t.Fatalf("full allowed = %v, want all %v", full, allTools())
|
|
}
|
|
}
|
|
|
|
func TestDecide_Event_LitToolIsAllowed(t *testing.T) {
|
|
d := New(activeConfig())
|
|
dec := d.Decide(context.Background(), in("帮我查一下资料"))
|
|
if len(dec.Events) != 1 {
|
|
t.Fatalf("expected one event, got %+v", dec.Events)
|
|
}
|
|
ev := dec.Events[0]
|
|
if !ev.IsAwaken() || ev.Tool == "" {
|
|
t.Fatalf("event should be an awaken with a lit tool, got %+v", ev)
|
|
}
|
|
if !contains(dec.AllowedTools, ev.Tool) {
|
|
t.Fatalf("lit tool %q not in allowed set %v", ev.Tool, dec.AllowedTools)
|
|
}
|
|
}
|
|
|
|
func TestDecide_CustomToolSets(t *testing.T) {
|
|
d := New(Config{
|
|
Enabled: true,
|
|
MinConfidence: 0.6,
|
|
ToolSets: map[State][]string{StateQuiet: {"terminal"}},
|
|
})
|
|
dec := d.Decide(context.Background(), in("你好"))
|
|
if !reflect.DeepEqual(dec.AllowedTools, []string{"terminal"}) {
|
|
t.Fatalf("custom quiet allowed = %v, want [terminal]", dec.AllowedTools)
|
|
}
|
|
}
|
|
|
|
func TestDecide_NilToolNames_EmptyAllowed(t *testing.T) {
|
|
d := New(activeConfig())
|
|
dec := d.Decide(context.Background(), Input{Messages: []string{"你好"}, CurrentState: StateQuiet})
|
|
if dec.State != StateQuiet {
|
|
t.Fatalf("state = %s, want quiet", dec.State)
|
|
}
|
|
if len(dec.AllowedTools) != 0 {
|
|
t.Fatalf("allowed with no known tools = %v, want empty", dec.AllowedTools)
|
|
}
|
|
}
|