@startsimpli/integrations 0.1.5 → 0.2.1

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,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
+ });
@@ -0,0 +1,250 @@
1
+ /**
2
+ * THE TRACKING SURFACE, rendered (bd startsim-768w.38.25).
3
+ *
4
+ * The screen makes one claim — "this is what is still owed" — and every case
5
+ * below is a way that claim could be false on screen while the data underneath
6
+ * is fine: a closed item shown as outstanding, an item with no owner rendered
7
+ * as though it had one, a filter that did nothing, a row with no traceable
8
+ * source, a run that closed something and did not say why.
9
+ */
10
+ import { render, screen, within } from '@testing-library/react';
11
+ import userEvent from '@testing-library/user-event';
12
+ import { describe, expect, it, vi } from 'vitest';
13
+
14
+ import { TrackerPage } from '../components/pages/TrackerPage';
15
+ import type {
16
+ Commitment,
17
+ CommitmentQuery,
18
+ CommitmentTrackerApi,
19
+ CorpusPage,
20
+ } from '../corpus/types';
21
+
22
+ const NOW = Date.parse('2026-08-10T12:00:00Z');
23
+ const SCOPE = { orgId: 'tandem' };
24
+
25
+ function commitment(overrides: Partial<Commitment> & { externalId: string; title: string }): Commitment {
26
+ return {
27
+ id: overrides.externalId,
28
+ dedupeKey: overrides.externalId.replace('commit:', ''),
29
+ subtype: 'action',
30
+ detail: '',
31
+ status: 'open',
32
+ subjectKey: '',
33
+ actionKey: '',
34
+ mentions: [
35
+ {
36
+ documentId: 'doc_1',
37
+ originKey: 'rules:x',
38
+ occurredAt: '2026-08-01T15:00:00Z',
39
+ quote: 'Status in the system reads "comments received", which is misleading.',
40
+ extractor: 'rules',
41
+ },
42
+ ],
43
+ firstSeenAt: '2026-08-01T15:00:00Z',
44
+ lastSeenAt: '2026-08-01T15:00:00Z',
45
+ extractorVersion: 'rules+model@1',
46
+ source: 'granola',
47
+ sourceLabel: 'Granola',
48
+ scope: SCOPE,
49
+ ...overrides,
50
+ };
51
+ }
52
+
53
+ const ROWS: Commitment[] = [
54
+ commitment({ externalId: 'commit:open1', title: 'Chase the inventor for comments on 2148', ownerName: 'Rae' }),
55
+ commitment({
56
+ externalId: 'commit:overdue1',
57
+ title: 'Send the equity sheet to Harry Holcomb',
58
+ dueAt: '2026-08-05T00:00:00Z',
59
+ }),
60
+ commitment({
61
+ externalId: 'commit:done1',
62
+ title: 'File the PCT for 2205',
63
+ status: 'done',
64
+ statusReason: '2205: filed as first PCT on June 25; already complete',
65
+ resolvedBy: 'doc_9',
66
+ resolvedAt: '2026-08-08T00:00:00Z',
67
+ lastSeenAt: '2026-08-08T00:00:00Z',
68
+ }),
69
+ ];
70
+
71
+ function makeApi(options: {
72
+ rows?: Commitment[];
73
+ appliedFilters?: string[];
74
+ onQuery?: (query: CommitmentQuery) => void;
75
+ extract?: CommitmentTrackerApi['extract'];
76
+ } = {}): CommitmentTrackerApi {
77
+ const rows = options.rows ?? ROWS;
78
+ return {
79
+ async listCommitments(query: CommitmentQuery = {}): Promise<CorpusPage<Commitment>> {
80
+ options.onQuery?.(query);
81
+ // Deliberately returns EVERYTHING, like the tenant API does today: the
82
+ // screen has to be correct against a server that ignores the filters.
83
+ return {
84
+ count: rows.length,
85
+ next: null,
86
+ previous: null,
87
+ results: rows,
88
+ appliedFilters: options.appliedFilters ?? [],
89
+ };
90
+ },
91
+ ...(options.extract ? { extract: options.extract } : {}),
92
+ };
93
+ }
94
+
95
+ describe('TrackerPage — what is outstanding', () => {
96
+ it('shows open items and hides closed ones', async () => {
97
+ render(<TrackerPage api={makeApi()} scope={SCOPE} now={NOW} />);
98
+
99
+ expect(await screen.findByText('Chase the inventor for comments on 2148')).toBeInTheDocument();
100
+ expect(screen.getByText('Send the equity sheet to Harry Holcomb')).toBeInTheDocument();
101
+ // The one thing this screen must never do: present finished work as owed.
102
+ expect(screen.queryByText('File the PCT for 2205')).not.toBeInTheDocument();
103
+ });
104
+
105
+ it('sends the status and window as SERVER parameters', async () => {
106
+ const queries: CommitmentQuery[] = [];
107
+ render(<TrackerPage api={makeApi({ onQuery: (query) => queries.push(query) })} scope={SCOPE} now={NOW} />);
108
+ await screen.findByText('Chase the inventor for comments on 2148');
109
+
110
+ expect(queries[0].status).toEqual(['open', 'blocked']);
111
+ expect(queries[0].pageSize).toBe(200);
112
+ });
113
+
114
+ it('says so when the data API does not honour the filters', async () => {
115
+ render(<TrackerPage api={makeApi()} scope={SCOPE} now={NOW} />);
116
+ // The Library's `appliedFilters` contract, owed here about status — a
117
+ // status filter that silently does nothing breaks the screen's only claim.
118
+ expect(
119
+ await screen.findByText(/does not filter by .*status/i),
120
+ ).toBeInTheDocument();
121
+ });
122
+
123
+ it('does NOT nag when the API reports it applied them', async () => {
124
+ render(
125
+ <TrackerPage
126
+ api={makeApi({ appliedFilters: ['status', 'owner', 'dueBefore', 'changedAfter'] })}
127
+ scope={SCOPE}
128
+ now={NOW}
129
+ />,
130
+ );
131
+ await screen.findByText('Chase the inventor for comments on 2148');
132
+ expect(screen.queryByText(/does not filter by/i)).not.toBeInTheDocument();
133
+ });
134
+
135
+ it('puts an overdue item above one with no due date', async () => {
136
+ render(<TrackerPage api={makeApi()} scope={SCOPE} now={NOW} />);
137
+ await screen.findByText('Chase the inventor for comments on 2148');
138
+ const titles = screen.getAllByRole('article').map((node) => within(node).getByRole('heading').textContent);
139
+ expect(titles[0]).toBe('Send the equity sheet to Harry Holcomb');
140
+ });
141
+
142
+ it('says an item is overdue, with the date', async () => {
143
+ render(<TrackerPage api={makeApi()} scope={SCOPE} now={NOW} />);
144
+ expect(await screen.findByText(/Overdue/)).toBeInTheDocument();
145
+ });
146
+
147
+ it('says how long something has been open, which is the point of the screen', async () => {
148
+ render(<TrackerPage api={makeApi()} scope={SCOPE} now={NOW} />);
149
+ // Raised 2026-08-01, read 2026-08-10 — nine calendar days, eight elapsed.
150
+ expect(await screen.findAllByText(/open for 8 days/)).not.toHaveLength(0);
151
+ });
152
+
153
+ it('states that nobody was named rather than rendering a blank', async () => {
154
+ render(<TrackerPage api={makeApi()} scope={SCOPE} now={NOW} />);
155
+ // "Nobody said who owns this" is a fact about the meeting, and it is the
156
+ // most actionable thing on the screen. A blank cell hides it.
157
+ expect(await screen.findByText('No owner named')).toBeInTheDocument();
158
+ expect(screen.getByText('Rae')).toBeInTheDocument();
159
+ });
160
+
161
+ it('renders every row with the meeting it came from and a verbatim quote', async () => {
162
+ render(<TrackerPage api={makeApi()} scope={SCOPE} now={NOW} />);
163
+ await screen.findByText('Chase the inventor for comments on 2148');
164
+ for (const article of screen.getAllByRole('article')) {
165
+ expect(within(article).getByText(/From 1 meeting/)).toBeInTheDocument();
166
+ expect(within(article).getByText(/Status in the system reads/)).toBeInTheDocument();
167
+ }
168
+ });
169
+
170
+ it('shows the closed tab with the sentence that closed it', async () => {
171
+ render(<TrackerPage api={makeApi()} scope={SCOPE} now={NOW} />);
172
+ await screen.findByText('Chase the inventor for comments on 2148');
173
+ await userEvent.click(screen.getByRole('tab', { name: 'Closed' }));
174
+
175
+ expect(await screen.findByText('File the PCT for 2205')).toBeInTheDocument();
176
+ // "Why does it say this is done" has to be answerable without opening the
177
+ // meeting, or nobody trusts a single closed row.
178
+ expect(screen.getByText(/Closed by: .*filed as first PCT/)).toBeInTheDocument();
179
+ });
180
+
181
+ it('filters by owner', async () => {
182
+ render(<TrackerPage api={makeApi()} scope={SCOPE} now={NOW} />);
183
+ await screen.findByText('Chase the inventor for comments on 2148');
184
+ await userEvent.type(screen.getByPlaceholderText('Owner'), 'Rae');
185
+
186
+ expect(screen.getByText('Chase the inventor for comments on 2148')).toBeInTheDocument();
187
+ expect(screen.queryByText('Send the equity sheet to Harry Holcomb')).not.toBeInTheDocument();
188
+ });
189
+
190
+ it('tells a fresh workspace what to do instead of showing an empty page', async () => {
191
+ render(<TrackerPage api={makeApi({ rows: [] })} scope={SCOPE} now={NOW} />);
192
+ expect(await screen.findByText(/Nothing has been pulled out of your meetings yet/)).toBeInTheDocument();
193
+ });
194
+ });
195
+
196
+ describe('TrackerPage — running the extraction', () => {
197
+ const RESULT = {
198
+ documentsProcessed: 16,
199
+ fromRules: 24,
200
+ fromModel: 11,
201
+ created: 30,
202
+ merged: 5,
203
+ resolved: 2,
204
+ needsReview: [{ commitmentId: 'commit:open1', documentId: 'doc_4', quote: '2148 came back from the inventor' }],
205
+ droppedOutOfScope: 0,
206
+ provider: { provider: 'anthropic', model: 'claude-sonnet-4-6', live: true },
207
+ };
208
+
209
+ it('reports what the run did, and what the MODEL added over the rules', async () => {
210
+ const extract = vi.fn(async () => RESULT);
211
+ render(<TrackerPage api={makeApi({ extract })} scope={SCOPE} now={NOW} />);
212
+ await screen.findByText('Chase the inventor for comments on 2148');
213
+ await userEvent.click(screen.getByRole('button', { name: 'Pull from meetings' }));
214
+
215
+ expect(extract).toHaveBeenCalledWith({ scope: SCOPE });
216
+ expect(await screen.findByText(/Read 16 meetings/)).toBeInTheDocument();
217
+ // The measurement of whether the model call is worth paying for, on screen.
218
+ expect(screen.getByText(/24 came from the meetings’ own next-step lists, 11 were read/)).toBeInTheDocument();
219
+ expect(screen.getByText(/anthropic · claude-sonnet-4-6/)).toBeInTheDocument();
220
+ });
221
+
222
+ it('surfaces weak resolution matches instead of quietly closing things', async () => {
223
+ render(<TrackerPage api={makeApi({ extract: async () => RESULT })} scope={SCOPE} now={NOW} />);
224
+ await screen.findByText('Chase the inventor for comments on 2148');
225
+ await userEvent.click(screen.getByRole('button', { name: 'Pull from meetings' }));
226
+
227
+ expect(await screen.findByText(/Nothing was closed automatically/)).toBeInTheDocument();
228
+ expect(screen.getByText(/2148 came back from the inventor/)).toBeInTheDocument();
229
+ });
230
+
231
+ it('reports a leaking data API rather than absorbing it', async () => {
232
+ render(
233
+ <TrackerPage
234
+ api={makeApi({ extract: async () => ({ ...RESULT, droppedOutOfScope: 3 }) })}
235
+ scope={SCOPE}
236
+ now={NOW}
237
+ />,
238
+ );
239
+ await screen.findByText('Chase the inventor for comments on 2148');
240
+ await userEvent.click(screen.getByRole('button', { name: 'Pull from meetings' }));
241
+
242
+ expect(await screen.findByText(/3 records from another\s+workspace were returned/)).toBeInTheDocument();
243
+ });
244
+
245
+ it('hides the control entirely on a read-only deployment', async () => {
246
+ render(<TrackerPage api={makeApi()} scope={SCOPE} now={NOW} />);
247
+ await screen.findByText('Chase the inventor for comments on 2148');
248
+ expect(screen.queryByRole('button', { name: 'Pull from meetings' })).not.toBeInTheDocument();
249
+ });
250
+ });