@semiont/react-ui 0.4.13 → 0.4.15

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.
Files changed (52) hide show
  1. package/README.md +18 -12
  2. package/dist/KnowledgeBaseSessionContext-CpYaCbnC.d.mts +174 -0
  3. package/dist/{PdfAnnotationCanvas.client-CW6SKH2U.mjs → PdfAnnotationCanvas.client-CHDCGQBR.mjs} +3 -3
  4. package/dist/{chunk-HNZOXH4L.mjs → chunk-OZICDVH7.mjs} +5 -3
  5. package/dist/chunk-OZICDVH7.mjs.map +1 -0
  6. package/dist/chunk-R2U7P4TK.mjs +865 -0
  7. package/dist/chunk-R2U7P4TK.mjs.map +1 -0
  8. package/dist/{chunk-BQJWOK4C.mjs → chunk-VN5NY4SN.mjs} +9 -8
  9. package/dist/chunk-VN5NY4SN.mjs.map +1 -0
  10. package/dist/index.d.mts +147 -171
  11. package/dist/index.mjs +2215 -1961
  12. package/dist/index.mjs.map +1 -1
  13. package/dist/test-utils.d.mts +13 -62
  14. package/dist/test-utils.mjs +40 -21
  15. package/dist/test-utils.mjs.map +1 -1
  16. package/package.json +5 -3
  17. package/src/components/ProtectedErrorBoundary.tsx +95 -0
  18. package/src/components/Toolbar.tsx +13 -13
  19. package/src/components/__tests__/ProtectedErrorBoundary.test.tsx +197 -0
  20. package/src/components/modals/PermissionDeniedModal.tsx +140 -0
  21. package/src/components/modals/ReferenceWizardModal.tsx +3 -2
  22. package/src/components/modals/SessionExpiredModal.tsx +101 -0
  23. package/src/components/modals/__tests__/PermissionDeniedModal.test.tsx +150 -0
  24. package/src/components/modals/__tests__/SessionExpiredModal.test.tsx +115 -0
  25. package/src/components/resource/AnnotationHistory.tsx +5 -6
  26. package/src/components/resource/HistoryEvent.tsx +7 -7
  27. package/src/components/resource/__tests__/AnnotationHistory.test.tsx +33 -34
  28. package/src/components/resource/__tests__/HistoryEvent.test.tsx +17 -19
  29. package/src/components/resource/__tests__/event-formatting.test.ts +70 -94
  30. package/src/components/resource/event-formatting.ts +56 -56
  31. package/src/components/resource/panels/CollaborationPanel.tsx +9 -1
  32. package/src/components/resource/panels/ReferenceEntry.tsx +7 -5
  33. package/src/components/resource/panels/ResourceInfoPanel.tsx +8 -6
  34. package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +12 -12
  35. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +1 -0
  36. package/src/features/resource-viewer/__tests__/AnnotationCreationPending.test.tsx +1 -1
  37. package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +4 -4
  38. package/src/features/resource-viewer/__tests__/AnnotationProgressDismissal.test.tsx +5 -10
  39. package/src/features/resource-viewer/__tests__/BindFlowIntegration.test.tsx +23 -54
  40. package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +6 -6
  41. package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +7 -19
  42. package/src/features/resource-viewer/__tests__/ToastNotifications.test.tsx +1 -1
  43. package/src/features/resource-viewer/__tests__/YieldFlowIntegration.test.tsx +18 -44
  44. package/src/features/resource-viewer/__tests__/annotation-progress-flow.test.tsx +6 -6
  45. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +31 -26
  46. package/src/styles/patterns/panels-base.css +12 -0
  47. package/dist/TranslationManager-CudgH3gw.d.mts +0 -107
  48. package/dist/chunk-BQJWOK4C.mjs.map +0 -1
  49. package/dist/chunk-HNZOXH4L.mjs.map +0 -1
  50. package/dist/chunk-OL5UST25.mjs +0 -413
  51. package/dist/chunk-OL5UST25.mjs.map +0 -1
  52. /package/dist/{PdfAnnotationCanvas.client-CW6SKH2U.mjs.map → PdfAnnotationCanvas.client-CHDCGQBR.mjs.map} +0 -0
@@ -5,6 +5,7 @@ import '@testing-library/jest-dom';
5
5
  import { renderWithProviders } from '../../../../test-utils';
6
6
  import userEvent from '@testing-library/user-event';
7
7
  import type { components } from '@semiont/core';
8
+ import { SemiontApiClient } from '@semiont/api-client';
8
9
  import type { RouteBuilder } from '../../../../contexts/RoutingContext';
9
10
 
10
11
  type Annotation = components['schemas']['Annotation'];
@@ -303,28 +304,27 @@ describe('ReferenceEntry', () => {
303
304
  expect(unlinkButton).not.toBeInTheDocument();
304
305
  });
305
306
 
306
- it('should emit bind:update-body on unlink click', async () => {
307
+ it('should call client.bind.body on unlink click', async () => {
307
308
  mockIsBodyResolved.mockReturnValue(true);
308
309
  mockGetBodySource.mockReturnValue('linked-doc');
309
- const unlinkHandler = vi.fn();
310
310
 
311
- const { container, eventBus } = renderWithProviders(
311
+ const bindSpy = vi.spyOn(SemiontApiClient.prototype, 'bindAnnotation').mockResolvedValue({ correlationId: 'c1' });
312
+
313
+ const { container } = renderWithProviders(
312
314
  <ReferenceEntry {...defaultProps} annotateMode={true} />,
313
- { returnEventBus: true }
314
315
  );
315
316
 
316
- const subscription = eventBus!.get('bind:update-body').subscribe(unlinkHandler);
317
-
318
317
  const unlinkButton = container.querySelector('.semiont-reference-unlink')!;
319
318
  await userEvent.click(unlinkButton);
320
319
 
321
- expect(unlinkHandler).toHaveBeenCalledWith({
322
- annotationId: 'ref-1',
323
- resourceId: 'resource-1',
324
- operations: [{ op: 'remove', item: { type: 'SpecificResource', source: 'linked-doc' } }],
325
- });
320
+ expect(bindSpy).toHaveBeenCalledWith(
321
+ 'resource-1',
322
+ 'ref-1',
323
+ { operations: [{ op: 'remove', item: { type: 'SpecificResource', source: 'linked-doc' } }] },
324
+ expect.anything(),
325
+ );
326
326
 
327
- subscription.unsubscribe();
327
+ bindSpy.mockRestore();
328
328
  });
329
329
  });
330
330
 
@@ -122,6 +122,7 @@ const renderWithEventBus = (component: React.ReactElement, tracker?: ReturnType<
122
122
 
123
123
  describe('ResourceInfoPanel Component', () => {
124
124
  const defaultProps = {
125
+ resourceId: 'test-resource-id',
125
126
  documentEntityTypes: [],
126
127
  documentLocale: undefined,
127
128
  primaryMediaType: undefined,
@@ -248,7 +248,7 @@ describe('Annotation creation clears pendingAnnotation', () => {
248
248
  const createdListener = vi.fn();
249
249
  // Set listener after first render so eventBus is captured
250
250
  await waitFor(() => expect(getEventBus()).toBeDefined());
251
- const subscription = getEventBus().get('mark:created').subscribe(createdListener);
251
+ const subscription = getEventBus().get('mark:create-ok').subscribe(createdListener);
252
252
 
253
253
  act(() => {
254
254
  emit('mark:requested', { selector: TEXT_SELECTOR, motivation: 'linking' });
@@ -27,6 +27,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
27
27
  import { render, waitFor } from '@testing-library/react';
28
28
  import { act } from 'react';
29
29
  import { useMarkFlow } from '../../../hooks/useMarkFlow';
30
+ import { useStoreTokenSync } from '../../../hooks/useStoreTokenSync';
30
31
  import { EventBusProvider, useEventBus } from '../../../contexts/EventBusContext';
31
32
 
32
33
  // Mock Toast module to prevent "useToast must be used within a ToastProvider" errors
@@ -69,8 +70,7 @@ describe('Annotation Deletion - Feature Integration', () => {
69
70
 
70
71
  function TestComponent() {
71
72
  eventBusInstance = useEventBus();
72
- // useMarkFlow is the single registration point for useBindFlow
73
- // (handles mark:delete mark:create annotate:detect-request, etc.)
73
+ useStoreTokenSync(); // Syncs auth token to namespace getToken
74
74
  useMarkFlow(testId);
75
75
  return null;
76
76
  }
@@ -138,7 +138,7 @@ describe('Annotation Deletion - Feature Integration', () => {
138
138
  const deletedListener = vi.fn();
139
139
 
140
140
  // Subscribe to success event
141
- eventBus.get('mark:deleted').subscribe(deletedListener);
141
+ eventBus.get('mark:delete-ok').subscribe(deletedListener);
142
142
 
143
143
  emitDelete('annotation-789');
144
144
 
@@ -175,7 +175,7 @@ describe('Annotation Deletion - Feature Integration', () => {
175
175
  // Verify failure event was emitted
176
176
  await waitFor(() => {
177
177
  expect(failedListener).toHaveBeenCalledWith({
178
- error: expect.any(Error),
178
+ message: expect.any(String),
179
179
  });
180
180
  });
181
181
  });
@@ -25,7 +25,7 @@ import { useMarkFlow } from '../../../hooks/useMarkFlow';
25
25
  import { EventBusProvider, useEventBus } from '../../../contexts/EventBusContext';
26
26
  import { ApiClientProvider } from '../../../contexts/ApiClientContext';
27
27
  import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
28
- import { SSEClient } from '@semiont/api-client';
28
+ import { SemiontApiClient } from '@semiont/api-client';
29
29
  import { resourceId } from '@semiont/core';
30
30
 
31
31
  // Mock Toast module to prevent "useToast must be used within a ToastProvider" errors
@@ -39,20 +39,15 @@ vi.mock('../../../components/Toast', () => ({
39
39
  }));
40
40
 
41
41
  describe('Detection Progress Dismissal Bug', () => {
42
- let mockStream: any;
43
42
  const rUri = resourceId('test');
44
43
 
45
44
  beforeEach(() => {
46
45
  vi.clearAllMocks();
47
46
 
48
- mockStream = {
49
- close: vi.fn(),
50
- };
51
-
52
- vi.spyOn(SSEClient.prototype, 'markReferences').mockReturnValue(mockStream);
53
- vi.spyOn(SSEClient.prototype, 'markHighlights').mockReturnValue(mockStream);
54
- vi.spyOn(SSEClient.prototype, 'markComments').mockReturnValue(mockStream);
55
- vi.spyOn(SSEClient.prototype, 'markAssessments').mockReturnValue(mockStream);
47
+ vi.spyOn(SemiontApiClient.prototype, 'annotateReferences').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
48
+ vi.spyOn(SemiontApiClient.prototype, 'annotateHighlights').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
49
+ vi.spyOn(SemiontApiClient.prototype, 'annotateComments').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
50
+ vi.spyOn(SemiontApiClient.prototype, 'annotateAssessments').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
56
51
  });
57
52
 
58
53
  afterEach(() => {
@@ -2,14 +2,14 @@
2
2
  * Layer 3: Feature Integration Test - Bind Flow (body update)
3
3
  *
4
4
  * Tests the write side of useBindFlow:
5
- * - bind:update-body → calls bindAnnotation API
6
- * - bind:update-body → emits bind:body-updated on success
5
+ * - bind:update-body → calls http.bindAnnotation API (plain POST)
7
6
  * - bind:update-body → emits bind:body-update-failed on error
8
7
  * - auth token passed to bindAnnotation
9
8
  *
10
- * The wizard modal (ReferenceWizardModal) handles modal state, context
11
- * gathering, search configuration, and result display. This test covers
12
- * only the downstream API calls after the wizard emits bind:update-body.
9
+ * After the UNIFIED-STREAM migration, bind is a plain POST returning
10
+ * {correlationId}. The state change arrives on the events-stream as
11
+ * mark:body-updated. These tests focus on the POST call, not the
12
+ * events-stream delivery (which is tested in AnnotationStore tests).
13
13
  *
14
14
  * Uses real providers (EventBus, ApiClient, AuthToken) with mocked API boundary.
15
15
  */
@@ -21,14 +21,15 @@ import { useBindFlow } from '../../../hooks/useBindFlow';
21
21
  import { EventBusProvider, useEventBus } from '../../../contexts/EventBusContext';
22
22
  import { ApiClientProvider } from '../../../contexts/ApiClientContext';
23
23
  import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
24
- import { SSEClient } from '@semiont/api-client';
25
- import { resourceId, accessToken, annotationId } from '@semiont/core';
24
+ import { SemiontApiClient } from '@semiont/api-client';
25
+ import { resourceId, annotationId } from '@semiont/core';
26
+
27
+ const mockShowError = vi.fn();
26
28
 
27
- // Mock Toast module to prevent "useToast must be used within a ToastProvider" errors
28
29
  vi.mock('../../../components/Toast', () => ({
29
30
  useToast: () => ({
30
31
  showSuccess: vi.fn(),
31
- showError: vi.fn(),
32
+ showError: mockShowError,
32
33
  showInfo: vi.fn(),
33
34
  showWarning: vi.fn(),
34
35
  }),
@@ -43,11 +44,9 @@ describe('Bind Flow - Body Update Integration', () => {
43
44
  beforeEach(() => {
44
45
  vi.clearAllMocks();
45
46
 
46
- bindAnnotationSpy = vi.fn().mockImplementation((_rId: any, annId: any, _req: any, opts: any) => {
47
- queueMicrotask(() => opts.eventBus.get('bind:finished').next({ annotationId: annId }));
48
- return { close: vi.fn() };
49
- });
50
- vi.spyOn(SSEClient.prototype, 'bindAnnotation').mockImplementation(bindAnnotationSpy as any);
47
+ // Mock the HTTP bindAnnotation method (plain POST, returns {correlationId})
48
+ bindAnnotationSpy = vi.fn().mockResolvedValue({ correlationId: 'corr-test' });
49
+ vi.spyOn(SemiontApiClient.prototype, 'bindAnnotation').mockImplementation(bindAnnotationSpy as any);
51
50
  });
52
51
 
53
52
  afterEach(() => {
@@ -82,10 +81,11 @@ describe('Bind Flow - Body Update Integration', () => {
82
81
 
83
82
  // ─── bind:update-body ──────────────────────────────────────────────────
84
83
 
85
- it('bind:update-body calls bindAnnotation API', async () => {
84
+ it('bind:update-body calls http.bindAnnotation (plain POST)', async () => {
86
85
  const { getEventBus } = renderBindFlow();
87
86
 
88
87
  act(() => { getEventBus().get('bind:update-body').next({
88
+ correlationId: 'corr-1',
89
89
  annotationId: annotationId('ann-body-1'),
90
90
  resourceId: resourceId('linked-resource-id'),
91
91
  operations: [{ op: 'add', item: { type: 'SpecificResource' as const, source: 'linked-resource-id' } }],
@@ -100,6 +100,7 @@ describe('Bind Flow - Body Update Integration', () => {
100
100
  const { getEventBus } = renderBindFlow();
101
101
 
102
102
  act(() => { getEventBus().get('bind:update-body').next({
103
+ correlationId: 'corr-2',
103
104
  annotationId: annotationId('ann-auth'),
104
105
  resourceId: resourceId('resource-id'),
105
106
  operations: [{ op: 'replace', newItem: { type: 'SpecificResource' as const, source: 'resource-id' } }],
@@ -111,57 +112,24 @@ describe('Bind Flow - Body Update Integration', () => {
111
112
 
112
113
  const callArgs = bindAnnotationSpy.mock.calls[0];
113
114
  expect(callArgs[3]).toHaveProperty('auth');
114
- expect(callArgs[3].auth).toBe(accessToken(testToken));
115
- });
116
-
117
- it('bind:update-body emits bind:body-updated on success', async () => {
118
- const { getEventBus } = renderBindFlow();
119
- const bodyUpdatedSpy = vi.fn();
120
-
121
- const subscription = getEventBus().get('bind:body-updated').subscribe(bodyUpdatedSpy);
122
-
123
- act(() => { getEventBus().get('bind:update-body').next({
124
- annotationId: annotationId('ann-success'),
125
- resourceId: resourceId('resource-id'),
126
- operations: [{ op: 'add', item: { type: 'SpecificResource' as const, source: 'resource-id' } }],
127
- }); });
128
-
129
- await waitFor(() => {
130
- expect(bodyUpdatedSpy).toHaveBeenCalledTimes(1);
131
- });
132
-
133
- subscription.unsubscribe();
134
-
135
- expect(bodyUpdatedSpy).toHaveBeenCalledWith({
136
- annotationId: annotationId('ann-success'),
137
- });
138
115
  });
139
116
 
140
- it('bind:update-body emits bind:body-update-failed on API error', async () => {
141
- bindAnnotationSpy.mockImplementation((_rId: any, _annId: any, _req: any, opts: any) => {
142
- queueMicrotask(() => opts.eventBus.get('bind:failed').next({ error: new Error('Update failed') }));
143
- return { close: vi.fn() };
144
- });
117
+ it('bind:update-body shows error toast on API error', async () => {
118
+ bindAnnotationSpy.mockRejectedValueOnce(new Error('Update failed'));
145
119
 
146
120
  const { getEventBus } = renderBindFlow();
147
- const bodyUpdateFailedSpy = vi.fn();
148
-
149
- const subscription = getEventBus().get('bind:body-update-failed').subscribe(bodyUpdateFailedSpy);
150
121
 
151
122
  act(() => { getEventBus().get('bind:update-body').next({
123
+ correlationId: 'corr-3',
152
124
  annotationId: annotationId('ann-fail'),
153
125
  resourceId: resourceId('resource-id'),
154
126
  operations: [{ op: 'remove', item: { type: 'SpecificResource' as const, source: 'old-id' } }],
155
127
  }); });
156
128
 
157
129
  await waitFor(() => {
158
- expect(bodyUpdateFailedSpy).toHaveBeenCalledTimes(1);
159
- });
160
-
161
- subscription.unsubscribe();
162
-
163
- expect(bodyUpdateFailedSpy).toHaveBeenCalledWith({
164
- error: expect.any(Error),
130
+ expect(mockShowError).toHaveBeenCalledWith(
131
+ expect.stringContaining('Update failed'),
132
+ );
165
133
  });
166
134
  });
167
135
 
@@ -169,6 +137,7 @@ describe('Bind Flow - Body Update Integration', () => {
169
137
  const { getEventBus } = renderBindFlow();
170
138
 
171
139
  act(() => { getEventBus().get('bind:update-body').next({
140
+ correlationId: 'corr-4',
172
141
  annotationId: annotationId('ann-dedup'),
173
142
  resourceId: resourceId('resource-id'),
174
143
  operations: [{ op: 'add', item: { type: 'SpecificResource' as const, source: 'resource-id' } }],
@@ -17,7 +17,7 @@ import { useMarkFlow } from '../../../hooks/useMarkFlow';
17
17
  import { EventBusProvider, useEventBus } from '../../../contexts/EventBusContext';
18
18
  import { ApiClientProvider } from '../../../contexts/ApiClientContext';
19
19
  import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
20
- import { SSEClient } from '@semiont/api-client';
20
+ import { SemiontApiClient } from '@semiont/api-client';
21
21
 
22
22
  // Mock Toast module to prevent "useToast must be used within a ToastProvider" errors
23
23
  vi.mock('../../../components/Toast', () => ({
@@ -33,11 +33,11 @@ describe('REPRODUCING BUG: Detection state not updating', () => {
33
33
  beforeEach(() => {
34
34
  vi.clearAllMocks();
35
35
 
36
- // Minimal mock - SSE streams not needed for this test
37
- vi.spyOn(SSEClient.prototype, 'markReferences').mockReturnValue({ onProgress: vi.fn().mockReturnThis(), onComplete: vi.fn().mockReturnThis(), onError: vi.fn().mockReturnThis(), close: vi.fn() } as any);
38
- vi.spyOn(SSEClient.prototype, 'markHighlights').mockReturnValue({ onProgress: vi.fn().mockReturnThis(), onComplete: vi.fn().mockReturnThis(), onError: vi.fn().mockReturnThis(), close: vi.fn() } as any);
39
- vi.spyOn(SSEClient.prototype, 'markComments').mockReturnValue({ onProgress: vi.fn().mockReturnThis(), onComplete: vi.fn().mockReturnThis(), onError: vi.fn().mockReturnThis(), close: vi.fn() } as any);
40
- vi.spyOn(SSEClient.prototype, 'markAssessments').mockReturnValue({ onProgress: vi.fn().mockReturnThis(), onComplete: vi.fn().mockReturnThis(), onError: vi.fn().mockReturnThis(), close: vi.fn() } as any);
36
+ // Minimal mock namespace methods call these HTTP methods internally
37
+ vi.spyOn(SemiontApiClient.prototype, 'annotateReferences').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
38
+ vi.spyOn(SemiontApiClient.prototype, 'annotateHighlights').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
39
+ vi.spyOn(SemiontApiClient.prototype, 'annotateComments').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
40
+ vi.spyOn(SemiontApiClient.prototype, 'annotateAssessments').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
41
41
  });
42
42
 
43
43
  afterEach(() => {
@@ -29,7 +29,7 @@ import { useMarkFlow } from '../../../hooks/useMarkFlow';
29
29
  import { EventBusProvider, useEventBus } from '../../../contexts/EventBusContext';
30
30
  import { ApiClientProvider } from '../../../contexts/ApiClientContext';
31
31
  import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
32
- import { SSEClient } from '@semiont/api-client';
32
+ import { SemiontApiClient } from '@semiont/api-client';
33
33
  import type { Motivation } from '@semiont/core';
34
34
  import { resourceId } from '@semiont/core';
35
35
  import type { Emitter } from 'mitt';
@@ -45,15 +45,7 @@ vi.mock('../../../components/Toast', () => ({
45
45
  }));
46
46
  import type { EventMap } from '@semiont/core';
47
47
 
48
- // Mock SSE stream - SSE now emits directly to EventBus, no callbacks
49
- const createMockSSEStream = () => {
50
- return {
51
- close: vi.fn(),
52
- };
53
- };
54
-
55
48
  describe('Detection Flow - Feature Integration', () => {
56
- let mockStream: ReturnType<typeof createMockSSEStream>;
57
49
  let markReferencesSpy: any;
58
50
  let markHighlightsSpy: any;
59
51
  let detectCommentsSpy: any;
@@ -61,14 +53,11 @@ describe('Detection Flow - Feature Integration', () => {
61
53
  beforeEach(() => {
62
54
  vi.clearAllMocks();
63
55
 
64
- // Create fresh mock stream for each test
65
- mockStream = createMockSSEStream();
66
-
67
- // Spy on SSEClient prototype methods
68
- markReferencesSpy = vi.spyOn(SSEClient.prototype, 'markReferences').mockReturnValue(mockStream as any);
69
- markHighlightsSpy = vi.spyOn(SSEClient.prototype, 'markHighlights').mockReturnValue(mockStream as any);
70
- detectCommentsSpy = vi.spyOn(SSEClient.prototype, 'markComments').mockReturnValue(mockStream as any);
71
- vi.spyOn(SSEClient.prototype, 'markAssessments').mockReturnValue(mockStream as any);
56
+ // Spy on SemiontApiClient prototype HTTP methods (namespace methods call these)
57
+ markReferencesSpy = vi.spyOn(SemiontApiClient.prototype, 'annotateReferences').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
58
+ markHighlightsSpy = vi.spyOn(SemiontApiClient.prototype, 'annotateHighlights').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
59
+ detectCommentsSpy = vi.spyOn(SemiontApiClient.prototype, 'annotateComments').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
60
+ vi.spyOn(SemiontApiClient.prototype, 'annotateAssessments').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
72
61
  });
73
62
 
74
63
  afterEach(() => {
@@ -298,8 +287,7 @@ describe('Detection Flow - Feature Integration', () => {
298
287
 
299
288
  // Reset for next test
300
289
  vi.clearAllMocks();
301
- mockStream = createMockSSEStream();
302
- detectCommentsSpy.mockReturnValue(mockStream);
290
+ detectCommentsSpy.mockResolvedValue({ correlationId: 'c2', jobId: 'j2' });
303
291
 
304
292
  // Test commenting
305
293
  act(() => {
@@ -158,7 +158,7 @@ describe('Toast Notifications - Verifies Toast Integration', () => {
158
158
  // Emit generation failed event
159
159
  act(() => {
160
160
  eventBusInstance.get('yield:failed').next({
161
- error: new Error('Failed to generate document'),
161
+ error: 'Failed to generate document',
162
162
  });
163
163
  });
164
164
 
@@ -26,10 +26,9 @@ import { EventBusProvider, useEventBus } from '../../../contexts/EventBusContext
26
26
  import { ApiClientProvider } from '../../../contexts/ApiClientContext';
27
27
  import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
28
28
  import { useBindFlow } from '../../../hooks/useBindFlow';
29
- import { SSEClient } from '@semiont/api-client';
29
+ import { SemiontApiClient } from '@semiont/api-client';
30
30
  import type { AnnotationId, ResourceId } from '@semiont/core';
31
31
  import { resourceId, annotationId } from '@semiont/core';
32
- import type { Emitter } from 'mitt';
33
32
  import type { EventMap } from '@semiont/core';
34
33
 
35
34
  // Mock Toast module to prevent "useToast must be used within a ToastProvider" errors
@@ -42,15 +41,7 @@ vi.mock('../../../components/Toast', () => ({
42
41
  }),
43
42
  }));
44
43
 
45
- // Mock SSE stream - SSE now emits directly to EventBus, no callbacks
46
- const createMockGenerationStream = () => {
47
- return {
48
- close: vi.fn(),
49
- };
50
- };
51
-
52
44
  describe('Generation Flow - Feature Integration', () => {
53
- let mockStream: ReturnType<typeof createMockGenerationStream>;
54
45
  let generateResourceSpy: any;
55
46
  let mockShowSuccess: ReturnType<typeof vi.fn>;
56
47
  let mockShowError: ReturnType<typeof vi.fn>;
@@ -59,11 +50,8 @@ describe('Generation Flow - Feature Integration', () => {
59
50
  beforeEach(() => {
60
51
  vi.clearAllMocks();
61
52
 
62
- // Create fresh mock stream for each test
63
- mockStream = createMockGenerationStream();
64
-
65
- // Spy on SSEClient prototype method
66
- generateResourceSpy = vi.spyOn(SSEClient.prototype, 'yieldResource').mockReturnValue(mockStream as any);
53
+ // Spy on SemiontApiClient prototype HTTP method (namespace methods call this)
54
+ generateResourceSpy = vi.spyOn(SemiontApiClient.prototype, 'yieldResourceFromAnnotation').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
67
55
 
68
56
  // Mock callbacks
69
57
  mockShowSuccess = vi.fn();
@@ -107,17 +95,13 @@ describe('Generation Flow - Feature Integration', () => {
107
95
  expect(generateResourceSpy).toHaveBeenCalledWith(
108
96
  testResourceId,
109
97
  testAnnotationId,
110
- {
98
+ expect.objectContaining({
111
99
  title: 'Generated Document',
112
100
  prompt: 'Create a comprehensive document',
113
101
  language: 'en',
114
102
  temperature: 0.7,
115
103
  maxTokens: 2000,
116
- context: {
117
- sourceText: 'Reference text from the document',
118
- entityTypes: ['Person', 'Organization'],
119
- },
120
- },
104
+ }),
121
105
  expect.objectContaining({ auth: undefined })
122
106
  );
123
107
  });
@@ -295,7 +279,7 @@ describe('Generation Flow - Feature Integration', () => {
295
279
 
296
280
  // Emit failure
297
281
  act(() => {
298
- getEventBus().get('yield:failed').next({ error: new Error('Network error') });
282
+ getEventBus().get('yield:failed').next({ error: 'Network error' });
299
283
  });
300
284
 
301
285
  // Verify: progress cleared and not generating
@@ -305,18 +289,14 @@ describe('Generation Flow - Feature Integration', () => {
305
289
  });
306
290
  });
307
291
 
308
- it('should only call API once even with multiple event listeners', async () => {
292
+ it('should only call API once even with multiple renders', async () => {
309
293
  const testResourceId = resourceId('test-resource');
310
294
  const testAnnotationId = annotationId('test-annotation');
311
295
 
312
- const { emitGenerationStart, getEventBus } = renderYieldFlow(
296
+ const { emitGenerationStart } = renderYieldFlow(
313
297
  testResourceId
314
298
  );
315
299
 
316
- // Add an additional event listener (simulating multiple subscribers)
317
- const additionalListener = vi.fn();
318
- const subscription = getEventBus().get('yield:request').subscribe(additionalListener);
319
-
320
300
  // Trigger generation
321
301
  act(() => {
322
302
  emitGenerationStart(testAnnotationId, testResourceId, {
@@ -330,13 +310,8 @@ describe('Generation Flow - Feature Integration', () => {
330
310
  expect(generateResourceSpy).toHaveBeenCalled();
331
311
  });
332
312
 
333
- // VERIFY: API called exactly once, even though multiple listeners exist
313
+ // VERIFY: API called exactly once
334
314
  expect(generateResourceSpy).toHaveBeenCalledTimes(1);
335
-
336
- // VERIFY: Our additional listener was called (events work)
337
- expect(additionalListener).toHaveBeenCalledTimes(1);
338
-
339
- subscription.unsubscribe();
340
315
  });
341
316
 
342
317
  it('should forward final chunk as progress before emitting complete', async () => {
@@ -385,15 +360,13 @@ describe('Generation Flow - Feature Integration', () => {
385
360
  function renderYieldFlow(
386
361
  testResourceId: ResourceId
387
362
  ) {
388
- let eventBusInstance: Emitter<EventMap>;
363
+ let eventBusInstance: ReturnType<typeof useEventBus>;
364
+ let generateFn: ReturnType<typeof useYieldFlow>['onGenerateDocument'];
389
365
 
390
366
  // Component to capture EventBus instance and set up event operations
391
367
  function EventBusCapture() {
392
368
  eventBusInstance = useEventBus();
393
-
394
- // Set up resolution flow (resolve:update-body, resolve:link)
395
369
  useBindFlow(testResourceId);
396
-
397
370
  return null;
398
371
  }
399
372
 
@@ -402,12 +375,15 @@ function renderYieldFlow(
402
375
  const {
403
376
  isGenerating,
404
377
  generationProgress,
378
+ onGenerateDocument,
405
379
  } = useYieldFlow(
406
380
  'en',
407
381
  testResourceId,
408
382
  vi.fn()
409
383
  );
410
384
 
385
+ generateFn = onGenerateDocument;
386
+
411
387
  return (
412
388
  <div>
413
389
  <div data-testid="is-generating">
@@ -434,9 +410,10 @@ function renderYieldFlow(
434
410
  return {
435
411
  emitGenerationStart: (
436
412
  aId: AnnotationId,
437
- rId: ResourceId,
413
+ _rId: ResourceId,
438
414
  options: {
439
415
  title: string;
416
+ storageUri?: string;
440
417
  prompt?: string;
441
418
  language?: string;
442
419
  temperature?: number;
@@ -444,11 +421,8 @@ function renderYieldFlow(
444
421
  context: any;
445
422
  }
446
423
  ) => {
447
- eventBusInstance.get('yield:request').next({
448
- annotationId: aId,
449
- resourceId: rId,
450
- options,
451
- });
424
+ // Call the hook's callback directly (no longer EventBus-driven)
425
+ generateFn(aId as string, { storageUri: options.storageUri ?? 'file:///tmp/test', ...options });
452
426
  },
453
427
  getEventBus: () => eventBusInstance,
454
428
  };
@@ -31,7 +31,7 @@ import { useMarkFlow } from '../../../hooks/useMarkFlow';
31
31
  import { EventBusProvider, useEventBus } from '../../../contexts/EventBusContext';
32
32
  import { ApiClientProvider } from '../../../contexts/ApiClientContext';
33
33
  import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
34
- import { SSEClient } from '@semiont/api-client';
34
+ import { SemiontApiClient } from '@semiont/api-client';
35
35
  import type { components } from '@semiont/core';
36
36
 
37
37
  // Mock Toast module to prevent "useToast must be used within a ToastProvider" errors
@@ -167,11 +167,11 @@ describe('Detection Progress Flow Integration (Layer 3)', () => {
167
167
  // Create fresh stream for each test
168
168
  mockStream = new MockSSEStream();
169
169
 
170
- // Spy on SSEClient prototype methods to inject mock stream
171
- vi.spyOn(SSEClient.prototype, 'markHighlights').mockReturnValue(mockStream as any);
172
- vi.spyOn(SSEClient.prototype, 'markAssessments').mockReturnValue(mockStream as any);
173
- vi.spyOn(SSEClient.prototype, 'markComments').mockReturnValue(mockStream as any);
174
- vi.spyOn(SSEClient.prototype, 'markReferences').mockReturnValue(mockStream as any);
170
+ // Spy on SemiontApiClient prototype HTTP methods (namespace methods call these)
171
+ vi.spyOn(SemiontApiClient.prototype, 'annotateHighlights').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
172
+ vi.spyOn(SemiontApiClient.prototype, 'annotateAssessments').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
173
+ vi.spyOn(SemiontApiClient.prototype, 'annotateComments').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
174
+ vi.spyOn(SemiontApiClient.prototype, 'annotateReferences').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
175
175
 
176
176
  mockAnnotations = [];
177
177
  });