@vertesia/workflow 1.5.0-dev.20260717.131047Z → 1.5.0-dev.20260722.120446Z

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 (43) hide show
  1. package/lib/activities/generateDocumentProperties.d.ts +54 -13
  2. package/lib/activities/generateDocumentProperties.d.ts.map +1 -1
  3. package/lib/activities/generateDocumentProperties.js +109 -34
  4. package/lib/activities/generateDocumentProperties.js.map +1 -1
  5. package/lib/activities/generateOrAssignContentType.d.ts +6 -16
  6. package/lib/activities/generateOrAssignContentType.d.ts.map +1 -1
  7. package/lib/activities/generateOrAssignContentType.js +70 -19
  8. package/lib/activities/generateOrAssignContentType.js.map +1 -1
  9. package/lib/activities/media/exec.d.ts +60 -0
  10. package/lib/activities/media/exec.d.ts.map +1 -0
  11. package/lib/activities/media/exec.js +212 -0
  12. package/lib/activities/media/exec.js.map +1 -0
  13. package/lib/activities/media/prepareAudio.d.ts.map +1 -1
  14. package/lib/activities/media/prepareAudio.js +7 -6
  15. package/lib/activities/media/prepareAudio.js.map +1 -1
  16. package/lib/activities/media/prepareVideo.d.ts +5 -0
  17. package/lib/activities/media/prepareVideo.d.ts.map +1 -1
  18. package/lib/activities/media/prepareVideo.js +37 -13
  19. package/lib/activities/media/prepareVideo.js.map +1 -1
  20. package/lib/activities/media/probeMediaStreams.d.ts.map +1 -1
  21. package/lib/activities/media/probeMediaStreams.js +7 -6
  22. package/lib/activities/media/probeMediaStreams.js.map +1 -1
  23. package/lib/activities/renditions/generateVideoRendition.d.ts.map +1 -1
  24. package/lib/activities/renditions/generateVideoRendition.js +7 -5
  25. package/lib/activities/renditions/generateVideoRendition.js.map +1 -1
  26. package/lib/dsl/dsl-workflow.js +3 -0
  27. package/lib/dsl/dsl-workflow.js.map +1 -1
  28. package/lib/workflows-bundle.js +16277 -6846
  29. package/package.json +9 -9
  30. package/src/activities/generateDocumentProperties.test.ts +642 -0
  31. package/src/activities/generateDocumentProperties.ts +187 -43
  32. package/src/activities/generateOrAssignContentType.test.ts +196 -0
  33. package/src/activities/generateOrAssignContentType.ts +115 -30
  34. package/src/activities/media/exec.test.ts +194 -0
  35. package/src/activities/media/exec.ts +265 -0
  36. package/src/activities/media/prepareAudio.ts +12 -7
  37. package/src/activities/media/prepareVideo.test.ts +27 -0
  38. package/src/activities/media/prepareVideo.ts +54 -18
  39. package/src/activities/media/probeMediaStreams.test.ts +7 -8
  40. package/src/activities/media/probeMediaStreams.ts +11 -7
  41. package/src/activities/renditions/generateVideoRendition.ts +12 -6
  42. package/src/dsl/dsl-workflow.test.ts +4 -0
  43. package/src/dsl/dsl-workflow.ts +3 -0
@@ -1,20 +1,28 @@
1
1
  import { ApplicationFailure, log } from '@temporalio/activity';
2
- import type { DSLActivityExecutionPayload, DSLActivitySpec, JSONObject } from '@vertesia/common';
2
+ import {
3
+ type ContentMetadata,
4
+ type ContentSource,
5
+ type DSLActivityExecutionPayload,
6
+ type DSLActivitySpec,
7
+ type JSONObject,
8
+ mergePreservingNonExtractable,
9
+ PDF_RENDITION_NAME,
10
+ type Rendition,
11
+ schemaForExtraction,
12
+ } from '@vertesia/common';
3
13
  import { setupActivity } from '../dsl/setup/ActivityContext.js';
14
+ import { md5 } from '../utils/blobs.js';
4
15
  import { type TruncateSpec, truncByMaxTokens } from '../utils/tokens.js';
5
16
  import { executeInteractionFromActivity, type InteractionExecutionParams } from './executeInteraction.js';
6
17
 
7
18
  const INT_EXTRACT_INFORMATION = 'sys:ExtractInformation';
8
19
 
20
+ export type GenerateDocumentPropertiesSource = 'auto' | 'text' | 'vision' | 'mixed';
21
+
9
22
  interface RetryableError extends Error {
10
23
  retryable?: boolean;
11
24
  }
12
25
 
13
- interface GeneratedPropertiesSummary {
14
- title?: unknown;
15
- description?: unknown;
16
- }
17
-
18
26
  function toRetryableError(error: unknown): RetryableError {
19
27
  if (error instanceof Error) {
20
28
  return error as RetryableError;
@@ -31,57 +39,209 @@ export interface GenerateDocumentPropertiesParams extends InteractionExecutionPa
31
39
 
32
40
  interactionName?: string;
33
41
 
42
+ /**
43
+ * @deprecated Use source: 'mixed'. Accepted for backward compatibility with existing DSL callers.
44
+ */
34
45
  use_vision?: boolean;
46
+
47
+ source?: GenerateDocumentPropertiesSource;
48
+
49
+ instructions?: string;
50
+
51
+ /**
52
+ * Scoped page-image evidence prepared by intake (`prepareVisionEvidence` scratch refs).
53
+ * When the param is PRESENT (even as an empty array) and the source calls for visual
54
+ * evidence, these refs are the ONLY visual evidence used — never the whole-object
55
+ * `store:{id}` ref, which resolves to ALL pages near-full-res through the rendition
56
+ * generate-and-poll path. An empty array means intake prepared no usable evidence
57
+ * (budget-impossible or no renderable source): extraction then runs text-only or fails
58
+ * with no-source instead of shipping an over-budget payload.
59
+ * Absent = legacy behavior for existing DSL callers.
60
+ */
61
+ evidence_images?: ContentSource[];
62
+
63
+ /**
64
+ * Opt-in freshness guard. When true, the activity computes a fingerprint of the extraction
65
+ * inputs (content etag, type + its object schema, source, instructions, interaction name)
66
+ * and returns `{ status: 'skipped' }` without executing the interaction when the fingerprint
67
+ * stored by a previous successful extraction matches and the object already has non-empty
68
+ * properties. Defaults to false so shared DSL callers keep the always-extract behavior.
69
+ * `payload.vars.forceGeneration` bypasses the skip.
70
+ */
71
+ skip_if_fresh?: boolean;
35
72
  }
36
73
  export interface GenerateDocumentProperties extends DSLActivitySpec<GenerateDocumentPropertiesParams> {
37
74
  name: 'generateDocumentProperties';
38
75
  }
76
+ export type GenerateDocumentPropertiesResult =
77
+ | { status: 'completed' }
78
+ | { status: 'failed'; error: string }
79
+ | { document: string; status: 'skipped'; message: string };
80
+
81
+ /**
82
+ * Stable fingerprint of the inputs that determine the extraction output. Key order is fixed by
83
+ * the object literal so the hash is deterministic.
84
+ */
85
+ export function computeExtractionFingerprint(input: {
86
+ content_etag?: string;
87
+ type_id?: string;
88
+ source: GenerateDocumentPropertiesSource;
89
+ instructions?: string;
90
+ interactionName: string;
91
+ /** The type's object_schema: a schema edit under the same type id must invalidate the fingerprint. */
92
+ object_schema?: unknown;
93
+ /** Scoped vision evidence refs: a different page/profile selection must invalidate the fingerprint.
94
+ * Undefined (legacy callers) keeps previously stored hashes valid. */
95
+ evidence?: string[];
96
+ /** The object's text etag: text can change under the same content etag (re-render,
97
+ * re-conversion, manual edit) and text-consuming extractions must invalidate.
98
+ * Undefined (vision-only callers) keeps previously stored hashes valid. */
99
+ text_etag?: string | null;
100
+ }): string {
101
+ return md5(
102
+ JSON.stringify({
103
+ content_etag: input.content_etag,
104
+ type_id: input.type_id,
105
+ source: input.source,
106
+ instructions: input.instructions,
107
+ interactionName: input.interactionName,
108
+ object_schema: input.object_schema,
109
+ ...(input.evidence ? { evidence: input.evidence } : {}),
110
+ ...(input.text_etag !== undefined ? { text_etag: input.text_etag } : {}),
111
+ }),
112
+ );
113
+ }
114
+
115
+ /**
116
+ * The fingerprint is persisted inside the generation run info of the last successful extraction
117
+ * (the server appends it to `metadata.generation_runs`). Other writers append runs without a
118
+ * fingerprint, so scan backwards for the most recent extraction entry.
119
+ */
120
+ function getStoredExtractionFingerprint(metadata: ContentMetadata | undefined): string | undefined {
121
+ const runs = metadata?.generation_runs;
122
+ if (!Array.isArray(runs)) {
123
+ return undefined;
124
+ }
125
+ for (let i = runs.length - 1; i >= 0; i--) {
126
+ if (runs[i]?.extraction_fingerprint) {
127
+ return runs[i].extraction_fingerprint;
128
+ }
129
+ }
130
+ return undefined;
131
+ }
132
+
133
+ function getPdfRenditionContent(doc: { metadata?: { renditions?: unknown } }): ContentSource | undefined {
134
+ const renditions = doc.metadata?.renditions;
135
+ if (!Array.isArray(renditions)) {
136
+ return undefined;
137
+ }
138
+ const pdfRendition = renditions.find(
139
+ (rendition): rendition is Rendition =>
140
+ typeof rendition === 'object' &&
141
+ rendition !== null &&
142
+ (rendition as { name?: unknown }).name === PDF_RENDITION_NAME &&
143
+ typeof (rendition as { content?: { source?: unknown } }).content?.source === 'string',
144
+ );
145
+ return pdfRendition?.content;
146
+ }
39
147
 
40
148
  export async function generateDocumentProperties(
41
149
  payload: DSLActivityExecutionPayload<GenerateDocumentPropertiesParams>,
42
- ) {
150
+ ): Promise<GenerateDocumentPropertiesResult> {
43
151
  const context = await setupActivity<GenerateDocumentPropertiesParams>(payload);
44
152
  const { params, client, objectId } = context;
45
153
  const interactionName = params.interactionName ?? INT_EXTRACT_INFORMATION;
46
154
 
47
155
  const project = await context.fetchProject();
48
156
 
49
- const doc = await client.objects.retrieve(objectId, '+text');
157
+ const doc = await client.objects.retrieve(objectId, '+text +metadata +content');
50
158
  const type = doc.type ? await client.types.catalog.resolve(doc.type) : undefined;
51
159
 
52
- if (!doc?.text && !params.use_vision && !doc?.content?.type?.startsWith('image/')) {
53
- log.warn(`Object ${objectId} not found or text is empty`);
54
- return { status: 'failed', error: 'no-text' };
55
- }
56
-
57
160
  if (!type?.object_schema) {
58
161
  log.info(`Object ${objectId} has no schema`);
59
162
  return { document: objectId, status: 'skipped', message: 'no schema defined on type' };
60
163
  }
61
164
 
62
- const getImageRef = () => {
165
+ const source = params.source ?? (params.use_vision ? 'mixed' : 'auto');
166
+ const evidenceProvided = Array.isArray(params.evidence_images);
167
+ const evidenceImages = params.evidence_images?.length ? params.evidence_images : undefined;
168
+
169
+ const extractionFingerprint = computeExtractionFingerprint({
170
+ content_etag: doc.content?.etag,
171
+ type_id: type.id,
172
+ source,
173
+ instructions: params.instructions,
174
+ interactionName,
175
+ object_schema: type.object_schema,
176
+ evidence: evidenceImages?.map((image) => image.source ?? image.name ?? ''),
177
+ // text may change under the same content etag; only vision-only extraction ignores it
178
+ text_etag: source === 'vision' ? undefined : (doc.text_etag ?? null),
179
+ });
180
+
181
+ if (params.skip_if_fresh && !payload.vars?.forceGeneration) {
182
+ const storedFingerprint = getStoredExtractionFingerprint(doc.metadata);
183
+ const hasProperties = !!doc.properties && Object.keys(doc.properties).length > 0;
184
+ if (storedFingerprint === extractionFingerprint && hasProperties) {
185
+ log.info(`Skipping property extraction for ${objectId}: extraction inputs unchanged`, {
186
+ extractionFingerprint,
187
+ });
188
+ return {
189
+ document: objectId,
190
+ status: 'skipped',
191
+ message: 'properties are fresh (extraction fingerprint matches)',
192
+ };
193
+ }
194
+ }
195
+
196
+ const getImageRef = (): string | ContentSource | undefined => {
63
197
  if (doc.content?.type?.startsWith('image/')) {
64
198
  return `store:${doc.id}`;
65
199
  }
66
200
 
67
- if (params.use_vision && doc.content?.type?.startsWith('application/pdf')) {
201
+ if (doc.content?.type?.startsWith('application/pdf')) {
68
202
  return `store:${doc.id}`;
69
203
  }
70
204
 
205
+ const pdfRendition = getPdfRenditionContent(doc);
206
+ if (pdfRendition?.type?.startsWith('application/pdf')) {
207
+ return pdfRendition;
208
+ }
209
+
71
210
  log.info(`Object ${objectId} is not an image or pdf`);
72
211
  return undefined;
73
212
  };
74
213
 
75
- const content = doc.text ? truncByMaxTokens(doc.text, params.truncate || 30000) : undefined;
214
+ const hasRealText = !!doc.text?.trim();
215
+ const content =
216
+ hasRealText && (source === 'auto' || source === 'text' || source === 'mixed')
217
+ ? truncByMaxTokens(doc.text ?? '', params.truncate || 30000)
218
+ : undefined;
219
+ const needsVisualEvidence = source === 'vision' || source === 'mixed' || (source === 'auto' && !content);
220
+ // Scoped scratch page refs win over the whole-object store: ref (which resolves to ALL
221
+ // pages near-full-res through the rendition generate-and-poll path). A PRESENT param —
222
+ // even empty — disables the store-ref fallback entirely: intake decided what evidence
223
+ // (if any) may be sent.
224
+ const scopedImages = needsVisualEvidence ? evidenceImages : undefined;
225
+ const imageRef = needsVisualEvidence && !evidenceProvided ? getImageRef() : undefined;
226
+
227
+ if (!content && !imageRef && !scopedImages) {
228
+ log.warn(`Object ${objectId} has no text or supported vision source`);
229
+ return { status: 'failed', error: 'no-source' };
230
+ }
76
231
 
77
232
  const promptData = {
78
233
  content: content,
79
- image: getImageRef() ?? undefined,
234
+ image: imageRef,
235
+ images: scopedImages,
236
+ extraction_instructions: params.instructions,
80
237
  human_context: project?.configuration?.human_context ?? undefined,
81
238
  };
82
239
 
240
+ // Strip properties marked x-extract:false so constrained decoding cannot invent them.
241
+ const extractionSchema = schemaForExtraction(type.object_schema);
242
+
83
243
  log.info(
84
- ` Extracting information from object ${objectId} with type ${type.name}`,
244
+ `Extracting information from object ${objectId} with type ${type.name}`,
85
245
  payload.debug_mode ? { params } : undefined,
86
246
  );
87
247
 
@@ -93,7 +253,7 @@ export async function generateDocumentProperties(
93
253
  {
94
254
  ...params,
95
255
  include_previous_error: true,
96
- result_schema: type.object_schema,
256
+ result_schema: extractionSchema,
97
257
  validate_result: type.strict_mode,
98
258
  },
99
259
  promptData,
@@ -125,38 +285,22 @@ export async function generateDocumentProperties(
125
285
  throw error;
126
286
  }
127
287
 
128
- const getText = () => {
129
- if (doc.text) {
130
- return undefined;
131
- }
132
- let text = '';
133
- const jsonResult = infoRes.result.object<GeneratedPropertiesSummary>();
134
- if (typeof jsonResult.title === 'string') {
135
- text += `${jsonResult.title}\n`;
136
- }
137
- if (typeof jsonResult.description === 'string') {
138
- text += jsonResult.description;
139
- }
140
- if (text) {
141
- return text;
142
- } else {
143
- return undefined;
144
- }
145
- };
146
-
147
288
  log.info(`Extracted information from object ${objectId} with type ${type.name}`, { runId: infoRes.id });
289
+ const extracted = infoRes.result.object<JSONObject>() ?? {};
290
+ const existing =
291
+ doc.properties && typeof doc.properties === 'object' && !Array.isArray(doc.properties)
292
+ ? (doc.properties as Record<string, unknown>)
293
+ : {};
294
+ const properties = mergePreservingNonExtractable(existing, extracted, type.object_schema) as JSONObject;
148
295
  await client.objects.update(
149
296
  doc.id,
150
297
  {
151
- properties: {
152
- ...infoRes.result.object<JSONObject>(),
153
- ...(doc.text_etag !== undefined ? { etag: doc.text_etag } : {}),
154
- },
155
- text: getText(),
298
+ properties,
156
299
  generation_run_info: {
157
300
  id: infoRes.id,
158
301
  date: new Date().toISOString(),
159
302
  model: infoRes.modelId ?? '',
303
+ extraction_fingerprint: extractionFingerprint,
160
304
  },
161
305
  },
162
306
  { suppressWorkflows: true },
@@ -0,0 +1,196 @@
1
+ import { MockActivityEnvironment } from '@temporalio/testing';
2
+ import type { VertesiaClient } from '@vertesia/client';
3
+ import {
4
+ ContentEventName,
5
+ type ContentObjectTypeItem,
6
+ type DSLActivityExecutionPayload,
7
+ PDF_RENDITION_NAME,
8
+ type Rendition,
9
+ } from '@vertesia/common';
10
+ import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
11
+ import type { ActivityContext } from '../dsl/setup/ActivityContext.js';
12
+ import { executeInteractionFromActivity } from './executeInteraction.js';
13
+ import {
14
+ type GenerateOrAssignContentTypeParams,
15
+ type GenerateOrAssignContentTypeResult,
16
+ generateOrAssignContentType,
17
+ } from './generateOrAssignContentType.js';
18
+
19
+ vi.mock('../dsl/setup/ActivityContext.js', async (importOriginal) => {
20
+ const actual = await importOriginal<typeof import('../dsl/setup/ActivityContext.js')>();
21
+ return { ...actual, setupActivity: vi.fn() };
22
+ });
23
+
24
+ vi.mock('./executeInteraction.js', async (importOriginal) => {
25
+ const actual = await importOriginal<typeof import('./executeInteraction.js')>();
26
+ return { ...actual, executeInteractionFromActivity: vi.fn() };
27
+ });
28
+
29
+ let testEnv: MockActivityEnvironment;
30
+
31
+ beforeAll(() => {
32
+ testEnv = new MockActivityEnvironment();
33
+ });
34
+
35
+ beforeEach(() => {
36
+ vi.clearAllMocks();
37
+ });
38
+
39
+ function payload(
40
+ params: GenerateOrAssignContentTypeParams,
41
+ ): DSLActivityExecutionPayload<GenerateOrAssignContentTypeParams> {
42
+ return {
43
+ account_id: 'test-account',
44
+ activity: { name: 'generateOrAssignContentType', params: {} },
45
+ auth_token: 'mock-token',
46
+ config: { studio_url: 'http://mock-studio', store_url: 'http://mock-store' },
47
+ event: ContentEventName.create,
48
+ input: { inputType: 'objectIds', objectIds: ['object-1'] },
49
+ objectIds: ['object-1'],
50
+ params,
51
+ project_id: 'test-project',
52
+ vars: {},
53
+ workflow_name: 'test-workflow',
54
+ };
55
+ }
56
+
57
+ function typeItem(name: string, status?: ContentObjectTypeItem['status']): ContentObjectTypeItem {
58
+ return {
59
+ id: `type-${name}`,
60
+ name,
61
+ status,
62
+ } as ContentObjectTypeItem;
63
+ }
64
+
65
+ function mockSelectionResult(documentType: string) {
66
+ vi.mocked(executeInteractionFromActivity).mockResolvedValue({
67
+ id: 'run-1',
68
+ modelId: 'model-1',
69
+ result: {
70
+ object: vi.fn(() => ({ document_type: documentType })),
71
+ },
72
+ } as unknown as Awaited<ReturnType<typeof executeInteractionFromActivity>>);
73
+ }
74
+
75
+ async function mockSetup(
76
+ types: ContentObjectTypeItem[],
77
+ options: {
78
+ contentType?: string;
79
+ renditions?: Rendition[];
80
+ text?: string;
81
+ } = {},
82
+ ) {
83
+ const { setupActivity } = await import('../dsl/setup/ActivityContext.js');
84
+ const update = vi.fn(() => Promise.resolve({}));
85
+ const retrieveObject = vi.fn(() =>
86
+ Promise.resolve({
87
+ id: 'object-1',
88
+ content: { type: options.contentType ?? 'text/plain' },
89
+ metadata: { renditions: options.renditions },
90
+ text: options.text ?? 'Document text',
91
+ }),
92
+ );
93
+ const listTypes = vi.fn(() => Promise.resolve(types));
94
+
95
+ vi.mocked(setupActivity).mockImplementation((activityPayload) =>
96
+ Promise.resolve({
97
+ client: {
98
+ objects: {
99
+ retrieve: retrieveObject,
100
+ update,
101
+ },
102
+ types: {
103
+ catalog: {
104
+ list: listTypes,
105
+ },
106
+ },
107
+ } as unknown as VertesiaClient,
108
+ fetchProject: vi.fn(() => Promise.resolve({ configuration: {} })),
109
+ objectId: 'object-1',
110
+ params: activityPayload.params,
111
+ } as unknown as ActivityContext<GenerateOrAssignContentTypeParams>),
112
+ );
113
+
114
+ return { listTypes, update };
115
+ }
116
+
117
+ describe('generateOrAssignContentType', () => {
118
+ it('excludes draft types from selection and does not assign them if returned by the model', async () => {
119
+ const { update } = await mockSetup([
120
+ typeItem('Invoice', 'active'),
121
+ typeItem('DraftInvoice', 'draft'),
122
+ typeItem('DocumentPart', 'active'),
123
+ typeItem('GenericDocument', 'active'),
124
+ typeItem('Rendition', 'active'),
125
+ ]);
126
+ mockSelectionResult('DraftInvoice');
127
+
128
+ const result: GenerateOrAssignContentTypeResult = await testEnv.run(
129
+ generateOrAssignContentType,
130
+ payload({ allowNewContentTypes: false }),
131
+ );
132
+
133
+ expect(executeInteractionFromActivity).toHaveBeenCalledWith(
134
+ expect.anything(),
135
+ 'sys:SelectDocumentType',
136
+ expect.objectContaining({ allowNewContentTypes: false }),
137
+ expect.objectContaining({
138
+ existing_types: [
139
+ expect.objectContaining({ name: 'Invoice' }),
140
+ expect.objectContaining({ name: 'GenericDocument' }),
141
+ ],
142
+ }),
143
+ );
144
+ expect(update).toHaveBeenCalledWith('object-1', { type: 'sys:GenericDocument' }, { suppressWorkflows: true });
145
+ expect(result).toEqual({
146
+ id: 'sys:GenericDocument',
147
+ isNew: false,
148
+ name: 'GenericDocument',
149
+ });
150
+ });
151
+
152
+ it('uses a PDF rendition as visual evidence for PowerPoint type selection', async () => {
153
+ const pdfRendition = {
154
+ name: PDF_RENDITION_NAME,
155
+ content: {
156
+ name: 'slides.pdf',
157
+ source: 'renditions/source-etag/slides.pdf',
158
+ type: 'application/pdf',
159
+ },
160
+ };
161
+ const { update } = await mockSetup(
162
+ [typeItem('MarketPresentation', 'active'), typeItem('GenericDocument', 'active')],
163
+ {
164
+ contentType: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
165
+ renditions: [pdfRendition],
166
+ text: '',
167
+ },
168
+ );
169
+ mockSelectionResult('MarketPresentation');
170
+
171
+ const result: GenerateOrAssignContentTypeResult = await testEnv.run(
172
+ generateOrAssignContentType,
173
+ payload({ allowNewContentTypes: false }),
174
+ );
175
+
176
+ expect(executeInteractionFromActivity).toHaveBeenCalledWith(
177
+ expect.anything(),
178
+ 'sys:SelectDocumentType',
179
+ expect.objectContaining({ allowNewContentTypes: false }),
180
+ expect.objectContaining({
181
+ content: undefined,
182
+ image: pdfRendition.content,
183
+ }),
184
+ );
185
+ expect(update).toHaveBeenCalledWith(
186
+ 'object-1',
187
+ { type: 'type-MarketPresentation' },
188
+ { suppressWorkflows: true },
189
+ );
190
+ expect(result).toEqual({
191
+ id: 'type-MarketPresentation',
192
+ isNew: false,
193
+ name: 'MarketPresentation',
194
+ });
195
+ });
196
+ });