12 KiB
Contributing to @lumbridgecorp/openclaw-mattermost
Thank you for your interest in contributing! This document provides guidelines for setting up your development environment, running tests, and submitting contributions.
Table of Contents
- Development Setup
- Running Tests
- Test Structure
- Writing Tests
- Code Style
- Pull Request Process
- Release Process
Development Setup
Prerequisites
- Node.js 22+ (required for native fetch and WebSocket support)
- pnpm (preferred) or npm/yarn
- Git
- Mattermost server (for integration tests)
Local Setup
-
Fork the repository on GitHub
-
Clone your fork:
git clone https://github.com/YOUR_USERNAME/openclaw-extentions-mattermost.git cd openclaw-extentions-mattermost -
Install dependencies:
pnpm install # or npm install -
Build the project:
pnpm build # or npm run build -
Verify the build:
pnpm typecheck
Running Tests
Quick Test Commands
# Run all tests
pnpm test
# Run tests in watch mode (for development)
pnpm test -- --watch
# Run tests with coverage
pnpm test -- --coverage
# Run a specific test file
pnpm test src/mattermost/client.test.ts
# Run tests matching a pattern
pnpm test -- -t "should create client"
# Run tests with verbose output
pnpm test -- --verbose
Test Categories
Unit Tests
Fast, isolated tests that don't require external services:
# Run only unit tests (no integration)
pnpm test -- --testPathIgnorePatterns="integration|e2e"
Unit tests cover:
- Error handling classes
- Configuration validation
- Utility functions
- Retry logic
- Security checks
Integration Tests with Docker Compose
Integration tests use Docker Compose to spin up a complete Mattermost instance with PostgreSQL. This provides isolated, reproducible tests against a real Mattermost API.
Setup and Run Integration Tests:
# Start the test infrastructure
docker-compose -f tests/integration/docker-compose.test.yml up -d
# Wait for Mattermost to be ready (first startup may take 1-2 minutes)
docker-compose -f tests/integration/docker-compose.test.yml ps
# Run integration tests
pnpm test tests/integration/
# Run specific integration test file
pnpm test tests/integration/authentication.test.ts
# Run integration tests with verbose output
pnpm test tests/integration/ --verbose
# Clean up after tests
docker-compose -f tests/integration/docker-compose.test.yml down -v
Integration Test Coverage:
| Test File | Coverage |
|---|---|
authentication.test.ts |
Bot token auth, PAT auth, error scenarios |
send-message.test.ts |
Send to channel/DM, threads, props, error cases |
edit-message.test.ts |
Edit own messages, props, permission errors |
delete-message.test.ts |
Delete messages, thread handling, cleanup |
download-file.test.ts |
File download, security restrictions, MIME validation |
interactive-buttons.test.ts |
Button rendering, directives, end-to-end flows |
Environment Variables for Integration Tests:
# Optional: Override defaults (only if not using Docker Compose)
export MATTERMOST_URL="http://localhost:8065"
export MATTERMOST_SYSADMIN_USER="sysadmin"
export MATTERMOST_SYSADMIN_PASSWORD="Sys@dmin123"
export MATTERMOST_TEST_USER="testuser"
export MATTERMOST_TEST_USER_PASSWORD="Test@user123"
export MATTERMOST_BOT_USERNAME="testbot"
Test Data Cleanup:
Integration tests automatically clean up:
- Created posts after each test suite
- Uploaded files
- Test channels and teams (via Docker volume reset)
Run docker-compose -f tests/integration/docker-compose.test.yml down -v to completely reset the test environment between runs.
Using a Custom Mattermost Instance:
If you prefer to use an existing Mattermost instance for integration tests:
# Set up environment variables
export MATTERMOST_URL="https://chat-test.example.com"
export MATTERMOST_SYSADMIN_TOKEN="your-sysadmin-token"
export MATTERMOST_BOT_TOKEN="your-bot-token"
# Run integration tests (skip Docker setup)
export SKIP_DOCKER_SETUP=true
pnpm test tests/integration/
⚠️ Important: Never run integration tests against production Mattermost instances. Tests create and delete data automatically.
Client Tests
Tests for the Mattermost API client:
pnpm test src/mattermost/client.test.ts
pnpm test src/mattermost/client.retry.test.ts
WebSocket Tests
Tests for real-time message monitoring:
pnpm test src/mattermost/monitor-websocket.test.ts
Test Environment Setup
Using Environment Variables
Create a .env.test file (not committed to git):
# Test Mattermost instance
MATTERMOST_TEST_URL=https://chat-test.example.com
MATTERMOST_TEST_TOKEN=your-test-bot-token
MATTERMOST_TEST_PAT=your-test-pat-optional
# Test configuration
TEST_TIMEOUT=30000
TEST_RETRY_COUNT=3
Using Test Fixtures
Some tests use mock fixtures instead of real connections:
// Example: Using mock client
import { createMockMattermostClient } from './test-helpers.js';
const mockClient = createMockMattermostClient({
baseUrl: 'https://mock.example.com',
botToken: 'mock-token'
});
Coverage Reports
Generate and view coverage:
# Generate coverage report
pnpm test -- --coverage
# View HTML report
open coverage/lcov-report/index.html
# Coverage thresholds (enforced in CI)
# Statements: 80%
# Branches: 75%
# Functions: 80%
# Lines: 80%
Test Structure
Test File Organization
extensions/mattermost/src/
├── mattermost/
│ ├── client.test.ts # Client unit tests
│ ├── client.retry.test.ts # Retry logic tests
│ ├── monitor-websocket.test.ts # WebSocket tests
│ ├── interactions.test.ts # Interactive components tests
│ └── ...
├── config-schema.test.ts # Configuration validation
├── security-audit.test.ts # Security audit tests
├── errors.test.ts # Error handling tests
└── ...
Test Naming Conventions
- File naming:
*.test.tsfor unit tests,*.integration.test.tsfor integration tests - Describe blocks: Use the name of the function/module being tested
- Test names: Start with "should" and describe the expected behavior
Example:
describe('editMessage', () => {
it('should successfully edit a bot message', async () => {
// test code
});
it('should fail when editing another users message', async () => {
// test code
});
it('should return PERMISSION_DENIED for system messages', async () => {
// test code
});
});
Writing Tests
Unit Test Template
import { describe, it, expect, vi } from 'vitest';
import { editMessage } from './actions.js';
describe('editMessage', () => {
it('should edit a message successfully', async () => {
// Arrange
const mockClient = createMockClient();
const params = {
cfg: mockConfig,
postId: 'post123',
channelId: 'channel123',
message: 'Updated'
};
// Act
const result = await editMessage(params);
// Assert
expect(result.ok).toBe(true);
expect(result.postId).toBe('post123');
});
it('should handle errors gracefully', async () => {
// Arrange
const params = { /* invalid params */ };
// Act
const result = await editMessage(params);
// Assert
expect(result.ok).toBe(false);
expect(result.errorCode).toBe('CONFIG_ERROR');
});
});
Testing Error Handling
import { MattermostError, ErrorCodes } from './errors.js';
describe('error handling', () => {
it('should create structured error with context', () => {
const error = new MattermostError({
code: ErrorCodes.AUTHENTICATION_FAILED,
message: 'Invalid token',
context: {
operation: 'test',
timestamp: new Date().toISOString()
}
});
expect(error.code).toBe(ErrorCodes.AUTHENTICATION_FAILED);
expect(error.retryable).toBe(false);
expect(error.toJSON()).toMatchObject({
code: ErrorCodes.AUTHENTICATION_FAILED
});
});
});
Testing Retries
import { withRetry } from './errors.js';
describe('withRetry', () => {
it('should retry on retryable errors', async () => {
let attempts = 0;
const operation = async () => {
attempts++;
if (attempts < 3) {
throw new NetworkError('Connection failed', context);
}
return 'success';
};
const result = await withRetry('test-op', operation, {
maxRetries: 3,
initialDelayMs: 10
});
expect(result).toBe('success');
expect(attempts).toBe(3);
});
});
Testing Security Features
import { collectMattermostSecurityAuditFindings } from './security-audit.js';
describe('security audit', () => {
it('should detect hardcoded tokens', async () => {
const findings = await collectMattermostSecurityAuditFindings({
cfg: mockConfig,
accountId: 'test',
account: {
...mockAccount,
botToken: 'hardcoded-token-123456789',
botTokenSource: 'config'
}
});
const hardcodedFinding = findings.find(
f => f.checkId === 'channels.mattermost.pat.hardcoded_token'
);
expect(hardcodedFinding).toBeDefined();
expect(hardcodedFinding?.severity).toBe('critical');
});
});
Mocking the Mattermost Client
import { vi } from 'vitest';
// Create mock client
const createMockClient = () => ({
request: vi.fn(),
fetchImpl: vi.fn(),
baseUrl: 'https://mock.example.com',
apiBaseUrl: 'https://mock.example.com/api/v4',
token: 'mock-token'
});
// Mock module
vi.mock('./client.js', () => ({
createMattermostClient: vi.fn(() => createMockClient())
}));
Code Style
TypeScript Guidelines
- Use strict TypeScript (
strict: truein tsconfig) - Explicit return types on public functions
- Avoid
any- useunknownwith type guards - Use
readonlyfor immutable properties
Code Formatting
# Check code style
pnpm lint
# Fix auto-fixable issues
pnpm lint --fix
Import Order
- External dependencies (e.g.,
openclaw/plugin-sdk) - Internal modules (relative imports)
- Types only imports
Example:
// 1. External
import { z } from 'openclaw/plugin-sdk/zod';
// 2. Internal
import { createMattermostClient } from './client.js';
import type { MattermostAccountConfig } from './types.js';
Pull Request Process
Before Submitting
-
Run all tests:
pnpm test -
Check type safety:
pnpm typecheck -
Run linter:
pnpm lint -
Build project:
pnpm build -
Check test coverage:
pnpm test -- --coverage
PR Requirements
- Clear description of changes
- Link to related issues
- Tests included for new features
- Documentation updated if needed
- No breaking changes (or clearly marked)
- CI checks passing
PR Template
## Description
Brief description of the change
## Type of Change
- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation update
## Testing
- [ ] Unit tests added/updated
- [ ] Integration tests added/updated
- [ ] All tests passing
## Checklist
- [ ] Code follows style guidelines
- [ ] Self-review completed
- [ ] Documentation updated
- [ ] No new warnings
Review Process
- Automated checks must pass (CI, coverage)
- At least one maintainer review required
- Address review feedback promptly
- Squash commits before merge (if requested)
Release Process
Version Bump
# Update version in package.json
npm version patch # or minor, major
Changelog Update
Add entry to CHANGELOG.md following Keep a Changelog format:
## [1.1.0] - 2026-04-15
### Added
- New feature description
### Fixed
- Bug fix description
Release Steps
- Update version in
package.json - Update
CHANGELOG.md - Create PR with version bump
- After merge, tag the release:
git tag -a v1.1.0 -m "Release v1.1.0" git push origin v1.1.0 - CI will automatically publish to npm
Questions?
- Open an issue on GitHub
- Check existing issues and discussions
- Review the API Reference
Thank you for contributing!