fix(web): Re-peer surfaces incomplete outcome instead of false success (BUG-2)

This commit is contained in:
Jannis Braun
2026-07-02 13:03:03 +02:00
parent c6f0e6f25d
commit ad7c86e89e
3 changed files with 97 additions and 4 deletions
+2 -2
View File
@@ -277,7 +277,7 @@ export class BackspaceApiClient {
};
readonly federation: {
initiatePeering: (data: { remoteOrigin: string }) => Promise<{ peer: FederationPeer }>;
initiatePeering: (data: { remoteOrigin: string }) => Promise<{ peer: FederationPeer; verified?: boolean }>;
ensurePeered: (data: { remoteOrigin: string }) => Promise<{ peeringStatus: string; peerId?: string; error?: string }>;
peers: () => Promise<{ peers: FederationPeer[] }>;
resetEvents: () => Promise<FederationResetEventsResponse>;
@@ -697,7 +697,7 @@ export class BackspaceApiClient {
this.federation = {
initiatePeering: (data: { remoteOrigin: string }) =>
request<{ peer: FederationPeer }>(
request<{ peer: FederationPeer; verified?: boolean }>(
'POST', '/federation/peer/initiate', data
),
ensurePeered: (data: { remoteOrigin: string }) =>
@@ -122,6 +122,84 @@ describe('FederationPanel — Reset cleanup', () => {
);
});
it('warns when Re-peer completes but the handshake is unverified (verified:false)', async () => {
peers.mockResolvedValue({ peers: [resetPeerFixture] });
resetEvents.mockResolvedValue({ events: [] });
resetPeer.mockResolvedValue({ success: true });
initiatePeering.mockResolvedValue({
peer: { ...resetPeerFixture, status: 'needs_attention' },
verified: false,
});
render(<FederationPanel />);
fireEvent.click(await screen.findByRole('button', { name: 'Re-peer' }));
fireEvent.click(await screen.findByRole('button', { name: 'Re-peer & heal' }));
await waitFor(() => expect(initiatePeering).toHaveBeenCalled());
await waitFor(() =>
expect(addToast).toHaveBeenCalledWith(
'Re-peer incomplete — Peer still holds stale peering for you. Its admin must reset their side, then Re-peer again.',
'warning',
),
);
expect(addToast).not.toHaveBeenCalledWith(
expect.stringContaining('Re-peering initiated'),
'success',
expect.anything(),
);
});
it('warns when the remote rejects with 409 PEER_EXISTS_RESET_REQUIRED', async () => {
peers.mockResolvedValue({ peers: [resetPeerFixture] });
resetEvents.mockResolvedValue({ events: [] });
resetPeer.mockResolvedValue({ success: true });
initiatePeering.mockRejectedValue(
new HttpError(409, 'Peer exists — reset required', {
error: 'Peer exists — reset required',
statusCode: 409,
code: 'PEER_EXISTS_RESET_REQUIRED',
}),
);
render(<FederationPanel />);
fireEvent.click(await screen.findByRole('button', { name: 'Re-peer' }));
fireEvent.click(await screen.findByRole('button', { name: 'Re-peer & heal' }));
await waitFor(() => expect(initiatePeering).toHaveBeenCalled());
await waitFor(() =>
expect(addToast).toHaveBeenCalledWith(
'The remote instance still holds stale peering for you. Ask its admin to reset their side, then Re-peer again.',
'warning',
),
);
});
it('shows the success toast when the handshake is verified (verified:true)', async () => {
peers.mockResolvedValue({ peers: [resetPeerFixture] });
resetEvents.mockResolvedValue({ events: [] });
resetPeer.mockResolvedValue({ success: true });
initiatePeering.mockResolvedValue({
peer: { ...resetPeerFixture, status: 'active' },
verified: true,
});
render(<FederationPanel />);
fireEvent.click(await screen.findByRole('button', { name: 'Re-peer' }));
fireEvent.click(await screen.findByRole('button', { name: 'Re-peer & heal' }));
await waitFor(() => expect(initiatePeering).toHaveBeenCalled());
await waitFor(() =>
expect(addToast).toHaveBeenCalledWith('Re-peering initiated with Peer', 'success', 3000),
);
expect(addToast).not.toHaveBeenCalledWith(
expect.stringContaining('still holds stale peering'),
'warning',
);
});
it('removes an orphaned account via the existing admin delete', async () => {
peers.mockResolvedValue({ peers: [] });
resetEvents.mockResolvedValue({ events: [resetEvent([orphanedAccount()])] });
@@ -910,8 +910,15 @@ function ResetCleanup() {
// Order matters: reset the stale local record BEFORE the fresh handshake,
// so activation heals stale friendships/DMs against the new incarnation.
await api.federation.resetPeer(peer.id);
await api.federation.initiatePeering({ remoteOrigin: peer.origin });
addToast(`Re-peering initiated with ${peerName(peer)}`, 'success', 3000);
const result = await api.federation.initiatePeering({ remoteOrigin: peer.origin });
if (result.verified === false || result.peer?.status === 'needs_attention') {
addToast(
`Re-peer incomplete — ${peerName(peer)} still holds stale peering for you. Its admin must reset their side, then Re-peer again.`,
'warning',
);
} else {
addToast(`Re-peering initiated with ${peerName(peer)}`, 'success', 3000);
}
await fetchAll();
} else {
const { account } = confirmAction;
@@ -938,6 +945,14 @@ function ResetCleanup() {
} else {
addToast(err instanceof Error ? err.message : 'Failed to remove account', 'warning');
}
} else if (
err instanceof HttpError && err.status === 409 &&
(err.body as { code?: string } | undefined)?.code === 'PEER_EXISTS_RESET_REQUIRED'
) {
addToast(
`The remote instance still holds stale peering for you. Ask its admin to reset their side, then Re-peer again.`,
'warning',
);
} else {
addToast(err instanceof Error ? err.message : 'Re-peering failed', 'warning');
}