feat: golang linter
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
version: "2"
|
||||||
|
# Default golangci-lint configuration (uses built-in defaults).
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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{})
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user