ci / vet-and-test (pull_request) Successful in 42s
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
103 lines
3.7 KiB
Go
103 lines
3.7 KiB
Go
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)
|
||
}
|
||
}
|