@startsimpli/integrations 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@startsimpli/integrations",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Provider-agnostic types, API clients, hooks, and UI primitives for email + calendar integrations across StartSimpli apps.",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -0,0 +1,196 @@
1
+ /**
2
+ * What the corpus surfaces SAY, as opposed to what they fetch.
3
+ *
4
+ * Three defects the pre-demo rehearsal (bd startsim-768w.38.10) put on a
5
+ * screenshot, all of them the kind that survives a green test suite because no
6
+ * assertion ever read the sentence:
7
+ *
8
+ * • Markdown rendered literally — `## Decisions` and `**$4,800**` as text
9
+ * (bd startsim-768w.38.10, MINOR);
10
+ * • `Answered by a none stub ((provider default))` — not English, and
11
+ * `(provider default)` where the model's name belongs
12
+ * (bd startsim-768w.38.21);
13
+ * • `1 more record were searched`.
14
+ */
15
+ import { describe, expect, it } from 'vitest';
16
+ import { render, screen, within } from '@testing-library/react';
17
+ import userEvent from '@testing-library/user-event';
18
+
19
+ import { Markdown, stripMarkdown } from '../components/Markdown';
20
+ import { AskPage } from '../components/pages/AskPage';
21
+ import { LibraryPage } from '../components/pages/LibraryPage';
22
+ import { configuredModel } from '../corpus/llm';
23
+ import { createFixtureCorpusApi, FIXTURE_DOCUMENTS, FIXTURE_ORG } from '../corpus/fixtures';
24
+ import type { AskResult, CorpusAskApi, SourceDocument } from '../corpus/types';
25
+
26
+ const SCOPE = { orgId: FIXTURE_ORG };
27
+
28
+ const MARKDOWN_BODY = [
29
+ '## Decisions',
30
+ '- Globex pricing agreed at **$4,800 per month**, 20 seats included.',
31
+ '- Additional seats are `$95` each per month.',
32
+ '',
33
+ 'See the [rate card](https://docs.example/rate-card) for the rest.',
34
+ ].join('\n');
35
+
36
+ function resultWith(overrides: Partial<AskResult>): AskResult {
37
+ const document = FIXTURE_DOCUMENTS[0];
38
+ return {
39
+ status: 'answered',
40
+ answer: MARKDOWN_BODY,
41
+ citations: [{ documentId: document.id, document, quote: '**SAML SSO has not shipped**' }],
42
+ considered: [{ document, score: 1, matchedTerms: ['globex'], snippet: '' }],
43
+ unresolvedCitations: [],
44
+ droppedOutOfScope: 0,
45
+ provider: { provider: 'openai', model: 'gpt-4o-mini', live: true },
46
+ ...overrides,
47
+ };
48
+ }
49
+
50
+ function askApi(result: AskResult): CorpusAskApi {
51
+ return { ask: async () => result };
52
+ }
53
+
54
+ async function ask(result: AskResult) {
55
+ const user = userEvent.setup();
56
+ render(<AskPage api={askApi(result)} scope={SCOPE} />);
57
+ await user.type(screen.getByRole('textbox'), 'What did we agree?');
58
+ await user.click(screen.getByRole('button', { name: 'Ask' }));
59
+ return screen.findByRole('region', { name: 'Answer' });
60
+ }
61
+
62
+ // ---------------------------------------------------------------------------
63
+
64
+ describe('Markdown is rendered, not printed', () => {
65
+ it('turns a heading into a heading rather than two hashes', () => {
66
+ render(<Markdown text={MARKDOWN_BODY} />);
67
+ expect(screen.getByText('Decisions')).toBeInTheDocument();
68
+ expect(screen.queryByText(/##/)).not.toBeInTheDocument();
69
+ });
70
+
71
+ it('emphasises an amount instead of wrapping it in asterisks', () => {
72
+ render(<Markdown text={MARKDOWN_BODY} />);
73
+ const strong = screen.getByText('$4,800 per month');
74
+ expect(strong.tagName).toBe('STRONG');
75
+ expect(screen.queryByText(/\*\*/)).not.toBeInTheDocument();
76
+ });
77
+
78
+ it('makes a list a list, and a link a link', () => {
79
+ render(<Markdown text={MARKDOWN_BODY} />);
80
+ expect(screen.getAllByRole('listitem')).toHaveLength(2);
81
+ expect(screen.getByRole('link', { name: 'rate card' })).toHaveAttribute(
82
+ 'href',
83
+ 'https://docs.example/rate-card',
84
+ );
85
+ });
86
+
87
+ it('keeps the label but drops the link for a scheme it does not trust', () => {
88
+ render(<Markdown text={'[click me](javascript:doSomething)'} />);
89
+ expect(screen.getByText('click me')).toBeInTheDocument();
90
+ expect(screen.queryByRole('link')).not.toBeInTheDocument();
91
+ });
92
+
93
+ it('leaves prose containing a lone asterisk exactly as written', () => {
94
+ // A false positive here silently edits somebody's contract.
95
+ render(<Markdown text={'Fees are 2.5% * the invoice total.'} />);
96
+ expect(screen.getByText('Fees are 2.5% * the invoice total.')).toBeInTheDocument();
97
+ });
98
+
99
+ it('flattens to plain text when a string is what is needed', () => {
100
+ expect(stripMarkdown('## Blocker\n- **SAML SSO** has not shipped')).toBe(
101
+ 'Blocker\nSAML SSO has not shipped',
102
+ );
103
+ });
104
+ });
105
+
106
+ describe('LibraryPage renders a document’s own Markdown', () => {
107
+ it('shows the summary as prose, not as source', async () => {
108
+ const user = userEvent.setup();
109
+ const document: SourceDocument = { ...FIXTURE_DOCUMENTS[0], body: MARKDOWN_BODY };
110
+ const api = createFixtureCorpusApi({ scope: SCOPE, documents: [document], moneyEvents: [] });
111
+ render(<LibraryPage api={api} scope={SCOPE} />);
112
+
113
+ await user.click(await screen.findByText(document.title));
114
+ const detail = await screen.findByRole('region', { name: 'Record detail' });
115
+ expect(within(detail).getByText('Decisions')).toBeInTheDocument();
116
+ expect(within(detail).getByText('$4,800 per month').tagName).toBe('STRONG');
117
+ });
118
+ });
119
+
120
+ describe('AskPage says who answered, in English', () => {
121
+ it('names the model when one answered', async () => {
122
+ const panel = await ask(resultWith({}));
123
+ expect(within(panel).getByText('Answered by openai · gpt-4o-mini')).toBeInTheDocument();
124
+ });
125
+
126
+ it('says no model is configured rather than "a none stub ((provider default))"', async () => {
127
+ const panel = await ask(
128
+ resultWith({ provider: { provider: 'none', model: '', live: false } }),
129
+ );
130
+ expect(within(panel).getByText(/No model is configured for this workspace/)).toBeInTheDocument();
131
+ // The honesty is kept: it still says nothing live ran.
132
+ expect(within(panel).getByText(/no live model was called/)).toBeInTheDocument();
133
+ expect(within(panel).queryByText(/\(\(/)).not.toBeInTheDocument();
134
+ expect(within(panel).queryByText(/none stub/)).not.toBeInTheDocument();
135
+ });
136
+
137
+ it('still calls a stub a stub', async () => {
138
+ const panel = await ask(
139
+ resultWith({ provider: { provider: 'fixture', model: 'fixture-echo', live: false } }),
140
+ );
141
+ expect(
142
+ within(panel).getByText('Answered by a fixture stub (fixture-echo) — no live model was called'),
143
+ ).toBeInTheDocument();
144
+ });
145
+
146
+ it('renders the answer’s Markdown and flattens the citation quote', async () => {
147
+ const panel = await ask(resultWith({}));
148
+ expect(within(panel).getByText('Decisions')).toBeInTheDocument();
149
+ expect(within(panel).getByText('“SAML SSO has not shipped”')).toBeInTheDocument();
150
+ });
151
+
152
+ it('agrees in number about how many records went uncited', async () => {
153
+ const document = FIXTURE_DOCUMENTS[1];
154
+ const panel = await ask(
155
+ resultWith({
156
+ considered: [
157
+ { document: FIXTURE_DOCUMENTS[0], score: 1, matchedTerms: [], snippet: '' },
158
+ { document, score: 0.5, matchedTerms: [], snippet: '' },
159
+ ],
160
+ }),
161
+ );
162
+ expect(within(panel).getByText('1 more record was searched but not cited')).toBeInTheDocument();
163
+ });
164
+
165
+ it('uses the plural when more than one was passed over', async () => {
166
+ const panel = await ask(
167
+ resultWith({
168
+ considered: [
169
+ { document: FIXTURE_DOCUMENTS[0], score: 1, matchedTerms: [], snippet: '' },
170
+ { document: FIXTURE_DOCUMENTS[1], score: 0.5, matchedTerms: [], snippet: '' },
171
+ { document: FIXTURE_DOCUMENTS[2], score: 0.4, matchedTerms: [], snippet: '' },
172
+ ],
173
+ }),
174
+ );
175
+ expect(within(panel).getByText('2 more records were searched but not cited')).toBeInTheDocument();
176
+ });
177
+ });
178
+
179
+ describe('the model named before the first call is the CONFIGURED one', () => {
180
+ it('reads the provider’s own variable', () => {
181
+ expect(configuredModel('openai', { OPENAI_MODEL: 'gpt-4o-mini' })).toBe('gpt-4o-mini');
182
+ expect(configuredModel('anthropic', { ANTHROPIC_MODEL: 'claude-x' })).toBe('claude-x');
183
+ });
184
+
185
+ it('prefers the specific variable over the generic one', () => {
186
+ expect(configuredModel('openai', { OPENAI_MODEL: 'gpt-4o-mini', LLM_MODEL: 'other' })).toBe(
187
+ 'gpt-4o-mini',
188
+ );
189
+ });
190
+
191
+ it('falls back to LLM_MODEL, and to nothing at all', () => {
192
+ expect(configuredModel('openai', { LLM_MODEL: 'gpt-5' })).toBe('gpt-5');
193
+ expect(configuredModel('openai', {})).toBe('');
194
+ expect(configuredModel('none', { OPENAI_MODEL: 'gpt-4o-mini' })).toBe('');
195
+ });
196
+ });
@@ -0,0 +1,301 @@
1
+ /**
2
+ * THE SEAM: tenant storage -> `SourceDocument` (bd startsim-768w.38.22).
3
+ *
4
+ * WHY THIS FILE EXISTS. Every other suite in this package starts from a domain
5
+ * object. That is correct for a composer — it should be handed a contract — but
6
+ * it meant nothing in the package had ever run against the shape the pipeline
7
+ * really writes. The corpus delivery lands people as typed EDGES and puts only a
8
+ * `participant_count` on the document; the mapper read an inline
9
+ * `data.participants` array that `declare_canonical()` never emits. Both halves
10
+ * were individually correct, both were tested, and the Library shipped with an
11
+ * empty Parties block on every document the pipeline had ever delivered.
12
+ *
13
+ * So the assertions below start at the WIRE — raw `Entity` rows and raw
14
+ * `Relationship` rows, through `createCorpusApi`, out as documents — and they
15
+ * are the ones that go red if the two halves ever disagree again.
16
+ */
17
+ import { describe, expect, it } from 'vitest';
18
+
19
+ import { createCorpusApi } from '../corpus/client';
20
+ import {
21
+ indexParticipants,
22
+ toMoneyEvent,
23
+ toParty,
24
+ toSourceDocument,
25
+ } from '../corpus/entities';
26
+ import type { TenantEntity, TenantRelationship } from '../corpus/entities';
27
+ import {
28
+ createFixtureCorpusHttp,
29
+ FIXTURE_ORG,
30
+ FIXTURE_SOURCE_LABELS,
31
+ FIXTURE_TENANT_DOCUMENTS,
32
+ FIXTURE_TENANT_MONEY_EVENTS,
33
+ FIXTURE_TENANT_PARTIES,
34
+ FIXTURE_TENANT_RELATIONSHIPS,
35
+ } from '../corpus/fixtures';
36
+ import { rankDocuments } from '../corpus/retrieval';
37
+
38
+ const SCOPE = { orgId: FIXTURE_ORG };
39
+
40
+ function apiOver(overrides: Parameters<typeof createFixtureCorpusHttp>[0] = {}) {
41
+ const { http, requests } = createFixtureCorpusHttp(overrides);
42
+ const api = createCorpusApi({ http, scope: SCOPE, sourceLabels: FIXTURE_SOURCE_LABELS });
43
+ return { api, requests };
44
+ }
45
+
46
+ /** The exact row the corpus delivery writes, as read back on live tandem. */
47
+ const DELIVERED_DOCUMENT: TenantEntity = {
48
+ id: '6c2f1e40-0000-4000-8000-000000000001',
49
+ entity_type: 'source_document',
50
+ external_id: 'granola:not_RHRSLnote00003',
51
+ name: 'Initech — security review',
52
+ data: {
53
+ url: 'https://notes.granola.ai/d/initech',
54
+ title: 'Initech — security review',
55
+ source: 'granola',
56
+ _origin: 'central-connector',
57
+ subtype: 'meeting',
58
+ summary: '## Blocker\n- **SAML SSO has not shipped**, and Initech will not sign without it.',
59
+ source_ref: 'not_RHRSLnote00003',
60
+ occurred_at: '2026-07-20T16:00:00Z',
61
+ participant_count: 2,
62
+ },
63
+ created_at: '2026-08-09T23:35:00Z',
64
+ };
65
+
66
+ const DELIVERED_PARTIES: TenantEntity[] = [
67
+ {
68
+ id: '6c2f1e40-0000-4000-8000-0000000000a1',
69
+ entity_type: 'party',
70
+ external_id: 'granola:priya@example.com',
71
+ name: 'Priya Nakamura',
72
+ data: {
73
+ source: 'granola', _origin: 'central-connector', subtype: 'person',
74
+ display_name: 'Priya Nakamura', email: 'priya@example.com', domain: 'example.com',
75
+ source_ref: 'priya@example.com',
76
+ },
77
+ },
78
+ {
79
+ id: '6c2f1e40-0000-4000-8000-0000000000a2',
80
+ entity_type: 'party',
81
+ external_id: 'granola:lena@example.com',
82
+ name: 'Lena Fischer',
83
+ data: {
84
+ source: 'granola', _origin: 'central-connector', subtype: 'person',
85
+ display_name: 'Lena Fischer', email: 'lena@example.com', domain: 'example.com',
86
+ source_ref: 'lena@example.com',
87
+ },
88
+ },
89
+ ];
90
+
91
+ const DELIVERED_EDGES: TenantRelationship[] = [
92
+ { id: 'e1', rel_type: 'authored_by', source: DELIVERED_DOCUMENT.id, target: DELIVERED_PARTIES[0].id },
93
+ { id: 'e2', rel_type: 'involves', source: DELIVERED_DOCUMENT.id, target: DELIVERED_PARTIES[1].id },
94
+ ];
95
+
96
+ // ---------------------------------------------------------------------------
97
+
98
+ describe('the row the pipeline actually delivers', () => {
99
+ it('has NO inline participants — only a count', () => {
100
+ // If this ever stops being true the bug class has changed, and the rest of
101
+ // this file is testing the wrong thing.
102
+ const data = DELIVERED_DOCUMENT.data as Record<string, unknown>;
103
+ expect(data.participants).toBeUndefined();
104
+ expect(data.participant_count).toBe(2);
105
+ });
106
+
107
+ it('renders its people from the EDGES', async () => {
108
+ const { api } = apiOver({
109
+ entities: [DELIVERED_DOCUMENT, ...DELIVERED_PARTIES],
110
+ relationships: DELIVERED_EDGES,
111
+ });
112
+
113
+ const page = await api.listDocuments({ page: 1 });
114
+ const document = page.results[0];
115
+
116
+ expect(document.title).toBe('Initech — security review');
117
+ expect(document.participants.map((party) => party.name)).toEqual([
118
+ 'Priya Nakamura',
119
+ 'Lena Fischer',
120
+ ]);
121
+ // The AUTHOR leads, whatever order the edges paged in.
122
+ expect(document.participants[0].email).toBe('priya@example.com');
123
+ });
124
+
125
+ it('resolves people on the DETAIL fetch too, not only in the list', async () => {
126
+ const { api } = apiOver({
127
+ entities: [DELIVERED_DOCUMENT, ...DELIVERED_PARTIES],
128
+ relationships: DELIVERED_EDGES,
129
+ });
130
+ const document = await api.getDocument(String(DELIVERED_DOCUMENT.id));
131
+ expect(document.participants.map((party) => party.name)).toContain('Lena Fischer');
132
+ });
133
+
134
+ it('keeps a canonical party readable — `display_name`, not `name`', () => {
135
+ const party = toParty(DELIVERED_PARTIES[0], SCOPE);
136
+ expect(party.name).toBe('Priya Nakamura');
137
+ expect(party.kind).toBe('person');
138
+ expect(party.source).toBe('granola');
139
+ });
140
+ });
141
+
142
+ describe('both storage shapes, because both are real', () => {
143
+ it('still reads an inline participants array', () => {
144
+ const inline: TenantEntity = {
145
+ id: 'doc_inline',
146
+ entity_type: 'source_document',
147
+ data: {
148
+ source: 'upload', title: 'Hand-entered note',
149
+ participants: [{ id: 'p1', name: 'Ada Byron', email: 'ada@example.com' }],
150
+ },
151
+ };
152
+ expect(toSourceDocument(inline, SCOPE).participants.map((p) => p.name)).toEqual(['Ada Byron']);
153
+ });
154
+
155
+ it('unions the two without listing anyone twice', () => {
156
+ const both: TenantEntity = {
157
+ id: 'doc_both',
158
+ entity_type: 'source_document',
159
+ data: { source: 'upload', participants: [{ id: 'party_dana', name: 'Dana Reed' }] },
160
+ };
161
+ const merged = toSourceDocument(both, SCOPE, {}, [
162
+ { id: 'party_dana', name: 'Dana Reed', kind: 'person' },
163
+ { id: 'party_sam', name: 'Sam Ortiz', kind: 'person' },
164
+ ]);
165
+ expect(merged.participants.map((party) => party.name)).toEqual(['Dana Reed', 'Sam Ortiz']);
166
+ });
167
+
168
+ it('never puts a DOCUMENT in the parties list — `derived_from` is not a person', () => {
169
+ const byRecord = indexParticipants(FIXTURE_TENANT_RELATIONSHIPS, [
170
+ { id: 'party_dana', name: 'Dana Reed' },
171
+ ]);
172
+ // `not_7Kq2ZbVnRt4aXm` is the TARGET of a derived_from edge from doc one.
173
+ expect(byRecord.get('not_1d3tmYTlCICgjy')?.map((p) => p.id)).toEqual(['party_dana']);
174
+ });
175
+
176
+ it('drops an edge whose far end is not a party it can name', () => {
177
+ const byRecord = indexParticipants(
178
+ [{ rel_type: 'involves', source: 'doc_1', target: 'party_nobody' }],
179
+ [{ id: 'party_dana', name: 'Dana Reed' }],
180
+ );
181
+ expect(byRecord.size).toBe(0);
182
+ });
183
+
184
+ it('reads a money event’s party from its `billed_to` edge', async () => {
185
+ const { api } = apiOver();
186
+ const page = await api.listMoneyEvents!({ page: 1 });
187
+ const subscription = page.results.find((event) => event.id === 'sub_RedSuite01Acme')!;
188
+ expect(subscription.party?.name).toBe('Acme Robotics');
189
+ // …and the amount is still MINOR units off the canonical attribute.
190
+ expect(subscription.amount).toBe(400000);
191
+ });
192
+ });
193
+
194
+ describe('the graph read is bounded, cached and fails soft', () => {
195
+ it('costs ONE graph read across a list, a detail and a second page', async () => {
196
+ const { api, requests } = apiOver();
197
+ await api.listDocuments({ page: 1 });
198
+ await api.listDocuments({ page: 1 });
199
+ await api.getDocument('dbx_9812f0');
200
+
201
+ const relationshipReads = requests.filter((request) => request.path.includes('relationships'));
202
+ expect(relationshipReads).toHaveLength(1);
203
+ });
204
+
205
+ it('does not read the graph at all for an EMPTY corpus', async () => {
206
+ const { api, requests } = apiOver({ entities: [], relationships: [] });
207
+ const page = await api.listDocuments({ page: 1 });
208
+
209
+ expect(page.results).toEqual([]);
210
+ expect(requests.filter((request) => request.path.includes('relationships'))).toHaveLength(0);
211
+ });
212
+
213
+ it('still renders the corpus when the relationships endpoint is missing', async () => {
214
+ // A tenant one deploy behind must degrade to what it showed before this
215
+ // existed — not to an error where a Library used to be.
216
+ const { api } = apiOver({ missing: ['relationships'] });
217
+ const page = await api.listDocuments({ page: 1 });
218
+
219
+ expect(page.results.length).toBeGreaterThan(0);
220
+ expect(page.results.every((document) => document.participants.length === 0)).toBe(true);
221
+ });
222
+
223
+ it('walks every page of the graph, since the tenant ignores page_size', async () => {
224
+ // Two rows a page: the edges for the last document are only reachable by
225
+ // paging, which is exactly what the real PAGE_SIZE=50 does at scale.
226
+ const { api, requests } = apiOver({ pageSize: 2 });
227
+ // `pageSize: 100` is ignored by the server, exactly as the tenant ignores
228
+ // it — so the Dropbox row is on page THREE however wide the caller asked.
229
+ const page = await api.listDocuments({ page: 3, pageSize: 100 });
230
+ const dropbox = page.results.find((document) => document.id === 'dbx_9812f0');
231
+
232
+ expect(dropbox?.participants.map((party) => party.name)).toEqual(['Acme Robotics', 'Dana Reed']);
233
+ expect(requests.filter((request) => request.path.includes('relationships')).length).toBeGreaterThan(1);
234
+ });
235
+ });
236
+
237
+ describe('retrieval scores the people, not an empty string', () => {
238
+ it('finds a meeting by an attendee’s name that appears nowhere in its text', () => {
239
+ // `FIELD_WEIGHTS.participants` is 2 — second only to title. Before the edge
240
+ // join it scored an empty string on every row the pipeline delivered, so a
241
+ // question naming a person only landed if the prose happened to repeat it.
242
+ const documents = [
243
+ toSourceDocument(
244
+ DELIVERED_DOCUMENT,
245
+ SCOPE,
246
+ FIXTURE_SOURCE_LABELS,
247
+ indexParticipants(DELIVERED_EDGES, DELIVERED_PARTIES.map((row) => toParty(row, SCOPE))).get(
248
+ String(DELIVERED_DOCUMENT.id),
249
+ ) ?? [],
250
+ ),
251
+ toSourceDocument(FIXTURE_TENANT_DOCUMENTS[1], SCOPE, FIXTURE_SOURCE_LABELS),
252
+ ];
253
+ expect(documents[0].body).not.toContain('Lena');
254
+
255
+ const ranked = rankDocuments(documents, 'What did Lena Fischer raise?', { limit: 2 });
256
+ expect(ranked[0]?.document.id).toBe(String(DELIVERED_DOCUMENT.id));
257
+ expect(ranked[0]?.matchedTerms).toContain('lena');
258
+ });
259
+ });
260
+
261
+ describe('the fixtures are the pipeline’s shape, not a friendlier one', () => {
262
+ it('holds no inline participants anywhere in the raw rows', () => {
263
+ for (const entity of [...FIXTURE_TENANT_DOCUMENTS, ...FIXTURE_TENANT_MONEY_EVENTS]) {
264
+ const data = (entity.data ?? {}) as Record<string, unknown>;
265
+ expect(data.participants).toBeUndefined();
266
+ expect(data.attendees).toBeUndefined();
267
+ expect(data.party).toBeUndefined();
268
+ }
269
+ });
270
+
271
+ it('namespaces every external_id as `<connector_key>:<vendor id>`', () => {
272
+ for (const entity of [...FIXTURE_TENANT_DOCUMENTS, ...FIXTURE_TENANT_PARTIES]) {
273
+ const data = (entity.data ?? {}) as Record<string, unknown>;
274
+ expect(entity.external_id).toBe(`${String(data.source)}:${String(data.source_ref)}`);
275
+ expect(data._origin).toBe('central-connector');
276
+ }
277
+ });
278
+
279
+ it('serves them through the adapter with people on every meeting', async () => {
280
+ const { api } = apiOver();
281
+ const page = await api.listDocuments({ page: 1 });
282
+ const weekly = page.results.find((document) => document.sourceRef === 'not_1d3tmYTlCICgjy')!;
283
+
284
+ expect(weekly.participants.map((party) => party.name)).toEqual([
285
+ 'Dana Reed', 'Sam Ortiz', 'Kim Li', 'Acme Robotics',
286
+ ]);
287
+ expect(weekly.sourceLabel).toBe('Granola');
288
+ });
289
+ });
290
+
291
+ describe('the tenant’s filters are still not honoured, and are still not faked', () => {
292
+ it('reports search as unapplied because the entities endpoint ignores it', async () => {
293
+ const { api, requests } = apiOver();
294
+ const page = await api.listDocuments({ search: 'SAML', page: 1 });
295
+
296
+ // The parameter IS sent — the day the API learns it, nothing here changes.
297
+ expect(requests[0].params.search).toBe('SAML');
298
+ expect(page.appliedFilters).toEqual([]);
299
+ expect(page.count).toBe(FIXTURE_TENANT_DOCUMENTS.length);
300
+ });
301
+ });