Files
Agent-System-Backend/internal/cmdbus/cmdbus_test.go
T
weichen f1302ed5b3 Enable canonical versioned Go module consumption
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
2026-10-13 15:47:16 +00:00

283 lines
11 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
import (
"context"
"encoding/json"
"errors"
"strings"
"sync"
"testing"
"time"
agentcommand "git-ssh.u1680832.nyat.app/weichen/Agent-Command.git"
"git-ssh.u1680832.nyat.app/weichen/Agent-System-Core.git"
)
// memAudit / memCards 测试替身。
type memAudit struct {
mu sync.Mutex
logs []agentcommand.AuditEnvelope
fail bool
}
func (m *memAudit) AppendAudit(owner string, env agentcommand.AuditEnvelope) error {
if m.fail {
return errors.New("audit down")
}
m.mu.Lock()
defer m.mu.Unlock()
m.logs = append(m.logs, env)
return nil
}
type memCards struct{ created []core.ActivityCard }
func (m *memCards) Create(owner string, c core.ActivityCard, ref string) (core.ActivityCard, error) {
c.ID = core.ID("card_x1")
m.created = append(m.created, c)
return c, nil
}
func (m *memCards) SetStatus(owner, id string, st core.CardStatus, tone, note string) (core.ActivityCard, error) {
return core.ActivityCard{ID: core.ID(id), Status: st}, nil
}
func newTestBus(a AuditStore, c CardPort) *Bus {
b := New(a, c, func() time.Time { return time.Unix(1700000000, 0).UTC() })
_ = b.RegisterBuiltin(agentcommand.CommandSpec{ID: "demo.ping", Title: "Ping"}, "/ping",
func(ctx context.Context, req Request) (any, error) { return map[string]any{"pong": true}, nil })
_ = b.RegisterBuiltin(agentcommand.CommandSpec{
ID: "demo.risky", Title: "危险演示", Danger: agentcommand.DangerHigh,
Description: "高危命令演示",
ParamsSchema: json.RawMessage(`{"type":"object","required":["target"],"properties":{"target":{"type":"string"}}}`),
}, "/risky", func(ctx context.Context, req Request) (any, error) {
t, _ := req.Params["target"].(string)
if t == "boom" {
return nil, errors.New("handler rejected")
}
return map[string]any{"did": t}, nil
})
return b
}
func TestDispatchFourEntrancesSameAudit(t *testing.T) {
a := &memAudit{}
b := newTestBus(a, &memCards{})
for _, ent := range []string{"button", "slash", "chat", "card", "api", "weird-unknown"} {
resp := b.Dispatch(context.Background(), Request{ID: "demo.ping", Entrance: ent})
if resp.Receipt.State != agentcommand.StateCommitted {
t.Fatalf("entrance %s: state=%s reason=%s", ent, resp.Receipt.State, resp.Receipt.Reason)
}
}
if len(a.logs) != 6 {
t.Fatalf("audit logs = %d, want 6", len(a.logs))
}
want := map[string]string{"button": "button", "slash": "slash", "chat": "chat", "card": "card", "api": "api", "weird-unknown": "api"}
for i, env := range a.logs {
if env.Source != agentcommand.SourceBuiltin {
t.Fatalf("log %d source = %s", i, env.Source)
}
}
_ = want
// 入口归一:未知名归 api
if got := a.logs[5].Entrance; got != "api" {
t.Fatalf("unknown entrance canonicalized to %s, want api", got)
}
if a.logs[0].OperationID == "" {
t.Fatal("committed receipt must carry operationID")
}
}
func TestDispatchUnknownAndBadParams(t *testing.T) {
a := &memAudit{}
b := newTestBus(a, &memCards{})
resp := b.Dispatch(context.Background(), Request{ID: "nope.x"})
if resp.Receipt.State != agentcommand.StateRejected || !strings.Contains(resp.Receipt.Reason, "unknown") {
t.Fatalf("unknown: %+v", resp.Receipt)
}
resp = b.Dispatch(context.Background(), Request{ID: "demo.risky", Confirmed: true, Params: map[string]any{}})
if resp.Receipt.State != agentcommand.StateRejected || !strings.Contains(resp.Receipt.Reason, "required param target") {
t.Fatalf("missing required: %+v", resp.Receipt)
}
}
func TestHighDangerConfirmFlow(t *testing.T) {
a := &memAudit{}
cards := &memCards{}
b := newTestBus(a, cards)
// 未确认:创建内联确认卡,回执 accepted + operationID=卡 ID。
resp := b.Dispatch(context.Background(), Request{ID: "demo.risky", Params: map[string]any{"target": "prod"}, Entrance: "button", ConversationID: "cnv_1", AnchorMessageID: "msg_1"})
if resp.Receipt.State != agentcommand.StateAccepted || resp.Receipt.OperationID != "card_x1" {
t.Fatalf("pre-confirm: %+v", resp.Receipt)
}
if resp.Card == nil || resp.Card.Kind != core.CardKindConfirm || resp.Card.Status != core.CardActionRequired {
t.Fatalf("confirm card not created properly: %+v", resp.Card)
}
if resp.Card.ConversationID != "cnv_1" || resp.Card.AnchorMessageID == "" {
t.Fatalf("card anchoring missing: %+v", resp.Card)
}
// 确认:执行挂起请求,committed。
resp2 := b.Confirm("owner", "card_x1", "confirm")
if resp2.Receipt.State != agentcommand.StateCommitted {
t.Fatalf("confirm: %+v", resp2.Receipt)
}
if m, _ := resp2.Payload.(map[string]any); m == nil || m["did"] != "prod" {
t.Fatalf("confirm payload: %+v", resp2.Payload)
}
// 二次确认(已消费):rejected expired。
resp3 := b.Confirm("owner", "card_x1", "confirm")
if resp3.Receipt.State != agentcommand.StateRejected || !strings.Contains(resp3.Receipt.Reason, "expired") {
t.Fatalf("re-confirm: %+v", resp3.Receipt)
}
// 审计链:accepted(button) → committed(card);过期确认未触碰命令,不产生审计。
if len(a.logs) != 2 {
t.Fatalf("audit logs = %d, want 2", len(a.logs))
}
if a.logs[0].State != agentcommand.StateAccepted || a.logs[0].Entrance != "button" ||
a.logs[1].State != agentcommand.StateCommitted || a.logs[1].Entrance != "card" {
t.Fatalf("audit chain wrong: %+v", a.logs)
}
}
func TestHighDangerCancelAndHandlerReject(t *testing.T) {
a := &memAudit{}
b := newTestBus(a, &memCards{})
b.Dispatch(context.Background(), Request{ID: "demo.risky", Params: map[string]any{"target": "x"}, ConversationID: "cnv_1"})
if r := b.Confirm("o", "card_x1", "cancel"); r.Receipt.State != agentcommand.StateRejected || !strings.Contains(r.Receipt.Reason, "cancelled") {
t.Fatalf("cancel: %+v", r.Receipt)
}
// handler 拒绝路径(确认后执行失败)→ rejected + 理由。
b.Dispatch(context.Background(), Request{ID: "demo.risky", Params: map[string]any{"target": "boom"}, ConversationID: "cnv_1"})
r := b.Confirm("o", "card_x1", "confirm")
if r.Receipt.State != agentcommand.StateRejected || !strings.Contains(r.Receipt.Reason, "handler rejected") {
t.Fatalf("handler reject: %+v", r.Receipt)
}
}
func TestHighDangerWithoutCardPortRejected(t *testing.T) {
b := newTestBus(&memAudit{}, nil)
resp := b.Dispatch(context.Background(), Request{ID: "demo.risky", Params: map[string]any{"target": "x"}, ConversationID: "cnv_1"})
if resp.Receipt.State != agentcommand.StateRejected || !strings.Contains(resp.Receipt.Reason, "conversation context") {
t.Fatalf("no card port: %+v", resp.Receipt)
}
}
func TestAuditUnavailableYieldsUnknown(t *testing.T) {
a := &memAudit{fail: true}
b := newTestBus(a, &memCards{})
resp := b.Dispatch(context.Background(), Request{ID: "demo.ping"})
if resp.Receipt.State != agentcommand.StateUnknown || !strings.Contains(resp.Receipt.Reason, "audit unavailable") {
t.Fatalf("audit down: %+v", resp.Receipt)
}
}
func TestPluginCommandsAndSlash(t *testing.T) {
b := newTestBus(&memAudit{}, &memCards{})
if err := b.RegisterPluginCommands([]json.RawMessage{json.RawMessage(`{"id":"plug.do","title":"插件命令"}`)}); err != nil {
t.Fatal(err)
}
if err := b.RegisterPluginCommands([]json.RawMessage{json.RawMessage(`{"id":"bad id","title":"x"}`)}); err == nil {
t.Fatal("invalid plugin spec must be rejected")
}
if id, ok := b.ResolveSlash("/risky"); !ok || id != "demo.risky" {
t.Fatalf("slash resolve: %s %v", id, ok)
}
if _, ok := b.ResolveSlash("/nope"); ok {
t.Fatal("unknown slash must not resolve")
}
if !b.HasCommand("plug.do") || b.HasCommand("plug.missing") {
t.Fatal("HasCommand wrong")
}
}
func TestValidateParamsScalars(t *testing.T) {
schema := json.RawMessage(`{"type":"object","required":["n"],"properties":{"n":{"type":"number"},"s":{"type":"string"},"b":{"type":"boolean"},"i":{"type":"integer"}}}`)
if err := validateParams(schema, map[string]any{"n": 1.5, "s": "x", "b": true, "i": 3}); err != nil {
t.Fatalf("valid params rejected: %v", err)
}
if err := validateParams(schema, map[string]any{}); err == nil || !strings.Contains(err.Error(), "required") {
t.Fatalf("missing required not caught: %v", err)
}
if err := validateParams(schema, map[string]any{"n": "not-a-number"}); err == nil {
t.Fatal("type mismatch not caught")
}
if err := validateParams(schema, map[string]any{"n": 1, "i": 2.5}); err == nil {
t.Fatal("integer fraction not caught")
}
if err := validateParams(nil, map[string]any{"any": 1}); err != nil {
t.Fatalf("nil schema must pass: %v", err)
}
}
// ---- EXT-1:进程内插件命令处理器 ----
func TestRegisterPluginCommandDispatch(t *testing.T) {
a := &memAudit{}
b := newTestBus(a, &memCards{})
err := b.RegisterPluginCommand(agentcommand.CommandSpec{ID: "rdm.ingest", Title: "入库"}, func(ctx context.Context, req Request) (any, error) {
return map[string]any{"echo": req.Params["ref"], "owner": req.Owner}, nil
})
if err != nil {
t.Fatal(err)
}
resp := b.Dispatch(context.Background(), Request{ID: "rdm.ingest", Params: map[string]any{"ref": "doc-1"}, Owner: "alice"})
if resp.Receipt.State != agentcommand.StateCommitted {
t.Fatalf("dispatch: %+v", resp.Receipt)
}
m, _ := resp.Payload.(map[string]any)
if m["echo"] != "doc-1" || m["owner"] != "alice" {
t.Fatalf("payload: %+v", resp.Payload)
}
if len(a.logs) != 1 || a.logs[0].Source != agentcommand.SourcePlugin {
t.Fatalf("audit: %+v", a.logs)
}
// 重复注册同一 ID:fail-closed。
if err := b.RegisterPluginCommand(agentcommand.CommandSpec{ID: "rdm.ingest", Title: "重复"}, nil); err == nil {
t.Fatal("duplicate plugin command must be rejected")
}
}
func TestPluginCommandWithoutHandlerRejected(t *testing.T) {
b := newTestBus(&memAudit{}, &memCards{})
// 声明式路径只登记规格(manifest provides.commands)。
if err := b.RegisterPluginCommands([]json.RawMessage{json.RawMessage(`{"id":"plug.only","title":"仅规格"}`)}); err != nil {
t.Fatal(err)
}
resp := b.Dispatch(context.Background(), Request{ID: "plug.only"})
if resp.Receipt.State != agentcommand.StateRejected || !strings.Contains(resp.Receipt.Reason, "no host handler bound") {
t.Fatalf("unbound: %+v", resp.Receipt)
}
}
func TestHighDangerPluginCommandConfirmsFirst(t *testing.T) {
a := &memAudit{}
cards := &memCards{}
b := newTestBus(a, cards)
executed := 0
err := b.RegisterPluginCommand(agentcommand.CommandSpec{
ID: "rdm.purge", Title: "清库", Danger: agentcommand.DangerHigh,
}, func(ctx context.Context, req Request) (any, error) {
executed++
return "purged", nil
})
if err != nil {
t.Fatal(err)
}
// 未确认:先出内联确认卡,处理器不得执行。
resp := b.Dispatch(context.Background(), Request{ID: "rdm.purge", ConversationID: "cnv_9", Owner: "alice"})
if resp.Receipt.State != agentcommand.StateAccepted || resp.Card == nil || resp.Card.Kind != core.CardKindConfirm {
t.Fatalf("pre-confirm: %+v card=%+v", resp.Receipt, resp.Card)
}
if executed != 0 {
t.Fatal("handler must not run before confirm")
}
// 确认后执行 committed。
resp2 := b.Confirm("alice", "card_x1", "confirm")
if resp2.Receipt.State != agentcommand.StateCommitted || resp2.Payload != "purged" || executed != 1 {
t.Fatalf("confirm: %+v executed=%d", resp2.Receipt, executed)
}
}