Files
kandev/apps/backend/Makefile
T
João Salavisa 9aaf3170e0 fix(lifecycle): drain StreamManager goroutines in tests (#1227)
* fix(lifecycle): drain StreamManager goroutines in tests

Goroutines spawned by `agentctl.Client.StreamUpdates` /
`StreamWorkspace` and by `lifecycle.StreamManager.connectWorkspaceStream`
could outlive the tests that created them on slow CI runners, causing
intermittent `goleak.VerifyTestMain` failures in
`internal/agent/runtime/lifecycle` even though every individual test
passed. The leak required the test to race: `client.Close()` could
return while a workspace dial was still in flight, leaving the
just-spawned WS read/write loops with nobody to drain them, and
`StreamManager.Wait()` only fired the drain when the external `stopCh`
had been closed first.

Make the close paths absolute drain barriers:

- `Client.Close()` now tracks every stream goroutine it spawns
  (`streamWG` + a per-stream `WorkspaceStream` reference) and blocks
  until all of them have exited. A `closed` flag flipped under the
  client mutex makes subsequent `StreamUpdates` / `StreamWorkspace`
  calls reject the dial, so a Close that races a dial in flight cannot
  leave behind a stranded WS connection. `manager_interaction.go`
  switches the restart path to per-stream Close helpers so it can keep
  reusing the client after teardown.
- `StreamManager.Wait()` closes a new internal `waitCh` that the retry
  backoff (`sleepOrStop`) and the connected `<-ws.Done() / <-stop>`
  select also observe. This makes `Wait` an absolute drain barrier
  regardless of whether the caller ever closes the external `stopCh`.
  `streamContext` carries both stops through `stopChannelContext` so
  in-flight WebSocket dials cancel on either signal.

Local stress (`make test-lifecycle-goleak LIFECYCLE_GOLEAK_COUNT=20`)
passes 20/20 with `-race` after the fix. The new
`TestClientClose_DrainsStreamGoroutines` covers the drain barrier and
the post-Close rejection path. Added a `test-lifecycle-goleak`
Makefile target as the repro hook for the CI-only flake.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(lifecycle): track mergeStops goroutine and guard workspace stream races

Address PR review feedback:
- stopChannelContext now registers the mergeStops goroutine on an optional
  *sync.WaitGroup so sm.wg.Wait stays a true drain barrier even for the
  connectUpdatesStream path (outer goroutine returns immediately after dial).
- StreamWorkspace re-checks workspaceStreamConn after dial to seal the
  concurrent-callers race that could orphan a conn.
- readWorkspaceStream's defer now guards the workspaceStreamConn reset by
  identity so an old read goroutine cannot zero out a newer conn pointer.
- Clarify comments on streamWG, Client.Close, and the mock.connected wait
  in the close-barrier test.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(lifecycle): drop agent-stream drain barrier to unblock workflow cascade

The previous Close drain barrier for the agent (updates) stream blocked
the on_turn_complete cascade flow. completeAndStopSession calls Close
synchronously on the current session's client, and Close was waiting on
streamWG (which tracked the read goroutine). The wait stalled the
cascade long enough that Step2's auto-started session never got its
profile name into the tab — surfacing as a flaky "Profile B tab not
visible" failure in workflow-agent-switch E2E shard 7.

Drain barrier is still in place for the workspace stream (where the
original goleak flake lived). Agent stream is closed but not awaited:
the cascade legitimately stops + restarts the agent stream on the same
client, and the workspace stream's existing barrier is sufficient for
the leak coverage.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-06-03 11:06:17 +01:00

251 lines
10 KiB
Makefile

# Kandev Makefile
# Variables
BINARY_NAME := kandev
BUILD_DIR := bin
GO := go
GOFLAGS := -v
LDFLAGS := -s -w
# Cross-platform commands
#
# On Windows, GNU Make invokes commands through cmd.exe, which does not
# support Unix `NAME=VAL cmd` env-var-prefix syntax. Instead, `set NAME=VAL&`
# (single ampersand) chains a subsequent command in the same shell, scoping
# the env var to one recipe line — the closest cmd analog to the Unix form.
# `EXE` adds the .exe suffix to build outputs; Go does NOT auto-append it
# when `-o` is explicit.
#
# Install GNU Make on Windows with `winget install ezwinports.make` (or via
# scoop/chocolatey). Targets that rely on Unix builtins (run, dev,
# start-debug, build-cross, build-agentctl-linux) remain Unix-only.
ifeq ($(OS),Windows_NT)
RM = cmd /c del /s /q
RMDIR = cmd /c rmdir /s /q
# cmd's `mkdir` errors when the dir already exists, unlike Unix `mkdir -p`.
# Call sites use `-@$(MKDIR) $(BUILD_DIR)` (note the `-` prefix) so the
# "already exists" failure is tolerated; on Unix the prefix is a no-op.
MKDIR = cmd /c mkdir
CGO_PREFIX = set CGO_ENABLED=1&
# Cross-compile prefix for the linux/amd64 agentctl bundled with the host
# build (used by remote-Sprites executors). cmd has no Unix env-var-prefix
# syntax; `set X=Y&` chains `set` and the next command in the same shell.
GO_LINUX_AMD64 = set CGO_ENABLED=0& set GOOS=linux& set GOARCH=amd64&
EXE = .exe
else
RM = rm -f
RMDIR = rm -rf
MKDIR = mkdir -p
CGO_PREFIX = CGO_ENABLED=1
GO_LINUX_AMD64 = CGO_ENABLED=0 GOOS=linux GOARCH=amd64
EXE =
endif
# Build info
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
BUILD_TIME := $(shell date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || powershell -c "Get-Date -Format 'yyyy-MM-ddTHH:mm:ssZ'" 2>/dev/null || echo "unknown")
LDFLAGS += -X main.Version=$(VERSION) -X main.Commit=$(COMMIT) -X main.BuildTime=$(BUILD_TIME)
.PHONY: all build build-all build-agentctl build-agentctl-linux build-acpdbg acpdbg build-mock-agent build-mock-agent-linux build-preview build-winjob clean run dev start-debug test test-e2e test-sprites-e2e test-lifecycle-goleak lint fmt vet help
## Default target
all: build
## Build the preview CLI for deploying PR preview environments to Sprites
build-preview:
@echo "Building preview..."
-@$(MKDIR) $(BUILD_DIR)
$(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/preview$(EXE) ./cmd/preview
## Build the mock-agent binary for testing
build-mock-agent:
@echo "Building mock-agent..."
-@$(MKDIR) $(BUILD_DIR)
$(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/mock-agent$(EXE) ./cmd/mock-agent
## Build the winjob helper — wraps a subprocess in a Windows Job Object so
## Ctrl-C at the top of the chain reliably terminates the entire subtree
## (kandev, agentctl, etc.) instead of leaking them. No-op passthrough on Unix.
## See apps/cli/src/dev.ts for how the CLI launcher uses it.
build-winjob:
@echo "Building winjob..."
-@$(MKDIR) $(BUILD_DIR)
$(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/winjob$(EXE) ./cmd/winjob
## Build and run acpdbg — usage: make acpdbg ARGS="probe auggie"
acpdbg: build-acpdbg
@$(BUILD_DIR)/acpdbg$(EXE) $(ARGS)
## Build the acpdbg binary — raw ACP JSON-RPC debugger (see .agents/skills/acp-debug)
build-acpdbg:
@echo "Building acpdbg..."
-@$(MKDIR) $(BUILD_DIR)
$(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/acpdbg$(EXE) ./cmd/acpdbg
## Build the unified kandev binary. Always includes the linux/amd64 agentctl
## variant because remote executors (local Docker, remote Docker, Sprites)
## bind-mount it into containers — without it, the first Docker session on a
## non-Linux host fails with "agentctl linux binary not found".
build: build-agentctl build-agentctl-linux build-mock-agent build-acpdbg build-winjob
@echo "Building $(BINARY_NAME)..."
-@$(MKDIR) $(BUILD_DIR)
$(CGO_PREFIX) $(GO) build $(GOFLAGS) -tags fts5 -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)$(EXE) ./cmd/kandev
## Build all binaries (unified + individual services + agentctl)
build-all: build build-agentctl
@echo "Building individual services..."
$(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/agent-manager$(EXE) ./cmd/agent-manager
## Build the agentctl sidecar binary (includes embedded MCP server)
build-agentctl:
@echo "Building agentctl..."
-@$(MKDIR) $(BUILD_DIR)
$(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/agentctl$(EXE) ./cmd/agentctl
## Build agentctl for linux/amd64 (used by Sprites remote executor)
build-agentctl-linux:
@echo "Building agentctl for linux/amd64..."
-@$(MKDIR) $(BUILD_DIR)
$(GO_LINUX_AMD64) $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/agentctl-linux-amd64 ./cmd/agentctl
## Build mock-agent for linux/amd64 (used by Docker E2E tests)
build-mock-agent-linux:
@echo "Building mock-agent for linux/amd64..."
@$(MKDIR) $(BUILD_DIR)
GOOS=linux GOARCH=amd64 $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/mock-agent-linux-amd64 ./cmd/mock-agent
## Clean build artifacts
clean:
@echo "Cleaning..."
@$(RMDIR) $(BUILD_DIR)
$(GO) clean
## Run the application
run: build
@echo "Running $(BINARY_NAME)..."
./$(BUILD_DIR)/$(BINARY_NAME)$(EXE)
## Run in development mode (with pprof enabled)
dev: build
@echo "Running $(BINARY_NAME) in development mode..."
@KANDEV_DEBUG_PPROF_ENABLED=true exec ./$(BUILD_DIR)/$(BINARY_NAME)$(EXE)
## Run in debug mode (pprof + debug logging)
start-debug: build
@echo "Running $(BINARY_NAME) in debug mode (pprof + debug logging)..."
@KANDEV_DEBUG_PPROF_ENABLED=true KANDEV_LOG_LEVEL=debug exec ./$(BUILD_DIR)/$(BINARY_NAME)$(EXE)
## Run tests
test:
@echo "Running tests..."
$(CGO_PREFIX) $(GO) test -tags fts5 ./...
## Run only the Windows-sensitive packages — mirrors the test-windows job in
## .github/workflows/backend-tests.yml. The broader suite uses Unix-only
## shell commands in test fixtures and would need GOOS guards to be portable;
## this narrow scope is the regression coverage for the conpty/job-object
## bug class. Tests that genuinely cannot run on Windows skip themselves via
## runtime.GOOS guards in source.
test-windows:
@echo "Running Windows-sensitive packages..."
$(CGO_PREFIX) $(GO) test -tags fts5 -v ./internal/agentctl/server/process/... ./internal/agent/runtime/agentctl/launcher/...
## Run E2E tests (real agents, costs money — manual only)
## Set agent commands via env vars: E2E_CLAUDE_CODE_COMMAND, E2E_AMP_COMMAND
## Enable debug: KANDEV_DEBUG_AGENT_MESSAGES=true KANDEV_DEBUG_LOG_DIR=/tmp/e2e-debug
test-e2e:
@echo "Running E2E adapter tests..."
$(GO) test -tags e2e -v -timeout 10m ./internal/agentctl/server/adapter/e2e/
## Run sprites e2e test (requires SPRITES_API_TOKEN env var)
## Sprites VMs are always x86_64/amd64 regardless of host architecture.
test-sprites-e2e: build-agentctl-linux
@echo "Running sprites e2e test..."
KANDEV_AGENTCTL_LINUX_BINARY=$(CURDIR)/$(BUILD_DIR)/agentctl-linux-amd64 \
$(GO) test -v -count=1 -tags sprites_e2e -run TestSpritesE2E -timeout 10m ./internal/agent/runtime/lifecycle/
## Stress-run the lifecycle goleak suite — repro target for the CI-only flake
## where StreamManager/WorkspaceStream goroutines linger past TestMain on
## slow runners. Local hardware almost never reproduces the leak under a
## single pass; -count=20 (configurable) plus -race is the smallest cadence
## that consistently surfaces the race during development.
##
## Override the loop count with `make test-lifecycle-goleak LIFECYCLE_GOLEAK_COUNT=N`.
LIFECYCLE_GOLEAK_COUNT ?= 20
test-lifecycle-goleak:
@echo "Stress-running lifecycle goleak suite ($(LIFECYCLE_GOLEAK_COUNT)x)..."
$(CGO_PREFIX) $(GO) test -tags fts5 -race -count=$(LIFECYCLE_GOLEAK_COUNT) -timeout 600s ./internal/agent/runtime/lifecycle/ ./internal/agent/runtime/agentctl/
## Run tests with coverage
test-coverage:
@echo "Running tests with coverage..."
$(CGO_PREFIX) $(GO) test -tags fts5 -v -coverprofile=coverage.out ./...
$(GO) tool cover -html=coverage.out -o coverage.html
## Run linter (requires golangci-lint)
lint:
@echo "Running linter..."
golangci-lint run ./...
## Format code
fmt:
@echo "Formatting code..."
$(GO) fmt ./...
## Run go vet
vet:
@echo "Running vet..."
$(GO) vet ./...
## Tidy dependencies
tidy:
@echo "Tidying dependencies..."
$(GO) mod tidy
## Download dependencies
deps:
@echo "Downloading dependencies..."
$(GO) mod download
## Build for multiple platforms
build-cross:
@echo "Building for multiple platforms..."
-@$(MKDIR) $(BUILD_DIR)
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 $(GO) build -tags fts5 -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64 ./cmd/kandev
CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 $(GO) build -tags fts5 -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-amd64 ./cmd/kandev
CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 $(GO) build -tags fts5 -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-arm64 ./cmd/kandev
CGO_ENABLED=1 GOOS=windows GOARCH=amd64 $(GO) build -tags fts5 -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-windows-amd64.exe ./cmd/kandev
## Show help
help:
@echo "Kandev - AI Agent Kanban Board"
@echo ""
@echo "Usage:"
@echo " make [target]"
@echo ""
@echo "Targets:"
@echo " build Build the unified kandev binary (default)"
@echo " build-all Build all binaries (unified + individual services + agentctl)"
@echo " build-agentctl Build the agentctl sidecar binary (includes embedded MCP server)"
@echo " build-mock-agent Build the mock-agent binary for testing"
@echo " build-preview Build the preview CLI for PR preview environments"
@echo " clean Remove build artifacts"
@echo " run Build and run the application"
@echo " dev Run in development mode (pprof enabled)"
@echo " start-debug Run in debug mode (pprof + debug logging)"
@echo " test Run tests (full suite — fails on Windows pending fixture cleanup)"
@echo " test-windows Run only the Windows-clean subset (matches CI windows-latest job)"
@echo " test-e2e Run E2E adapter tests (real agents, costs money)"
@echo " test-lifecycle-goleak Stress-run lifecycle goleak suite (override LIFECYCLE_GOLEAK_COUNT=N)"
@echo " test-coverage Run tests with coverage report"
@echo " lint Run golangci-lint"
@echo " fmt Format code"
@echo " vet Run go vet"
@echo " tidy Tidy go.mod dependencies"
@echo " deps Download dependencies"
@echo " build-cross Build for Linux, macOS, and Windows"
@echo " help Show this help message"