@tuturuuu/utils 0.0.3 → 0.6.0

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 (186) hide show
  1. package/CHANGELOG.md +305 -0
  2. package/biome.json +5 -0
  3. package/jsr.json +8 -8
  4. package/package.json +63 -32
  5. package/src/__tests__/ai-temp-auth.test.ts +309 -0
  6. package/src/__tests__/api-proxy-guard.test.ts +1451 -0
  7. package/src/__tests__/app-url.test.ts +270 -0
  8. package/src/__tests__/avatar-url.test.ts +97 -0
  9. package/src/__tests__/color-helper.test.ts +179 -0
  10. package/src/__tests__/constants.test.ts +351 -0
  11. package/src/__tests__/crypto.test.ts +107 -0
  12. package/src/__tests__/date-helper.test.ts +408 -0
  13. package/src/__tests__/fixtures/task-description-full-featured.json +456 -0
  14. package/src/__tests__/format.test.ts +317 -0
  15. package/src/__tests__/html-sanitizer.test.ts +360 -0
  16. package/src/__tests__/interest-calculator.test.ts +336 -0
  17. package/src/__tests__/interest-detector.test.ts +222 -0
  18. package/src/__tests__/label-colors.test.ts +241 -0
  19. package/src/__tests__/name-helper.test.ts +158 -0
  20. package/src/__tests__/node-diff.test.ts +576 -0
  21. package/src/__tests__/notification-service.test.ts +210 -0
  22. package/src/__tests__/onboarding-helper.test.ts +331 -0
  23. package/src/__tests__/path-helper.test.ts +152 -0
  24. package/src/__tests__/permissions.test.tsx +81 -0
  25. package/src/__tests__/request-emoji-limit.test.ts +172 -0
  26. package/src/__tests__/search-helper.test.ts +51 -0
  27. package/src/__tests__/storage-display-name.test.ts +37 -0
  28. package/src/__tests__/storage-path.test.ts +238 -0
  29. package/src/__tests__/tag-utils.test.ts +205 -0
  30. package/src/__tests__/task-description-yjs-state.test.ts +581 -0
  31. package/src/__tests__/task-helper-board-api-routing.test.ts +94 -0
  32. package/src/__tests__/task-helper-create-task.test.ts +129 -0
  33. package/src/__tests__/task-helpers.test.ts +464 -0
  34. package/src/__tests__/task-overrides.test.ts +305 -0
  35. package/src/__tests__/task-reorder-cache.test.ts +74 -0
  36. package/src/__tests__/task-sort-keys.test.ts +36 -0
  37. package/src/__tests__/task-transformers.test.ts +62 -0
  38. package/src/__tests__/text-helper.test.ts +776 -0
  39. package/src/__tests__/time-helper.test.ts +70 -0
  40. package/src/__tests__/time-tracker-period.test.ts +55 -0
  41. package/src/__tests__/timezone.test.ts +117 -0
  42. package/src/__tests__/upstash-rest.test.ts +77 -0
  43. package/src/__tests__/uuid-helper.test.ts +133 -0
  44. package/src/__tests__/workspace-helper.test.ts +859 -0
  45. package/src/__tests__/workspace-limits.test.ts +255 -0
  46. package/src/__tests__/yjs-helper.test.ts +581 -0
  47. package/src/abuse-protection/__tests__/backend-rate-limit.test.ts +113 -0
  48. package/src/abuse-protection/__tests__/edge.test.ts +136 -0
  49. package/src/abuse-protection/__tests__/index.test.ts +562 -0
  50. package/src/abuse-protection/__tests__/reputation.test.ts +192 -0
  51. package/src/abuse-protection/backend-rate-limit.ts +44 -0
  52. package/src/abuse-protection/constants.ts +117 -0
  53. package/src/abuse-protection/edge.ts +223 -0
  54. package/src/abuse-protection/index.ts +1545 -0
  55. package/src/abuse-protection/reputation.ts +587 -0
  56. package/src/abuse-protection/types.ts +97 -0
  57. package/src/abuse-protection/user-agent.ts +124 -0
  58. package/src/abuse-protection/user-suspension.ts +231 -0
  59. package/src/ai-temp-auth.ts +315 -0
  60. package/src/api-proxy-guard.ts +965 -0
  61. package/src/app-url.ts +96 -0
  62. package/src/avatar-url.ts +64 -0
  63. package/src/break-duration.ts +84 -0
  64. package/src/calendar-auth-token.test.ts +37 -0
  65. package/src/calendar-auth-token.ts +19 -0
  66. package/src/calendar-sync-coordination.md +197 -0
  67. package/src/calendar-utils.test.ts +169 -0
  68. package/src/calendar-utils.ts +91 -0
  69. package/src/color-helper.ts +110 -0
  70. package/src/common/nextjs.tsx +99 -0
  71. package/src/common/scan.tsx +15 -0
  72. package/src/configs/reports.ts +160 -0
  73. package/src/constants.ts +85 -0
  74. package/src/crypto.ts +21 -0
  75. package/src/currencies.ts +97 -0
  76. package/src/date-helper.ts +313 -0
  77. package/src/editor/convert-to-task.ts +264 -0
  78. package/src/editor/index.ts +5 -0
  79. package/src/email/__tests__/client.test.ts +141 -0
  80. package/src/email/__tests__/validation.test.ts +46 -0
  81. package/src/email/client.ts +92 -0
  82. package/src/email/server.ts +128 -0
  83. package/src/email/validation.ts +11 -0
  84. package/src/encryption/__tests__/calendar-events.test.ts +411 -0
  85. package/src/encryption/__tests__/configuration.test.ts +114 -0
  86. package/src/encryption/__tests__/field-encryption.test.ts +232 -0
  87. package/src/encryption/__tests__/key-generation.test.ts +30 -0
  88. package/src/encryption/__tests__/performance-edge-cases.test.ts +187 -0
  89. package/src/encryption/__tests__/test-helpers.ts +22 -0
  90. package/src/encryption/__tests__/workspace-key-encryption.test.ts +129 -0
  91. package/src/encryption/encryption-service.ts +343 -0
  92. package/src/encryption/index.ts +25 -0
  93. package/src/encryption/types.ts +57 -0
  94. package/src/exchange-rates.ts +49 -0
  95. package/src/feature-flags/__tests__/feature-flags.test.ts +302 -0
  96. package/src/feature-flags/core.ts +322 -0
  97. package/src/feature-flags/data.ts +16 -0
  98. package/src/feature-flags/default.ts +18 -0
  99. package/src/feature-flags/index.ts +7 -0
  100. package/src/feature-flags/requestable-features.ts +79 -0
  101. package/src/feature-flags/types.ts +4 -0
  102. package/src/fetcher.ts +2 -0
  103. package/src/finance/index.ts +4 -0
  104. package/src/finance/interest-calculator.ts +456 -0
  105. package/src/finance/interest-detector.ts +141 -0
  106. package/src/finance/transform-invoice-results.ts +219 -0
  107. package/src/finance/wallet-permissions.test.ts +169 -0
  108. package/src/finance/wallet-permissions.ts +82 -0
  109. package/src/format.ts +120 -1
  110. package/src/generated/platform-build-metadata.ts +11 -0
  111. package/src/hooks/use-platform.ts +64 -0
  112. package/src/html-sanitizer.ts +155 -0
  113. package/src/internal-domains.ts +497 -0
  114. package/src/keyboard-preset.ts +109 -0
  115. package/src/label-colors.ts +213 -0
  116. package/src/launchable-apps.test.ts +126 -0
  117. package/src/launchable-apps.ts +490 -0
  118. package/src/name-helper.ts +269 -0
  119. package/src/next-config.test.ts +234 -0
  120. package/src/next-config.ts +203 -0
  121. package/src/node-diff.ts +375 -0
  122. package/src/notification-service.ts +379 -0
  123. package/src/nova/scores/__tests__/calculate.test.ts +254 -0
  124. package/src/nova/scores/calculate.ts +132 -0
  125. package/src/nova/submissions/check-permission.ts +132 -0
  126. package/src/onboarding-helper.ts +213 -0
  127. package/src/path-helper.ts +93 -0
  128. package/src/permissions.tsx +1170 -0
  129. package/src/plan-helpers.test.ts +188 -0
  130. package/src/plan-helpers.ts +80 -0
  131. package/src/platform-release.test.ts +74 -0
  132. package/src/platform-release.ts +155 -0
  133. package/src/portless.ts +124 -0
  134. package/src/priority-styles.ts +42 -0
  135. package/src/request-emoji-limit.ts +335 -0
  136. package/src/search-helper.ts +18 -0
  137. package/src/search.test.ts +89 -0
  138. package/src/search.ts +355 -0
  139. package/src/storage-display-name.ts +30 -0
  140. package/src/storage-path.ts +147 -0
  141. package/src/tag-utils.ts +159 -0
  142. package/src/task/reorder.ts +245 -0
  143. package/src/task/transformers.ts +149 -0
  144. package/src/task-date-timezone.ts +133 -0
  145. package/src/task-description-content.ts +240 -0
  146. package/src/task-helper/board.ts +193 -0
  147. package/src/task-helper/bulk-actions.ts +564 -0
  148. package/src/task-helper/personal-external-staging.ts +21 -0
  149. package/src/task-helper/recycle-bin.ts +202 -0
  150. package/src/task-helper/relationships.ts +346 -0
  151. package/src/task-helper/shared.ts +109 -0
  152. package/src/task-helper/sort-keys.ts +337 -0
  153. package/src/task-helper/task-hooks-basic.ts +342 -0
  154. package/src/task-helper/task-hooks-move.ts +264 -0
  155. package/src/task-helper/task-operations.ts +278 -0
  156. package/src/task-helper.ts +12 -0
  157. package/src/task-helpers.ts +241 -0
  158. package/src/task-list-status.ts +62 -0
  159. package/src/task-overrides.ts +82 -0
  160. package/src/task-snapshot.ts +374 -0
  161. package/src/text-diff.ts +81 -0
  162. package/src/text-helper.ts +537 -0
  163. package/src/time-helper.ts +63 -0
  164. package/src/time-tracker-period.ts +73 -0
  165. package/src/timeblock-helper.ts +418 -0
  166. package/src/timezone.ts +190 -0
  167. package/src/timezones.json +1271 -0
  168. package/src/upstash-rest.ts +56 -0
  169. package/src/user-helper.ts +296 -0
  170. package/src/uuid-helper.ts +11 -0
  171. package/src/workspace-handle.ts +10 -0
  172. package/src/workspace-helper.ts +1408 -0
  173. package/src/workspace-limits.ts +68 -0
  174. package/src/yjs-helper.ts +217 -0
  175. package/src/yjs-task-description.ts +81 -0
  176. package/tsconfig.json +3 -5
  177. package/tsconfig.typecheck.json +33 -0
  178. package/vitest.config.ts +36 -0
  179. package/dist/index.d.ts +0 -8
  180. package/dist/index.js +0 -2
  181. package/dist/index.js.map +0 -1
  182. package/dist/index.mjs +0 -2
  183. package/dist/index.mjs.map +0 -1
  184. package/eslint.config.mjs +0 -20
  185. package/rollup.config.js +0 -41
  186. package/src/index.ts +0 -1
@@ -0,0 +1,581 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { describe, expect, it } from 'vitest';
4
+ import {
5
+ convertJsonContentToYjsState,
6
+ convertYjsStateToJsonContent,
7
+ } from '../yjs-helper';
8
+ import {
9
+ deriveTaskDescriptionYjsState,
10
+ isValidTaskDescriptionYjsState,
11
+ taskDescriptionSchema,
12
+ } from '../yjs-task-description';
13
+
14
+ const fullFeaturedDescription = readFileSync(
15
+ join(__dirname, 'fixtures', 'task-description-full-featured.json'),
16
+ 'utf8'
17
+ );
18
+
19
+ function walkNodes(
20
+ node: unknown,
21
+ visitor: (item: Record<string, unknown>) => void
22
+ ): void {
23
+ if (Array.isArray(node)) {
24
+ for (const child of node) {
25
+ walkNodes(child, visitor);
26
+ }
27
+ return;
28
+ }
29
+ if (!node || typeof node !== 'object') {
30
+ return;
31
+ }
32
+ const item = node as Record<string, unknown>;
33
+ visitor(item);
34
+ walkNodes(item.content, visitor);
35
+ }
36
+
37
+ describe('deriveTaskDescriptionYjsState', () => {
38
+ it('returns null for empty descriptions', () => {
39
+ expect(deriveTaskDescriptionYjsState(null)).toBeNull();
40
+ expect(deriveTaskDescriptionYjsState(undefined)).toBeNull();
41
+ expect(deriveTaskDescriptionYjsState('')).toBeNull();
42
+ expect(deriveTaskDescriptionYjsState(' ')).toBeNull();
43
+ });
44
+
45
+ it('derives yjs state from plain text', () => {
46
+ const state = deriveTaskDescriptionYjsState(
47
+ 'Mobile plain text description'
48
+ );
49
+
50
+ expect(state).not.toBeNull();
51
+ expect(Array.isArray(state)).toBe(true);
52
+ expect(state?.length ?? 0).toBeGreaterThan(0);
53
+ });
54
+
55
+ it('derives yjs state from TipTap JSON descriptions', () => {
56
+ const state = deriveTaskDescriptionYjsState(
57
+ JSON.stringify({
58
+ type: 'doc',
59
+ content: [
60
+ {
61
+ type: 'heading',
62
+ attrs: { level: 2 },
63
+ content: [{ type: 'text', text: 'Task heading' }],
64
+ },
65
+ {
66
+ type: 'taskList',
67
+ content: [
68
+ {
69
+ type: 'taskItem',
70
+ attrs: { checked: true },
71
+ content: [
72
+ {
73
+ type: 'paragraph',
74
+ content: [{ type: 'text', text: 'Checked item' }],
75
+ },
76
+ ],
77
+ },
78
+ ],
79
+ },
80
+ ],
81
+ })
82
+ );
83
+
84
+ expect(state).not.toBeNull();
85
+ expect(state?.length ?? 0).toBeGreaterThan(0);
86
+ });
87
+
88
+ it('falls back to plain text conversion for unsupported JSON nodes', () => {
89
+ const state = deriveTaskDescriptionYjsState(
90
+ JSON.stringify({
91
+ type: 'doc',
92
+ content: [
93
+ {
94
+ type: 'unsupportedNodeType',
95
+ content: [{ type: 'text', text: 'fallback text' }],
96
+ },
97
+ ],
98
+ })
99
+ );
100
+
101
+ expect(state).not.toBeNull();
102
+ expect(state?.length ?? 0).toBeGreaterThan(0);
103
+ });
104
+
105
+ it('handles full-featured web editor payloads', () => {
106
+ const state = deriveTaskDescriptionYjsState(fullFeaturedDescription);
107
+
108
+ expect(state).not.toBeNull();
109
+ expect(state?.length ?? 0).toBeGreaterThan(0);
110
+
111
+ const decoded = convertYjsStateToJsonContent(
112
+ new Uint8Array(state ?? []),
113
+ taskDescriptionSchema
114
+ ) as Record<string, unknown>;
115
+
116
+ let hasTable = false;
117
+ let hasImage = false;
118
+ let hasLink = false;
119
+ let hasBlockquote = false;
120
+ let hasCodeBlock = false;
121
+ let hasCheckedTaskItem = false;
122
+
123
+ walkNodes(decoded, (item) => {
124
+ const type = item.type;
125
+ if (type === 'table') {
126
+ hasTable = true;
127
+ }
128
+ if (type === 'imageResize') {
129
+ hasImage = true;
130
+ }
131
+ if (type === 'blockquote') {
132
+ hasBlockquote = true;
133
+ }
134
+ if (type === 'codeBlock') {
135
+ hasCodeBlock = true;
136
+ }
137
+ if (type === 'taskItem') {
138
+ const attrs = item.attrs as { checked?: boolean } | undefined;
139
+ if (attrs?.checked === true) {
140
+ hasCheckedTaskItem = true;
141
+ }
142
+ }
143
+ const marks = item.marks;
144
+ if (Array.isArray(marks)) {
145
+ for (const mark of marks) {
146
+ if (
147
+ mark &&
148
+ typeof mark === 'object' &&
149
+ (mark as { type?: string }).type === 'link'
150
+ ) {
151
+ hasLink = true;
152
+ }
153
+ }
154
+ }
155
+ });
156
+
157
+ expect(hasTable).toBe(true);
158
+ expect(hasImage).toBe(true);
159
+ expect(hasLink).toBe(true);
160
+ expect(hasBlockquote).toBe(true);
161
+ expect(hasCodeBlock).toBe(true);
162
+ expect(hasCheckedTaskItem).toBe(true);
163
+ });
164
+
165
+ it('transforms legacy image nodes to imageResize for schema compatibility', () => {
166
+ const state = deriveTaskDescriptionYjsState(
167
+ JSON.stringify({
168
+ type: 'doc',
169
+ content: [
170
+ {
171
+ type: 'image',
172
+ attrs: {
173
+ src: '/api/v1/workspaces/ws-1/storage/share?path=task-images%2Ftask-1%2Fimage.png',
174
+ alt: 'Test image',
175
+ title: 'Test',
176
+ },
177
+ },
178
+ ],
179
+ })
180
+ );
181
+
182
+ expect(state).not.toBeNull();
183
+ expect(state?.length ?? 0).toBeGreaterThan(0);
184
+ });
185
+
186
+ it('transforms nested image nodes inside complex structures', () => {
187
+ const state = deriveTaskDescriptionYjsState(
188
+ JSON.stringify({
189
+ type: 'doc',
190
+ content: [
191
+ {
192
+ type: 'blockquote',
193
+ content: [
194
+ {
195
+ type: 'paragraph',
196
+ content: [
197
+ {
198
+ type: 'image',
199
+ attrs: {
200
+ src: '/api/v1/workspaces/ws-1/storage/share?path=task-images%2Ftask-1%2Fnested.png',
201
+ alt: null,
202
+ title: null,
203
+ },
204
+ },
205
+ ],
206
+ },
207
+ ],
208
+ },
209
+ ],
210
+ })
211
+ );
212
+
213
+ expect(state).not.toBeNull();
214
+ expect(state?.length ?? 0).toBeGreaterThan(0);
215
+ });
216
+
217
+ it('handles image nodes alongside other supported nodes', () => {
218
+ const state = deriveTaskDescriptionYjsState(
219
+ JSON.stringify({
220
+ type: 'doc',
221
+ content: [
222
+ {
223
+ type: 'heading',
224
+ attrs: { level: 1 },
225
+ content: [{ type: 'text', text: 'Image Test' }],
226
+ },
227
+ {
228
+ type: 'paragraph',
229
+ content: [{ type: 'text', text: 'Description with ' }],
230
+ },
231
+ {
232
+ type: 'image',
233
+ attrs: {
234
+ src: '/api/v1/workspaces/ws-1/storage/share?path=task-images%2Ftask-1%2Fhero.png',
235
+ alt: 'Hero image',
236
+ title: 'Hero',
237
+ width: 800,
238
+ height: null,
239
+ },
240
+ },
241
+ {
242
+ type: 'taskList',
243
+ content: [
244
+ {
245
+ type: 'taskItem',
246
+ attrs: { checked: false },
247
+ content: [
248
+ {
249
+ type: 'paragraph',
250
+ content: [{ type: 'text', text: 'Task with image above' }],
251
+ },
252
+ ],
253
+ },
254
+ ],
255
+ },
256
+ ],
257
+ })
258
+ );
259
+
260
+ expect(state).not.toBeNull();
261
+ expect(state?.length ?? 0).toBeGreaterThan(0);
262
+ });
263
+
264
+ it('handles video and youtube nodes without errors', () => {
265
+ const state = deriveTaskDescriptionYjsState(
266
+ JSON.stringify({
267
+ type: 'doc',
268
+ content: [
269
+ {
270
+ type: 'video',
271
+ attrs: {
272
+ src: '/api/v1/workspaces/ws-1/storage/share?path=task-images%2Ftask-1%2Fvideo.mp4',
273
+ },
274
+ },
275
+ {
276
+ type: 'youtube',
277
+ attrs: {
278
+ src: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
279
+ videoId: 'dQw4w9WgXcQ',
280
+ },
281
+ },
282
+ ],
283
+ })
284
+ );
285
+
286
+ expect(state).not.toBeNull();
287
+ expect(state?.length ?? 0).toBeGreaterThan(0);
288
+ });
289
+
290
+ it('rejects Yjs state that decodes to unsupported editor nodes', () => {
291
+ const malformedState = convertJsonContentToYjsState({
292
+ type: 'doc',
293
+ content: [
294
+ {
295
+ type: 'unsupportedNode',
296
+ content: [{ type: 'text', text: 'Invalid node' }],
297
+ },
298
+ ],
299
+ });
300
+
301
+ expect(isValidTaskDescriptionYjsState(Array.from(malformedState))).toBe(
302
+ false
303
+ );
304
+ });
305
+
306
+ it('handles mixed imageResize and legacy image nodes in same document', () => {
307
+ const state = deriveTaskDescriptionYjsState(
308
+ JSON.stringify({
309
+ type: 'doc',
310
+ content: [
311
+ {
312
+ type: 'imageResize',
313
+ attrs: {
314
+ src: '/api/v1/workspaces/ws-1/storage/share?path=task-images%2Ftask-1%2Fnew.png',
315
+ alt: 'New image',
316
+ title: null,
317
+ width: 600,
318
+ height: null,
319
+ },
320
+ },
321
+ {
322
+ type: 'image',
323
+ attrs: {
324
+ src: '/api/v1/workspaces/ws-1/storage/share?path=task-images%2Ftask-1%2Flegacy.png',
325
+ alt: 'Legacy image',
326
+ title: null,
327
+ },
328
+ },
329
+ ],
330
+ })
331
+ );
332
+
333
+ expect(state).not.toBeNull();
334
+ expect(state?.length ?? 0).toBeGreaterThan(0);
335
+ });
336
+
337
+ it('handles deeply nested list structures', () => {
338
+ const state = deriveTaskDescriptionYjsState(
339
+ JSON.stringify({
340
+ type: 'doc',
341
+ content: [
342
+ {
343
+ type: 'bulletList',
344
+ content: [
345
+ {
346
+ type: 'listItem',
347
+ content: [
348
+ {
349
+ type: 'paragraph',
350
+ content: [{ type: 'text', text: 'Level 1' }],
351
+ },
352
+ {
353
+ type: 'bulletList',
354
+ content: [
355
+ {
356
+ type: 'listItem',
357
+ content: [
358
+ {
359
+ type: 'paragraph',
360
+ content: [{ type: 'text', text: 'Level 2' }],
361
+ },
362
+ ],
363
+ },
364
+ ],
365
+ },
366
+ ],
367
+ },
368
+ ],
369
+ },
370
+ ],
371
+ })
372
+ );
373
+
374
+ expect(state).not.toBeNull();
375
+ expect(state?.length ?? 0).toBeGreaterThan(0);
376
+ });
377
+
378
+ it('handles table with various cell content types', () => {
379
+ const state = deriveTaskDescriptionYjsState(
380
+ JSON.stringify({
381
+ type: 'doc',
382
+ content: [
383
+ {
384
+ type: 'table',
385
+ content: [
386
+ {
387
+ type: 'tableRow',
388
+ content: [
389
+ {
390
+ type: 'tableHeader',
391
+ attrs: { colspan: 1, rowspan: 1, colwidth: null },
392
+ content: [
393
+ {
394
+ type: 'paragraph',
395
+ content: [{ type: 'text', text: 'Header A' }],
396
+ },
397
+ ],
398
+ },
399
+ {
400
+ type: 'tableCell',
401
+ attrs: { colspan: 1, rowspan: 1, colwidth: null },
402
+ content: [
403
+ {
404
+ type: 'paragraph',
405
+ content: [{ type: 'text', text: 'Cell B' }],
406
+ },
407
+ ],
408
+ },
409
+ ],
410
+ },
411
+ ],
412
+ },
413
+ ],
414
+ })
415
+ );
416
+
417
+ expect(state).not.toBeNull();
418
+ expect(state?.length ?? 0).toBeGreaterThan(0);
419
+ });
420
+
421
+ it('handles mention nodes with all attribute variations', () => {
422
+ const state = deriveTaskDescriptionYjsState(
423
+ JSON.stringify({
424
+ type: 'doc',
425
+ content: [
426
+ {
427
+ type: 'paragraph',
428
+ content: [
429
+ {
430
+ type: 'mention',
431
+ attrs: {
432
+ id: 'user-1',
433
+ label: 'John Doe',
434
+ userId: 'user-1',
435
+ entityId: 'user-1',
436
+ entityType: 'user',
437
+ displayName: 'John Doe',
438
+ avatarUrl: 'https://example.com/avatar.png',
439
+ subtitle: 'Engineer',
440
+ priority: null,
441
+ listColor: null,
442
+ assignees: null,
443
+ workspaceId: 'ws-1',
444
+ },
445
+ },
446
+ ],
447
+ },
448
+ ],
449
+ })
450
+ );
451
+
452
+ expect(state).not.toBeNull();
453
+ expect(state?.length ?? 0).toBeGreaterThan(0);
454
+ });
455
+
456
+ it('handles text with all mark types', () => {
457
+ const state = deriveTaskDescriptionYjsState(
458
+ JSON.stringify({
459
+ type: 'doc',
460
+ content: [
461
+ {
462
+ type: 'paragraph',
463
+ content: [
464
+ {
465
+ type: 'text',
466
+ marks: [{ type: 'bold' }],
467
+ text: 'Bold',
468
+ },
469
+ { type: 'text', marks: [{ type: 'italic' }], text: 'Italic' },
470
+ { type: 'text', marks: [{ type: 'strike' }], text: 'Strike' },
471
+ {
472
+ type: 'text',
473
+ marks: [{ type: 'underline' }],
474
+ text: 'Underline',
475
+ },
476
+ { type: 'text', marks: [{ type: 'code' }], text: 'Code' },
477
+ {
478
+ type: 'text',
479
+ marks: [{ type: 'subscript' }],
480
+ text: 'Sub',
481
+ },
482
+ {
483
+ type: 'text',
484
+ marks: [{ type: 'superscript' }],
485
+ text: 'Super',
486
+ },
487
+ {
488
+ type: 'text',
489
+ marks: [{ type: 'highlight', attrs: { color: '#FFF59D' } }],
490
+ text: 'Highlight',
491
+ },
492
+ {
493
+ type: 'text',
494
+ marks: [
495
+ {
496
+ type: 'link',
497
+ attrs: {
498
+ href: 'https://example.com',
499
+ target: '_blank',
500
+ rel: 'noopener noreferrer',
501
+ class: 'link',
502
+ title: null,
503
+ },
504
+ },
505
+ ],
506
+ text: 'Link',
507
+ },
508
+ ],
509
+ },
510
+ ],
511
+ })
512
+ );
513
+
514
+ expect(state).not.toBeNull();
515
+ expect(state?.length ?? 0).toBeGreaterThan(0);
516
+ });
517
+
518
+ it('handles horizontal rule nodes', () => {
519
+ const state = deriveTaskDescriptionYjsState(
520
+ JSON.stringify({
521
+ type: 'doc',
522
+ content: [
523
+ {
524
+ type: 'paragraph',
525
+ content: [{ type: 'text', text: 'Before' }],
526
+ },
527
+ { type: 'horizontalRule' },
528
+ {
529
+ type: 'paragraph',
530
+ content: [{ type: 'text', text: 'After' }],
531
+ },
532
+ ],
533
+ })
534
+ );
535
+
536
+ expect(state).not.toBeNull();
537
+ expect(state?.length ?? 0).toBeGreaterThan(0);
538
+ });
539
+
540
+ it('handles blockquote with mixed content', () => {
541
+ const state = deriveTaskDescriptionYjsState(
542
+ JSON.stringify({
543
+ type: 'doc',
544
+ content: [
545
+ {
546
+ type: 'blockquote',
547
+ content: [
548
+ {
549
+ type: 'paragraph',
550
+ content: [
551
+ {
552
+ type: 'text',
553
+ marks: [{ type: 'italic' }],
554
+ text: 'Quoted text',
555
+ },
556
+ ],
557
+ },
558
+ {
559
+ type: 'bulletList',
560
+ content: [
561
+ {
562
+ type: 'listItem',
563
+ content: [
564
+ {
565
+ type: 'paragraph',
566
+ content: [{ type: 'text', text: 'Item in quote' }],
567
+ },
568
+ ],
569
+ },
570
+ ],
571
+ },
572
+ ],
573
+ },
574
+ ],
575
+ })
576
+ );
577
+
578
+ expect(state).not.toBeNull();
579
+ expect(state?.length ?? 0).toBeGreaterThan(0);
580
+ });
581
+ });
@@ -0,0 +1,94 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ const { createWorkspaceTaskBoardMock, getWorkspaceTaskBoardMock } = vi.hoisted(
4
+ () => ({
5
+ createWorkspaceTaskBoardMock: vi.fn(),
6
+ getWorkspaceTaskBoardMock: vi.fn(),
7
+ })
8
+ );
9
+
10
+ vi.mock('@tuturuuu/internal-api/tasks', () => ({
11
+ createWorkspaceTaskBoard: createWorkspaceTaskBoardMock,
12
+ getWorkspaceTaskBoard: getWorkspaceTaskBoardMock,
13
+ listWorkspaceTasks: vi.fn(),
14
+ listWorkspaceTaskLists: vi.fn(),
15
+ listTaskBoardStatusTemplates: vi.fn(),
16
+ updateWorkspaceTask: vi.fn(),
17
+ createWorkspaceTaskRelationship: vi.fn(),
18
+ createWorkspaceTaskWithRelationship: vi.fn(),
19
+ deleteWorkspaceTaskRelationship: vi.fn(),
20
+ deleteWorkspaceTask: vi.fn(),
21
+ moveWorkspaceTask: vi.fn(),
22
+ createWorkspaceTask: vi.fn(),
23
+ createWorkspaceTaskList: vi.fn(),
24
+ }));
25
+
26
+ vi.mock('@tuturuuu/supabase/next/client', () => ({
27
+ createClient: vi.fn(() => ({})),
28
+ }));
29
+
30
+ import { createBoardWithTemplate, getTaskBoard } from '../task-helper';
31
+
32
+ describe('task-helper workspace board API routing', () => {
33
+ beforeEach(() => {
34
+ vi.clearAllMocks();
35
+ });
36
+
37
+ it('getTaskBoard uses explicit workspace id', async () => {
38
+ getWorkspaceTaskBoardMock.mockResolvedValue({
39
+ board: { id: 'board-1', ws_id: 'ws-1', name: 'Board 1' },
40
+ });
41
+
42
+ const result = await getTaskBoard('board-1', 'ws-1', {
43
+ baseUrl: 'https://internal.example.com',
44
+ });
45
+
46
+ expect(getWorkspaceTaskBoardMock).toHaveBeenCalledWith('ws-1', 'board-1', {
47
+ baseUrl: 'https://internal.example.com',
48
+ });
49
+ expect(result).toEqual({ id: 'board-1', ws_id: 'ws-1', name: 'Board 1' });
50
+ });
51
+
52
+ it('getTaskBoard skips internal API call when workspace id is omitted', async () => {
53
+ const result = await getTaskBoard('board-1', undefined, {
54
+ baseUrl: 'https://internal.example.com',
55
+ });
56
+
57
+ expect(getWorkspaceTaskBoardMock).not.toHaveBeenCalled();
58
+ expect(result).toBeNull();
59
+ });
60
+
61
+ it('createBoardWithTemplate creates board through internal API helper', async () => {
62
+ createWorkspaceTaskBoardMock.mockResolvedValue({
63
+ board: {
64
+ id: 'board-1',
65
+ ws_id: 'ws-1',
66
+ name: 'Board 1',
67
+ template_id: 'template-1',
68
+ },
69
+ });
70
+
71
+ const result = await createBoardWithTemplate(
72
+ 'ws-1',
73
+ 'Board 1',
74
+ 'template-1'
75
+ );
76
+
77
+ expect(createWorkspaceTaskBoardMock).toHaveBeenCalledWith(
78
+ 'ws-1',
79
+ {
80
+ name: 'Board 1',
81
+ template_id: 'template-1',
82
+ icon: null,
83
+ },
84
+ undefined
85
+ );
86
+ expect(result).toEqual(
87
+ expect.objectContaining({
88
+ id: 'board-1',
89
+ ws_id: 'ws-1',
90
+ name: 'Board 1',
91
+ })
92
+ );
93
+ });
94
+ });