Skip to content

Commit 7322205

Browse files
committed
Add systematic test coverage CI for all components
- Add codecov.yml with 70% threshold and component flags - Frontend: Set up Jest + React Testing Library with initial tests - Add test scripts to package.json - Create jest.config.js and jest.setup.js - Add initial tests for status-badge, utils, and API client - Backend: Add initial handler tests (helpers_test.go) - Operator: Add resource type tests (resources_test.go) - Python Runner: Add pytest-cov configuration to pyproject.toml - GitHub Actions: Update all CI workflows with coverage reporting - Update go-lint.yml for backend and operator coverage - Update frontend-lint.yml for frontend coverage - Add new python-test.yml for Python runner coverage - All coverage reports upload to Codecov (informational, won't block PRs) Test validation (local): - Backend: 7 tests passing - Operator: 15 tests passing - Frontend: 21 tests passing (3 suites) - Python: Requires container environment
1 parent 77841b8 commit 7322205

File tree

14 files changed

+4729
-45
lines changed

14 files changed

+4729
-45
lines changed

.github/workflows/frontend-lint.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,19 @@ jobs:
6666
cd components/frontend
6767
npm run build
6868
69+
- name: Run tests with coverage
70+
run: |
71+
cd components/frontend
72+
npm run test:coverage
73+
74+
- name: Upload coverage to Codecov
75+
uses: codecov/codecov-action@v4
76+
with:
77+
token: ${{ secrets.CODECOV_TOKEN }}
78+
files: ./components/frontend/coverage/lcov.info
79+
flags: frontend
80+
name: frontend-coverage
81+
6982
lint-summary:
7083
runs-on: ubuntu-latest
7184
needs: [detect-frontend-changes, lint-frontend]

.github/workflows/go-lint.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,19 @@ jobs:
6969
working-directory: components/backend
7070
args: --timeout=5m
7171

72+
- name: Run tests with coverage
73+
run: |
74+
cd components/backend
75+
go test ./... -coverprofile=coverage.out -covermode=atomic
76+
77+
- name: Upload coverage to Codecov
78+
uses: codecov/codecov-action@v4
79+
with:
80+
token: ${{ secrets.CODECOV_TOKEN }}
81+
files: ./components/backend/coverage.out
82+
flags: backend
83+
name: backend-coverage
84+
7285
lint-operator:
7386
runs-on: ubuntu-latest
7487
needs: detect-go-changes
@@ -107,6 +120,19 @@ jobs:
107120
working-directory: components/operator
108121
args: --timeout=5m
109122

123+
- name: Run tests with coverage
124+
run: |
125+
cd components/operator
126+
go test ./... -coverprofile=coverage.out -covermode=atomic
127+
128+
- name: Upload coverage to Codecov
129+
uses: codecov/codecov-action@v4
130+
with:
131+
token: ${{ secrets.CODECOV_TOKEN }}
132+
files: ./components/operator/coverage.out
133+
flags: operator
134+
name: operator-coverage
135+
110136
lint-summary:
111137
runs-on: ubuntu-latest
112138
needs: [detect-go-changes, lint-backend, lint-operator]

.github/workflows/python-test.yml

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
name: Python Test and Coverage
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
workflow_dispatch:
9+
10+
jobs:
11+
detect-python-changes:
12+
runs-on: ubuntu-latest
13+
outputs:
14+
runner: ${{ steps.filter.outputs.runner }}
15+
steps:
16+
- name: Checkout code
17+
uses: actions/checkout@v5
18+
19+
- name: Check for Python changes
20+
uses: dorny/paths-filter@v3
21+
id: filter
22+
with:
23+
filters: |
24+
runner:
25+
- 'components/runners/claude-code-runner/**/*.py'
26+
- 'components/runners/claude-code-runner/pyproject.toml'
27+
- 'components/runners/claude-code-runner/uv.lock'
28+
29+
test-runner:
30+
runs-on: ubuntu-latest
31+
needs: detect-python-changes
32+
if: needs.detect-python-changes.outputs.runner == 'true' || github.event_name == 'workflow_dispatch'
33+
steps:
34+
- name: Checkout code
35+
uses: actions/checkout@v5
36+
37+
- name: Set up Python
38+
uses: actions/setup-python@v5
39+
with:
40+
python-version: '3.11'
41+
42+
- name: Install uv
43+
run: |
44+
curl -LsSf https://astral.sh/uv/install.sh | sh
45+
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
46+
47+
- name: Install dependencies
48+
run: |
49+
cd components/runners/claude-code-runner
50+
uv pip install --system -e '.[dev]'
51+
52+
- name: Run tests with coverage
53+
run: |
54+
cd components/runners/claude-code-runner
55+
pytest --cov=. --cov-report=xml --cov-report=term
56+
57+
- name: Upload coverage to Codecov
58+
uses: codecov/codecov-action@v4
59+
with:
60+
token: ${{ secrets.CODECOV_TOKEN }}
61+
files: ./components/runners/claude-code-runner/coverage.xml
62+
flags: python-runner
63+
name: claude-runner-coverage
64+
65+
test-summary:
66+
runs-on: ubuntu-latest
67+
needs: [detect-python-changes, test-runner]
68+
if: always()
69+
steps:
70+
- name: Check overall status
71+
run: |
72+
if [ "${{ needs.test-runner.result }}" == "failure" ]; then
73+
echo "Python tests failed"
74+
exit 1
75+
fi
76+
echo "All Python tests passed!"
77+

codecov.yml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
coverage:
2+
status:
3+
project:
4+
default:
5+
target: 70%
6+
threshold: 5%
7+
informational: true # Don't block PRs
8+
patch:
9+
default:
10+
target: 70%
11+
informational: true
12+
13+
flag_management:
14+
default_rules:
15+
statuses:
16+
- type: project
17+
target: 70%
18+
informational: true
19+
20+
flags:
21+
backend:
22+
paths:
23+
- components/backend/
24+
operator:
25+
paths:
26+
- components/operator/
27+
frontend:
28+
paths:
29+
- components/frontend/
30+
python-runner:
31+
paths:
32+
- components/runners/claude-code-runner/
33+
34+
comment:
35+
layout: "reach,diff,flags,tree"
36+
behavior: default
37+
require_changes: false
38+
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
package handlers
2+
3+
import (
4+
"errors"
5+
"strings"
6+
"testing"
7+
"time"
8+
9+
"k8s.io/apimachinery/pkg/runtime/schema"
10+
)
11+
12+
func TestGetProjectSettingsResource(t *testing.T) {
13+
gvr := GetProjectSettingsResource()
14+
15+
tests := []struct {
16+
name string
17+
expected string
18+
actual string
19+
}{
20+
{
21+
name: "Group should be vteam.ambient-code",
22+
expected: "vteam.ambient-code",
23+
actual: gvr.Group,
24+
},
25+
{
26+
name: "Version should be v1alpha1",
27+
expected: "v1alpha1",
28+
actual: gvr.Version,
29+
},
30+
{
31+
name: "Resource should be projectsettings",
32+
expected: "projectsettings",
33+
actual: gvr.Resource,
34+
},
35+
}
36+
37+
for _, tt := range tests {
38+
t.Run(tt.name, func(t *testing.T) {
39+
if tt.actual != tt.expected {
40+
t.Errorf("expected %s, got %s", tt.expected, tt.actual)
41+
}
42+
})
43+
}
44+
}
45+
46+
func TestRetryWithBackoff(t *testing.T) {
47+
t.Run("success on first attempt", func(t *testing.T) {
48+
attempts := 0
49+
operation := func() error {
50+
attempts++
51+
return nil
52+
}
53+
54+
err := RetryWithBackoff(3, 10*time.Millisecond, 100*time.Millisecond, operation)
55+
if err != nil {
56+
t.Errorf("expected no error, got %v", err)
57+
}
58+
if attempts != 1 {
59+
t.Errorf("expected 1 attempt, got %d", attempts)
60+
}
61+
})
62+
63+
t.Run("success after retries", func(t *testing.T) {
64+
attempts := 0
65+
operation := func() error {
66+
attempts++
67+
if attempts < 3 {
68+
return errors.New("temporary failure")
69+
}
70+
return nil
71+
}
72+
73+
err := RetryWithBackoff(5, 10*time.Millisecond, 100*time.Millisecond, operation)
74+
if err != nil {
75+
t.Errorf("expected no error, got %v", err)
76+
}
77+
if attempts != 3 {
78+
t.Errorf("expected 3 attempts, got %d", attempts)
79+
}
80+
})
81+
82+
t.Run("failure after max retries", func(t *testing.T) {
83+
attempts := 0
84+
expectedError := errors.New("persistent failure")
85+
operation := func() error {
86+
attempts++
87+
return expectedError
88+
}
89+
90+
err := RetryWithBackoff(3, 10*time.Millisecond, 100*time.Millisecond, operation)
91+
if err == nil {
92+
t.Error("expected error, got nil")
93+
}
94+
if attempts != 3 {
95+
t.Errorf("expected 3 attempts, got %d", attempts)
96+
}
97+
})
98+
99+
t.Run("respects max delay", func(t *testing.T) {
100+
startTime := time.Now()
101+
attempts := 0
102+
operation := func() error {
103+
attempts++
104+
return errors.New("failure")
105+
}
106+
107+
maxDelay := 50 * time.Millisecond
108+
RetryWithBackoff(3, 10*time.Millisecond, maxDelay, operation)
109+
duration := time.Since(startTime)
110+
111+
// With 3 retries and max delay of 50ms, total time should be less than 150ms
112+
// (allowing some buffer for execution time)
113+
if duration > 200*time.Millisecond {
114+
t.Errorf("expected duration less than 200ms, got %v", duration)
115+
}
116+
})
117+
}
118+
119+
func TestRetryWithBackoffZeroRetries(t *testing.T) {
120+
attempts := 0
121+
operation := func() error {
122+
attempts++
123+
return errors.New("failure")
124+
}
125+
126+
err := RetryWithBackoff(0, 10*time.Millisecond, 100*time.Millisecond, operation)
127+
if err == nil {
128+
t.Error("expected error, got nil")
129+
}
130+
if attempts != 0 {
131+
t.Errorf("expected 0 attempts, got %d", attempts)
132+
}
133+
}
134+
135+
func BenchmarkRetryWithBackoffSuccess(b *testing.B) {
136+
operation := func() error {
137+
return nil
138+
}
139+
140+
b.ResetTimer()
141+
for i := 0; i < b.N; i++ {
142+
RetryWithBackoff(3, 1*time.Millisecond, 10*time.Millisecond, operation)
143+
}
144+
}
145+
146+
// TestGroupVersionResource verifies the GVR format is correct
147+
func TestGroupVersionResource(t *testing.T) {
148+
gvr := GetProjectSettingsResource()
149+
150+
// Verify it's a valid GVR that can be used with dynamic client
151+
if gvr.Empty() {
152+
t.Error("GVR should not be empty")
153+
}
154+
155+
// Verify string representation contains expected parts
156+
gvrString := gvr.String()
157+
if !strings.Contains(gvrString, "vteam.ambient-code") {
158+
t.Errorf("GVR string should contain group: %s", gvrString)
159+
}
160+
if !strings.Contains(gvrString, "v1alpha1") {
161+
t.Errorf("GVR string should contain version: %s", gvrString)
162+
}
163+
if !strings.Contains(gvrString, "projectsettings") {
164+
t.Errorf("GVR string should contain resource: %s", gvrString)
165+
}
166+
}
167+
168+
// Mock test for schema validation
169+
func TestSchemaGroupVersionResource(t *testing.T) {
170+
gvr := GetProjectSettingsResource()
171+
172+
// Verify the type
173+
var _ schema.GroupVersionResource = gvr
174+
175+
// Verify the individual components instead of string format
176+
if gvr.Group != "vteam.ambient-code" {
177+
t.Errorf("Expected group 'vteam.ambient-code', got '%s'", gvr.Group)
178+
}
179+
if gvr.Version != "v1alpha1" {
180+
t.Errorf("Expected version 'v1alpha1', got '%s'", gvr.Version)
181+
}
182+
if gvr.Resource != "projectsettings" {
183+
t.Errorf("Expected resource 'projectsettings', got '%s'", gvr.Resource)
184+
}
185+
}
186+

components/frontend/jest.config.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
const nextJest = require('next/jest')
2+
3+
const createJestConfig = nextJest({
4+
dir: './',
5+
})
6+
7+
const customJestConfig = {
8+
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
9+
testEnvironment: 'jest-environment-jsdom',
10+
moduleNameMapper: {
11+
'^@/(.*)$': '<rootDir>/src/$1',
12+
},
13+
collectCoverageFrom: [
14+
'src/**/*.{js,jsx,ts,tsx}',
15+
'!src/**/*.d.ts',
16+
'!src/**/*.stories.{js,jsx,ts,tsx}',
17+
'!src/**/__tests__/**',
18+
],
19+
coverageThreshold: {
20+
global: {
21+
branches: 70,
22+
functions: 70,
23+
lines: 70,
24+
statements: 70,
25+
},
26+
},
27+
}
28+
29+
module.exports = createJestConfig(customJestConfig)
30+

components/frontend/jest.setup.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import '@testing-library/jest-dom'
2+

0 commit comments

Comments
 (0)