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,
});
});
});