@vertesia/workflow 1.5.0-dev.20260714.072725Z → 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.
- package/lib/activities/generateDocumentProperties.d.ts +54 -13
- package/lib/activities/generateDocumentProperties.d.ts.map +1 -1
- package/lib/activities/generateDocumentProperties.js +109 -34
- package/lib/activities/generateDocumentProperties.js.map +1 -1
- package/lib/activities/generateEmbeddings.d.ts.map +1 -1
- package/lib/activities/generateEmbeddings.js +14 -22
- package/lib/activities/generateEmbeddings.js.map +1 -1
- package/lib/activities/generateOrAssignContentType.d.ts +6 -16
- package/lib/activities/generateOrAssignContentType.d.ts.map +1 -1
- package/lib/activities/generateOrAssignContentType.js +70 -19
- package/lib/activities/generateOrAssignContentType.js.map +1 -1
- package/lib/activities/media/exec.d.ts +60 -0
- package/lib/activities/media/exec.d.ts.map +1 -0
- package/lib/activities/media/exec.js +212 -0
- package/lib/activities/media/exec.js.map +1 -0
- package/lib/activities/media/prepareAudio.d.ts.map +1 -1
- package/lib/activities/media/prepareAudio.js +7 -6
- package/lib/activities/media/prepareAudio.js.map +1 -1
- package/lib/activities/media/prepareVideo.d.ts +5 -0
- package/lib/activities/media/prepareVideo.d.ts.map +1 -1
- package/lib/activities/media/prepareVideo.js +37 -13
- package/lib/activities/media/prepareVideo.js.map +1 -1
- package/lib/activities/media/probeMediaStreams.d.ts.map +1 -1
- package/lib/activities/media/probeMediaStreams.js +7 -6
- package/lib/activities/media/probeMediaStreams.js.map +1 -1
- package/lib/activities/renditions/generateVideoRendition.d.ts.map +1 -1
- package/lib/activities/renditions/generateVideoRendition.js +7 -5
- package/lib/activities/renditions/generateVideoRendition.js.map +1 -1
- package/lib/dsl/dsl-workflow.js +3 -0
- package/lib/dsl/dsl-workflow.js.map +1 -1
- package/lib/dsl/dslProxyActivities.d.ts +1 -0
- package/lib/dsl/dslProxyActivities.d.ts.map +1 -1
- package/lib/dsl/dslProxyActivities.js +9 -0
- package/lib/dsl/dslProxyActivities.js.map +1 -1
- package/lib/errors.d.ts.map +1 -1
- package/lib/errors.js +5 -0
- package/lib/errors.js.map +1 -1
- package/lib/workflows-bundle.js +16111 -6668
- package/package.json +17 -16
- package/src/activities/generateDocumentProperties.test.ts +642 -0
- package/src/activities/generateDocumentProperties.ts +187 -43
- package/src/activities/generateEmbeddings.test.ts +114 -0
- package/src/activities/generateEmbeddings.ts +17 -24
- package/src/activities/generateOrAssignContentType.test.ts +196 -0
- package/src/activities/generateOrAssignContentType.ts +115 -30
- package/src/activities/media/exec.test.ts +194 -0
- package/src/activities/media/exec.ts +265 -0
- package/src/activities/media/prepareAudio.ts +12 -7
- package/src/activities/media/prepareVideo.test.ts +27 -0
- package/src/activities/media/prepareVideo.ts +54 -18
- package/src/activities/media/probeMediaStreams.test.ts +7 -8
- package/src/activities/media/probeMediaStreams.ts +11 -7
- package/src/activities/renditions/generateVideoRendition.ts +12 -6
- package/src/dsl/dsl-workflow.test.ts +4 -0
- package/src/dsl/dsl-workflow.ts +3 -0
- package/src/dsl/dslProxyActivities.test.ts +23 -0
- package/src/dsl/dslProxyActivities.ts +11 -0
- package/src/errors.ts +5 -0
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
import { MockActivityEnvironment } from '@temporalio/testing';
|
|
2
|
+
import type { VertesiaClient } from '@vertesia/client';
|
|
3
|
+
import {
|
|
4
|
+
ContentEventName,
|
|
5
|
+
type DSLActivityExecutionPayload,
|
|
6
|
+
type GenerationRunMetadata,
|
|
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
|
+
computeExtractionFingerprint,
|
|
15
|
+
type GenerateDocumentPropertiesParams,
|
|
16
|
+
type GenerateDocumentPropertiesResult,
|
|
17
|
+
generateDocumentProperties,
|
|
18
|
+
} from './generateDocumentProperties.js';
|
|
19
|
+
|
|
20
|
+
vi.mock('../dsl/setup/ActivityContext.js', async (importOriginal) => {
|
|
21
|
+
const actual = await importOriginal<typeof import('../dsl/setup/ActivityContext.js')>();
|
|
22
|
+
return { ...actual, setupActivity: vi.fn() };
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
vi.mock('./executeInteraction.js', async (importOriginal) => {
|
|
26
|
+
const actual = await importOriginal<typeof import('./executeInteraction.js')>();
|
|
27
|
+
return { ...actual, executeInteractionFromActivity: vi.fn() };
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
let testEnv: MockActivityEnvironment;
|
|
31
|
+
|
|
32
|
+
beforeAll(() => {
|
|
33
|
+
testEnv = new MockActivityEnvironment();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
vi.clearAllMocks();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
function mockExtractionResult(properties: Record<string, unknown>) {
|
|
41
|
+
vi.mocked(executeInteractionFromActivity).mockResolvedValue({
|
|
42
|
+
id: 'run-1',
|
|
43
|
+
modelId: 'model-1',
|
|
44
|
+
result: {
|
|
45
|
+
object: vi.fn(() => properties),
|
|
46
|
+
},
|
|
47
|
+
} as unknown as Awaited<ReturnType<typeof executeInteractionFromActivity>>);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function payload(
|
|
51
|
+
params: GenerateDocumentPropertiesParams,
|
|
52
|
+
vars: Record<string, unknown> = {},
|
|
53
|
+
): DSLActivityExecutionPayload<GenerateDocumentPropertiesParams> {
|
|
54
|
+
return {
|
|
55
|
+
auth_token: 'mock-token',
|
|
56
|
+
account_id: 'test-account',
|
|
57
|
+
project_id: 'test-project',
|
|
58
|
+
params,
|
|
59
|
+
config: { studio_url: 'http://mock-studio', store_url: 'http://mock-store' },
|
|
60
|
+
workflow_name: 'test-workflow',
|
|
61
|
+
event: ContentEventName.create,
|
|
62
|
+
objectIds: ['object-1'],
|
|
63
|
+
input: { inputType: 'objectIds', objectIds: ['object-1'] },
|
|
64
|
+
vars,
|
|
65
|
+
activity: { name: 'generateDocumentProperties', params: {} },
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const DEFAULT_OBJECT_SCHEMA = {
|
|
70
|
+
type: 'object',
|
|
71
|
+
properties: {
|
|
72
|
+
title: { type: 'string' },
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
async function mockSetup(options: {
|
|
77
|
+
text?: string;
|
|
78
|
+
contentType?: string;
|
|
79
|
+
contentEtag?: string;
|
|
80
|
+
objectSchema?: Record<string, unknown>;
|
|
81
|
+
renditions?: Rendition[];
|
|
82
|
+
properties?: Record<string, unknown>;
|
|
83
|
+
generationRuns?: GenerationRunMetadata[];
|
|
84
|
+
}) {
|
|
85
|
+
const { setupActivity } = await import('../dsl/setup/ActivityContext.js');
|
|
86
|
+
const update = vi.fn(async () => ({}));
|
|
87
|
+
const retrieveObject = vi.fn(async () => ({
|
|
88
|
+
id: 'object-1',
|
|
89
|
+
text: options.text,
|
|
90
|
+
text_etag: 'source-etag',
|
|
91
|
+
properties: options.properties,
|
|
92
|
+
content: {
|
|
93
|
+
type: options.contentType,
|
|
94
|
+
etag: options.contentEtag,
|
|
95
|
+
},
|
|
96
|
+
metadata: {
|
|
97
|
+
renditions: options.renditions,
|
|
98
|
+
generation_runs: options.generationRuns,
|
|
99
|
+
},
|
|
100
|
+
type: { id: 'type-1', name: 'Invoice', ref_type: 'stored' },
|
|
101
|
+
}));
|
|
102
|
+
const resolveType = vi.fn(async () => ({
|
|
103
|
+
id: 'type-1',
|
|
104
|
+
name: 'Invoice',
|
|
105
|
+
object_schema: options.objectSchema ?? DEFAULT_OBJECT_SCHEMA,
|
|
106
|
+
}));
|
|
107
|
+
|
|
108
|
+
vi.mocked(setupActivity).mockImplementation(
|
|
109
|
+
async (activityPayload) =>
|
|
110
|
+
({
|
|
111
|
+
objectId: 'object-1',
|
|
112
|
+
params: activityPayload.params,
|
|
113
|
+
fetchProject: vi.fn(async () => ({ configuration: {} })),
|
|
114
|
+
client: {
|
|
115
|
+
objects: {
|
|
116
|
+
retrieve: retrieveObject,
|
|
117
|
+
update,
|
|
118
|
+
},
|
|
119
|
+
types: {
|
|
120
|
+
catalog: {
|
|
121
|
+
resolve: resolveType,
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
} as unknown as VertesiaClient,
|
|
125
|
+
}) as unknown as ActivityContext<GenerateDocumentPropertiesParams>,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
return { retrieveObject, resolveType, update };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
describe('generateDocumentProperties', () => {
|
|
132
|
+
it('does not execute extraction when no text or supported vision source is available', async () => {
|
|
133
|
+
await mockSetup({
|
|
134
|
+
contentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const result: GenerateDocumentPropertiesResult = await testEnv.run(
|
|
138
|
+
generateDocumentProperties,
|
|
139
|
+
payload({ source: 'vision' }),
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
expect(result).toEqual({ status: 'failed', error: 'no-source' });
|
|
143
|
+
expect(executeInteractionFromActivity).not.toHaveBeenCalled();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('uses a PDF rendition as vision evidence for non-visual source documents', async () => {
|
|
147
|
+
const pdfRendition = {
|
|
148
|
+
name: PDF_RENDITION_NAME,
|
|
149
|
+
content: {
|
|
150
|
+
name: 'slides.pdf',
|
|
151
|
+
source: 'renditions/source-etag/slides.pdf',
|
|
152
|
+
type: 'application/pdf',
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
await mockSetup({
|
|
156
|
+
contentType: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
157
|
+
renditions: [pdfRendition],
|
|
158
|
+
});
|
|
159
|
+
mockExtractionResult({ title: 'Presentation' });
|
|
160
|
+
|
|
161
|
+
const result: GenerateDocumentPropertiesResult = await testEnv.run(
|
|
162
|
+
generateDocumentProperties,
|
|
163
|
+
payload({ source: 'vision' }),
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
expect(result).toEqual({ status: 'completed' });
|
|
167
|
+
expect(executeInteractionFromActivity).toHaveBeenCalledWith(
|
|
168
|
+
expect.anything(),
|
|
169
|
+
'sys:ExtractInformation',
|
|
170
|
+
expect.anything(),
|
|
171
|
+
expect.objectContaining({
|
|
172
|
+
content: undefined,
|
|
173
|
+
image: pdfRendition.content,
|
|
174
|
+
}),
|
|
175
|
+
false,
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it.each([
|
|
180
|
+
{
|
|
181
|
+
source: 'auto' as const,
|
|
182
|
+
text: 'PDF text content',
|
|
183
|
+
expectedContent: 'PDF text content',
|
|
184
|
+
expectedImage: undefined,
|
|
185
|
+
},
|
|
186
|
+
{ source: 'auto' as const, text: undefined, expectedContent: undefined, expectedImage: 'store:object-1' },
|
|
187
|
+
{
|
|
188
|
+
source: 'text' as const,
|
|
189
|
+
text: 'PDF text content',
|
|
190
|
+
expectedContent: 'PDF text content',
|
|
191
|
+
expectedImage: undefined,
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
source: 'vision' as const,
|
|
195
|
+
text: 'PDF text content',
|
|
196
|
+
expectedContent: undefined,
|
|
197
|
+
expectedImage: 'store:object-1',
|
|
198
|
+
},
|
|
199
|
+
{ source: 'vision' as const, text: undefined, expectedContent: undefined, expectedImage: 'store:object-1' },
|
|
200
|
+
{
|
|
201
|
+
source: 'mixed' as const,
|
|
202
|
+
text: 'PDF text content',
|
|
203
|
+
expectedContent: 'PDF text content',
|
|
204
|
+
expectedImage: 'store:object-1',
|
|
205
|
+
},
|
|
206
|
+
{ source: 'mixed' as const, text: undefined, expectedContent: undefined, expectedImage: 'store:object-1' },
|
|
207
|
+
])('uses $source extraction source with text=$text', async ({ source, text, expectedContent, expectedImage }) => {
|
|
208
|
+
await mockSetup({
|
|
209
|
+
text,
|
|
210
|
+
contentType: 'application/pdf',
|
|
211
|
+
});
|
|
212
|
+
mockExtractionResult({ title: source });
|
|
213
|
+
|
|
214
|
+
const result: GenerateDocumentPropertiesResult = await testEnv.run(
|
|
215
|
+
generateDocumentProperties,
|
|
216
|
+
payload({ source }),
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
expect(result).toEqual({ status: 'completed' });
|
|
220
|
+
expect(executeInteractionFromActivity).toHaveBeenCalledWith(
|
|
221
|
+
expect.anything(),
|
|
222
|
+
'sys:ExtractInformation',
|
|
223
|
+
expect.anything(),
|
|
224
|
+
expect.objectContaining({
|
|
225
|
+
content: expectedContent,
|
|
226
|
+
image: expectedImage,
|
|
227
|
+
}),
|
|
228
|
+
false,
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it.each([
|
|
233
|
+
{ source: 'text' as const, text: undefined },
|
|
234
|
+
{ source: 'text' as const, text: ' ' },
|
|
235
|
+
])('fails for $source extraction when text is absent or whitespace-only', async ({ source, text }) => {
|
|
236
|
+
await mockSetup({
|
|
237
|
+
text,
|
|
238
|
+
contentType: 'application/pdf',
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const result: GenerateDocumentPropertiesResult = await testEnv.run(
|
|
242
|
+
generateDocumentProperties,
|
|
243
|
+
payload({ source }),
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
expect(result).toEqual({ status: 'failed', error: 'no-source' });
|
|
247
|
+
expect(executeInteractionFromActivity).not.toHaveBeenCalled();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('accepts deprecated use_vision as a mixed source alias', async () => {
|
|
251
|
+
await mockSetup({
|
|
252
|
+
text: 'PDF text content',
|
|
253
|
+
contentType: 'application/pdf',
|
|
254
|
+
});
|
|
255
|
+
mockExtractionResult({ title: 'Mixed alias' });
|
|
256
|
+
|
|
257
|
+
const result: GenerateDocumentPropertiesResult = await testEnv.run(
|
|
258
|
+
generateDocumentProperties,
|
|
259
|
+
payload({ use_vision: true }),
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
expect(result).toEqual({ status: 'completed' });
|
|
263
|
+
expect(executeInteractionFromActivity).toHaveBeenCalledWith(
|
|
264
|
+
expect.anything(),
|
|
265
|
+
'sys:ExtractInformation',
|
|
266
|
+
expect.anything(),
|
|
267
|
+
expect.objectContaining({
|
|
268
|
+
content: 'PDF text content',
|
|
269
|
+
image: 'store:object-1',
|
|
270
|
+
}),
|
|
271
|
+
false,
|
|
272
|
+
);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe('scoped vision evidence (evidence_images contract)', () => {
|
|
276
|
+
const evidence = [
|
|
277
|
+
{ source: 'vision/etag-1/standard/page-7.jpeg', type: 'image/jpeg', name: 'page-7.jpeg' },
|
|
278
|
+
{ source: 'vision/etag-1/standard/page-3.jpeg', type: 'image/jpeg', name: 'page-3.jpeg' },
|
|
279
|
+
];
|
|
280
|
+
|
|
281
|
+
it('sends scratch page refs as images and never the store ref for vision', async () => {
|
|
282
|
+
await mockSetup({ text: 'PDF text content', contentType: 'application/pdf' });
|
|
283
|
+
mockExtractionResult({ title: 'Scoped' });
|
|
284
|
+
|
|
285
|
+
const result: GenerateDocumentPropertiesResult = await testEnv.run(
|
|
286
|
+
generateDocumentProperties,
|
|
287
|
+
payload({ source: 'vision', evidence_images: evidence }),
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
expect(result).toEqual({ status: 'completed' });
|
|
291
|
+
expect(executeInteractionFromActivity).toHaveBeenCalledWith(
|
|
292
|
+
expect.anything(),
|
|
293
|
+
'sys:ExtractInformation',
|
|
294
|
+
expect.anything(),
|
|
295
|
+
expect.objectContaining({
|
|
296
|
+
content: undefined,
|
|
297
|
+
image: undefined,
|
|
298
|
+
images: evidence,
|
|
299
|
+
}),
|
|
300
|
+
false,
|
|
301
|
+
);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('sends text AND scratch page refs for mixed', async () => {
|
|
305
|
+
await mockSetup({ text: 'PDF text content', contentType: 'application/pdf' });
|
|
306
|
+
mockExtractionResult({ title: 'Mixed scoped' });
|
|
307
|
+
|
|
308
|
+
await testEnv.run(generateDocumentProperties, payload({ source: 'mixed', evidence_images: evidence }));
|
|
309
|
+
|
|
310
|
+
expect(executeInteractionFromActivity).toHaveBeenCalledWith(
|
|
311
|
+
expect.anything(),
|
|
312
|
+
'sys:ExtractInformation',
|
|
313
|
+
expect.anything(),
|
|
314
|
+
expect.objectContaining({
|
|
315
|
+
content: 'PDF text content',
|
|
316
|
+
image: undefined,
|
|
317
|
+
images: evidence,
|
|
318
|
+
}),
|
|
319
|
+
false,
|
|
320
|
+
);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('fails with no-source (no store-ref fallback) when the provided evidence is empty and no text exists', async () => {
|
|
324
|
+
await mockSetup({ contentType: 'application/pdf' });
|
|
325
|
+
|
|
326
|
+
const result: GenerateDocumentPropertiesResult = await testEnv.run(
|
|
327
|
+
generateDocumentProperties,
|
|
328
|
+
payload({ source: 'vision', evidence_images: [] }),
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
expect(result).toEqual({ status: 'failed', error: 'no-source' });
|
|
332
|
+
expect(executeInteractionFromActivity).not.toHaveBeenCalled();
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('extracts from text only when the provided evidence is empty for mixed', async () => {
|
|
336
|
+
await mockSetup({ text: 'PDF text content', contentType: 'application/pdf' });
|
|
337
|
+
mockExtractionResult({ title: 'Text only' });
|
|
338
|
+
|
|
339
|
+
const result: GenerateDocumentPropertiesResult = await testEnv.run(
|
|
340
|
+
generateDocumentProperties,
|
|
341
|
+
payload({ source: 'mixed', evidence_images: [] }),
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
expect(result).toEqual({ status: 'completed' });
|
|
345
|
+
expect(executeInteractionFromActivity).toHaveBeenCalledWith(
|
|
346
|
+
expect.anything(),
|
|
347
|
+
'sys:ExtractInformation',
|
|
348
|
+
expect.anything(),
|
|
349
|
+
expect.objectContaining({
|
|
350
|
+
content: 'PDF text content',
|
|
351
|
+
image: undefined,
|
|
352
|
+
images: undefined,
|
|
353
|
+
}),
|
|
354
|
+
false,
|
|
355
|
+
);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('ignores evidence images for the text source', async () => {
|
|
359
|
+
await mockSetup({ text: 'PDF text content', contentType: 'application/pdf' });
|
|
360
|
+
mockExtractionResult({ title: 'Text' });
|
|
361
|
+
|
|
362
|
+
await testEnv.run(generateDocumentProperties, payload({ source: 'text', evidence_images: evidence }));
|
|
363
|
+
|
|
364
|
+
expect(executeInteractionFromActivity).toHaveBeenCalledWith(
|
|
365
|
+
expect.anything(),
|
|
366
|
+
'sys:ExtractInformation',
|
|
367
|
+
expect.anything(),
|
|
368
|
+
expect.objectContaining({
|
|
369
|
+
content: 'PDF text content',
|
|
370
|
+
image: undefined,
|
|
371
|
+
images: undefined,
|
|
372
|
+
}),
|
|
373
|
+
false,
|
|
374
|
+
);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it('folds the evidence refs into the extraction fingerprint (absent = legacy hash)', () => {
|
|
378
|
+
const base = {
|
|
379
|
+
content_etag: 'etag-1',
|
|
380
|
+
type_id: 'type-1',
|
|
381
|
+
source: 'vision' as const,
|
|
382
|
+
interactionName: 'sys:ExtractInformation',
|
|
383
|
+
object_schema: DEFAULT_OBJECT_SCHEMA,
|
|
384
|
+
};
|
|
385
|
+
const legacy = computeExtractionFingerprint(base);
|
|
386
|
+
const withEvidence = computeExtractionFingerprint({
|
|
387
|
+
...base,
|
|
388
|
+
evidence: ['vision/etag-1/standard/page-7.jpeg'],
|
|
389
|
+
});
|
|
390
|
+
const withOtherEvidence = computeExtractionFingerprint({
|
|
391
|
+
...base,
|
|
392
|
+
evidence: ['vision/etag-1/high/page-7.jpeg'],
|
|
393
|
+
});
|
|
394
|
+
expect(withEvidence).not.toBe(legacy);
|
|
395
|
+
expect(withEvidence).not.toBe(withOtherEvidence);
|
|
396
|
+
// undefined evidence must keep previously stored legacy hashes valid
|
|
397
|
+
expect(computeExtractionFingerprint({ ...base, evidence: undefined })).toBe(legacy);
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('updates properties without writing title or description into object text', async () => {
|
|
402
|
+
const { update } = await mockSetup({
|
|
403
|
+
text: 'source text',
|
|
404
|
+
contentType: 'text/plain',
|
|
405
|
+
});
|
|
406
|
+
mockExtractionResult({ title: 'Statement', description: 'Summary' });
|
|
407
|
+
|
|
408
|
+
const result: GenerateDocumentPropertiesResult = await testEnv.run(generateDocumentProperties, payload({}));
|
|
409
|
+
|
|
410
|
+
expect(result).toEqual({ status: 'completed' });
|
|
411
|
+
expect(update).toHaveBeenCalledWith(
|
|
412
|
+
'object-1',
|
|
413
|
+
expect.not.objectContaining({
|
|
414
|
+
text: expect.anything(),
|
|
415
|
+
}),
|
|
416
|
+
{ suppressWorkflows: true },
|
|
417
|
+
);
|
|
418
|
+
expect(update).toHaveBeenCalledWith(
|
|
419
|
+
'object-1',
|
|
420
|
+
expect.objectContaining({
|
|
421
|
+
properties: {
|
|
422
|
+
title: 'Statement',
|
|
423
|
+
description: 'Summary',
|
|
424
|
+
},
|
|
425
|
+
}),
|
|
426
|
+
{ suppressWorkflows: true },
|
|
427
|
+
);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('clears stale extractable properties omitted by re-extraction while preserving non-extractable fields', async () => {
|
|
431
|
+
const { update } = await mockSetup({
|
|
432
|
+
text: 'source text',
|
|
433
|
+
contentType: 'text/plain',
|
|
434
|
+
properties: {
|
|
435
|
+
po_number: 'STALE-HALLUCINATION',
|
|
436
|
+
total: 10,
|
|
437
|
+
match_id: 'erp-123',
|
|
438
|
+
},
|
|
439
|
+
objectSchema: {
|
|
440
|
+
type: 'object',
|
|
441
|
+
properties: {
|
|
442
|
+
po_number: { type: 'string' },
|
|
443
|
+
total: { type: 'number' },
|
|
444
|
+
match_id: { type: 'string', 'x-extract': false },
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
});
|
|
448
|
+
mockExtractionResult({ total: 12 });
|
|
449
|
+
|
|
450
|
+
const result: GenerateDocumentPropertiesResult = await testEnv.run(generateDocumentProperties, payload({}));
|
|
451
|
+
|
|
452
|
+
expect(result).toEqual({ status: 'completed' });
|
|
453
|
+
expect(update).toHaveBeenCalledWith(
|
|
454
|
+
'object-1',
|
|
455
|
+
expect.objectContaining({
|
|
456
|
+
properties: {
|
|
457
|
+
total: 12,
|
|
458
|
+
match_id: 'erp-123',
|
|
459
|
+
},
|
|
460
|
+
}),
|
|
461
|
+
{ suppressWorkflows: true },
|
|
462
|
+
);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
describe('skip_if_fresh freshness guard', () => {
|
|
466
|
+
const CONTENT_ETAG = 'content-etag-1';
|
|
467
|
+
const freshFingerprint = computeExtractionFingerprint({
|
|
468
|
+
content_etag: CONTENT_ETAG,
|
|
469
|
+
type_id: 'type-1',
|
|
470
|
+
source: 'auto',
|
|
471
|
+
instructions: undefined,
|
|
472
|
+
interactionName: 'sys:ExtractInformation',
|
|
473
|
+
object_schema: DEFAULT_OBJECT_SCHEMA,
|
|
474
|
+
// the mocked document carries text_etag 'source-etag'; text-consuming
|
|
475
|
+
// extraction fingerprints include it (text can change under one content etag)
|
|
476
|
+
text_etag: 'source-etag',
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
function freshSetupOptions(overrides: Parameters<typeof mockSetup>[0] = {}) {
|
|
480
|
+
return {
|
|
481
|
+
text: 'source text',
|
|
482
|
+
contentType: 'text/plain',
|
|
483
|
+
contentEtag: CONTENT_ETAG,
|
|
484
|
+
properties: { title: 'Statement' },
|
|
485
|
+
generationRuns: [
|
|
486
|
+
{
|
|
487
|
+
id: 'run-0',
|
|
488
|
+
date: '2026-01-01T00:00:00.000Z',
|
|
489
|
+
model: 'model-1',
|
|
490
|
+
extraction_fingerprint: freshFingerprint,
|
|
491
|
+
},
|
|
492
|
+
],
|
|
493
|
+
...overrides,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
it('skips extraction when the stored fingerprint matches and properties are present', async () => {
|
|
498
|
+
const { update } = await mockSetup(freshSetupOptions());
|
|
499
|
+
|
|
500
|
+
const result: GenerateDocumentPropertiesResult = await testEnv.run(
|
|
501
|
+
generateDocumentProperties,
|
|
502
|
+
payload({ skip_if_fresh: true }),
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
expect(result).toEqual({
|
|
506
|
+
document: 'object-1',
|
|
507
|
+
status: 'skipped',
|
|
508
|
+
message: expect.stringContaining('fresh'),
|
|
509
|
+
});
|
|
510
|
+
expect(executeInteractionFromActivity).not.toHaveBeenCalled();
|
|
511
|
+
expect(update).not.toHaveBeenCalled();
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it('scans backwards past generation runs written without a fingerprint', async () => {
|
|
515
|
+
await mockSetup(
|
|
516
|
+
freshSetupOptions({
|
|
517
|
+
generationRuns: [
|
|
518
|
+
{
|
|
519
|
+
id: 'run-0',
|
|
520
|
+
date: '2026-01-01T00:00:00.000Z',
|
|
521
|
+
model: 'model-1',
|
|
522
|
+
extraction_fingerprint: freshFingerprint,
|
|
523
|
+
},
|
|
524
|
+
{ id: 'run-1', date: '2026-01-02T00:00:00.000Z', model: 'model-2' },
|
|
525
|
+
],
|
|
526
|
+
}),
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
const result: GenerateDocumentPropertiesResult = await testEnv.run(
|
|
530
|
+
generateDocumentProperties,
|
|
531
|
+
payload({ skip_if_fresh: true }),
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
expect(result).toMatchObject({ status: 'skipped' });
|
|
535
|
+
expect(executeInteractionFromActivity).not.toHaveBeenCalled();
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it('re-extracts by default: the guard is opt-in', async () => {
|
|
539
|
+
await mockSetup(freshSetupOptions());
|
|
540
|
+
mockExtractionResult({ title: 'Statement' });
|
|
541
|
+
|
|
542
|
+
const result: GenerateDocumentPropertiesResult = await testEnv.run(generateDocumentProperties, payload({}));
|
|
543
|
+
|
|
544
|
+
expect(result).toEqual({ status: 'completed' });
|
|
545
|
+
expect(executeInteractionFromActivity).toHaveBeenCalled();
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
it('re-extracts when the stored fingerprint is stale', async () => {
|
|
549
|
+
await mockSetup(
|
|
550
|
+
freshSetupOptions({
|
|
551
|
+
generationRuns: [
|
|
552
|
+
{
|
|
553
|
+
id: 'run-0',
|
|
554
|
+
date: '2026-01-01T00:00:00.000Z',
|
|
555
|
+
model: 'model-1',
|
|
556
|
+
extraction_fingerprint: 'stale-fingerprint',
|
|
557
|
+
},
|
|
558
|
+
],
|
|
559
|
+
}),
|
|
560
|
+
);
|
|
561
|
+
mockExtractionResult({ title: 'Statement' });
|
|
562
|
+
|
|
563
|
+
const result: GenerateDocumentPropertiesResult = await testEnv.run(
|
|
564
|
+
generateDocumentProperties,
|
|
565
|
+
payload({ skip_if_fresh: true }),
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
expect(result).toEqual({ status: 'completed' });
|
|
569
|
+
expect(executeInteractionFromActivity).toHaveBeenCalled();
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
it('re-extracts when the type schema changed under the same type id', async () => {
|
|
573
|
+
await mockSetup(
|
|
574
|
+
freshSetupOptions({
|
|
575
|
+
objectSchema: {
|
|
576
|
+
type: 'object',
|
|
577
|
+
properties: {
|
|
578
|
+
title: { type: 'string' },
|
|
579
|
+
total: { type: 'number' },
|
|
580
|
+
},
|
|
581
|
+
},
|
|
582
|
+
}),
|
|
583
|
+
);
|
|
584
|
+
mockExtractionResult({ title: 'Statement', total: 12 });
|
|
585
|
+
|
|
586
|
+
const result: GenerateDocumentPropertiesResult = await testEnv.run(
|
|
587
|
+
generateDocumentProperties,
|
|
588
|
+
payload({ skip_if_fresh: true }),
|
|
589
|
+
);
|
|
590
|
+
|
|
591
|
+
expect(result).toEqual({ status: 'completed' });
|
|
592
|
+
expect(executeInteractionFromActivity).toHaveBeenCalled();
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
it('re-extracts when the fingerprint matches but the object has no properties', async () => {
|
|
596
|
+
await mockSetup(freshSetupOptions({ properties: {} }));
|
|
597
|
+
mockExtractionResult({ title: 'Statement' });
|
|
598
|
+
|
|
599
|
+
const result: GenerateDocumentPropertiesResult = await testEnv.run(
|
|
600
|
+
generateDocumentProperties,
|
|
601
|
+
payload({ skip_if_fresh: true }),
|
|
602
|
+
);
|
|
603
|
+
|
|
604
|
+
expect(result).toEqual({ status: 'completed' });
|
|
605
|
+
expect(executeInteractionFromActivity).toHaveBeenCalled();
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
it('re-extracts when payload vars carry forceGeneration', async () => {
|
|
609
|
+
await mockSetup(freshSetupOptions());
|
|
610
|
+
mockExtractionResult({ title: 'Statement' });
|
|
611
|
+
|
|
612
|
+
const result: GenerateDocumentPropertiesResult = await testEnv.run(
|
|
613
|
+
generateDocumentProperties,
|
|
614
|
+
payload({ skip_if_fresh: true }, { forceGeneration: true }),
|
|
615
|
+
);
|
|
616
|
+
|
|
617
|
+
expect(result).toEqual({ status: 'completed' });
|
|
618
|
+
expect(executeInteractionFromActivity).toHaveBeenCalled();
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
it('persists the extraction fingerprint in the generation run info after a successful extraction', async () => {
|
|
622
|
+
const { update } = await mockSetup(freshSetupOptions({ generationRuns: undefined }));
|
|
623
|
+
mockExtractionResult({ title: 'Statement' });
|
|
624
|
+
|
|
625
|
+
const result: GenerateDocumentPropertiesResult = await testEnv.run(
|
|
626
|
+
generateDocumentProperties,
|
|
627
|
+
payload({ skip_if_fresh: true }),
|
|
628
|
+
);
|
|
629
|
+
|
|
630
|
+
expect(result).toEqual({ status: 'completed' });
|
|
631
|
+
expect(update).toHaveBeenCalledWith(
|
|
632
|
+
'object-1',
|
|
633
|
+
expect.objectContaining({
|
|
634
|
+
generation_run_info: expect.objectContaining({
|
|
635
|
+
extraction_fingerprint: freshFingerprint,
|
|
636
|
+
}),
|
|
637
|
+
}),
|
|
638
|
+
{ suppressWorkflows: true },
|
|
639
|
+
);
|
|
640
|
+
});
|
|
641
|
+
});
|
|
642
|
+
});
|