@startsimpli/integrations 0.1.4 → 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.
@@ -0,0 +1,216 @@
1
+ /**
2
+ * SCOPE ISOLATION — the test bd startsim-768w.38.7 asks for by name.
3
+ *
4
+ * "An AI that retrieves across a scope boundary leaks data in the most
5
+ * convincing possible format. Write a deliberate test for this — do not just
6
+ * assume the query layer handles it."
7
+ *
8
+ * So this file assumes the query layer DOES NOT handle it. Every case below runs
9
+ * against a corpus API deliberately configured to leak (`leakScope: true`),
10
+ * which is what a mis-scoped serializer, a forgotten `org_id` filter or a
11
+ * cached response would look like from the client's side. If the pipeline is
12
+ * only safe because the server is, these tests fail — which is the point.
13
+ *
14
+ * The bait is `nw_contract_44` in `FIXTURE_OTHER_ORG`: a document that answers
15
+ * the tandem question BETTER than anything tandem owns. A ranker with no guard
16
+ * puts it first.
17
+ */
18
+ import { describe, expect, it, vi } from 'vitest';
19
+
20
+ import { askCorpus } from '../corpus/ask';
21
+ import {
22
+ createFixtureCorpusApi,
23
+ createFixtureLlm,
24
+ FIXTURE_DOCUMENTS,
25
+ FIXTURE_ORG,
26
+ FIXTURE_OTHER_ORG,
27
+ } from '../corpus/fixtures';
28
+ import { enforceScope, isInScope, rankDocuments, retrieveForQuestion } from '../corpus/retrieval';
29
+ import type { AskRequest, CorpusLlm, SourceDocument } from '../corpus/types';
30
+
31
+ const TANDEM = { orgId: FIXTURE_ORG };
32
+ const NORTHWIND = { orgId: FIXTURE_OTHER_ORG };
33
+
34
+ /** The question whose most attractive answer lives in the OTHER workspace. */
35
+ const BAIT = 'What did we agree with Acme Robotics on renewal rate and seats?';
36
+
37
+ function foreignDocuments(documents: SourceDocument[], orgId: string) {
38
+ return documents.filter((document) => document.scope.orgId !== orgId);
39
+ }
40
+
41
+ describe('isInScope — the boundary itself', () => {
42
+ const row = { scope: { orgId: 'tandem', ownerSub: 'usr_dana' } };
43
+
44
+ it('admits a row from the same workspace', () => {
45
+ expect(isInScope(row, { orgId: 'tandem' })).toBe(true);
46
+ });
47
+
48
+ it('refuses a row from another workspace', () => {
49
+ expect(isInScope(row, { orgId: 'northwind' })).toBe(false);
50
+ });
51
+
52
+ it('refuses everything when the scope carries no org claim', () => {
53
+ // A principal with no org_id must read NOTHING, not everything. This is the
54
+ // failure mode where a missing claim reads as "unscoped" and returns the world.
55
+ expect(isInScope(row, { orgId: '' })).toBe(false);
56
+ });
57
+
58
+ it('enforces the per-row owner only when the caller asked for it', () => {
59
+ expect(isInScope(row, { orgId: 'tandem', ownerSub: 'usr_dana' })).toBe(true);
60
+ expect(isInScope(row, { orgId: 'tandem', ownerSub: 'usr_sam' })).toBe(false);
61
+ // Not asking for an owner is the tenant API's real posture (org + readable
62
+ // type), so the guard must not silently claim a narrower one.
63
+ expect(isInScope(row, { orgId: 'tandem' })).toBe(true);
64
+ });
65
+
66
+ it('refuses a row whose org is a PREFIX of the caller’s, not a match', () => {
67
+ // Guards written with startsWith leak between `acme` and `acme-eu`.
68
+ expect(isInScope({ scope: { orgId: 'tandem-eu' } }, { orgId: 'tandem' })).toBe(false);
69
+ expect(isInScope({ scope: { orgId: 'tandem' } }, { orgId: 'tandem-eu' })).toBe(false);
70
+ });
71
+ });
72
+
73
+ describe('enforceScope — nothing is dropped silently', () => {
74
+ it('partitions a mixed set and reports what it removed', () => {
75
+ const { kept, dropped } = enforceScope(FIXTURE_DOCUMENTS, TANDEM);
76
+ expect(kept.length + dropped.length).toBe(FIXTURE_DOCUMENTS.length);
77
+ expect(dropped.length).toBe(foreignDocuments(FIXTURE_DOCUMENTS, FIXTURE_ORG).length);
78
+ expect(dropped.every((document) => document.scope.orgId === FIXTURE_OTHER_ORG)).toBe(true);
79
+ });
80
+ });
81
+
82
+ describe('retrieval refuses the bait', () => {
83
+ it('ranks the foreign document FIRST when the guard is removed', () => {
84
+ // Establishes that the guard is doing real work: without it, the leak wins.
85
+ const unguarded = rankDocuments(FIXTURE_DOCUMENTS, BAIT, { limit: 5 });
86
+ expect(unguarded[0]?.document.id).toBe('nw_contract_44');
87
+ });
88
+
89
+ it('never returns it once the guard runs', () => {
90
+ const { retrieved, droppedOutOfScope } = retrieveForQuestion(FIXTURE_DOCUMENTS, BAIT, TANDEM, {
91
+ limit: 5,
92
+ });
93
+ expect(retrieved.length).toBeGreaterThan(0);
94
+ expect(retrieved.map((item) => item.document.id)).not.toContain('nw_contract_44');
95
+ expect(retrieved.every((item) => item.document.scope.orgId === FIXTURE_ORG)).toBe(true);
96
+ expect(droppedOutOfScope).toBeGreaterThan(0);
97
+ });
98
+ });
99
+
100
+ describe('askCorpus — the leak never reaches the model', () => {
101
+ /** A model that records exactly what it was shown. */
102
+ function spyLlm(): { llm: CorpusLlm; seen: string[][] } {
103
+ const seen: string[][] = [];
104
+ const inner = createFixtureLlm();
105
+ return {
106
+ seen,
107
+ llm: {
108
+ describe: inner.describe,
109
+ async answer(input) {
110
+ seen.push(input.documents.map((document) => document.id));
111
+ return inner.answer(input);
112
+ },
113
+ },
114
+ };
115
+ }
116
+
117
+ const request = (question = BAIT): AskRequest => ({ question, mode: 'ask', scope: TANDEM });
118
+
119
+ it('does not put a foreign document in the prompt even when the API returns one', async () => {
120
+ const corpus = createFixtureCorpusApi({ scope: TANDEM, leakScope: true });
121
+ const { llm, seen } = spyLlm();
122
+
123
+ const result = await askCorpus(request(), { corpus, llm });
124
+
125
+ expect(seen).toHaveLength(1); // ONE call, not one per document
126
+ expect(seen[0]).not.toContain('nw_contract_44');
127
+ expect(result.droppedOutOfScope).toBeGreaterThan(0);
128
+ expect(result.citations.every((citation) => citation.document.scope.orgId === FIXTURE_ORG)).toBe(true);
129
+ });
130
+
131
+ it('never cites a foreign document, and says the API returned some', async () => {
132
+ const corpus = createFixtureCorpusApi({ scope: TANDEM, leakScope: true });
133
+ const result = await askCorpus(request(), { corpus, llm: createFixtureLlm() });
134
+
135
+ expect(result.answer).not.toContain('Northwind');
136
+ expect(result.answer).not.toContain('2,750');
137
+ expect(result.citations.map((citation) => citation.documentId)).not.toContain('nw_contract_44');
138
+ // The count is surfaced so a leaking API is visible rather than absorbed.
139
+ expect(result.droppedOutOfScope).toBeGreaterThan(0);
140
+ });
141
+
142
+ it('answers a cross-scope question with NOTHING rather than someone else’s records', async () => {
143
+ // "Northwind" is the one term in the fixture set that appears ONLY in the
144
+ // other workspace's document, so a question built on it has no honest answer
145
+ // inside tandem. The wrong outcome here is not an error — it is a fluent,
146
+ // confident, correctly-formatted answer sourced from another customer.
147
+ const corpus = createFixtureCorpusApi({ scope: TANDEM, leakScope: true });
148
+ const result = await askCorpus(
149
+ { question: 'Northwind', mode: 'ask', scope: TANDEM },
150
+ { corpus, llm: createFixtureLlm() },
151
+ );
152
+
153
+ expect(result.status).toBe('no_evidence');
154
+ expect(result.citations).toHaveLength(0);
155
+ expect(result.considered).toHaveLength(0);
156
+ expect(result.answer).not.toMatch(/2,?750|unlimited/i);
157
+ });
158
+
159
+ it('answers a shared-term question from its OWN records, never the other workspace’s', async () => {
160
+ // The subtler half: the question is answerable in both workspaces. Silence
161
+ // would be over-blocking; the requirement is that the answer is tandem's.
162
+ const corpus = createFixtureCorpusApi({ scope: TANDEM, leakScope: true });
163
+ const result = await askCorpus(
164
+ { question: 'What notice period do we have for termination for convenience?', mode: 'ask', scope: TANDEM },
165
+ { corpus, llm: createFixtureLlm() },
166
+ );
167
+
168
+ expect(result.status).toBe('answered');
169
+ expect(result.citations.length).toBeGreaterThan(0);
170
+ expect(result.citations.every((citation) => citation.document.scope.orgId === FIXTURE_ORG)).toBe(true);
171
+ expect(result.considered.every((item) => item.document.scope.orgId === FIXTURE_ORG)).toBe(true);
172
+ expect(result.answer).not.toMatch(/2,?750|unlimited|Northwind/i);
173
+ });
174
+
175
+ it('gives the OTHER workspace its own answer — isolation, not blanket denial', async () => {
176
+ const corpus = createFixtureCorpusApi({ scope: NORTHWIND, leakScope: true });
177
+ const result = await askCorpus(
178
+ { question: 'What renewal rate did we agree with Acme Robotics?', mode: 'ask', scope: NORTHWIND },
179
+ { corpus, llm: createFixtureLlm() },
180
+ );
181
+
182
+ expect(result.status).toBe('answered');
183
+ expect(result.citations.map((citation) => citation.documentId)).toEqual(['nw_contract_44']);
184
+ expect(result.citations.every((citation) => citation.document.scope.orgId === FIXTURE_OTHER_ORG)).toBe(true);
185
+ });
186
+
187
+ it('refuses outright when the session carries no workspace claim', async () => {
188
+ const corpus = createFixtureCorpusApi({ leakScope: true });
189
+ const listDocuments = vi.spyOn(corpus, 'listDocuments');
190
+
191
+ const result = await askCorpus(
192
+ { question: BAIT, mode: 'ask', scope: { orgId: '' } },
193
+ { corpus, llm: createFixtureLlm() },
194
+ );
195
+
196
+ expect(result.status).toBe('no_evidence');
197
+ expect(result.citations).toHaveLength(0);
198
+ // It does not even ASK — an unscoped read is not made and then filtered.
199
+ expect(listDocuments).not.toHaveBeenCalled();
200
+ });
201
+
202
+ it('honours a per-owner scope when one is supplied', async () => {
203
+ const corpus = createFixtureCorpusApi({ scope: TANDEM, leakScope: true });
204
+ const result = await askCorpus(
205
+ {
206
+ question: 'ad-hoc call with no calendar invite',
207
+ mode: 'ask',
208
+ scope: { orgId: FIXTURE_ORG, ownerSub: 'usr_dana' },
209
+ },
210
+ { corpus, llm: createFixtureLlm() },
211
+ );
212
+
213
+ // `not_Pd9wLs3EhY6cQz` is owned by usr_sam and is the only match.
214
+ expect(result.citations.map((citation) => citation.documentId)).not.toContain('not_Pd9wLs3EhY6cQz');
215
+ });
216
+ });
@@ -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
+ });