@tellescope/sdk 1.245.1 → 1.246.2

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 (67) hide show
  1. package/lib/cjs/tests/api_tests/account_switcher.test.d.ts +6 -0
  2. package/lib/cjs/tests/api_tests/account_switcher.test.d.ts.map +1 -0
  3. package/lib/cjs/tests/api_tests/account_switcher.test.js +445 -0
  4. package/lib/cjs/tests/api_tests/account_switcher.test.js.map +1 -0
  5. package/lib/cjs/tests/api_tests/beluga_pharmacy_mappings.test.d.ts +6 -0
  6. package/lib/cjs/tests/api_tests/beluga_pharmacy_mappings.test.d.ts.map +1 -0
  7. package/lib/cjs/tests/api_tests/beluga_pharmacy_mappings.test.js +357 -0
  8. package/lib/cjs/tests/api_tests/beluga_pharmacy_mappings.test.js.map +1 -0
  9. package/lib/cjs/tests/api_tests/calendar_event_limits.test.d.ts.map +1 -1
  10. package/lib/cjs/tests/api_tests/calendar_event_limits.test.js +163 -0
  11. package/lib/cjs/tests/api_tests/calendar_event_limits.test.js.map +1 -1
  12. package/lib/cjs/tests/api_tests/calendar_events_bulk_update.test.d.ts +5 -0
  13. package/lib/cjs/tests/api_tests/calendar_events_bulk_update.test.d.ts.map +1 -0
  14. package/lib/cjs/tests/api_tests/calendar_events_bulk_update.test.js +483 -0
  15. package/lib/cjs/tests/api_tests/calendar_events_bulk_update.test.js.map +1 -0
  16. package/lib/cjs/tests/api_tests/email_utils.test.d.ts +2 -0
  17. package/lib/cjs/tests/api_tests/email_utils.test.d.ts.map +1 -0
  18. package/lib/cjs/tests/api_tests/email_utils.test.js +141 -0
  19. package/lib/cjs/tests/api_tests/email_utils.test.js.map +1 -0
  20. package/lib/cjs/tests/api_tests/organization_settings_duplicates.test.d.ts +6 -0
  21. package/lib/cjs/tests/api_tests/organization_settings_duplicates.test.d.ts.map +1 -0
  22. package/lib/cjs/tests/api_tests/organization_settings_duplicates.test.js +268 -0
  23. package/lib/cjs/tests/api_tests/organization_settings_duplicates.test.js.map +1 -0
  24. package/lib/cjs/tests/api_tests/time_tracks.test.d.ts +20 -0
  25. package/lib/cjs/tests/api_tests/time_tracks.test.d.ts.map +1 -1
  26. package/lib/cjs/tests/api_tests/time_tracks.test.js +692 -20
  27. package/lib/cjs/tests/api_tests/time_tracks.test.js.map +1 -1
  28. package/lib/cjs/tests/tests.d.ts.map +1 -1
  29. package/lib/cjs/tests/tests.js +157 -122
  30. package/lib/cjs/tests/tests.js.map +1 -1
  31. package/lib/esm/tests/api_tests/account_switcher.test.d.ts +6 -0
  32. package/lib/esm/tests/api_tests/account_switcher.test.d.ts.map +1 -0
  33. package/lib/esm/tests/api_tests/account_switcher.test.js +438 -0
  34. package/lib/esm/tests/api_tests/account_switcher.test.js.map +1 -0
  35. package/lib/esm/tests/api_tests/beluga_pharmacy_mappings.test.d.ts +6 -0
  36. package/lib/esm/tests/api_tests/beluga_pharmacy_mappings.test.d.ts.map +1 -0
  37. package/lib/esm/tests/api_tests/beluga_pharmacy_mappings.test.js +353 -0
  38. package/lib/esm/tests/api_tests/beluga_pharmacy_mappings.test.js.map +1 -0
  39. package/lib/esm/tests/api_tests/calendar_event_limits.test.d.ts.map +1 -1
  40. package/lib/esm/tests/api_tests/calendar_event_limits.test.js +163 -0
  41. package/lib/esm/tests/api_tests/calendar_event_limits.test.js.map +1 -1
  42. package/lib/esm/tests/api_tests/calendar_events_bulk_update.test.d.ts +5 -0
  43. package/lib/esm/tests/api_tests/calendar_events_bulk_update.test.d.ts.map +1 -0
  44. package/lib/esm/tests/api_tests/calendar_events_bulk_update.test.js +479 -0
  45. package/lib/esm/tests/api_tests/calendar_events_bulk_update.test.js.map +1 -0
  46. package/lib/esm/tests/api_tests/email_utils.test.d.ts +2 -0
  47. package/lib/esm/tests/api_tests/email_utils.test.d.ts.map +1 -0
  48. package/lib/esm/tests/api_tests/email_utils.test.js +137 -0
  49. package/lib/esm/tests/api_tests/email_utils.test.js.map +1 -0
  50. package/lib/esm/tests/api_tests/organization_settings_duplicates.test.d.ts +6 -0
  51. package/lib/esm/tests/api_tests/organization_settings_duplicates.test.d.ts.map +1 -0
  52. package/lib/esm/tests/api_tests/organization_settings_duplicates.test.js +264 -0
  53. package/lib/esm/tests/api_tests/organization_settings_duplicates.test.js.map +1 -0
  54. package/lib/esm/tests/api_tests/time_tracks.test.d.ts +20 -0
  55. package/lib/esm/tests/api_tests/time_tracks.test.d.ts.map +1 -1
  56. package/lib/esm/tests/api_tests/time_tracks.test.js +687 -20
  57. package/lib/esm/tests/api_tests/time_tracks.test.js.map +1 -1
  58. package/lib/esm/tests/tests.d.ts.map +1 -1
  59. package/lib/esm/tests/tests.js +158 -123
  60. package/lib/esm/tests/tests.js.map +1 -1
  61. package/lib/tsconfig.tsbuildinfo +1 -1
  62. package/package.json +10 -10
  63. package/src/tests/api_tests/beluga_pharmacy_mappings.test.ts +351 -0
  64. package/src/tests/api_tests/calendar_event_limits.test.ts +195 -0
  65. package/src/tests/api_tests/time_tracks.test.ts +542 -16
  66. package/src/tests/tests.ts +34 -5
  67. package/test_generated.pdf +0 -0
@@ -8,6 +8,18 @@ const log = console.log
8
8
 
9
9
  const host = process.env.REACT_APP_TELLESCOPE_API_URL || 'http://localhost:8080'
10
10
 
11
+ // Helper to assert that an async function throws an error
12
+ const assert_throws = async (fn: () => Promise<any>, description: string) => {
13
+ try {
14
+ await fn()
15
+ assert(false, `${description} - expected error but succeeded`)
16
+ } catch (e: any) {
17
+ // SDK parseError returns the response body { message, info } for 4xx errors
18
+ assert(e?.code === 400 || e?.statusCode === 400 || typeof e?.message === 'string',
19
+ `${description} - expected error, got: ${JSON.stringify(e)}`)
20
+ }
21
+ }
22
+
11
23
  // Unit tests for calculateTimeTrackDuration
12
24
  const test_calculateTimeTrackDuration = () => {
13
25
  log_header("calculateTimeTrackDuration Unit Tests")
@@ -66,7 +78,7 @@ const test_calculateTimeTrackDuration = () => {
66
78
  ])
67
79
  assert(test7 === 2700000, `Three intervals (10+30+5 min) should be 2700000 ms, got ${test7}`)
68
80
 
69
- log("All calculateTimeTrackDuration unit tests passed")
81
+ log("All calculateTimeTrackDuration unit tests passed")
70
82
  }
71
83
 
72
84
  // API tests for time_tracks CRUD operations
@@ -92,21 +104,21 @@ export const time_tracks_tests = async ({ sdk, sdkNonAdmin }: { sdk: Session, sd
92
104
  assert(timeTrack.timestamps?.[0].type === 'start', `First timestamp should be 'start', got ${timeTrack.timestamps?.[0].type}`)
93
105
  assert(!timeTrack.closedAt, "closedAt should not be set initially")
94
106
  assert(!timeTrack.totalDurationInMS, "totalDurationInMS should not be set initially")
95
- log("Time track created with auto-set userId and initial timestamp")
107
+ log("Time track created with auto-set userId and initial timestamp")
96
108
 
97
109
  // Test 2: Read the time track
98
110
  log("Reading time track...")
99
111
  const fetchedTimeTrack = await sdk.api.time_tracks.getOne(timeTrack.id)
100
112
  assert(fetchedTimeTrack.id === timeTrack.id, "Fetched time track should have same id")
101
113
  assert(fetchedTimeTrack.title === "Test Time Track", "Fetched title should match")
102
- log("Time track retrieved successfully")
114
+ log("Time track retrieved successfully")
103
115
 
104
116
  // Test 3: Get all time tracks for current user
105
117
  log("Getting all time tracks for current user...")
106
118
  const allTimeTracks = await sdk.api.time_tracks.getSome({ filter: { userId } })
107
119
  assert(allTimeTracks.length >= 1, `Should have at least 1 time track, got ${allTimeTracks.length}`)
108
120
  assert(!!allTimeTracks.find(t => t.id === timeTrack.id), "Should find our created time track")
109
- log("Retrieved all time tracks for user")
121
+ log("Retrieved all time tracks for user")
110
122
 
111
123
  // Test 4: Create enduser and link time track
112
124
  log("Creating enduser for time track linkage...")
@@ -116,7 +128,7 @@ export const time_tracks_tests = async ({ sdk, sdkNonAdmin }: { sdk: Session, sd
116
128
  lname: "Track",
117
129
  })
118
130
  enduserId = enduser.id
119
- log("Enduser created")
131
+ log("Enduser created")
120
132
 
121
133
  // Test 5: Create time track with enduserId
122
134
  log("Creating time track linked to enduser...")
@@ -125,7 +137,7 @@ export const time_tracks_tests = async ({ sdk, sdkNonAdmin }: { sdk: Session, sd
125
137
  enduserId: enduser.id,
126
138
  } as any)
127
139
  assert(linkedTimeTrack.enduserId === enduser.id, `enduserId should be set to ${enduser.id}, got ${linkedTimeTrack.enduserId}`)
128
- log("Time track linked to enduser")
140
+ log("Time track linked to enduser")
129
141
 
130
142
  // Test 6: Update time track - add pause timestamp
131
143
  log("Adding pause timestamp...")
@@ -138,7 +150,7 @@ export const time_tracks_tests = async ({ sdk, sdkNonAdmin }: { sdk: Session, sd
138
150
  }, { replaceObjectFields: true })
139
151
  assert(pausedTimeTrack.timestamps?.length === 2, `Should have 2 timestamps after pause, got ${pausedTimeTrack.timestamps?.length}`)
140
152
  assert(pausedTimeTrack.timestamps?.[1].type === 'pause', `Second timestamp should be 'pause', got ${pausedTimeTrack.timestamps?.[1].type}`)
141
- log("Pause timestamp added")
153
+ log("Pause timestamp added")
142
154
 
143
155
  // Test 7: Update time track - add resume timestamp
144
156
  log("Adding resume timestamp...")
@@ -151,7 +163,7 @@ export const time_tracks_tests = async ({ sdk, sdkNonAdmin }: { sdk: Session, sd
151
163
  }, { replaceObjectFields: true })
152
164
  assert(resumedTimeTrack.timestamps?.length === 3, `Should have 3 timestamps after resume, got ${resumedTimeTrack.timestamps?.length}`)
153
165
  assert(resumedTimeTrack.timestamps?.[2].type === 'resume', `Third timestamp should be 'resume', got ${resumedTimeTrack.timestamps?.[2].type}`)
154
- log("Resume timestamp added")
166
+ log("Resume timestamp added")
155
167
 
156
168
  // Test 8: Close time track and verify auto-calculation
157
169
  log("Closing time track with closedAt...")
@@ -168,7 +180,7 @@ export const time_tracks_tests = async ({ sdk, sdkNonAdmin }: { sdk: Session, sd
168
180
  assert(!!finalTimeTrack.closedAt, "closedAt should be set")
169
181
  assert(typeof finalTimeTrack.totalDurationInMS === 'number', `totalDurationInMS should be a number, got ${typeof finalTimeTrack.totalDurationInMS}`)
170
182
  assert(finalTimeTrack.totalDurationInMS! > 0, `totalDurationInMS should be > 0, got ${finalTimeTrack.totalDurationInMS}`)
171
- log(`✅ Time track closed with auto-calculated duration: ${finalTimeTrack.totalDurationInMS} ms`)
183
+ log(`Time track closed with auto-calculated duration: ${finalTimeTrack.totalDurationInMS} ms`)
172
184
 
173
185
  // Test 9: Update title
174
186
  log("Updating time track title...")
@@ -176,7 +188,7 @@ export const time_tracks_tests = async ({ sdk, sdkNonAdmin }: { sdk: Session, sd
176
188
  title: "Updated Time Track Title",
177
189
  })
178
190
  assert(updatedTimeTrack.title === "Updated Time Track Title", "Title should be updated")
179
- log("Time track title updated")
191
+ log("Time track title updated")
180
192
 
181
193
  // Test 10: Filter by closedAt (get active time tracks)
182
194
  log("Filtering for active time tracks (no closedAt)...")
@@ -186,14 +198,14 @@ export const time_tracks_tests = async ({ sdk, sdkNonAdmin }: { sdk: Session, sd
186
198
  const activeTimeTracks = allUserTimeTracks.filter(t => !t.closedAt)
187
199
  assert(!activeTimeTracks.find(t => t.id === timeTrack.id), "Closed time track should not appear in active filter")
188
200
  assert(!!activeTimeTracks.find(t => t.id === linkedTimeTrack.id), "Unclosed time track should appear in active filter")
189
- log("Active time tracks filtered correctly")
201
+ log("Active time tracks filtered correctly")
190
202
 
191
203
  // Test 11: Access control - non-admin user should only see their own time tracks
192
204
  log("Testing access control with non-admin user...")
193
205
  const nonAdminTimeTracks = await sdkNonAdmin.api.time_tracks.getSome({})
194
206
  const hasOtherUserTimeTrack = nonAdminTimeTracks.find(t => t.userId !== sdkNonAdmin.userInfo.id)
195
207
  assert(!hasOtherUserTimeTrack, "Non-admin user should not see other users' time tracks")
196
- log("Access control working correctly")
208
+ log("Access control working correctly")
197
209
 
198
210
  // Test 12: Delete time tracks
199
211
  log("Deleting time tracks...")
@@ -203,9 +215,9 @@ export const time_tracks_tests = async ({ sdk, sdkNonAdmin }: { sdk: Session, sd
203
215
  const deletedCheck = await sdk.api.time_tracks.getSome({})
204
216
  const stillExists = deletedCheck.filter(t => t.id === timeTrack.id || t.id === linkedTimeTrack.id)
205
217
  assert(stillExists.length === 0, "Time tracks should be deleted")
206
- log("Time tracks deleted successfully")
218
+ log("Time tracks deleted successfully")
207
219
 
208
- log("All time tracks API tests passed!")
220
+ log("All time tracks API tests passed!")
209
221
 
210
222
  } finally {
211
223
  // Cleanup
@@ -226,6 +238,515 @@ export const time_tracks_tests = async ({ sdk, sdkNonAdmin }: { sdk: Session, sd
226
238
  }
227
239
  }
228
240
 
241
+ // ============================================================
242
+ // Group A: Historical Time Track Creation
243
+ // ============================================================
244
+ export const time_tracks_historical_tests = async ({ sdk, sdkNonAdmin }: { sdk: Session, sdkNonAdmin: Session }) => {
245
+ log_header("Time Tracks - Historical Creation Tests")
246
+
247
+ const trackIds: string[] = []
248
+
249
+ try {
250
+ const now = new Date()
251
+ const oneHourAgo = new Date(now.getTime() - 3600000)
252
+ const twoHoursAgo = new Date(now.getTime() - 7200000)
253
+
254
+ // Test A1: Create historical time track with all required fields
255
+ log("A1: Creating historical time track with all required fields...")
256
+ const historical = await sdk.api.time_tracks.createOne({
257
+ title: "Historical Entry",
258
+ isHistorical: true,
259
+ closedAt: now,
260
+ lockedAt: now,
261
+ lockedByUserId: sdk.userInfo.id,
262
+ totalDurationInMS: 3600000,
263
+ timestamps: [
264
+ { type: 'start', timestamp: twoHoursAgo },
265
+ { type: 'pause', timestamp: oneHourAgo },
266
+ ],
267
+ } as any)
268
+ trackIds.push(historical.id)
269
+
270
+ assert(historical.isHistorical === true, "isHistorical should be true")
271
+ assert(!!historical.closedAt, "closedAt should be set")
272
+ assert(!!historical.lockedAt, "lockedAt should be set")
273
+ assert(historical.lockedByUserId === sdk.userInfo.id, "lockedByUserId should match")
274
+ assert(historical.totalDurationInMS === 3600000, `totalDurationInMS should be 3600000, got ${historical.totalDurationInMS}`)
275
+ assert(historical.timestamps?.length === 2, "timestamps should have 2 entries")
276
+ log("A1: Historical time track created with all fields persisted")
277
+
278
+ // Test A2: Create historical without closedAt - expect 400
279
+ log("A2: Creating historical without closedAt (should fail)...")
280
+ await assert_throws(
281
+ () => sdk.api.time_tracks.createOne({
282
+ title: "Missing ClosedAt",
283
+ isHistorical: true,
284
+ lockedAt: now,
285
+ lockedByUserId: sdk.userInfo.id,
286
+ totalDurationInMS: 3600000,
287
+ timestamps: [{ type: 'start', timestamp: twoHoursAgo }],
288
+ } as any),
289
+ "A2: Historical without closedAt"
290
+ )
291
+ log("A2: Correctly rejected historical without closedAt")
292
+
293
+ // Test A3: Create historical without lockedAt - expect 400
294
+ log("A3: Creating historical without lockedAt (should fail)...")
295
+ await assert_throws(
296
+ () => sdk.api.time_tracks.createOne({
297
+ title: "Missing LockedAt",
298
+ isHistorical: true,
299
+ closedAt: now,
300
+ lockedByUserId: sdk.userInfo.id,
301
+ totalDurationInMS: 3600000,
302
+ timestamps: [{ type: 'start', timestamp: twoHoursAgo }],
303
+ } as any),
304
+ "A3: Historical without lockedAt"
305
+ )
306
+ log("A3: Correctly rejected historical without lockedAt")
307
+
308
+ // Test A4: Verify isHistorical cannot be updated (updatesDisabled)
309
+ log("A4: Attempting to update isHistorical (should be rejected)...")
310
+ await assert_throws(
311
+ () => sdk.api.time_tracks.updateOne(historical.id, {
312
+ isHistorical: false,
313
+ } as any),
314
+ "A4: isHistorical update should be rejected"
315
+ )
316
+ // Confirm it's still true
317
+ const fetched = await sdk.api.time_tracks.getOne(historical.id)
318
+ assert(fetched.isHistorical === true, "isHistorical should remain true after rejected update")
319
+ log("A4: isHistorical correctly rejected on update")
320
+
321
+ log("All historical creation tests passed!")
322
+
323
+ } finally {
324
+ for (const id of trackIds) {
325
+ try { await sdk.api.time_tracks.deleteOne(id) } catch (e) {}
326
+ }
327
+ }
328
+ }
329
+
330
+ // ============================================================
331
+ // Group B: Correction Flow
332
+ // ============================================================
333
+ export const time_tracks_correction_tests = async ({ sdk, sdkNonAdmin }: { sdk: Session, sdkNonAdmin: Session }) => {
334
+ log_header("Time Tracks - Correction Flow Tests")
335
+
336
+ const trackIds: string[] = []
337
+
338
+ try {
339
+ const now = new Date()
340
+
341
+ // Create a real-time track and close it
342
+ log("B0: Creating and closing real-time track...")
343
+ const track = await sdk.api.time_tracks.createOne({
344
+ title: "Track for Correction",
345
+ } as any)
346
+ trackIds.push(track.id)
347
+
348
+ // Add pause
349
+ await sdk.api.time_tracks.updateOne(track.id, {
350
+ timestamps: [
351
+ ...(track.timestamps || []),
352
+ { type: 'pause', timestamp: new Date() },
353
+ ]
354
+ }, { replaceObjectFields: true })
355
+
356
+ // Close it
357
+ const closedAt = new Date()
358
+ await sdk.api.time_tracks.updateOne(track.id, { closedAt })
359
+
360
+ // Wait for auto-calculated duration
361
+ await wait(undefined, 1500)
362
+
363
+ const closed = await sdk.api.time_tracks.getOne(track.id)
364
+ assert(!!closed.closedAt, "Track should be closed")
365
+ assert(typeof closed.totalDurationInMS === 'number', "Should have auto-calculated totalDurationInMS")
366
+ const originalDuration = closed.totalDurationInMS!
367
+ log(`B0: Track closed with auto-calculated duration: ${originalDuration} ms`)
368
+
369
+ // Test B1: Apply correction with all required fields
370
+ log("B1: Applying correction with all required fields...")
371
+ const correctionTime = new Date()
372
+ const correctedTrack = await sdk.api.time_tracks.updateOne(track.id, {
373
+ correctedAt: correctionTime,
374
+ correctedByUserId: sdk.userInfo.id,
375
+ correctionNote: "Forgot to pause during lunch",
376
+ originalTotalDurationInMS: originalDuration,
377
+ totalDurationInMS: originalDuration - 1800000, // Subtract 30 min
378
+ lockedAt: correctionTime,
379
+ lockedByUserId: sdk.userInfo.id,
380
+ } as any)
381
+ assert(!!correctedTrack.correctedAt, "correctedAt should be set")
382
+ assert(correctedTrack.correctedByUserId === sdk.userInfo.id, "correctedByUserId should match")
383
+ assert(correctedTrack.correctionNote === "Forgot to pause during lunch", "correctionNote should match")
384
+ assert(correctedTrack.originalTotalDurationInMS === originalDuration, "originalTotalDurationInMS should preserve old value")
385
+ assert(correctedTrack.totalDurationInMS === originalDuration - 1800000, "totalDurationInMS should be corrected value")
386
+ assert(!!correctedTrack.lockedAt, "lockedAt should be set")
387
+ log("B1: Correction applied successfully with all fields persisted")
388
+
389
+ // Test B2: Correction without originalTotalDurationInMS - expect 400
390
+ log("B2: Correction without originalTotalDurationInMS (should fail)...")
391
+ const track2 = await sdk.api.time_tracks.createOne({ title: "Track for B2" } as any)
392
+ trackIds.push(track2.id)
393
+ await sdk.api.time_tracks.updateOne(track2.id, { closedAt: new Date() })
394
+ await wait(undefined, 1000)
395
+ const closed2 = await sdk.api.time_tracks.getOne(track2.id)
396
+ await assert_throws(
397
+ () => sdk.api.time_tracks.updateOne(track2.id, {
398
+ correctedAt: new Date(),
399
+ totalDurationInMS: 1000,
400
+ lockedAt: new Date(),
401
+ lockedByUserId: sdk.userInfo.id,
402
+ } as any),
403
+ "B2: Correction without originalTotalDurationInMS"
404
+ )
405
+ log("B2: Correctly rejected correction without originalTotalDurationInMS")
406
+
407
+ // Test B3: Correction without lockedAt - expect 400
408
+ log("B3: Correction without lockedAt (should fail)...")
409
+ await assert_throws(
410
+ () => sdk.api.time_tracks.updateOne(track2.id, {
411
+ correctedAt: new Date(),
412
+ originalTotalDurationInMS: closed2.totalDurationInMS || 0,
413
+ totalDurationInMS: 1000,
414
+ lockedByUserId: sdk.userInfo.id,
415
+ } as any),
416
+ "B3: Correction without lockedAt"
417
+ )
418
+ log("B3: Correctly rejected correction without lockedAt")
419
+
420
+ // Test B4: After lock, attempt to update title - expect 400
421
+ log("B4: Updating title on locked track (should fail)...")
422
+ await assert_throws(
423
+ () => sdk.api.time_tracks.updateOne(track.id, {
424
+ title: "Should Not Work",
425
+ } as any),
426
+ "B4: Title update on locked track"
427
+ )
428
+ log("B4: Correctly rejected title update on locked track")
429
+
430
+ // Test B5: After lock, attempt second correction - expect 400
431
+ log("B5: Second correction on locked track (should fail)...")
432
+ await assert_throws(
433
+ () => sdk.api.time_tracks.updateOne(track.id, {
434
+ correctedAt: new Date(),
435
+ originalTotalDurationInMS: correctedTrack.totalDurationInMS,
436
+ totalDurationInMS: 500,
437
+ lockedAt: new Date(),
438
+ lockedByUserId: sdk.userInfo.id,
439
+ } as any),
440
+ "B5: Second correction on locked track"
441
+ )
442
+ log("B5: Correctly rejected second correction on locked track")
443
+
444
+ log("All correction flow tests passed!")
445
+
446
+ } finally {
447
+ for (const id of trackIds) {
448
+ try { await sdk.api.time_tracks.deleteOne(id) } catch (e) {}
449
+ }
450
+ }
451
+ }
452
+
453
+ // ============================================================
454
+ // Group C: Review Flow
455
+ // ============================================================
456
+ export const time_tracks_review_tests = async ({ sdk, sdkNonAdmin }: { sdk: Session, sdkNonAdmin: Session }) => {
457
+ log_header("Time Tracks - Review Flow Tests")
458
+
459
+ const trackIds: string[] = []
460
+
461
+ try {
462
+ const now = new Date()
463
+ const oneHourAgo = new Date(now.getTime() - 3600000)
464
+ const twoHoursAgo = new Date(now.getTime() - 7200000)
465
+
466
+ // Create a historical track for review testing
467
+ log("C0: Creating historical track for review tests...")
468
+ const historical = await sdk.api.time_tracks.createOne({
469
+ title: "Track for Review",
470
+ isHistorical: true,
471
+ closedAt: now,
472
+ lockedAt: now,
473
+ lockedByUserId: sdk.userInfo.id,
474
+ totalDurationInMS: 3600000,
475
+ timestamps: [
476
+ { type: 'start', timestamp: twoHoursAgo },
477
+ { type: 'pause', timestamp: oneHourAgo },
478
+ ],
479
+ } as any)
480
+ trackIds.push(historical.id)
481
+ log("C0: Historical track created")
482
+
483
+ // Test C1: Review by different user (approval)
484
+ log("C1: Review by different user (approval)...")
485
+ const reviewTime = new Date()
486
+ const reviewed = await sdk.api.time_tracks.updateOne(historical.id, {
487
+ reviewedAt: reviewTime,
488
+ reviewedByUserId: sdkNonAdmin.userInfo.id,
489
+ reviewApproved: true,
490
+ } as any)
491
+ assert(!!reviewed.reviewedAt, "reviewedAt should be set")
492
+ assert(reviewed.reviewedByUserId === sdkNonAdmin.userInfo.id, "reviewedByUserId should match non-admin")
493
+ assert(reviewed.reviewApproved === true, "reviewApproved should be true")
494
+ log("C1: Review approved successfully by different user")
495
+
496
+ // Test C2: Self-review (owner reviews own track) - expect 400
497
+ log("C2: Self-review (should fail)...")
498
+ const historical2 = await sdk.api.time_tracks.createOne({
499
+ title: "Track for Self-Review",
500
+ isHistorical: true,
501
+ closedAt: now,
502
+ lockedAt: now,
503
+ lockedByUserId: sdk.userInfo.id,
504
+ totalDurationInMS: 3600000,
505
+ timestamps: [
506
+ { type: 'start', timestamp: twoHoursAgo },
507
+ { type: 'pause', timestamp: oneHourAgo },
508
+ ],
509
+ } as any)
510
+ trackIds.push(historical2.id)
511
+
512
+ await assert_throws(
513
+ () => sdk.api.time_tracks.updateOne(historical2.id, {
514
+ reviewedAt: new Date(),
515
+ reviewedByUserId: sdk.userInfo.id, // same as track owner
516
+ reviewApproved: true,
517
+ } as any),
518
+ "C2: Self-review"
519
+ )
520
+ log("C2: Correctly rejected self-review")
521
+
522
+ // Test C3: Review fields updatable even after lock
523
+ log("C3: Review fields updatable after lock...")
524
+ const updatedReview = await sdk.api.time_tracks.updateOne(historical.id, {
525
+ reviewedAt: new Date(),
526
+ reviewedByUserId: sdkNonAdmin.userInfo.id,
527
+ reviewApproved: false,
528
+ reviewNote: "Hours seem too high, please double-check",
529
+ } as any)
530
+ assert(updatedReview.reviewApproved === false, "reviewApproved should be updated to false")
531
+ assert(updatedReview.reviewNote === "Hours seem too high, please double-check", "reviewNote should be set")
532
+ log("C3: Review fields correctly updatable on locked track")
533
+
534
+ // Test C4: Rejection flow with reviewNote
535
+ log("C4: Rejection flow with reviewNote...")
536
+ const historical3 = await sdk.api.time_tracks.createOne({
537
+ title: "Track for Rejection",
538
+ isHistorical: true,
539
+ closedAt: now,
540
+ lockedAt: now,
541
+ lockedByUserId: sdk.userInfo.id,
542
+ totalDurationInMS: 1800000,
543
+ timestamps: [
544
+ { type: 'start', timestamp: twoHoursAgo },
545
+ { type: 'pause', timestamp: oneHourAgo },
546
+ ],
547
+ } as any)
548
+ trackIds.push(historical3.id)
549
+
550
+ const rejected = await sdk.api.time_tracks.updateOne(historical3.id, {
551
+ reviewedAt: new Date(),
552
+ reviewedByUserId: sdkNonAdmin.userInfo.id,
553
+ reviewApproved: false,
554
+ reviewNote: "Rejected - timestamps don't match claimed duration",
555
+ } as any)
556
+ assert(rejected.reviewApproved === false, "reviewApproved should be false")
557
+ assert(rejected.reviewNote === "Rejected - timestamps don't match claimed duration", "reviewNote should match")
558
+ log("C4: Rejection flow completed successfully")
559
+
560
+ log("All review flow tests passed!")
561
+
562
+ } finally {
563
+ for (const id of trackIds) {
564
+ try { await sdk.api.time_tracks.deleteOne(id) } catch (e) {}
565
+ }
566
+ }
567
+ }
568
+
569
+ // ============================================================
570
+ // Group D: Lock Enforcement
571
+ // ============================================================
572
+ export const time_tracks_lock_tests = async ({ sdk, sdkNonAdmin }: { sdk: Session, sdkNonAdmin: Session }) => {
573
+ log_header("Time Tracks - Lock Enforcement Tests")
574
+
575
+ const trackIds: string[] = []
576
+
577
+ try {
578
+ const now = new Date()
579
+ const oneHourAgo = new Date(now.getTime() - 3600000)
580
+ const twoHoursAgo = new Date(now.getTime() - 7200000)
581
+
582
+ // Create a locked historical track
583
+ log("D0: Creating locked historical track...")
584
+ const locked = await sdk.api.time_tracks.createOne({
585
+ title: "Locked Track",
586
+ isHistorical: true,
587
+ closedAt: now,
588
+ lockedAt: now,
589
+ lockedByUserId: sdk.userInfo.id,
590
+ totalDurationInMS: 3600000,
591
+ timestamps: [
592
+ { type: 'start', timestamp: twoHoursAgo },
593
+ { type: 'pause', timestamp: oneHourAgo },
594
+ ],
595
+ } as any)
596
+ trackIds.push(locked.id)
597
+ log("D0: Locked track created")
598
+
599
+ // Test D1: Locked track rejects updates to timestamps
600
+ log("D1: Updating timestamps on locked track (should fail)...")
601
+ await assert_throws(
602
+ () => sdk.api.time_tracks.updateOne(locked.id, {
603
+ timestamps: [{ type: 'start', timestamp: new Date() }],
604
+ } as any),
605
+ "D1: timestamps update on locked track"
606
+ )
607
+ log("D1: Correctly rejected timestamps update")
608
+
609
+ // Test D2: Locked track rejects updates to closedAt
610
+ log("D2: Updating closedAt on locked track (should fail)...")
611
+ await assert_throws(
612
+ () => sdk.api.time_tracks.updateOne(locked.id, {
613
+ closedAt: new Date(),
614
+ } as any),
615
+ "D2: closedAt update on locked track"
616
+ )
617
+ log("D2: Correctly rejected closedAt update")
618
+
619
+ // Test D3: Locked track rejects updates to totalDurationInMS
620
+ log("D3: Updating totalDurationInMS on locked track (should fail)...")
621
+ await assert_throws(
622
+ () => sdk.api.time_tracks.updateOne(locked.id, {
623
+ totalDurationInMS: 999,
624
+ } as any),
625
+ "D3: totalDurationInMS update on locked track"
626
+ )
627
+ log("D3: Correctly rejected totalDurationInMS update")
628
+
629
+ // Test D4: Locked track rejects updates to correctedAt
630
+ log("D4: Updating correctedAt on locked track (should fail)...")
631
+ await assert_throws(
632
+ () => sdk.api.time_tracks.updateOne(locked.id, {
633
+ correctedAt: new Date(),
634
+ originalTotalDurationInMS: 3600000,
635
+ totalDurationInMS: 1800000,
636
+ lockedAt: new Date(),
637
+ lockedByUserId: sdk.userInfo.id,
638
+ } as any),
639
+ "D4: correctedAt update on locked track"
640
+ )
641
+ log("D4: Correctly rejected correction on locked track")
642
+
643
+ // Test D5: Locked track rejects updates to title
644
+ log("D5: Updating title on locked track (should fail)...")
645
+ await assert_throws(
646
+ () => sdk.api.time_tracks.updateOne(locked.id, {
647
+ title: "Should Not Change",
648
+ } as any),
649
+ "D5: title update on locked track"
650
+ )
651
+ log("D5: Correctly rejected title update")
652
+
653
+ // Test D6: Locked track allows updates to review fields
654
+ log("D6: Updating review fields on locked track (should succeed)...")
655
+ const reviewed = await sdk.api.time_tracks.updateOne(locked.id, {
656
+ reviewedAt: new Date(),
657
+ reviewedByUserId: sdkNonAdmin.userInfo.id,
658
+ reviewApproved: true,
659
+ reviewNote: "Looks good",
660
+ } as any)
661
+ assert(reviewed.reviewApproved === true, "reviewApproved should be set")
662
+ assert(reviewed.reviewNote === "Looks good", "reviewNote should be set")
663
+ assert(reviewed.reviewedByUserId === sdkNonAdmin.userInfo.id, "reviewedByUserId should match")
664
+ log("D6: Review fields correctly updatable on locked track")
665
+
666
+ log("All lock enforcement tests passed!")
667
+
668
+ } finally {
669
+ for (const id of trackIds) {
670
+ try { await sdk.api.time_tracks.deleteOne(id) } catch (e) {}
671
+ }
672
+ }
673
+ }
674
+
675
+ // ============================================================
676
+ // Group E: Edge Cases
677
+ // ============================================================
678
+ export const time_tracks_edge_case_tests = async ({ sdk, sdkNonAdmin }: { sdk: Session, sdkNonAdmin: Session }) => {
679
+ log_header("Time Tracks - Edge Case Tests")
680
+
681
+ const trackIds: string[] = []
682
+
683
+ try {
684
+ // Test E1: Normal real-time track - lockedAt stays undefined, all fields updatable
685
+ log("E1: Normal real-time track is fully updatable...")
686
+ const track = await sdk.api.time_tracks.createOne({
687
+ title: "Normal Track",
688
+ } as any)
689
+ trackIds.push(track.id)
690
+
691
+ assert(!track.lockedAt, "lockedAt should be undefined for normal track")
692
+ assert(!track.isHistorical, "isHistorical should be undefined for normal track")
693
+
694
+ // Should be able to update title
695
+ const updated = await sdk.api.time_tracks.updateOne(track.id, {
696
+ title: "Updated Normal Track",
697
+ })
698
+ assert(updated.title === "Updated Normal Track", "Title should be updatable on unlocked track")
699
+
700
+ // Should be able to update timestamps
701
+ const withPause = await sdk.api.time_tracks.updateOne(track.id, {
702
+ timestamps: [
703
+ ...(track.timestamps || []),
704
+ { type: 'pause', timestamp: new Date() },
705
+ ]
706
+ }, { replaceObjectFields: true })
707
+ assert(withPause.timestamps?.length === 2, "timestamps should be updatable on unlocked track")
708
+
709
+ log("E1: Normal track fully updatable as expected")
710
+
711
+ // Test E2: Original totalDurationInMS preserved after correction
712
+ log("E2: Original duration preserved after correction...")
713
+ const track2 = await sdk.api.time_tracks.createOne({
714
+ title: "Track for Duration Preservation",
715
+ } as any)
716
+ trackIds.push(track2.id)
717
+
718
+ // Close the track
719
+ await sdk.api.time_tracks.updateOne(track2.id, { closedAt: new Date() })
720
+ await wait(undefined, 1500)
721
+
722
+ const closedTrack = await sdk.api.time_tracks.getOne(track2.id)
723
+ const originalDuration = closedTrack.totalDurationInMS!
724
+
725
+ // Apply correction
726
+ const corrected = await sdk.api.time_tracks.updateOne(track2.id, {
727
+ correctedAt: new Date(),
728
+ correctedByUserId: sdk.userInfo.id,
729
+ originalTotalDurationInMS: originalDuration,
730
+ totalDurationInMS: 5000,
731
+ lockedAt: new Date(),
732
+ lockedByUserId: sdk.userInfo.id,
733
+ } as any)
734
+
735
+ assert(corrected.originalTotalDurationInMS === originalDuration,
736
+ `originalTotalDurationInMS should be ${originalDuration}, got ${corrected.originalTotalDurationInMS}`)
737
+ assert(corrected.totalDurationInMS === 5000,
738
+ `totalDurationInMS should be corrected to 5000, got ${corrected.totalDurationInMS}`)
739
+ log("E2: Original duration correctly preserved in originalTotalDurationInMS")
740
+
741
+ log("All edge case tests passed!")
742
+
743
+ } finally {
744
+ for (const id of trackIds) {
745
+ try { await sdk.api.time_tracks.deleteOne(id) } catch (e) {}
746
+ }
747
+ }
748
+ }
749
+
229
750
  // Allow running this test file independently
230
751
  if (require.main === module) {
231
752
  const sdk = new Session({ host })
@@ -238,15 +759,20 @@ if (require.main === module) {
238
759
  // Then run API tests
239
760
  await setup_tests(sdk, sdkNonAdmin)
240
761
  await time_tracks_tests({ sdk, sdkNonAdmin })
762
+ await time_tracks_historical_tests({ sdk, sdkNonAdmin })
763
+ await time_tracks_correction_tests({ sdk, sdkNonAdmin })
764
+ await time_tracks_review_tests({ sdk, sdkNonAdmin })
765
+ await time_tracks_lock_tests({ sdk, sdkNonAdmin })
766
+ await time_tracks_edge_case_tests({ sdk, sdkNonAdmin })
241
767
  }
242
768
 
243
769
  runTests()
244
770
  .then(() => {
245
- console.log("Time tracks test suite completed successfully")
771
+ console.log("Time tracks test suite completed successfully")
246
772
  process.exit(0)
247
773
  })
248
774
  .catch((error) => {
249
- console.error("Time tracks test suite failed:", error)
775
+ console.error("Time tracks test suite failed:", error)
250
776
  process.exit(1)
251
777
  })
252
778
  }