mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
180 lines
4.4 KiB
Go
180 lines
4.4 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/sipeed/picoclaw/pkg/bus"
|
|
"github.com/sipeed/picoclaw/pkg/config"
|
|
)
|
|
|
|
type builtinAutoHookConfig struct {
|
|
Model string `json:"model"`
|
|
Suffix string `json:"suffix"`
|
|
}
|
|
|
|
type builtinAutoHook struct {
|
|
model string
|
|
suffix string
|
|
}
|
|
|
|
func (h *builtinAutoHook) BeforeLLM(
|
|
ctx context.Context,
|
|
req *LLMHookRequest,
|
|
) (*LLMHookRequest, HookDecision, error) {
|
|
next := req.Clone()
|
|
next.Model = h.model
|
|
return next, HookDecision{Action: HookActionModify}, nil
|
|
}
|
|
|
|
func (h *builtinAutoHook) AfterLLM(
|
|
ctx context.Context,
|
|
resp *LLMHookResponse,
|
|
) (*LLMHookResponse, HookDecision, error) {
|
|
next := resp.Clone()
|
|
if next.Response != nil {
|
|
next.Response.Content += h.suffix
|
|
}
|
|
return next, HookDecision{Action: HookActionModify}, nil
|
|
}
|
|
|
|
func newConfiguredHookLoop(t *testing.T, provider *llmHookTestProvider, hooks config.HooksConfig) *AgentLoop {
|
|
t.Helper()
|
|
|
|
cfg := &config.Config{
|
|
Agents: config.AgentsConfig{
|
|
Defaults: config.AgentDefaults{
|
|
Workspace: t.TempDir(),
|
|
ModelName: "test-model",
|
|
MaxTokens: 4096,
|
|
MaxToolIterations: 10,
|
|
},
|
|
},
|
|
Hooks: hooks,
|
|
}
|
|
|
|
return NewAgentLoop(cfg, bus.NewMessageBus(), provider)
|
|
}
|
|
|
|
func TestAgentLoop_ProcessDirectWithChannel_AutoMountsBuiltinHook(t *testing.T) {
|
|
const hookName = "test-auto-builtin-hook"
|
|
|
|
if err := RegisterBuiltinHook(hookName, func(
|
|
ctx context.Context,
|
|
spec config.BuiltinHookConfig,
|
|
) (any, error) {
|
|
var hookCfg builtinAutoHookConfig
|
|
if len(spec.Config) > 0 {
|
|
if err := json.Unmarshal(spec.Config, &hookCfg); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return &builtinAutoHook{
|
|
model: hookCfg.Model,
|
|
suffix: hookCfg.Suffix,
|
|
}, nil
|
|
}); err != nil {
|
|
t.Fatalf("RegisterBuiltinHook failed: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
unregisterBuiltinHook(hookName)
|
|
})
|
|
|
|
rawCfg, err := json.Marshal(builtinAutoHookConfig{
|
|
Model: "builtin-model",
|
|
Suffix: "|builtin",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("json.Marshal failed: %v", err)
|
|
}
|
|
|
|
provider := &llmHookTestProvider{}
|
|
al := newConfiguredHookLoop(t, provider, config.HooksConfig{
|
|
Enabled: true,
|
|
Builtins: map[string]config.BuiltinHookConfig{
|
|
hookName: {
|
|
Enabled: true,
|
|
Config: rawCfg,
|
|
},
|
|
},
|
|
})
|
|
defer al.Close()
|
|
|
|
resp, err := al.ProcessDirectWithChannel(context.Background(), "hello", "session-1", "cli", "direct")
|
|
if err != nil {
|
|
t.Fatalf("ProcessDirectWithChannel failed: %v", err)
|
|
}
|
|
if resp != "provider content|builtin" {
|
|
t.Fatalf("expected builtin-hooked content, got %q", resp)
|
|
}
|
|
|
|
provider.mu.Lock()
|
|
lastModel := provider.lastModel
|
|
provider.mu.Unlock()
|
|
if lastModel != "builtin-model" {
|
|
t.Fatalf("expected builtin model, got %q", lastModel)
|
|
}
|
|
}
|
|
|
|
func TestAgentLoop_ProcessDirectWithChannel_AutoMountsProcessHook(t *testing.T) {
|
|
provider := &llmHookTestProvider{}
|
|
eventLog := filepath.Join(t.TempDir(), "events.log")
|
|
|
|
al := newConfiguredHookLoop(t, provider, config.HooksConfig{
|
|
Enabled: true,
|
|
Processes: map[string]config.ProcessHookConfig{
|
|
"ipc-auto": {
|
|
Enabled: true,
|
|
Command: processHookHelperCommand(),
|
|
Env: map[string]string{
|
|
"PICOCLAW_HOOK_HELPER": "1",
|
|
"PICOCLAW_HOOK_MODE": "rewrite",
|
|
"PICOCLAW_HOOK_EVENT_LOG": eventLog,
|
|
},
|
|
Observe: []string{"turn_end"},
|
|
Intercept: []string{"before_llm", "after_llm"},
|
|
},
|
|
},
|
|
})
|
|
defer al.Close()
|
|
|
|
resp, err := al.ProcessDirectWithChannel(context.Background(), "hello", "session-1", "cli", "direct")
|
|
if err != nil {
|
|
t.Fatalf("ProcessDirectWithChannel failed: %v", err)
|
|
}
|
|
if resp != "provider content|ipc" {
|
|
t.Fatalf("expected process-hooked content, got %q", resp)
|
|
}
|
|
|
|
provider.mu.Lock()
|
|
lastModel := provider.lastModel
|
|
provider.mu.Unlock()
|
|
if lastModel != "process-model" {
|
|
t.Fatalf("expected process model, got %q", lastModel)
|
|
}
|
|
|
|
waitForFileContains(t, eventLog, "turn_end")
|
|
}
|
|
|
|
func TestAgentLoop_ProcessDirectWithChannel_InvalidConfiguredHookFails(t *testing.T) {
|
|
provider := &llmHookTestProvider{}
|
|
al := newConfiguredHookLoop(t, provider, config.HooksConfig{
|
|
Enabled: true,
|
|
Processes: map[string]config.ProcessHookConfig{
|
|
"bad-hook": {
|
|
Enabled: true,
|
|
Command: processHookHelperCommand(),
|
|
Intercept: []string{"not_supported"},
|
|
},
|
|
},
|
|
})
|
|
defer al.Close()
|
|
|
|
_, err := al.ProcessDirectWithChannel(context.Background(), "hello", "session-1", "cli", "direct")
|
|
if err == nil {
|
|
t.Fatal("expected invalid configured hook error")
|
|
}
|
|
}
|