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
264 changes: 264 additions & 0 deletions .github/workflows/pr-coverage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
name: PR Coverage Check

on:
pull_request:
branches: [main, dev/*, hotfix/*]

jobs:
coverage:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write

steps:
- name: Checkout PR branch
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Cache turbo build setup
uses: actions/cache@v4
with:
path: .turbo
key: ${{ runner.os }}-turbo-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20.x

- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 9.1.3
run_install: |
- recursive: true
args: [--frozen-lockfile, --strict-peer-dependencies]

- name: Build
run: pnpm turbo run build --cache-dir=.turbo

- name: Run tests with coverage
run: pnpm turbo run test:lib --cache-dir=.turbo

- name: Merge coverage reports
run: |
mkdir -p coverage
# Find all lcov.info files and merge them
find . -path ./node_modules -prune -o -name 'lcov.info' -print 2>/dev/null | head -20 > lcov-files.txt
if [ -s lcov-files.txt ]; then
npx lcov-result-merger './libs/**/coverage/lcov.info' './apps/**/coverage/lcov.info' ./coverage/lcov.info || true
fi

- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v44
with:
files: |
**/*.ts
**/*.tsx
files_ignore: |
# Test files
**/*.test.ts
**/*.test.tsx
**/*.spec.ts
**/*.spec.tsx
**/__test__/**
**/__tests__/**
**/__mocks__/**
**/test-setup.ts
**/test-globals.ts
**/setup.ts
# Type definitions and models
**/*.d.ts
**/models/**
**/types/**
**/interfaces/**
**/*Types.ts
**/*Types.tsx
**/*Model.ts
**/*Models.ts
**/*Interface.ts
**/*Interfaces.ts
**/types.ts
**/types.tsx
**/constants.ts
**/constants/**
# Config files
**/*.config.ts
**/*.config.tsx
**/vite.config.*
**/vitest.config.*
**/tsconfig.*
# Index/barrel files (just re-exports)
**/index.ts
**/index.tsx
# Node modules and build output
**/node_modules/**
**/dist/**
**/build/**

- name: Check coverage on changed files
id: coverage-check
run: |
echo "## 📊 Coverage Report for Changed Files" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY

# Get the list of changed source files
CHANGED_FILES="${{ steps.changed-files.outputs.all_changed_files }}"

if [ -z "$CHANGED_FILES" ]; then
echo "No source files changed in this PR." >> $GITHUB_STEP_SUMMARY
echo "coverage_status=skip" >> $GITHUB_OUTPUT
exit 0
fi

echo "### Changed Files:" >> $GITHUB_STEP_SUMMARY
for file in $CHANGED_FILES; do
echo "- \`$file\`" >> $GITHUB_STEP_SUMMARY
done
echo "" >> $GITHUB_STEP_SUMMARY

# Check if coverage file exists
if [ ! -f "./coverage/lcov.info" ]; then
echo "⚠️ No merged coverage report found. Checking individual coverage files..." >> $GITHUB_STEP_SUMMARY

# Look for individual coverage files
COVERAGE_FILES=$(find . -path ./node_modules -prune -o -name 'lcov.info' -print 2>/dev/null | grep -v node_modules || true)

if [ -z "$COVERAGE_FILES" ]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "❌ **No coverage data found.** Please ensure tests generate coverage reports." >> $GITHUB_STEP_SUMMARY
echo "coverage_status=no_data" >> $GITHUB_OUTPUT
exit 0
fi
fi

# Parse coverage and check changed files
UNCOVERED_FILES=""
COVERED_FILES=""
PARTIAL_FILES=""

for file in $CHANGED_FILES; do
# Skip if file doesn't exist (might have been deleted)
if [ ! -f "$file" ]; then
continue
fi

# Find the corresponding coverage file for this source file
# Look in the package's coverage directory
PKG_DIR=$(echo "$file" | sed 's|/src/.*||')
LCOV_FILE="$PKG_DIR/coverage/lcov.info"

if [ -f "$LCOV_FILE" ]; then
# Check if this file is mentioned in the coverage report
if grep -q "SF:.*$(basename $file)" "$LCOV_FILE" 2>/dev/null; then
# Get line coverage for this file
FILE_SECTION=$(awk "/SF:.*$(basename $file)/,/end_of_record/" "$LCOV_FILE" 2>/dev/null || true)

if [ -n "$FILE_SECTION" ]; then
LINES_FOUND=$(echo "$FILE_SECTION" | grep "^LF:" | cut -d: -f2 || echo "0")
LINES_HIT=$(echo "$FILE_SECTION" | grep "^LH:" | cut -d: -f2 || echo "0")

if [ "$LINES_FOUND" -gt 0 ] 2>/dev/null; then
COVERAGE_PCT=$((LINES_HIT * 100 / LINES_FOUND))
if [ "$COVERAGE_PCT" -ge 80 ]; then
COVERED_FILES="$COVERED_FILES\n✅ \`$file\` - ${COVERAGE_PCT}% covered"
elif [ "$COVERAGE_PCT" -gt 0 ]; then
PARTIAL_FILES="$PARTIAL_FILES\n⚠️ \`$file\` - ${COVERAGE_PCT}% covered (needs improvement)"
else
UNCOVERED_FILES="$UNCOVERED_FILES\n❌ \`$file\` - 0% covered"
fi
fi
fi
else
UNCOVERED_FILES="$UNCOVERED_FILES\n❌ \`$file\` - Not covered by tests"
fi
else
# No coverage file found for this package - might be acceptable
echo "ℹ️ No coverage data for package: $PKG_DIR" >> $GITHUB_STEP_SUMMARY
fi
done

echo "### Coverage Results:" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY

if [ -n "$COVERED_FILES" ]; then
echo "#### ✅ Well Covered:" >> $GITHUB_STEP_SUMMARY
echo -e "$COVERED_FILES" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
fi

if [ -n "$PARTIAL_FILES" ]; then
echo "#### ⚠️ Partially Covered (< 80%):" >> $GITHUB_STEP_SUMMARY
echo -e "$PARTIAL_FILES" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
fi

if [ -n "$UNCOVERED_FILES" ]; then
echo "#### ❌ Not Covered:" >> $GITHUB_STEP_SUMMARY
echo -e "$UNCOVERED_FILES" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Please add tests for the uncovered files before merging.**" >> $GITHUB_STEP_SUMMARY
echo "coverage_status=fail" >> $GITHUB_OUTPUT
else
echo "" >> $GITHUB_STEP_SUMMARY
echo "🎉 **All changed files have adequate test coverage!**" >> $GITHUB_STEP_SUMMARY
echo "coverage_status=pass" >> $GITHUB_OUTPUT
fi

- name: Post coverage comment on PR
if: always()
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');

// Read the step summary
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
let summary = '';
try {
summary = fs.readFileSync(summaryPath, 'utf8');
} catch (e) {
summary = '📊 Coverage check completed. See workflow run for details.';
}

// Find existing comment
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});

const botComment = comments.find(comment =>
comment.user.type === 'Bot' &&
comment.body.includes('📊 Coverage Report for Changed Files')
);

const commentBody = summary || '📊 Coverage check completed. See workflow run for details.';

if (botComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: commentBody
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: commentBody
});
}

- name: Fail if coverage is insufficient
if: steps.coverage-check.outputs.coverage_status == 'fail'
run: |
echo "❌ Some changed files lack test coverage. Please add tests before merging."
exit 1
2 changes: 1 addition & 1 deletion apps/iframe-app/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default defineConfig({
include: ['src/**/*.{test,spec}.{ts,tsx}'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
reporter: ['text', 'json', 'html', 'lcov'],
exclude: ['node_modules/', 'src/test/', '**/*.d.ts', '**/*.config.*', '**/mockData.ts'],
},
},
Expand Down
2 changes: 1 addition & 1 deletion apps/vs-code-designer/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default defineConfig({
name: packageJson.name,
environment: 'node',
setupFiles: ['test-setup.ts'],
coverage: { enabled: true, provider: 'istanbul', include: ['src/**/*'], reporter: ['html', 'cobertura'] },
coverage: { enabled: true, provider: 'istanbul', include: ['src/**/*'], reporter: ['html', 'cobertura', 'lcov'] },
restoreMocks: true,
// Exclude E2E tests that use Mocha instead of Vitest
exclude: [
Expand Down
2 changes: 1 addition & 1 deletion apps/vs-code-react/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export default defineProject({
name: packageJson.name,
environment: 'jsdom',
setupFiles: ['test-setup.ts'],
coverage: { enabled: true, provider: 'istanbul', include: ['src/app/**/*'], reporter: ['html', 'cobertura'] },
coverage: { enabled: true, provider: 'istanbul', include: ['src/app/**/*'], reporter: ['html', 'cobertura', 'lcov'] },
restoreMocks: true,
},
});
20 changes: 18 additions & 2 deletions e2e/chatClient/fixtures/sse-fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,26 @@
import { test as base, type Page } from '@playwright/test';
import { generateSSEResponse, AGENT_CARD } from '../mocks/sse-generators';

// Helper to encode a string to Base64 with proper UTF-8 support (for Unicode characters)
function utf8ToBase64(str: string): string {
// Use Buffer in Node.js environment (Playwright runs in Node)
if (typeof Buffer !== 'undefined') {
return Buffer.from(str, 'utf-8').toString('base64');
}
// Fallback for browser environment
const encoder = new TextEncoder();
const bytes = encoder.encode(str);
let binary = '';
for (const byte of bytes) {
binary += String.fromCharCode(byte);
}
return btoa(binary);
}

// Helper to create a mock JWT token with a given payload
function createMockJwt(payload: Record<string, unknown>): string {
const header = btoa(JSON.stringify({ alg: 'RS256', typ: 'JWT' }));
const payloadStr = btoa(JSON.stringify(payload));
const header = utf8ToBase64(JSON.stringify({ alg: 'RS256', typ: 'JWT' }));
const payloadStr = utf8ToBase64(JSON.stringify(payload));
const signature = 'mock-signature';
return `${header}.${payloadStr}.${signature}`;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,8 @@ const AGENT_CARD_URL = 'http://localhost:3001/api/agents/test/.well-known/agent-

// Helper to encode a string to Base64 with proper UTF-8 support (for Unicode characters)
function utf8ToBase64(str: string): string {
const encoder = new TextEncoder();
const bytes = encoder.encode(str);
let binary = '';
for (const byte of bytes) {
binary += String.fromCharCode(byte);
}
return btoa(binary);
// Use Buffer in Node.js environment (Playwright runs in Node)
return Buffer.from(str, 'utf-8').toString('base64');
}

test.describe('Username Display', { tag: '@mock' }, () => {
Expand Down Expand Up @@ -185,9 +180,9 @@ test.describe('Username Edge Cases', { tag: '@mock' }, () => {
test('should handle missing name claim in JWT gracefully', async ({ page }) => {
// Override the mock to return a JWT without a 'name' claim
await page.route('**/.auth/me', async (route) => {
const header = btoa(JSON.stringify({ alg: 'RS256', typ: 'JWT' }));
const header = utf8ToBase64(JSON.stringify({ alg: 'RS256', typ: 'JWT' }));
// JWT with no 'name' claim - only has sub and exp
const payload = btoa(JSON.stringify({ sub: 'test-user-123', exp: Math.floor(Date.now() / 1000) + 3600 }));
const payload = utf8ToBase64(JSON.stringify({ sub: 'test-user-123', exp: Math.floor(Date.now() / 1000) + 3600 }));
const mockJwt = `${header}.${payload}.mock-signature`;

await route.fulfill({
Expand Down
2 changes: 1 addition & 1 deletion libs/a2a-core/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default defineConfig({
setupFiles: ['./src/react/test/setup.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
reporter: ['text', 'json', 'html', 'lcov'],
exclude: ['node_modules/', 'dist/', '**/*.d.ts', '**/*.config.ts', '**/index.ts'],
thresholds: {
lines: 70,
Expand Down
2 changes: 1 addition & 1 deletion libs/chatbot/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export default defineProject({
name: packageJson.name,
environment: 'jsdom',
setupFiles: ['test-setup.ts'],
coverage: { enabled: true, provider: 'istanbul', include: ['src/**/*'], reporter: ['html', 'cobertura'] },
coverage: { enabled: true, provider: 'istanbul', include: ['src/**/*'], reporter: ['html', 'cobertura', 'lcov'] },
restoreMocks: true,
},
});
2 changes: 1 addition & 1 deletion libs/data-mapper-v2/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export default defineProject({
name: packageJson.name,
environment: 'jsdom',
setupFiles: ['test-setup.ts'],
coverage: { enabled: true, provider: 'istanbul', include: ['src/**/*'], reporter: ['html', 'cobertura'] },
coverage: { enabled: true, provider: 'istanbul', include: ['src/**/*'], reporter: ['html', 'cobertura', 'lcov'] },
restoreMocks: true,
},
});
2 changes: 1 addition & 1 deletion libs/data-mapper/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export default defineProject({
name: packageJson.name,
environment: 'jsdom',
setupFiles: ['test-setup.ts'],
coverage: { enabled: true, provider: 'istanbul', include: ['src/**/*'], reporter: ['html', 'cobertura'] },
coverage: { enabled: true, provider: 'istanbul', include: ['src/**/*'], reporter: ['html', 'cobertura', 'lcov'] },
restoreMocks: true,
},
});
2 changes: 1 addition & 1 deletion libs/designer-ui/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default defineProject({
root: './',
include: ['src/**/*.{test,spec}.{ts,tsx}'],
exclude: ['node_modules', 'build'],
coverage: { enabled: true, provider: 'istanbul', include: ['src/**/*'], reporter: ['html', 'cobertura'] },
coverage: { enabled: true, provider: 'istanbul', include: ['src/**/*'], reporter: ['html', 'cobertura', 'lcov'] },
restoreMocks: true,
},
});
Loading
Loading