agent: extract expandSystemPrompt, 100% coverage for BuildAgentEnv

- Extract expandSystemPrompt(content, cwd) from loadSystemPrompt for
  testability without file I/O
- Add tests: ListTools error, tools populated, TrustedTools injection,
  agent prompt without system prompt, exec dispatch success/error/isError
- Add mockDispatcher and mockTrustedSetter test helpers
This commit is contained in:
Levi Neely 2026-04-21 15:56:35 +02:00
parent d957bda8a0
commit 0b705a1ee7
2 changed files with 137 additions and 1 deletions

View File

@ -131,6 +131,10 @@ func loadSystemPrompt(cwd string) string {
if err != nil {
panic(fmt.Sprintf("loadSystemPrompt: %s: %v", path, err))
}
return expandSystemPrompt(string(data), cwd)
}
func expandSystemPrompt(content, cwd string) string {
if cwd == "" {
cwd, _ = os.Getwd()
}
@ -150,7 +154,7 @@ func loadSystemPrompt(cwd string) string {
return os.Getenv(key)
}
}
return os.Expand(string(data), mapper)
return os.Expand(content, mapper)
}

View File

@ -2269,3 +2269,135 @@ func TestBuildAgentEnv_ExecUnknownTool(t *testing.T) {
t.Errorf("expected unknown tool error; got %v", err)
}
}
// --- mock dispatcher for BuildAgentEnv tests ---
type mockDispatcher struct {
tools []tools.ToolInfo
listErr error
servers map[string]tools.Server
dispatch func(ctx context.Context, server, tool string, args json.RawMessage) (json.RawMessage, error)
}
func (m *mockDispatcher) AddServer(name string, s tools.Server) {
if m.servers == nil {
m.servers = make(map[string]tools.Server)
}
m.servers[name] = s
}
func (m *mockDispatcher) GetServer(name string) (tools.Server, bool) {
s, ok := m.servers[name]
return s, ok
}
func (m *mockDispatcher) ListTools() ([]tools.ToolInfo, error) {
return m.tools, m.listErr
}
func (m *mockDispatcher) Dispatch(ctx context.Context, server, tool string, args json.RawMessage) (json.RawMessage, error) {
if m.dispatch != nil {
return m.dispatch(ctx, server, tool, args)
}
return nil, fmt.Errorf("not implemented")
}
type mockTrustedSetter struct {
tools.Server
trusted []string
}
func (m *mockTrustedSetter) SetTrustedTools(t []string) { m.trusted = t }
func (m *mockTrustedSetter) ListTools() ([]tools.ToolInfo, error) {
return nil, nil
}
func (m *mockTrustedSetter) CallTool(ctx context.Context, tool string, args json.RawMessage) (json.RawMessage, error) {
return nil, nil
}
func TestBuildAgentEnv_ListToolsError(t *testing.T) {
setupCfgDir(t, "")
d := &mockDispatcher{listErr: fmt.Errorf("boom")}
env := BuildAgentEnv(nil, d, t.TempDir())
if len(env.Messages) != 1 || !strings.Contains(env.Messages[0], "boom") {
t.Errorf("Messages = %v", env.Messages)
}
}
func TestBuildAgentEnv_ToolsPopulated(t *testing.T) {
setupCfgDir(t, "")
d := &mockDispatcher{
tools: []tools.ToolInfo{{Server: "s1", Name: "mytool", Description: "desc", InputSchema: json.RawMessage(`{}`)}},
}
env := BuildAgentEnv(nil, d, t.TempDir())
if len(env.tools) != 1 || env.tools[0].Name != "mytool" {
t.Errorf("tools = %+v", env.tools)
}
}
func TestBuildAgentEnv_TrustedTools(t *testing.T) {
setupCfgDir(t, "")
setter := &mockTrustedSetter{}
d := &mockDispatcher{servers: map[string]tools.Server{"execute": setter}}
cfg := &config.Config{TrustedTools: []string{"bash"}}
BuildAgentEnv(cfg, d, t.TempDir())
if len(setter.trusted) != 1 || setter.trusted[0] != "bash" {
t.Errorf("trusted = %v", setter.trusted)
}
}
func TestBuildAgentEnv_AgentPromptNoSystemPrompt(t *testing.T) {
// Empty system prompt file — agentPrompt should be the entire system prompt.
setupCfgDir(t, "")
d := tools.NewDispatcher()
cfg := &config.Config{Prompt: "only agent"}
env := BuildAgentEnv(cfg, d, t.TempDir())
if env.systemPrompt != "only agent" {
t.Errorf("systemPrompt = %q; want %q", env.systemPrompt, "only agent")
}
}
func TestBuildAgentEnv_ExecDispatchSuccess(t *testing.T) {
setupCfgDir(t, "")
d := &mockDispatcher{
tools: []tools.ToolInfo{{Server: "s1", Name: "mytool"}},
dispatch: func(ctx context.Context, server, tool string, args json.RawMessage) (json.RawMessage, error) {
return json.RawMessage(`{"content":[{"type":"text","text":"ok"}]}`), nil
},
}
env := BuildAgentEnv(nil, d, t.TempDir())
result, err := env.exec(context.Background(), "mytool", json.RawMessage(`{}`))
if err != nil {
t.Fatal(err)
}
if result != "ok" {
t.Errorf("result = %q", result)
}
}
func TestBuildAgentEnv_ExecDispatchError(t *testing.T) {
setupCfgDir(t, "")
d := &mockDispatcher{
tools: []tools.ToolInfo{{Server: "s1", Name: "mytool"}},
dispatch: func(ctx context.Context, server, tool string, args json.RawMessage) (json.RawMessage, error) {
return nil, fmt.Errorf("dispatch failed")
},
}
env := BuildAgentEnv(nil, d, t.TempDir())
_, err := env.exec(context.Background(), "mytool", json.RawMessage(`{}`))
if err == nil || !strings.Contains(err.Error(), "dispatch failed") {
t.Errorf("err = %v", err)
}
}
func TestBuildAgentEnv_ExecToolResultIsError(t *testing.T) {
setupCfgDir(t, "")
d := &mockDispatcher{
tools: []tools.ToolInfo{{Server: "s1", Name: "mytool"}},
dispatch: func(ctx context.Context, server, tool string, args json.RawMessage) (json.RawMessage, error) {
return json.RawMessage(`{"isError":true,"content":[{"type":"text","text":"bad thing"}]}`), nil
},
}
env := BuildAgentEnv(nil, d, t.TempDir())
_, err := env.exec(context.Background(), "mytool", json.RawMessage(`{}`))
if err == nil || !strings.Contains(err.Error(), "bad thing") {
t.Errorf("err = %v", err)
}
}