channelKeyCollision.test.ts3.0 KBView on GitHub
import { containerKeyOf, findFeedItem } from '@/modules/inbox/hooks/use-channel-artifact-item';
import type { InboxItem } from '@/modules/inbox/types';

/**
 * A container key does not identify a container on its own.
 *
 * `channel_containers` is keyed on `(organization_id, channel, account_ref, external_id)`, and a
 * SLACK CONNECT channel is what exercises the difference: both workspaces sync the same `C…` id
 * under their own `account_ref`. Four such pairs are live today (`C0B4ZN3A68Z` is
 * `cedar-concentrate` under one workspace and `ext-cedar-concentrate` under the other). They
 * straddle two orgs, so no single seat sees both yet — but the feed row and the container read
 * both have to answer the ambiguity the SAME way, or the panel and the inbox open different
 * chats for one click depending only on whether the feed happened to be loaded.
 *
 * The rule, on both sides: most recently active wins, id breaks the tie.
 */

function slackItem(workspaceId: string, channelId: string, sortedAt: string): InboxItem {
  return {
    id: `slack:${workspaceId}:${channelId}`,
    channel: 'slack',
    sortedAt,
    counterpart: { name: 'Slack', subtitle: `#${workspaceId}` },
    snippet: '',
    unread: false,
    starred: false,
    hasDraft: false,
    done: false,
    labelIds: [],
    participantCount: 1,
    ref: { kind: 'slack', conversationId: '', slackChannelId: channelId, workspaceId },
  } as InboxItem;
}

/**
 * The real `findFeedItem`, fed the store's own shape — every loaded row, in the order the
 * per-channel windows happened to arrive. That order is the variable under test: it is exactly
 * what the old `find()` followed.
 */
function pick(items: InboxItem[], channelId: string): InboxItem | undefined {
  return findFeedItem(items, 'slack', channelId);
}

describe('a container key shared by two workspaces', () => {
  const CHANNEL = 'C0B4ZN3A68Z';
  const OURS = slackItem('T08AHSLJBTL', CHANNEL, '2026-08-27T17:58:19.268Z');
  const THEIRS = slackItem('T08UTA22H5Y', CHANNEL, '2026-08-14T20:08:33.691Z');

  it('reads the same key off both rows — which is the whole problem', () => {
    expect(containerKeyOf(OURS)).toBe(CHANNEL);
    expect(containerKeyOf(THEIRS)).toBe(CHANNEL);
    expect(OURS.id).not.toBe(THEIRS.id);
  });

  it('resolves to the most recently active one, whichever order the feed loaded them in', () => {
    expect(pick([OURS, THEIRS], CHANNEL)).toBe(OURS);
    // Load order is what `find()` followed, so the reversed case is the one that used to
    // disagree with itself.
    expect(pick([THEIRS, OURS], CHANNEL)).toBe(OURS);
  });

  it('breaks a tie on id, so two idle chats cannot flip between renders', () => {
    const a = slackItem('T00000000AA', CHANNEL, '');
    const b = slackItem('T00000000BB', CHANNEL, '');
    expect(pick([a, b], CHANNEL)).toBe(pick([b, a], CHANNEL));
  });

  it('still finds a lone row, and still misses a key nothing answers to', () => {
    expect(pick([THEIRS], CHANNEL)).toBe(THEIRS);
    expect(pick([OURS, THEIRS], 'C_NOT_A_CHANNEL')).toBeUndefined();
  });
});