playbook-document-owner.test.tsx4.7 KBView on GitHub
/**
 * The playbook a caller HANDS this editor beats the member picker.
 *
 * `PlaybookDocument` is mounted two ways. The administered surfaces resolve their
 * document FROM the picker and show `AdministeredUserBar`, so an ambient read is
 * right there. The playground does the opposite: it passes a specific teammate's
 * `documentId` and mounts no picker, while the picker still holds whoever the
 * staff user selected last. The two disagree, and the document is authoritative.
 * Otherwise the reference lookup, the subagent creation and the extensions'
 * `ownerUserId` all run against a teammate whose playbook is not on screen.
 */

import { render, waitFor } from '@testing-library/react';

const PICKER_USER = 'user-A';
const DOCUMENT_OWNER = 'user-B';

// The factories run before the module body, so anything they close over has to be
// named `mock*`, hence the literal here rather than the constant above.
jest.mock('@/modules/administeredUser', () => ({
  useTargetUserId: () => 'user-A',
}));

const mockGetDocQuery = jest.fn<Promise<{ id: string }>, [{ targetUserId?: string }]>(
  async () => ({ id: 'doc_ref' }),
);
jest.mock('@/providers/query-provider', () => ({
  useTRPC: () => ({
    documents: { getDoc: { queryOptions: () => ({ queryKey: ['getDoc'], queryFn: () => null }) } },
  }),
  trpcClient: {
    documents: {
      getDoc: {
        query: (...args: Parameters<typeof mockGetDocQuery>) => mockGetDocQuery(...args),
      },
    },
  },
}));

jest.mock('@tanstack/react-query', () => ({
  useQuery: () => ({ data: undefined, isPending: false, error: null }),
}));

jest.mock('react-router', () => ({ useNavigate: () => jest.fn() }));

jest.mock('@/modules/documents/document', () => ({ Document: () => null }));

const mockCreatePlaybookExtensions =
  jest.fn<AnyExtension[], [PlaybookExtensionsOptions]>(() => []);
jest.mock('@/modules/documents/playbook/playbookExtensions', () => ({
  createPlaybookExtensions: (options: PlaybookExtensionsOptions) =>
    mockCreatePlaybookExtensions(options),
  createPlaybookSlashCommands: () => [],
}));

const mockUseSubagentCreation = jest.fn<
  { onCreateSubagent: () => void; subagentDialog: null },
  [Parameters<typeof useSubagentCreation>[0]]
>(() => ({ onCreateSubagent: jest.fn(), subagentDialog: null }));
jest.mock('@/modules/documents/playbook/useSubagentCreation', () => ({
  useSubagentCreation: (options: Parameters<typeof useSubagentCreation>[0]) =>
    mockUseSubagentCreation(options),
}));

jest.mock('@/modules/documents/playbook/useDocumentCreation', () => ({
  useDocumentCreation: () => ({ onCreateDocument: jest.fn(), documentDialog: null }),
}));

import { PlaybookDocument } from '@/modules/documents/playbook/PlaybookDocument';
import type { AnyExtension } from '@tiptap/core';
import type { PlaybookExtensionsOptions } from '@/modules/documents/playbook/playbookExtensions';
import type { useSubagentCreation } from '@/modules/documents/playbook/useSubagentCreation';


/** The three scoped call sites, as the owner each of them actually received. */
async function ownersUsedBy(ownerUserId?: string) {
  mockCreatePlaybookExtensions.mockClear();
  mockUseSubagentCreation.mockClear();
  mockGetDocQuery.mockClear();

  render(
    <PlaybookDocument aop="aop_1" documentId="doc_playbook" ownerUserId={ownerUserId} />,
  );

  const extensionOptions = mockCreatePlaybookExtensions.mock.calls.at(-1)?.[0];
  const subagentOptions = mockUseSubagentCreation.mock.calls.at(-1)?.[0];
  if (!extensionOptions || !subagentOptions) {
    throw new Error('the document rendered without building its extensions');
  }

  // Follow an `@` reference. The lookup is by PATH, so it is the scope that
  // decides whose copy of the resource opens.
  await extensionOptions.onOpenReference?.('resources/email-style');
  await waitFor(() => expect(mockGetDocQuery).toHaveBeenCalled());
  const lookupInput = mockGetDocQuery.mock.calls.at(-1)?.[0];
  if (!lookupInput) throw new Error('the reference lookup ran with no input');

  return {
    extensions: extensionOptions.ownerUserId,
    subagent: subagentOptions.ownerUserId,
    referenceLookup: lookupInput.targetUserId,
  };
}

describe('PlaybookDocument owner precedence', () => {
  it('uses the document owner, not the picker, when the caller supplies one', async () => {
    expect(await ownersUsedBy(DOCUMENT_OWNER)).toEqual({
      extensions: DOCUMENT_OWNER,
      subagent: DOCUMENT_OWNER,
      referenceLookup: DOCUMENT_OWNER,
    });
  });

  it('falls back to the picker when no owner is passed, so administered surfaces still scope', async () => {
    expect(await ownersUsedBy(undefined)).toEqual({
      extensions: PICKER_USER,
      subagent: PICKER_USER,
      referenceLookup: PICKER_USER,
    });
  });
});