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
72 changes: 72 additions & 0 deletions .github/workflows/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# CI/CD Workflows

This directory contains GitHub Actions workflows for automated testing, building, and deployment.

## 🧪 Test Workflow (`test.yml`)

**Triggers**: Pull requests and pushes to `main`/`develop`

### Jobs:

1. **`test`** - Unit Tests
- Runs all Jest unit tests
- Ensures all tests pass

2. **`lint`** - Code Quality
- Checks TypeScript compilation
- Validates code formatting (Prettier)

3. **`build`** - Build Verification
- Ensures project builds successfully
- Uploads build artifacts

4. **`all-checks-passed`** - Gate Keeper
- Only passes if ALL other jobs succeed
- **This is the required status check for branch protection**

### Test Requirements:

- All unit tests must pass
- No failing test cases

## 🚀 E2E Workflows

- `pr-main-e2e.yml` - E2E tests against production
- `pr-prod-e2e.yml` - Production E2E validation
- `manual-e2e.yml` - Manual E2E trigger
- `publish-on-merge.yml` - Package publishing

## 🔧 Local Development

```bash
# Run tests
yarn test

# Run tests in watch mode
yarn test:watch

# Run tests in CI mode
yarn test:ci

# Type checking
yarn type-check

# Code formatting
yarn lint
yarn lint:fix

# Full CI simulation
yarn ci:test
```

## 📊 Artifacts

Each workflow run produces:

- **Build Artifacts** (3 days retention)
- **Test Results** (30 days retention for E2E)

## 🔍 Monitoring

- Test results visible in workflow logs
- Failed runs block PR merging automatically
2 changes: 2 additions & 0 deletions .github/workflows/manual-e2e.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
name: Manual E2E Validation
permissions:
contents: read

on:
workflow_dispatch:
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/pr-main-e2e.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
name: UI prod e2e with local sdk build

permissions:
contents: read

on:
pull_request:
branches: [main]
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/pr-prod-e2e.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
name: UI prod e2e with staging sdk build
permissions:
contents: read

on:
pull_request:
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/publish-on-merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ on:

jobs:
publish:
permissions:
contents: write
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand Down
144 changes: 144 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
name: Unit Tests
permissions:
contents: read

on:
pull_request:
branches: [main, prod]
types: [opened, synchronize, reopened]
push:
branches: [main, prod]

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true

jobs:
type-check:
runs-on: ubuntu-latest
timeout-minutes: 10

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

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'yarn'
cache-dependency-path: yarn.lock

- name: Install dependencies
run: yarn install --frozen-lockfile

- name: Run TypeScript type checking
run: yarn tsc --noEmit

lint:
runs-on: ubuntu-latest
timeout-minutes: 10

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

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'yarn'
cache-dependency-path: yarn.lock

- name: Install dependencies
run: yarn install --frozen-lockfile

- name: Check code formatting
run: |
if command -v yarn prettier &> /dev/null; then
echo "Running Prettier check..."
yarn prettier --check "src/**/*.{ts,tsx,js,jsx}" || {
echo "❌ Code formatting issues found. Please run 'yarn prettier --write .' to fix them."
exit 1
}
else
echo "Prettier not configured, skipping format check"
fi

test:
runs-on: ubuntu-latest
timeout-minutes: 15
needs: [type-check, lint]

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

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'yarn'
cache-dependency-path: yarn.lock

- name: Install dependencies
run: yarn install --frozen-lockfile

- name: Run unit tests with coverage
run: yarn test:ci --coverage
env:
CI: true

build:
runs-on: ubuntu-latest
timeout-minutes: 10

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

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'yarn'
cache-dependency-path: yarn.lock

- name: Install dependencies
run: yarn install --frozen-lockfile

- name: Build project
run: yarn build

- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build-artifacts-${{ github.run_id }}
path: |
dist/
retention-days: 3

# This job will only run if all previous jobs succeed
# It serves as a single status check for branch protection
all-checks-passed:
runs-on: ubuntu-latest
needs: [type-check, lint, test, build]
if: always()

steps:
- name: Check if all jobs succeeded
run: |
if [[ "${{ needs.type-check.result }}" == "success" &&
"${{ needs.lint.result }}" == "success" &&
"${{ needs.test.result }}" == "success" &&
"${{ needs.build.result }}" == "success" ]]; then
echo "✅ All checks passed!"
exit 0
else
echo "❌ Some checks failed:"
echo " Type Check: ${{ needs.type-check.result }}"
echo " Lint: ${{ needs.lint.result }}"
echo " Test: ${{ needs.test.result }}"
echo " Build: ${{ needs.build.result }}"
exit 1
fi
8 changes: 1 addition & 7 deletions demo/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,7 @@ export function App() {
mode,
enableAnalytics: false,
auth: { type: 'key', clientKey },
streamOptions: {
streamWarmup: warmup,
sessionTimeout,
compatibilityMode,
fluent,
},
streamOptions: { streamWarmup: warmup, sessionTimeout, compatibilityMode, fluent },
});

async function onClick() {
Expand All @@ -52,7 +47,6 @@ export function App() {
<section>
<div id="left">
<textarea
type="text"
placeholder="Enter text to stream"
value={text}
onInput={e => setText(e.currentTarget.value)}
Expand Down
21 changes: 18 additions & 3 deletions demo/hooks/useAgentManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,31 @@ interface UseAgentManagerOptions {
wsURL: string;
mode: ChatMode;
auth: Auth;
distinctId?: string;
enableAnalytics?: boolean;
streamOptions?: {
streamWarmup?: boolean;
sessionTimeout?: number;
compatibilityMode?: 'on' | 'off' | 'auto';
fluent?: boolean;
};
enableAnalytics?: boolean;
distinctId?: string;
mixpanelKey?: string;
mixpanelAdditionalProperties?: Record<string, any>;
}

export function useAgentManager(props: UseAgentManagerOptions) {
const { agentId, baseURL, wsURL, mode, auth, enableAnalytics, distinctId, streamOptions } = props;
const {
agentId,
baseURL,
wsURL,
mode,
auth,
enableAnalytics,
distinctId,
streamOptions,
mixpanelKey,
mixpanelAdditionalProperties,
} = props;

const [isSpeaking, setIsSpeaking] = useState(false);
const [messages, setMessages] = useState<Message[]>([]);
Expand Down Expand Up @@ -88,6 +101,8 @@ export function useAgentManager(props: UseAgentManagerOptions) {
wsURL,
enableAnalitics: enableAnalytics,
distinctId,
mixpanelKey,
mixpanelAdditionalProperties,
streamOptions,
});

Expand Down
42 changes: 42 additions & 0 deletions jest.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
module.exports = {
testEnvironment: 'jsdom',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
roots: ['<rootDir>/src'],
testMatch: ['**/**.spec.ts', '**/**.test.ts', '**/**.test.tsx'],
transform: { '^.+\\.tsx?$': ['ts-jest', { useESM: true, tsconfig: '<rootDir>/tsconfig.json' }] },
extensionsToTreatAsEsm: ['.ts', '.tsx'],
moduleNameMapper: { '^\\$/(.*)$': '<rootDir>/src/$1', '^%/(.*)$': '<rootDir>/src/types/$1' },
setupFiles: ['<rootDir>/jest.setup.ts'],
...(process.env.CI && { reporters: ['summary'] }),

// Coverage configuration - only for files that have tests
collectCoverage: false, // Only collect when explicitly requested
collectCoverageFrom: [
// Only collect coverage from files that have corresponding test files
'src/services/agent-manager/index.ts',
'src/services/agent-manager/connect-to-manager.ts',
'src/services/streaming-manager/index.ts',
],
coverageDirectory: 'coverage',
coverageReporters: ['text', 'text-summary', 'json-summary'],
coveragePathIgnorePatterns: [
'node_modules',
'.*/test-utils/*',
'.*/types/*',
'.*/config/environment.ts',
'.*mock.*',
],
coverageThreshold: {
global: {
branches: 70,
functions: 86,
lines: 87,
statements: 80,
},
},

// CI/CD optimizations
maxWorkers: process.env.CI ? 2 : '50%',
testTimeout: process.env.CI ? 10000 : 5000,
verbose: process.env.CI ? false : true,
};
21 changes: 21 additions & 0 deletions jest.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const mockDataChannel = { onopen: null, onmessage: null, send: jest.fn(), readyState: 'open' };

const mockPeerConnection = {
createDataChannel: jest.fn(() => mockDataChannel),
onicecandidate: jest.fn(),
oniceconnectionstatechange: jest.fn(),
ontrack: jest.fn(),
setRemoteDescription: jest.fn().mockResolvedValue(undefined),
createAnswer: jest.fn().mockResolvedValue({ type: 'answer', sdp: 'mock-sdp' }),
setLocalDescription: jest.fn().mockResolvedValue(undefined),
close: jest.fn(),
iceConnectionState: 'connected',
};

const mockRTCPeerConnection = jest.fn().mockImplementation(() => mockPeerConnection);
(mockRTCPeerConnection as any).generateCertificate = jest.fn().mockResolvedValue({});

// Mock MediaStream
global.MediaStream = jest.fn().mockImplementation(() => ({ getTracks: jest.fn(() => []) }));

global.window.RTCPeerConnection = mockRTCPeerConnection as any;
Loading