feat(#37-41): Add domains, ideas, relationships, agents, widgets schema

Schema additions for issues #37-41:

New models:
- Domain (#37): Life domains (work, marriage, homelab, etc.)
- Idea (#38): Brain dumps with pgvector embeddings
- Relationship (#39): Generic entity linking (blocks, depends_on)
- Agent (#40): ClawdBot agent tracking with metrics
- AgentSession (#40): Conversation session tracking
- WidgetDefinition (#41): HUD widget registry
- UserLayout (#41): Per-user dashboard configuration

Updated models:
- Task, Event, Project: Added domainId foreign key
- User, Workspace: Added new relations

New enums:
- IdeaStatus: CAPTURED, PROCESSING, ACTIONABLE, ARCHIVED, DISCARDED
- RelationshipType: BLOCKS, BLOCKED_BY, DEPENDS_ON, etc.
- AgentStatus: IDLE, WORKING, WAITING, ERROR, TERMINATED
- EntityType: Added IDEA, DOMAIN

Migration: 20260129182803_add_domains_ideas_agents_widgets
This commit is contained in:
Jason Woltje
2026-01-29 12:29:21 -06:00
parent a220c2dc0a
commit 973502f26e
308 changed files with 18374 additions and 113 deletions

View File

@@ -0,0 +1,509 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
/**
* Docker Stack Integration Tests
* Tests the full Docker Compose stack deployment
*/
describe('Docker Stack Integration Tests', () => {
const TIMEOUT = 120000; // 2 minutes for Docker operations
const HEALTH_CHECK_RETRIES = 30;
const HEALTH_CHECK_INTERVAL = 2000;
/**
* Helper function to execute Docker Compose commands
*/
async function dockerCompose(command: string): Promise<string> {
const { stdout } = await execAsync(`docker compose ${command}`, {
cwd: process.cwd(),
});
return stdout;
}
/**
* Helper function to check if a service is healthy
*/
async function waitForService(
serviceName: string,
retries = HEALTH_CHECK_RETRIES,
): Promise<boolean> {
for (let i = 0; i < retries; i++) {
try {
const { stdout } = await execAsync(
`docker compose ps --format json ${serviceName}`,
);
const serviceInfo = JSON.parse(stdout);
if (
serviceInfo.Health === 'healthy' ||
serviceInfo.State === 'running'
) {
return true;
}
} catch (error) {
// Service not ready yet
}
await new Promise((resolve) => setTimeout(resolve, HEALTH_CHECK_INTERVAL));
}
return false;
}
/**
* Helper function to check HTTP endpoint
*/
async function checkHttpEndpoint(url: string): Promise<boolean> {
try {
const response = await fetch(url);
return response.ok;
} catch (error) {
return false;
}
}
describe('Core Services', () => {
beforeAll(async () => {
// Ensure clean state
await dockerCompose('down -v').catch(() => {});
}, TIMEOUT);
afterAll(async () => {
// Cleanup
await dockerCompose('down -v').catch(() => {});
}, TIMEOUT);
it(
'should start PostgreSQL with health check',
async () => {
// Start only PostgreSQL
await dockerCompose('up -d postgres');
// Wait for service to be healthy
const isHealthy = await waitForService('postgres');
expect(isHealthy).toBe(true);
// Verify container is running
const ps = await dockerCompose('ps postgres');
expect(ps).toContain('mosaic-postgres');
expect(ps).toContain('Up');
},
TIMEOUT,
);
it(
'should start Valkey with health check',
async () => {
// Start Valkey
await dockerCompose('up -d valkey');
// Wait for service to be healthy
const isHealthy = await waitForService('valkey');
expect(isHealthy).toBe(true);
// Verify container is running
const ps = await dockerCompose('ps valkey');
expect(ps).toContain('mosaic-valkey');
expect(ps).toContain('Up');
},
TIMEOUT,
);
it(
'should have proper network configuration',
async () => {
// Check if mosaic-internal network exists
const { stdout } = await execAsync('docker network ls');
expect(stdout).toContain('mosaic-internal');
expect(stdout).toContain('mosaic-public');
},
TIMEOUT,
);
it(
'should have proper volume configuration',
async () => {
// Check if volumes are created
const { stdout } = await execAsync('docker volume ls');
expect(stdout).toContain('mosaic-postgres-data');
expect(stdout).toContain('mosaic-valkey-data');
},
TIMEOUT,
);
});
describe('Application Services', () => {
beforeAll(async () => {
// Start core services first
await dockerCompose('up -d postgres valkey');
await waitForService('postgres');
await waitForService('valkey');
}, TIMEOUT);
afterAll(async () => {
await dockerCompose('down -v').catch(() => {});
}, TIMEOUT);
it(
'should start API service with dependencies',
async () => {
// Start API
await dockerCompose('up -d api');
// Wait for API to be healthy
const isHealthy = await waitForService('api');
expect(isHealthy).toBe(true);
// Verify API is accessible
const apiHealthy = await checkHttpEndpoint('http://localhost:3001/health');
expect(apiHealthy).toBe(true);
},
TIMEOUT,
);
it(
'should start Web service with dependencies',
async () => {
// Ensure API is running
await dockerCompose('up -d api');
await waitForService('api');
// Start Web
await dockerCompose('up -d web');
// Wait for Web to be healthy
const isHealthy = await waitForService('web');
expect(isHealthy).toBe(true);
// Verify Web is accessible
const webHealthy = await checkHttpEndpoint('http://localhost:3000');
expect(webHealthy).toBe(true);
},
TIMEOUT,
);
});
describe('Optional Services (Profiles)', () => {
afterAll(async () => {
await dockerCompose('down -v').catch(() => {});
}, TIMEOUT);
it(
'should start Authentik services with profile',
async () => {
// Start Authentik services using profile
await dockerCompose('--profile authentik up -d');
// Wait for Authentik dependencies
await waitForService('authentik-postgres');
await waitForService('authentik-redis');
// Verify Authentik server starts
const isHealthy = await waitForService('authentik-server');
expect(isHealthy).toBe(true);
// Verify Authentik is accessible
const authentikHealthy = await checkHttpEndpoint(
'http://localhost:9000/-/health/live/',
);
expect(authentikHealthy).toBe(true);
},
TIMEOUT,
);
it(
'should start Ollama service with profile',
async () => {
// Start Ollama using profile
await dockerCompose('--profile ollama up -d ollama');
// Wait for Ollama to start
const isHealthy = await waitForService('ollama');
expect(isHealthy).toBe(true);
// Verify Ollama is accessible
const ollamaHealthy = await checkHttpEndpoint(
'http://localhost:11434/api/tags',
);
expect(ollamaHealthy).toBe(true);
},
TIMEOUT,
);
});
describe('Full Stack', () => {
it(
'should start entire stack with all services',
async () => {
// Clean slate
await dockerCompose('down -v').catch(() => {});
// Start all core services (without profiles)
await dockerCompose('up -d');
// Wait for all core services
const postgresHealthy = await waitForService('postgres');
const valkeyHealthy = await waitForService('valkey');
const apiHealthy = await waitForService('api');
const webHealthy = await waitForService('web');
expect(postgresHealthy).toBe(true);
expect(valkeyHealthy).toBe(true);
expect(apiHealthy).toBe(true);
expect(webHealthy).toBe(true);
// Verify all services are running
const ps = await dockerCompose('ps');
expect(ps).toContain('mosaic-postgres');
expect(ps).toContain('mosaic-valkey');
expect(ps).toContain('mosaic-api');
expect(ps).toContain('mosaic-web');
},
TIMEOUT * 2,
);
});
describe('Service Dependencies', () => {
beforeAll(async () => {
await dockerCompose('down -v').catch(() => {});
}, TIMEOUT);
afterAll(async () => {
await dockerCompose('down -v').catch(() => {});
}, TIMEOUT);
it(
'should enforce dependency order',
async () => {
// Start web service (should auto-start dependencies)
await dockerCompose('up -d web');
// Wait a bit for services to start
await new Promise((resolve) => setTimeout(resolve, 10000));
// Verify all dependencies are running
const ps = await dockerCompose('ps');
expect(ps).toContain('mosaic-postgres');
expect(ps).toContain('mosaic-valkey');
expect(ps).toContain('mosaic-api');
expect(ps).toContain('mosaic-web');
},
TIMEOUT,
);
});
describe('Container Labels', () => {
beforeAll(async () => {
await dockerCompose('up -d postgres valkey');
}, TIMEOUT);
afterAll(async () => {
await dockerCompose('down -v').catch(() => {});
}, TIMEOUT);
it('should have proper labels on containers', async () => {
// Check PostgreSQL labels
const { stdout: pgLabels } = await execAsync(
'docker inspect mosaic-postgres --format "{{json .Config.Labels}}"',
);
const pgLabelsObj = JSON.parse(pgLabels);
expect(pgLabelsObj['com.mosaic.service']).toBe('database');
expect(pgLabelsObj['com.mosaic.description']).toBeDefined();
// Check Valkey labels
const { stdout: valkeyLabels } = await execAsync(
'docker inspect mosaic-valkey --format "{{json .Config.Labels}}"',
);
const valkeyLabelsObj = JSON.parse(valkeyLabels);
expect(valkeyLabelsObj['com.mosaic.service']).toBe('cache');
expect(valkeyLabelsObj['com.mosaic.description']).toBeDefined();
});
describe('Failure Scenarios', () => {
const TIMEOUT = 120000;
beforeAll(async () => {
await dockerCompose('down -v').catch(() => {});
}, TIMEOUT);
afterAll(async () => {
await dockerCompose('down -v').catch(() => {});
}, TIMEOUT);
it(
'should handle service restart after crash',
async () => {
await dockerCompose('up -d postgres');
await waitForService('postgres');
const { stdout: containerName } = await execAsync(
'docker compose ps -q postgres',
);
const trimmedName = containerName.trim();
await execAsync(`docker kill ${trimmedName}`);
await new Promise((resolve) => setTimeout(resolve, 3000));
await dockerCompose('up -d postgres');
const isHealthy = await waitForService('postgres');
expect(isHealthy).toBe(true);
},
TIMEOUT,
);
it(
'should handle port conflict gracefully',
async () => {
try {
const { stdout } = await execAsync(
'docker run -d -p 5432:5432 --name port-blocker postgres:17-alpine',
);
await dockerCompose('up -d postgres').catch((error) => {
expect(error.message).toContain('port is already allocated');
});
} finally {
await execAsync('docker rm -f port-blocker').catch(() => {});
}
},
TIMEOUT,
);
it(
'should handle invalid volume mount paths',
async () => {
try {
await execAsync(
'docker run -d --name invalid-mount -v /nonexistent/path:/data postgres:17-alpine',
);
const { stdout } = await execAsync(
'docker ps -a -f name=invalid-mount --format "{{.Status}}"',
);
expect(stdout).toBeDefined();
} catch (error) {
expect(error).toBeDefined();
} finally {
await execAsync('docker rm -f invalid-mount').catch(() => {});
}
},
TIMEOUT,
);
it(
'should handle network partition scenarios',
async () => {
await dockerCompose('up -d postgres valkey');
await waitForService('postgres');
await waitForService('valkey');
const { stdout: postgresContainer } = await execAsync(
'docker compose ps -q postgres',
);
const trimmedPostgres = postgresContainer.trim();
await execAsync(
`docker network disconnect mosaic-internal ${trimmedPostgres}`,
);
await new Promise((resolve) => setTimeout(resolve, 2000));
await execAsync(
`docker network connect mosaic-internal ${trimmedPostgres}`,
);
const isHealthy = await waitForService('postgres');
expect(isHealthy).toBe(true);
},
TIMEOUT,
);
it(
'should handle container out of memory scenario',
async () => {
try {
const { stdout } = await execAsync(
'docker run -d --name mem-limited --memory="10m" postgres:17-alpine',
);
await new Promise((resolve) => setTimeout(resolve, 5000));
const { stdout: status } = await execAsync(
'docker inspect mem-limited --format "{{.State.Status}}"',
);
expect(['exited', 'running']).toContain(status.trim());
} finally {
await execAsync('docker rm -f mem-limited').catch(() => {});
}
},
TIMEOUT,
);
it(
'should handle disk space issues gracefully',
async () => {
await dockerCompose('up -d postgres');
await waitForService('postgres');
const { stdout } = await execAsync('docker system df');
expect(stdout).toContain('Images');
expect(stdout).toContain('Containers');
expect(stdout).toContain('Volumes');
},
TIMEOUT,
);
it(
'should handle service dependency failures',
async () => {
await dockerCompose('up -d postgres').catch(() => {});
const { stdout: postgresContainer } = await execAsync(
'docker compose ps -q postgres',
);
const trimmedPostgres = postgresContainer.trim();
if (trimmedPostgres) {
await execAsync(`docker stop ${trimmedPostgres}`);
try {
await dockerCompose('up -d api');
} catch (error) {
expect(error).toBeDefined();
}
await dockerCompose('start postgres').catch(() => {});
await waitForService('postgres');
await dockerCompose('up -d api');
const apiHealthy = await waitForService('api');
expect(apiHealthy).toBe(true);
}
},
TIMEOUT,
);
it(
'should recover from corrupted volume data',
async () => {
await dockerCompose('up -d postgres');
await waitForService('postgres');
await dockerCompose('down');
await execAsync('docker volume rm mosaic-postgres-data').catch(() => {});
await dockerCompose('up -d postgres');
const isHealthy = await waitForService('postgres');
expect(isHealthy).toBe(true);
},
TIMEOUT,
);
});
});

View File

@@ -0,0 +1,80 @@
# Docker Integration Tests
This directory contains integration tests for Docker Compose deployment modes.
## Traefik Integration Tests
Tests for Traefik reverse proxy integration in bundled, upstream, and none modes.
### Prerequisites
- Docker and Docker Compose installed
- `jq` for JSON parsing: `apt install jq` or `brew install jq`
- `curl` for HTTP testing
### Running Tests
```bash
# Run all tests
./traefik.test.sh all
# Run specific mode tests
./traefik.test.sh bundled
./traefik.test.sh upstream
./traefik.test.sh none
```
### Test Coverage
#### Bundled Mode Tests
- Traefik container starts with `traefik-bundled` profile
- Traefik dashboard is accessible
- Traefik API responds correctly
- Services have proper Traefik labels
- Routes are registered with Traefik
#### Upstream Mode Tests
- Bundled Traefik does not start
- Services connect to external Traefik network
- Services have labels for external discovery
- Correct network configuration
#### None Mode Tests
- No Traefik container starts
- Traefik labels are disabled
- Direct port access works
- Services accessible via published ports
### CI/CD Integration
These tests can be run in CI/CD pipelines:
```yaml
# Example GitHub Actions
test-docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install dependencies
run: sudo apt-get install -y jq
- name: Run Traefik integration tests
run: ./tests/integration/docker/traefik.test.sh all
```
### Troubleshooting
#### Test cleanup issues
If tests fail and leave containers running:
```bash
docker compose -f docker-compose.test.yml down -v
docker network rm traefik-public-test
```
#### Permission denied
Make sure the test script is executable:
```bash
chmod +x traefik.test.sh
```
#### Port conflicts
Ensure ports 8080, 3000, 3001 are available before running tests.

View File

@@ -0,0 +1,390 @@
#!/bin/bash
# Integration tests for Traefik deployment modes
# Tests bundled, upstream, and none modes
#
# Usage:
# ./traefik.test.sh [bundled|upstream|none|all]
#
# Requirements:
# - Docker and Docker Compose installed
# - jq for JSON parsing
# - curl for HTTP testing
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Test counters
TESTS_RUN=0
TESTS_PASSED=0
TESTS_FAILED=0
# Test result tracking
declare -a FAILED_TESTS=()
# Logging functions
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
log_test() {
echo -e "\n${YELLOW}[TEST]${NC} $1"
((TESTS_RUN++))
}
log_pass() {
echo -e "${GREEN}[PASS]${NC} $1"
((TESTS_PASSED++))
}
log_fail() {
echo -e "${RED}[FAIL]${NC} $1"
((TESTS_FAILED++))
FAILED_TESTS+=("$1")
}
# Cleanup function
cleanup() {
log_info "Cleaning up test environment..."
docker compose -f docker-compose.test.yml down -v 2>/dev/null || true
docker network rm traefik-public-test 2>/dev/null || true
rm -f .env.test docker-compose.test.yml
}
# Trap cleanup on exit
trap cleanup EXIT
# Check prerequisites
check_prerequisites() {
log_info "Checking prerequisites..."
if ! command -v docker &> /dev/null; then
log_error "Docker is not installed"
exit 1
fi
if ! command -v docker compose &> /dev/null; then
log_error "Docker Compose is not installed"
exit 1
fi
if ! command -v jq &> /dev/null; then
log_error "jq is not installed (required for JSON parsing)"
exit 1
fi
if ! command -v curl &> /dev/null; then
log_error "curl is not installed"
exit 1
fi
log_pass "All prerequisites satisfied"
}
# Test bundled Traefik mode
test_bundled_mode() {
log_info "=========================================="
log_info "Testing Bundled Traefik Mode"
log_info "=========================================="
# Create test environment file
cat > .env.test <<EOF
TRAEFIK_MODE=bundled
MOSAIC_API_DOMAIN=api.mosaic.test
MOSAIC_WEB_DOMAIN=mosaic.test
MOSAIC_AUTH_DOMAIN=auth.mosaic.test
TRAEFIK_DASHBOARD_ENABLED=true
TRAEFIK_TLS_ENABLED=false
TRAEFIK_ACME_EMAIL=test@example.com
POSTGRES_PASSWORD=test_password
AUTHENTIK_SECRET_KEY=test_secret_key_minimum_50_characters_long_for_testing
AUTHENTIK_POSTGRES_PASSWORD=test_auth_password
AUTHENTIK_BOOTSTRAP_PASSWORD=test_admin
JWT_SECRET=test_jwt_secret_minimum_32_chars
EOF
# Copy docker-compose.yml to test version
cp docker-compose.yml docker-compose.test.yml
log_test "Starting services with bundled Traefik profile"
if docker compose -f docker-compose.test.yml --env-file .env.test --profile traefik-bundled up -d; then
log_pass "Services started successfully"
else
log_fail "Failed to start services"
return 1
fi
sleep 10 # Wait for services to initialize
log_test "Verifying Traefik container is running"
if docker ps --filter "name=mosaic-traefik" --format "{{.Names}}" | grep -q "mosaic-traefik"; then
log_pass "Traefik container is running"
else
log_fail "Traefik container not found"
fi
log_test "Verifying Traefik dashboard is accessible"
if curl -sf http://localhost:8080/dashboard/ > /dev/null; then
log_pass "Traefik dashboard is accessible"
else
log_fail "Traefik dashboard not accessible"
fi
log_test "Verifying Traefik API endpoint"
if curl -sf http://localhost:8080/api/overview | jq -e '.http.routers' > /dev/null; then
log_pass "Traefik API is responding"
else
log_fail "Traefik API not responding correctly"
fi
log_test "Verifying API service has Traefik labels"
LABELS=$(docker inspect mosaic-api --format='{{json .Config.Labels}}')
if echo "$LABELS" | jq -e '."traefik.enable"' > /dev/null; then
log_pass "API service has Traefik labels"
else
log_fail "API service missing Traefik labels"
fi
log_test "Verifying Web service has Traefik labels"
LABELS=$(docker inspect mosaic-web --format='{{json .Config.Labels}}')
if echo "$LABELS" | jq -e '."traefik.enable"' > /dev/null; then
log_pass "Web service has Traefik labels"
else
log_fail "Web service missing Traefik labels"
fi
log_test "Checking Traefik routes configuration"
ROUTES=$(curl -sf http://localhost:8080/api/http/routers | jq -r 'keys[]')
if echo "$ROUTES" | grep -q "mosaic-api"; then
log_pass "API route registered with Traefik"
else
log_fail "API route not found in Traefik"
fi
if echo "$ROUTES" | grep -q "mosaic-web"; then
log_pass "Web route registered with Traefik"
else
log_fail "Web route not found in Traefik"
fi
# Cleanup
docker compose -f docker-compose.test.yml --env-file .env.test down -v
}
# Test upstream Traefik mode
test_upstream_mode() {
log_info "=========================================="
log_info "Testing Upstream Traefik Mode"
log_info "=========================================="
# Create external network for upstream Traefik
log_test "Creating external Traefik network"
if docker network create traefik-public-test 2>/dev/null; then
log_pass "External network created"
else
log_warn "Network already exists or creation failed"
fi
# Create test environment file
cat > .env.test <<EOF
TRAEFIK_MODE=upstream
MOSAIC_API_DOMAIN=api.mosaic.test
MOSAIC_WEB_DOMAIN=mosaic.test
MOSAIC_AUTH_DOMAIN=auth.mosaic.test
TRAEFIK_NETWORK=traefik-public-test
POSTGRES_PASSWORD=test_password
AUTHENTIK_SECRET_KEY=test_secret_key_minimum_50_characters_long_for_testing
AUTHENTIK_POSTGRES_PASSWORD=test_auth_password
AUTHENTIK_BOOTSTRAP_PASSWORD=test_admin
JWT_SECRET=test_jwt_secret_minimum_32_chars
EOF
# Copy docker-compose.yml to test version
cp docker-compose.yml docker-compose.test.yml
log_test "Starting services in upstream mode (no bundled Traefik)"
if docker compose -f docker-compose.test.yml --env-file .env.test up -d; then
log_pass "Services started successfully"
else
log_fail "Failed to start services"
return 1
fi
sleep 10 # Wait for services to initialize
log_test "Verifying Traefik container is NOT running"
if ! docker ps --filter "name=mosaic-traefik" --format "{{.Names}}" | grep -q "mosaic-traefik"; then
log_pass "Bundled Traefik correctly not started"
else
log_fail "Bundled Traefik should not be running in upstream mode"
fi
log_test "Verifying API service is connected to external network"
NETWORKS=$(docker inspect mosaic-api --format='{{json .NetworkSettings.Networks}}' | jq -r 'keys[]')
if echo "$NETWORKS" | grep -q "traefik-public-test"; then
log_pass "API service connected to external Traefik network"
else
log_fail "API service not connected to external Traefik network"
fi
log_test "Verifying Web service is connected to external network"
NETWORKS=$(docker inspect mosaic-web --format='{{json .NetworkSettings.Networks}}' | jq -r 'keys[]')
if echo "$NETWORKS" | grep -q "traefik-public-test"; then
log_pass "Web service connected to external Traefik network"
else
log_fail "Web service not connected to external Traefik network"
fi
log_test "Verifying API service has correct Traefik labels for upstream"
LABELS=$(docker inspect mosaic-api --format='{{json .Config.Labels}}')
if echo "$LABELS" | jq -e '."traefik.enable" == "true"' > /dev/null && \
echo "$LABELS" | jq -e '."traefik.docker.network"' > /dev/null; then
log_pass "API service has correct upstream Traefik labels"
else
log_fail "API service missing or incorrect upstream Traefik labels"
fi
# Cleanup
docker compose -f docker-compose.test.yml --env-file .env.test down -v
docker network rm traefik-public-test 2>/dev/null || true
}
# Test none mode (direct port exposure)
test_none_mode() {
log_info "=========================================="
log_info "Testing None Mode (Direct Ports)"
log_info "=========================================="
# Create test environment file
cat > .env.test <<EOF
TRAEFIK_MODE=none
API_PORT=3001
WEB_PORT=3000
POSTGRES_PASSWORD=test_password
AUTHENTIK_SECRET_KEY=test_secret_key_minimum_50_characters_long_for_testing
AUTHENTIK_POSTGRES_PASSWORD=test_auth_password
AUTHENTIK_BOOTSTRAP_PASSWORD=test_admin
JWT_SECRET=test_jwt_secret_minimum_32_chars
EOF
# Copy docker-compose.yml to test version
cp docker-compose.yml docker-compose.test.yml
log_test "Starting services in none mode (no Traefik)"
if docker compose -f docker-compose.test.yml --env-file .env.test up -d; then
log_pass "Services started successfully"
else
log_fail "Failed to start services"
return 1
fi
sleep 10 # Wait for services to initialize
log_test "Verifying Traefik container is NOT running"
if ! docker ps --filter "name=mosaic-traefik" --format "{{.Names}}" | grep -q "mosaic-traefik"; then
log_pass "Traefik correctly not started in none mode"
else
log_fail "Traefik should not be running in none mode"
fi
log_test "Verifying API service Traefik labels are disabled"
LABELS=$(docker inspect mosaic-api --format='{{json .Config.Labels}}')
if echo "$LABELS" | jq -e '."traefik.enable" == "false"' > /dev/null; then
log_pass "API service Traefik labels correctly disabled"
else
log_fail "API service Traefik labels should be disabled"
fi
log_test "Verifying direct port access to API"
MAX_RETRIES=30
RETRY_COUNT=0
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
if curl -sf http://localhost:3001/health > /dev/null 2>&1; then
log_pass "API accessible via direct port 3001"
break
fi
((RETRY_COUNT++))
sleep 2
done
if [ $RETRY_COUNT -eq $MAX_RETRIES ]; then
log_fail "API not accessible via direct port 3001"
fi
# Cleanup
docker compose -f docker-compose.test.yml --env-file .env.test down -v
}
# Print test summary
print_summary() {
echo ""
log_info "=========================================="
log_info "Test Summary"
log_info "=========================================="
echo "Total tests run: $TESTS_RUN"
echo -e "${GREEN}Passed: $TESTS_PASSED${NC}"
echo -e "${RED}Failed: $TESTS_FAILED${NC}"
if [ $TESTS_FAILED -gt 0 ]; then
echo ""
log_error "Failed tests:"
for test in "${FAILED_TESTS[@]}"; do
echo " - $test"
done
echo ""
exit 1
else
echo ""
log_pass "All tests passed!"
exit 0
fi
}
# Main execution
main() {
TEST_MODE="${1:-all}"
log_info "Mosaic Stack - Traefik Integration Tests"
log_info "Test mode: $TEST_MODE"
echo ""
check_prerequisites
case "$TEST_MODE" in
bundled)
test_bundled_mode
;;
upstream)
test_upstream_mode
;;
none)
test_none_mode
;;
all)
test_bundled_mode
test_upstream_mode
test_none_mode
;;
*)
log_error "Invalid test mode: $TEST_MODE"
log_info "Usage: $0 [bundled|upstream|none|all]"
exit 1
;;
esac
print_summary
}
main "$@"

View File

@@ -0,0 +1,17 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
name: 'docker-integration',
include: ['**/*.test.ts'],
testTimeout: 120000, // 2 minutes for Docker operations
hookTimeout: 120000,
globals: true,
environment: 'node',
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
include: ['tests/integration/**/*.ts'],
},
},
});