Files
Cyrene_v2/internal/runtime/capacity/capacity_test.go
T
AskaEth 321cc3b763
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(backthink): 思考轮工具面补齐(裁决 15)—— 能力面接入 + 只读 capability_list + 本轮状态快照
- 缺口 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 同惯例)
2026-10-05 13:54:57 +08:00

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)
}
}