Test Framework Documentation
This document describes the unit testing framework setup for the Wegent project.
Pre-push checks run the complete frontend unit suite with two Jest workers by default to prevent concurrent JSDOM instances from causing resource-contention timeouts. Set FRONTEND_PRE_PUSH_TEST_WORKERS to adjust concurrency; this does not change test scope or timeout limits.
Overviewβ
The project includes comprehensive unit testing support across all modules:
- Backend (FastAPI): pytest + pytest-asyncio + pytest-cov + pytest-mock
- Executor (AI Agent Engine): pytest + pytest-mock + pytest-asyncio
- Executor Manager (Task Management): pytest + pytest-mock + pytest-cov
- Shared (Utilities): pytest + pytest-cov
- Frontend (Next.js + React 19): Jest + @testing-library/react
Current Test Coverageβ
Backend (backend/)β
- β Core security: Authentication, JWT tokens, password hashing
- β Configuration management
- β Exception handling
- β User service and models
- β GitHub repository provider
- β³ API endpoints (placeholder directory exists)
Executor (executor/)β
- β Agent factory
- β Base agent classes
- β Mocked AI client interactions (Anthropic, OpenAI)
Executor Manager (executor_manager/)β
- β Base executor classes
- β Task dispatcher
- β Docker executor and utilities
- β Docker constants and configuration
Shared (shared/)β
- β Cryptography utilities
- β Sensitive data masking (tokens, API keys, etc.)
Frontend (frontend/)β
- β³ Component tests (basic setup in place)
- β³ Hook tests
- β³ Utility tests
Test Coverage Goalsβ
- Target: 40-60% code coverage initially
- Priority: Core business logic and critical paths
- Strategy: Incremental coverage improvement
Backend Testsβ
Running Testsβ
cd backend
pytest # Run all tests
pytest tests/core/ # Run core tests only
pytest --cov=app # Run with coverage report
pytest -v # Verbose output
pytest -k test_security # Run specific test pattern
pytest -m unit # Run only unit tests
pytest -m integration # Run only integration tests
Test Structureβ
backend/tests/
βββ conftest.py # Global test fixtures
βββ core/ # Core infrastructure tests
β βββ test_security.py # Authentication & JWT tests
β βββ test_config.py # Configuration tests
β βββ test_exceptions.py # Exception handler tests
βββ services/ # Service layer tests
β βββ test_user_service.py # User service tests
βββ models/ # Data model tests
β βββ test_user_model.py # User model tests
βββ repository/ # Repository integration tests
β βββ test_github_provider.py
βββ api/ # API endpoint tests (placeholder)
Test Configurationβ
The backend uses pytest.ini for configuration with the following settings:
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts =
-v
--strict-markers
--cov=app
--cov-report=term-missing
--cov-report=html
--cov-report=xml
asyncio_mode = auto
markers =
unit: Unit tests
integration: Integration tests
slow: Slow running tests
Key Fixturesβ
test_db: SQLite in-memory database session (function scope)test_settings: Test settings with overridden valuestest_user: Test user instancetest_admin_user: Test admin user instancetest_inactive_user: Inactive test user instancetest_token: Valid JWT token for test usertest_admin_token: Valid JWT token for admin usertest_client: FastAPI test client with database overridemock_redis: Mocked Redis client
Executor Testsβ
Running Testsβ
cd executor
pytest tests/ --cov=agents
Test Structureβ
executor/tests/
βββ conftest.py # Executor-specific fixtures
βββ agents/ # Agent tests
Key Fixturesβ
mock_anthropic_client: Mocked Anthropic API client for testing Claude modelsmock_openai_client: Mocked OpenAI API client for testing GPT modelsmock_callback_client: Mocked callback HTTP client for agent responsessuppress_resource_warnings: Session-scoped fixture to suppress ResourceWarning messagescleanup_logging: Session-scoped fixture to clean up logging handlers and prevent daemon thread errors
Executor Manager Testsβ
Running Testsβ
cd executor_manager
pytest tests/ --cov=executors
Key Fixturesβ
mock_docker_client: Mocked Docker SDK client for container operationsmock_executor_config: Mock executor configuration with image, CPU, memory, and network settings
Test Structureβ
executor_manager/tests/
βββ conftest.py # Executor manager fixtures
βββ executors/ # Executor tests
βββ test_base.py
βββ test_dispatcher.py
βββ test_docker_executor.py
βββ test_docker_utils.py
βββ test_docker_constants.py
Shared Testsβ
Running Testsβ
cd shared
pytest tests/ --cov=utils
Test Structureβ
shared/tests/
βββ utils/
βββ test_crypto.py # Encryption/decryption tests
βββ test_sensitive_data_masker.py # Sensitive data masking tests
Key Features Testedβ
- Cryptography: Encryption and decryption of sensitive data (Git tokens, API keys)
- Data Masking: Automatic masking of sensitive information in logs and outputs
- GitHub tokens (
github_pat_*) - Anthropic API keys (
sk-ant-api03-*) - OpenAI API keys
- Generic API keys and secrets
- File path protection (no false positives)
- URL protection (no false positives)
- GitHub tokens (
Frontend Testsβ
Running Testsβ
cd Wegent
pnpm --dir frontend test # Run all tests
pnpm --dir frontend run test:watch # Watch mode
pnpm --dir frontend run test:coverage # With coverage report
Test Structureβ
frontend/src/__tests__/
βββ utils/ # Utility function tests
βββ hooks/ # React hooks tests
βββ components/ # Component tests
Wework Unit Testsβ
Run the complete Wework unit test suite from the repository root:
pnpm --dir wework test
When debugging one test file, pass its path or file name to the test script. The following forms are equivalent and collect only matching Vitest files:
pnpm --dir wework test runtimePaneMessages.test.ts
pnpm --dir wework test -- runtimePaneMessages.test.ts
For compatibility, the test script removes one standalone leading --, then
passes all remaining file filters and Vitest options through unchanged. For a
focused run, confirm that the reported Test Files count matches the intended
scope so an accidental full-suite run is caught immediately.
Wework Desktop Cloud Device Testsβ
Run the cloud-device feature E2E from the repository root:
pnpm --filter wework e2e:desktop:cloud-features
This scenario starts a real backend, Redis, cloud-device executor, and Electron
application. It verifies cloud project tasks, Goal auto-continuation and unread
state, busy-turn Goal handoff, and the local/cloud model protocol matrix. On
failure, use the reported wework/test-results/desktop-e2e/<run-id>/ directory
to correlate frontend, backend, and executor logs before changing behavior.
Continuous Integrationβ
GitHub Actions Workflowβ
The test and cache workflows run automatically as follows:
.github/workflows/test.ymlresponds to PRs targetingmainand merge-queue checks..github/workflows/ci-cache-warmup.ymlprewarms shared caches after related dependencies, source, or CI configuration entermain.
CI first runs .github/scripts/classify-ci-changes.sh to select module jobs from
the changed paths. Dependency relationships are included. For example, a
shared/ change runs the Backend, Executor Manager, Knowledge Engine, Shared,
and CLI suites, while a packages/chat-core/ change runs both Frontend and
Wework checks. Changes to the test workflows or the classifier itself select
every module so CI orchestration changes receive full validation.
Apply the ci:all label to a PR to bypass path classification and force every
module test, the platform E2E suite, Wework browser and desktop E2E, and the
Wework macOS memory gate. This is useful when changed paths might not capture
the full blast radius of a change. All three test workflows respond to label
events; without ci:all, test selection still follows the changed paths.
ci:memory continues to trigger only the Wework macOS memory gate.
Feature-branch pushes do not run a duplicate CI suite; the pull_request event
validates the branch after a PR is opened. A newer commit to the same PR or to
main cancels the older in-progress run. test-summary and lint-summary
always appear and verify that every selected job actually succeeded. Skipped,
unrelated modules do not make the summary fail.
CI reliability depends on these invariants:
- The platform E2E MySQL service must allow enough startup grace for its first
initialization. Its health-check
start-periodshould cover a cold start on a shared runner before the interval and retry count determine failure; reruns must not hide initialization timeouts. - Repository Policy jobs triggered by
pull_request_targetmust run policy scripts from the trusted base checkout, while checking out the scan target by the PR head repository and immutable head SHA. Do not depend onrefs/pull/<number>/merge, which can be replaced or removed while a PR is synchronized, and never execute code supplied by the PR in the policy job. - When final text and related controls render independently in desktop E2E, wait for each target element before reading attributes or asserting state. Visible final text does not imply that associated disclosure controls or timelines have mounted.
- After desktop E2E triggers an asynchronous export, disable, reload, immediate inspection, creation, or archival, do not treat a success notice, request count, re-enabled button, or optimistic DOM update as the final result. Assert at least one independently verifiable outcome from the actual artifact, persisted file, stopped service, completed-state copy, or backend/UI state loaded again. If the test environment exposes no real outcome boundary, do not fabricate coverage with a mocked readback.
- After a page reload, task switch, or injected lifecycle/transcript event, desktop E2E must not wait only for sidebar, composer, or debug state. Before reading a virtualized transcript or asserting text occurrence counts, wait for the expected conversation text to appear in a page snapshot, then retain exact occurrence assertions so fixed delays or reruns cannot hide missing or duplicated messages.
- Wework release-note lookups for GitHub commit authors retry transient API or TLS failures a limited number of times with exponential backoff. The release job must still fail after retries are exhausted instead of silently omitting attribution or publishing incomplete notes.
CI Cache Ownershipβ
Large caches are prewarmed and owned by main. Pull requests and merge-queue
runs may restore the default-branch cache, but they do not save another copy:
- Linux workspace
node_moduleshas one shared cache keyed by the Node version andpnpm-lock.yaml. Tests, Lint, and platform E2E reuse it. - Wework browser and desktop E2E use dependency images published to GHCR. Image tags are derived from the corresponding Dockerfile content hash. The workflow builds and publishes an image only when that tag is missing, then runs downstream jobs directly in the image container instead of installing Node, pnpm, uv, Rust, Playwright browsers, or desktop system packages per job. Local debugging commands do not build these CI images.
- uv caches downloads and build results only, not project
.venvdirectories. Python 3.10, 3.11, and 3.12 each have one shared cache whose key includes the dependency lockfiles, and CI pruning runs before save. - Playwright browsers, the Next.js build cache, local harness CLIs, and desktop
Cargo targets use explicit restore/save steps; only
refs/heads/mainsaves. OpenCode, Claude Code, and Kimi Code share the integrity-locked npm install defined by.github/claude-code-cli/package-lock.json. - Rust unit tests, the Windows check, release and snapshot binaries, and the
macOS memory gate use sccache. The
mainwarmup runs the same Electron desktop build onmacos-14; non-mainjobs access the shared compiler cache in read-only mode. If sccache installation is temporarily unavailable, jobs emit a warning and compile without the cache instead of failing. - Wework Desktop Core E2E retains its
main-owned Cargo target cache because several desktop jobs must reuse the same complete binary output. - Platform E2E, Release, and Snapshot Docker BuildKit caches live in corresponding GHCR build-cache tags instead of consuming GitHub Actions dependency cache storage.
- Outside Wework E2E, Executor and Electron system dependency installers first
check packages already present on the runner. Only
mainwrites the APT download cache; PRs restore it read-only.
.github/workflows/ci-cache-warmup.yml runs after related source, dependency
lockfile, or cache implementation changes enter main, and rejects manual
warmups from non-main refs. It downloads dependencies or compiles cache
entries without repeating tests already validated by the merge queue. Do not add large automatically saved PR or
merge-group caches. Use actions/cache/restore plus a main-only
actions/cache/save, or reuse the existing shared action.
Writing GitHub Actions Outputsβ
Every line written to $GITHUB_OUTPUT must use the name=value format. Do not
write the substituted output of a command that can emit progress or diagnostic
messages directly to that file. Extract the needed value first, then write the
output. For example, to resolve the Playwright version:
version="$(pnpm exec playwright --version | sed -n 's/^Version //p')"
echo "version=$version" >> "$GITHUB_OUTPUT"
This prevents package-manager supply-chain checks or other auxiliary logs from invalidating the workflow output format.
Workflow Jobsβ
Regular PR checks must cover module tests; E2E jobs do not replace them:
- test-backend runs the Backend suite with Python 3.11 and
uv run pytest. - test-executor runs
cargo fmt --check,cargo test --all-features, and Clippy for the Rust Executor. It placesCARGO_TARGET_DIRunder the runner's temporary directory to avoid shared build contamination and workspace disk pressure. - test-executor-manager, test-shared, and test-knowledge-engine run
their Python suites with
uv run pytest. - test-frontend runs the Chat Core and Frontend unit tests.
- test-wework runs the Wework unit suite with Vitest and generates coverage.
- test-wegent-cli and test-wegent-cli-integration run CLI unit tests and integration tests backed by MySQL, Redis, and a real Backend, respectively.
- test-summary always runs and depends on every job above. It must fail when any module test job fails.
E2E coverage lives in dedicated workflows: .github/workflows/e2e-tests.yml
covers product end-to-end flows, while .github/workflows/wework-e2e.yml covers
Wework flows. They add cross-module verification and do not replace the module
tests in .github/workflows/test.yml. Platform E2E runs only when Backend,
Frontend, Executor, Executor Manager, Shared, Chat Shell, Chat Core, Docker, or
related build configuration changes, so a Wework-only PR no longer starts an
unrelated platform E2E suite. Draft PRs skip expensive E2E and run it after they
become ready for review.
The full platform E2E suite runs daily at 02:00 UTC, and the full Wework E2E suite runs daily at 04:00 UTC. Scheduled runs use a different concurrency group from PR and merge-queue runs, so they do not cancel each other.
Coverage Reportsβ
Coverage reports are automatically uploaded to Codecov (if configured).
Mocking Strategyβ
External APIsβ
- GitHub/GitLab/Gitee: Mock with
httpx-mockorpytest-mock - Anthropic/OpenAI: Mock SDK clients
- Redis: Use
fakeredisor mock
Databaseβ
- Test DB: SQLite in-memory database
- Isolation: Each test gets a fresh transaction
- Cleanup: Automatic rollback after each test
Dockerβ
- Mock
docker.from_env()and container operations
Best Practicesβ
Writing Testsβ
- One assertion per test: Each test should verify one specific behavior
- Descriptive names: Use clear, descriptive test function names that explain what is being tested
- AAA pattern: Arrange, Act, Assert - structure your tests clearly
- Mock external dependencies: Never call real external services (APIs, databases, etc.)
- Use fixtures: Share common test setup via fixtures to reduce duplication
- Test edge cases: Include tests for error conditions, boundary values, and unusual inputs
- Keep tests independent: Each test should be able to run independently without relying on other tests
Security Testing Best Practicesβ
The project includes comprehensive security testing examples in backend/tests/core/test_security.py:
- Password hashing and verification (bcrypt)
- JWT token creation and validation
- Token expiration handling
- User authentication with valid/invalid credentials
- Inactive user detection
- Role-based access control (admin vs regular users)
Example test pattern for security features:
@pytest.mark.unit
class TestPasswordHashing:
"""Test password hashing and verification functions"""
def test_verify_password_with_correct_password(self):
"""Test password verification with correct password"""
password = "testpassword123"
hashed = get_password_hash(password)
assert verify_password(password, hashed) is True
def test_verify_password_with_incorrect_password(self):
"""Test password verification with incorrect password"""
password = "testpassword123"
hashed = get_password_hash(password)
assert verify_password("wrongpassword", hashed) is False
Test Organizationβ
@pytest.mark.unit
class TestFeatureName:
"""Test feature description"""
def test_success_case(self):
"""Test successful operation"""
# Arrange
data = {"key": "value"}
# Act
result = function_under_test(data)
# Assert
assert result == expected_value
def test_error_case(self):
"""Test error handling"""
with pytest.raises(ExpectedException):
function_under_test(invalid_data)
Using Test Markersβ
Test markers help categorize and selectively run tests:
# Run only unit tests
pytest -m unit
# Run only integration tests
pytest -m integration
# Run slow tests
pytest -m slow
# Skip slow tests
pytest -m "not slow"
Async Testsβ
@pytest.mark.asyncio
async def test_async_function():
"""Test asynchronous function"""
result = await async_function()
assert result is not None
The backend's pytest.ini has asyncio_mode = auto which automatically detects and runs async tests.
Adding New Testsβ
Backendβ
- Create test file in appropriate
tests/subdirectory (e.g.,tests/services/test_new_service.py) - Import necessary fixtures from
conftest.py - Use
@pytest.mark.unitor@pytest.mark.integrationto categorize tests - Follow the AAA (Arrange-Act-Assert) pattern
- Write test classes and methods with descriptive names
- Run tests locally before committing:
pytest tests/ -v - Ensure coverage is maintained or improved:
pytest --cov=app --cov-report=term-missing
Frontendβ
- Create test file in
src/__tests__/matching source structure - Use
@testing-library/reactfor component tests - Mock API calls and external dependencies
- Ensure tests pass with
pnpm --filter wecode-ai-assistant test
Debugging Testsβ
Backendβ
# Run specific test with verbose output
pytest tests/core/test_security.py::TestPasswordHashing::test_verify_password_with_correct_password -v
# Drop into debugger on failure
pytest --pdb
# Show print statements
pytest -s
Frontendβ
# Run tests in watch mode
pnpm --dir frontend run test:watch
# Debug specific test file
pnpm --dir frontend test -- src/__tests__/utils/test_example.test.ts
Configuration Filesβ
Backendβ
backend/pytest.ini: pytest configuration with coverage settings and test markers- Enables verbose output, strict markers, and automatic async mode
- Configures coverage reports in terminal, HTML, and XML formats
- Defines custom markers:
unit,integration,slow
Executor/Executor Manager/Sharedβ
pytest.ini: Module-specific pytest configuration- Similar setup to backend but with module-specific coverage targets
Frontendβ
frontend/jest.config.ts: Jest configurationfrontend/jest.setup.js: Test environment setup
Future Improvementsβ
- Increase coverage to 70-80%
- Add integration tests for API endpoints (currently placeholder)
- Add E2E tests for critical user flows
- Performance/load testing
- Mutation testing with
mutmut - Add more frontend component tests
- Implement database migration tests
- Add tests for WebSocket connections and real-time features
Troubleshootingβ
Common Issuesβ
Import errors in tests:
- Ensure you're running pytest from the correct directory
- Check that modules are installed:
uv sync
Database errors:
- Tests use SQLite in-memory DB, no setup needed
- Check that fixtures are imported correctly
Frontend test failures:
- Ensure Node.js 20+ is installed
- Run
pnpm install --frozen-lockfilefrom the repository root to install exact dependency versions - Clear Jest cache:
pnpm --dir frontend exec jest --clearCache