Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions .github/workflows/backend-unit-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# This workflow runs tests to verify all the API Server REST Endpoints
name: Backend Unit Tests
env:
TESTS_DIR: "./components/backend/handlers"
TESTS_LABEL: "unit"
JUNIT_FILENAME: "junit.xml"

on:
push:
branches: [main]

workflow_dispatch:
inputs:
test_label:
description: "Test label that you want to filter on and run"
default: 'unit'
required: true
type: string
default_namespace:
description: "Default namespace for testing"
default: 'test-namespace'
required: false
type: string

pull_request:
paths:
- '.github/workflows/backend-unit-tests.yml'
- './components/backend/**'
- '!**/*.md'

concurrency:
group: backend-unit-tests-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
backend-unit-test:
runs-on: ubuntu-latest
name: Ambient Code Backend Unit Tests

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Create reports directory
shell: bash
working-directory: ${{ env.TESTS_DIR }}
run: |
mkdir -p reports

- name: Configure Input Variables
shell: bash
id: configure
run: |
TEST_LABEL=${{ env.TESTS_LABEL }}
DEFAULT_NAMESPACE="test-namespace"
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
TEST_LABEL=${{ inputs.test_label }}
DEFAULT_NAMESPACE=${{ inputs.default_namespace }}
fi

{
echo "TEST_LABEL=$TEST_LABEL"
echo "DEFAULT_NAMESPACE=$DEFAULT_NAMESPACE"
} >> "$GITHUB_OUTPUT"

- name: Run Tests
id: run-tests
shell: bash
working-directory: ${{ env.TESTS_DIR }}
run: |
go run github.com/onsi/ginkgo/v2/ginkgo -r -v --cover --keep-going --github-output=true --tags=test --label-filter=${{ steps.configure.outputs.TEST_LABEL }} --junit-report=${{ env.JUNIT_FILENAME }} --output-dir=reports -- -testNamespace=${{ steps.configure.outputs.DEFAULT_NAMESPACE }}
continue-on-error: true

- name: Install Junit2Html plugin and generate report
if: (!cancelled())
shell: bash
run: |
pip install junit2html
junit2html ${{ env.TESTS_DIR }}/reports/${{ env.JUNIT_FILENAME }} ${{ env.TESTS_DIR }}/reports/test-report.html
continue-on-error: true

- name: Configure report name
id: name_gen
shell: bash
run: |
uuid=$(uuidgen)
REPORT_NAME="Backend Unit Tests HTML Report - ${{ github.run_id }}_${{ github.job }}_$uuid"
echo "REPORT_NAME=$REPORT_NAME" >> "$GITHUB_OUTPUT"

- name: Upload HTML Report
id: upload
uses: actions/upload-artifact@v4
if: (!cancelled())
with:
name: ${{ steps.name_gen.outputs.REPORT_NAME }}
path: ${{ env.TESTS_DIR }}/reports/test-report.html
retention-days: 7
continue-on-error: true

- name: Publish Test Summary With HTML Report
id: publish
uses: kubeflow/pipelines/.github/actions/junit-summary@master
if: (!cancelled()) && steps.upload.outcome != 'failure'
with:
xml_files: '${{ env.TESTS_DIR }}/reports'
custom_data: '{\"HTML Report\": \"${{ steps.upload.outputs.artifact-url }}\"}'
continue-on-error: true

- name: Publish Test Summary
id: summary
uses: kubeflow/pipelines/.github/actions/junit-summary@master
if: (!cancelled()) && steps.upload.outcome == 'failure'
with:
xml_files: '${{ env.TESTS_DIR }}/reports'
continue-on-error: true

- name: Mark Workflow failure if test step failed
if: steps.run-tests.outcome != 'success' && !cancelled()
shell: bash
run: exit 1

- name: Mark Workflow failure if test reporting failed
if: (steps.publish.outcome == 'failure' || steps.summary.outcome == 'failure' || steps.upload.outcome != 'success') && !cancelled()
shell: bash
run: exit 1
9 changes: 9 additions & 0 deletions .github/workflows/go-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,15 @@ jobs:
working-directory: components/backend
args: --timeout=5m

# The backend unit tests require -tags=test (to compile test-only hooks used by handlers tests).
# We lint both production build (default) and test build (with tags) to avoid hiding issues.
- name: Run golangci-lint (test build tags)
uses: golangci/golangci-lint-action@v9
with:
version: latest
working-directory: components/backend
args: --timeout=5m --build-tags=test

lint-operator:
runs-on: ubuntu-latest
needs: detect-go-changes
Expand Down
31 changes: 30 additions & 1 deletion .github/workflows/test-local-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v5

- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: 'components/backend/go.mod'
cache-dependency-path: 'components/backend/go.sum'

- name: Install minikube and kubectl
run: |
Expand All @@ -35,7 +41,7 @@ jobs:
- name: Deploy using Makefile
run: |
echo "Using Makefile to deploy complete stack..."
make local-up CONTAINER_ENGINE=docker
make local-up CONTAINER_ENGINE=docker CI_MODE=true

- name: Wait for deployments
run: |
Expand All @@ -58,6 +64,29 @@ jobs:
kubectl describe deployment agentic-operator -n ambient-code | tail -50
exit 1
}

- name: Run backend integration tests (real k8s auth path)
run: |
set -euo pipefail

echo "Setting up ServiceAccount + RBAC for backend integration tests..."
kubectl -n ambient-code create serviceaccount backend-integration-test 2>/dev/null || true
kubectl -n ambient-code create role backend-integration-test \
--verb=get,list \
--resource=configmaps 2>/dev/null || true
kubectl -n ambient-code create rolebinding backend-integration-test \
--role=backend-integration-test \
--serviceaccount=ambient-code:backend-integration-test 2>/dev/null || true

TEST_TOKEN="$(kubectl -n ambient-code create token backend-integration-test)"
echo "::add-mask::$TEST_TOKEN"

echo "Running Go integration tests (skips any external-provider tests without env)..."
cd components/backend
INTEGRATION_TESTS=true \
K8S_TEST_TOKEN="$TEST_TOKEN" \
K8S_TEST_NAMESPACE="ambient-code" \
go test ./tests/integration/... -count=1 -timeout=10m

- name: Run Makefile smoke tests
run: |
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,7 @@ e2e/langfuse/.env.langfuse-keys
# AI assistant configuration
.cursor/
.tessl/

# Test Reporting
logs/
reports/
72 changes: 48 additions & 24 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.PHONY: help setup build-all build-frontend build-backend build-operator build-runner deploy clean
.PHONY: local-up local-down local-clean local-status local-rebuild local-reload-backend local-reload-frontend local-reload-operator local-sync-version
.PHONY: local-dev-token
.PHONY: local-logs local-logs-backend local-logs-frontend local-logs-operator local-shell local-shell-frontend
.PHONY: local-test local-test-dev local-test-quick test-all local-url local-troubleshoot local-port-forward local-stop-port-forward
.PHONY: push-all registry-login setup-hooks remove-hooks check-minikube check-kubectl
Expand All @@ -16,6 +17,19 @@ PLATFORM ?= linux/amd64
BUILD_FLAGS ?=
NAMESPACE ?= ambient-code
REGISTRY ?= quay.io/your-org
CI_MODE ?= false

# In CI we want full command output to diagnose failures. Locally we keep the Makefile quieter.
# GitHub Actions sets CI=true by default; the workflow can also pass CI_MODE=true explicitly.
ifeq ($(CI),true)
CI_MODE := true
endif

ifeq ($(CI_MODE),true)
QUIET_REDIRECT :=
else
QUIET_REDIRECT := >/dev/null 2>&1
endif

# Image tags
FRONTEND_IMAGE ?= vteam-frontend:latest
Expand Down Expand Up @@ -122,40 +136,40 @@ local-up: check-minikube check-kubectl ## Start local development environment (m
@echo ""
@echo "$(COLOR_BLUE)▶$(COLOR_RESET) Step 1/8: Starting minikube..."
@if [ "$(CONTAINER_ENGINE)" = "docker" ]; then \
minikube start --driver=docker --memory=4096 --cpus=2 2>/dev/null || \
minikube start --driver=docker --memory=4096 --cpus=2 $(QUIET_REDIRECT) || \
(minikube status >/dev/null 2>&1 && echo "$(COLOR_GREEN)✓$(COLOR_RESET) Minikube already running") || \
(echo "$(COLOR_RED)✗$(COLOR_RESET) Failed to start minikube" && exit 1); \
else \
minikube start --driver=podman --memory=4096 --cpus=2 --kubernetes-version=v1.28.3 --container-runtime=cri-o 2>/dev/null || \
minikube start --driver=podman --memory=4096 --cpus=2 --kubernetes-version=v1.28.3 --container-runtime=cri-o $(QUIET_REDIRECT) || \
(minikube status >/dev/null 2>&1 && echo "$(COLOR_GREEN)✓$(COLOR_RESET) Minikube already running") || \
(echo "$(COLOR_RED)✗$(COLOR_RESET) Failed to start minikube" && exit 1); \
fi
@echo "$(COLOR_BLUE)▶$(COLOR_RESET) Step 2/8: Enabling addons..."
@minikube addons enable ingress >/dev/null 2>&1 || true
@minikube addons enable storage-provisioner >/dev/null 2>&1 || true
@minikube addons enable ingress $(QUIET_REDIRECT) || true
@minikube addons enable storage-provisioner $(QUIET_REDIRECT) || true
@echo "$(COLOR_BLUE)▶$(COLOR_RESET) Step 3/8: Building images..."
@$(MAKE) --no-print-directory _build-and-load
@echo "$(COLOR_BLUE)▶$(COLOR_RESET) Step 4/8: Creating namespace..."
@kubectl create namespace $(NAMESPACE) --dry-run=client -o yaml | kubectl apply -f - >/dev/null 2>&1
@kubectl create namespace $(NAMESPACE) --dry-run=client -o yaml | kubectl apply -f - $(QUIET_REDIRECT)
@echo "$(COLOR_BLUE)▶$(COLOR_RESET) Step 5/8: Applying CRDs and RBAC..."
@kubectl apply -f components/manifests/base/crds/ >/dev/null 2>&1 || true
@kubectl apply -f components/manifests/base/rbac/ >/dev/null 2>&1 || true
@kubectl apply -f components/manifests/minikube/local-dev-rbac.yaml >/dev/null 2>&1 || true
@kubectl apply -f components/manifests/base/crds/ $(QUIET_REDIRECT) || true
@kubectl apply -f components/manifests/base/rbac/ $(QUIET_REDIRECT) || true
@kubectl apply -f components/manifests/minikube/local-dev-rbac.yaml $(QUIET_REDIRECT) || true
@echo "$(COLOR_BLUE)▶$(COLOR_RESET) Step 6/8: Creating storage..."
@kubectl apply -f components/manifests/base/workspace-pvc.yaml -n $(NAMESPACE) >/dev/null 2>&1 || true
@kubectl apply -f components/manifests/base/workspace-pvc.yaml -n $(NAMESPACE) $(QUIET_REDIRECT) || true
@echo "$(COLOR_BLUE)▶$(COLOR_RESET) Step 6.5/8: Configuring operator..."
@$(MAKE) --no-print-directory _create-operator-config
@$(MAKE) --no-print-directory local-sync-version
@echo "$(COLOR_BLUE)▶$(COLOR_RESET) Step 7/8: Deploying services..."
@kubectl apply -f components/manifests/minikube/backend-deployment.yaml >/dev/null 2>&1
@kubectl apply -f components/manifests/minikube/backend-service.yaml >/dev/null 2>&1
@kubectl apply -f components/manifests/minikube/frontend-deployment.yaml >/dev/null 2>&1
@kubectl apply -f components/manifests/minikube/frontend-service.yaml >/dev/null 2>&1
@kubectl apply -f components/manifests/minikube/operator-deployment.yaml >/dev/null 2>&1
@kubectl apply -f components/manifests/minikube/backend-deployment.yaml $(QUIET_REDIRECT)
@kubectl apply -f components/manifests/minikube/backend-service.yaml $(QUIET_REDIRECT)
@kubectl apply -f components/manifests/minikube/frontend-deployment.yaml $(QUIET_REDIRECT)
@kubectl apply -f components/manifests/minikube/frontend-service.yaml $(QUIET_REDIRECT)
@kubectl apply -f components/manifests/minikube/operator-deployment.yaml $(QUIET_REDIRECT)
@echo "$(COLOR_BLUE)▶$(COLOR_RESET) Step 8/8: Setting up ingress..."
@kubectl wait --namespace ingress-nginx --for=condition=ready pod \
--selector=app.kubernetes.io/component=controller --timeout=90s >/dev/null 2>&1 || true
@kubectl apply -f components/manifests/minikube/ingress.yaml >/dev/null 2>&1 || true
@kubectl apply -f components/manifests/minikube/ingress.yaml $(QUIET_REDIRECT) || true
@echo ""
@echo "$(COLOR_GREEN)✓ Ambient Code Platform is starting up!$(COLOR_RESET)"
@echo ""
Expand Down Expand Up @@ -356,7 +370,7 @@ local-test-quick: check-kubectl check-minikube ## Quick smoke test of local envi
@echo "$(COLOR_BLUE)▶$(COLOR_RESET) Testing namespace..."
@kubectl get namespace $(NAMESPACE) >/dev/null 2>&1 && echo "$(COLOR_GREEN)✓$(COLOR_RESET) Namespace exists" || (echo "$(COLOR_RED)✗$(COLOR_RESET) Namespace missing" && exit 1)
@echo "$(COLOR_BLUE)▶$(COLOR_RESET) Waiting for pods to be ready..."
@kubectl wait --for=condition=ready pod -l app=backend -n $(NAMESPACE) --timeout=60s >/dev/null 2>&1 && \
@kubectl wait --for=condition=ready pod -l app=backend-api -n $(NAMESPACE) --timeout=60s >/dev/null 2>&1 && \
kubectl wait --for=condition=ready pod -l app=frontend -n $(NAMESPACE) --timeout=60s >/dev/null 2>&1 && \
echo "$(COLOR_GREEN)✓$(COLOR_RESET) Pods ready" || (echo "$(COLOR_RED)✗$(COLOR_RESET) Pods not ready" && exit 1)
@echo "$(COLOR_BLUE)▶$(COLOR_RESET) Testing backend health..."
Expand Down Expand Up @@ -492,13 +506,13 @@ check-kubectl: ## Check if kubectl is installed

_build-and-load: ## Internal: Build and load images
@echo " Building backend..."
@$(CONTAINER_ENGINE) build -t $(BACKEND_IMAGE) components/backend >/dev/null 2>&1
@$(CONTAINER_ENGINE) build -t $(BACKEND_IMAGE) components/backend $(QUIET_REDIRECT)
@echo " Building frontend..."
@$(CONTAINER_ENGINE) build -t $(FRONTEND_IMAGE) components/frontend >/dev/null 2>&1
@$(CONTAINER_ENGINE) build -t $(FRONTEND_IMAGE) components/frontend $(QUIET_REDIRECT)
@echo " Building operator..."
@$(CONTAINER_ENGINE) build -t $(OPERATOR_IMAGE) components/operator >/dev/null 2>&1
@$(CONTAINER_ENGINE) build -t $(OPERATOR_IMAGE) components/operator $(QUIET_REDIRECT)
@echo " Building runner..."
@$(CONTAINER_ENGINE) build -t $(RUNNER_IMAGE) -f components/runners/claude-code-runner/Dockerfile components/runners >/dev/null 2>&1
@$(CONTAINER_ENGINE) build -t $(RUNNER_IMAGE) -f components/runners/claude-code-runner/Dockerfile components/runners $(QUIET_REDIRECT)
@echo " Tagging images with localhost prefix..."
@$(CONTAINER_ENGINE) tag $(BACKEND_IMAGE) localhost/$(BACKEND_IMAGE) 2>/dev/null || true
@$(CONTAINER_ENGINE) tag $(FRONTEND_IMAGE) localhost/$(FRONTEND_IMAGE) 2>/dev/null || true
Expand All @@ -510,10 +524,10 @@ _build-and-load: ## Internal: Build and load images
@$(CONTAINER_ENGINE) save -o /tmp/minikube-images/frontend.tar localhost/$(FRONTEND_IMAGE)
@$(CONTAINER_ENGINE) save -o /tmp/minikube-images/operator.tar localhost/$(OPERATOR_IMAGE)
@$(CONTAINER_ENGINE) save -o /tmp/minikube-images/runner.tar localhost/$(RUNNER_IMAGE)
@minikube image load /tmp/minikube-images/backend.tar >/dev/null 2>&1
@minikube image load /tmp/minikube-images/frontend.tar >/dev/null 2>&1
@minikube image load /tmp/minikube-images/operator.tar >/dev/null 2>&1
@minikube image load /tmp/minikube-images/runner.tar >/dev/null 2>&1
@minikube image load /tmp/minikube-images/backend.tar $(QUIET_REDIRECT)
@minikube image load /tmp/minikube-images/frontend.tar $(QUIET_REDIRECT)
@minikube image load /tmp/minikube-images/operator.tar $(QUIET_REDIRECT)
@minikube image load /tmp/minikube-images/runner.tar $(QUIET_REDIRECT)
@rm -rf /tmp/minikube-images
@echo "$(COLOR_GREEN)✓$(COLOR_RESET) Images built and loaded"

Expand Down Expand Up @@ -549,6 +563,16 @@ _show-access-info: ## Internal: Show access information
@echo ""
@echo "$(COLOR_YELLOW)⚠ SECURITY NOTE:$(COLOR_RESET) Authentication is DISABLED for local development."

local-dev-token: check-kubectl ## Print a TokenRequest token for local-dev-user (for local dev API calls)
@kubectl get serviceaccount local-dev-user -n $(NAMESPACE) >/dev/null 2>&1 || \
(echo "$(COLOR_RED)✗$(COLOR_RESET) local-dev-user ServiceAccount not found in namespace $(NAMESPACE). Run 'make local-up' first." && exit 1)
@TOKEN=$$(kubectl -n $(NAMESPACE) create token local-dev-user 2>/dev/null); \
if [ -z "$$TOKEN" ]; then \
echo "$(COLOR_RED)✗$(COLOR_RESET) Failed to mint token (kubectl create token). Ensure TokenRequest is supported and kubectl is v1.24+"; \
exit 1; \
fi; \
echo "$$TOKEN"

_create-operator-config: ## Internal: Create operator config from environment variables
@VERTEX_PROJECT_ID=$${ANTHROPIC_VERTEX_PROJECT_ID:-""}; \
VERTEX_KEY_FILE=$${GOOGLE_APPLICATION_CREDENTIALS:-""}; \
Expand Down
6 changes: 6 additions & 0 deletions components/backend/.golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ linters:
- staticcheck
- govet

# Exclude style checks from test utilities (common patterns in test code)
- path: tests/.*\.go
linters:
- staticcheck
text: "(should not use|should not use dot imports|should not use ALL_CAPS|at least one file)"

# Allow type assertions in K8s unstructured object parsing (intentional pattern)
- path: (handlers|jira)/.*\.go
text: "type assertion"
Expand Down
Loading
Loading