Skip to content
Open
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
16 changes: 16 additions & 0 deletions .github/workflows/frontend-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,29 @@ jobs:
- name: Run TypeScript type check
run: |
cd components/frontend
# Clean .next to avoid stale type definitions from previous builds
# Next.js post-install generates types that may reference non-existent files
rm -rf .next
npx tsc --noEmit

- name: Build check
run: |
cd components/frontend
npm run build

- name: Run tests with coverage
run: |
cd components/frontend
npm run test:coverage

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./components/frontend/coverage/lcov.info
flags: frontend
name: frontend-coverage

lint-summary:
runs-on: ubuntu-latest
needs: [detect-frontend-changes, lint-frontend]
Expand Down
26 changes: 26 additions & 0 deletions .github/workflows/go-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,19 @@ jobs:
working-directory: components/backend
args: --timeout=5m

- name: Run tests with coverage
run: |
cd components/backend
go test ./... -coverprofile=coverage.out -covermode=atomic

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./components/backend/coverage.out
flags: backend
name: backend-coverage

lint-operator:
runs-on: ubuntu-latest
needs: detect-go-changes
Expand Down Expand Up @@ -107,6 +120,19 @@ jobs:
working-directory: components/operator
args: --timeout=5m

- name: Run tests with coverage
run: |
cd components/operator
go test ./... -coverprofile=coverage.out -covermode=atomic

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./components/operator/coverage.out
flags: operator
name: operator-coverage

lint-summary:
runs-on: ubuntu-latest
needs: [detect-go-changes, lint-backend, lint-operator]
Expand Down
78 changes: 78 additions & 0 deletions .github/workflows/python-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: Python Test and Coverage

on:
push:
branches: [main]
paths:
- 'components/runners/claude-code-runner/**/*.py'
- 'components/runners/claude-code-runner/pyproject.toml'
- 'components/runners/claude-code-runner/uv.lock'
- '.github/workflows/python-test.yml'
pull_request:
branches: [main]
paths:
- 'components/runners/claude-code-runner/**/*.py'
- 'components/runners/claude-code-runner/pyproject.toml'
- 'components/runners/claude-code-runner/uv.lock'
- '.github/workflows/python-test.yml'
workflow_dispatch:

jobs:
test-runner:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Install uv
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.cargo/bin" >> $GITHUB_PATH

- name: Install dependencies
run: |
cd components/runners/claude-code-runner
uv pip install --system -e '.[dev]'

- name: Run tests with coverage
run: |
cd components/runners/claude-code-runner
# Allow exit code 5 (no tests collected) - tests require container environment
pytest --cov=. --cov-report=xml --cov-report=term || EXIT_CODE=$?
if [ "${EXIT_CODE:-0}" -eq 5 ]; then
echo "No tests collected (requires container environment) - this is expected"
echo "Generating empty coverage report for Codecov"
echo '<?xml version="1.0" ?><coverage version="1.0"><packages></packages></coverage>' > coverage.xml
exit 0
elif [ "${EXIT_CODE:-0}" -ne 0 ]; then
echo "Tests failed with exit code $EXIT_CODE"
exit $EXIT_CODE
fi

- name: Upload coverage to Codecov
if: always()
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./components/runners/claude-code-runner/coverage.xml
flags: python-runner
name: claude-runner-coverage

test-summary:
runs-on: ubuntu-latest
needs: [test-runner]
if: always()
steps:
- name: Check overall status
run: |
if [ "${{ needs.test-runner.result }}" == "failure" ]; then
echo "Python tests failed"
exit 1
fi
echo "All Python tests passed!"

49 changes: 49 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
coverage:
status:
project:
default:
target: 50%
threshold: 5%
informational: true # Don't block PRs
patch:
default:
target: 50%
informational: true

flag_management:
default_rules:
statuses:
- type: project
target: 50%
informational: true

flags:
backend:
paths:
- components/backend/
target: 60%
carryforward: true
operator:
paths:
- components/operator/
target: 70%
carryforward: true
frontend:
paths:
- components/frontend/
target: 50%
carryforward: true
python-runner:
paths:
- components/runners/claude-code-runner/
target: 60%
carryforward: true

comment:
layout: "reach,diff,flags,tree"
behavior: default
require_changes: false
require_base: false
require_head: false
after_n_builds: 0

185 changes: 185 additions & 0 deletions components/backend/handlers/helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package handlers

import (
"errors"
"strings"
"testing"
"time"

"k8s.io/apimachinery/pkg/runtime/schema"
)

func TestGetProjectSettingsResource(t *testing.T) {
gvr := GetProjectSettingsResource()

tests := []struct {
name string
expected string
actual string
}{
{
name: "Group should be vteam.ambient-code",
expected: "vteam.ambient-code",
actual: gvr.Group,
},
{
name: "Version should be v1alpha1",
expected: "v1alpha1",
actual: gvr.Version,
},
{
name: "Resource should be projectsettings",
expected: "projectsettings",
actual: gvr.Resource,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.actual != tt.expected {
t.Errorf("expected %s, got %s", tt.expected, tt.actual)
}
})
}
}

func TestRetryWithBackoff(t *testing.T) {
t.Run("success on first attempt", func(t *testing.T) {
attempts := 0
operation := func() error {
attempts++
return nil
}

err := RetryWithBackoff(3, 10*time.Millisecond, 100*time.Millisecond, operation)
if err != nil {
t.Errorf("expected no error, got %v", err)
}
if attempts != 1 {
t.Errorf("expected 1 attempt, got %d", attempts)
}
})

t.Run("success after retries", func(t *testing.T) {
attempts := 0
operation := func() error {
attempts++
if attempts < 3 {
return errors.New("temporary failure")
}
return nil
}

err := RetryWithBackoff(5, 10*time.Millisecond, 100*time.Millisecond, operation)
if err != nil {
t.Errorf("expected no error, got %v", err)
}
if attempts != 3 {
t.Errorf("expected 3 attempts, got %d", attempts)
}
})

t.Run("failure after max retries", func(t *testing.T) {
attempts := 0
expectedError := errors.New("persistent failure")
operation := func() error {
attempts++
return expectedError
}

err := RetryWithBackoff(3, 10*time.Millisecond, 100*time.Millisecond, operation)
if err == nil {
t.Error("expected error, got nil")
}
if attempts != 3 {
t.Errorf("expected 3 attempts, got %d", attempts)
}
})

t.Run("respects max delay", func(t *testing.T) {
startTime := time.Now()
attempts := 0
operation := func() error {
attempts++
return errors.New("failure")
}

maxDelay := 50 * time.Millisecond
RetryWithBackoff(3, 10*time.Millisecond, maxDelay, operation)
duration := time.Since(startTime)

// With 3 retries and max delay of 50ms, total time should be less than 150ms
// Allowing 500ms buffer for slow CI environments to prevent flakiness
if duration > 500*time.Millisecond {
t.Errorf("expected duration less than 500ms, got %v", duration)
}
})
}

func TestRetryWithBackoffZeroRetries(t *testing.T) {
attempts := 0
operation := func() error {
attempts++
return errors.New("failure")
}

err := RetryWithBackoff(0, 10*time.Millisecond, 100*time.Millisecond, operation)
if err == nil {
t.Error("expected error, got nil")
}
if attempts != 0 {
t.Errorf("expected 0 attempts, got %d", attempts)
}
}

func BenchmarkRetryWithBackoffSuccess(b *testing.B) {
operation := func() error {
return nil
}

b.ResetTimer()
for i := 0; i < b.N; i++ {
RetryWithBackoff(3, 1*time.Millisecond, 10*time.Millisecond, operation)
}
}

// TestGroupVersionResource verifies the GVR format is correct
func TestGroupVersionResource(t *testing.T) {
gvr := GetProjectSettingsResource()

// Verify it's a valid GVR that can be used with dynamic client
if gvr.Empty() {
t.Error("GVR should not be empty")
}

// Verify string representation contains expected parts
gvrString := gvr.String()
if !strings.Contains(gvrString, "vteam.ambient-code") {
t.Errorf("GVR string should contain group: %s", gvrString)
}
if !strings.Contains(gvrString, "v1alpha1") {
t.Errorf("GVR string should contain version: %s", gvrString)
}
if !strings.Contains(gvrString, "projectsettings") {
t.Errorf("GVR string should contain resource: %s", gvrString)
}
}

// Mock test for schema validation
func TestSchemaGroupVersionResource(t *testing.T) {
gvr := GetProjectSettingsResource()

// Verify the type
var _ schema.GroupVersionResource = gvr

// Verify the individual components instead of string format
if gvr.Group != "vteam.ambient-code" {
t.Errorf("Expected group 'vteam.ambient-code', got '%s'", gvr.Group)
}
if gvr.Version != "v1alpha1" {
t.Errorf("Expected version 'v1alpha1', got '%s'", gvr.Version)
}
if gvr.Resource != "projectsettings" {
t.Errorf("Expected resource 'projectsettings', got '%s'", gvr.Resource)
}
}
5 changes: 5 additions & 0 deletions components/frontend/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Required for React 19 compatibility with testing libraries
# @testing-library/react currently supports React ^18.0.0
# This allows installation despite peer dependency mismatch
legacy-peer-deps=true

Loading
Loading