~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
441 lines · 11.8 KB Raw History

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

TaskDescriptionCommit Message
1Docker health checks + Linux fixfeat: add health checks and Linux support to docker-compose
2Docker services helperfeat: add docker services helper for e2e tests
3Node server relay configfeat: configure node server helper to use local relay
4Wire into test setupfeat: wire docker services into e2e test setup
5Relay sync teststest: add relay sync test suite for debugging
6Integration verificationtest: complete local relay integration for e2e tests