Files
backspace/packages/server/test/federation-handshake-desync.test.ts
T
TheZwiss c7d88481ad test: give federation integration suites a realistic per-test timeout (#7)
The three test/ files boot real federated instances and drive S2S over HTTP,
with several tests deliberately waiting on log matchers (e.g. logMatched(...,
1_000) per remote). Vitest's 5s default per-test timeout is meant for unit tests
and is too tight here: under CI load the multi-remote fan-out tests in
federation-identity-deletion intermittently timed out (observed on the post-merge
main run), producing a flaky red check.

Set a file-level testTimeout of 30s in each of the three harness-based suites via
vi.setConfig. Scoped per-file so unit tests keep the strict 5s default; a genuine
hang still trips the 30s ceiling well before the 90s hook budget. No test logic
changed.
2026-07-10 00:51:18 +02:00

135 lines
5.2 KiB
TypeScript

import { describe, it, expect, afterEach, vi } from 'vitest';
import {
bootTwoInstancesForHandshake,
registerAdmin,
initiatePeering,
resetPeer,
getPeer,
s2sHealthy,
simulateReset,
} from './helpers/realHandshake.js';
import type { TwoInstanceHarness } from './helpers/twoInstanceHarness.js';
import { openInspector } from './helpers/dbInspect.js';
// Boots real federated instances and drives the peering handshake over HTTP; the
// 5s unit-test default is too tight under CI load. See
// federation-identity-deletion.test.ts for the rationale. A genuine hang still
// trips this ceiling.
vi.setConfig({ testTimeout: 30_000 });
/**
* Acceptance-gate integration suite for the federation handshake desync bugs.
*
* These cases drive the REAL cross-instance `/peer/initiate`→`/peer/accept`
* handshake (via realHandshake.ts) rather than seeding peer rows. This is the
* RED baseline: #1 (control) passes on current code, #2 (BUG-1) and #4 (BUG-2)
* are EXPECTED to fail against current (unfixed) code — the fixes in later tasks
* turn them green. Do NOT modify product code to make #2/#4 pass here.
*/
let h: TwoInstanceHarness;
afterEach(async () => {
if (h) await h.cleanup();
});
describe('federation handshake desync (BUG-1/BUG-2)', () => {
it('#1 control: clean handshake activates both sides with matching secret', async () => {
h = await bootTwoInstancesForHandshake();
const adminI = await registerAdmin(h.home);
await registerAdmin(h.remote);
const { status } = await initiatePeering(h.home, adminI.token, h.remote);
expect(status).toBe(200);
expect(await s2sHealthy(h.home, h.remote)).toBe(true);
expect(await s2sHealthy(h.remote, h.home)).toBe(true);
const homeInsp = openInspector(h.home);
const remoteInsp = openInspector(h.remote);
try {
const iSecret = homeInsp.federationPeer(h.remote.origin)?.hmacSecret;
const rSecret = remoteInsp.federationPeer(h.home.origin)?.hmacSecret;
expect(iSecret).toBeTruthy();
expect(rSecret).toBeTruthy();
expect(iSecret).toBe(rSecret);
} finally {
homeInsp.close();
remoteInsp.close();
}
}, 60_000);
it('#2 BUG-1: initiator must NOT false-activate against a survivor that holds a row', async () => {
h = await bootTwoInstancesForHandshake();
const adminI = await registerAdmin(h.home);
await registerAdmin(h.remote);
// Establish peering, then reset home (fresh incarnation, no row).
// remote still holds its old active row for home's origin.
await initiatePeering(h.home, adminI.token, h.remote);
expect(await s2sHealthy(h.home, h.remote)).toBe(true);
const freshHome = await simulateReset(h.home, h);
const adminI2 = await registerAdmin(freshHome);
// Fresh home re-initiates into the survivor remote (which still holds its old active row).
const { status, body } = await initiatePeering(freshHome, adminI2.token, h.remote);
// FIX (green): remote refuses honestly with 409; fresh home does NOT end up active-but-dead.
expect(status).toBe(409);
expect(body.code).toBe('PEER_EXISTS_RESET_REQUIRED');
// Fresh home must hold no false-active row for remote's origin.
const insp = openInspector(freshHome);
try {
const iRow = insp.federationPeer(h.remote.origin);
expect(iRow?.status === 'active').toBe(false);
} finally {
insp.close();
}
}, 60_000);
it('#4 BUG-2: one-click Re-peer never reports success on a dead peering; genuinely recovers', async () => {
h = await bootTwoInstancesForHandshake();
const adminI = await registerAdmin(h.home);
const adminR = await registerAdmin(h.remote);
await initiatePeering(h.home, adminI.token, h.remote);
expect(await s2sHealthy(h.home, h.remote)).toBe(true);
const freshHome = await simulateReset(h.home, h);
const adminIf = await registerAdmin(freshHome);
// Fresh home initiates into remote → triggers detection (remote's row moves to
// needs_attention via markPeerReset). On buggy code fresh home ALSO false-activates;
// after the fix fresh home gets 409 and stays clean.
await initiatePeering(freshHome, adminIf.token, h.remote);
// One-click Re-peer on the SURVIVOR (remote): detection must have fired.
const rPeer = await getPeer(h.remote, adminR.token, freshHome.origin);
expect(rPeer).toBeTruthy();
expect(rPeer!.status).toBe('needs_attention');
// Reset remote's stale row, then re-initiate the handshake from remote.
await resetPeer(h.remote, adminR.token, rPeer!.id);
const rebuilt = await initiatePeering(h.remote, adminR.token, freshHome);
// GENUINE recovery: 200 + working secret in both directions + matching stored secrets.
expect(rebuilt.status).toBe(200);
expect(await s2sHealthy(h.remote, freshHome)).toBe(true);
expect(await s2sHealthy(freshHome, h.remote)).toBe(true);
const remoteInsp = openInspector(h.remote);
const homeInsp = openInspector(freshHome);
try {
const rSecret = remoteInsp.federationPeer(freshHome.origin)?.hmacSecret;
const iSecret = homeInsp.federationPeer(h.remote.origin)?.hmacSecret;
expect(rSecret).toBeTruthy();
expect(iSecret).toBeTruthy();
expect(rSecret).toBe(iSecret);
} finally {
remoteInsp.close();
homeInsp.close();
}
}, 60_000);
});