Mirror of https://github.com/roostorg/coop github.com/roostorg/coop
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 339 lines 11 kB view raw
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}