Mirror of https://github.com/roostorg/coop
github.com/roostorg/coop
1import {
2 ContainerTypes,
3 ScalarTypes,
4 type ContainerType,
5 type Field,
6 type TaggedScalar,
7} from '@roostorg/types';
8import fc from 'fast-check';
9
10import { DerivedFieldSpecArbitrary } from '../../test/arbitraries/ContentType.js';
11import { makeTestWithFixture } from '../../test/utils.js';
12import { instantiateOpaqueType } from '../../utils/typescript-types.js';
13import {
14 toNormalizedItemDataOrErrors,
15 type ItemSubmission,
16 type RawItemData,
17 type SubmissionId,
18} from '../itemProcessingService/index.js';
19import { type ItemSchema } from '../moderationConfigService/index.js';
20import { type TransientRunSignalWithCache } from '../orgAwareSignalExecutionService/signalExecutionService.js';
21import { SignalType } from '../signalsService/index.js';
22import {
23 getDerivedFieldValue,
24 parseDerivedFieldSpec,
25 serializeDerivedFieldSpec,
26} from './helpers.js';
27
28describe('Item type schemas', () => {
29 describe('Derived Field handling', () => {
30 describe('getDerivedContentFieldValue', () => {
31 const testWithMockRunSignal = makeTestWithFixture(() => ({
32 mockRunSignal: jest.fn<TransientRunSignalWithCache>(
33 async ({ signal, value }) => {
34 if (signal.type !== SignalType.OPEN_AI_WHISPER_TRANSCRIPTION) {
35 throw new Error('expected type to match our derivation recipe.');
36 }
37
38 return {
39 outputType: { scalarType: ScalarTypes.STRING },
40 score:
41 'Transcription of url ' +
42 (value as TaggedScalar<ScalarTypes['VIDEO']>).value.url,
43 };
44 },
45 ),
46 }));
47
48 const sclarVideoField = {
49 name: 'hello' as const,
50 type: ScalarTypes.VIDEO,
51 required: false,
52 container: null,
53 };
54
55 const objectVideoField: Field<ContainerType> = {
56 name: 'hello' as const,
57 type: ContainerTypes.MAP,
58 required: false,
59 container: {
60 containerType: ContainerTypes.MAP,
61 valueScalarType: ScalarTypes.VIDEO,
62 keyScalarType: ScalarTypes.STRING,
63 },
64 };
65
66 const derivedFieldSpec = {
67 derivationType: 'VIDEO_TRANSCRIPTION',
68 source: {
69 type: 'CONTENT_FIELD',
70 name: 'hello',
71 contentTypeId: 'some-content-type',
72 },
73 } as const;
74
75 testWithMockRunSignal(
76 'should return the value according to the spec + content submission',
77 async ({ mockRunSignal }) => {
78 const schema = [sclarVideoField] as const;
79 const res = await getDerivedFieldValue(
80 mockRunSignal,
81 'org-123',
82 instantiateOpaqueType<ItemSubmission>({
83 submissionId: instantiateOpaqueType<SubmissionId>(
84 'content-submission-123',
85 ),
86 submissionTime: new Date(),
87 itemId: 'content-123',
88 creator: { id: 'user-456', typeId: 'type-123' },
89 data: toNormalizedContent(schema, {
90 hello: 'https://my-dummy-video.com/',
91 }),
92 itemType: {
93 id: 'some-content-type',
94 name: 'Some Content Type',
95 schema,
96 kind: 'CONTENT',
97 description: null,
98 version: 'some version',
99 schemaVariant: 'original',
100 orgId: 'org-123',
101 schemaFieldRoles: {},
102 },
103 }),
104 derivedFieldSpec,
105 );
106
107 expect(mockRunSignal.mock.calls.length).toBe(1);
108 expect(mockRunSignal.mock.calls[0]).toMatchInlineSnapshot(`
109 [
110 {
111 "orgId": "org-123",
112 "signal": {
113 "type": "OPEN_AI_WHISPER_TRANSCRIPTION",
114 },
115 "subcategory": undefined,
116 "userId": "user-456",
117 "value": {
118 "type": "VIDEO",
119 "value": {
120 "url": "https://my-dummy-video.com/",
121 },
122 },
123 },
124 ]
125 `);
126 expect(res).toEqual({
127 type: ScalarTypes.STRING,
128 value: 'Transcription of url https://my-dummy-video.com/',
129 });
130 },
131 );
132
133 testWithMockRunSignal(
134 'should properly handle non-scalar inputs',
135 async ({ mockRunSignal }) => {
136 const schema = [objectVideoField] as const;
137 const res = await getDerivedFieldValue(
138 mockRunSignal,
139 'org-123',
140 instantiateOpaqueType<ItemSubmission>({
141 submissionId: instantiateOpaqueType<SubmissionId>(
142 'content-submission-123',
143 ),
144 itemId: 'content-123',
145 creator: { id: 'user-456', typeId: 'type-123' },
146 data: toNormalizedContent(schema, {
147 hello: {
148 first_video: 'https://my-dummy-video.com/',
149 second_video: 'https://my-second-video.com/',
150 },
151 }),
152 itemType: {
153 id: 'some-content-type',
154 name: 'Some Content Type',
155 schema,
156 kind: 'CONTENT',
157 description: null,
158 version: 'some version',
159 schemaVariant: 'original',
160 orgId: 'org-123',
161 schemaFieldRoles: {},
162 },
163 }),
164 derivedFieldSpec,
165 );
166
167 expect(mockRunSignal.mock.calls.length).toBe(2);
168 expect(mockRunSignal.mock.calls).toMatchInlineSnapshot(`
169 [
170 [
171 {
172 "orgId": "org-123",
173 "signal": {
174 "type": "OPEN_AI_WHISPER_TRANSCRIPTION",
175 },
176 "subcategory": undefined,
177 "userId": "user-456",
178 "value": {
179 "type": "VIDEO",
180 "value": {
181 "url": "https://my-dummy-video.com/",
182 },
183 },
184 },
185 ],
186 [
187 {
188 "orgId": "org-123",
189 "signal": {
190 "type": "OPEN_AI_WHISPER_TRANSCRIPTION",
191 },
192 "subcategory": undefined,
193 "userId": "user-456",
194 "value": {
195 "type": "VIDEO",
196 "value": {
197 "url": "https://my-second-video.com/",
198 },
199 },
200 },
201 ],
202 ]
203 `);
204 expect(res).toEqual([
205 {
206 type: ScalarTypes.STRING,
207 value: 'Transcription of url https://my-dummy-video.com/',
208 },
209 {
210 type: ScalarTypes.STRING,
211 value: 'Transcription of url https://my-second-video.com/',
212 },
213 ]);
214 },
215 );
216
217 testWithMockRunSignal(
218 "should return undefined if there's no proper field to source from",
219 async ({ mockRunSignal }) => {
220 const schema = [sclarVideoField] as const;
221 const fieldVal = await getDerivedFieldValue(
222 mockRunSignal,
223 'org-123',
224 instantiateOpaqueType<ItemSubmission>({
225 submissionId: instantiateOpaqueType<SubmissionId>(
226 'content-submission-123',
227 ),
228 itemId: 'content-123',
229 creator: { id: 'user-456', typeId: 'type-123' },
230 data: toNormalizedContent(schema, {}), // hello field missing
231 itemType: {
232 id: 'some-content-type',
233 name: 'Some Content Type',
234 schema,
235 kind: 'CONTENT',
236 description: null,
237 version: 'some version',
238 schemaVariant: 'original',
239 orgId: 'org-123',
240 schemaFieldRoles: {},
241 },
242 }),
243 derivedFieldSpec,
244 );
245
246 expect(mockRunSignal.mock.calls.length).toBe(0);
247 expect(fieldVal).toBe(undefined);
248 },
249 );
250 });
251
252 describe('parseDerivedFieldSpec/serializeDerivedFieldSpec', () => {
253 test('should losslessly round-trip derived field specs', () => {
254 fc.assert(
255 fc.property(DerivedFieldSpecArbitrary, (spec) => {
256 expect(
257 parseDerivedFieldSpec(serializeDerivedFieldSpec(spec)),
258 ).toEqual(spec);
259 }),
260 );
261 });
262
263 test('should reject invalid specs', () => {
264 expect(() => {
265 parseDerivedFieldSpec(
266 serializeDerivedFieldSpec({
267 source: { type: 'CONTENT_FIELD', name: 'hi', contentTypeId: '1' },
268 derivationType: 'hasOwnProperty' as any, // invalid, hacking attempt.
269 }),
270 );
271 }).toThrowErrorMatchingInlineSnapshot(`"Invalid derived field spec"`);
272
273 expect(() => {
274 parseDerivedFieldSpec(
275 serializeDerivedFieldSpec({
276 // extra `name` prop should make parsing fail.
277 // we use the cast so ts doesn't complain about that prop.
278 // prettier-ignore
279 // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
280 source: { type: 'FULL_ITEM', name: 'hi' } as { type: 'FULL_ITEM'; },
281 derivationType: 'VIDEO_TRANSCRIPTION',
282 }),
283 );
284 }).toThrowErrorMatchingInlineSnapshot(`"Invalid derived field spec"`);
285
286 expect(() => {
287 parseDerivedFieldSpec(
288 serializeDerivedFieldSpec({
289 source: { type: 'CONTENT_COOP_INPUT', name: 'hi' as any }, // unknown input name.
290 derivationType: 'VIDEO_TRANSCRIPTION',
291 }),
292 );
293 }).toThrowErrorMatchingInlineSnapshot(`"Invalid derived field spec"`);
294
295 expect(() => {
296 parseDerivedFieldSpec(
297 serializeDerivedFieldSpec({
298 source: { type: 'CONTENT_COOP_INPUT' } as any, // missing name.
299 derivationType: 'VIDEO_TRANSCRIPTION',
300 }),
301 );
302 }).toThrowErrorMatchingInlineSnapshot(`"Invalid derived field spec"`);
303
304 expect(() => {
305 parseDerivedFieldSpec(
306 serializeDerivedFieldSpec({
307 source: { type: '__proto__' } as any, // invalid type.
308 derivationType: 'VIDEO_TRANSCRIPTION',
309 }),
310 );
311 }).toThrowErrorMatchingInlineSnapshot(`"Invalid derived field spec"`);
312 });
313 });
314 });
315});
316
317function toNormalizedContent(schema: ItemSchema, it: RawItemData) {
318 const dataOrErrors = toNormalizedItemDataOrErrors(
319 [],
320 {
321 id: 'test',
322 kind: 'CONTENT',
323 name: 'test',
324 description: 'test',
325 version: 'test',
326 schemaVariant: 'original',
327 orgId: 'test orgId',
328 schemaFieldRoles: {},
329 schema,
330 },
331 it,
332 );
333
334 if (Array.isArray(dataOrErrors)) {
335 throw new Error('Unexpected errors');
336 }
337
338 return dataOrErrors;
339}