@startsimpli/integrations 0.2.33 → 0.2.35
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 +1 -1
- package/src/__tests__/connection-activity.test.tsx +160 -0
- package/src/__tests__/corpus-folders.test.tsx +727 -0
- package/src/__tests__/corpus-pages.test.tsx +4 -4
- package/src/__tests__/library-failure-reason.test.tsx +178 -0
- package/src/components/FolderTree.tsx +425 -0
- package/src/components/index.ts +6 -0
- package/src/components/pages/ConnectionsPage.tsx +174 -68
- package/src/components/pages/LibraryPage.tsx +333 -35
- package/src/corpus/client.ts +123 -9
- package/src/corpus/entities.ts +75 -0
- package/src/corpus/fixtures.ts +263 -1
- package/src/corpus/index.ts +8 -1
- package/src/corpus/types.ts +121 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@startsimpli/integrations",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.35",
|
|
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,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE CONNECTION CARD SAYS WHY A RUN FAILED (bd startsim-ei8gm.2, part 2).
|
|
3
|
+
*
|
|
4
|
+
* `ConnectionRun.error_message` has been DECLARED in this package's own types
|
|
5
|
+
* for as long as the endpoint has existed, and no component in
|
|
6
|
+
* `packages/integrations` or `apps/foundry` has ever read it. `connectionRuns`
|
|
7
|
+
* is likewise required on both api interfaces and called by nothing. So the
|
|
8
|
+
* activity endpoint that `#375` just taught to answer a real reason for source
|
|
9
|
+
* runs has no reader at all: a failed run does not render as a status word — it
|
|
10
|
+
* does not render.
|
|
11
|
+
*
|
|
12
|
+
* ONE COMPOSER, TWO SURFACES. `ConnectionsPage` is mounted by the control plane
|
|
13
|
+
* (`apps/foundry`'s `<ConnectionsPage api={api.foundry} foundrySlug={slug} />`)
|
|
14
|
+
* and by a tenant member's own screen in `personal` mode. The disclosure is
|
|
15
|
+
* gated on the VERB EXISTING — the rule the file already applies to Workflows
|
|
16
|
+
* and Settings — so an app pinned to an older `@startsimpli/api` degrades to no
|
|
17
|
+
* control rather than to a button whose handler is `undefined`.
|
|
18
|
+
*
|
|
19
|
+
* NOTHING IS SCRUBBED HERE. The text arrives already redacted against that
|
|
20
|
+
* connection's stored credential plus the platform app_secret
|
|
21
|
+
* (`apps/connectors/services/surface.py::_reason_reader` ->
|
|
22
|
+
* `dispatch.redaction_list`), and a run whose reason could not be proven clean
|
|
23
|
+
* arrives as a sentence saying so. A second redaction path in the browser would
|
|
24
|
+
* be a second thing to get wrong, and the browser does not hold the secrets to
|
|
25
|
+
* do it with.
|
|
26
|
+
*/
|
|
27
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
28
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
29
|
+
import userEvent from '@testing-library/user-event';
|
|
30
|
+
|
|
31
|
+
import { ConnectionsPage } from '../components/pages/ConnectionsPage';
|
|
32
|
+
import type { ConnectionsApi, ConnectionRun } from '../components/pages/types';
|
|
33
|
+
|
|
34
|
+
const CATALOG = {
|
|
35
|
+
results: [
|
|
36
|
+
{
|
|
37
|
+
key: 'granola', name: 'Granola', description: 'Meeting notes',
|
|
38
|
+
logo_url: '', docs_url: '', capabilities: ['poll'], auth_mode: 'api_key',
|
|
39
|
+
credential_schema: { version: 1, fields: [] },
|
|
40
|
+
config_schema: { version: 1, fields: [] },
|
|
41
|
+
record_types: [], supports_webhooks: false,
|
|
42
|
+
availability: { available: true, reason: '' },
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const CONNECTED = {
|
|
48
|
+
results: [
|
|
49
|
+
{
|
|
50
|
+
id: 'c1', public_id: 'pub1', name: 'Tandem Granola',
|
|
51
|
+
connector_key: 'granola', connector_name: 'Granola',
|
|
52
|
+
external_account_id: '', account_label: 'tandem',
|
|
53
|
+
capabilities_enabled: ['poll'], config: {}, status: 'connected',
|
|
54
|
+
health: 'ok', auth_mode: 'api_key',
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
permissions: { view: true, manage: true, issue_tokens: true },
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** What production held, verbatim, for each of the twenty failed polls. */
|
|
61
|
+
const THE_REAL_REASON =
|
|
62
|
+
'connector_granola poll failed on page 1: [config_error] the response '
|
|
63
|
+
+ 'exceeded the 262144-byte cap and was aborted [granola]';
|
|
64
|
+
|
|
65
|
+
const RUNS: ConnectionRun[] = [
|
|
66
|
+
{
|
|
67
|
+
id: 'r-new', kind: 'destination_run', object_id: 'd1', name: 'Corpus delivery',
|
|
68
|
+
status: 'completed', run_type: 'scheduled',
|
|
69
|
+
occurred_at: '2026-08-27T04:24:08Z', error_message: '',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: 'r-old', kind: 'source_run', object_id: 's1', name: 'granola-poll',
|
|
73
|
+
status: 'failed', run_type: 'incremental',
|
|
74
|
+
occurred_at: '2026-08-12T20:21:46Z', error_message: THE_REAL_REASON,
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
function stubApi(overrides: Partial<ConnectionsApi> = {}): ConnectionsApi {
|
|
79
|
+
return {
|
|
80
|
+
connectorCatalog: vi.fn().mockResolvedValue(CATALOG),
|
|
81
|
+
connections: vi.fn().mockResolvedValue(CONNECTED),
|
|
82
|
+
connect: vi.fn(),
|
|
83
|
+
checkConnection: vi.fn().mockResolvedValue({ ok: true, account_label: '', detail: '' }),
|
|
84
|
+
disconnect: vi.fn(),
|
|
85
|
+
connectionChoices: vi.fn(),
|
|
86
|
+
wireTarget: vi.fn(),
|
|
87
|
+
confirmTarget: vi.fn(),
|
|
88
|
+
connectionActions: vi.fn(),
|
|
89
|
+
invokeConnectionAction: vi.fn(),
|
|
90
|
+
connectionRuns: vi.fn().mockResolvedValue({ results: RUNS }),
|
|
91
|
+
updateConnection: vi.fn(),
|
|
92
|
+
rotateCredential: vi.fn(),
|
|
93
|
+
...overrides,
|
|
94
|
+
} as unknown as ConnectionsApi;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function openActivity(api: ConnectionsApi = stubApi()) {
|
|
98
|
+
render(<ConnectionsPage api={api} foundrySlug="tandem" />);
|
|
99
|
+
const button = await screen.findByRole('button', { name: /activity/i });
|
|
100
|
+
await userEvent.click(button);
|
|
101
|
+
return api;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
describe('ConnectionCard — recent activity', () => {
|
|
105
|
+
it('has a control at all, which is the whole of the gap', async () => {
|
|
106
|
+
render(<ConnectionsPage api={stubApi()} foundrySlug="tandem" />);
|
|
107
|
+
|
|
108
|
+
expect(await screen.findByRole('button', { name: /activity/i })).toBeInTheDocument();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('reads the runs endpoint that nothing has ever called', async () => {
|
|
112
|
+
const api = await openActivity();
|
|
113
|
+
|
|
114
|
+
await waitFor(() => expect(api.connectionRuns).toHaveBeenCalledWith('tandem', 'c1'));
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('renders the reason a failed run gives, not only its status word', async () => {
|
|
118
|
+
await openActivity();
|
|
119
|
+
|
|
120
|
+
expect(await screen.findByText(/262144-byte cap/)).toBeInTheDocument();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('names which leg the run belongs to, since the card merges three tables', async () => {
|
|
124
|
+
await openActivity();
|
|
125
|
+
|
|
126
|
+
const activity = await screen.findByRole('group', { name: /recent activity/i });
|
|
127
|
+
expect(activity).toHaveTextContent(/granola-poll/);
|
|
128
|
+
expect(activity).toHaveTextContent(/Corpus delivery/);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('invents no reason for a run that succeeded', async () => {
|
|
132
|
+
await openActivity(stubApi({
|
|
133
|
+
connectionRuns: vi.fn().mockResolvedValue({ results: [RUNS[0]] }),
|
|
134
|
+
}) as ConnectionsApi);
|
|
135
|
+
|
|
136
|
+
const activity = await screen.findByRole('group', { name: /recent activity/i });
|
|
137
|
+
expect(activity).not.toHaveTextContent(/262144/);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('says so when there is no activity, rather than rendering an empty box', async () => {
|
|
141
|
+
await openActivity(stubApi({
|
|
142
|
+
connectionRuns: vi.fn().mockResolvedValue({ results: [] }),
|
|
143
|
+
}) as ConnectionsApi);
|
|
144
|
+
|
|
145
|
+
const activity = await screen.findByRole('group', { name: /recent activity/i });
|
|
146
|
+
expect(activity).toHaveTextContent(/no runs yet/i);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('offers no control when the client is too old to have the verb', async () => {
|
|
150
|
+
// Apps consume `@startsimpli/*` as PUBLISHED packages, so a client pinned
|
|
151
|
+
// to an older `@startsimpli/api` is a normal state of the world. The gate
|
|
152
|
+
// is the verb existing — the same rule Workflows and Settings use.
|
|
153
|
+
const api = stubApi();
|
|
154
|
+
delete (api as Partial<ConnectionsApi>).connectionRuns;
|
|
155
|
+
render(<ConnectionsPage api={api} foundrySlug="tandem" />);
|
|
156
|
+
|
|
157
|
+
await screen.findByText('Tandem Granola');
|
|
158
|
+
expect(screen.queryByRole('button', { name: /activity/i })).not.toBeInTheDocument();
|
|
159
|
+
});
|
|
160
|
+
});
|