~ 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:

```yaml
    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:

```yaml
  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**

```bash
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**

```js
// 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**

```bash
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:

```js
// 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**

```bash
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):

```js
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:

```js
  // 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:

```js
}, 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**

```bash
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:

```js
  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**

```bash
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**

```bash
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` |