chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
E2E Local Relay Integration Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Integrate local PLC and relay from docker-compose into e2e tests for debugging sync issues and comprehensive federation testing.
Architecture: Tests use a hybrid docker management approach - check if services are running, start them if not, reset volumes for clean state. The Node.js PDS is configured with relayUrl to notify the local relay after commits. New test suite verifies events flow through the relay correctly.
Tech Stack: Docker Compose, Vitest, Node.js child_process for docker management, WebSocket for firehose tests.
Task 1: Update docker-compose.yml with Health Checks
Files:
- Modify:
docker-compose.yml
Step 1: Add health check to PLC service
Add after the command line in the plc service:
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:2582/_health || exit 1"]
interval: 2s
timeout: 5s
retries: 15
Step 2: Update relay service with health check and Linux fix
Replace the entire relay service with:
relay:
image: ghcr.io/bluesky-social/indigo:latest
ports:
- "2470:2470"
environment:
- GOLOG_LOG_LEVEL=info
- DATA_DIR=/data
- RELAY_ADMIN_KEY=localdev
- ATP_PLC_HOST=http://plc:2582
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- relay_data:/data
command: ["/bigsky", "--api-listen", ":2470"]
depends_on:
plc:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:2470/xrpc/_health || exit 1"]
interval: 2s
timeout: 5s
retries: 15
Step 3: Verify docker-compose syntax
Run: docker-compose config
Expected: YAML output with no errors
Step 4: Commit
git add docker-compose.yml
git commit -m "feat: add health checks and Linux support to docker-compose"
Task 2: Create Docker Services Helper
Files:
- Create:
test/helpers/docker-services.js
Step 1: Create the docker services helper
// test/helpers/docker-services.js
import { execSync } from 'node:child_process';
const RELAY_URL = 'http://localhost:2470';
const PLC_URL = 'http://localhost:2582';
/**
* Wait for a service to respond
* @param {string} url
* @param {string} name
* @param {number} maxAttempts
*/
async function waitForService(url, name, maxAttempts = 30) {
for (let i = 0; i < maxAttempts; i++) {
try {
const res = await fetch(url);
if (res.ok) {
console.log(`${name} is ready`);
return;
}
} catch {
// Service not ready yet
}
await new Promise((r) => setTimeout(r, 1000));
}
throw new Error(`${name} failed to start after ${maxAttempts} attempts`);
}
/**
* Ensure docker-compose services are running
* Resets volumes for clean state on first start
*/
export async function ensureDockerServices() {
// Check if relay is already responding
try {
const res = await fetch(RELAY_URL);
if (res.ok) {
console.log('Docker services already running');
return;
}
} catch {
// Services not running, need to start
}
console.log('Starting docker services (this may take a moment)...');
// Reset volumes for clean slate
execSync('docker-compose down -v', { stdio: 'inherit' });
execSync('docker-compose up -d', { stdio: 'inherit' });
// Wait for services to be ready
await waitForService(PLC_URL, 'PLC');
await waitForService(RELAY_URL, 'Relay');
console.log('Docker services are ready');
}
/**
* Request the relay to crawl a PDS
* @param {string} hostname - PDS hostname (e.g., 'host.docker.internal:3000')
*/
export async function requestRelayCrawl(hostname) {
const res = await fetch(`${RELAY_URL}/admin/pds/requestCrawl`, {
method: 'POST',
headers: {
Authorization: 'Bearer localdev',
'Content-Type': 'application/json',
},
body: JSON.stringify({ hostname }),
});
if (!res.ok) {
throw new Error(`Failed to request crawl: ${res.status}`);
}
}
export { RELAY_URL, PLC_URL };
Step 2: Verify file was created
Run: ls -la test/helpers/docker-services.js
Expected: File exists
Step 3: Commit
git add test/helpers/docker-services.js
git commit -m "feat: add docker services helper for e2e tests"
Task 3: Update Node Server Helper with Relay Config
Files:
- Modify:
test/helpers/node-server.js
Step 1: Add USE_LOCAL_INFRA constant and relayUrl config
Replace the entire file with:
// test/helpers/node-server.js
import { mkdirSync, rmSync } from 'node:fs';
import { createServer } from '@pdsjs/node';
const TEST_DATA_DIR = './test-data';
const TEST_PORT = 3000;
const USE_LOCAL_INFRA = process.env.USE_LOCAL_INFRA !== 'false';
/**
* Start Node.js PDS server for e2e tests
* Cleans test data directory for fresh state each run
* @returns {Promise<{close: () => Promise<void>}>} Server instance with close() method
*/
export async function startNodeServer() {
// Fresh data each run
rmSync(TEST_DATA_DIR, { recursive: true, force: true });
mkdirSync(TEST_DATA_DIR, { recursive: true });
const server = await createServer({
port: TEST_PORT,
dbPath: `${TEST_DATA_DIR}/pds.db`,
blobsDir: `${TEST_DATA_DIR}/blobs`,
jwtSecret: 'test-secret-for-e2e',
hostname: USE_LOCAL_INFRA ? 'host.docker.internal:3000' : `localhost:${TEST_PORT}`,
password: 'test-password',
// Use local relay when docker infrastructure is available
relayUrl: USE_LOCAL_INFRA ? 'localhost:2470' : undefined,
// Keep appview pointing to production (for proxy tests)
appviewUrl: 'https://api.bsky.app',
appviewDid: 'did:web:api.bsky.app',
});
await server.listen();
return server;
}
/**
* Stop Node.js PDS server
* @param {{close: () => Promise<void>}} server - Server instance from startNodeServer
*/
export async function stopNodeServer(server) {
await server.close();
}
export { USE_LOCAL_INFRA, TEST_PORT };
Step 2: Commit
git add test/helpers/node-server.js
git commit -m "feat: configure node server helper to use local relay"
Task 4: Wire Docker Services into E2E Test Setup
Files:
- Modify:
test/e2e.test.js:1-40
Step 1: Add docker services import
Add after the existing imports (around line 11):
import { ensureDockerServices, RELAY_URL } from './helpers/docker-services.js';
import { USE_LOCAL_INFRA } from './helpers/node-server.js';
Step 2: Update beforeAll to start docker services
Find the beforeAll block and update it. Add at the very start of the beforeAll callback, before any existing code:
// Start docker services if enabled (PLC, relay)
if (USE_LOCAL_INFRA) {
await ensureDockerServices();
}
Step 3: Increase beforeAll timeout
Change the beforeAll timeout from whatever it currently is to 120000 (2 minutes) to accommodate docker startup:
}, 120000);
Step 4: Run tests to verify setup works
Run: USE_LOCAL_INFRA=false npm run test:e2e:node 2>&1 | tail -5
Expected: Tests pass (docker services skipped)
Step 5: Commit
git add test/e2e.test.js
git commit -m "feat: wire docker services into e2e test setup"
Task 5: Add Relay Sync Test Suite
Files:
- Modify:
test/e2e.test.js(add new describe block before Cleanup)
Step 1: Add relay sync test suite
Add this new describe block just before the describe('Cleanup', ...) block:
describe('Relay sync (requires docker)', () => {
// Skip entire suite if docker infrastructure is disabled
beforeAll(function () {
if (!USE_LOCAL_INFRA) {
console.log('Skipping relay sync tests (USE_LOCAL_INFRA=false)');
}
});
it('relay receives repo after PDS commits', async () => {
if (!USE_LOCAL_INFRA) return;
// Create a record (triggers notifyRelay internally)
const { status } = await jsonPost(
'/xrpc/com.atproto.repo.createRecord',
{
repo: DID,
collection: 'app.bsky.feed.post',
record: { text: 'relay sync test', createdAt: new Date().toISOString() },
},
{ Authorization: `Bearer ${token}` },
);
expect(status).toBe(200);
// Poll relay until it has synced the repo (or timeout)
let synced = false;
for (let i = 0; i < 30; i++) {
try {
const res = await fetch(
`${RELAY_URL}/xrpc/com.atproto.sync.getLatestCommit?did=${DID}`,
);
if (res.ok) {
const data = await res.json();
if (data.rev && data.rev.length > 0) {
synced = true;
console.log(`Relay synced after ${i + 1} attempts:`, data);
break;
}
}
} catch {
// Relay not ready or error
}
await new Promise((r) => setTimeout(r, 500));
}
expect(synced, 'Relay should have synced the repo').toBe(true);
});
it('relay firehose emits commit events', async () => {
if (!USE_LOCAL_INFRA) return;
// Connect to relay firehose
const wsUrl = RELAY_URL.replace('http', 'ws');
const ws = new WebSocket(
`${wsUrl}/xrpc/com.atproto.sync.subscribeRepos?cursor=0`,
);
const events = [];
await new Promise((resolve, reject) => {
ws.on('message', (data) => {
events.push(data);
if (events.length >= 1) {
ws.close();
resolve();
}
});
ws.on('error', (err) => {
console.error('Relay WebSocket error:', err);
reject(err);
});
ws.on('open', () => {
// Create a record to generate an event
jsonPost(
'/xrpc/com.atproto.repo.createRecord',
{
repo: DID,
collection: 'app.bsky.feed.post',
record: { text: 'firehose test', createdAt: new Date().toISOString() },
},
{ Authorization: `Bearer ${token}` },
);
// Give time for events, then close
setTimeout(() => {
ws.close();
resolve();
}, 5000);
});
setTimeout(() => reject(new Error('Relay firehose timeout')), 15000);
});
console.log(`Received ${events.length} events from relay firehose`);
expect(events.length).toBeGreaterThan(0);
});
});
Step 2: Run tests with docker infrastructure
Run: npm run test:e2e:node 2>&1 | grep -E "relay|Relay|sync"
Expected: Relay sync tests run (may pass or fail - this is debugging!)
Step 3: Commit
git add test/e2e.test.js
git commit -m "test: add relay sync test suite for debugging"
Task 6: Test Full Integration
Step 1: Clean start with docker
Run: docker-compose down -v
Expected: Services stopped, volumes removed
Step 2: Run full e2e test suite
Run: npm run test:e2e:node 2>&1 | tail -20
Expected: All tests run, docker services started automatically
Step 3: Check relay logs for any errors
Run: docker-compose logs relay 2>&1 | tail -50
Expected: Log output showing crawl requests and any sync errors
Step 4: Final commit if any adjustments made
git add -A
git commit -m "test: complete local relay integration for e2e tests"
Summary
| Task | Description | Commit Message |
|---|---|---|
| 1 | Docker health checks + Linux fix | feat: add health checks and Linux support to docker-compose |
| 2 | Docker services helper | feat: add docker services helper for e2e tests |
| 3 | Node server relay config | feat: configure node server helper to use local relay |
| 4 | Wire into test setup | feat: wire docker services into e2e test setup |
| 5 | Relay sync tests | test: add relay sync test suite for debugging |
| 6 | Integration verification | test: complete local relay integration for e2e tests |