Expand the docs/ directory with 109 markdown files covering: Getting Started: - Installation guide (binary, source, Docker, Termux) - Quick start and configuration basics - First chat tutorial User Guide: - Complete CLI reference (9 command docs) - 11 channel setup guides (Telegram, Discord, Slack, etc.) - 9 LLM provider guides (OpenRouter, Zhipu, Anthropic, etc.) - 8 tool documentation files - Skills system documentation - Workspace customization guides - Advanced features (multi-agent, routing, security, etc.) Developer Guide: - Architecture and data flow - Building and testing - Contributing guidelines - Extension guides for tools, providers, channels, skills - API reference for all interfaces Deployment: - Docker and systemd guides - Termux for Android - SBC guides (Raspberry Pi, LicheeRV Nano, MaixCAM) - Production security checklist Operations: - Health endpoints and monitoring - Logging and device monitoring - Troubleshooting guide Tutorials: - Step-by-step guides for common use cases - Hardware control tutorial Community: - Project roadmap - Support channels - Contributing roles Includes SUMMARY.md for mdBook/GitBook navigation. Generated with [Z.ai](https://z.ai/subscribe?ic=JGTYCX7ZO7) Co-Authored-By: Z.ai GLM-5
7.7 KiB
7.7 KiB
Running Tests
This guide explains how to run and write tests for PicoClaw.
Running Tests
Run All Tests
Run the complete test suite:
make test
Or using Go directly:
go test ./pkg/...
Run Tests for a Package
Test a specific package:
go test ./pkg/agent/... -v
go test ./pkg/providers/... -v
go test ./pkg/tools/... -v
Run Specific Tests
Run tests matching a pattern:
go test ./pkg/providers/... -v -run TestFallbackChain
go test ./pkg/tools/... -v -run TestMessageTool
Run with Verbose Output
go test ./pkg/... -v
Run with Coverage
Generate coverage reports:
# Coverage for all packages
go test ./pkg/... -cover
# Detailed coverage report
go test ./pkg/... -coverprofile=coverage.out
go tool cover -html=coverage.out -o coverage.html
Run Integration Tests
Integration tests require external APIs and are tagged separately:
go test ./pkg/... -v -tags=integration
Integration tests are located in files with the build tag:
//go:build integration
// +build integration
Test Organization
Directory Structure
pkg/
├── agent/
│ ├── loop.go
│ ├── loop_test.go # Unit tests
│ └── loop_integration_test.go # Integration tests
├── providers/
│ ├── fallback.go
│ ├── fallback_test.go
│ └── openai_compat/
│ ├── provider.go
│ └── provider_test.go
└── tools/
├── message.go
├── message_test.go
└── ...
Test File Naming
- Unit tests:
*_test.go - Integration tests:
*_integration_test.go(with build tag)
Writing Tests
Basic Test
package tools
import (
"context"
"testing"
)
func TestMessageTool(t *testing.T) {
tool := NewMessageTool()
// Test Name()
if tool.Name() != "message" {
t.Errorf("expected name 'message', got %s", tool.Name())
}
// Test Description()
if tool.Description() == "" {
t.Error("description should not be empty")
}
}
Table-Driven Tests
func TestToolResult(t *testing.T) {
tests := []struct {
name string
input string
expected string
isError bool
}{
{
name: "success",
input: "valid input",
expected: "valid input",
isError: false,
},
{
name: "error case",
input: "",
expected: "input is required",
isError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := processInput(tt.input)
if result.ForLLM != tt.expected {
t.Errorf("expected %q, got %q", tt.expected, result.ForLLM)
}
if result.IsError != tt.isError {
t.Errorf("expected isError=%v, got %v", tt.isError, result.IsError)
}
})
}
}
Testing Tool Execution
func TestMessageToolExecute(t *testing.T) {
tool := NewMessageTool()
// Set up mock callback
var sentMessage string
tool.SetSendCallback(func(channel, chatID, content string) error {
sentMessage = content
return nil
})
// Set context
tool.SetContext("telegram", "123456")
// Execute
result := tool.Execute(context.Background(), map[string]interface{}{
"content": "Hello, world!",
})
// Verify
if result.IsError {
t.Errorf("unexpected error: %s", result.ForLLM)
}
if sentMessage != "Hello, world!" {
t.Errorf("expected 'Hello, world!', got %q", sentMessage)
}
}
Testing with Context
func TestWithContext(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
result := longRunningOperation(ctx)
if result == nil {
t.Error("expected result")
}
}
Testing Error Cases
func TestErrorHandling(t *testing.T) {
tool := NewMessageTool()
// Execute without setting context
result := tool.Execute(context.Background(), map[string]interface{}{
"content": "Hello",
})
if !result.IsError {
t.Error("expected error when context not set")
}
}
Mocking
Create mock implementations for testing:
// Mock provider for testing
type MockProvider struct {
response *providers.LLMResponse
err error
}
func (m *MockProvider) Chat(ctx context.Context, messages []providers.Message,
tools []providers.ToolDefinition, model string,
options map[string]interface{}) (*providers.LLMResponse, error) {
return m.response, m.err
}
func (m *MockProvider) GetDefaultModel() string {
return "mock-model"
}
func TestWithMockProvider(t *testing.T) {
mock := &MockProvider{
response: &providers.LLMResponse{
Content: "Hello!",
},
}
// Use mock in tests
// ...
}
Test Utilities
Helper Functions
// Create a test message bus
func newTestBus() *bus.MessageBus {
return bus.NewMessageBus()
}
// Create test config
func newTestConfig() *config.Config {
return &config.Config{
Agents: config.AgentsConfig{
Defaults: config.AgentDefaults{
Model: "test-model",
},
},
}
}
Setup and Teardown
func TestWithSetup(t *testing.T) {
// Setup
tmpDir, err := os.MkdirTemp("", "picoclaw-test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir) // Teardown
// Test using tmpDir
// ...
}
Benchmark Tests
Writing Benchmarks
func BenchmarkToolExecute(b *testing.B) {
tool := NewMessageTool()
tool.SetContext("telegram", "123456")
tool.SetSendCallback(func(channel, chatID, content string) error {
return nil
})
args := map[string]interface{}{
"content": "test message",
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
tool.Execute(context.Background(), args)
}
}
Running Benchmarks
go test ./pkg/tools/... -bench=.
go test ./pkg/tools/... -bench=. -benchmem
Continuous Integration
Tests are run automatically in CI. Ensure all tests pass before submitting PRs:
# Run full check
make check
This runs:
make deps- Download dependenciesmake fmt- Format codemake vet- Run lintermake test- Run tests
Linting
Run the Go linter:
make vet
Or directly:
go vet ./pkg/...
Common Issues
Test Fails with "context deadline exceeded"
The test may be timing out. Increase the timeout or check for blocking operations:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
Test Fails with "no such file or directory"
Tests may be looking for files in the wrong location. Use absolute paths or create temp directories:
tmpDir := t.TempDir() // Automatically cleaned up
Race Condition Detected
Run tests with race detection:
go test ./pkg/... -race
Fix race conditions by adding proper synchronization.
Best Practices
- Write tests for new features - Aim for good coverage
- Use table-driven tests - Test multiple cases efficiently
- Test error paths - Don't just test happy paths
- Use mocks for external dependencies - Isolate unit tests
- Keep tests fast - Use short timeouts and mock slow operations
- Clean up resources - Use
t.Cleanup()or defer - Use meaningful test names - Describe what's being tested
Test Coverage Goals
- Aim for >70% coverage on core packages
- 100% coverage on critical paths (tool execution, message routing)
- Integration tests for end-to-end scenarios