Why: Chat intent, cross-conversation anchors, payload changes, and native workspace tools could bypass project source analysis. Evidence: Command-bus, app, server, and squad-adapter suites pass with centralized preflight and project-tool fail-closed behavior. Impact: Every classified formal entrance binds actor, conversation, project, command, payload digest, and source watermark before dispatch. Refs: TT-N03 TT-R01
99 lines
3.5 KiB
Go
99 lines
3.5 KiB
Go
package cmdbus
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"testing"
|
||
|
||
agentcommand "agent-command"
|
||
)
|
||
|
||
// EXT-10② 红测:插件命令规格自带 slash 别名 → 四入口同源(slash 命中同一命令
|
||
// id,经总线分发同审计信封)。
|
||
func TestPluginSlashAliasBindsAndDispatches(t *testing.T) {
|
||
bus := New(nopAudit{}, nil, nil)
|
||
called := ""
|
||
if err := bus.RegisterPluginCommand(spec(t, `{
|
||
"id": "rdm.ping", "title": "自检", "slash": "/rdm.ping"
|
||
}`), func(ctx context.Context, req Request) (any, error) {
|
||
called = req.ID + "|" + req.Entrance
|
||
return map[string]any{"ok": true}, nil
|
||
}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
id, ok := bus.ResolveSlash("/rdm.ping")
|
||
if !ok || id != "rdm.ping" {
|
||
t.Fatalf("ResolveSlash = %q ok=%v(插件别名未绑定)", id, ok)
|
||
}
|
||
// 大小写与首尾空白归一。
|
||
if id, ok := bus.ResolveSlash(" /RDM.PING "); !ok || id != "rdm.ping" {
|
||
t.Fatalf("归一解析失败: %q %v", id, ok)
|
||
}
|
||
resp := bus.Dispatch(context.Background(), Request{ID: id, Entrance: "slash", Owner: "alice"})
|
||
if resp.Receipt.State != "committed" {
|
||
t.Fatalf("slash 分发 = %+v", resp.Receipt)
|
||
}
|
||
if called != "rdm.ping|slash" {
|
||
t.Fatalf("处理器见到 %q(须同命令 id)", called)
|
||
}
|
||
// 同一命令按钮入口分发——同源同信封。
|
||
if resp2 := bus.Dispatch(context.Background(), Request{ID: "rdm.ping", Entrance: "button", Owner: "alice"}); resp2.Receipt.State != "committed" {
|
||
t.Fatalf("button 分发 = %+v", resp2.Receipt)
|
||
}
|
||
}
|
||
|
||
// G2-2 红测:意图声明→归一包含匹配(最长短语优先)→路由到命令(chat 入口)。
|
||
func TestIntentDeclareMatchRoute(t *testing.T) {
|
||
bus := New(nopAudit{}, nil, nil)
|
||
_ = bus.RegisterPluginCommand(spec(t, `{
|
||
"id": "rdm.draft.compose", "title": "起草"
|
||
}`), func(ctx context.Context, req Request) (any, error) {
|
||
return map[string]any{"sectionId": req.Params["sectionId"]}, nil
|
||
})
|
||
if err := bus.Intents().Declare(Intent{
|
||
ID: "rdm.write-proposal", Phrases: []string{"帮我写项目建议书", "写项目建议书"},
|
||
Command: "rdm.draft.compose", Params: map[string]any{"sectionId": "sec-1"},
|
||
Plugin: "rdm",
|
||
}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
// 未命中。
|
||
if _, ok := bus.Intents().Match("今天天气如何"); ok {
|
||
t.Fatal("无关文本不得命中")
|
||
}
|
||
// 命中(含大小写/空白归一)。
|
||
in, ok := bus.Intents().Match("请 帮我写项目建议书,尽快")
|
||
if !ok || in.ID != "rdm.write-proposal" {
|
||
t.Fatalf("匹配 = %+v ok=%v", in, ok)
|
||
}
|
||
// 路由:匹配到的命令经总线 chat 入口分发成功。
|
||
bus.SetProjectWorkPreflight(func(context.Context, Request) error { return nil })
|
||
resp := bus.Dispatch(context.Background(), Request{ID: in.Command, Params: in.Params, Entrance: "chat", Owner: "alice"})
|
||
if resp.Receipt.State != "committed" {
|
||
t.Fatalf("路由分发 = %+v", resp.Receipt)
|
||
}
|
||
// 声明校验:缺短语拒绝。
|
||
if err := bus.Intents().Declare(Intent{ID: "x", Command: "y"}); err == nil {
|
||
t.Fatal("不完整声明须拒绝")
|
||
}
|
||
// 最长短语优先:更具体的意图先命中。
|
||
if err := bus.Intents().Declare(Intent{
|
||
ID: "rdm.write-competitive", Phrases: []string{"写竞品分析"},
|
||
Command: "rdm.draft.compose", Plugin: "rdm",
|
||
}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if in, _ := bus.Intents().Match("帮我写项目建议书"); in.ID != "rdm.write-proposal" {
|
||
t.Fatalf("长短语意图应先命中: %+v", in)
|
||
}
|
||
}
|
||
|
||
func spec(t *testing.T, raw string) agentcommand.CommandSpec {
|
||
t.Helper()
|
||
var s agentcommand.CommandSpec
|
||
if err := json.Unmarshal([]byte(raw), &s); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
return s
|
||
}
|