feat: golang linter

This commit is contained in:
Carlos Florêncio
2026-01-15 19:05:16 +00:00
parent 84252719b4
commit e1bce95bac
38 changed files with 601 additions and 336 deletions
+7 -1
View File
@@ -33,6 +33,13 @@ jobs:
- name: Run go vet - name: Run go vet
run: go vet ./... run: go vet ./...
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v6
with:
version: v1.61.0
args: ./...
working-directory: apps/backend
- name: Run tests with coverage - name: Run tests with coverage
run: | run: |
go test -v -race -coverprofile=coverage.out -covermode=atomic -json ./... > test-results.json 2>&1 || true go test -v -race -coverprofile=coverage.out -covermode=atomic -json ./... > test-results.json 2>&1 || true
@@ -85,4 +92,3 @@ jobs:
- name: Build - name: Build
run: go build -v ./cmd/kandev run: go build -v ./cmd/kandev
+43
View File
@@ -0,0 +1,43 @@
name: Frontend Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
frontend:
name: Run Frontend Lint, Tests, and Build
runs-on: ubuntu-latest
defaults:
run:
working-directory: apps
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
cache-dependency-path: apps/pnpm-lock.yaml
- name: Set up pnpm
uses: pnpm/action-setup@v4
with:
version: '9'
- name: Install dependencies
run: pnpm install
- name: Run lint
run: pnpm --filter @kandev/web lint
- name: Run tests
run: pnpm --filter @kandev/web test
- name: Build
run: pnpm --filter @kandev/web build
+6
View File
@@ -7,6 +7,12 @@ repos:
language: system language: system
pass_filenames: false pass_filenames: false
- id: go-lint
name: Go Lint (backend)
entry: make lint-backend
language: system
pass_filenames: false
- id: web-tests - id: web-tests
name: Web tests name: Web tests
entry: make test-web entry: make test-web
+2
View File
@@ -0,0 +1,2 @@
version: "2"
# Default golangci-lint configuration (uses built-in defaults).
+3 -2
View File
@@ -68,7 +68,9 @@ func main() {
fmt.Fprintf(os.Stderr, "failed to initialize logger: %v\n", err) fmt.Fprintf(os.Stderr, "failed to initialize logger: %v\n", err)
os.Exit(1) os.Exit(1)
} }
defer log.Sync() defer func() {
_ = log.Sync()
}()
// Determine mode // Determine mode
mode := detectMode(cfg.Mode) mode := detectMode(cfg.Mode)
@@ -290,4 +292,3 @@ func collectAgentEnv() []string {
} }
return env return env
} }
+16 -7
View File
@@ -69,7 +69,6 @@ var (
flagAgentRuntime = flag.String("agent-runtime", "", "Agent runtime mode: 'docker' or 'standalone'") flagAgentRuntime = flag.String("agent-runtime", "", "Agent runtime mode: 'docker' or 'standalone'")
flagPort = flag.Int("port", 0, "HTTP server port (default: 8080)") flagPort = flag.Int("port", 0, "HTTP server port (default: 8080)")
flagLogLevel = flag.String("log-level", "", "Log level: debug, info, warn, error") flagLogLevel = flag.String("log-level", "", "Log level: debug, info, warn, error")
flagConfigPath = flag.String("config", "", "Path to config file")
flagHelp = flag.Bool("help", false, "Show help message") flagHelp = flag.Bool("help", false, "Show help message")
flagVersion = flag.Bool("version", false, "Show version information") flagVersion = flag.Bool("version", false, "Show version information")
) )
@@ -127,7 +126,9 @@ func main() {
fmt.Fprintf(os.Stderr, "Failed to initialize logger: %v\n", err) fmt.Fprintf(os.Stderr, "Failed to initialize logger: %v\n", err)
os.Exit(1) os.Exit(1)
} }
defer log.Sync() defer func() {
_ = log.Sync()
}()
logger.SetDefault(log) logger.SetDefault(log)
log.Info("Starting Kandev (unified mode)...") log.Info("Starting Kandev (unified mode)...")
@@ -158,7 +159,9 @@ func main() {
log.Warn("Failed to initialize Docker client - agent features will be disabled", zap.Error(err)) log.Warn("Failed to initialize Docker client - agent features will be disabled", zap.Error(err))
dockerClient = nil dockerClient = nil
} else { } else {
defer dockerClient.Close() defer func() {
_ = dockerClient.Close()
}()
if err := dockerClient.Ping(ctx); err != nil { if err := dockerClient.Ping(ctx); err != nil {
log.Warn("Docker daemon not available - agent features will be disabled", zap.Error(err)) log.Warn("Docker daemon not available - agent features will be disabled", zap.Error(err))
dockerClient = nil dockerClient = nil
@@ -184,20 +187,26 @@ func main() {
if err != nil { if err != nil {
log.Fatal("Failed to initialize SQLite database", zap.Error(err), zap.String("db_path", dbPath)) log.Fatal("Failed to initialize SQLite database", zap.Error(err), zap.String("db_path", dbPath))
} }
defer taskRepo.Close() defer func() {
_ = taskRepo.Close()
}()
log.Info("SQLite database initialized", zap.String("db_path", dbPath)) log.Info("SQLite database initialized", zap.String("db_path", dbPath))
agentSettingsRepo, err := settingsstore.NewSQLiteRepository(dbPath) agentSettingsRepo, err := settingsstore.NewSQLiteRepository(dbPath)
if err != nil { if err != nil {
log.Fatal("Failed to initialize agent settings store", zap.Error(err), zap.String("db_path", dbPath)) log.Fatal("Failed to initialize agent settings store", zap.Error(err), zap.String("db_path", dbPath))
} }
defer agentSettingsRepo.Close() defer func() {
_ = agentSettingsRepo.Close()
}()
userRepo, err := userstore.NewSQLiteRepository(dbPath) userRepo, err := userstore.NewSQLiteRepository(dbPath)
if err != nil { if err != nil {
log.Fatal("Failed to initialize user store", zap.Error(err), zap.String("db_path", dbPath)) log.Fatal("Failed to initialize user store", zap.Error(err), zap.String("db_path", dbPath))
} }
defer userRepo.Close() defer func() {
_ = userRepo.Close()
}()
discoveryRegistry, err := discovery.LoadRegistry() discoveryRegistry, err := discovery.LoadRegistry()
if err != nil { if err != nil {
@@ -1041,7 +1050,7 @@ func (a *messageCreatorAdapter) CreateToolCallMessage(ctx context.Context, taskI
} }
// Add args if provided (contains kind, path, locations, raw_input) // Add args if provided (contains kind, path, locations, raw_input)
if args != nil && len(args) > 0 { if len(args) > 0 {
metadata["args"] = args metadata["args"] = args
// Extract kind as tool_name for icon selection in the frontend // Extract kind as tool_name for icon selection in the frontend
+32 -8
View File
@@ -51,7 +51,11 @@ func (c *Client) Initialize(ctx context.Context, clientName, clientVersion strin
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer resp.Body.Close() defer func() {
if err := resp.Body.Close(); err != nil {
c.logger.Debug("failed to close initialize response body", zap.Error(err))
}
}()
var result InitializeResponse var result InitializeResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
@@ -91,7 +95,11 @@ func (c *Client) NewSession(ctx context.Context, cwd string) (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
defer resp.Body.Close() defer func() {
if err := resp.Body.Close(); err != nil {
c.logger.Debug("failed to close session response body", zap.Error(err))
}
}()
var result NewSessionResponse var result NewSessionResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
@@ -137,7 +145,11 @@ func (c *Client) Prompt(ctx context.Context, text string) (*PromptResponse, erro
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer resp.Body.Close() defer func() {
if err := resp.Body.Close(); err != nil {
c.logger.Debug("failed to close prompt response body", zap.Error(err))
}
}()
var result PromptResponse var result PromptResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
@@ -230,7 +242,9 @@ func (c *Client) StreamUpdates(ctx context.Context, handler func(SessionUpdate))
c.mu.Lock() c.mu.Lock()
c.acpConn = nil c.acpConn = nil
c.mu.Unlock() c.mu.Unlock()
conn.Close() if err := conn.Close(); err != nil {
c.logger.Debug("failed to close updates websocket", zap.Error(err))
}
}() }()
for { for {
@@ -268,7 +282,9 @@ func (c *Client) CloseACPStream() {
defer c.mu.Unlock() defer c.mu.Unlock()
if c.acpConn != nil { if c.acpConn != nil {
c.acpConn.Close() if err := c.acpConn.Close(); err != nil {
c.logger.Debug("failed to close ACP stream", zap.Error(err))
}
c.acpConn = nil c.acpConn = nil
} }
} }
@@ -294,7 +310,9 @@ func (c *Client) StreamPermissions(ctx context.Context, handler func(*Permission
c.mu.Lock() c.mu.Lock()
c.permissionConn = nil c.permissionConn = nil
c.mu.Unlock() c.mu.Unlock()
conn.Close() if err := conn.Close(); err != nil {
c.logger.Debug("failed to close permissions websocket", zap.Error(err))
}
}() }()
for { for {
@@ -327,7 +345,9 @@ func (c *Client) ClosePermissionStream() {
defer c.mu.Unlock() defer c.mu.Unlock()
if c.permissionConn != nil { if c.permissionConn != nil {
c.permissionConn.Close() if err := c.permissionConn.Close(); err != nil {
c.logger.Debug("failed to close permission stream", zap.Error(err))
}
c.permissionConn = nil c.permissionConn = nil
} }
} }
@@ -355,7 +375,11 @@ func (c *Client) RespondToPermission(ctx context.Context, pendingID, optionID st
if err != nil { if err != nil {
return err return err
} }
defer resp.Body.Close() defer func() {
if err := resp.Body.Close(); err != nil {
c.logger.Debug("failed to close permission response body", zap.Error(err))
}
}()
var result PermissionRespondResponse var result PermissionRespondResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
+26 -10
View File
@@ -62,7 +62,7 @@ func (c *Client) Health(ctx context.Context) error {
if err != nil { if err != nil {
return err return err
} }
defer resp.Body.Close() defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return fmt.Errorf("health check failed: %d", resp.StatusCode) return fmt.Errorf("health check failed: %d", resp.StatusCode)
@@ -81,7 +81,7 @@ func (c *Client) GetStatus(ctx context.Context) (*StatusResponse, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer resp.Body.Close() defer func() { _ = resp.Body.Close() }()
var status StatusResponse var status StatusResponse
if err := json.NewDecoder(resp.Body).Decode(&status); err != nil { if err := json.NewDecoder(resp.Body).Decode(&status); err != nil {
@@ -101,7 +101,7 @@ func (c *Client) Start(ctx context.Context) error {
if err != nil { if err != nil {
return err return err
} }
defer resp.Body.Close() defer func() { _ = resp.Body.Close() }()
var result struct { var result struct {
Success bool `json:"success"` Success bool `json:"success"`
@@ -127,7 +127,7 @@ func (c *Client) Stop(ctx context.Context) error {
if err != nil { if err != nil {
return err return err
} }
defer resp.Body.Close() defer func() { _ = resp.Body.Close() }()
var result struct { var result struct {
Success bool `json:"success"` Success bool `json:"success"`
@@ -205,7 +205,9 @@ func (c *Client) StreamGitStatus(ctx context.Context, handler func(*GitStatusUpd
c.mu.Lock() c.mu.Lock()
c.gitStatusConn = nil c.gitStatusConn = nil
c.mu.Unlock() c.mu.Unlock()
conn.Close() if err := conn.Close(); err != nil {
c.logger.Debug("failed to close git status websocket", zap.Error(err))
}
}() }()
for { for {
@@ -238,7 +240,9 @@ func (c *Client) CloseGitStatusStream() {
defer c.mu.Unlock() defer c.mu.Unlock()
if c.gitStatusConn != nil { if c.gitStatusConn != nil {
c.gitStatusConn.Close() if err := c.gitStatusConn.Close(); err != nil {
c.logger.Debug("failed to close git status stream", zap.Error(err))
}
c.gitStatusConn = nil c.gitStatusConn = nil
} }
} }
@@ -264,7 +268,9 @@ func (c *Client) StreamFileChanges(ctx context.Context, handler func(*FileChange
c.mu.Lock() c.mu.Lock()
c.fileChangesConn = nil c.fileChangesConn = nil
c.mu.Unlock() c.mu.Unlock()
conn.Close() if err := conn.Close(); err != nil {
c.logger.Debug("failed to close file changes websocket", zap.Error(err))
}
}() }()
for { for {
@@ -304,7 +310,11 @@ func (c *Client) RequestFileTree(ctx context.Context, path string, depth int) (*
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to request file tree: %w", err) return nil, fmt.Errorf("failed to request file tree: %w", err)
} }
defer resp.Body.Close() defer func() {
if err := resp.Body.Close(); err != nil {
c.logger.Debug("failed to close file tree response body", zap.Error(err))
}
}()
var response FileTreeResponse var response FileTreeResponse
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
@@ -331,7 +341,11 @@ func (c *Client) RequestFileContent(ctx context.Context, path string) (*FileCont
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to request file content: %w", err) return nil, fmt.Errorf("failed to request file content: %w", err)
} }
defer resp.Body.Close() defer func() {
if err := resp.Body.Close(); err != nil {
c.logger.Debug("failed to close file content response body", zap.Error(err))
}
}()
var response FileContentResponse var response FileContentResponse
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
@@ -351,7 +365,9 @@ func (c *Client) CloseFileChangesStream() {
defer c.mu.Unlock() defer c.mu.Unlock()
if c.fileChangesConn != nil { if c.fileChangesConn != nil {
c.fileChangesConn.Close() if err := c.fileChangesConn.Close(); err != nil {
c.logger.Debug("failed to close file changes stream", zap.Error(err))
}
c.fileChangesConn = nil c.fileChangesConn = nil
} }
} }
@@ -164,7 +164,9 @@ func (l *Launcher) Start(ctx context.Context) error {
// Wait for health check to pass // Wait for health check to pass
if err := l.waitForHealthy(ctx); err != nil { if err := l.waitForHealthy(ctx); err != nil {
// Kill the process if health check fails // Kill the process if health check fails
l.cmd.Process.Kill() if killErr := l.cmd.Process.Kill(); killErr != nil {
l.logger.Warn("failed to kill agentctl process after failed health check", zap.Error(killErr))
}
return fmt.Errorf("agentctl failed to become healthy: %w", err) return fmt.Errorf("agentctl failed to become healthy: %w", err)
} }
@@ -230,7 +232,9 @@ func (l *Launcher) checkPortAvailable() error {
if err != nil { if err != nil {
return err return err
} }
ln.Close() if err := ln.Close(); err != nil {
return err
}
return nil return nil
} }
@@ -255,7 +259,9 @@ func (l *Launcher) waitForHealthy(ctx context.Context) error {
resp, err := client.Get(healthURL) resp, err := client.Get(healthURL)
if err == nil { if err == nil {
resp.Body.Close() if closeErr := resp.Body.Close(); closeErr != nil {
l.logger.Debug("failed to close health response body", zap.Error(closeErr))
}
if resp.StatusCode == http.StatusOK { if resp.StatusCode == http.StatusOK {
return nil return nil
} }
@@ -72,7 +72,9 @@ func (s *StandaloneCtl) Health(ctx context.Context) error {
if err != nil { if err != nil {
return err return err
} }
defer resp.Body.Close() defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return fmt.Errorf("health check failed: %d", resp.StatusCode) return fmt.Errorf("health check failed: %d", resp.StatusCode)
@@ -97,13 +99,15 @@ func (s *StandaloneCtl) CreateInstance(ctx context.Context, req *CreateInstanceR
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create instance: %w", err) return nil, fmt.Errorf("failed to create instance: %w", err)
} }
defer resp.Body.Close() defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
var errResp struct { var errResp struct {
Error string `json:"error"` Error string `json:"error"`
} }
json.NewDecoder(resp.Body).Decode(&errResp) if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil {
return nil, fmt.Errorf("failed to decode error response: %w", err)
}
return nil, fmt.Errorf("failed to create instance: %s (status %d)", errResp.Error, resp.StatusCode) return nil, fmt.Errorf("failed to create instance: %s (status %d)", errResp.Error, resp.StatusCode)
} }
@@ -130,13 +134,17 @@ func (s *StandaloneCtl) DeleteInstance(ctx context.Context, instanceID string) e
if err != nil { if err != nil {
return fmt.Errorf("failed to delete instance: %w", err) return fmt.Errorf("failed to delete instance: %w", err)
} }
defer resp.Body.Close() defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent { if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
var errResp struct { var errResp struct {
Error string `json:"error"` Error string `json:"error"`
} }
json.NewDecoder(resp.Body).Decode(&errResp) if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil {
return fmt.Errorf("failed to decode error response: %w", err)
}
return fmt.Errorf("failed to delete instance: %s (status %d)", errResp.Error, resp.StatusCode) return fmt.Errorf("failed to delete instance: %s (status %d)", errResp.Error, resp.StatusCode)
} }
@@ -155,7 +163,7 @@ func (s *StandaloneCtl) GetInstance(ctx context.Context, instanceID string) (*In
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get instance: %w", err) return nil, fmt.Errorf("failed to get instance: %w", err)
} }
defer resp.Body.Close() defer func() { _ = resp.Body.Close() }()
if resp.StatusCode == http.StatusNotFound { if resp.StatusCode == http.StatusNotFound {
return nil, fmt.Errorf("instance %q not found", instanceID) return nil, fmt.Errorf("instance %q not found", instanceID)
@@ -182,7 +190,7 @@ func (s *StandaloneCtl) ListInstances(ctx context.Context) ([]*InstanceInfo, err
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to list instances: %w", err) return nil, fmt.Errorf("failed to list instances: %w", err)
} }
defer resp.Body.Close() defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to list instances: status %d", resp.StatusCode) return nil, fmt.Errorf("failed to list instances: status %d", resp.StatusCode)
@@ -196,4 +204,3 @@ func (s *StandaloneCtl) ListInstances(ctx context.Context) ([]*InstanceInfo, err
} }
return result.Instances, nil return result.Instances, nil
} }
@@ -47,8 +47,7 @@ func TestManager_GetCredential_FromEnv(t *testing.T) {
// Set up test environment variable // Set up test environment variable
testKey := "TEST_CREDENTIAL_KEY_12345" testKey := "TEST_CREDENTIAL_KEY_12345"
testValue := "test-secret-value" testValue := "test-secret-value"
os.Setenv(testKey, testValue) t.Setenv(testKey, testValue)
defer os.Unsetenv(testKey)
log := newTestLogger() log := newTestLogger()
mgr := NewManager(log) mgr := NewManager(log)
@@ -74,8 +73,7 @@ func TestManager_GetCredential_FromEnv(t *testing.T) {
func TestManager_GetCredential_Cached(t *testing.T) { func TestManager_GetCredential_Cached(t *testing.T) {
testKey := "TEST_CACHED_KEY" testKey := "TEST_CACHED_KEY"
testValue := "cached-value" testValue := "cached-value"
os.Setenv(testKey, testValue) t.Setenv(testKey, testValue)
defer os.Unsetenv(testKey)
log := newTestLogger() log := newTestLogger()
mgr := NewManager(log) mgr := NewManager(log)
@@ -90,7 +88,9 @@ func TestManager_GetCredential_Cached(t *testing.T) {
} }
// Remove from env - should still get cached value // Remove from env - should still get cached value
os.Unsetenv(testKey) if err := os.Unsetenv(testKey); err != nil {
t.Fatalf("failed to unset env var: %v", err)
}
cred2, err := mgr.GetCredential(ctx, testKey) cred2, err := mgr.GetCredential(ctx, testKey)
if err != nil { if err != nil {
@@ -117,12 +117,8 @@ func TestManager_GetCredential_NotFound(t *testing.T) {
func TestManager_GetCredentials(t *testing.T) { func TestManager_GetCredentials(t *testing.T) {
testKey1 := "TEST_MULTI_KEY_1" testKey1 := "TEST_MULTI_KEY_1"
testKey2 := "TEST_MULTI_KEY_2" testKey2 := "TEST_MULTI_KEY_2"
os.Setenv(testKey1, "value1") t.Setenv(testKey1, "value1")
os.Setenv(testKey2, "value2") t.Setenv(testKey2, "value2")
defer func() {
os.Unsetenv(testKey1)
os.Unsetenv(testKey2)
}()
log := newTestLogger() log := newTestLogger()
mgr := NewManager(log) mgr := NewManager(log)
@@ -147,8 +143,7 @@ func TestManager_GetCredentials(t *testing.T) {
func TestManager_GetCredentials_PartialFailure(t *testing.T) { func TestManager_GetCredentials_PartialFailure(t *testing.T) {
testKey := "TEST_PARTIAL_KEY" testKey := "TEST_PARTIAL_KEY"
os.Setenv(testKey, "value") t.Setenv(testKey, "value")
defer os.Unsetenv(testKey)
log := newTestLogger() log := newTestLogger()
mgr := NewManager(log) mgr := NewManager(log)
@@ -169,8 +164,7 @@ func TestManager_GetCredentials_PartialFailure(t *testing.T) {
func TestManager_BuildEnvVars(t *testing.T) { func TestManager_BuildEnvVars(t *testing.T) {
testKey := "TEST_BUILD_ENV_KEY" testKey := "TEST_BUILD_ENV_KEY"
testValue := "build-env-value" testValue := "build-env-value"
os.Setenv(testKey, testValue) t.Setenv(testKey, testValue)
defer os.Unsetenv(testKey)
log := newTestLogger() log := newTestLogger()
mgr := NewManager(log) mgr := NewManager(log)
@@ -219,8 +213,7 @@ func TestManager_BuildEnvVars_MissingRequired(t *testing.T) {
func TestManager_HasCredential(t *testing.T) { func TestManager_HasCredential(t *testing.T) {
testKey := "TEST_HAS_CREDENTIAL" testKey := "TEST_HAS_CREDENTIAL"
os.Setenv(testKey, "value") t.Setenv(testKey, "value")
defer os.Unsetenv(testKey)
log := newTestLogger() log := newTestLogger()
mgr := NewManager(log) mgr := NewManager(log)
@@ -238,8 +231,7 @@ func TestManager_HasCredential(t *testing.T) {
func TestManager_ListAvailable(t *testing.T) { func TestManager_ListAvailable(t *testing.T) {
testKey := "TEST_LIST_AVAILABLE_API_KEY" testKey := "TEST_LIST_AVAILABLE_API_KEY"
os.Setenv(testKey, "value") t.Setenv(testKey, "value")
defer os.Unsetenv(testKey)
log := newTestLogger() log := newTestLogger()
mgr := NewManager(log) mgr := NewManager(log)
@@ -263,8 +255,7 @@ func TestManager_ListAvailable(t *testing.T) {
func TestManager_ClearCache(t *testing.T) { func TestManager_ClearCache(t *testing.T) {
testKey := "TEST_CLEAR_CACHE" testKey := "TEST_CLEAR_CACHE"
os.Setenv(testKey, "original-value") t.Setenv(testKey, "original-value")
defer os.Unsetenv(testKey)
log := newTestLogger() log := newTestLogger()
mgr := NewManager(log) mgr := NewManager(log)
@@ -279,7 +270,9 @@ func TestManager_ClearCache(t *testing.T) {
} }
// Update the env and clear cache // Update the env and clear cache
os.Setenv(testKey, "new-value") if err := os.Setenv(testKey, "new-value"); err != nil {
t.Fatalf("failed to update env var: %v", err)
}
mgr.ClearCache() mgr.ClearCache()
// Should get new value // Should get new value
@@ -301,8 +294,7 @@ func TestEnvProvider_Name(t *testing.T) {
func TestEnvProvider_GetCredential(t *testing.T) { func TestEnvProvider_GetCredential(t *testing.T) {
testKey := "TEST_ENV_PROVIDER_KEY" testKey := "TEST_ENV_PROVIDER_KEY"
testValue := "env-provider-value" testValue := "env-provider-value"
os.Setenv(testKey, testValue) t.Setenv(testKey, testValue)
defer os.Unsetenv(testKey)
provider := NewEnvProvider("") provider := NewEnvProvider("")
ctx := context.Background() ctx := context.Background()
@@ -323,8 +315,7 @@ func TestEnvProvider_GetCredential_WithPrefix(t *testing.T) {
prefix := "KANDEV_" prefix := "KANDEV_"
testKey := "MY_SECRET" testKey := "MY_SECRET"
testValue := "prefixed-value" testValue := "prefixed-value"
os.Setenv(prefix+testKey, testValue) t.Setenv(prefix+testKey, testValue)
defer os.Unsetenv(prefix + testKey)
provider := NewEnvProvider(prefix) provider := NewEnvProvider(prefix)
ctx := context.Background() ctx := context.Background()
@@ -343,8 +334,7 @@ func TestEnvProvider_GetCredential_WithPrefix(t *testing.T) {
func TestEnvProvider_ListAvailable(t *testing.T) { func TestEnvProvider_ListAvailable(t *testing.T) {
testKey := "ANTHROPIC_API_KEY" testKey := "ANTHROPIC_API_KEY"
os.Setenv(testKey, "test-value") t.Setenv(testKey, "test-value")
defer os.Unsetenv(testKey)
provider := NewEnvProvider("") provider := NewEnvProvider("")
ctx := context.Background() ctx := context.Background()
@@ -412,7 +402,9 @@ func TestFileProvider_GetCredential_NotFound(t *testing.T) {
creds := map[string]string{"EXISTING": "value"} creds := map[string]string{"EXISTING": "value"}
data, _ := json.Marshal(creds) data, _ := json.Marshal(creds)
os.WriteFile(configPath, data, 0644) if err := os.WriteFile(configPath, data, 0644); err != nil {
t.Fatalf("failed to write test file: %v", err)
}
provider := NewFileProvider(configPath) provider := NewFileProvider(configPath)
ctx := context.Background() ctx := context.Background()
@@ -438,7 +430,9 @@ func TestFileProvider_InvalidJSON(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, "invalid.json") configPath := filepath.Join(tmpDir, "invalid.json")
os.WriteFile(configPath, []byte("invalid json"), 0644) if err := os.WriteFile(configPath, []byte("invalid json"), 0644); err != nil {
t.Fatalf("failed to write test file: %v", err)
}
provider := NewFileProvider(configPath) provider := NewFileProvider(configPath)
ctx := context.Background() ctx := context.Background()
@@ -458,7 +452,9 @@ func TestFileProvider_ListAvailable(t *testing.T) {
"KEY_2": "value2", "KEY_2": "value2",
} }
data, _ := json.Marshal(creds) data, _ := json.Marshal(creds)
os.WriteFile(configPath, data, 0644) if err := os.WriteFile(configPath, data, 0644); err != nil {
t.Fatalf("failed to write test file: %v", err)
}
provider := NewFileProvider(configPath) provider := NewFileProvider(configPath)
ctx := context.Background() ctx := context.Background()
@@ -480,7 +476,9 @@ func TestFileProvider_Reload(t *testing.T) {
// Initial content // Initial content
creds := map[string]string{"KEY": "original"} creds := map[string]string{"KEY": "original"}
data, _ := json.Marshal(creds) data, _ := json.Marshal(creds)
os.WriteFile(configPath, data, 0644) if err := os.WriteFile(configPath, data, 0644); err != nil {
t.Fatalf("failed to write test file: %v", err)
}
provider := NewFileProvider(configPath) provider := NewFileProvider(configPath)
ctx := context.Background() ctx := context.Background()
@@ -494,7 +492,9 @@ func TestFileProvider_Reload(t *testing.T) {
// Update file // Update file
creds["KEY"] = "updated" creds["KEY"] = "updated"
data, _ = json.Marshal(creds) data, _ = json.Marshal(creds)
os.WriteFile(configPath, data, 0644) if err := os.WriteFile(configPath, data, 0644); err != nil {
t.Fatalf("failed to write test file: %v", err)
}
// Reload // Reload
err := provider.Reload() err := provider.Reload()
@@ -508,4 +508,3 @@ func TestFileProvider_Reload(t *testing.T) {
t.Errorf("expected updated, got %q", cred.Value) t.Errorf("expected updated, got %q", cred.Value)
} }
} }
+23 -13
View File
@@ -107,7 +107,11 @@ func (c *Client) PullImage(ctx context.Context, imageName string) error {
c.logger.Error("Failed to pull image", zap.String("image", imageName), zap.Error(err)) c.logger.Error("Failed to pull image", zap.String("image", imageName), zap.Error(err))
return fmt.Errorf("failed to pull image %s: %w", imageName, err) return fmt.Errorf("failed to pull image %s: %w", imageName, err)
} }
defer reader.Close() defer func() {
if err := reader.Close(); err != nil {
c.logger.Warn("Failed to close image pull reader", zap.Error(err))
}
}()
// Read the output to ensure the image is fully pulled // Read the output to ensure the image is fully pulled
_, err = io.Copy(io.Discard, reader) _, err = io.Copy(io.Discard, reader)
@@ -303,14 +307,8 @@ func (c *Client) GetContainerIP(ctx context.Context, containerID string) (string
return "", err return "", err
} }
// Try to get IP from default bridge network first
if inspect.NetworkSettings != nil { if inspect.NetworkSettings != nil {
// Check bridge network // Check available networks for an IP address.
if inspect.NetworkSettings.IPAddress != "" {
return inspect.NetworkSettings.IPAddress, nil
}
// Check other networks
for netName, netSettings := range inspect.NetworkSettings.Networks { for netName, netSettings := range inspect.NetworkSettings.Networks {
if netSettings.IPAddress != "" { if netSettings.IPAddress != "" {
c.logger.Debug("Found container IP", c.logger.Debug("Found container IP",
@@ -538,7 +536,9 @@ func (c *Client) AttachContainer(ctx context.Context, containerID string) (*Atta
// Start goroutine to copy from pipe to container // Start goroutine to copy from pipe to container
go func() { go func() {
io.Copy(resp.Conn, stdinReader) if _, err := io.Copy(resp.Conn, stdinReader); err != nil {
c.logger.Debug("failed to copy stdin to container", zap.Error(err))
}
}() }()
// Create a pipe for demultiplexed stdout // Create a pipe for demultiplexed stdout
@@ -547,7 +547,11 @@ func (c *Client) AttachContainer(ctx context.Context, containerID string) (*Atta
// Start goroutine to demultiplex stdout/stderr // Start goroutine to demultiplex stdout/stderr
go func() { go func() {
defer stdoutWriter.Close() defer func() {
if err := stdoutWriter.Close(); err != nil {
c.logger.Debug("failed to close stdout writer", zap.Error(err))
}
}()
c.demultiplexStream(resp.Reader, stdoutWriter) c.demultiplexStream(resp.Reader, stdoutWriter)
}() }()
@@ -594,7 +598,9 @@ func (c *Client) demultiplexStream(reader io.Reader, writer io.Writer) {
// Write stdout (type 1) and stderr (type 2) to writer // Write stdout (type 1) and stderr (type 2) to writer
// We want both for ACP since errors should be visible // We want both for ACP since errors should be visible
if streamType == 1 || streamType == 2 { if streamType == 1 || streamType == 2 {
writer.Write(data) if _, err := writer.Write(data); err != nil {
c.logger.Debug("failed to write demultiplexed data", zap.Error(err))
}
} }
} }
} }
@@ -603,10 +609,14 @@ func (c *Client) demultiplexStream(reader io.Reader, writer io.Writer) {
// Close closes the attach result // Close closes the attach result
func (a *AttachResult) Close() error { func (a *AttachResult) Close() error {
if a.Stdin != nil { if a.Stdin != nil {
a.Stdin.Close() if err := a.Stdin.Close(); err != nil {
return err
}
} }
if a.Conn != nil { if a.Conn != nil {
a.Conn.Close() if err := a.Conn.Close(); err != nil {
return err
}
} }
return nil return nil
} }
@@ -135,26 +135,6 @@ func newTestRegistry() *registry.Registry {
return reg return reg
} }
// managerWithMock creates a manager with a mock docker client
// We need to use reflection or a wrapper to inject the mock
type testManager struct {
*Manager
mockDocker *MockDockerClient
}
func newTestManager(mockDocker *MockDockerClient, eventBus bus.EventBus) *testManager {
log := newTestLogger()
reg := newTestRegistry()
// Create a real manager first
mgr := NewManager(nil, reg, eventBus, testAgentConfig(), log)
return &testManager{
Manager: mgr,
mockDocker: mockDocker,
}
}
func TestNewManager(t *testing.T) { func TestNewManager(t *testing.T) {
log := newTestLogger() log := newTestLogger()
reg := newTestRegistry() reg := newTestRegistry()
@@ -519,4 +499,3 @@ func TestManager_StartStop(t *testing.T) {
t.Fatalf("unexpected error stopping manager: %v", err) t.Fatalf("unexpected error stopping manager: %v", err)
} }
} }
@@ -38,7 +38,9 @@ func NewSQLiteRepository(dbPath string) (*SQLiteRepository, error) {
repo := &SQLiteRepository{db: db} repo := &SQLiteRepository{db: db}
if err := repo.initSchema(); err != nil { if err := repo.initSchema(); err != nil {
db.Close() if closeErr := db.Close(); closeErr != nil {
return nil, fmt.Errorf("failed to close database after schema error: %w", closeErr)
}
return nil, fmt.Errorf("failed to initialize schema: %w", err) return nil, fmt.Errorf("failed to initialize schema: %w", err)
} }
return repo, nil return repo, nil
@@ -181,7 +183,9 @@ func (r *SQLiteRepository) ListAgents(ctx context.Context) ([]*models.Agent, err
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer func() {
_ = rows.Close()
}()
var result []*models.Agent var result []*models.Agent
for rows.Next() { for rows.Next() {
@@ -258,7 +262,9 @@ func (r *SQLiteRepository) ListAgentProfiles(ctx context.Context, agentID string
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer func() {
_ = rows.Close()
}()
var result []*models.AgentProfile var result []*models.AgentProfile
for rows.Next() { for rows.Next() {
@@ -338,7 +344,9 @@ func columnExists(db *sql.DB, table, column string) (bool, error) {
if err != nil { if err != nil {
return false, err return false, err
} }
defer rows.Close() defer func() {
_ = rows.Close()
}()
for rows.Next() { for rows.Next() {
var cid int var cid int
var name, ctype string var name, ctype string
@@ -240,7 +240,9 @@ func (m *Manager) createWorktree(ctx context.Context, req CreateRequest) (*Workt
if m.store != nil { if m.store != nil {
if err := m.store.CreateWorktree(ctx, wt); err != nil { if err := m.store.CreateWorktree(ctx, wt); err != nil {
// Cleanup on failure // Cleanup on failure
m.removeWorktreeDir(ctx, worktreePath, req.RepositoryPath) if cleanupErr := m.removeWorktreeDir(ctx, worktreePath, req.RepositoryPath); cleanupErr != nil {
m.logger.Warn("failed to cleanup worktree after persist failure", zap.Error(cleanupErr))
}
return nil, fmt.Errorf("failed to persist worktree: %w", err) return nil, fmt.Errorf("failed to persist worktree: %w", err)
} }
} }
@@ -561,7 +563,9 @@ func (m *Manager) removeWorktreeDir(ctx context.Context, worktreePath, repoPath
// Prune stale worktree entries // Prune stale worktree entries
cmd = exec.CommandContext(ctx, "git", "worktree", "prune") cmd = exec.CommandContext(ctx, "git", "worktree", "prune")
cmd.Dir = repoPath cmd.Dir = repoPath
cmd.Run() // Ignore errors if err := cmd.Run(); err != nil {
m.logger.Debug("git worktree prune failed", zap.Error(err))
}
} }
return nil return nil
} }
@@ -570,13 +574,17 @@ func (m *Manager) removeWorktreeDir(ctx context.Context, worktreePath, repoPath
func (m *Manager) recreate(ctx context.Context, existing *Worktree, req CreateRequest) (*Worktree, error) { func (m *Manager) recreate(ctx context.Context, existing *Worktree, req CreateRequest) (*Worktree, error) {
// Clean up existing directory if present // Clean up existing directory if present
if existing.Path != "" { if existing.Path != "" {
os.RemoveAll(existing.Path) if err := os.RemoveAll(existing.Path); err != nil {
m.logger.Debug("failed to remove existing worktree path", zap.Error(err))
}
} }
// Remove from git worktree list // Remove from git worktree list
cmd := exec.CommandContext(ctx, "git", "worktree", "prune") cmd := exec.CommandContext(ctx, "git", "worktree", "prune")
cmd.Dir = req.RepositoryPath cmd.Dir = req.RepositoryPath
cmd.Run() // Ignore errors if err := cmd.Run(); err != nil {
m.logger.Debug("git worktree prune failed", zap.Error(err))
}
// Get repository lock // Get repository lock
repoLock := m.getRepoLock(req.RepositoryPath) repoLock := m.getRepoLock(req.RepositoryPath)
@@ -623,4 +631,4 @@ func (m *Manager) recreate(ctx context.Context, existing *Worktree, req CreateRe
zap.String("path", worktreePath)) zap.String("path", worktreePath))
return existing, nil return existing, nil
} }
@@ -151,7 +151,7 @@ func (s *SQLiteStore) GetWorktreesByTaskID(ctx context.Context, taskID string) (
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer func() { _ = rows.Close() }()
return s.scanWorktrees(rows) return s.scanWorktrees(rows)
} }
@@ -166,7 +166,7 @@ func (s *SQLiteStore) GetWorktreesByRepositoryID(ctx context.Context, repoID str
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer func() { _ = rows.Close() }()
return s.scanWorktrees(rows) return s.scanWorktrees(rows)
} }
@@ -217,7 +217,7 @@ func (s *SQLiteStore) ListActiveWorktrees(ctx context.Context) ([]*Worktree, err
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer func() { _ = rows.Close() }()
return s.scanWorktrees(rows) return s.scanWorktrees(rows)
} }
@@ -93,10 +93,10 @@ func (a *CodexAdapter) Initialize(ctx context.Context) error {
}, },
}) })
if err != nil { if err != nil {
return fmt.Errorf("Codex initialize handshake failed: %w", err) return fmt.Errorf("codex initialize handshake failed: %w", err)
} }
if resp.Error != nil { if resp.Error != nil {
return fmt.Errorf("Codex initialize error: %s", resp.Error.Message) return fmt.Errorf("codex initialize error: %s", resp.Error.Message)
} }
// Parse initialize result // Parse initialize result
@@ -563,7 +563,9 @@ func (a *CodexAdapter) handleRequest(id interface{}, method string, params json.
var p codex.CommandApprovalParams var p codex.CommandApprovalParams
if err := json.Unmarshal(params, &p); err != nil { if err := json.Unmarshal(params, &p); err != nil {
a.logger.Warn("failed to parse command approval request", zap.Error(err)) a.logger.Warn("failed to parse command approval request", zap.Error(err))
a.client.SendResponse(id, nil, &codex.Error{Code: codex.InvalidParams, Message: "invalid params"}) if err := a.client.SendResponse(id, nil, &codex.Error{Code: codex.InvalidParams, Message: "invalid params"}); err != nil {
a.logger.Warn("failed to send invalid params response", zap.Error(err))
}
return return
} }
a.handleApprovalRequest(id, handler, threadID, p.ItemID, types.ActionTypeCommand, p.Command, map[string]interface{}{ a.handleApprovalRequest(id, handler, threadID, p.ItemID, types.ActionTypeCommand, p.Command, map[string]interface{}{
@@ -576,7 +578,9 @@ func (a *CodexAdapter) handleRequest(id interface{}, method string, params json.
var p codex.FileChangeApprovalParams var p codex.FileChangeApprovalParams
if err := json.Unmarshal(params, &p); err != nil { if err := json.Unmarshal(params, &p); err != nil {
a.logger.Warn("failed to parse file change approval request", zap.Error(err)) a.logger.Warn("failed to parse file change approval request", zap.Error(err))
a.client.SendResponse(id, nil, &codex.Error{Code: codex.InvalidParams, Message: "invalid params"}) if err := a.client.SendResponse(id, nil, &codex.Error{Code: codex.InvalidParams, Message: "invalid params"}); err != nil {
a.logger.Warn("failed to send invalid params response", zap.Error(err))
}
return return
} }
a.handleApprovalRequest(id, handler, threadID, p.ItemID, types.ActionTypeFileWrite, p.Path, map[string]interface{}{ a.handleApprovalRequest(id, handler, threadID, p.ItemID, types.ActionTypeFileWrite, p.Path, map[string]interface{}{
@@ -587,7 +591,9 @@ func (a *CodexAdapter) handleRequest(id interface{}, method string, params json.
default: default:
a.logger.Warn("unhandled request", zap.String("method", method)) a.logger.Warn("unhandled request", zap.String("method", method))
a.client.SendResponse(id, nil, &codex.Error{Code: codex.MethodNotFound, Message: "method not found"}) if err := a.client.SendResponse(id, nil, &codex.Error{Code: codex.MethodNotFound, Message: "method not found"}); err != nil {
a.logger.Warn("failed to send method not found response", zap.Error(err))
}
} }
} }
@@ -606,9 +612,10 @@ func (a *CodexAdapter) handleApprovalRequest(
options := make([]PermissionOption, len(optionStrings)) options := make([]PermissionOption, len(optionStrings))
for i, opt := range optionStrings { for i, opt := range optionStrings {
kind := "allow_once" kind := "allow_once"
if opt == "approveAlways" { switch opt {
case "approveAlways":
kind = "allow_always" kind = "allow_always"
} else if opt == "reject" { case "reject":
kind = "reject_once" kind = "reject_once"
} }
options[i] = PermissionOption{ options[i] = PermissionOption{
@@ -638,10 +645,12 @@ func (a *CodexAdapter) handleApprovalRequest(
if handler == nil { if handler == nil {
// Auto-approve if no handler // Auto-approve if no handler
a.logger.Info("auto-approving request (no handler)", zap.String("item_id", itemID)) a.logger.Info("auto-approving request (no handler)", zap.String("item_id", itemID))
a.client.SendResponse(id, &codex.ApprovalResponse{ if err := a.client.SendResponse(id, &codex.ApprovalResponse{
ItemID: itemID, ItemID: itemID,
Decision: "approve", Decision: "approve",
}, nil) }, nil); err != nil {
a.logger.Warn("failed to send approval response", zap.Error(err))
}
return return
} }
@@ -650,10 +659,12 @@ func (a *CodexAdapter) handleApprovalRequest(
resp, err := handler(ctx, req) resp, err := handler(ctx, req)
if err != nil { if err != nil {
a.logger.Error("permission handler error", zap.Error(err)) a.logger.Error("permission handler error", zap.Error(err))
a.client.SendResponse(id, &codex.ApprovalResponse{ if err := a.client.SendResponse(id, &codex.ApprovalResponse{
ItemID: itemID, ItemID: itemID,
Decision: "reject", Decision: "reject",
}, nil) }, nil); err != nil {
a.logger.Warn("failed to send reject response", zap.Error(err))
}
return return
} }
@@ -664,12 +675,13 @@ func (a *CodexAdapter) handleApprovalRequest(
decision = resp.OptionID decision = resp.OptionID
} }
a.client.SendResponse(id, &codex.ApprovalResponse{ if err := a.client.SendResponse(id, &codex.ApprovalResponse{
ItemID: itemID, ItemID: itemID,
Decision: decision, Decision: decision,
}, nil) }, nil); err != nil {
a.logger.Warn("failed to send approval response", zap.Error(err))
}
} }
// Verify interface implementation // Verify interface implementation
var _ AgentAdapter = (*CodexAdapter)(nil) var _ AgentAdapter = (*CodexAdapter)(nil)
+27 -33
View File
@@ -196,7 +196,11 @@ func (s *Server) handleACPStreamWS(c *gin.Context) {
s.logger.Error("WebSocket upgrade failed", zap.Error(err)) s.logger.Error("WebSocket upgrade failed", zap.Error(err))
return return
} }
defer conn.Close() defer func() {
if err := conn.Close(); err != nil {
s.logger.Debug("failed to close ACP stream websocket", zap.Error(err))
}
}()
s.logger.Info("ACP stream WebSocket connected") s.logger.Info("ACP stream WebSocket connected")
@@ -204,22 +208,15 @@ func (s *Server) handleACPStreamWS(c *gin.Context) {
updatesCh := s.procMgr.GetUpdates() updatesCh := s.procMgr.GetUpdates()
// Stream session notifications to WebSocket // Stream session notifications to WebSocket
for { for notification := range updatesCh {
select { data, err := json.Marshal(notification)
case notification, ok := <-updatesCh: if err != nil {
if !ok { s.logger.Error("failed to marshal notification", zap.Error(err))
return continue
} }
data, err := json.Marshal(notification) if err := conn.WriteMessage(websocket.TextMessage, data); err != nil {
if err != nil { return
s.logger.Error("failed to marshal notification", zap.Error(err))
continue
}
if err := conn.WriteMessage(websocket.TextMessage, data); err != nil {
return
}
} }
} }
} }
@@ -318,7 +315,11 @@ func (s *Server) handlePermissionStreamWS(c *gin.Context) {
s.logger.Error("WebSocket upgrade failed", zap.Error(err)) s.logger.Error("WebSocket upgrade failed", zap.Error(err))
return return
} }
defer conn.Close() defer func() {
if err := conn.Close(); err != nil {
s.logger.Debug("failed to close permission websocket", zap.Error(err))
}
}()
s.logger.Info("Permission stream WebSocket connected") s.logger.Info("Permission stream WebSocket connected")
@@ -326,23 +327,16 @@ func (s *Server) handlePermissionStreamWS(c *gin.Context) {
permissionCh := s.procMgr.GetPermissionRequests() permissionCh := s.procMgr.GetPermissionRequests()
// Stream permission notifications to WebSocket // Stream permission notifications to WebSocket
for { for notification := range permissionCh {
select { data, err := json.Marshal(notification)
case notification, ok := <-permissionCh: if err != nil {
if !ok { s.logger.Error("failed to marshal permission notification", zap.Error(err))
return continue
} }
data, err := json.Marshal(notification) if err := conn.WriteMessage(websocket.TextMessage, data); err != nil {
if err != nil { s.logger.Debug("WebSocket write error", zap.Error(err))
s.logger.Error("failed to marshal permission notification", zap.Error(err)) return
continue
}
if err := conn.WriteMessage(websocket.TextMessage, data); err != nil {
s.logger.Debug("WebSocket write error", zap.Error(err))
return
}
} }
} }
} }
@@ -400,28 +400,25 @@ func (m *ControlServer) handleACPStreamWS(c *gin.Context) {
m.logger.Error("WebSocket upgrade failed", zap.Error(err)) m.logger.Error("WebSocket upgrade failed", zap.Error(err))
return return
} }
defer conn.Close() defer func() {
if err := conn.Close(); err != nil {
m.logger.Debug("failed to close ACP stream websocket", zap.Error(err))
}
}()
m.logger.Info("ACP stream WebSocket connected") m.logger.Info("ACP stream WebSocket connected")
updatesCh := m.singleProcMgr.GetUpdates() updatesCh := m.singleProcMgr.GetUpdates()
for { for notification := range updatesCh {
select { data, err := json.Marshal(notification)
case notification, ok := <-updatesCh: if err != nil {
if !ok { m.logger.Error("failed to marshal notification", zap.Error(err))
return continue
} }
data, err := json.Marshal(notification) if err := conn.WriteMessage(websocket.TextMessage, data); err != nil {
if err != nil { return
m.logger.Error("failed to marshal notification", zap.Error(err))
continue
}
if err := conn.WriteMessage(websocket.TextMessage, data); err != nil {
return
}
} }
} }
} }
@@ -458,29 +455,26 @@ func (m *ControlServer) handlePermissionStreamWS(c *gin.Context) {
m.logger.Error("WebSocket upgrade failed", zap.Error(err)) m.logger.Error("WebSocket upgrade failed", zap.Error(err))
return return
} }
defer conn.Close() defer func() {
if err := conn.Close(); err != nil {
m.logger.Debug("failed to close permission websocket", zap.Error(err))
}
}()
m.logger.Info("Permission stream WebSocket connected") m.logger.Info("Permission stream WebSocket connected")
permissionCh := m.singleProcMgr.GetPermissionRequests() permissionCh := m.singleProcMgr.GetPermissionRequests()
for { for notification := range permissionCh {
select { data, err := json.Marshal(notification)
case notification, ok := <-permissionCh: if err != nil {
if !ok { m.logger.Error("failed to marshal permission notification", zap.Error(err))
return continue
} }
data, err := json.Marshal(notification) if err := conn.WriteMessage(websocket.TextMessage, data); err != nil {
if err != nil { m.logger.Debug("WebSocket write error", zap.Error(err))
m.logger.Error("failed to marshal permission notification", zap.Error(err)) return
continue
}
if err := conn.WriteMessage(websocket.TextMessage, data); err != nil {
m.logger.Debug("WebSocket write error", zap.Error(err))
return
}
} }
} }
} }
@@ -523,7 +517,11 @@ func (m *ControlServer) handleGitStatusStreamWS(c *gin.Context) {
m.logger.Error("WebSocket upgrade failed", zap.Error(err)) m.logger.Error("WebSocket upgrade failed", zap.Error(err))
return return
} }
defer conn.Close() defer func() {
if err := conn.Close(); err != nil {
m.logger.Debug("failed to close git status websocket", zap.Error(err))
}
}()
sub := m.singleProcMgr.GetWorkspaceTracker().SubscribeGitStatus() sub := m.singleProcMgr.GetWorkspaceTracker().SubscribeGitStatus()
defer m.singleProcMgr.GetWorkspaceTracker().UnsubscribeGitStatus(sub) defer m.singleProcMgr.GetWorkspaceTracker().UnsubscribeGitStatus(sub)
@@ -565,7 +563,11 @@ func (m *ControlServer) handleFilesStreamWS(c *gin.Context) {
m.logger.Error("WebSocket upgrade failed", zap.Error(err)) m.logger.Error("WebSocket upgrade failed", zap.Error(err))
return return
} }
defer conn.Close() defer func() {
if err := conn.Close(); err != nil {
m.logger.Debug("failed to close files websocket", zap.Error(err))
}
}()
sub := m.singleProcMgr.GetWorkspaceTracker().SubscribeFiles() sub := m.singleProcMgr.GetWorkspaceTracker().SubscribeFiles()
defer m.singleProcMgr.GetWorkspaceTracker().UnsubscribeFiles(sub) defer m.singleProcMgr.GetWorkspaceTracker().UnsubscribeFiles(sub)
@@ -607,7 +609,11 @@ func (m *ControlServer) handleFileChangesStreamWS(c *gin.Context) {
m.logger.Error("WebSocket upgrade failed", zap.Error(err)) m.logger.Error("WebSocket upgrade failed", zap.Error(err))
return return
} }
defer conn.Close() defer func() {
if err := conn.Close(); err != nil {
m.logger.Debug("failed to close file changes websocket", zap.Error(err))
}
}()
sub := m.singleProcMgr.GetWorkspaceTracker().SubscribeFileChanges() sub := m.singleProcMgr.GetWorkspaceTracker().SubscribeFileChanges()
defer m.singleProcMgr.GetWorkspaceTracker().UnsubscribeFileChanges(sub) defer m.singleProcMgr.GetWorkspaceTracker().UnsubscribeFileChanges(sub)
@@ -675,4 +681,4 @@ func (m *ControlServer) handleFileContent(c *gin.Context) {
} }
c.JSON(200, process.FileContentResponse{Path: path, Content: content, Size: size}) c.JSON(200, process.FileContentResponse{Path: path, Content: content, Size: size})
} }
@@ -16,7 +16,11 @@ func (s *Server) handleGitStatusStreamWS(c *gin.Context) {
s.logger.Error("WebSocket upgrade failed", zap.Error(err)) s.logger.Error("WebSocket upgrade failed", zap.Error(err))
return return
} }
defer conn.Close() defer func() {
if err := conn.Close(); err != nil {
s.logger.Debug("failed to close git status websocket", zap.Error(err))
}
}()
// Subscribe to git status updates // Subscribe to git status updates
sub := s.procMgr.GetWorkspaceTracker().SubscribeGitStatus() sub := s.procMgr.GetWorkspaceTracker().SubscribeGitStatus()
@@ -62,7 +66,11 @@ func (s *Server) handleFilesStreamWS(c *gin.Context) {
s.logger.Error("WebSocket upgrade failed", zap.Error(err)) s.logger.Error("WebSocket upgrade failed", zap.Error(err))
return return
} }
defer conn.Close() defer func() {
if err := conn.Close(); err != nil {
s.logger.Debug("failed to close files websocket", zap.Error(err))
}
}()
// Subscribe to file updates // Subscribe to file updates
sub := s.procMgr.GetWorkspaceTracker().SubscribeFiles() sub := s.procMgr.GetWorkspaceTracker().SubscribeFiles()
@@ -108,7 +116,11 @@ func (s *Server) handleFileChangesStreamWS(c *gin.Context) {
s.logger.Error("WebSocket upgrade failed", zap.Error(err)) s.logger.Error("WebSocket upgrade failed", zap.Error(err))
return return
} }
defer conn.Close() defer func() {
if err := conn.Close(); err != nil {
s.logger.Debug("failed to close file changes websocket", zap.Error(err))
}
}()
// Subscribe to file change notifications // Subscribe to file change notifications
sub := s.procMgr.GetWorkspaceTracker().SubscribeFileChanges() sub := s.procMgr.GetWorkspaceTracker().SubscribeFileChanges()
@@ -186,6 +198,8 @@ func (s *Server) handleFileContent(c *gin.Context) {
// mustParseInt parses a string to int, returns 0 on error // mustParseInt parses a string to int, returns 0 on error
func mustParseInt(s string) int { func mustParseInt(s string) int {
var n int var n int
json.Unmarshal([]byte(s), &n) if err := json.Unmarshal([]byte(s), &n); err != nil {
return 0
}
return n return n
} }
@@ -124,7 +124,9 @@ func (m *Manager) CreateInstance(ctx context.Context, req *CreateRequest) (*Crea
// Default to a simple handler if no factory is set // Default to a simple handler if no factory is set
handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable) w.WriteHeader(http.StatusServiceUnavailable)
w.Write([]byte("server factory not configured")) if _, err := w.Write([]byte("server factory not configured")); err != nil {
m.logger.Debug("failed to write default response", zap.Error(err))
}
}) })
} }
@@ -277,4 +279,3 @@ func collectEnvForInstance(env map[string]string) []string {
} }
return result return result
} }
@@ -312,7 +312,9 @@ func (m *Manager) Stop(ctx context.Context) error {
// Close the adapter // Close the adapter
if m.adapter != nil { if m.adapter != nil {
m.adapter.Close() if err := m.adapter.Close(); err != nil {
m.logger.Debug("failed to close adapter", zap.Error(err))
}
} }
// Close stop channel to signal readers // Close stop channel to signal readers
@@ -322,7 +324,9 @@ func (m *Manager) Stop(ctx context.Context) error {
// Close stdin to signal EOF to agent // Close stdin to signal EOF to agent
if m.stdin != nil { if m.stdin != nil {
m.stdin.Close() if err := m.stdin.Close(); err != nil {
m.logger.Debug("failed to close stdin", zap.Error(err))
}
} }
// Wait for process to exit with timeout // Wait for process to exit with timeout
@@ -339,7 +343,9 @@ func (m *Manager) Stop(ctx context.Context) error {
// Force kill // Force kill
if m.cmd != nil && m.cmd.Process != nil { if m.cmd != nil && m.cmd.Process != nil {
m.logger.Warn("force killing agent process") m.logger.Warn("force killing agent process")
m.cmd.Process.Kill() if err := m.cmd.Process.Kill(); err != nil {
m.logger.Warn("failed to kill agent process", zap.Error(err))
}
} }
} }
@@ -563,4 +569,3 @@ func (m *Manager) GetPendingPermissions() []*PendingPermission {
return result return result
} }
@@ -86,7 +86,9 @@ func (wt *WorkspaceTracker) Start(ctx context.Context) {
func (wt *WorkspaceTracker) Stop() { func (wt *WorkspaceTracker) Stop() {
close(wt.stopCh) close(wt.stopCh)
if wt.watcher != nil { if wt.watcher != nil {
wt.watcher.Close() if err := wt.watcher.Close(); err != nil {
wt.logger.Debug("failed to close watcher", zap.Error(err))
}
} }
wt.wg.Wait() wt.wg.Wait()
wt.logger.Info("workspace tracker stopped") wt.logger.Info("workspace tracker stopped")
@@ -611,7 +613,9 @@ func (wt *WorkspaceTracker) watchFilesystem(ctx context.Context) {
// If a directory was created, watch it // If a directory was created, watch it
if event.Op&fsnotify.Create == fsnotify.Create { if event.Op&fsnotify.Create == fsnotify.Create {
if info, err := os.Stat(event.Name); err == nil && info.IsDir() { if info, err := os.Stat(event.Name); err == nil && info.IsDir() {
wt.addDirectoryRecursive(event.Name) if err := wt.addDirectoryRecursive(event.Name); err != nil {
wt.logger.Debug("failed to watch new directory", zap.Error(err))
}
} }
} }
@@ -752,7 +756,9 @@ func (wt *WorkspaceTracker) GetFileContent(reqPath string) (string, int64, error
if err != nil { if err != nil {
return "", 0, fmt.Errorf("failed to open file: %w", err) return "", 0, fmt.Errorf("failed to open file: %w", err)
} }
defer file.Close() defer func() {
_ = file.Close()
}()
content, err := io.ReadAll(file) content, err := io.ReadAll(file)
if err != nil { if err != nil {
@@ -763,4 +769,3 @@ func (wt *WorkspaceTracker) GetFileContent(reqPath string) (string, int64, error
} }
+3 -1
View File
@@ -226,7 +226,9 @@ func (b *MemoryEventBus) Request(ctx context.Context, subject string, event *Eve
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create reply subscription: %w", err) return nil, fmt.Errorf("failed to create reply subscription: %w", err)
} }
defer sub.Unsubscribe() defer func() {
_ = sub.Unsubscribe()
}()
// Add reply subject to event data // Add reply subject to event data
if event.Data == nil { if event.Data == nil {
+34 -11
View File
@@ -49,7 +49,9 @@ func TestMemoryEventBus_PublishSubscribe(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Subscribe failed: %v", err) t.Fatalf("Subscribe failed: %v", err)
} }
defer sub.Unsubscribe() defer func() {
_ = sub.Unsubscribe()
}()
event := NewEvent("test.type", "test-source", map[string]interface{}{"key": "value"}) event := NewEvent("test.type", "test-source", map[string]interface{}{"key": "value"})
if err := bus.Publish(ctx, "test.subject", event); err != nil { if err := bus.Publish(ctx, "test.subject", event); err != nil {
@@ -86,7 +88,9 @@ func TestMemoryEventBus_MultipleSubscribers(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Subscribe %d failed: %v", i, err) t.Fatalf("Subscribe %d failed: %v", i, err)
} }
defer sub.Unsubscribe() defer func() {
_ = sub.Unsubscribe()
}()
} }
event := NewEvent("test.type", "test-source", nil) event := NewEvent("test.type", "test-source", nil)
@@ -159,7 +163,9 @@ func TestMemoryEventBus_SingleTokenWildcard(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Subscribe failed: %v", err) t.Fatalf("Subscribe failed: %v", err)
} }
defer sub.Unsubscribe() defer func() {
_ = sub.Unsubscribe()
}()
// Should match - "user" fills the * slot // Should match - "user" fills the * slot
event1 := NewEvent("user.created", "test", nil) event1 := NewEvent("user.created", "test", nil)
@@ -201,7 +207,9 @@ func TestMemoryEventBus_MultiTokenWildcard(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Subscribe failed: %v", err) t.Fatalf("Subscribe failed: %v", err)
} }
defer sub.Unsubscribe() defer func() {
_ = sub.Unsubscribe()
}()
// Should match - single remaining token // Should match - single remaining token
event1 := NewEvent("email", "test", nil) event1 := NewEvent("email", "test", nil)
@@ -238,7 +246,9 @@ func TestMemoryEventBus_WildcardNoMatch(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Subscribe failed: %v", err) t.Fatalf("Subscribe failed: %v", err)
} }
defer sub.Unsubscribe() defer func() {
_ = sub.Unsubscribe()
}()
// This should NOT match - missing middle token // This should NOT match - missing middle token
event := NewEvent("test", "test", nil) event := NewEvent("test", "test", nil)
@@ -269,7 +279,9 @@ func TestMemoryEventBus_ExactMatch(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Subscribe failed: %v", err) t.Fatalf("Subscribe failed: %v", err)
} }
defer sub.Unsubscribe() defer func() {
_ = sub.Unsubscribe()
}()
// Should match exactly // Should match exactly
event1 := NewEvent("test", "test", nil) event1 := NewEvent("test", "test", nil)
@@ -312,7 +324,9 @@ func TestMemoryEventBus_QueueSubscribe(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("QueueSubscribe %d failed: %v", i, err) t.Fatalf("QueueSubscribe %d failed: %v", i, err)
} }
defer sub.Unsubscribe() defer func() {
_ = sub.Unsubscribe()
}()
} }
// Publish multiple events // Publish multiple events
@@ -338,6 +352,7 @@ func TestMemoryEventBus_ConcurrentAccess(t *testing.T) {
ctx := context.Background() ctx := context.Background()
var receivedCount int32 var receivedCount int32
var publishErrorCount int32
var wg sync.WaitGroup var wg sync.WaitGroup
// Subscribe // Subscribe
@@ -348,7 +363,9 @@ func TestMemoryEventBus_ConcurrentAccess(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Subscribe failed: %v", err) t.Fatalf("Subscribe failed: %v", err)
} }
defer sub.Unsubscribe() defer func() {
_ = sub.Unsubscribe()
}()
// Publish concurrently from multiple goroutines // Publish concurrently from multiple goroutines
numGoroutines := 10 numGoroutines := 10
@@ -360,12 +377,17 @@ func TestMemoryEventBus_ConcurrentAccess(t *testing.T) {
defer wg.Done() defer wg.Done()
for j := 0; j < eventsPerGoroutine; j++ { for j := 0; j < eventsPerGoroutine; j++ {
event := NewEvent("test.type", "test-source", nil) event := NewEvent("test.type", "test-source", nil)
bus.Publish(ctx, "test.concurrent", event) if err := bus.Publish(ctx, "test.concurrent", event); err != nil {
atomic.AddInt32(&publishErrorCount, 1)
}
} }
}() }()
} }
wg.Wait() wg.Wait()
if publishErrorCount > 0 {
t.Errorf("publish errors: %d", publishErrorCount)
}
time.Sleep(200 * time.Millisecond) // Allow handlers to complete time.Sleep(200 * time.Millisecond) // Allow handlers to complete
expectedCount := int32(numGoroutines * eventsPerGoroutine) expectedCount := int32(numGoroutines * eventsPerGoroutine)
@@ -426,7 +448,9 @@ func TestMemoryEventBus_Request(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Subscribe failed: %v", err) t.Fatalf("Subscribe failed: %v", err)
} }
defer sub.Unsubscribe() defer func() {
_ = sub.Unsubscribe()
}()
// Make a request // Make a request
request := NewEvent("echo.request", "requester", map[string]interface{}{ request := NewEvent("echo.request", "requester", map[string]interface{}{
@@ -484,4 +508,3 @@ func TestNewEvent(t *testing.T) {
t.Error("Expected timestamp to be set correctly") t.Error("Expected timestamp to be set correctly")
} }
} }
@@ -57,14 +57,17 @@ func NewClient(id string, conn *websocket.Conn, hub *Hub, log *logger.Logger) *C
func (c *Client) ReadPump(ctx context.Context) { func (c *Client) ReadPump(ctx context.Context) {
defer func() { defer func() {
c.hub.Unregister(c) c.hub.Unregister(c)
c.conn.Close() if err := c.conn.Close(); err != nil {
c.logger.Debug("failed to close websocket connection", zap.Error(err))
}
}() }()
c.conn.SetReadLimit(maxMessageSize) c.conn.SetReadLimit(maxMessageSize)
c.conn.SetReadDeadline(time.Now().Add(pongWait)) if err := c.conn.SetReadDeadline(time.Now().Add(pongWait)); err != nil {
c.logger.Debug("failed to set read deadline", zap.Error(err))
}
c.conn.SetPongHandler(func(string) error { c.conn.SetPongHandler(func(string) error {
c.conn.SetReadDeadline(time.Now().Add(pongWait)) return c.conn.SetReadDeadline(time.Now().Add(pongWait))
return nil
}) })
for { for {
@@ -310,16 +313,22 @@ func (c *Client) WritePump() {
ticker := time.NewTicker(pingPeriod) ticker := time.NewTicker(pingPeriod)
defer func() { defer func() {
ticker.Stop() ticker.Stop()
c.conn.Close() if err := c.conn.Close(); err != nil {
c.logger.Debug("failed to close websocket connection", zap.Error(err))
}
}() }()
for { for {
select { select {
case message, ok := <-c.send: case message, ok := <-c.send:
c.conn.SetWriteDeadline(time.Now().Add(writeWait)) if err := c.conn.SetWriteDeadline(time.Now().Add(writeWait)); err != nil {
c.logger.Debug("failed to set write deadline", zap.Error(err))
}
if !ok { if !ok {
// Hub closed the channel // Hub closed the channel
c.conn.WriteMessage(websocket.CloseMessage, []byte{}) if err := c.conn.WriteMessage(websocket.CloseMessage, []byte{}); err != nil {
c.logger.Debug("failed to write close message", zap.Error(err))
}
return return
} }
@@ -327,13 +336,25 @@ func (c *Client) WritePump() {
if err != nil { if err != nil {
return return
} }
w.Write(message) if _, err := w.Write(message); err != nil {
c.logger.Debug("failed to write websocket message", zap.Error(err))
_ = w.Close()
return
}
// Batch additional queued messages // Batch additional queued messages
n := len(c.send) n := len(c.send)
for i := 0; i < n; i++ { for i := 0; i < n; i++ {
w.Write([]byte{'\n'}) if _, err := w.Write([]byte{'\n'}); err != nil {
w.Write(<-c.send) c.logger.Debug("failed to write websocket delimiter", zap.Error(err))
_ = w.Close()
return
}
if _, err := w.Write(<-c.send); err != nil {
c.logger.Debug("failed to write queued websocket message", zap.Error(err))
_ = w.Close()
return
}
} }
if err := w.Close(); err != nil { if err := w.Close(); err != nil {
@@ -341,7 +362,9 @@ func (c *Client) WritePump() {
} }
case <-ticker.C: case <-ticker.C:
c.conn.SetWriteDeadline(time.Now().Add(writeWait)) if err := c.conn.SetWriteDeadline(time.Now().Add(writeWait)); err != nil {
c.logger.Debug("failed to set write deadline", zap.Error(err))
}
if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil { if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
return return
} }
@@ -61,7 +61,11 @@ func NewTestServer(t *testing.T) *TestServer {
if err != nil { if err != nil {
t.Fatalf("failed to create test repository: %v", err) t.Fatalf("failed to create test repository: %v", err)
} }
t.Cleanup(func() { taskRepo.Close() }) t.Cleanup(func() {
if err := taskRepo.Close(); err != nil {
t.Errorf("failed to close task repo: %v", err)
}
})
// Initialize task service // Initialize task service
taskSvc := taskservice.NewService(taskRepo, eventBus, log, taskservice.RepositoryDiscoveryConfig{}) taskSvc := taskservice.NewService(taskRepo, eventBus, log, taskservice.RepositoryDiscoveryConfig{})
@@ -109,7 +113,9 @@ func NewTestServer(t *testing.T) *TestServer {
func (ts *TestServer) Close() { func (ts *TestServer) Close() {
ts.cancelFunc() ts.cancelFunc()
ts.Server.Close() ts.Server.Close()
ts.TaskRepo.Close() if err := ts.TaskRepo.Close(); err != nil {
ts.Logger.Error("failed to close task repo: " + err.Error())
}
ts.EventBus.Close() ts.EventBus.Close()
} }
@@ -216,7 +222,9 @@ func (c *WSClient) readPump() {
// Close closes the WebSocket connection // Close closes the WebSocket connection
func (c *WSClient) Close() { func (c *WSClient) Close() {
c.conn.Close() if err := c.conn.Close(); err != nil {
c.t.Logf("failed to close websocket: %v", err)
}
<-c.done <-c.done
} }
@@ -476,7 +484,9 @@ func TestTaskCRUD(t *testing.T) {
// Debug: print error if there is one // Debug: print error if there is one
if resp.Type == ws.MessageTypeError { if resp.Type == ws.MessageTypeError {
var errPayload ws.ErrorPayload var errPayload ws.ErrorPayload
resp.ParsePayload(&errPayload) if err := resp.ParsePayload(&errPayload); err != nil {
t.Fatalf("failed to parse error payload: %v", err)
}
t.Logf("Error response: %+v", errPayload) t.Logf("Error response: %+v", errPayload)
} }
@@ -587,7 +597,9 @@ func TestTaskStateTransitions(t *testing.T) {
"name": "State Test Board", "name": "State Test Board",
}) })
var boardPayload map[string]interface{} var boardPayload map[string]interface{}
boardResp.ParsePayload(&boardPayload) if err := boardResp.ParsePayload(&boardPayload); err != nil {
t.Fatalf("failed to parse board payload: %v", err)
}
boardID := boardPayload["id"].(string) boardID := boardPayload["id"].(string)
colResp, _ := client.SendRequest("col-1", ws.ActionColumnCreate, map[string]interface{}{ colResp, _ := client.SendRequest("col-1", ws.ActionColumnCreate, map[string]interface{}{
@@ -596,7 +608,9 @@ func TestTaskStateTransitions(t *testing.T) {
"position": 0, "position": 0,
}) })
var colPayload map[string]interface{} var colPayload map[string]interface{}
colResp.ParsePayload(&colPayload) if err := colResp.ParsePayload(&colPayload); err != nil {
t.Fatalf("failed to parse column payload: %v", err)
}
columnID := colPayload["id"].(string) columnID := colPayload["id"].(string)
taskResp, _ := client.SendRequest("task-1", ws.ActionTaskCreate, map[string]interface{}{ taskResp, _ := client.SendRequest("task-1", ws.ActionTaskCreate, map[string]interface{}{
@@ -609,7 +623,9 @@ func TestTaskStateTransitions(t *testing.T) {
"base_branch": "main", "base_branch": "main",
}) })
var taskPayload map[string]interface{} var taskPayload map[string]interface{}
taskResp.ParsePayload(&taskPayload) if err := taskResp.ParsePayload(&taskPayload); err != nil {
t.Fatalf("failed to parse task payload: %v", err)
}
taskID := taskPayload["id"].(string) taskID := taskPayload["id"].(string)
// Test state transitions // Test state transitions
@@ -665,7 +681,9 @@ func TestTaskMove(t *testing.T) {
"name": "Move Test Board", "name": "Move Test Board",
}) })
var boardPayload map[string]interface{} var boardPayload map[string]interface{}
boardResp.ParsePayload(&boardPayload) if err := boardResp.ParsePayload(&boardPayload); err != nil {
t.Fatalf("failed to parse board payload: %v", err)
}
boardID := boardPayload["id"].(string) boardID := boardPayload["id"].(string)
col1Resp, _ := client.SendRequest("col-1", ws.ActionColumnCreate, map[string]interface{}{ col1Resp, _ := client.SendRequest("col-1", ws.ActionColumnCreate, map[string]interface{}{
@@ -674,7 +692,9 @@ func TestTaskMove(t *testing.T) {
"position": 0, "position": 0,
}) })
var col1Payload map[string]interface{} var col1Payload map[string]interface{}
col1Resp.ParsePayload(&col1Payload) if err := col1Resp.ParsePayload(&col1Payload); err != nil {
t.Fatalf("failed to parse column payload: %v", err)
}
col1ID := col1Payload["id"].(string) col1ID := col1Payload["id"].(string)
col2Resp, _ := client.SendRequest("col-2", ws.ActionColumnCreate, map[string]interface{}{ col2Resp, _ := client.SendRequest("col-2", ws.ActionColumnCreate, map[string]interface{}{
@@ -683,7 +703,9 @@ func TestTaskMove(t *testing.T) {
"position": 1, "position": 1,
}) })
var col2Payload map[string]interface{} var col2Payload map[string]interface{}
col2Resp.ParsePayload(&col2Payload) if err := col2Resp.ParsePayload(&col2Payload); err != nil {
t.Fatalf("failed to parse column payload: %v", err)
}
col2ID := col2Payload["id"].(string) col2ID := col2Payload["id"].(string)
// Create task in first column // Create task in first column
@@ -696,7 +718,9 @@ func TestTaskMove(t *testing.T) {
"base_branch": "main", "base_branch": "main",
}) })
var taskPayload map[string]interface{} var taskPayload map[string]interface{}
taskResp.ParsePayload(&taskPayload) if err := taskResp.ParsePayload(&taskPayload); err != nil {
t.Fatalf("failed to parse task payload: %v", err)
}
taskID := taskPayload["id"].(string) taskID := taskPayload["id"].(string)
// Move task to second column // Move task to second column
@@ -913,7 +937,9 @@ func TestTaskSubscription(t *testing.T) {
"name": "Sub Test Board", "name": "Sub Test Board",
}) })
var boardPayload map[string]interface{} var boardPayload map[string]interface{}
boardResp.ParsePayload(&boardPayload) if err := boardResp.ParsePayload(&boardPayload); err != nil {
t.Fatalf("failed to parse board payload: %v", err)
}
boardID := boardPayload["id"].(string) boardID := boardPayload["id"].(string)
colResp, _ := client.SendRequest("col-1", ws.ActionColumnCreate, map[string]interface{}{ colResp, _ := client.SendRequest("col-1", ws.ActionColumnCreate, map[string]interface{}{
@@ -922,7 +948,9 @@ func TestTaskSubscription(t *testing.T) {
"position": 0, "position": 0,
}) })
var colPayload map[string]interface{} var colPayload map[string]interface{}
colResp.ParsePayload(&colPayload) if err := colResp.ParsePayload(&colPayload); err != nil {
t.Fatalf("failed to parse column payload: %v", err)
}
columnID := colPayload["id"].(string) columnID := colPayload["id"].(string)
taskResp, _ := client.SendRequest("task-1", ws.ActionTaskCreate, map[string]interface{}{ taskResp, _ := client.SendRequest("task-1", ws.ActionTaskCreate, map[string]interface{}{
@@ -934,7 +962,9 @@ func TestTaskSubscription(t *testing.T) {
"base_branch": "main", "base_branch": "main",
}) })
var taskPayload map[string]interface{} var taskPayload map[string]interface{}
taskResp.ParsePayload(&taskPayload) if err := taskResp.ParsePayload(&taskPayload); err != nil {
t.Fatalf("failed to parse task payload: %v", err)
}
taskID := taskPayload["id"].(string) taskID := taskPayload["id"].(string)
// Subscribe to the task // Subscribe to the task
@@ -989,7 +1019,9 @@ func TestConcurrentRequests(t *testing.T) {
"name": "Concurrent Test Board", "name": "Concurrent Test Board",
}) })
var boardPayload map[string]interface{} var boardPayload map[string]interface{}
boardResp.ParsePayload(&boardPayload) if err := boardResp.ParsePayload(&boardPayload); err != nil {
t.Fatalf("failed to parse board payload: %v", err)
}
boardID := boardPayload["id"].(string) boardID := boardPayload["id"].(string)
colResp, _ := client.SendRequest("col-1", ws.ActionColumnCreate, map[string]interface{}{ colResp, _ := client.SendRequest("col-1", ws.ActionColumnCreate, map[string]interface{}{
@@ -998,7 +1030,9 @@ func TestConcurrentRequests(t *testing.T) {
"position": 0, "position": 0,
}) })
var colPayload map[string]interface{} var colPayload map[string]interface{}
colResp.ParsePayload(&colPayload) if err := colResp.ParsePayload(&colPayload); err != nil {
t.Fatalf("failed to parse column payload: %v", err)
}
columnID := colPayload["id"].(string) columnID := colPayload["id"].(string)
// Create multiple tasks concurrently // Create multiple tasks concurrently
@@ -340,7 +340,9 @@ func (s *SimulatedAgentManagerClient) PromptAgent(ctx context.Context, agentInst
event := bus.NewEvent(events.ACPMessage, "simulated-agent", msgData) event := bus.NewEvent(events.ACPMessage, "simulated-agent", msgData)
subject := events.BuildACPSubject(instance.taskID) subject := events.BuildACPSubject(instance.taskID)
s.eventBus.Publish(context.Background(), subject, event) if err := s.eventBus.Publish(context.Background(), subject, event); err != nil {
s.logger.Warn("failed to publish simulated ACP message", zap.Error(err))
}
}() }()
return &executor.PromptResult{ return &executor.PromptResult{
@@ -474,7 +476,11 @@ func NewOrchestratorTestServer(t *testing.T) *OrchestratorTestServer {
if err != nil { if err != nil {
t.Fatalf("failed to create test repository: %v", err) t.Fatalf("failed to create test repository: %v", err)
} }
t.Cleanup(func() { taskRepo.Close() }) t.Cleanup(func() {
if err := taskRepo.Close(); err != nil {
t.Errorf("failed to close task repo: %v", err)
}
})
// Initialize task service // Initialize task service
taskSvc := taskservice.NewService(taskRepo, eventBus, log, taskservice.RepositoryDiscoveryConfig{}) taskSvc := taskservice.NewService(taskRepo, eventBus, log, taskservice.RepositoryDiscoveryConfig{})
@@ -554,11 +560,15 @@ func NewOrchestratorTestServer(t *testing.T) *OrchestratorTestServer {
// Close shuts down the test server // Close shuts down the test server
func (ts *OrchestratorTestServer) Close() { func (ts *OrchestratorTestServer) Close() {
ts.OrchestratorSvc.Stop() if err := ts.OrchestratorSvc.Stop(); err != nil {
ts.Logger.Warn("failed to stop orchestrator", zap.Error(err))
}
ts.AgentManager.Close() ts.AgentManager.Close()
ts.cancelFunc() ts.cancelFunc()
ts.Server.Close() ts.Server.Close()
ts.TaskRepo.Close() if err := ts.TaskRepo.Close(); err != nil {
ts.Logger.Warn("failed to close task repo", zap.Error(err))
}
ts.EventBus.Close() ts.EventBus.Close()
} }
@@ -657,8 +667,7 @@ func createOrchestratorWorkspace(t *testing.T, client *OrchestratorWSClient) str
require.NoError(t, err) require.NoError(t, err)
var payload map[string]interface{} var payload map[string]interface{}
err = resp.ParsePayload(&payload) require.NoError(t, resp.ParsePayload(&payload))
require.NoError(t, err)
return payload["id"].(string) return payload["id"].(string)
} }
@@ -693,7 +702,9 @@ func (c *OrchestratorWSClient) readPump() {
// Close closes the WebSocket connection // Close closes the WebSocket connection
func (c *OrchestratorWSClient) Close() { func (c *OrchestratorWSClient) Close() {
c.conn.Close() if err := c.conn.Close(); err != nil {
c.t.Logf("failed to close websocket: %v", err)
}
<-c.done <-c.done
} }
@@ -1020,7 +1031,10 @@ func TestOrchestratorConcurrentTasks(t *testing.T) {
} }
var payload map[string]interface{} var payload map[string]interface{}
resp.ParsePayload(&payload) if err := resp.ParsePayload(&payload); err != nil {
t.Logf("failed to parse start response: %v", err)
return
}
results <- payload results <- payload
}(i, taskID) }(i, taskID)
} }
@@ -126,7 +126,11 @@ func createTestExecutor(t *testing.T, agentMgr *mockAgentManager, log *logger.Lo
if err != nil { if err != nil {
t.Fatalf("failed to create test repository: %v", err) t.Fatalf("failed to create test repository: %v", err)
} }
t.Cleanup(func() { repo.Close() }) t.Cleanup(func() {
if err := repo.Close(); err != nil {
t.Errorf("failed to close repo: %v", err)
}
})
cfg := executor.ExecutorConfig{ cfg := executor.ExecutorConfig{
MaxConcurrent: maxConcurrent, MaxConcurrent: maxConcurrent,
WorktreeEnabled: true, WorktreeEnabled: true,
@@ -215,7 +219,9 @@ func TestStartAlreadyRunning(t *testing.T) {
s := NewScheduler(q, exec, taskRepo, log, DefaultSchedulerConfig()) s := NewScheduler(q, exec, taskRepo, log, DefaultSchedulerConfig())
_ = s.Start(context.Background()) _ = s.Start(context.Background())
defer s.Stop() defer func() {
_ = s.Stop()
}()
err := s.Start(context.Background()) err := s.Start(context.Background())
if err != ErrSchedulerAlreadyRunning { if err != ErrSchedulerAlreadyRunning {
@@ -204,7 +204,9 @@ func (s *Service) Start(ctx context.Context) error {
// Start the scheduler processing loop // Start the scheduler processing loop
if err := s.scheduler.Start(ctx); err != nil { if err := s.scheduler.Start(ctx); err != nil {
s.watcher.Stop() if stopErr := s.watcher.Stop(); stopErr != nil {
s.logger.Warn("failed to stop watcher after scheduler start failure", zap.Error(stopErr))
}
s.mu.Lock() s.mu.Lock()
s.running = false s.running = false
s.mu.Unlock() s.mu.Unlock()
@@ -640,7 +642,7 @@ func buildACPMetadata(msg *protocol.Message) map[string]interface{} {
"provider_type": string(msg.Type), "provider_type": string(msg.Type),
"provider_agent": msg.AgentID, "provider_agent": msg.AgentID,
} }
if msg.Data != nil && len(msg.Data) > 0 { if len(msg.Data) > 0 {
metadata["provider_data"] = msg.Data metadata["provider_data"] = msg.Data
} }
return metadata return metadata
@@ -59,7 +59,9 @@ func (b *mockEventBus) Publish(ctx context.Context, subject string, event *bus.E
// Call handlers for matching subjects // Call handlers for matching subjects
if handlers, ok := b.subscriptions[subject]; ok { if handlers, ok := b.subscriptions[subject]; ok {
for _, handler := range handlers { for _, handler := range handlers {
go handler(ctx, event) go func(handler bus.EventHandler) {
_ = handler(ctx, event)
}(handler)
} }
} }
@@ -67,7 +69,9 @@ func (b *mockEventBus) Publish(ctx context.Context, subject string, event *bus.E
for _, queueGroup := range b.queuedSubs { for _, queueGroup := range b.queuedSubs {
if handlers, ok := queueGroup[subject]; ok && len(handlers) > 0 { if handlers, ok := queueGroup[subject]; ok && len(handlers) > 0 {
// Deliver to first handler (simplified) // Deliver to first handler (simplified)
go handlers[0](ctx, event) go func(handler bus.EventHandler) {
_ = handler(ctx, event)
}(handlers[0])
} }
} }
@@ -217,7 +221,9 @@ func TestTaskEventHandling(t *testing.T) {
w := NewWatcher(eventBus, handlers, log) w := NewWatcher(eventBus, handlers, log)
_ = w.Start(context.Background()) _ = w.Start(context.Background())
defer w.Stop() defer func() {
_ = w.Stop()
}()
// Simulate publishing a task state changed event // Simulate publishing a task state changed event
oldState := v1.TaskStateTODO oldState := v1.TaskStateTODO
@@ -262,7 +268,9 @@ func TestAgentEventHandling(t *testing.T) {
w := NewWatcher(eventBus, handlers, log) w := NewWatcher(eventBus, handlers, log)
_ = w.Start(context.Background()) _ = w.Start(context.Background())
defer w.Stop() defer func() {
_ = w.Stop()
}()
// Simulate publishing an agent completed event // Simulate publishing an agent completed event
exitCode := 0 exitCode := 0
@@ -312,7 +320,9 @@ func TestACPMessageHandling(t *testing.T) {
w := NewWatcher(eventBus, handlers, log) w := NewWatcher(eventBus, handlers, log)
_ = w.Start(context.Background()) _ = w.Start(context.Background())
defer w.Stop() defer func() {
_ = w.Stop()
}()
// Simulate publishing an ACP message // Simulate publishing an ACP message
event := bus.NewEvent(events.BuildACPSubject("task-789"), "test", map[string]interface{}{ event := bus.NewEvent(events.BuildACPSubject("task-789"), "test", map[string]interface{}{
@@ -407,4 +417,3 @@ func TestIsRunning(t *testing.T) {
t.Error("watcher should not be running after Stop") t.Error("watcher should not be running after Stop")
} }
} }
@@ -626,15 +626,6 @@ func isSessionActive(status models.AgentSessionState) bool {
status == models.AgentSessionStateStarting status == models.AgentSessionStateStarting
} }
func (r *MemoryRepository) getTaskPlacement(taskID, boardID string) (*taskPlacement, bool) {
placements, ok := r.taskPlacements[taskID]
if !ok {
return nil, false
}
placement, ok := placements[boardID]
return placement, ok
}
// Message operations // Message operations
// CreateMessage creates a new message // CreateMessage creates a new message
+20 -43
View File
@@ -48,7 +48,9 @@ func NewSQLiteRepository(dbPath string) (*SQLiteRepository, error) {
// Initialize schema // Initialize schema
if err := repo.initSchema(); err != nil { if err := repo.initSchema(); err != nil {
db.Close() if closeErr := db.Close(); closeErr != nil {
return nil, fmt.Errorf("failed to close database after schema error: %w", closeErr)
}
return nil, fmt.Errorf("failed to initialize schema: %w", err) return nil, fmt.Errorf("failed to initialize schema: %w", err)
} }
@@ -342,7 +344,7 @@ func (r *SQLiteRepository) columnExists(table, column string) (bool, error) {
if err != nil { if err != nil {
return false, err return false, err
} }
defer rows.Close() defer func() { _ = rows.Close() }()
for rows.Next() { for rows.Next() {
var cid int var cid int
@@ -589,26 +591,6 @@ func (r *SQLiteRepository) ensureWorkspaceIndexes() error {
return nil return nil
} }
func (r *SQLiteRepository) backfillTaskMappings() error {
ctx := context.Background()
_, err := r.db.ExecContext(ctx, `
INSERT OR IGNORE INTO task_boards (task_id, board_id)
SELECT id, board_id FROM tasks WHERE board_id != ''
`)
if err != nil {
return err
}
_, err = r.db.ExecContext(ctx, `
INSERT OR IGNORE INTO task_columns (task_id, board_id, column_id, position)
SELECT id, board_id, column_id, position
FROM tasks
WHERE board_id != '' AND column_id != ''
`)
return err
}
// Close closes the database connection // Close closes the database connection
func (r *SQLiteRepository) Close() error { func (r *SQLiteRepository) Close() error {
return r.db.Close() return r.db.Close()
@@ -733,7 +715,7 @@ func (r *SQLiteRepository) ListWorkspaces(ctx context.Context) ([]*models.Worksp
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer func() { _ = rows.Close() }()
var result []*models.Workspace var result []*models.Workspace
for rows.Next() { for rows.Next() {
@@ -794,7 +776,9 @@ func (r *SQLiteRepository) CreateTask(ctx context.Context, task *models.Task) er
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, task.ID, task.WorkspaceID, task.BoardID, task.ColumnID, task.Title, task.Description, task.State, task.Priority, task.RepositoryID, task.BaseBranch, task.AssignedTo, task.Position, string(metadata), task.CreatedAt, task.UpdatedAt) `, task.ID, task.WorkspaceID, task.BoardID, task.ColumnID, task.Title, task.Description, task.State, task.Priority, task.RepositoryID, task.BaseBranch, task.AssignedTo, task.Position, string(metadata), task.CreatedAt, task.UpdatedAt)
if err != nil { if err != nil {
tx.Rollback() if rollbackErr := tx.Rollback(); rollbackErr != nil {
return fmt.Errorf("failed to rollback task insert: %w", rollbackErr)
}
return err return err
} }
@@ -875,7 +859,7 @@ func (r *SQLiteRepository) ListTasks(ctx context.Context, boardID string) ([]*mo
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer func() { _ = rows.Close() }()
return r.scanTasks(rows) return r.scanTasks(rows)
} }
@@ -890,7 +874,7 @@ func (r *SQLiteRepository) ListTasksByColumn(ctx context.Context, columnID strin
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer func() { _ = rows.Close() }()
return r.scanTasks(rows) return r.scanTasks(rows)
} }
@@ -960,13 +944,6 @@ func (r *SQLiteRepository) RemoveTaskFromBoard(ctx context.Context, taskID, boar
return err return err
} }
func (r *SQLiteRepository) addTaskToBoardTx(ctx context.Context, tx *sql.Tx, taskID, boardID, columnID string, position int) error {
_, err := tx.ExecContext(ctx, `
UPDATE tasks SET board_id = ?, column_id = ?, position = ?, updated_at = ? WHERE id = ?
`, boardID, columnID, position, time.Now().UTC(), taskID)
return err
}
// Board operations // Board operations
// CreateBoard creates a new board // CreateBoard creates a new board
@@ -1049,7 +1026,7 @@ func (r *SQLiteRepository) ListBoards(ctx context.Context, workspaceID string) (
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer func() { _ = rows.Close() }()
var result []*models.Board var result []*models.Board
for rows.Next() { for rows.Next() {
@@ -1153,7 +1130,7 @@ func (r *SQLiteRepository) ListColumns(ctx context.Context, boardID string) ([]*
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer func() { _ = rows.Close() }()
var result []*models.Column var result []*models.Column
for rows.Next() { for rows.Next() {
@@ -1242,7 +1219,7 @@ func (r *SQLiteRepository) ListMessages(ctx context.Context, sessionID string) (
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer func() { _ = rows.Close() }()
var result []*models.Message var result []*models.Message
for rows.Next() { for rows.Next() {
@@ -1327,7 +1304,7 @@ func (r *SQLiteRepository) ListMessagesPaginated(ctx context.Context, sessionID
if err != nil { if err != nil {
return nil, false, err return nil, false, err
} }
defer rows.Close() defer func() { _ = rows.Close() }()
var result []*models.Message var result []*models.Message
for rows.Next() { for rows.Next() {
@@ -1516,7 +1493,7 @@ func (r *SQLiteRepository) ListRepositories(ctx context.Context, workspaceID str
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer func() { _ = rows.Close() }()
var result []*models.Repository var result []*models.Repository
for rows.Next() { for rows.Next() {
@@ -1604,7 +1581,7 @@ func (r *SQLiteRepository) ListRepositoryScripts(ctx context.Context, repository
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer func() { _ = rows.Close() }()
var result []*models.RepositoryScript var result []*models.RepositoryScript
for rows.Next() { for rows.Next() {
@@ -1949,7 +1926,7 @@ func (r *SQLiteRepository) ListAgentSessions(ctx context.Context, taskID string)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer func() { _ = rows.Close() }()
return r.scanAgentSessions(rows) return r.scanAgentSessions(rows)
} }
@@ -1966,7 +1943,7 @@ func (r *SQLiteRepository) ListActiveAgentSessions(ctx context.Context) ([]*mode
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer func() { _ = rows.Close() }()
return r.scanAgentSessions(rows) return r.scanAgentSessions(rows)
} }
@@ -2194,7 +2171,7 @@ func (r *SQLiteRepository) ListExecutors(ctx context.Context) ([]*models.Executo
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer func() { _ = rows.Close() }()
var result []*models.Executor var result []*models.Executor
for rows.Next() { for rows.Next() {
@@ -2313,7 +2290,7 @@ func (r *SQLiteRepository) ListEnvironments(ctx context.Context) ([]*models.Envi
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer func() { _ = rows.Close() }()
var result []*models.Environment var result []*models.Environment
for rows.Next() { for rows.Next() {
@@ -22,8 +22,12 @@ func createTestSQLiteRepo(t *testing.T) (*SQLiteRepository, func()) {
} }
cleanup := func() { cleanup := func() {
repo.Close() if err := repo.Close(); err != nil {
os.Remove(dbPath) t.Errorf("failed to close repo: %v", err)
}
if err := os.Remove(dbPath); err != nil && !os.IsNotExist(err) {
t.Errorf("failed to remove db path: %v", err)
}
} }
return repo, cleanup return repo, cleanup
@@ -475,14 +479,20 @@ func TestSQLiteRepository_Persistence(t *testing.T) {
_ = repo1.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "Workspace"}) _ = repo1.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "Workspace"})
board := &models.Board{ID: "persist-board", WorkspaceID: "ws-1", Name: "Persistent Board"} board := &models.Board{ID: "persist-board", WorkspaceID: "ws-1", Name: "Persistent Board"}
_ = repo1.CreateBoard(ctx, board) _ = repo1.CreateBoard(ctx, board)
repo1.Close() if err := repo1.Close(); err != nil {
t.Fatalf("failed to close repo: %v", err)
}
// Reopen repository and verify data persisted // Reopen repository and verify data persisted
repo2, err := NewSQLiteRepository(dbPath) repo2, err := NewSQLiteRepository(dbPath)
if err != nil { if err != nil {
t.Fatalf("failed to create second repository: %v", err) t.Fatalf("failed to create second repository: %v", err)
} }
defer repo2.Close() defer func() {
if err := repo2.Close(); err != nil {
t.Errorf("failed to close repo: %v", err)
}
}()
retrieved, err := repo2.GetBoard(ctx, "persist-board") retrieved, err := repo2.GetBoard(ctx, "persist-board")
if err != nil { if err != nil {
@@ -277,7 +277,11 @@ func newDiscoveryService(t *testing.T, root string) *Service {
if err != nil { if err != nil {
t.Fatalf("failed to create test repository: %v", err) t.Fatalf("failed to create test repository: %v", err)
} }
t.Cleanup(func() { repo.Close() }) t.Cleanup(func() {
if err := repo.Close(); err != nil {
t.Errorf("failed to close repo: %v", err)
}
})
log, _ := logger.NewLogger(logger.LoggingConfig{Level: "error", Format: "json", OutputPath: "stdout"}) log, _ := logger.NewLogger(logger.LoggingConfig{Level: "error", Format: "json", OutputPath: "stdout"})
eventBus := bus.NewMemoryEventBus(log) eventBus := bus.NewMemoryEventBus(log)
return NewService(repo, eventBus, log, RepositoryDiscoveryConfig{ return NewService(repo, eventBus, log, RepositoryDiscoveryConfig{
@@ -74,7 +74,11 @@ func createTestService(t *testing.T) (*Service, *MockEventBus, repository.Reposi
if err != nil { if err != nil {
t.Fatalf("failed to create test repository: %v", err) t.Fatalf("failed to create test repository: %v", err)
} }
t.Cleanup(func() { repo.Close() }) t.Cleanup(func() {
if err := repo.Close(); err != nil {
t.Errorf("failed to close repo: %v", err)
}
})
eventBus := NewMockEventBus() eventBus := NewMockEventBus()
log, _ := logger.NewLogger(logger.LoggingConfig{Level: "error", Format: "json", OutputPath: "stdout"}) log, _ := logger.NewLogger(logger.LoggingConfig{Level: "error", Format: "json", OutputPath: "stdout"})
svc := NewService(repo, eventBus, log, RepositoryDiscoveryConfig{}) svc := NewService(repo, eventBus, log, RepositoryDiscoveryConfig{})
+6 -2
View File
@@ -42,7 +42,9 @@ func NewSQLiteRepository(dbPath string) (*SQLiteRepository, error) {
repo := &SQLiteRepository{db: db} repo := &SQLiteRepository{db: db}
if err := repo.initSchema(); err != nil { if err := repo.initSchema(); err != nil {
db.Close() if closeErr := db.Close(); closeErr != nil {
return nil, fmt.Errorf("failed to close database after schema error: %w", closeErr)
}
return nil, fmt.Errorf("failed to initialize schema: %w", err) return nil, fmt.Errorf("failed to initialize schema: %w", err)
} }
return repo, nil return repo, nil
@@ -112,7 +114,9 @@ func (r *SQLiteRepository) columnExists(table, column string) (bool, error) {
if err != nil { if err != nil {
return false, err return false, err
} }
defer rows.Close() defer func() {
_ = rows.Close()
}()
for rows.Next() { for rows.Next() {
var cid int var cid int
+3 -2
View File
@@ -228,7 +228,8 @@ func (c *Client) handleRequest(id interface{}, method string, params json.RawMes
c.onRequest(id, method, params) c.onRequest(id, method, params)
} else { } else {
c.logger.Warn("received request but no handler registered", zap.String("method", method)) c.logger.Warn("received request but no handler registered", zap.String("method", method))
c.SendResponse(id, nil, &Error{Code: MethodNotFound, Message: "Method not found"}) if err := c.SendResponse(id, nil, &Error{Code: MethodNotFound, Message: "Method not found"}); err != nil {
c.logger.Warn("failed to send method not found response", zap.Error(err))
}
} }
} }