feat(0144c): launcher wiring + adapter al tool-use loop LLM
Schema DeviceMeshConfig en AgentConfig. Adapter ToolsForLLM convierte ToolSpec → tools.Tool transparente al LLM existente. URL via env var override. tools_allowed filter. agent-wsl-lucas blank import en launcher. LLM ve los tools como cualquier otra herramienta. Effects runner ya soporta ActionKindDeviceMesh como fallback. Build + tests verdes.
This commit is contained in:
@@ -40,6 +40,7 @@ import (
|
||||
_ "github.com/enmanuel/agents/agents/wikipedia-bot"
|
||||
_ "github.com/enmanuel/agents/agents/exchange-bot"
|
||||
_ "github.com/enmanuel/agents/agents/reminder-bot"
|
||||
_ "github.com/enmanuel/agents/agents/agent-wsl-lucas"
|
||||
testbot "github.com/enmanuel/agents/agents/test-bot"
|
||||
)
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/enmanuel/agents/internal/config"
|
||||
"github.com/enmanuel/agents/pkg/memory"
|
||||
devicemeshtools "github.com/enmanuel/agents/pkg/tools/devicemesh"
|
||||
shellknowledge "github.com/enmanuel/agents/shell/knowledge"
|
||||
shellmcp "github.com/enmanuel/agents/shell/mcp"
|
||||
shellskills "github.com/enmanuel/agents/shell/skills"
|
||||
@@ -291,9 +292,112 @@ func buildToolRegistry(
|
||||
logger.Debug("registered skills tools")
|
||||
}
|
||||
|
||||
// Device-mesh tools — exposed when the agent's config has a populated
|
||||
// `device_mesh:` block with enabled=true. The builtin catalog (issue 0144
|
||||
// §2.1) is filtered by Mode and then narrowed by ToolsAllowed; each
|
||||
// surviving spec is adapted to a tools.Tool whose Exec routes through
|
||||
// the devicemesh.ToolRegistry (validate → ArgMapping → HTTP dispatch →
|
||||
// ResultMapping). See pkg/tools/devicemesh/adapter.go.
|
||||
if dmReg := buildDeviceMeshRegistry(cfg, logger); dmReg != nil {
|
||||
for _, t := range devicemeshtools.ToolsForLLM(dmReg) {
|
||||
reg.Register(t)
|
||||
}
|
||||
logger.Info("device_mesh tools registered",
|
||||
"host", cfg.DeviceMesh.ResolvedHost(),
|
||||
"mode", normalizeMeshMode(cfg.DeviceMesh.Mode),
|
||||
"count", dmReg.Len(),
|
||||
"names", dmReg.Names(),
|
||||
)
|
||||
}
|
||||
|
||||
return reg
|
||||
}
|
||||
|
||||
// buildDeviceMeshRegistry constructs the per-agent devicemesh.ToolRegistry
|
||||
// from cfg.DeviceMesh and returns it ready to be adapted. Returns nil when
|
||||
// the block is absent, disabled, or yields zero tools so the caller can
|
||||
// skip registration cleanly. Pure(-ish) — only side effect is os.Getenv
|
||||
// for the URL override; the rest is pure data shuffling.
|
||||
func buildDeviceMeshRegistry(cfg *config.AgentConfig, logger *slog.Logger) *devicemeshtools.ToolRegistry {
|
||||
if cfg == nil || cfg.DeviceMesh == nil || !cfg.DeviceMesh.Enabled {
|
||||
return nil
|
||||
}
|
||||
dm := cfg.DeviceMesh
|
||||
|
||||
// Resolve the device_agent URL: env override wins when present and
|
||||
// non-empty; otherwise fall back to the literal URL from YAML. This
|
||||
// keeps endpoints out of git while staying explicit.
|
||||
url := dm.DeviceAgentURL
|
||||
if dm.URLEnv != "" {
|
||||
if v := os.Getenv(dm.URLEnv); v != "" {
|
||||
url = v
|
||||
}
|
||||
}
|
||||
if url == "" {
|
||||
logger.Warn("device_mesh enabled but no URL resolved (neither device_agent_url nor URLEnv)",
|
||||
"url_env", dm.URLEnv,
|
||||
"host", dm.ResolvedHost(),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
client := devicemeshtools.NewClient(url)
|
||||
if t := dm.ResolvedTimeoutSeconds(); t > 0 {
|
||||
client.Timeout = time.Duration(t) * time.Second
|
||||
}
|
||||
|
||||
mode := normalizeMeshMode(dm.Mode)
|
||||
reg := devicemeshtools.NewToolRegistry(client)
|
||||
registered := devicemeshtools.RegisterBuiltins(reg, mode)
|
||||
logger.Debug("device_mesh builtins registered", "mode", mode, "count", len(registered), "names", registered)
|
||||
|
||||
// Narrow by tools_allowed if the config asks for it. The filter is a
|
||||
// pure transform — same Client, fewer specs.
|
||||
if len(dm.ToolsAllowed) > 0 {
|
||||
filtered := devicemeshtools.FilterByAllowed(reg, dm.ToolsAllowed)
|
||||
// Warn on names that the config asked for but the catalog does not
|
||||
// provide — typical drift between template and code after a new
|
||||
// builtin lands.
|
||||
present := make(map[string]bool, len(registered))
|
||||
for _, n := range registered {
|
||||
present[n] = true
|
||||
}
|
||||
for _, n := range dm.ToolsAllowed {
|
||||
if !present[n] {
|
||||
logger.Warn("device_mesh tools_allowed lists unknown tool",
|
||||
"name", n,
|
||||
"mode", mode,
|
||||
)
|
||||
}
|
||||
}
|
||||
reg = filtered
|
||||
}
|
||||
|
||||
if reg.Len() == 0 {
|
||||
logger.Warn("device_mesh registry empty after filter — skipping",
|
||||
"host", dm.ResolvedHost(),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
return reg
|
||||
}
|
||||
|
||||
// normalizeMeshMode maps the YAML "mode" string to the RegistrationMode
|
||||
// enum, defaulting to ModeUser. Pure function — used by both the registry
|
||||
// builder and tests.
|
||||
func normalizeMeshMode(s string) devicemeshtools.RegistrationMode {
|
||||
switch s {
|
||||
case "sudo":
|
||||
return devicemeshtools.ModeSudo
|
||||
case "all":
|
||||
return devicemeshtools.ModeAll
|
||||
case "user", "":
|
||||
return devicemeshtools.ModeUser
|
||||
default:
|
||||
return devicemeshtools.ModeUser
|
||||
}
|
||||
}
|
||||
|
||||
// resolveDataBase returns the base directory for agent runtime data.
|
||||
// Priority: config storage.base_path > $AGENTS_DATA_DIR/<id> > <config-dir>/data
|
||||
func resolveDataBase(cfg *config.AgentConfig) string {
|
||||
|
||||
@@ -171,3 +171,147 @@ func assertToolNotRegistered(t *testing.T, reg interface{ Names() []string }, na
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildToolRegistry_DeviceMeshDisabled(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
|
||||
cfg := &config.AgentConfig{
|
||||
Agent: config.AgentMeta{ID: "test-agent"},
|
||||
DeviceMesh: nil,
|
||||
}
|
||||
roomCtx := &toolmemory.RoomContext{}
|
||||
|
||||
reg := buildToolRegistry(cfg, nil, nil, nil, nil, nil, nil, nil, nil, roomCtx, logger)
|
||||
|
||||
// None of the device_mesh tool names should appear when the block is nil.
|
||||
assertToolNotRegistered(t, reg, "exec")
|
||||
assertToolNotRegistered(t, reg, "shell.eval")
|
||||
assertToolNotRegistered(t, reg, "fs.read")
|
||||
}
|
||||
|
||||
func TestBuildDeviceMeshRegistry_NoURLReturnsNil(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
|
||||
cfg := &config.AgentConfig{
|
||||
Agent: config.AgentMeta{ID: "agent-x"},
|
||||
DeviceMesh: &config.DeviceMeshConfig{
|
||||
Enabled: true,
|
||||
Mode: "user",
|
||||
// no URL, no URLEnv
|
||||
},
|
||||
}
|
||||
if got := buildDeviceMeshRegistry(cfg, logger); got != nil {
|
||||
t.Errorf("expected nil registry when no URL is set, got %d tools", got.Len())
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildDeviceMeshRegistry_URLEnvOverride(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
|
||||
t.Setenv("TEST_DM_URL", "http://10.42.0.99:7474")
|
||||
|
||||
cfg := &config.AgentConfig{
|
||||
Agent: config.AgentMeta{ID: "agent-x"},
|
||||
DeviceMesh: &config.DeviceMeshConfig{
|
||||
Enabled: true,
|
||||
Mode: "user",
|
||||
DeviceAgentURL: "http://stale-url",
|
||||
URLEnv: "TEST_DM_URL",
|
||||
},
|
||||
}
|
||||
reg := buildDeviceMeshRegistry(cfg, logger)
|
||||
if reg == nil {
|
||||
t.Fatalf("expected non-nil registry")
|
||||
}
|
||||
if reg.Client().BaseURL != "http://10.42.0.99:7474" {
|
||||
t.Errorf("URLEnv override failed: got %q", reg.Client().BaseURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildDeviceMeshRegistry_UserModeFiltersApproval(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
|
||||
cfg := &config.AgentConfig{
|
||||
Agent: config.AgentMeta{ID: "agent-x"},
|
||||
DeviceMesh: &config.DeviceMeshConfig{
|
||||
Enabled: true,
|
||||
Mode: "user",
|
||||
DeviceAgentURL: "http://dummy:7474",
|
||||
},
|
||||
}
|
||||
reg := buildDeviceMeshRegistry(cfg, logger)
|
||||
if reg == nil {
|
||||
t.Fatalf("expected non-nil registry")
|
||||
}
|
||||
for _, n := range reg.Names() {
|
||||
// User mode: pkg.install (requires approval) must not be present.
|
||||
if n == "pkg.install" {
|
||||
t.Errorf("user mode leaked approval-only tool: %s", n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildDeviceMeshRegistry_SudoModeKeepsOnlyApproval(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
|
||||
cfg := &config.AgentConfig{
|
||||
Agent: config.AgentMeta{ID: "agent-x-sudo"},
|
||||
DeviceMesh: &config.DeviceMeshConfig{
|
||||
Enabled: true,
|
||||
Mode: "sudo",
|
||||
DeviceAgentURL: "http://dummy:7474",
|
||||
},
|
||||
}
|
||||
reg := buildDeviceMeshRegistry(cfg, logger)
|
||||
if reg == nil {
|
||||
t.Fatalf("expected non-nil registry")
|
||||
}
|
||||
// pkg.install MUST be there in sudo mode.
|
||||
assertToolRegistered(t, reg, "pkg.install")
|
||||
// shell.eval is always registered (special-cased) and promoted to approval.
|
||||
spec, ok := reg.Get("shell.eval")
|
||||
if !ok {
|
||||
t.Fatalf("shell.eval should be registered in sudo mode too")
|
||||
}
|
||||
if !spec.RequiresApproval {
|
||||
t.Errorf("shell.eval in sudo mode should have RequiresApproval=true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildDeviceMeshRegistry_ToolsAllowedNarrows(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
|
||||
cfg := &config.AgentConfig{
|
||||
Agent: config.AgentMeta{ID: "agent-x"},
|
||||
DeviceMesh: &config.DeviceMeshConfig{
|
||||
Enabled: true,
|
||||
Mode: "user",
|
||||
DeviceAgentURL: "http://dummy:7474",
|
||||
ToolsAllowed: []string{"exec", "fs.read", "zzz.unknown"},
|
||||
},
|
||||
}
|
||||
reg := buildDeviceMeshRegistry(cfg, logger)
|
||||
if reg == nil {
|
||||
t.Fatalf("expected non-nil registry")
|
||||
}
|
||||
if reg.Len() != 2 {
|
||||
t.Errorf("expected 2 tools after filter, got %d: %v", reg.Len(), reg.Names())
|
||||
}
|
||||
assertToolRegistered(t, reg, "exec")
|
||||
assertToolRegistered(t, reg, "fs.read")
|
||||
}
|
||||
|
||||
func TestBuildToolRegistry_DeviceMeshAdaptedIntoMainRegistry(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
|
||||
cfg := &config.AgentConfig{
|
||||
Agent: config.AgentMeta{ID: "agent-x"},
|
||||
DeviceMesh: &config.DeviceMeshConfig{
|
||||
Enabled: true,
|
||||
Mode: "user",
|
||||
DeviceAgentURL: "http://dummy:7474",
|
||||
ToolsAllowed: []string{"exec"},
|
||||
},
|
||||
}
|
||||
roomCtx := &toolmemory.RoomContext{}
|
||||
|
||||
reg := buildToolRegistry(cfg, nil, nil, nil, nil, nil, nil, nil, nil, roomCtx, logger)
|
||||
|
||||
// The "exec" tool should appear in the main agent tool registry, alongside
|
||||
// the always-on tools, ready for the LLM tool-use loop to invoke.
|
||||
assertToolRegistered(t, reg, "exec")
|
||||
assertToolRegistered(t, reg, "current_time")
|
||||
}
|
||||
|
||||
+16
-2
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/enmanuel/agents/pkg/memory"
|
||||
"github.com/enmanuel/agents/pkg/personality"
|
||||
"github.com/enmanuel/agents/pkg/sanitize"
|
||||
devicemeshtools "github.com/enmanuel/agents/pkg/tools/devicemesh"
|
||||
"github.com/enmanuel/agents/shell/audit"
|
||||
"github.com/enmanuel/agents/shell/bus"
|
||||
shellcron "github.com/enmanuel/agents/shell/cron"
|
||||
@@ -140,8 +141,21 @@ func New(cfg *config.AgentConfig, rules []decision.Rule, agentACL acl.ACL, logge
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Effects runner
|
||||
runner := effects.NewRunner(matrixClient, sshExec, logger)
|
||||
// Effects runner — wire the device_mesh registry when the agent config
|
||||
// enables it, so decision.ActionKindDeviceMesh actions dispatched by the
|
||||
// rules layer can reach the remote device_agent. The LLM tool-use loop
|
||||
// goes through tools.Registry (see buildToolRegistry below), but the
|
||||
// Action-emitting path needs its own handle to the same registry.
|
||||
var dmRegForRunner *devicemeshtools.ToolRegistry
|
||||
if cfg.DeviceMesh != nil && cfg.DeviceMesh.Enabled {
|
||||
dmRegForRunner = buildDeviceMeshRegistry(cfg, logger)
|
||||
}
|
||||
var runner *effects.Runner
|
||||
if dmRegForRunner != nil {
|
||||
runner = effects.NewRunnerWithDeviceMesh(matrixClient, sshExec, dmRegForRunner, logger)
|
||||
} else {
|
||||
runner = effects.NewRunner(matrixClient, sshExec, logger)
|
||||
}
|
||||
|
||||
// Resolve base data path for this agent
|
||||
dataBase := resolveDataBase(cfg)
|
||||
|
||||
@@ -17,12 +17,93 @@ type AgentConfig struct {
|
||||
Memory MemoryCfg `yaml:"memory"`
|
||||
Skills SkillsCfg `yaml:"skills"`
|
||||
|
||||
// DeviceMesh holds the optional device-mesh block. When nil the agent has
|
||||
// no device_mesh tools; when set and Enabled the runtime constructs a
|
||||
// devicemesh.Client + ToolRegistry and registers the builtin tools (filtered
|
||||
// by ToolsAllowed). See issue 0144 §6.1 + .claude/rules/cpp_apps.md.
|
||||
DeviceMesh *DeviceMeshConfig `yaml:"device_mesh,omitempty"`
|
||||
|
||||
// ConfigDir is the directory containing the config file. Set by the loader
|
||||
// at load time, not from YAML. Used to resolve relative paths like
|
||||
// system_prompt_file correctly regardless of where the agent lives.
|
||||
ConfigDir string `yaml:"-"`
|
||||
}
|
||||
|
||||
// DeviceMeshConfig is the optional device-mesh block on the agent config.
|
||||
// When DeviceMesh is non-nil and Enabled is true, the launcher builds a
|
||||
// devicemesh.Client + ToolRegistry, registers builtin tools filtered by
|
||||
// Mode (user|sudo), optionally narrows them via ToolsAllowed, and exposes
|
||||
// each tool to the LLM tool-use loop via the standard tool registry.
|
||||
type DeviceMeshConfig struct {
|
||||
// Enabled gates the whole block. False keeps it inert even when present.
|
||||
Enabled bool `yaml:"enabled"`
|
||||
|
||||
// Host identifies the target device for log/audit context. Matches
|
||||
// device_id from the manifest (ex "home-wsl", "aurgi-pc").
|
||||
Host string `yaml:"host"`
|
||||
|
||||
// DeviceID is an alias for Host. Templates use device_id; keep both for
|
||||
// compatibility. When both are set Host wins.
|
||||
DeviceID string `yaml:"device_id,omitempty"`
|
||||
|
||||
// Mode controls which subset of the builtin catalog gets registered.
|
||||
// "user" → non-approval tools. "sudo" → approval-gated tools (shell.eval
|
||||
// promoted to requires_approval). Empty defaults to "user".
|
||||
Mode string `yaml:"mode"`
|
||||
|
||||
// DeviceAgentURL is the http://host:port URL of the remote device_agent.
|
||||
// May be empty when URLEnv is set.
|
||||
DeviceAgentURL string `yaml:"device_agent_url"`
|
||||
|
||||
// URLEnv allows the agent_url to be supplied at runtime via env var
|
||||
// (ex "AGENT_HOME_WSL_DEVICE_MESH_URL"). When non-empty the runtime reads
|
||||
// the env var; if both are set, the env var wins when non-empty. This
|
||||
// keeps device URLs out of the YAML/git history.
|
||||
URLEnv string `yaml:"device_agent_url_env,omitempty"`
|
||||
|
||||
// ManifestID is metadata for log/audit context. The device_agent enforces
|
||||
// the actual manifest binding. Empty allowed.
|
||||
ManifestID string `yaml:"manifest_id,omitempty"`
|
||||
|
||||
// ToolsAllowed is a whitelist applied AFTER RegisterBuiltins. Empty means
|
||||
// "keep all tools the mode-filter accepted". Names that do not match any
|
||||
// registered tool are logged and ignored.
|
||||
ToolsAllowed []string `yaml:"tools_allowed,omitempty"`
|
||||
|
||||
// TimeoutSeconds overrides the per-call HTTP timeout. 0 → DefaultTimeout
|
||||
// of the devicemesh client (30s).
|
||||
TimeoutSeconds int `yaml:"timeout_seconds,omitempty"`
|
||||
|
||||
// ClientTimeoutS is an alias for TimeoutSeconds. Templates use
|
||||
// client_timeout_s; we accept both. When both set, ClientTimeoutS wins
|
||||
// when non-zero.
|
||||
ClientTimeoutS int `yaml:"client_timeout_s,omitempty"`
|
||||
}
|
||||
|
||||
// ResolvedHost returns Host if non-empty, otherwise DeviceID. Used by the
|
||||
// runtime to log audit context without caring which key the YAML used.
|
||||
func (d *DeviceMeshConfig) ResolvedHost() string {
|
||||
if d == nil {
|
||||
return ""
|
||||
}
|
||||
if d.Host != "" {
|
||||
return d.Host
|
||||
}
|
||||
return d.DeviceID
|
||||
}
|
||||
|
||||
// ResolvedTimeoutSeconds returns the first non-zero of TimeoutSeconds and
|
||||
// ClientTimeoutS. 0 means "use devicemesh defaults".
|
||||
func (d *DeviceMeshConfig) ResolvedTimeoutSeconds() int {
|
||||
if d == nil {
|
||||
return 0
|
||||
}
|
||||
if d.TimeoutSeconds > 0 {
|
||||
return d.TimeoutSeconds
|
||||
}
|
||||
return d.ClientTimeoutS
|
||||
}
|
||||
|
||||
// ── Identity ──────────────────────────────────────────────────────────────
|
||||
|
||||
type AgentMeta struct {
|
||||
|
||||
@@ -209,3 +209,114 @@ skills:
|
||||
t.Error("security.sanitize.enabled should be true")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeviceMeshConfig_Parse verifies that the device_mesh block parses into
|
||||
// the expected DeviceMeshConfig pointer with both YAML key variants (host vs
|
||||
// device_id, timeout_seconds vs client_timeout_s, tools_allowed list).
|
||||
func TestDeviceMeshConfig_Parse(t *testing.T) {
|
||||
const yamlBody = `
|
||||
agent:
|
||||
id: agent-home-wsl
|
||||
name: home wsl
|
||||
enabled: true
|
||||
matrix:
|
||||
homeserver: "https://matrix.example.com"
|
||||
user_id: "@agent-home-wsl:matrix.example.com"
|
||||
llm:
|
||||
primary:
|
||||
provider: anthropic
|
||||
model: claude-sonnet
|
||||
device_mesh:
|
||||
enabled: true
|
||||
device_id: home-wsl
|
||||
mode: user
|
||||
device_agent_url: "http://10.42.0.10:7474"
|
||||
device_agent_url_env: AGENT_HOME_WSL_DEVICE_MESH_URL
|
||||
manifest_id: manifest_home-wsl_v1
|
||||
client_timeout_s: 60
|
||||
tools_allowed:
|
||||
- exec
|
||||
- fs.read
|
||||
- fs.list
|
||||
`
|
||||
var cfg AgentConfig
|
||||
if err := yaml.Unmarshal([]byte(yamlBody), &cfg); err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
if cfg.DeviceMesh == nil {
|
||||
t.Fatalf("expected DeviceMesh to be non-nil")
|
||||
}
|
||||
dm := cfg.DeviceMesh
|
||||
if !dm.Enabled {
|
||||
t.Error("enabled should be true")
|
||||
}
|
||||
if dm.DeviceID != "home-wsl" {
|
||||
t.Errorf("device_id: got %q", dm.DeviceID)
|
||||
}
|
||||
if dm.ResolvedHost() != "home-wsl" {
|
||||
t.Errorf("ResolvedHost(): got %q", dm.ResolvedHost())
|
||||
}
|
||||
if dm.Mode != "user" {
|
||||
t.Errorf("mode: got %q", dm.Mode)
|
||||
}
|
||||
if dm.DeviceAgentURL != "http://10.42.0.10:7474" {
|
||||
t.Errorf("device_agent_url: got %q", dm.DeviceAgentURL)
|
||||
}
|
||||
if dm.URLEnv != "AGENT_HOME_WSL_DEVICE_MESH_URL" {
|
||||
t.Errorf("device_agent_url_env: got %q", dm.URLEnv)
|
||||
}
|
||||
if dm.ManifestID != "manifest_home-wsl_v1" {
|
||||
t.Errorf("manifest_id: got %q", dm.ManifestID)
|
||||
}
|
||||
if dm.ResolvedTimeoutSeconds() != 60 {
|
||||
t.Errorf("ResolvedTimeoutSeconds(): got %d", dm.ResolvedTimeoutSeconds())
|
||||
}
|
||||
if len(dm.ToolsAllowed) != 3 {
|
||||
t.Errorf("tools_allowed: got %d entries", len(dm.ToolsAllowed))
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeviceMeshConfig_Absent ensures the field stays nil when the block is
|
||||
// not present in YAML — the runtime relies on the nil-check to short-circuit.
|
||||
func TestDeviceMeshConfig_Absent(t *testing.T) {
|
||||
const yamlBody = `
|
||||
agent:
|
||||
id: plain-bot
|
||||
enabled: true
|
||||
matrix:
|
||||
homeserver: "https://matrix.example.com"
|
||||
user_id: "@plain-bot:matrix.example.com"
|
||||
llm:
|
||||
primary:
|
||||
provider: openai
|
||||
model: gpt-4o
|
||||
`
|
||||
var cfg AgentConfig
|
||||
if err := yaml.Unmarshal([]byte(yamlBody), &cfg); err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
if cfg.DeviceMesh != nil {
|
||||
t.Errorf("expected nil DeviceMesh, got %+v", cfg.DeviceMesh)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeviceMeshConfig_TimeoutFallback verifies that timeout_seconds is used
|
||||
// when client_timeout_s is absent.
|
||||
func TestDeviceMeshConfig_TimeoutFallback(t *testing.T) {
|
||||
dm := &DeviceMeshConfig{TimeoutSeconds: 45}
|
||||
if got := dm.ResolvedTimeoutSeconds(); got != 45 {
|
||||
t.Errorf("expected 45, got %d", got)
|
||||
}
|
||||
dm2 := &DeviceMeshConfig{ClientTimeoutS: 90}
|
||||
if got := dm2.ResolvedTimeoutSeconds(); got != 90 {
|
||||
t.Errorf("expected 90, got %d", got)
|
||||
}
|
||||
// TimeoutSeconds wins when both set.
|
||||
dm3 := &DeviceMeshConfig{TimeoutSeconds: 30, ClientTimeoutS: 60}
|
||||
if got := dm3.ResolvedTimeoutSeconds(); got != 30 {
|
||||
t.Errorf("expected 30, got %d", got)
|
||||
}
|
||||
if (*DeviceMeshConfig)(nil).ResolvedTimeoutSeconds() != 0 {
|
||||
t.Errorf("nil receiver should return 0")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user