Skip to content
Open
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
754 changes: 751 additions & 3 deletions bun.lock

Large diffs are not rendered by default.

21 changes: 20 additions & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,26 @@ export default tseslint.config(
files: ['**/*.test.ts', '**/__tests__/**/*.ts'],
languageOptions: {
parserOptions: {
project: ['./tsconfig.json', './packages/*/tsconfig.test.json'],
project: ['./tsconfig.json', './packages/*/tsconfig.test.json', './packages/e2e/tsconfig.json'],
},
},
},

// Override for E2E package - allow console for setup and mock server
{
files: ['packages/e2e/**/*.ts'],
rules: {
'no-console': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
},

// Override for jest.config.js files
{
files: ['**/jest.config.js'],
languageOptions: {
parserOptions: {
project: false,
},
},
}
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@
"test:ci": "bun test test/unit services/proxy/tests tests/unit",
"test:unit": "bun test test/unit",
"test:integration": "bun test test/integration",
"test:e2e": "bun test test/e2e",
"test:e2e": "cd packages/e2e && bun run test",
"test:e2e:watch": "cd packages/e2e && bun run test:watch",
"test:css": "bun test test/unit/css-validation.test.ts",
"test:coverage": "bun test --coverage",
"test:watch": "bun test --watch",
Expand All @@ -53,6 +54,7 @@
"db:backup:file": "bun run scripts/db/backup-database.ts --file",
"db:migrate:token-usage": "bun run scripts/migrate-token-usage.ts",
"db:copy-conversation": "bun run scripts/copy-conversation.ts",
"db:export-conversation": "bun run scripts/export-conversation.ts",
"auth:generate-key": "bun run scripts/auth/generate-api-key.ts",
"auth:oauth-status": "bun run scripts/auth/check-oauth-status.ts",
"auth:oauth-refresh": "bun run scripts/auth/oauth-refresh-all.ts",
Expand Down
147 changes: 147 additions & 0 deletions packages/e2e/README-E2E.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# E2E Test Framework for Claude Nexus Proxy

This package provides an end-to-end testing framework for testing conversation tracking through the actual proxy.

## Overview

The E2E test framework:

- Runs real requests through the proxy (not mocks)
- Uses a mock Claude API server to avoid hitting the real API
- Validates conversation tracking in the database
- Supports dynamic value substitution with keywords
- Can import real conversations as test fixtures

## Architecture

```
E2E Test Flow:
1. PostgreSQL Container (test database)
2. Mock Claude API Server (returns canned responses)
3. Proxy Server (configured to use mock API)
4. Test Runner (sends requests and validates database state)
```

## Features

### Dynamic Value Substitution

Tests can use keywords for expected values:

- `$new` - Expect a new UUID
- `$same` - Same as previous request
- `$different` - Different from previous request
- `$previous` - Reference to previous request ID
- `$null` - Expect null value
- `$main` - Main branch
- `$branch_*` - Any branch starting with prefix

### Test Case Structure

```typescript
{
description: "Test description",
variables: {
// Define variables for reuse
conv_id: "uuid"
},
requests: [
{
domain: "test.example.com",
body: {
model: "claude-3-sonnet-20240229",
messages: [...]
},
expectDatabase: {
conversationId: "$new",
branchId: "$main",
messageCount: 1
}
}
]
}
```

## Running Tests

### Manual Testing

1. Start PostgreSQL:

```bash
docker run -d --name e2e-postgres \
-e POSTGRES_USER=test_user \
-e POSTGRES_PASSWORD=test_pass \
-e POSTGRES_DB=claude_nexus_test \
-p 5433:5432 \
postgres:16-alpine
```

2. Initialize database:

```bash
PGPASSWORD=test_pass psql -h localhost -p 5433 -U test_user \
-d claude_nexus_test -f ../../scripts/init-database.sql
```

3. Start mock Claude API:

```bash
cd packages/e2e
bun run src/setup/run-mock-claude.ts
```

4. Start proxy:

```bash
DATABASE_URL=postgresql://test_user:test_pass@localhost:5433/claude_nexus_test \
PORT=3100 \
STORAGE_ENABLED=true \
ENABLE_CLIENT_AUTH=false \
CLAUDE_API_URL=http://localhost:3101/mock-claude \
bun run services/proxy/src/main.ts
```

5. Run tests:

```bash
cd packages/e2e
DATABASE_URL=postgresql://test_user:test_pass@localhost:5433/claude_nexus_test \
PROXY_URL=http://localhost:3100 \
bun test src/__tests__/conversation-tracking.simple.test.ts
```

### Automated Testing (WIP)

The framework includes Jest configuration with global setup/teardown, but currently has issues with TypeScript module resolution. A simpler Bun test approach is recommended.

## Export Script

To export real conversations as test fixtures:

```bash
bun run scripts/export-conversation.ts \
--conversation-id <uuid> \
--output packages/e2e/src/fixtures/my-test.json
```

## Test Files

- `src/types/test-case.ts` - TypeScript types for test cases
- `src/utils/test-runner.ts` - Core test execution logic
- `src/setup/mock-claude.ts` - Mock Claude API server
- `src/__tests__/conversation-tracking.simple.test.ts` - Simple test examples

## Known Issues

1. Jest has issues with TypeScript ES modules in global setup
2. Testcontainers can be slow to start PostgreSQL
3. The proxy needs time to write to the database (500ms delay added)

## Future Improvements

1. Replace fixed delays with proper event-based synchronization
2. Add comprehensive error handling
3. Support for streaming responses
4. More sophisticated mock Claude responses
5. Better integration with CI/CD
121 changes: 121 additions & 0 deletions packages/e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# E2E Tests for Claude Nexus Proxy

This package contains end-to-end tests for the Claude Nexus Proxy, focusing on conversation tracking and database state validation.

## Architecture

The E2E tests use:

- **Testcontainers** to spin up a real PostgreSQL instance
- **Jest** as the test runner with custom setup/teardown
- **JSON fixtures** for declarative test cases
- **Dynamic value substitution** for handling UUIDs and relationships

## Running Tests

```bash
# From root directory
bun run test:e2e

# Watch mode
bun run test:e2e:watch

# From e2e package directory
cd packages/e2e
bun test
```

## Test Case Format

Test cases are defined in JSON with the following structure:

```json
{
"description": "Test description",
"variables": {
"varName": "uuid" | "timestamp" | "string"
},
"requests": [
{
"domain": "test.example.com",
"body": { /* Claude API request body */ },
"expectDatabase": {
"conversationId": "$new" | "$same" | "$different",
"branchId": "$main" | "$branch_*" | "$compact_*",
"parentRequestId": "$null" | "$previous" | "$request:N",
// ... other expectations
}
}
]
}
```

### Keywords

- `$new` - Expect a new unique value
- `$same` - Expect same value as previous request
- `$different` - Expect different value from previous
- `$null` - Expect null value
- `$previous` - Reference to previous request ID
- `$request:N` - Reference to specific request index
- `$any` - Any non-null value
- `$main` - Main branch
- `$branch_*` - Branch pattern matching
- `$compact_*` - Compact conversation branch

## Exporting Conversations

You can export real conversations from the database as test fixtures:

```bash
# Export a full conversation by ID
bun run db:export-conversation <conversation-id>

# Export specific requests
bun run db:export-conversation <request-id-1> <request-id-2> ...

# Export to specific file
bun run db:export-conversation <conversation-id> --output=my-test.json
```

## Creating New Tests

1. Create a new JSON fixture in `src/fixtures/`
2. Define the test case following the format above
3. Run the tests to validate

Example fixture:

```json
{
"description": "Test branching conversation",
"requests": [
{
"domain": "test.example.com",
"body": {
"model": "claude-3-sonnet-20240229",
"messages": [{ "role": "user", "content": "Hello" }]
},
"expectDatabase": {
"conversationId": "$new",
"branchId": "$main"
}
}
]
}
```

## Test Isolation

Each test case:

- Gets a fresh database state (no cleanup between requests in a test case)
- Runs against the same proxy instance
- Uses unique domains to avoid conflicts

## Debugging

- Check `packages/e2e/jest.config.js` for test timeout settings
- Use `console.log` in test files for debugging
- Database queries wait 500ms for async storage to complete
- Proxy logs are captured but not displayed by default
2 changes: 2 additions & 0 deletions packages/e2e/bunfig.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[test]
preload = ["./src/setup/setup.ts"]
26 changes: 26 additions & 0 deletions packages/e2e/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/** @type {import('jest').Config} */
export default {
preset: 'ts-jest/presets/default-esm',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/__tests__/**/*.test.ts'],
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
},
transform: {
'^.+\\.tsx?$': [
'ts-jest',
{
useESM: true,
tsconfig: {
moduleResolution: 'NodeNext',
module: 'NodeNext',
},
},
],
},
extensionsToTreatAsEsm: ['.ts'],
globalSetup: '<rootDir>/src/setup/global-setup.ts',
globalTeardown: '<rootDir>/src/setup/global-teardown.ts',
testTimeout: 30000,
}
44 changes: 44 additions & 0 deletions packages/e2e/manual-test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/bin/bash

echo "🧪 Manual E2E Test"
echo "==================="
echo ""
echo "Prerequisites:"
echo "1. Start PostgreSQL: docker run -d --name e2e-postgres -e POSTGRES_USER=test_user -e POSTGRES_PASSWORD=test_pass -e POSTGRES_DB=claude_nexus_test -p 5433:5432 postgres:16-alpine"
echo "2. Init database: PGPASSWORD=test_pass psql -h localhost -p 5433 -U test_user -d claude_nexus_test -f ../../scripts/init-database.sql"
echo "3. Start mock Claude: cd packages/e2e && bun run src/setup/run-mock-claude.ts"
echo "4. Start proxy: DATABASE_URL=postgresql://test_user:test_pass@localhost:5433/claude_nexus_test PORT=3100 STORAGE_ENABLED=true ENABLE_CLIENT_AUTH=false CLAUDE_API_URL=http://localhost:3101/mock-claude bun run services/proxy/src/main.ts"
echo ""
echo "Running test requests..."

# Test 1: Single message
echo "Test 1: Single message creates new conversation"
curl -X POST http://localhost:3100/v1/messages \
-H "Content-Type: application/json" \
-H "Host: test.example.com" \
-H "Authorization: Bearer cnp_test_e2e_key" \
-d '{
"model": "claude-3-sonnet-20240229",
"messages": [{"role": "user", "content": "Hello, Claude!"}],
"max_tokens": 10
}' | jq .

sleep 1

echo ""
echo "Checking database..."
PGPASSWORD=test_pass psql -h localhost -p 5433 -U test_user -d claude_nexus_test -c "
SELECT
request_id,
conversation_id,
branch_id,
parent_request_id,
message_count
FROM api_requests
WHERE domain = 'test.example.com'
ORDER BY timestamp DESC
LIMIT 1;
"

echo ""
echo "✅ If you see a conversation_id, branch_id='main', and message_count=1, the test passed!"
Loading
Loading