@vertesia/ui 1.6.0-dev.20260912.110338Z → 1.6.0-dev.20260918.063824Z
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/lib/env/index.d.ts +10 -4
- package/lib/env/index.d.ts.map +1 -1
- package/lib/env/index.js +8 -0
- package/lib/env/index.js.map +1 -1
- package/lib/features/agent/PayloadBuilder.d.ts.map +1 -1
- package/lib/features/agent/PayloadBuilder.js +6 -1
- package/lib/features/agent/PayloadBuilder.js.map +1 -1
- package/lib/features/agent/chat/AgentRunFeedback.d.ts +7 -0
- package/lib/features/agent/chat/AgentRunFeedback.d.ts.map +1 -1
- package/lib/features/agent/chat/AgentRunFeedback.js +13 -1
- package/lib/features/agent/chat/AgentRunFeedback.js.map +1 -1
- package/lib/features/agent/chat/ModernAgentConversation.js +4 -3
- package/lib/features/agent/chat/ModernAgentConversation.js.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.js +2 -2
- package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.js.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/MessageItem.js +2 -2
- package/lib/features/agent/chat/ModernAgentOutput/MessageItem.js.map +1 -1
- package/lib/features/agent/chat/hooks/useAgentPlans.d.ts +1 -1
- package/lib/features/agent/chat/hooks/useAgentPlans.d.ts.map +1 -1
- package/lib/features/agent/chat/hooks/useAgentPlans.js +5 -5
- package/lib/features/agent/chat/hooks/useAgentPlans.js.map +1 -1
- package/lib/features/store/objects/search/DocumentSearchContext.d.ts.map +1 -1
- package/lib/features/store/objects/search/DocumentSearchContext.js +6 -1
- package/lib/features/store/objects/search/DocumentSearchContext.js.map +1 -1
- package/lib/features/user/UserInfo.d.ts +2 -0
- package/lib/features/user/UserInfo.d.ts.map +1 -1
- package/lib/session/UserSessionProvider.js +3 -3
- package/lib/session/UserSessionProvider.js.map +1 -1
- package/lib/session/auth/domainRouting.d.ts +3 -4
- package/lib/session/auth/domainRouting.d.ts.map +1 -1
- package/lib/session/auth/domainRouting.js +3 -4
- package/lib/session/auth/domainRouting.js.map +1 -1
- package/lib/session/auth/firebase.d.ts.map +1 -1
- package/lib/session/auth/firebase.js +10 -0
- package/lib/session/auth/firebase.js.map +1 -1
- package/lib/shell/login/SignInAuthPending.js +1 -1
- package/lib/shell/login/SignInAuthPending.js.map +1 -1
- package/lib/shell/login/SignInEmailStep.d.ts.map +1 -1
- package/lib/shell/login/SignInEmailStep.js +2 -2
- package/lib/shell/login/SignInEmailStep.js.map +1 -1
- package/lib/shell/login/SignInIcons.d.ts +1 -1
- package/lib/shell/login/SignInIcons.d.ts.map +1 -1
- package/lib/shell/login/signInUtils.d.ts +1 -1
- package/lib/shell/login/signInUtils.d.ts.map +1 -1
- package/lib/shell/login/signInUtils.js +5 -6
- package/lib/shell/login/signInUtils.js.map +1 -1
- package/lib/vertesia-ui-core.js +83 -41
- package/lib/vertesia-ui-core.js.map +1 -1
- package/lib/vertesia-ui-env.js +1 -1
- package/lib/vertesia-ui-env.js.map +1 -1
- package/lib/vertesia-ui-features.js +336 -317
- package/lib/vertesia-ui-features.js.map +1 -1
- package/lib/vertesia-ui-layout.js +43 -19
- package/lib/vertesia-ui-layout.js.map +1 -1
- package/lib/vertesia-ui-rich-text.js +123 -74
- package/lib/vertesia-ui-rich-text.js.map +1 -1
- package/lib/vertesia-ui-router.js.map +1 -1
- package/lib/vertesia-ui-session.js +1 -1
- package/lib/vertesia-ui-session.js.map +1 -1
- package/lib/vertesia-ui-shell.js +57 -28
- package/lib/vertesia-ui-shell.js.map +1 -1
- package/lib/vertesia-ui-widgets.js +153 -97
- package/lib/vertesia-ui-widgets.js.map +1 -1
- package/lib/widgets/xml-viewer/hooks/useCollapsible.d.ts +1 -1
- package/package.json +20 -20
- package/src/env/index.test.ts +48 -0
- package/src/env/index.ts +15 -2
- package/src/features/agent/PayloadBuilder.tsx +6 -1
- package/src/features/agent/chat/AgentRunFeedback.test.tsx +18 -1
- package/src/features/agent/chat/AgentRunFeedback.tsx +13 -0
- package/src/features/agent/chat/ModernAgentConversation.test.tsx +1 -1
- package/src/features/agent/chat/ModernAgentConversation.tsx +6 -5
- package/src/features/agent/chat/ModernAgentOutput/AllMessagesMixed.tsx +2 -2
- package/src/features/agent/chat/ModernAgentOutput/MessageItem.tsx +2 -2
- package/src/features/agent/chat/hooks/useAgentPlans.test.ts +52 -0
- package/src/features/agent/chat/hooks/useAgentPlans.ts +5 -5
- package/src/features/store/objects/search/DocumentSearchContext.ts +8 -3
- package/src/router/path.test.ts +5 -5
- package/src/session/UserSessionProvider.tsx +3 -3
- package/src/session/auth/auth-flow.md +21 -9
- package/src/session/auth/domainRouting.test.ts +1 -1
- package/src/session/auth/domainRouting.ts +3 -4
- package/src/session/auth/firebase.test.ts +25 -0
- package/src/session/auth/firebase.ts +10 -0
- package/src/shell/login/SignInAuthPending.tsx +3 -3
- package/src/shell/login/SignInEmailStep.test.tsx +32 -0
- package/src/shell/login/SignInEmailStep.tsx +13 -9
- package/src/shell/login/signInUtils.ts +12 -4
- package/src/widgets/markdown/CollaborativeMarkdownRenderer.test.tsx +11 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vertesia/ui",
|
|
3
|
-
"version": "1.6.0-dev.
|
|
3
|
+
"version": "1.6.0-dev.20260918.063824Z",
|
|
4
4
|
"description": "Vertesia UI components and and hooks",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -45,11 +45,11 @@
|
|
|
45
45
|
"@radix-ui/react-switch": "^1.3.7",
|
|
46
46
|
"@radix-ui/react-tabs": "^1.1.21",
|
|
47
47
|
"@radix-ui/react-tooltip": "^1.2.16",
|
|
48
|
-
"@vertesia/client": "1.6.0-dev.
|
|
49
|
-
"@vertesia/common": "1.6.0-dev.
|
|
50
|
-
"@vertesia/fusion-ux": "1.6.0-dev.
|
|
51
|
-
"@vertesia/json": "1.6.0-dev.
|
|
52
|
-
"@vertesia/rich-text": "1.6.0-dev.
|
|
48
|
+
"@vertesia/client": "1.6.0-dev.20260918.063824Z",
|
|
49
|
+
"@vertesia/common": "1.6.0-dev.20260918.063824Z",
|
|
50
|
+
"@vertesia/fusion-ux": "1.6.0-dev.20260918.063824Z",
|
|
51
|
+
"@vertesia/json": "1.6.0-dev.20260918.063824Z",
|
|
52
|
+
"@vertesia/rich-text": "1.6.0-dev.20260918.063824Z",
|
|
53
53
|
"ajv": "^8.20.0",
|
|
54
54
|
"ajv-formats": "^3.0.1",
|
|
55
55
|
"aria-hidden": "^1.2.6",
|
|
@@ -58,30 +58,30 @@
|
|
|
58
58
|
"cmdk": "^1.1.1",
|
|
59
59
|
"dayjs": "^1.11.23",
|
|
60
60
|
"debounce": "^3.0.0",
|
|
61
|
-
"dompurify": "^3.4.
|
|
61
|
+
"dompurify": "^3.4.15",
|
|
62
62
|
"fast-xml-parser": "^5.11.1",
|
|
63
63
|
"firebase": "^12.18.0",
|
|
64
|
-
"framer-motion": "^13.
|
|
65
|
-
"i18next": "^26.4.
|
|
64
|
+
"framer-motion": "^13.2.0",
|
|
65
|
+
"i18next": "^26.4.2",
|
|
66
66
|
"jwt-decode": "^4.0.0",
|
|
67
|
-
"katex": "^0.18.
|
|
67
|
+
"katex": "^0.18.7",
|
|
68
68
|
"lodash-es": "^4.18.1",
|
|
69
|
-
"lucide-react": "^1.
|
|
69
|
+
"lucide-react": "^1.42.0",
|
|
70
70
|
"mermaid": "^11.17.2",
|
|
71
71
|
"monaco-editor": "^0.56.0",
|
|
72
|
-
"motion": "^13.
|
|
72
|
+
"motion": "^13.2.0",
|
|
73
73
|
"papaparse": "^5.7.0",
|
|
74
74
|
"react": "19.2.8",
|
|
75
75
|
"react-calendar": "^6.0.1",
|
|
76
76
|
"react-date-picker": "^12.1.0",
|
|
77
77
|
"react-dom": "19.2.8",
|
|
78
|
-
"react-error-boundary": "^6.1.
|
|
79
|
-
"react-i18next": "^17.0.
|
|
78
|
+
"react-error-boundary": "^6.1.5",
|
|
79
|
+
"react-i18next": "^17.0.13",
|
|
80
80
|
"react-markdown": "^10.1.0",
|
|
81
81
|
"react-pdf": "^10.5.0",
|
|
82
82
|
"react-remove-scroll": "^2.7.2",
|
|
83
83
|
"react-remove-scroll-bar": "^2.3.8",
|
|
84
|
-
"react-resizable-panels": "^4.12.
|
|
84
|
+
"react-resizable-panels": "^4.12.4",
|
|
85
85
|
"react-style-singleton": "^2.2.3",
|
|
86
86
|
"react-vega": "^8.0.0",
|
|
87
87
|
"rehype-katex": "^7.0.1",
|
|
@@ -106,16 +106,16 @@
|
|
|
106
106
|
"@types/node": "^24.13.3",
|
|
107
107
|
"@types/papaparse": "^5.5.2",
|
|
108
108
|
"@types/react": "19.2.18",
|
|
109
|
-
"@types/react-dom": "19.2.
|
|
109
|
+
"@types/react-dom": "19.2.7",
|
|
110
110
|
"@vertesia/tsconfig": "0.1.0",
|
|
111
111
|
"axe-core": "^4.13.0",
|
|
112
112
|
"culori": "^4.0.2",
|
|
113
|
-
"i18next-cli": "^1.
|
|
113
|
+
"i18next-cli": "^1.73.1",
|
|
114
114
|
"jsdom": "^30.0.1",
|
|
115
|
-
"rolldown": "1.2.
|
|
115
|
+
"rolldown": "1.2.8",
|
|
116
116
|
"tailwindcss": "^4.3.3",
|
|
117
117
|
"typescript": "7.0.2",
|
|
118
|
-
"vitest": "
|
|
118
|
+
"vitest": "5.0.0",
|
|
119
119
|
"vitest-axe": "^0.1.0",
|
|
120
120
|
"wcag-contrast": "^3.0.0"
|
|
121
121
|
},
|
|
@@ -188,7 +188,7 @@
|
|
|
188
188
|
"url": "https://github.com/vertesia/composableai.git",
|
|
189
189
|
"directory": "packages/ui"
|
|
190
190
|
},
|
|
191
|
-
"gitHead": "
|
|
191
|
+
"gitHead": "5e9878a6ab82875248b0696621bb957732d42377",
|
|
192
192
|
"scripts": {
|
|
193
193
|
"clean:lib": "rimraf ./lib ./tsconfig.tsbuildinfo",
|
|
194
194
|
"clean": "rimraf ./node_modules ./lib ./tsconfig.tsbuildinfo",
|
package/src/env/index.test.ts
CHANGED
|
@@ -81,6 +81,27 @@ describe('VertesiaEnvironment runtime configuration', () => {
|
|
|
81
81
|
expect(window.AUTH_MODE).toBeUndefined();
|
|
82
82
|
});
|
|
83
83
|
|
|
84
|
+
it('uses the gateway broker ahead of an app build endpoint', () => {
|
|
85
|
+
vi.stubGlobal('window', {
|
|
86
|
+
__VERTESIA_RUNTIME_CONFIG__: { authMode: 'central', authUrl: 'https://auth.dev1.vertesia.io/' },
|
|
87
|
+
});
|
|
88
|
+
const env = new VertesiaEnvironment().init({
|
|
89
|
+
...baseProps,
|
|
90
|
+
endpoints: { ...baseProps.endpoints, auth: 'https://internal-auth.vertesia.app/' },
|
|
91
|
+
});
|
|
92
|
+
expect(env.endpoints.auth).toBe('https://auth.dev1.vertesia.io/');
|
|
93
|
+
expect(window.AUTH_MODE).toBe('central');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('preserves the app broker with an older central runtime contract', () => {
|
|
97
|
+
vi.stubGlobal('window', { __VERTESIA_RUNTIME_CONFIG__: { authMode: 'central' } });
|
|
98
|
+
const env = new VertesiaEnvironment().init({
|
|
99
|
+
...baseProps,
|
|
100
|
+
endpoints: { ...baseProps.endpoints, auth: 'https://auth.dev1.vertesia.io/' },
|
|
101
|
+
});
|
|
102
|
+
expect(env.endpoints.auth).toBe('https://auth.dev1.vertesia.io/');
|
|
103
|
+
});
|
|
104
|
+
|
|
84
105
|
it('accepts injected Central Auth mode without Firebase configuration', () => {
|
|
85
106
|
vi.stubGlobal('window', {
|
|
86
107
|
__VERTESIA_RUNTIME_CONFIG__: { authMode: 'central' },
|
|
@@ -192,3 +213,30 @@ describe('build-time default workspace', () => {
|
|
|
192
213
|
expect(env.defaultAuthSelection).toEqual({ accountId: 'explicit' });
|
|
193
214
|
});
|
|
194
215
|
});
|
|
216
|
+
|
|
217
|
+
describe('fixed Firebase tenant', () => {
|
|
218
|
+
afterEach(() => vi.unstubAllGlobals());
|
|
219
|
+
|
|
220
|
+
it('combines an app tenant with gateway credentials', () => {
|
|
221
|
+
vi.stubGlobal('window', {
|
|
222
|
+
__VERTESIA_RUNTIME_CONFIG__: {
|
|
223
|
+
authMode: 'firebase',
|
|
224
|
+
firebase: {
|
|
225
|
+
apiKey: 'gateway',
|
|
226
|
+
authDomain: 'app.example.com',
|
|
227
|
+
projectId: 'project',
|
|
228
|
+
appId: 'app',
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
const env = new VertesiaEnvironment().init(baseProps, { VITE_FIREBASE_TENANT_ID: ' example-tenant ' });
|
|
233
|
+
expect(env.firebase?.tenantId).toBe('example-tenant');
|
|
234
|
+
expect(env.firebase?.apiKey).toBe('gateway');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('does not enable Firebase just because a tenant is supplied', () => {
|
|
238
|
+
vi.stubGlobal('window', {});
|
|
239
|
+
const env = new VertesiaEnvironment().init(baseProps, { VITE_FIREBASE_TENANT_ID: 'tenant' });
|
|
240
|
+
expect(env.firebase).toBeUndefined();
|
|
241
|
+
});
|
|
242
|
+
});
|
package/src/env/index.ts
CHANGED
|
@@ -18,8 +18,8 @@ export interface EnvProps {
|
|
|
18
18
|
/**
|
|
19
19
|
* Central Auth broker that issues the sign-in redirect and hosts `/logout`.
|
|
20
20
|
*
|
|
21
|
-
* Optional: when unset the session falls back to the
|
|
22
|
-
* does not set it
|
|
21
|
+
* Optional: when unset the session falls back to the first-party broker, so an app that
|
|
22
|
+
* does not set it uses auth.vertesia.io. Set it to move one environment at a
|
|
23
23
|
* time onto a different broker.
|
|
24
24
|
*/
|
|
25
25
|
auth?: string;
|
|
@@ -34,6 +34,8 @@ export interface EnvProps {
|
|
|
34
34
|
projectId: string;
|
|
35
35
|
appId?: string;
|
|
36
36
|
providerType?: string;
|
|
37
|
+
/** Fixed Identity Platform tenant, preserved throughout sign-in. */
|
|
38
|
+
tenantId?: string;
|
|
37
39
|
};
|
|
38
40
|
/** Default workspace selection when the URL does not explicitly select an account or project. */
|
|
39
41
|
defaultAuthSelection?: { accountId?: string; projectId?: string };
|
|
@@ -77,6 +79,8 @@ export type VertesiaRuntimeConfig =
|
|
|
77
79
|
}
|
|
78
80
|
| {
|
|
79
81
|
authMode: 'central';
|
|
82
|
+
/** Broker selected by the serving gateway. Overrides build-time configuration. */
|
|
83
|
+
authUrl?: string;
|
|
80
84
|
};
|
|
81
85
|
|
|
82
86
|
declare global {
|
|
@@ -139,6 +143,15 @@ export class VertesiaEnvironment implements Readonly<EnvProps> {
|
|
|
139
143
|
const runtimeConfig = injectedRuntimeConfig() ?? buildRuntimeConfig(buildEnv);
|
|
140
144
|
const runtimeFirebase = runtimeConfig?.authMode === 'firebase' ? runtimeConfig.firebase : undefined;
|
|
141
145
|
this._props = props && runtimeFirebase && !props.firebase ? { ...props, firebase: runtimeFirebase } : props;
|
|
146
|
+
const authUrl = runtimeConfig?.authMode === 'central' ? runtimeConfig.authUrl?.trim() : undefined;
|
|
147
|
+
if (this._props && authUrl) {
|
|
148
|
+
this._props = { ...this._props, endpoints: { ...this._props.endpoints, auth: authUrl } };
|
|
149
|
+
}
|
|
150
|
+
const tenantId =
|
|
151
|
+
typeof buildEnv?.VITE_FIREBASE_TENANT_ID === 'string' ? buildEnv.VITE_FIREBASE_TENANT_ID.trim() : '';
|
|
152
|
+
if (this._props?.firebase && tenantId && !this._props.firebase.tenantId) {
|
|
153
|
+
this._props = { ...this._props, firebase: { ...this._props.firebase, tenantId } };
|
|
154
|
+
}
|
|
142
155
|
if (this._props && this._props.defaultAuthSelection === undefined && buildEnv) {
|
|
143
156
|
const accountId =
|
|
144
157
|
typeof buildEnv.VITE_VERTESIA_ACCOUNT_ID === 'string'
|
|
@@ -332,7 +332,12 @@ export class PayloadBuilder {
|
|
|
332
332
|
this._model_options = interaction.model_options as ModelOptions | undefined;
|
|
333
333
|
if (interaction.runtime?.environment) {
|
|
334
334
|
const envId = interaction.runtime.environment;
|
|
335
|
-
this.vertesia.environments
|
|
335
|
+
this.vertesia.environments
|
|
336
|
+
.retrieve(envId)
|
|
337
|
+
.then((environment) => (this.environment = environment))
|
|
338
|
+
.catch(() => {
|
|
339
|
+
this.environment = undefined;
|
|
340
|
+
});
|
|
336
341
|
}
|
|
337
342
|
}
|
|
338
343
|
this.onStateChanged();
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { fireEvent, screen, waitFor } from '@testing-library/react';
|
|
2
2
|
import type { AgentRunFeedbackEntry, AgentRunFeedbackPayload, AgentRunFeedbackStatus } from '@vertesia/common';
|
|
3
|
+
import { AgentMessageType } from '@vertesia/common';
|
|
3
4
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
5
|
import { renderWithProviders } from '../../../__tests__/test-utils.js';
|
|
5
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
AgentRunFeedback,
|
|
8
|
+
AgentRunFeedbackProvider,
|
|
9
|
+
agentRunFeedbackReasonCodes,
|
|
10
|
+
isAgentMessageRatable,
|
|
11
|
+
} from './AgentRunFeedback';
|
|
6
12
|
|
|
7
13
|
const mocks = vi.hoisted(() => {
|
|
8
14
|
const recordFeedback = vi.fn();
|
|
@@ -38,6 +44,17 @@ function payloadOf(call: number): AgentRunFeedbackPayload {
|
|
|
38
44
|
return mocks.recordFeedback.mock.calls[call][1] as AgentRunFeedbackPayload;
|
|
39
45
|
}
|
|
40
46
|
|
|
47
|
+
describe('isAgentMessageRatable', () => {
|
|
48
|
+
it('offers a rating on a final answer only, never on a streaming placeholder', () => {
|
|
49
|
+
expect(isAgentMessageRatable({ type: AgentMessageType.ANSWER, details: { streamed: true } })).toBe(true);
|
|
50
|
+
expect(isAgentMessageRatable({ type: AgentMessageType.ANSWER, details: undefined })).toBe(true);
|
|
51
|
+
expect(isAgentMessageRatable({ type: AgentMessageType.ANSWER, details: { source: 'streaming_summary' } })).toBe(
|
|
52
|
+
false,
|
|
53
|
+
);
|
|
54
|
+
expect(isAgentMessageRatable({ type: AgentMessageType.THOUGHT, details: undefined })).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
41
58
|
describe('AgentRunFeedback', () => {
|
|
42
59
|
beforeEach(() => {
|
|
43
60
|
mocks.recordFeedback.mockReset();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AGENT_RUN_FEEDBACK_COMMENT_MAX_LENGTH,
|
|
3
3
|
type AgentMessage,
|
|
4
|
+
AgentMessageType,
|
|
4
5
|
type AgentRunFeedbackEntry,
|
|
5
6
|
type AgentRunFeedbackPayload,
|
|
6
7
|
type AgentRunFeedbackRating,
|
|
@@ -61,6 +62,18 @@ export function agentMessageFeedbackId(message: Pick<AgentMessage, 'workstream_i
|
|
|
61
62
|
return `${message.workstream_id || 'main'}:${message.timestamp}`;
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Whether a transcript message can carry a rating. Only a final answer can: while an answer is
|
|
67
|
+
* still streaming, the transcript shows a placeholder stamped with the browser clock, and a
|
|
68
|
+
* rating keyed to that timestamp would never match the answer the worker eventually posts, so
|
|
69
|
+
* the same answer would end up rated twice.
|
|
70
|
+
*/
|
|
71
|
+
export function isAgentMessageRatable(message: Pick<AgentMessage, 'type' | 'details'>): boolean {
|
|
72
|
+
if (message.type !== AgentMessageType.ANSWER) return false;
|
|
73
|
+
const details = message.details as { source?: unknown } | undefined;
|
|
74
|
+
return details?.source !== 'streaming_summary';
|
|
75
|
+
}
|
|
76
|
+
|
|
64
77
|
/** Idempotency key for one submission; the server counts a retried id once. */
|
|
65
78
|
function newFeedbackId(): string {
|
|
66
79
|
if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) return crypto.randomUUID();
|
|
@@ -1278,7 +1278,7 @@ describe('ModernAgentConversation send handling', () => {
|
|
|
1278
1278
|
expect.objectContaining({ workstream_id: 'beta', status: 'running' }),
|
|
1279
1279
|
expect.objectContaining({ workstream_id: 'gamma', status: 'completed' }),
|
|
1280
1280
|
]);
|
|
1281
|
-
expect(latestRightPanelProps.activeTab).toBe('
|
|
1281
|
+
expect(latestRightPanelProps.activeTab).toBe('workstreams');
|
|
1282
1282
|
expect(latestMessageInputProps.activeTaskCount).toBe(2);
|
|
1283
1283
|
expect(latestMessageInputProps.activeWorkstreams).toEqual([
|
|
1284
1284
|
expect.objectContaining({ workstream_id: 'alpha', status: 'running' }),
|
|
@@ -1554,7 +1554,7 @@ function ModernAgentConversationInner({
|
|
|
1554
1554
|
showInput,
|
|
1555
1555
|
showSlidingPanel,
|
|
1556
1556
|
setShowSlidingPanel,
|
|
1557
|
-
} = useAgentPlans(messages, interactive
|
|
1557
|
+
} = useAgentPlans(messages, interactive);
|
|
1558
1558
|
|
|
1559
1559
|
const {
|
|
1560
1560
|
openDocuments,
|
|
@@ -1938,9 +1938,10 @@ function ModernAgentConversationInner({
|
|
|
1938
1938
|
// Unified right panel state
|
|
1939
1939
|
// ────────────────────────────────────────────
|
|
1940
1940
|
type RightPanelTab = 'plan' | 'workstreams' | 'documents' | 'uploads' | 'artifacts' | 'payload' | 'conversation';
|
|
1941
|
-
const [rightPanelTab, setRightPanelTab] = useState<RightPanelTab>(
|
|
1942
|
-
conversationContent || conversationTab ? 'conversation' :
|
|
1941
|
+
const [rightPanelTab, setRightPanelTab] = useState<RightPanelTab | undefined>(
|
|
1942
|
+
conversationContent || conversationTab ? 'conversation' : undefined,
|
|
1943
1943
|
);
|
|
1944
|
+
const defaultRightPanelTab = plans.length > 0 || panelWorkstreams.length === 0 ? 'plan' : 'workstreams';
|
|
1944
1945
|
const [selectedArtifactPath, setSelectedArtifactPath] = useState<string | null>(null);
|
|
1945
1946
|
const [rightPanelWidth, setRightPanelWidth] = useState(400);
|
|
1946
1947
|
const [isRightPanelResizing, setIsRightPanelResizing] = useState(false);
|
|
@@ -3085,8 +3086,8 @@ function ModernAgentConversationInner({
|
|
|
3085
3086
|
conversationContent={conversationTab ? conversationAreaJsx : conversationContent}
|
|
3086
3087
|
// Panel control
|
|
3087
3088
|
onClose={handleCloseRightPanel}
|
|
3088
|
-
defaultTab={rightPanelTab}
|
|
3089
|
-
activeTab={rightPanelTab}
|
|
3089
|
+
defaultTab={rightPanelTab ?? defaultRightPanelTab}
|
|
3090
|
+
activeTab={rightPanelTab ?? defaultRightPanelTab}
|
|
3090
3091
|
onTabChange={setRightPanelTab}
|
|
3091
3092
|
/>
|
|
3092
3093
|
</div>
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
Wrench,
|
|
30
30
|
} from 'lucide-react';
|
|
31
31
|
import React, { Component, type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
32
|
-
import { AgentRunFeedback, agentMessageFeedbackId } from '../AgentRunFeedback';
|
|
32
|
+
import { AgentRunFeedback, agentMessageFeedbackId, isAgentMessageRatable } from '../AgentRunFeedback';
|
|
33
33
|
import { AnimatedThinkingDots, PulsatingCircle } from '../AnimatedThinkingDots';
|
|
34
34
|
import { AskUserWidget, isAskUserOptions } from '../AskUserWidget';
|
|
35
35
|
import { DocumentEditingActionCard, parseMarkdownEditingAction } from '../DocumentEditingActionCard.js';
|
|
@@ -823,7 +823,7 @@ function SummaryMessage({
|
|
|
823
823
|
</MarkdownRenderer>
|
|
824
824
|
</div>
|
|
825
825
|
)}
|
|
826
|
-
{feedbackAgentRunId && message
|
|
826
|
+
{feedbackAgentRunId && isAgentMessageRatable(message) && (
|
|
827
827
|
<AgentRunFeedback
|
|
828
828
|
agentRunId={feedbackAgentRunId}
|
|
829
829
|
messageId={agentMessageFeedbackId(message)}
|
|
@@ -27,7 +27,7 @@ import type React from 'react';
|
|
|
27
27
|
import { memo, useEffect, useMemo, useRef, useState } from 'react';
|
|
28
28
|
import dayjs from '../../../../core/utils/dayjs.js';
|
|
29
29
|
import { useDownloadFile } from '../../../store/objects/components/useDownloadFile.js';
|
|
30
|
-
import { AgentRunFeedback, agentMessageFeedbackId } from '../AgentRunFeedback';
|
|
30
|
+
import { AgentRunFeedback, agentMessageFeedbackId, isAgentMessageRatable } from '../AgentRunFeedback';
|
|
31
31
|
import { PulsatingCircle } from '../AnimatedThinkingDots';
|
|
32
32
|
import { AskUserWidget, isAskUserOptions } from '../AskUserWidget';
|
|
33
33
|
import { DocumentEditingActionCard, parseMarkdownEditingAction } from '../DocumentEditingActionCard.js';
|
|
@@ -683,7 +683,7 @@ function MessageItemComponent({
|
|
|
683
683
|
)
|
|
684
684
|
)}
|
|
685
685
|
|
|
686
|
-
{feedbackAgentRunId && message
|
|
686
|
+
{feedbackAgentRunId && isAgentMessageRatable(message) && (
|
|
687
687
|
<AgentRunFeedback
|
|
688
688
|
agentRunId={feedbackAgentRunId}
|
|
689
689
|
messageId={agentMessageFeedbackId(message)}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { act, cleanup, renderHook } from '@testing-library/react';
|
|
2
|
+
import { type AgentMessage, AgentMessageType } from '@vertesia/common';
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
4
|
+
import { useAgentPlans } from './useAgentPlans.js';
|
|
5
|
+
|
|
6
|
+
const question: AgentMessage = {
|
|
7
|
+
timestamp: 1,
|
|
8
|
+
workflow_run_id: 'run-1',
|
|
9
|
+
workstream_id: 'main',
|
|
10
|
+
type: AgentMessageType.QUESTION,
|
|
11
|
+
message: 'Help with a task',
|
|
12
|
+
};
|
|
13
|
+
const plan: AgentMessage = {
|
|
14
|
+
...question,
|
|
15
|
+
timestamp: 2,
|
|
16
|
+
type: AgentMessageType.PLAN,
|
|
17
|
+
details: { plan: [{ id: 1, goal: 'Review the task', status: 'pending' }] },
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
describe('useAgentPlans sidebar visibility', () => {
|
|
21
|
+
beforeEach(() => sessionStorage.clear());
|
|
22
|
+
afterEach(cleanup);
|
|
23
|
+
|
|
24
|
+
it('stays closed while history loads and when history has no plan', () => {
|
|
25
|
+
const { result, rerender } = renderHook(({ messages }) => useAgentPlans(messages, true), {
|
|
26
|
+
initialProps: { messages: [] as AgentMessage[] },
|
|
27
|
+
});
|
|
28
|
+
expect(result.current.showSlidingPanel).toBe(false);
|
|
29
|
+
rerender({ messages: [question] });
|
|
30
|
+
expect(result.current.showSlidingPanel).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('opens when a plan arrives and resets for a new empty run', () => {
|
|
34
|
+
const { result, rerender } = renderHook(({ messages }) => useAgentPlans(messages, true), {
|
|
35
|
+
initialProps: { messages: [question] },
|
|
36
|
+
});
|
|
37
|
+
rerender({ messages: [question, plan] });
|
|
38
|
+
expect(result.current.showSlidingPanel).toBe(true);
|
|
39
|
+
expect(result.current.plans).toHaveLength(1);
|
|
40
|
+
rerender({ messages: [] });
|
|
41
|
+
expect(result.current.showSlidingPanel).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('preserves manual opening when subsequent messages have no plan', () => {
|
|
45
|
+
const { result, rerender } = renderHook(({ messages }) => useAgentPlans(messages, true), {
|
|
46
|
+
initialProps: { messages: [question] },
|
|
47
|
+
});
|
|
48
|
+
act(() => result.current.setShowSlidingPanel(true));
|
|
49
|
+
rerender({ messages: [question, { ...question, timestamp: 3, type: AgentMessageType.ANSWER }] });
|
|
50
|
+
expect(result.current.showSlidingPanel).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -18,14 +18,14 @@ export interface UseAgentPlansResult {
|
|
|
18
18
|
* Key improvement: incremental processing. Instead of scanning ALL messages
|
|
19
19
|
* on every change, tracks `lastProcessedIndex` and only scans new messages.
|
|
20
20
|
*/
|
|
21
|
-
export function useAgentPlans(messages: AgentMessage[], interactive: boolean
|
|
21
|
+
export function useAgentPlans(messages: AgentMessage[], interactive: boolean): UseAgentPlansResult {
|
|
22
22
|
const [plans, setPlans] = useState<Array<{ plan: Plan; timestamp: number }>>([]);
|
|
23
23
|
const [activePlanIndex, setActivePlanIndex] = useState<number>(0);
|
|
24
24
|
const [workstreamStatusMap, setWorkstreamStatusMap] = useState<
|
|
25
25
|
Map<number, Map<string, 'pending' | 'in_progress' | 'completed' | 'skipped'>>
|
|
26
26
|
>(new Map());
|
|
27
27
|
const [showInput, setShowInput] = useState(interactive);
|
|
28
|
-
const [showSlidingPanel, setShowSlidingPanel] = useState<boolean>(
|
|
28
|
+
const [showSlidingPanel, setShowSlidingPanel] = useState<boolean>(false);
|
|
29
29
|
|
|
30
30
|
// Incremental processing: track how far we've scanned
|
|
31
31
|
const lastProcessedIndex = useRef<number>(-1);
|
|
@@ -38,12 +38,12 @@ export function useAgentPlans(messages: AgentMessage[], interactive: boolean, is
|
|
|
38
38
|
setPlans([]);
|
|
39
39
|
setActivePlanIndex(0);
|
|
40
40
|
setWorkstreamStatusMap(new Map());
|
|
41
|
-
//
|
|
42
|
-
setShowSlidingPanel(
|
|
41
|
+
// Open automatically only once a plan is available.
|
|
42
|
+
setShowSlidingPanel(false);
|
|
43
43
|
lastProcessedIndex.current = -1;
|
|
44
44
|
knownPlanTimestamps.current.clear();
|
|
45
45
|
}
|
|
46
|
-
}, [messages.length
|
|
46
|
+
}, [messages.length]);
|
|
47
47
|
|
|
48
48
|
// Helper to determine showInput from the latest message
|
|
49
49
|
const updateShowInput = useCallback(
|
|
@@ -176,9 +176,14 @@ export class DocumentSearch implements SearchInterface {
|
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
computeFacets(_query: ObjectSearchQuery) {
|
|
179
|
-
this._facetsRequest()
|
|
180
|
-
|
|
181
|
-
|
|
179
|
+
this._facetsRequest()
|
|
180
|
+
.then((facets) => {
|
|
181
|
+
this.facets.value = facets;
|
|
182
|
+
})
|
|
183
|
+
.catch((err: unknown) => {
|
|
184
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
185
|
+
this.result.value = { ...this.result.value, error };
|
|
186
|
+
});
|
|
182
187
|
}
|
|
183
188
|
|
|
184
189
|
async _search(loadMore = false, noFacets = false): Promise<boolean> {
|
package/src/router/path.test.ts
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it } from 'vitest';
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import { getMountBasename, joinPath, stripMountBasename, withMountBasename } from './path';
|
|
3
3
|
|
|
4
4
|
// Simulate a served `<base href>` (or its absence) by stubbing document.querySelector + document.URL.
|
|
5
5
|
function setBaseHref(href: string | null, docUrl = 'https://gw.example.com/x') {
|
|
6
|
-
(
|
|
6
|
+
vi.stubGlobal('document', {
|
|
7
7
|
URL: docUrl,
|
|
8
8
|
querySelector: (sel: string) =>
|
|
9
9
|
sel === 'base[href]' && href ? ({ href: new URL(href, docUrl).href } as unknown) : null,
|
|
10
|
-
};
|
|
10
|
+
});
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
const MOUNT = '/tenants/05948c_5ed5f4/apps/furniture-catalog/versions/20260620T064257113Z/app';
|
|
14
14
|
|
|
15
15
|
describe('mount basename helpers', () => {
|
|
16
16
|
afterEach(() => {
|
|
17
|
-
|
|
17
|
+
vi.unstubAllGlobals();
|
|
18
18
|
});
|
|
19
19
|
|
|
20
20
|
describe('getMountBasename', () => {
|
|
@@ -31,7 +31,7 @@ describe('mount basename helpers', () => {
|
|
|
31
31
|
expect(getMountBasename()).toBe('');
|
|
32
32
|
});
|
|
33
33
|
it('returns "" when there is no document (SSR)', () => {
|
|
34
|
-
|
|
34
|
+
vi.stubGlobal('document', undefined);
|
|
35
35
|
expect(getMountBasename()).toBe('');
|
|
36
36
|
});
|
|
37
37
|
});
|
|
@@ -134,7 +134,7 @@ export function UserSessionProvider({ children, loadOnboardingStatus = true }: U
|
|
|
134
134
|
session.setSession = setSession;
|
|
135
135
|
getComposableToken(selectedAccount, selectedProject, Env.devAuthToken)
|
|
136
136
|
.then((res) => {
|
|
137
|
-
session.login(res.rawToken).then(() => setSession(session.clone()));
|
|
137
|
+
return session.login(res.rawToken).then(() => setSession(session.clone()));
|
|
138
138
|
})
|
|
139
139
|
.catch((err) => {
|
|
140
140
|
if (surfaceAuthError(err)) return;
|
|
@@ -169,7 +169,7 @@ export function UserSessionProvider({ children, loadOnboardingStatus = true }: U
|
|
|
169
169
|
}
|
|
170
170
|
getComposableToken(selectedAccount, selectedProject, token, false, shouldRedirectToCentralAuth())
|
|
171
171
|
.then((res) => {
|
|
172
|
-
session.login(res.rawToken, { loadOnboardingStatus }).then(() => {
|
|
172
|
+
return session.login(res.rawToken, { loadOnboardingStatus }).then(() => {
|
|
173
173
|
setSession(session.clone());
|
|
174
174
|
clearAuthHash();
|
|
175
175
|
});
|
|
@@ -238,7 +238,7 @@ export function UserSessionProvider({ children, loadOnboardingStatus = true }: U
|
|
|
238
238
|
shouldRedirectToCentralAuth(),
|
|
239
239
|
)
|
|
240
240
|
.then((res) => {
|
|
241
|
-
session
|
|
241
|
+
return session
|
|
242
242
|
.login(res.rawToken, { loadOnboardingStatus })
|
|
243
243
|
.then(() => setSession(session.clone()));
|
|
244
244
|
})
|
|
@@ -6,10 +6,22 @@ This document describes the authentication flow for the Vertesia user-facing app
|
|
|
6
6
|
|
|
7
7
|
The authentication system uses:
|
|
8
8
|
- **Firebase** for identity management and OAuth provider integration
|
|
9
|
-
- **Central Auth** (
|
|
9
|
+
- **Central Auth** (auth.vertesia.io) for centralized authentication flow
|
|
10
10
|
- **STS (Security Token Service)** (sts.vertesia.io) to validate Firebase tokens and generate Vertesia JWT tokens
|
|
11
11
|
- **Session state management** to validate redirects and prevent CSRF attacks
|
|
12
12
|
|
|
13
|
+
### Central Auth endpoint selection
|
|
14
|
+
|
|
15
|
+
`https://auth.vertesia.io/` is the default broker. Regional scaffolding can set
|
|
16
|
+
`VITE_AUTH_ENDPOINT`; applications can also configure the `Env` initialization property `endpoints.auth` explicitly.
|
|
17
|
+
For compatible generated app shells, a gateway-provided central runtime config
|
|
18
|
+
(`window.__VERTESIA_RUNTIME_CONFIG__` with `authMode: 'central'` and `authUrl`)
|
|
19
|
+
takes precedence over the build-time endpoint. A runtime config without `authUrl`
|
|
20
|
+
retains the configured endpoint or default. Explicit Firebase mode uses direct
|
|
21
|
+
Firebase authentication instead of the central broker.
|
|
22
|
+
|
|
23
|
+
The diagrams below use the default hostname; redirects use the selected broker.
|
|
24
|
+
|
|
13
25
|
### STS Endpoints
|
|
14
26
|
- Production/Preview: `https://sts.vertesia.io`
|
|
15
27
|
- Staging: `https://sts.dev1.vertesia.io`
|
|
@@ -179,7 +191,7 @@ The authentication system uses:
|
|
|
179
191
|
┌──────────────────────┐ │ - Microsoft │ ┌──────────────────────┐
|
|
180
192
|
│ Build redirect URL: │ └──────────┬───────────┘ │ Redirect to SSO │
|
|
181
193
|
│ │ │ │ provider (Okta, │
|
|
182
|
-
│
|
|
194
|
+
│ auth.vertesia.io │ ▼ │ Azure AD, etc.) │
|
|
183
195
|
│ .vertesia.app │ ┌──────────────────────┐ └──────────┬───────────┘
|
|
184
196
|
│ ?sts=... │ │ Firebase OAuth │ │
|
|
185
197
|
│ &redirect_uri=... │ │ redirect & return │ ▼
|
|
@@ -329,7 +341,7 @@ The authentication system uses:
|
|
|
329
341
|
|
|
330
342
|
### Path A: Central Auth (Option 1) - Returns with JWT
|
|
331
343
|
1. User clicks "Continue with Central Auth"
|
|
332
|
-
2. Redirects to
|
|
344
|
+
2. Redirects to auth.vertesia.io
|
|
333
345
|
3. Central auth authenticates user and gets Firebase token
|
|
334
346
|
4. **Central auth calls STS** (`POST sts.vertesia.io/token/issue` with Firebase token)
|
|
335
347
|
5. **STS validates Firebase token and generates Vertesia JWT**
|
|
@@ -472,7 +484,7 @@ The authentication system uses:
|
|
|
472
484
|
│ Redirect to Central │ │ getFirebaseAuth() │
|
|
473
485
|
│ Auth for logout: │ │ .signOut() │
|
|
474
486
|
│ │ │ │
|
|
475
|
-
│
|
|
487
|
+
│ auth.vertesia.io │ │ (Triggers │
|
|
476
488
|
│ .vertesia.app │ │ onAuthStateChanged │
|
|
477
489
|
│ /logout │ │ with anonymous user)│
|
|
478
490
|
│ │ └──────────┬───────────┘
|
|
@@ -712,7 +724,7 @@ flowchart TB
|
|
|
712
724
|
subgraph Option1[OPTION 1: CENTRAL AUTH]
|
|
713
725
|
C1[Generate state<br/>Save to sessionStorage]
|
|
714
726
|
C2[Get account/project<br/>from URL or localStorage]
|
|
715
|
-
C3[Build redirect URL:<br/>
|
|
727
|
+
C3[Build redirect URL:<br/>auth.vertesia.io<br/>?sts=...&redirect_uri=...&state=...]
|
|
716
728
|
C4[window.location.replace<br/>to central auth]
|
|
717
729
|
C5[Central Auth Page<br/>User authenticates<br/>Gets Firebase token]
|
|
718
730
|
C6[Central auth calls STS:<br/>POST sts.vertesia.io/token/issue<br/>Authorization: Bearer Firebase<br/>Body: type, account_id, project_id]
|
|
@@ -774,7 +786,7 @@ flowchart TB
|
|
|
774
786
|
|
|
775
787
|
Check -->|YES| ShouldRedirect{shouldRedirectTo<br/>CentralAuth?}
|
|
776
788
|
|
|
777
|
-
ShouldRedirect -->|YES| RedirectLogout[Redirect to Central Auth<br/>for logout<br/><br/>
|
|
789
|
+
ShouldRedirect -->|YES| RedirectLogout[Redirect to Central Auth<br/>for logout<br/><br/>auth.vertesia.io<br/>/logout<br/><br/>Central auth handles<br/>Firebase logout]
|
|
778
790
|
|
|
779
791
|
ShouldRedirect -->|NO| FirebaseSignout[getFirebaseAuth.signOut<br/><br/>Triggers onAuthStateChanged<br/>with anonymous user]
|
|
780
792
|
|
|
@@ -837,7 +849,7 @@ flowchart TB
|
|
|
837
849
|
sequenceDiagram
|
|
838
850
|
actor User
|
|
839
851
|
participant App as Composable UI
|
|
840
|
-
participant CentralAuth as Central Auth<br/>(
|
|
852
|
+
participant CentralAuth as Central Auth<br/>(auth.vertesia.io)
|
|
841
853
|
participant Firebase as Firebase Auth
|
|
842
854
|
participant STS as STS<br/>(sts.vertesia.io)
|
|
843
855
|
|
|
@@ -1030,7 +1042,7 @@ flowchart TB
|
|
|
1030
1042
|
|
|
1031
1043
|
Check -->|YES| ShouldRedirect{shouldRedirectTo<br/>CentralAuth?}
|
|
1032
1044
|
|
|
1033
|
-
ShouldRedirect -->|YES| RedirectLogout[Redirect to Central Auth<br/>for logout<br/><br/>
|
|
1045
|
+
ShouldRedirect -->|YES| RedirectLogout[Redirect to Central Auth<br/>for logout<br/><br/>auth.vertesia.io<br/>/logout<br/><br/>Central auth handles<br/>Firebase logout]
|
|
1034
1046
|
|
|
1035
1047
|
ShouldRedirect -->|NO| FirebaseSignout[getFirebaseAuth.signOut<br/><br/>Triggers onAuthStateChanged<br/>with anonymous user]
|
|
1036
1048
|
|
|
@@ -1078,7 +1090,7 @@ flowchart TB
|
|
|
1078
1090
|
LO2[session.logout or<br/>session.signOut]
|
|
1079
1091
|
LO3{authToken<br/>exists?}
|
|
1080
1092
|
LO4{shouldRedirectTo<br/>CentralAuth?}
|
|
1081
|
-
LO5[Redirect to Central Auth<br/>for logout<br/><br/>
|
|
1093
|
+
LO5[Redirect to Central Auth<br/>for logout<br/><br/>auth.vertesia.io<br/>/logout<br/><br/>Central auth handles<br/>Firebase logout]
|
|
1082
1094
|
LO6[getFirebaseAuth.signOut<br/><br/>Triggers onAuthStateChanged<br/>with anonymous user]
|
|
1083
1095
|
LO7[Clear session data:<br/>- authError = undefined<br/>- isLoading = false<br/>- authToken = undefined<br/>- typeRegistry = undefined]
|
|
1084
1096
|
LO8[client.withAuthCallback<br/>undefined<br/><br/>Clear client auth]
|
|
@@ -77,7 +77,7 @@ describe('centralAuthUrl', () => {
|
|
|
77
77
|
it('falls back to the default broker when no auth endpoint is configured', () => {
|
|
78
78
|
initEnv(undefined);
|
|
79
79
|
expect(centralAuthUrl()).toBe(DEFAULT_CENTRAL_AUTH_URL);
|
|
80
|
-
expect(DEFAULT_CENTRAL_AUTH_URL).toBe('https://
|
|
80
|
+
expect(DEFAULT_CENTRAL_AUTH_URL).toBe('https://auth.vertesia.io/');
|
|
81
81
|
});
|
|
82
82
|
|
|
83
83
|
it('uses the configured auth endpoint when one is set', () => {
|