@@ -84,14 +84,15 @@ describe('ScryingPoolController', () => {
|
||||
// ── AC-2: action() happy path ─────────────────────────────────────────────
|
||||
|
||||
describe('action() happy path (AC-2)', () => {
|
||||
it('stores a PendingOp in _pendingOps keyed by participantId', () => {
|
||||
it('registers a PendingOp via socketHandler.registerPendingOp with correct shape', () => {
|
||||
// With self-confirm, _pendingOps is cleared synchronously after action().
|
||||
// Verify the op was passed to registerPendingOp before being confirmed.
|
||||
controller.action('ui', 'user-1', 'hidden', 'op-1', 0);
|
||||
expect(controller._pendingOps.has('user-1')).toBe(true);
|
||||
expect(controller._pendingOps.get('user-1')).toMatchObject({
|
||||
opId: 'op-1',
|
||||
userId: 'user-1',
|
||||
targetState: 'hidden',
|
||||
});
|
||||
expect(socketHandler.registerPendingOp).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ opId: 'op-1', userId: 'user-1', targetState: 'hidden' }),
|
||||
'scrying-pool.visibility.set',
|
||||
expect.objectContaining({ opId: 'op-1' })
|
||||
);
|
||||
});
|
||||
|
||||
it('calls stateStore.setVisibility with the target state (optimistic update)', () => {
|
||||
@@ -117,18 +118,24 @@ describe('ScryingPoolController', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('fires Hooks.callAll scrying-pool:controllerAction with correct payload', () => {
|
||||
it('fires Hooks.callAll scrying-pool:controllerAction after self-confirm', () => {
|
||||
// Self-confirm calls _onEcho which fires the hook with source: 'echo'.
|
||||
controller.action('ui', 'user-1', 'hidden', 'op-1', 0);
|
||||
expect(hooksStub.callAll).toHaveBeenCalledWith(
|
||||
'scrying-pool:controllerAction',
|
||||
expect.objectContaining({ participantId: 'user-1', targetState: 'hidden', source: 'ui', opId: 'op-1' })
|
||||
expect.objectContaining({ participantId: 'user-1', targetState: 'hidden', source: 'echo', opId: 'op-1' })
|
||||
);
|
||||
});
|
||||
|
||||
it('sets previousState to null-coalesced "never-connected" when participant is new', () => {
|
||||
it('sets previousState to "active" when participant is new (not yet in matrix)', () => {
|
||||
// With self-confirm, _pendingOps is cleared synchronously. Verify via registerPendingOp arg.
|
||||
controller.action('ui', 'new-user', 'hidden', 'op-1', 0);
|
||||
const op = controller._pendingOps.get('new-user');
|
||||
expect(op.previousState).toBe('never-connected');
|
||||
// 'active' is the render-time default for users not in the matrix.
|
||||
expect(socketHandler.registerPendingOp).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ previousState: 'active' }),
|
||||
expect.any(String),
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -226,23 +233,30 @@ describe('ScryingPoolController', () => {
|
||||
return adapter.socket.on.mock.calls[0][1];
|
||||
}
|
||||
|
||||
// Helper: directly register a pending op (bypasses action() self-confirm)
|
||||
function seedPendingOp(userId, opId, targetState = 'hidden') {
|
||||
const op = { opId, userId, targetState, previousState: 'active' };
|
||||
controller._pendingOps.set(userId, op);
|
||||
socketHandler.registerPendingOp(op, 'scrying-pool.visibility.set', {});
|
||||
}
|
||||
|
||||
it('calls socketHandler.confirmPendingOp with the opId', () => {
|
||||
controller.action('ui', 'user-1', 'hidden', 'op-1', 0);
|
||||
const echoHandler = getEchoHandler();
|
||||
seedPendingOp('user-1', 'op-1');
|
||||
echoHandler({ opId: 'op-1', userId: 'user-1', state: 'hidden', revision: 1 });
|
||||
expect(socketHandler.confirmPendingOp).toHaveBeenCalledWith('op-1');
|
||||
});
|
||||
|
||||
it('stores the echo revision in _revisions for the userId', () => {
|
||||
controller.action('ui', 'user-1', 'hidden', 'op-2', 0);
|
||||
const echoHandler = getEchoHandler();
|
||||
seedPendingOp('user-1', 'op-2');
|
||||
echoHandler({ opId: 'op-2', userId: 'user-1', state: 'hidden', revision: 7 });
|
||||
expect(controller._revisions.get('user-1')).toBe(7);
|
||||
});
|
||||
|
||||
it('calls stateStore.setVisibility with the authoritative state', () => {
|
||||
controller.action('ui', 'user-1', 'active', 'op-3', 0);
|
||||
const echoHandler = getEchoHandler();
|
||||
seedPendingOp('user-1', 'op-3', 'active');
|
||||
const setSpy = vi.spyOn(stateStore, 'setVisibility');
|
||||
|
||||
echoHandler({ opId: 'op-3', userId: 'user-1', state: 'active', revision: 2 });
|
||||
@@ -251,8 +265,8 @@ describe('ScryingPoolController', () => {
|
||||
});
|
||||
|
||||
it('fires Hooks.callAll scrying-pool:controllerAction with source: echo', () => {
|
||||
controller.action('ui', 'user-1', 'hidden', 'op-4', 0);
|
||||
const echoHandler = getEchoHandler();
|
||||
seedPendingOp('user-1', 'op-4');
|
||||
echoHandler({ opId: 'op-4', userId: 'user-1', state: 'hidden', revision: 1 });
|
||||
|
||||
expect(hooksStub.callAll).toHaveBeenCalledWith(
|
||||
@@ -262,20 +276,18 @@ describe('ScryingPoolController', () => {
|
||||
});
|
||||
|
||||
it('removes the participant from _pendingOps after echo', () => {
|
||||
// Register a pending op first
|
||||
controller.action('ui', 'user-1', 'hidden', 'op-1', 0);
|
||||
const echoHandler = getEchoHandler();
|
||||
seedPendingOp('user-1', 'op-1');
|
||||
expect(controller._pendingOps.has('user-1')).toBe(true);
|
||||
|
||||
const echoHandler = getEchoHandler();
|
||||
echoHandler({ opId: 'op-1', userId: 'user-1', state: 'hidden', revision: 1 });
|
||||
|
||||
expect(controller._pendingOps.has('user-1')).toBe(false);
|
||||
});
|
||||
|
||||
it('defaults revision to 0 when echo payload omits revision field', () => {
|
||||
// Register a pending op first (required by new validation)
|
||||
controller.action('ui', 'user-1', 'hidden', 'op-1', 0);
|
||||
const echoHandler = getEchoHandler();
|
||||
seedPendingOp('user-1', 'op-1');
|
||||
echoHandler({ opId: 'op-1', userId: 'user-1', state: 'hidden' }); // no revision
|
||||
expect(controller._revisions.get('user-1')).toBe(0);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user