@startsimpli/integrations 0.1.4 → 0.1.5

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,479 @@
1
+ /**
2
+ * The two corpus composers, rendered (bd startsim-768w.38.6, startsim-768w.38.7).
3
+ *
4
+ * THE RULE THIS SUITE ENFORCES, and it is the same rule the connections suite
5
+ * enforces one level down: no per-vendor anything. So the central case here runs
6
+ * the whole Library against a corpus whose ONLY connected source is Dropbox and
7
+ * asserts the screen is just as complete — badge, filter, detail, long-form
8
+ * section, freshness, empty states. A screen that renders meeting notes well and
9
+ * files badly is a failed design even though it would demo fine on Monday.
10
+ *
11
+ * The second rule is CLAUDE.md rule 8: every filter and every page turn is a
12
+ * SERVER round trip. The fixture api records the queries it was handed, so
13
+ * "the browser filtered it" is a detectable, and failing, outcome.
14
+ */
15
+ import { describe, expect, it, vi } from 'vitest';
16
+ import { render, screen, waitFor, within } from '@testing-library/react';
17
+ import userEvent from '@testing-library/user-event';
18
+
19
+ import { AskPage } from '../components/pages/AskPage';
20
+ import { LibraryPage } from '../components/pages/LibraryPage';
21
+ import { askCorpus } from '../corpus/ask';
22
+ import {
23
+ createFixtureCorpusApi,
24
+ createFixtureLlm,
25
+ FIXTURE_DOCUMENTS,
26
+ FIXTURE_ORG,
27
+ } from '../corpus/fixtures';
28
+ import type {
29
+ AskRequest,
30
+ CorpusApi,
31
+ CorpusAskApi,
32
+ DocumentQuery,
33
+ SourceDocument,
34
+ SourceFreshness,
35
+ } from '../corpus/types';
36
+
37
+ const SCOPE = { orgId: FIXTURE_ORG };
38
+
39
+ function ownDocuments(): SourceDocument[] {
40
+ return FIXTURE_DOCUMENTS.filter((document) => document.scope.orgId === FIXTURE_ORG);
41
+ }
42
+
43
+ /** A corpus with exactly ONE connected source, whichever one is named. */
44
+ function singleSourceCorpus(source: string, freshness: SourceFreshness) {
45
+ const documents = ownDocuments().filter((document) => document.source === source);
46
+ const queries: Array<[string, unknown]> = [];
47
+ const api = createFixtureCorpusApi({
48
+ scope: SCOPE,
49
+ documents,
50
+ moneyEvents: [],
51
+ sources: [freshness],
52
+ defaultPageSize: 20,
53
+ onQuery: (endpoint, query) => queries.push([endpoint, query]),
54
+ });
55
+ return { api, documents, queries };
56
+ }
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // THE GENERIC TEST
60
+ // ---------------------------------------------------------------------------
61
+
62
+ describe('LibraryPage — the only connected source is Dropbox', () => {
63
+ const freshness: SourceFreshness = {
64
+ source: 'dropbox',
65
+ label: 'Dropbox',
66
+ documentCount: 1,
67
+ lastSyncedAt: '2026-08-02T06:00:00Z',
68
+ status: 'stale',
69
+ detail: 'Last sync was more than 26 hours ago.',
70
+ };
71
+
72
+ it('renders a complete screen with no meeting anywhere in it', async () => {
73
+ const { api } = singleSourceCorpus('dropbox', freshness);
74
+ render(<LibraryPage api={api} scope={SCOPE} />);
75
+
76
+ // A row, with its source as a visible badge.
77
+ expect(await screen.findByText(/Master Services Agreement/)).toBeInTheDocument();
78
+ expect(screen.getAllByText('Dropbox').length).toBeGreaterThan(0);
79
+
80
+ // The source filter is DATA — Dropbox is an option because it exists.
81
+ expect(screen.getByRole('option', { name: 'Dropbox' })).toBeInTheDocument();
82
+
83
+ // Freshness is on the screen, and says the uncomfortable thing.
84
+ expect(screen.getByText('Stale')).toBeInTheDocument();
85
+ expect(screen.getByText(/more than 26 hours ago/)).toBeInTheDocument();
86
+
87
+ // Nothing in the chrome assumes a meeting.
88
+ expect(screen.queryByText(/transcript/i)).not.toBeInTheDocument();
89
+ expect(screen.queryByText(/attendee/i)).not.toBeInTheDocument();
90
+ expect(screen.queryByText(/meeting/i)).not.toBeInTheDocument();
91
+ });
92
+
93
+ it('opens a file’s detail with its long-form text under the SOURCE’s own label', async () => {
94
+ const user = userEvent.setup();
95
+ const { api } = singleSourceCorpus('dropbox', freshness);
96
+ render(<LibraryPage api={api} scope={SCOPE} />);
97
+
98
+ await user.click(await screen.findByText(/Master Services Agreement/));
99
+
100
+ const detail = await screen.findByRole('region', { name: 'Record detail' });
101
+ // "Extracted text" for a PDF, exactly where "Transcript" sits for a meeting.
102
+ // The composer never learned which is which.
103
+ expect(within(detail).getByText('Extracted text')).toBeInTheDocument();
104
+ expect(within(detail).getByText(/Open in Dropbox/)).toBeInTheDocument();
105
+ expect(within(detail).getByRole('button', { name: /Acme Robotics/ })).toBeInTheDocument();
106
+ });
107
+ });
108
+
109
+ describe('LibraryPage — the same screen, Granola only', () => {
110
+ it('renders long-form under Granola’s word for it, from the same code', async () => {
111
+ const user = userEvent.setup();
112
+ const { api } = singleSourceCorpus('granola', {
113
+ source: 'granola', label: 'Granola', documentCount: 4,
114
+ lastSyncedAt: '2026-08-07T06:00:00Z', status: 'ok',
115
+ });
116
+ render(<LibraryPage api={api} scope={SCOPE} />);
117
+
118
+ await user.click(await screen.findByText('Acme <> StartSimpli weekly sync'));
119
+ const detail = await screen.findByRole('region', { name: 'Record detail' });
120
+ expect(within(detail).getByText('Transcript')).toBeInTheDocument();
121
+ });
122
+ });
123
+
124
+ // ---------------------------------------------------------------------------
125
+ // server-side everything
126
+ // ---------------------------------------------------------------------------
127
+
128
+ describe('LibraryPage — filtering and paging are SERVER round trips', () => {
129
+ it('sends the search term to the server instead of filtering the page', async () => {
130
+ const user = userEvent.setup();
131
+ const queries: DocumentQuery[] = [];
132
+ const api = createFixtureCorpusApi({
133
+ scope: SCOPE,
134
+ onQuery: (endpoint, query) => { if (endpoint === 'documents') queries.push(query as DocumentQuery); },
135
+ });
136
+ render(<LibraryPage api={api} scope={SCOPE} />);
137
+
138
+ await screen.findByText('Library');
139
+ await user.type(screen.getByRole('searchbox'), 'renewal');
140
+ await user.click(screen.getByRole('button', { name: 'Search' }));
141
+
142
+ await waitFor(() => expect(queries.some((query) => query.search === 'renewal')).toBe(true));
143
+ });
144
+
145
+ it('asks the server for the next page rather than slicing a loaded list', async () => {
146
+ const user = userEvent.setup();
147
+ const queries: DocumentQuery[] = [];
148
+ const api = createFixtureCorpusApi({
149
+ scope: SCOPE,
150
+ defaultPageSize: 2,
151
+ onQuery: (endpoint, query) => { if (endpoint === 'documents') queries.push(query as DocumentQuery); },
152
+ });
153
+ render(<LibraryPage api={api} scope={SCOPE} pageSize={2} />);
154
+
155
+ await screen.findByText(/page 1 of 3/);
156
+ await user.click(screen.getByRole('button', { name: 'Next' }));
157
+
158
+ await waitFor(() => expect(queries.some((query) => query.page === 2)).toBe(true));
159
+ expect(await screen.findByText(/page 2 of 3/)).toBeInTheDocument();
160
+ });
161
+
162
+ it('returns to page 1 when a filter changes', async () => {
163
+ const user = userEvent.setup();
164
+ const queries: DocumentQuery[] = [];
165
+ const api = createFixtureCorpusApi({
166
+ scope: SCOPE,
167
+ defaultPageSize: 2,
168
+ onQuery: (endpoint, query) => { if (endpoint === 'documents') queries.push(query as DocumentQuery); },
169
+ });
170
+ render(<LibraryPage api={api} scope={SCOPE} pageSize={2} />);
171
+
172
+ await screen.findByText(/page 1 of 3/);
173
+ await user.click(screen.getByRole('button', { name: 'Next' }));
174
+ await screen.findByText(/page 2 of 3/);
175
+
176
+ await user.selectOptions(screen.getByLabelText('Source'), 'dropbox');
177
+
178
+ // Page 3 of the old result set is not page 3 of the new one; keeping it
179
+ // silently renders an empty list over a filter that worked.
180
+ await waitFor(() => {
181
+ const last = queries[queries.length - 1];
182
+ expect(last.source).toBe('dropbox');
183
+ expect(last.page).toBe(1);
184
+ });
185
+ });
186
+
187
+ it('says so on screen when the server ignored a filter it was sent', async () => {
188
+ const user = userEvent.setup();
189
+ // A backend that pages but does not search — which is what the tenant
190
+ // entities endpoint does TODAY. Rendering the unfiltered list under a
191
+ // filled-in search box is a lie the user cannot catch.
192
+ const base = createFixtureCorpusApi({ scope: SCOPE });
193
+ const api: CorpusApi = {
194
+ ...base,
195
+ async listDocuments(query) {
196
+ const page = await base.listDocuments({ ...query, search: undefined });
197
+ return { ...page, appliedFilters: [] };
198
+ },
199
+ };
200
+ render(<LibraryPage api={api} scope={SCOPE} />);
201
+
202
+ await screen.findByText('Library');
203
+ await user.type(screen.getByRole('searchbox'), 'renewal');
204
+ await user.click(screen.getByRole('button', { name: 'Search' }));
205
+
206
+ expect(await screen.findByText(/does not filter by search yet/)).toBeInTheDocument();
207
+ });
208
+ });
209
+
210
+ describe('LibraryPage — the nothings', () => {
211
+ it('tells an unconnected tenant to connect a source', async () => {
212
+ const api = createFixtureCorpusApi({ scope: SCOPE, documents: [], moneyEvents: [], sources: [] });
213
+ render(<LibraryPage api={api} scope={SCOPE} />);
214
+ expect(await screen.findByText(/Connect a source/)).toBeInTheDocument();
215
+ });
216
+
217
+ it('names the failing source when a connected tenant has nothing', async () => {
218
+ const api = createFixtureCorpusApi({
219
+ scope: SCOPE,
220
+ documents: [],
221
+ moneyEvents: [],
222
+ sources: [{
223
+ source: 'gdrive', label: 'Google Drive', documentCount: 0,
224
+ lastSyncedAt: '2026-08-06T07:30:00Z', status: 'error',
225
+ detail: 'The vendor rejected this key (401).',
226
+ }],
227
+ });
228
+ render(<LibraryPage api={api} scope={SCOPE} />);
229
+ // "No results" would send the user to the wrong place entirely.
230
+ expect(await screen.findByText(/Google Drive.*failing to sync/)).toBeInTheDocument();
231
+ });
232
+
233
+ it('renders a record that arrived with no text at all', async () => {
234
+ const user = userEvent.setup();
235
+ const api = createFixtureCorpusApi({ scope: SCOPE });
236
+ render(<LibraryPage api={api} scope={SCOPE} />);
237
+
238
+ await user.click(await screen.findByText(/Board prep/));
239
+ const detail = await screen.findByRole('region', { name: 'Record detail' });
240
+ expect(within(detail).getByText(/arrived with no text/)).toBeInTheDocument();
241
+ });
242
+
243
+ it('reads a refusal as a refusal, not as an empty corpus', async () => {
244
+ const api = createFixtureCorpusApi({ scope: SCOPE });
245
+ api.listDocuments = async () => {
246
+ throw Object.assign(new Error('nope'), { status: 403 });
247
+ };
248
+ render(<LibraryPage api={api} scope={SCOPE} />);
249
+ expect(await screen.findByRole('alert')).toHaveTextContent(/do not have access/i);
250
+ });
251
+ });
252
+
253
+ describe('LibraryPage — a record from another workspace never renders', () => {
254
+ it('never draws a foreign row in the LIST, and says the API returned one', async () => {
255
+ // The row is the leak. Its title and preview are on screen before anyone
256
+ // clicks, so guarding only the detail panel guards nothing.
257
+ const foreign = FIXTURE_DOCUMENTS.find((document) => document.id === 'nw_contract_44')!;
258
+ const api = createFixtureCorpusApi({ scope: SCOPE, leakScope: true, documents: [foreign] });
259
+ render(<LibraryPage api={api} scope={SCOPE} />);
260
+
261
+ expect(await screen.findByRole('alert')).toHaveTextContent(/belonging to another workspace/);
262
+ expect(screen.queryByText(/2,750/)).not.toBeInTheDocument();
263
+ expect(screen.queryByText(/Northwind terms/)).not.toBeInTheDocument();
264
+ // The list is EMPTY, not "one row we chose not to expand".
265
+ expect(screen.getByText(/Nothing has arrived yet/)).toBeInTheDocument();
266
+ });
267
+
268
+ it('keeps warning about a leaking API after paging past the offending page', async () => {
269
+ const user = userEvent.setup();
270
+ // The foreign row sorts first, so it lands on page 1 and page 2 is clean. A
271
+ // warning that vanishes on page 2 reads as "fixed", which it is not.
272
+ const api = createFixtureCorpusApi({ scope: SCOPE, leakScope: true, defaultPageSize: 3 });
273
+ render(<LibraryPage api={api} scope={SCOPE} pageSize={3} />);
274
+
275
+ expect(await screen.findByRole('alert')).toHaveTextContent(/belonging to another workspace/);
276
+ await user.click(screen.getByRole('button', { name: 'Next' }));
277
+
278
+ await screen.findByText(/page 2 of/);
279
+ expect(screen.getByRole('alert')).toHaveTextContent(/belonging to another workspace/);
280
+ });
281
+
282
+ it('refuses to display a foreign record even when one is deep-linked', async () => {
283
+ const foreign = FIXTURE_DOCUMENTS.find((document) => document.id === 'nw_contract_44')!;
284
+ const api = createFixtureCorpusApi({ scope: SCOPE, leakScope: true, documents: [foreign] });
285
+ render(<LibraryPage api={api} scope={SCOPE} initialDocumentId="nw_contract_44" />);
286
+
287
+ const alerts = await screen.findAllByRole('alert');
288
+ expect(alerts.some((node) => /belongs to another workspace/.test(node.textContent ?? ''))).toBe(true);
289
+ expect(screen.queryByText(/2,750/)).not.toBeInTheDocument();
290
+ });
291
+ });
292
+
293
+ // ---------------------------------------------------------------------------
294
+ // AskPage
295
+ // ---------------------------------------------------------------------------
296
+
297
+ /** The route an app would wire: `askCorpus` behind a promise. */
298
+ function askApiOver(api: CorpusApi, extra: Partial<CorpusAskApi> = {}): CorpusAskApi {
299
+ return {
300
+ ask: (request: AskRequest) => askCorpus(request, { corpus: api, llm: createFixtureLlm() }),
301
+ ...extra,
302
+ };
303
+ }
304
+
305
+ describe('AskPage — an answer is only an answer when it is traceable', () => {
306
+ it('renders the answer with a citation per record, clickable through to it', async () => {
307
+ const user = userEvent.setup();
308
+ const onOpenDocument = vi.fn();
309
+ render(
310
+ <AskPage
311
+ api={askApiOver(createFixtureCorpusApi({ scope: SCOPE }))}
312
+ scope={SCOPE}
313
+ onOpenDocument={onOpenDocument}
314
+ />,
315
+ );
316
+
317
+ await user.type(screen.getByRole('textbox'), 'Acme renewal rate and seats');
318
+ await user.click(screen.getByRole('button', { name: 'Ask' }));
319
+
320
+ const answer = await screen.findByRole('region', { name: 'Answer' });
321
+ expect(within(answer).getByText(/source/)).toBeInTheDocument();
322
+
323
+ const citation = within(answer).getByRole('button', { name: /Master Services Agreement/ });
324
+ await user.click(citation);
325
+ expect(onOpenDocument).toHaveBeenCalledWith('dbx_9812f0');
326
+ });
327
+
328
+ it('warns loudly when the model produced text it cannot trace', async () => {
329
+ const user = userEvent.setup();
330
+ const api: CorpusAskApi = {
331
+ async ask() {
332
+ return {
333
+ status: 'uncited',
334
+ answer: 'You agreed four thousand a month.',
335
+ citations: [],
336
+ considered: [],
337
+ unresolvedCitations: [],
338
+ droppedOutOfScope: 0,
339
+ provider: { provider: 'openai', model: 'gpt-x', live: true },
340
+ };
341
+ },
342
+ };
343
+ render(<AskPage api={api} scope={SCOPE} />);
344
+
345
+ await user.type(screen.getByRole('textbox'), 'what did we agree');
346
+ await user.click(screen.getByRole('button', { name: 'Ask' }));
347
+
348
+ const alert = await screen.findByRole('alert');
349
+ expect(alert).toHaveTextContent(/could not be traced/i);
350
+ expect(alert).toHaveTextContent(/would sign/i);
351
+ // The text is still shown — hiding it is its own kind of lying.
352
+ expect(screen.getByText(/four thousand/)).toBeInTheDocument();
353
+ });
354
+
355
+ it('names invented citations rather than dropping them quietly', async () => {
356
+ const user = userEvent.setup();
357
+ const api: CorpusAskApi = {
358
+ async ask() {
359
+ return {
360
+ status: 'answered',
361
+ answer: 'Twelve months.',
362
+ citations: [{
363
+ documentId: 'dbx_9812f0',
364
+ document: FIXTURE_DOCUMENTS.find((d) => d.id === 'dbx_9812f0')!,
365
+ quote: 'twelve (12) months',
366
+ }],
367
+ considered: [],
368
+ unresolvedCitations: ['not_invented_01'],
369
+ droppedOutOfScope: 0,
370
+ provider: { provider: 'openai', model: 'gpt-x', live: true },
371
+ };
372
+ },
373
+ };
374
+ render(<AskPage api={api} scope={SCOPE} />);
375
+
376
+ await user.type(screen.getByRole('textbox'), 'how long is the term');
377
+ await user.click(screen.getByRole('button', { name: 'Ask' }));
378
+
379
+ expect(await screen.findByText(/never given to it/)).toBeInTheDocument();
380
+ expect(screen.getByText(/not_invented_01/)).toBeInTheDocument();
381
+ });
382
+
383
+ it('says when the answer came from a stub rather than a live model', async () => {
384
+ const user = userEvent.setup();
385
+ render(<AskPage api={askApiOver(createFixtureCorpusApi({ scope: SCOPE }))} scope={SCOPE} />);
386
+
387
+ await user.type(screen.getByRole('textbox'), 'Acme renewal');
388
+ await user.click(screen.getByRole('button', { name: 'Ask' }));
389
+
390
+ expect(await screen.findByText(/no live model was called/)).toBeInTheDocument();
391
+ });
392
+
393
+ it('reports a leaking data API on the answer itself', async () => {
394
+ const user = userEvent.setup();
395
+ render(
396
+ <AskPage
397
+ api={askApiOver(createFixtureCorpusApi({ scope: SCOPE, leakScope: true }))}
398
+ scope={SCOPE}
399
+ />,
400
+ );
401
+
402
+ await user.type(screen.getByRole('textbox'), 'Acme renewal rate and seats');
403
+ await user.click(screen.getByRole('button', { name: 'Ask' }));
404
+
405
+ expect(await screen.findByText(/from another workspace were returned/)).toBeInTheDocument();
406
+ });
407
+
408
+ it('renders "nothing found" as its own state, not as an empty answer', async () => {
409
+ const user = userEvent.setup();
410
+ render(<AskPage api={askApiOver(createFixtureCorpusApi({ scope: SCOPE }))} scope={SCOPE} />);
411
+
412
+ await user.type(screen.getByRole('textbox'), 'Northwind');
413
+ await user.click(screen.getByRole('button', { name: 'Ask' }));
414
+
415
+ expect(await screen.findByText('No answer from your records')).toBeInTheDocument();
416
+ expect(screen.queryByText(/2,750/)).not.toBeInTheDocument();
417
+ });
418
+
419
+ it('shows the records it searched but did not cite', async () => {
420
+ const user = userEvent.setup();
421
+ const api: CorpusAskApi = {
422
+ async ask() {
423
+ const full = await askCorpus(
424
+ { question: 'Acme renewal rate and seats', mode: 'ask', scope: SCOPE },
425
+ { corpus: createFixtureCorpusApi({ scope: SCOPE }), llm: createFixtureLlm() },
426
+ );
427
+ return { ...full, citations: full.citations.slice(0, 1) };
428
+ },
429
+ };
430
+ render(<AskPage api={api} scope={SCOPE} />);
431
+
432
+ await user.type(screen.getByRole('textbox'), 'Acme renewal rate and seats');
433
+ await user.click(screen.getByRole('button', { name: 'Ask' }));
434
+
435
+ expect(await screen.findByText(/were searched but not cited/)).toBeInTheDocument();
436
+ });
437
+ });
438
+
439
+ describe('AskPage — generate', () => {
440
+ it('saves a cited draft back into the corpus', async () => {
441
+ const user = userEvent.setup();
442
+ const saveDraft: NonNullable<CorpusAskApi['saveDraft']> = vi.fn(async () => FIXTURE_DOCUMENTS[0]);
443
+ const savedCitations = () =>
444
+ (saveDraft as unknown as { mock: { calls: Array<[{ citations: string[] }]> } }).mock.calls[0][0]
445
+ .citations;
446
+ render(
447
+ <AskPage
448
+ api={askApiOver(createFixtureCorpusApi({ scope: SCOPE }), { saveDraft })}
449
+ scope={SCOPE}
450
+ initialMode="generate"
451
+ />,
452
+ );
453
+
454
+ await user.type(screen.getAllByRole('textbox')[0], 'Draft a renewal letter for Acme Robotics');
455
+ await user.click(screen.getByRole('button', { name: 'Generate draft' }));
456
+
457
+ await user.click(await screen.findByRole('button', { name: 'Save to library' }));
458
+ await waitFor(() => expect(saveDraft).toHaveBeenCalled());
459
+ expect(savedCitations().length).toBeGreaterThan(0);
460
+ expect(await screen.findByText('Saved to your library')).toBeInTheDocument();
461
+ });
462
+
463
+ it('offers no Save control when the deployment cannot write', async () => {
464
+ const user = userEvent.setup();
465
+ render(
466
+ <AskPage
467
+ api={askApiOver(createFixtureCorpusApi({ scope: SCOPE }))}
468
+ scope={SCOPE}
469
+ initialMode="generate"
470
+ />,
471
+ );
472
+
473
+ await user.type(screen.getAllByRole('textbox')[0], 'Draft a renewal letter');
474
+ await user.click(screen.getByRole('button', { name: 'Generate draft' }));
475
+
476
+ await screen.findByRole('region', { name: 'Answer' });
477
+ expect(screen.queryByRole('button', { name: 'Save to library' })).not.toBeInTheDocument();
478
+ });
479
+ });