@tuturuuu/utils 0.33.3 → 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.
Files changed (54) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/package.json +6 -4
  3. package/src/__tests__/api-proxy-bearer-trust.test.ts +3 -0
  4. package/src/__tests__/api-proxy-guard.test.ts +6 -13
  5. package/src/__tests__/api-proxy-offline-response.test.ts +48 -0
  6. package/src/__tests__/api-proxy-test-request.ts +19 -0
  7. package/src/__tests__/calendar-provider-readonly.test.ts +45 -0
  8. package/src/__tests__/calendar-recurrence-edit.test.ts +140 -0
  9. package/src/__tests__/calendar-recurrence.test.ts +479 -0
  10. package/src/__tests__/daily-operation-download-isolation.test.ts +137 -0
  11. package/src/__tests__/offline-download-guard.test.ts +340 -0
  12. package/src/__tests__/permissions.test.tsx +21 -0
  13. package/src/__tests__/request-workspace.test.ts +63 -0
  14. package/src/__tests__/tutoring-teacher-hours.test.ts +274 -0
  15. package/src/__tests__/username-policy.test.ts +70 -0
  16. package/src/account-benefits-server.ts +43 -0
  17. package/src/account-benefits.ts +45 -0
  18. package/src/api-proxy-guard.ts +8 -42
  19. package/src/api-proxy-read-policy.ts +43 -0
  20. package/src/calendar-provider-readonly.ts +49 -0
  21. package/src/calendar-recurrence-edit.ts +148 -0
  22. package/src/calendar-recurrence-expansion.ts +131 -0
  23. package/src/calendar-recurrence-local.ts +45 -0
  24. package/src/calendar-recurrence-pattern.ts +54 -0
  25. package/src/calendar-recurrence-providers.ts +361 -0
  26. package/src/calendar-recurrence-validation.ts +180 -0
  27. package/src/calendar-recurrence.ts +5 -0
  28. package/src/calendar-utils.ts +41 -3
  29. package/src/common/metadata.test.ts +0 -6
  30. package/src/constants.ts +3 -0
  31. package/src/deployment-vault-permissions.tsx +26 -0
  32. package/src/desktop-signing-profiles.ts +20 -0
  33. package/src/google-calendar-import-fence.test.ts +182 -0
  34. package/src/google-calendar-import-fence.ts +166 -0
  35. package/src/next-config.ts +8 -1
  36. package/src/offline-download-guard.ts +160 -0
  37. package/src/permissions.tsx +2 -10
  38. package/src/platform-release.test.ts +1 -1
  39. package/src/platform-release.ts +1 -1
  40. package/src/playground-schema.test.ts +119 -0
  41. package/src/playground-schema.ts +173 -0
  42. package/src/playground-templates.ts +65 -0
  43. package/src/release-note-policy-core.cjs +28 -0
  44. package/src/release-note-policy-core.d.cts +5 -0
  45. package/src/release-note-policy.json +9 -0
  46. package/src/release-note-policy.ts +4 -0
  47. package/src/request-workspace.ts +29 -0
  48. package/src/seo-policy.json +196 -0
  49. package/src/seo.test.ts +168 -0
  50. package/src/seo.ts +49 -0
  51. package/src/tutoring-teacher-hours.ts +220 -0
  52. package/src/username-policy.json +87 -0
  53. package/src/username-policy.ts +18 -0
  54. package/vitest.config.ts +7 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,60 @@
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
+
24
+ ## [0.34.0](https://github.com/tutur3u/platform/compare/utils-v0.33.3...utils-v0.34.0) (2026-10-04)
25
+
26
+
27
+ ### Features
28
+
29
+ * **calendar:** support full Google color choices and scoped edits ([#5661](https://github.com/tutur3u/platform/issues/5661)) ([8cafac5](https://github.com/tutur3u/platform/commit/8cafac53438b504eafa8bcc8213d18448e4a17bc))
30
+ * **identity:** enforce creator identity and media policies ([1078119](https://github.com/tutur3u/platform/commit/1078119eb4871568f76e4bc31005ba9f1b1ddd76)) ([e71e94f](https://github.com/tutur3u/platform/commit/e71e94fd1a643c5ee76979ba9444976ad4798906))
31
+ * **identity:** protect creator usernames and profile media ([#5740](https://github.com/tutur3u/platform/issues/5740)) ([f8e18a4](https://github.com/tutur3u/platform/commit/f8e18a4c39d5bb65acfc5a680ccb9b2557ae4bed))
32
+ * **mobile:** support offline data, cached images, and safe inventory checkout ([a45d634](https://github.com/tutur3u/platform/commit/a45d63498cc1e686d1d070f5861cf938c4e9a9ef)) ([#5734](https://github.com/tutur3u/platform/issues/5734)) ([2f0f397](https://github.com/tutur3u/platform/commit/2f0f39763daea56f1029fcc3e99e0be53c60e106))
33
+
34
+
35
+ ### Bug Fixes
36
+
37
+ * **calendar:** fence metadata reads and retain recurrence during imports ([3895d28](https://github.com/tutur3u/platform/commit/3895d28fb4acd0164f6f021ad15df4c39a83938e))
38
+ * **identity:** align live support validation and profile parity ([7d5e1b0](https://github.com/tutur3u/platform/commit/7d5e1b056d34d316177c21b178f201edf7680651))
39
+ * **identity:** close profile policy and upload bypasses ([6f5aae5](https://github.com/tutur3u/platform/commit/6f5aae52c2ba3e633b4f24dd3f683d4b564a952f))
40
+ * **identity:** enforce verified upload budgets and canonical profile validation ([8138fb5](https://github.com/tutur3u/platform/commit/8138fb59276191508416031f69195de291b2b06a))
41
+ * **identity:** integrate main and order unapplied creator migrations ([5512e24](https://github.com/tutur3u/platform/commit/5512e2414abaa0c78b68cbdd3186a1d4bab861e7))
42
+ * **identity:** preserve budgeted avatar tickets and atomic handle claims ([b18092f](https://github.com/tutur3u/platform/commit/b18092fb7bf8632f6ba5f99ab8a61308d09544b9))
43
+ * **mobile:** harden offline inventory and scoped cache replay ([d9c0b13](https://github.com/tutur3u/platform/commit/d9c0b13b386287fb874be62a8b8ec794deeb6407))
44
+ * **release:** gate critical app promotion on staged API probes ([f39cea3](https://github.com/tutur3u/platform/commit/f39cea3621b504a9320b6d79675aa42b905b0476)) ([#5761](https://github.com/tutur3u/platform/issues/5761)) ([391397f](https://github.com/tutur3u/platform/commit/391397f90f651d041d8f0e9ae566f8c3eab60dd7))
45
+ * **release:** preserve manual test notes and share filtering policy ([ff312ca](https://github.com/tutur3u/platform/commit/ff312ca8b16e36bdc7f65484cded768de3073c17))
46
+ * **releases:** omit merge bookkeeping from release summaries ([c66e0b1](https://github.com/tutur3u/platform/commit/c66e0b139c89706db32ec70c3e87fcc9c4018b6c)) ([#5741](https://github.com/tutur3u/platform/issues/5741)) ([552cd5a](https://github.com/tutur3u/platform/commit/552cd5ae85d19d9147f1367475f5f6b3bf522a63))
47
+ * **sdk:** bound mixed judge jobs and validate playground exports ([c5efff9](https://github.com/tutur3u/platform/commit/c5efff9427dc36cae6c4212871a335f9cd4bde03))
48
+ * **security:** isolate bulk limits from ordinary web operations ([b3a9add](https://github.com/tutur3u/platform/commit/b3a9addf90271c7e28319a2138bbd52aaa294bc1))
49
+ * **security:** isolate offline bulk protection from daily web operations ([#5752](https://github.com/tutur3u/platform/issues/5752)) ([fd32aca](https://github.com/tutur3u/platform/commit/fd32aca3319c67d82149975e8c3a7615dfbca275))
50
+ * **security:** stage offline download protection activation ([018f6ab](https://github.com/tutur3u/platform/commit/018f6ab2ada3fef843d66789dee6ec45fba905d3))
51
+
52
+
53
+ ### Performance Improvements
54
+
55
+ * **devboxes:** parallelize bounded execution and retain warm playgrounds ([#5746](https://github.com/tutur3u/platform/issues/5746)) ([efd8ac4](https://github.com/tutur3u/platform/commit/efd8ac44b5c9f8cbf2ab5d64fbe89e1f7afe4f96))
56
+ * **devboxes:** parallelize bounded runner execution and retain warm playgrounds ([c31cbf3](https://github.com/tutur3u/platform/commit/c31cbf3a2a7cb5214d75183896791366affdc7e6))
57
+
3
58
  ## [0.33.3](https://github.com/tutur3u/platform/compare/utils-v0.33.2...utils-v0.33.3) (2026-10-02)
4
59
 
5
60
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tuturuuu/utils",
3
3
  "license": "MIT",
4
- "version": "0.33.3",
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,8 +25,9 @@
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.48.0",
28
+ "@tuturuuu/internal-api": "0.50.0",
28
29
  "@tuturuuu/supabase": "0.5.2",
30
+ "@tuturuuu/turnstile": "0.1.3",
29
31
  "@upstash/ratelimit": "^2.0.8",
30
32
  "@upstash/redis": "^1.38.4",
31
33
  "cn": "^0.3.0",
@@ -34,7 +36,7 @@
34
36
  "diff": "^9.0.0",
35
37
  "effect": "^3.22.2",
36
38
  "moment": "^2.30.1",
37
- "next": "^16.3.5",
39
+ "next": "^16.4.0",
38
40
  "prosemirror-model": "^1.25.11",
39
41
  "react": "^19.2.8",
40
42
  "react-dom": "^19.2.8",
@@ -45,7 +47,7 @@
45
47
  "zod": "^4.6.2"
46
48
  },
47
49
  "devDependencies": {
48
- "@tuturuuu/types": "0.33.1",
50
+ "@tuturuuu/types": "0.34.0",
49
51
  "@tuturuuu/typescript-config": "0.1.2",
50
52
  "@types/diff": "^8.0.0",
51
53
  "@types/node": "^26.5.1",
@@ -7,6 +7,9 @@ const mocks = vi.hoisted(() => ({
7
7
  blocked: vi.fn(),
8
8
  prefixes: [] as string[],
9
9
  }));
10
+ vi.mock('../offline-download-guard', () => ({
11
+ guardOfflineDownloadRequest: async () => null,
12
+ }));
10
13
  vi.mock('../required-mfa-runtime', () => ({
11
14
  enforceRequiredMfaRequest: async () => null,
12
15
  }));
@@ -3,6 +3,12 @@ vi.mock('../required-mfa-runtime', () => ({
3
3
  }));
4
4
 
5
5
  import { NextRequest } from 'next/server';
6
+ import { makeRequest } from './api-proxy-test-request';
7
+
8
+ vi.mock('../offline-download-guard', () => ({
9
+ guardOfflineDownloadRequest: async () => null,
10
+ }));
11
+
6
12
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
7
13
 
8
14
  const mocks = vi.hoisted(() => ({
@@ -74,19 +80,6 @@ vi.mock('../request-emoji-limit', () => ({
74
80
  ) => mocks.validateEmoji(req, options),
75
81
  }));
76
82
 
77
- function makeRequest(
78
- pathname = '/api/test',
79
- method = 'POST',
80
- headers?: Record<string, string>,
81
- body = '{}'
82
- ) {
83
- return new NextRequest(`http://localhost${pathname}`, {
84
- method,
85
- headers,
86
- body: method === 'GET' || method === 'HEAD' ? undefined : body,
87
- });
88
- }
89
-
90
83
  function singleReadRoutePolicies() {
91
84
  return [
92
85
  {
@@ -0,0 +1,48 @@
1
+ import { NextRequest } from 'next/server';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+
4
+ const guard = vi.hoisted(() => vi.fn());
5
+ vi.mock('../required-mfa-runtime', () => ({
6
+ enforceRequiredMfaRequest: async () => null,
7
+ }));
8
+ vi.mock('../offline-download-guard', () => ({
9
+ guardOfflineDownloadRequest: guard,
10
+ }));
11
+
12
+ import { guardApiProxyRequest } from '../api-proxy-guard';
13
+ import { isFinanceInvoiceCreateSupportRead } from '../api-proxy-read-policy';
14
+
15
+ describe('offline guard proxy integration', () => {
16
+ it('propagates a download challenge without continuing the request', async () => {
17
+ const challenge = Response.json(
18
+ { message: 'Verify download' },
19
+ { status: 403 }
20
+ );
21
+ guard.mockResolvedValueOnce(challenge);
22
+ const request = new NextRequest(
23
+ 'https://example.test/api/v1/workspaces/ws/tasks'
24
+ );
25
+ expect(
26
+ await guardApiProxyRequest(request, { prefixBase: 'test:offline' })
27
+ ).toBe(challenge);
28
+ expect(guard).toHaveBeenCalledWith(request);
29
+ });
30
+ it.each(['permissions', 'members'])(
31
+ 'excludes static settings/%s from invoice reads',
32
+ (name) => {
33
+ const request = new NextRequest(
34
+ `https://example.test/api/v1/workspaces/ws/settings/${name}`
35
+ );
36
+ expect(isFinanceInvoiceCreateSupportRead(request)).toBe(false);
37
+ }
38
+ );
39
+ it.each(['configs', 'custom-config'])(
40
+ 'retains supported settings/%s invoice reads',
41
+ (name) => {
42
+ const request = new NextRequest(
43
+ `https://example.test/api/v1/workspaces/ws/settings/${name}`
44
+ );
45
+ expect(isFinanceInvoiceCreateSupportRead(request)).toBe(true);
46
+ }
47
+ );
48
+ });
@@ -0,0 +1,19 @@
1
+ import { NextRequest } from 'next/server';
2
+
3
+ export function makeRequest(
4
+ pathname = '/api/test',
5
+ method = 'POST',
6
+ headers?: Record<string, string>,
7
+ body = '{}'
8
+ ) {
9
+ return new NextRequest(`http://localhost${pathname}`, {
10
+ method,
11
+ headers: {
12
+ ...(method !== 'GET' && method !== 'HEAD'
13
+ ? { 'content-type': 'application/json' }
14
+ : {}),
15
+ ...headers,
16
+ },
17
+ body: method === 'GET' || method === 'HEAD' ? undefined : body,
18
+ });
19
+ }
@@ -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
+ });