Files
Agent-System-Backend/internal/cmdbus/command_tools_test.go
T
weichen 367dc207b3
ci / vet-and-test (pull_request) Successful in 42s
Add the generative-ui plugin, capability gating, and surface projection
Why: Generative UI U3 - the agent needs a real submission path and the
next turn needs to see prior pages: a built-in in-process plugin
registers generative-ui.present (exposeAsTool) carrying the catalog
(prompt=validation single source) with a schema-repair strategy
measured against real MiniMax-M3 deviations; the command bus gains an
owner-scoped tool gate so a disabled plugin stops injecting its tool
(execution guard remains in surfacesvc); ctxasm projects saved surfaces
back into context so the model can revise existing structure; doc-refs
are authorized against the owner's real artifacts.

Evidence: unit suites green (genui guards/epoch/doc-ref, gate fail-closed,
projection content); full go test ./... green; cmdbus/ctxasm also green
under AGENT_PG_TEST_DSN; real-model closed loop (env-gated
TestRealModelSurfaceLoop) passes: task->spec->save->projection->revise
with revision 1->2 and structure change, CNY 0.08 total.

Impact: New internal/genui package; additive cmdbus/ctxasm/runmgr/
runtime hooks; no storage or contract changes (U2 layer untouched).

Refs: Documents proposal r2 U3; Core PR #25/#26; Backend #143; Dev task
feature/generative-ui-u3
2026-10-11 10:38:15 +00:00

103 lines
3.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package cmdbus
// EXT-14:Bus.CommandTools 只投影带 ExposeAsTool 的已启用命令;risk 缺省 low、
// description 缺省取 title;未暴露/未启用的命令不出现。
import (
"context"
"encoding/json"
"strings"
"testing"
"time"
agentcommand "agent-command"
)
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)
}
}