@tuturuuu/utils 0.34.0 → 0.35.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.35.0](https://github.com/tutur3u/platform/compare/utils-v0.34.0...utils-v0.35.0) (2026-10-09)
4
+
5
+
6
+ ### Features
7
+
8
+ * **docs:** expand platform guides and enforce app SEO and runtime pause ([e79cb7e](https://github.com/tutur3u/platform/commit/e79cb7e96c7db4d1baedb09b7420ca41b75e367f))
9
+ * **docs:** expand platform guides, enforce SEO, and pause inactive runtimes ([#6085](https://github.com/tutur3u/platform/issues/6085)) ([df7a159](https://github.com/tutur3u/platform/commit/df7a159e9c657fee4f6e0b593c776d6887b2ae11))
10
+ * **tutoring:** define teacher hours domain contract ([b6fa164](https://github.com/tutur3u/platform/commit/b6fa1647d0c6afd9788778de7648d34b037dde61)) ([#6001](https://github.com/tutur3u/platform/issues/6001)) ([c23e08a](https://github.com/tutur3u/platform/commit/c23e08a403fbf59b3b74cc1650f3abc1505ad1aa))
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **calendar:** stabilize civil dates and today rendering ([c1abdf0](https://github.com/tutur3u/platform/commit/c1abdf0df75c0a02cc00c37b7a0a5e55c862bf89)) ([#5950](https://github.com/tutur3u/platform/issues/5950)) ([5a00326](https://github.com/tutur3u/platform/commit/5a00326171bec65ec7210de161dd5272bfda22b2))
16
+ * **finance:** reconcile attendance summary with current main ([fb00e6e](https://github.com/tutur3u/platform/commit/fb00e6ef4373adc1b6b9107f9bba883c101c3438))
17
+ * **utils:** keep teacher-hours tests within package boundary ([bf55e46](https://github.com/tutur3u/platform/commit/bf55e46d5a842d70ea87c03b9b0e40c65c14f60d)) ([#6006](https://github.com/tutur3u/platform/issues/6006)) ([1039959](https://github.com/tutur3u/platform/commit/1039959d7258ed9fc612c0871ae575a71f3e4fe8))
18
+
19
+
20
+ ### Performance Improvements
21
+
22
+ * **calendar:** reject oversized recurrence scans before expansion ([ac3441b](https://github.com/tutur3u/platform/commit/ac3441b4881b9ef0f8f380fdcf22c46e8794e145)) ([#5908](https://github.com/tutur3u/platform/issues/5908)) ([f662fef](https://github.com/tutur3u/platform/commit/f662fef3995435caa84153b06e7bb8e4c74aaf45))
23
+
3
24
  ## [0.34.0](https://github.com/tutur3u/platform/compare/utils-v0.33.3...utils-v0.34.0) (2026-10-04)
4
25
 
5
26
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tuturuuu/utils",
3
3
  "license": "MIT",
4
- "version": "0.34.0",
4
+ "version": "0.35.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/tutur3u/platform",
@@ -17,6 +17,7 @@
17
17
  },
18
18
  "dependencies": {
19
19
  "@dnd-kit/core": "^6.3.1",
20
+ "@js-temporal/polyfill": "^0.5.1",
20
21
  "@noble/ciphers": "^2.4.0",
21
22
  "@noble/hashes": "^2.4.0",
22
23
  "@tanstack/react-query": "^5.102.8",
@@ -24,7 +25,7 @@
24
25
  "@tiptap/react": "3.31.3",
25
26
  "@tuturuuu/google": "0.1.1",
26
27
  "@tuturuuu/icons": "0.1.1",
27
- "@tuturuuu/internal-api": "0.49.0",
28
+ "@tuturuuu/internal-api": "0.50.0",
28
29
  "@tuturuuu/supabase": "0.5.2",
29
30
  "@tuturuuu/turnstile": "0.1.3",
30
31
  "@upstash/ratelimit": "^2.0.8",
@@ -35,7 +36,7 @@
35
36
  "diff": "^9.0.0",
36
37
  "effect": "^3.22.2",
37
38
  "moment": "^2.30.1",
38
- "next": "^16.3.5",
39
+ "next": "^16.4.0",
39
40
  "prosemirror-model": "^1.25.11",
40
41
  "react": "^19.2.8",
41
42
  "react-dom": "^19.2.8",
@@ -0,0 +1,45 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ isProviderRecurrenceReadonly,
4
+ markProviderRecurrenceReadonly,
5
+ providerReadonlyProjection,
6
+ } from '../calendar-provider-readonly';
7
+
8
+ describe('readonly provider projection', () => {
9
+ it.each(['google', 'microsoft'] as const)(
10
+ 'projects %s internal marker without leaking raw provider snapshot',
11
+ (provider) => {
12
+ const original = { id: 'instance', summary: 'Private title' };
13
+ const projected = providerReadonlyProjection(
14
+ markProviderRecurrenceReadonly(original, provider, 'master')
15
+ );
16
+ expect(projected).toEqual({
17
+ locked: true,
18
+ scheduling_metadata: {
19
+ provider_recurrence: {
20
+ state: 'unsupported',
21
+ provider,
22
+ master_id: 'master',
23
+ },
24
+ },
25
+ });
26
+ expect(isProviderRecurrenceReadonly(projected)).toBe(true);
27
+ expect(original).toEqual({ id: 'instance', summary: 'Private title' });
28
+ }
29
+ );
30
+ it.each([
31
+ null,
32
+ {},
33
+ {
34
+ __tuturuuuProviderReadonlyRecurrence: {
35
+ state: 'unsupported',
36
+ provider: 'foreign',
37
+ master_id: 'master',
38
+ },
39
+ },
40
+ ])('ignores malformed markers', (value) =>
41
+ expect(providerReadonlyProjection(value)).toEqual({})
42
+ );
43
+ it('does not classify an ordinary locked event as provider readonly', () =>
44
+ expect(isProviderRecurrenceReadonly({ locked: true })).toBe(false));
45
+ });
@@ -0,0 +1,140 @@
1
+ import type { CalendarRecurrenceRule } from '@tuturuuu/types/primitives/calendar-recurrence';
2
+ import { describe, expect, it } from 'vitest';
3
+ import {
4
+ calendarAnchorAtSlot,
5
+ calendarProviderDateTimeLocal,
6
+ calendarRecurrenceSlotBounds,
7
+ calendarRecurrenceSlotInstant,
8
+ inspectCalendarRecurrenceSlot,
9
+ } from '../calendar-recurrence-edit';
10
+
11
+ const anchor = {
12
+ startLocal: '2026-03-07T02:30:00',
13
+ endLocal: '2026-03-07T03:30:00',
14
+ allDay: false,
15
+ };
16
+ const rule: CalendarRecurrenceRule = {
17
+ version: 1,
18
+ frequency: 'daily',
19
+ interval: 1,
20
+ timeZone: 'America/New_York',
21
+ end: { type: 'count', count: 4 },
22
+ };
23
+ describe('recurrence edit identity and future split', () => {
24
+ it('does not consume nonexistent DST slots when splitting count', () => {
25
+ const result = inspectCalendarRecurrenceSlot({
26
+ rule,
27
+ anchor,
28
+ originalStartLocal: '2026-03-10T02:30:00',
29
+ });
30
+ expect(result.precedingCount).toBe(2);
31
+ expect(result.remainingRule.end).toEqual({ type: 'count', count: 2 });
32
+ expect(result.previousRule?.end).toEqual({
33
+ type: 'until',
34
+ date: '2026-03-09',
35
+ });
36
+ });
37
+ it.each([
38
+ '2026-03-08T02:30:00',
39
+ '2026-03-07T03:30:00',
40
+ '2026-03-06T02:30:00',
41
+ '2026-03-12T02:30:00',
42
+ ])('rejects nonexistent or exhausted slot %s', (originalStartLocal) => {
43
+ expect(() =>
44
+ inspectCalendarRecurrenceSlot({ rule, anchor, originalStartLocal })
45
+ ).toThrow('Not a series occurrence');
46
+ });
47
+ it('maps first-slot future edit to whole-series edit', () => {
48
+ expect(
49
+ inspectCalendarRecurrenceSlot({
50
+ rule,
51
+ anchor,
52
+ originalStartLocal: anchor.startLocal,
53
+ }).previousRule
54
+ ).toBeNull();
55
+ });
56
+ it('rejects calendar-day boundaries after inclusive until', () => {
57
+ expect(() =>
58
+ inspectCalendarRecurrenceSlot({
59
+ rule: { ...rule, end: { type: 'until', date: '2026-03-09' } },
60
+ anchor,
61
+ originalStartLocal: '2026-03-10T02:30:00',
62
+ })
63
+ ).toThrow();
64
+ });
65
+ it('preserves multi-day local duration at future boundary', () => {
66
+ expect(
67
+ calendarAnchorAtSlot(
68
+ { ...anchor, endLocal: '2026-03-09T03:30:00' },
69
+ '2026-03-15T02:30:00'
70
+ ).endLocal
71
+ ).toBe('2026-03-17T03:30:00');
72
+ });
73
+ it('does not admit intervening dates on weekly interval', () => {
74
+ expect(() =>
75
+ inspectCalendarRecurrenceSlot({
76
+ rule: { ...rule, frequency: 'weekly', interval: 2, weekdays: ['SA'] },
77
+ anchor,
78
+ originalStartLocal: '2026-03-14T02:30:00',
79
+ })
80
+ ).toThrow();
81
+ });
82
+ });
83
+
84
+ describe('provider occurrence timezone identity', () => {
85
+ it('keeps the immutable slot instant after daylight saving changes', () => {
86
+ expect(
87
+ calendarRecurrenceSlotInstant({
88
+ rule,
89
+ anchor,
90
+ originalStartLocal: '2026-03-09T02:30:00',
91
+ })
92
+ ).toBe('2026-03-09T06:30:00Z');
93
+ });
94
+ it('rejects a nonexistent original slot instead of shifting its identity', () => {
95
+ expect(() =>
96
+ calendarRecurrenceSlotInstant({
97
+ rule,
98
+ anchor,
99
+ originalStartLocal: '2026-03-08T02:30:00',
100
+ })
101
+ ).toThrow();
102
+ });
103
+ it.each([
104
+ { dateTime: '2026-03-09T06:30:00Z' },
105
+ { dateTime: '2026-03-09T02:30:00-04:00' },
106
+ { dateTime: '2026-03-09T06:30:00.0000000', timeZone: 'UTC' },
107
+ ])('normalizes provider time without machine timezone: %j', (input) => {
108
+ expect(calendarProviderDateTimeLocal(input, 'America/New_York')).toBe(
109
+ '2026-03-09T02:30:00'
110
+ );
111
+ });
112
+ it('rejects unsupported timezone names', () => {
113
+ expect(() =>
114
+ calendarProviderDateTimeLocal(
115
+ { dateTime: '2026-03-09T09:00:00', timeZone: 'not-a-zone' },
116
+ 'UTC'
117
+ )
118
+ ).toThrow();
119
+ });
120
+ });
121
+
122
+ describe('provider original interval coverage', () => {
123
+ it('keeps wall-clock end boundaries across DST for multi-day slots', () => {
124
+ expect(
125
+ calendarRecurrenceSlotBounds({
126
+ rule,
127
+ anchor: { ...anchor, endLocal: '2026-03-09T03:30:00' },
128
+ originalStartLocal: '2026-03-07T02:30:00',
129
+ })
130
+ ).toEqual({ start: '2026-03-07T07:30:00Z', end: '2026-03-09T07:30:00Z' });
131
+ });
132
+ it('refuses to truncate nonzero provider fractional seconds into another immutable identity', () => {
133
+ expect(() =>
134
+ calendarProviderDateTimeLocal(
135
+ { dateTime: '2026-03-09T06:30:00.0000001Z' },
136
+ 'America/New_York'
137
+ )
138
+ ).toThrow('fractional');
139
+ });
140
+ });