Why: Allow consumers to download an immutable module revision without nested source checkouts or local path replacements. Evidence: SELF package tests and the prior canonical-path rejection are recorded in the Dev dependency-workspace task card; remote consumption is replayed against the committed candidate. Impact: Change the module identity and its Go imports while preserving API behavior. Existing historical submodule revisions remain buildable. Refs: Versioned dependencies U1/U3; feature/dependency-workspace
103 lines
3.8 KiB
Go
103 lines
3.8 KiB
Go
package cmdbus
|
||
|
||
// EXT-14:Bus.CommandTools 只投影带 ExposeAsTool 的已启用命令;risk 缺省 low、
|
||
// description 缺省取 title;未暴露/未启用的命令不出现。
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
|
||
agentcommand "git-ssh.u1680832.nyat.app/weichen/Agent-Command.git"
|
||
)
|
||
|
||
func newToolBus(t *testing.T) *Bus {
|
||
t.Helper()
|
||
return New(nopAudit{}, nil, func() time.Time { return time.Now().UTC() })
|
||
}
|
||
|
||
func TestCommandToolsFiltering(t *testing.T) {
|
||
b := newToolBus(t)
|
||
specs := []json.RawMessage{
|
||
[]byte(`{"id":"rdm.trace","title":"追溯查询","description":"查网","paramsSchema":{"type":"object"},"exposeAsTool":{"risk":"medium"}}`),
|
||
[]byte(`{"id":"rdm.baseline","title":"基线视图","exposeAsTool":{}}`),
|
||
[]byte(`{"id":"rdm.hidden","title":"未暴露","description":"不出现"}`),
|
||
}
|
||
if err := b.RegisterPluginCommands(specs); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
tools := b.CommandTools()
|
||
byID := map[string]CommandTool{}
|
||
for _, ct := range tools {
|
||
byID[ct.ID] = ct
|
||
}
|
||
if len(byID) != 2 {
|
||
t.Fatalf("projected = %+v, want 2(rdm.trace + rdm.baseline)", tools)
|
||
}
|
||
if ct := byID["rdm.trace"]; ct.Risk != "medium" || ct.Description != "查网" || string(ct.Params) != `{"type":"object"}` {
|
||
t.Fatalf("rdm.trace = %+v", ct)
|
||
}
|
||
if ct := byID["rdm.baseline"]; ct.Risk != "low" || ct.Description != "基线视图" {
|
||
t.Fatalf("rdm.baseline = %+v(risk 缺省 low、description 缺省 title)", ct)
|
||
}
|
||
if _, ok := byID["rdm.hidden"]; ok {
|
||
t.Fatal("未声明 exposeAsTool 的命令不得投影")
|
||
}
|
||
}
|
||
|
||
func TestCommandToolDispatchEnvelope(t *testing.T) {
|
||
b := newToolBus(t)
|
||
// 注册一个带处理器的暴露命令,经 Dispatch 分发验证完整回执链路。
|
||
var spec agentcommand.CommandSpec
|
||
if err := json.Unmarshal([]byte(`{"id":"rdm.echo","title":"回声","exposeAsTool":{"risk":"low"}}`), &spec); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := b.RegisterPluginCommand(spec, func(ctx context.Context, req Request) (any, error) {
|
||
return map[string]any{"echo": req.Params["v"], "actor": req.Actor, "entrance": req.Entrance}, nil
|
||
}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
resp := b.Dispatch(context.Background(), Request{ID: "rdm.echo", Params: map[string]any{"v": 1},
|
||
Entrance: "agent", Actor: "bob", Owner: "bob", ConversationID: "cnv-1"})
|
||
if resp.Receipt.State != "committed" {
|
||
t.Fatalf("receipt = %+v (%s)", resp.Receipt, resp.Receipt.Reason)
|
||
}
|
||
m, _ := resp.Payload.(map[string]any)
|
||
if m["entrance"] != "agent" || m["actor"] != "bob" {
|
||
t.Fatalf("payload = %+v(Entrance=agent 原样入审计)", m)
|
||
}
|
||
}
|
||
|
||
type nopAudit struct{}
|
||
|
||
func (nopAudit) AppendAudit(owner string, env agentcommand.AuditEnvelope) error { return nil }
|
||
|
||
// U3:owner 级注入闸——gate 拒绝的命令不投影(禁用即不注入能力);fail-closed
|
||
// (owner 为空且 gate 判否时不投影)。
|
||
func TestCommandToolsForGate(t *testing.T) {
|
||
bus := New(nil, nil, func() time.Time { return time.Now().UTC() })
|
||
if err := bus.RegisterPluginCommands([]json.RawMessage{
|
||
[]byte(`{"id":"generative-ui.present","title":"生成页面","description":"d","paramsSchema":{"type":"object"},"exposeAsTool":{"risk":"low"}}`),
|
||
[]byte(`{"id":"other.echo","title":"回声","description":"e","paramsSchema":{"type":"object"},"exposeAsTool":{"risk":"low"}}`),
|
||
}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
bus.SetToolGate(func(owner, id string) bool {
|
||
return !strings.HasPrefix(id, "generative-ui.")
|
||
})
|
||
on := bus.CommandToolsFor("alice")
|
||
for _, ct := range on {
|
||
if strings.HasPrefix(ct.ID, "generative-ui.") {
|
||
t.Fatalf("gated command leaked for enabled owner: %+v", ct)
|
||
}
|
||
}
|
||
if len(on) != 1 {
|
||
t.Fatalf("expected only other.echo, got %+v", on)
|
||
}
|
||
if n := len(bus.CommandToolsFor("")); n != 1 {
|
||
t.Fatalf("empty owner must be fail-closed on gated commands, got %d", n)
|
||
}
|
||
}
|