claudeup 6.2.0 → 6.3.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 (153) hide show
  1. package/README.md +22 -0
  2. package/bin/claudeup.js +6 -1
  3. package/package.json +6 -6
  4. package/src/__tests__/alias-adopt.test.ts +331 -329
  5. package/src/__tests__/alias-parser.test.ts +384 -376
  6. package/src/__tests__/alias-shell-writer.test.ts +638 -625
  7. package/src/__tests__/alias-store.test.ts +143 -143
  8. package/src/__tests__/catalog-cache-store.test.ts +265 -265
  9. package/src/__tests__/catalog-notice.test.ts +123 -123
  10. package/src/__tests__/cli-apply-seams.test.ts +12 -8
  11. package/src/__tests__/cli-router.test.ts +132 -3
  12. package/src/__tests__/cli-update-view.test.ts +27 -8
  13. package/src/__tests__/community-staleness.test.ts +0 -2
  14. package/src/__tests__/component-badge.test.ts +4 -4
  15. package/src/__tests__/content-drift.test.ts +5 -1
  16. package/src/__tests__/conventions-integration.test.ts +774 -689
  17. package/src/__tests__/conventions-manager.test.ts +1216 -1152
  18. package/src/__tests__/doctor-bins.test.ts +14 -6
  19. package/src/__tests__/doctor.test.ts +438 -425
  20. package/src/__tests__/dual-write-prevention.test.ts +277 -282
  21. package/src/__tests__/enabled-not-installed.test.ts +71 -63
  22. package/src/__tests__/file-locking.test.ts +196 -196
  23. package/src/__tests__/gap-fill-versions.test.ts +318 -311
  24. package/src/__tests__/github-budget.test.ts +170 -164
  25. package/src/__tests__/gitignore-detector.test.ts +160 -152
  26. package/src/__tests__/gitignore-fixer.test.ts +285 -247
  27. package/src/__tests__/gitignore-prerun.test.ts +57 -57
  28. package/src/__tests__/gitignore-resolver.test.ts +173 -146
  29. package/src/__tests__/gitignore-service.test.ts +121 -119
  30. package/src/__tests__/go-module.test.ts +43 -0
  31. package/src/__tests__/install-command.test.ts +3 -3
  32. package/src/__tests__/install-plan.test.ts +13 -7
  33. package/src/__tests__/manifest.test.ts +16 -5
  34. package/src/__tests__/markdown-renderer.test.ts +0 -1
  35. package/src/__tests__/marketplace-badge.test.ts +111 -90
  36. package/src/__tests__/marketplace-version.test.ts +64 -61
  37. package/src/__tests__/plugin-manager-fallback.test.ts +249 -239
  38. package/src/__tests__/plugin-requires.test.ts +55 -1
  39. package/src/__tests__/plugin-setup.test.ts +111 -9
  40. package/src/__tests__/profile-command.test.ts +1 -7
  41. package/src/__tests__/profile-materializer.test.ts +31 -8
  42. package/src/__tests__/profile-sync.test.ts +23 -5
  43. package/src/__tests__/registry-version-resolution.test.ts +61 -47
  44. package/src/__tests__/resolve-executable.test.ts +1 -1
  45. package/src/__tests__/resolver.test.ts +38 -13
  46. package/src/__tests__/scope-action.test.ts +52 -50
  47. package/src/__tests__/scope-squares.test.tsx +4 -5
  48. package/src/__tests__/skill-install-files.test.ts +111 -103
  49. package/src/__tests__/symlink-manager.test.ts +27 -10
  50. package/src/__tests__/theme-env.test.ts +281 -0
  51. package/src/__tests__/theme-resolve.test.ts +380 -0
  52. package/src/__tests__/update-apply.test.ts +1 -3
  53. package/src/__tests__/useGitignoreModal.test.ts +69 -61
  54. package/src/__tests__/version-snapshot.test.ts +183 -165
  55. package/src/cli/ansi.ts +23 -15
  56. package/src/cli/doctor.ts +10 -9
  57. package/src/cli/install.ts +3 -3
  58. package/src/cli/profile.ts +5 -4
  59. package/src/cli/prompt.ts +0 -1
  60. package/src/cli/router.ts +69 -2
  61. package/src/cli/update-view.ts +11 -6
  62. package/src/cli/update.ts +9 -4
  63. package/src/cli/upgrade.ts +6 -4
  64. package/src/data/alias-flags.ts +260 -260
  65. package/src/data/gitignore-defaults.ts +20 -20
  66. package/src/data/gitignore-reasons.ts +86 -92
  67. package/src/data/gitignore-templates.ts +14 -14
  68. package/src/data/predefined-profiles.ts +1 -2
  69. package/src/data/skill-repos.ts +43 -35
  70. package/src/main.tsx +29 -6
  71. package/src/opentui.d.ts +1 -3
  72. package/src/prerunner/index.ts +7 -8
  73. package/src/services/alias-settings.ts +20 -23
  74. package/src/services/alias-shell-writer.ts +800 -803
  75. package/src/services/alias-store.ts +77 -77
  76. package/src/services/claude-settings.ts +27 -18
  77. package/src/services/content-drift.ts +14 -4
  78. package/src/services/conventions-manager.ts +692 -617
  79. package/src/services/doctor.ts +464 -403
  80. package/src/services/gitignore-detector.ts +113 -103
  81. package/src/services/gitignore-fixer.ts +204 -189
  82. package/src/services/gitignore-prerun.ts +26 -26
  83. package/src/services/gitignore-resolver.ts +162 -144
  84. package/src/services/gitignore-service.ts +120 -117
  85. package/src/services/install-plan.ts +4 -4
  86. package/src/services/local-marketplace.ts +7 -5
  87. package/src/services/manifest.ts +1 -1
  88. package/src/services/marketplace-catalog-git.ts +5 -2
  89. package/src/services/marketplace-fetcher.ts +241 -235
  90. package/src/services/plugin-manager.ts +17 -20
  91. package/src/services/plugin-mcp-config.ts +2 -2
  92. package/src/services/plugin-requires.ts +16 -8
  93. package/src/services/plugin-setup.ts +32 -15
  94. package/src/services/plugin-version-check.ts +4 -4
  95. package/src/services/profile-materializer.ts +9 -5
  96. package/src/services/profile-sync.ts +12 -4
  97. package/src/services/profiles.ts +3 -3
  98. package/src/services/resolver.ts +10 -6
  99. package/src/services/settings-manager.ts +82 -46
  100. package/src/services/skills-manager.ts +11 -9
  101. package/src/services/symlink-manager.ts +2 -5
  102. package/src/services/toolchain.ts +3 -3
  103. package/src/services/update-cache.ts +1 -1
  104. package/src/services/update-engine.ts +4 -4
  105. package/src/services/update-plan.ts +9 -11
  106. package/src/services/version-snapshot.ts +88 -88
  107. package/src/types/gitignore.ts +28 -28
  108. package/src/ui/App.tsx +36 -36
  109. package/src/ui/adapters/settingsAdapter.ts +2 -2
  110. package/src/ui/components/CategoryHeader.tsx +1 -1
  111. package/src/ui/components/FlagDetailEditor.tsx +0 -0
  112. package/src/ui/components/ScopeIndicator.tsx +3 -1
  113. package/src/ui/components/ScrollableList.tsx +2 -1
  114. package/src/ui/components/StyledText.tsx +1 -1
  115. package/src/ui/components/layout/FooterHints.tsx +5 -3
  116. package/src/ui/components/layout/Panel.tsx +2 -2
  117. package/src/ui/components/layout/ScreenLayout.tsx +5 -4
  118. package/src/ui/components/modals/InputModal.tsx +7 -1
  119. package/src/ui/components/modals/ModalContainer.tsx +3 -3
  120. package/src/ui/components/modals/SelectModal.tsx +1 -1
  121. package/src/ui/components/primitives/DetailSection.tsx +1 -1
  122. package/src/ui/components/primitives/KeyValueLine.tsx +1 -1
  123. package/src/ui/components/primitives/ScopeSquares.tsx +2 -2
  124. package/src/ui/hooks/index.ts +0 -1
  125. package/src/ui/hooks/useGitignoreModal.ts +1 -5
  126. package/src/ui/hooks/useMismatchModal.ts +4 -10
  127. package/src/ui/renderers/cliToolRenderers.tsx +2 -2
  128. package/src/ui/renderers/gitignoreRenderers.tsx +2 -2
  129. package/src/ui/renderers/mcpRenderers.tsx +1 -1
  130. package/src/ui/renderers/pluginRenderers.tsx +3 -9
  131. package/src/ui/renderers/profileRenderers.tsx +9 -8
  132. package/src/ui/renderers/settingsRenderers.tsx +4 -4
  133. package/src/ui/renderers/skillRenderers.tsx +14 -14
  134. package/src/ui/screens/AliasScreen.tsx +31 -34
  135. package/src/ui/screens/CliToolsScreen.tsx +14 -13
  136. package/src/ui/screens/EnvVarsScreen.tsx +8 -8
  137. package/src/ui/screens/GitignoreScreen.tsx +15 -14
  138. package/src/ui/screens/McpRegistryScreen.tsx +10 -7
  139. package/src/ui/screens/McpScreen.tsx +10 -10
  140. package/src/ui/screens/PluginsScreen.tsx +17 -21
  141. package/src/ui/screens/ProfilesScreen.tsx +25 -24
  142. package/src/ui/screens/SkillsScreen.tsx +16 -14
  143. package/src/ui/state/AppContext.tsx +4 -8
  144. package/src/ui/state/DimensionsContext.tsx +3 -2
  145. package/src/ui/theme-mode.ts +20 -38
  146. package/src/ui/theme-resolve.ts +148 -0
  147. package/src/utils/clipboard.ts +5 -5
  148. package/src/utils/command-utils.ts +2 -2
  149. package/src/utils/config-dir.ts +1 -3
  150. package/src/utils/file-locking.ts +88 -86
  151. package/src/utils/go-module.ts +36 -0
  152. package/src/ui/hooks/useAsyncData.ts +0 -127
  153. package/src/ui/state/AnimationContext.tsx +0 -76
@@ -1,15 +1,15 @@
1
- import { describe, it, expect, beforeEach } from "bun:test";
1
+ import { beforeEach, describe, expect, it } from "bun:test";
2
2
  import {
3
- activeCooldowns,
4
- cooldownFor,
5
- describeCooldown,
6
- formatWait,
7
- hostOf,
8
- recordRateLimit,
9
- recordSuccess,
10
- remainingFromHeaders,
11
- resetGitHubBudget,
12
- waitMs,
3
+ activeCooldowns,
4
+ cooldownFor,
5
+ describeCooldown,
6
+ formatWait,
7
+ hostOf,
8
+ recordRateLimit,
9
+ recordSuccess,
10
+ remainingFromHeaders,
11
+ resetGitHubBudget,
12
+ waitMs,
13
13
  } from "../services/github-budget";
14
14
 
15
15
  /**
@@ -26,175 +26,181 @@ import {
26
26
  const NOW = 1_786_980_000_000;
27
27
 
28
28
  beforeEach(() => {
29
- resetGitHubBudget();
29
+ resetGitHubBudget();
30
30
  });
31
31
 
32
32
  describe("hostOf", () => {
33
- it("recognises the two budgeted hosts and nothing else", () => {
34
- expect(hostOf("https://api.github.com/repos/x/y")).toBe("api.github.com");
35
- expect(hostOf("https://raw.githubusercontent.com/x/y/main/f.json")).toBe(
36
- "raw.githubusercontent.com",
37
- );
38
- expect(hostOf("https://github.com/x/y")).toBeNull();
39
- expect(hostOf("not a url")).toBeNull();
40
- });
33
+ it("recognises the two budgeted hosts and nothing else", () => {
34
+ expect(hostOf("https://api.github.com/repos/x/y")).toBe("api.github.com");
35
+ expect(hostOf("https://raw.githubusercontent.com/x/y/main/f.json")).toBe(
36
+ "raw.githubusercontent.com",
37
+ );
38
+ expect(hostOf("https://github.com/x/y")).toBeNull();
39
+ expect(hostOf("not a url")).toBeNull();
40
+ });
41
41
  });
42
42
 
43
43
  describe("recordRateLimit — exact timing when GitHub provides it", () => {
44
- it("honours retry-after in seconds", () => {
45
- const cooldown = recordRateLimit(
46
- "api.github.com",
47
- new Headers({ "retry-after": "120" }),
48
- NOW,
49
- );
50
- expect(cooldown.exact).toBe(true);
51
- expect(cooldown.until).toBe(NOW + 120_000);
52
- expect(describeCooldown(cooldown, NOW)).toBe(
53
- "GitHub rate limit resets in 2m",
54
- );
55
- });
56
-
57
- it("honours retry-after as an HTTP date", () => {
58
- const when = new Date(NOW + 300_000).toUTCString();
59
- const cooldown = recordRateLimit(
60
- "api.github.com",
61
- new Headers({ "retry-after": when }),
62
- NOW,
63
- );
64
- expect(cooldown.exact).toBe(true);
65
- // UTCString drops sub-second precision, so allow a second of slack.
66
- expect(Math.abs(cooldown.until - (NOW + 300_000))).toBeLessThanOrEqual(1000);
67
- });
68
-
69
- it("reads x-ratelimit-reset as epoch SECONDS, not milliseconds", () => {
70
- // Treating it as ms lands in 1970, so the cooldown expires instantly and the
71
- // UI reports "not rate limited" while GitHub is still refusing every call.
72
- const resetEpochSeconds = Math.floor(NOW / 1000) + 600;
73
- const cooldown = recordRateLimit(
74
- "api.github.com",
75
- new Headers({ "x-ratelimit-reset": String(resetEpochSeconds) }),
76
- NOW,
77
- );
78
- expect(cooldown.exact).toBe(true);
79
- expect(cooldown.until).toBe(resetEpochSeconds * 1000);
80
- expect(waitMs("api.github.com", NOW)).toBeGreaterThan(0);
81
- });
82
-
83
- it("caps an implausible server window so a bad header cannot wedge the UI", () => {
84
- const cooldown = recordRateLimit(
85
- "api.github.com",
86
- new Headers({ "retry-after": String(60 * 60 * 24 * 30) }),
87
- NOW,
88
- );
89
- expect(cooldown.until - NOW).toBeLessThanOrEqual(60 * 60_000);
90
- });
44
+ it("honours retry-after in seconds", () => {
45
+ const cooldown = recordRateLimit(
46
+ "api.github.com",
47
+ new Headers({ "retry-after": "120" }),
48
+ NOW,
49
+ );
50
+ expect(cooldown.exact).toBe(true);
51
+ expect(cooldown.until).toBe(NOW + 120_000);
52
+ expect(describeCooldown(cooldown, NOW)).toBe(
53
+ "GitHub rate limit resets in 2m",
54
+ );
55
+ });
56
+
57
+ it("honours retry-after as an HTTP date", () => {
58
+ const when = new Date(NOW + 300_000).toUTCString();
59
+ const cooldown = recordRateLimit(
60
+ "api.github.com",
61
+ new Headers({ "retry-after": when }),
62
+ NOW,
63
+ );
64
+ expect(cooldown.exact).toBe(true);
65
+ // UTCString drops sub-second precision, so allow a second of slack.
66
+ expect(Math.abs(cooldown.until - (NOW + 300_000))).toBeLessThanOrEqual(
67
+ 1000,
68
+ );
69
+ });
70
+
71
+ it("reads x-ratelimit-reset as epoch SECONDS, not milliseconds", () => {
72
+ // Treating it as ms lands in 1970, so the cooldown expires instantly and the
73
+ // UI reports "not rate limited" while GitHub is still refusing every call.
74
+ const resetEpochSeconds = Math.floor(NOW / 1000) + 600;
75
+ const cooldown = recordRateLimit(
76
+ "api.github.com",
77
+ new Headers({ "x-ratelimit-reset": String(resetEpochSeconds) }),
78
+ NOW,
79
+ );
80
+ expect(cooldown.exact).toBe(true);
81
+ expect(cooldown.until).toBe(resetEpochSeconds * 1000);
82
+ expect(waitMs("api.github.com", NOW)).toBeGreaterThan(0);
83
+ });
84
+
85
+ it("caps an implausible server window so a bad header cannot wedge the UI", () => {
86
+ const cooldown = recordRateLimit(
87
+ "api.github.com",
88
+ new Headers({ "retry-after": String(60 * 60 * 24 * 30) }),
89
+ NOW,
90
+ );
91
+ expect(cooldown.until - NOW).toBeLessThanOrEqual(60 * 60_000);
92
+ });
91
93
  });
92
94
 
93
95
  describe("recordRateLimit — backoff when GitHub provides nothing", () => {
94
- it("does not claim a reset time it was never told", () => {
95
- // This is the raw.githubusercontent.com case, measured: a bare 429.
96
- const cooldown = recordRateLimit(
97
- "raw.githubusercontent.com",
98
- new Headers(),
99
- NOW,
100
- );
101
- expect(cooldown.exact).toBe(false);
102
- expect(describeCooldown(cooldown, NOW)).toContain("next attempt in");
103
- expect(describeCooldown(cooldown, NOW)).not.toContain("resets in");
104
- });
105
-
106
- it("backs off further on each consecutive failure", () => {
107
- const first = recordRateLimit("raw.githubusercontent.com", undefined, NOW);
108
- const second = recordRateLimit("raw.githubusercontent.com", undefined, NOW);
109
- const third = recordRateLimit("raw.githubusercontent.com", undefined, NOW);
110
- expect(second.until).toBeGreaterThan(first.until);
111
- expect(third.until).toBeGreaterThan(second.until);
112
- expect(third.strikes).toBe(3);
113
- });
114
-
115
- it("ignores a retry-after that is garbage or already past", () => {
116
- const garbage = recordRateLimit(
117
- "raw.githubusercontent.com",
118
- new Headers({ "retry-after": "soon-ish" }),
119
- NOW,
120
- );
121
- expect(garbage.exact).toBe(false);
122
- expect(garbage.until).toBeGreaterThan(NOW);
123
- });
96
+ it("does not claim a reset time it was never told", () => {
97
+ // This is the raw.githubusercontent.com case, measured: a bare 429.
98
+ const cooldown = recordRateLimit(
99
+ "raw.githubusercontent.com",
100
+ new Headers(),
101
+ NOW,
102
+ );
103
+ expect(cooldown.exact).toBe(false);
104
+ expect(describeCooldown(cooldown, NOW)).toContain("next attempt in");
105
+ expect(describeCooldown(cooldown, NOW)).not.toContain("resets in");
106
+ });
107
+
108
+ it("backs off further on each consecutive failure", () => {
109
+ const first = recordRateLimit("raw.githubusercontent.com", undefined, NOW);
110
+ const second = recordRateLimit("raw.githubusercontent.com", undefined, NOW);
111
+ const third = recordRateLimit("raw.githubusercontent.com", undefined, NOW);
112
+ expect(second.until).toBeGreaterThan(first.until);
113
+ expect(third.until).toBeGreaterThan(second.until);
114
+ expect(third.strikes).toBe(3);
115
+ });
116
+
117
+ it("ignores a retry-after that is garbage or already past", () => {
118
+ const garbage = recordRateLimit(
119
+ "raw.githubusercontent.com",
120
+ new Headers({ "retry-after": "soon-ish" }),
121
+ NOW,
122
+ );
123
+ expect(garbage.exact).toBe(false);
124
+ expect(garbage.until).toBeGreaterThan(NOW);
125
+ });
124
126
  });
125
127
 
126
128
  describe("the gate", () => {
127
- it("blocks while cooling and clears when the window passes", () => {
128
- const cooldown = recordRateLimit(
129
- "raw.githubusercontent.com",
130
- undefined,
131
- NOW,
132
- );
133
- expect(cooldownFor("raw.githubusercontent.com", NOW)).not.toBeNull();
134
- expect(
135
- cooldownFor("raw.githubusercontent.com", cooldown.until + 1),
136
- ).toBeNull();
137
- expect(waitMs("raw.githubusercontent.com", cooldown.until + 1)).toBe(0);
138
- });
139
-
140
- it("keeps the two hosts on separate budgets", () => {
141
- // They are separate buckets upstream. Blocking raw because the API is
142
- // exhausted would stop work that would have succeeded.
143
- recordRateLimit("api.github.com", new Headers({ "retry-after": "60" }), NOW);
144
- expect(cooldownFor("api.github.com", NOW)).not.toBeNull();
145
- expect(cooldownFor("raw.githubusercontent.com", NOW)).toBeNull();
146
- });
147
-
148
- it("clears the cooldown and the strike count on success", () => {
149
- // Without resetting strikes, one bad afternoon pins the backoff at ten
150
- // minutes for the rest of the session even though calls are succeeding.
151
- recordRateLimit("raw.githubusercontent.com", undefined, NOW);
152
- recordRateLimit("raw.githubusercontent.com", undefined, NOW);
153
- recordSuccess("raw.githubusercontent.com");
154
- expect(cooldownFor("raw.githubusercontent.com", NOW)).toBeNull();
155
-
156
- const afterRecovery = recordRateLimit(
157
- "raw.githubusercontent.com",
158
- undefined,
159
- NOW,
160
- );
161
- expect(afterRecovery.strikes).toBe(1);
162
- });
163
-
164
- it("lists active cooldowns soonest first", () => {
165
- recordRateLimit(
166
- "api.github.com",
167
- new Headers({ "retry-after": "600" }),
168
- NOW,
169
- );
170
- recordRateLimit(
171
- "raw.githubusercontent.com",
172
- new Headers({ "retry-after": "30" }),
173
- NOW,
174
- );
175
- const active = activeCooldowns(NOW);
176
- expect(active.map((c) => c.host)).toEqual([
177
- "raw.githubusercontent.com",
178
- "api.github.com",
179
- ]);
180
- });
129
+ it("blocks while cooling and clears when the window passes", () => {
130
+ const cooldown = recordRateLimit(
131
+ "raw.githubusercontent.com",
132
+ undefined,
133
+ NOW,
134
+ );
135
+ expect(cooldownFor("raw.githubusercontent.com", NOW)).not.toBeNull();
136
+ expect(
137
+ cooldownFor("raw.githubusercontent.com", cooldown.until + 1),
138
+ ).toBeNull();
139
+ expect(waitMs("raw.githubusercontent.com", cooldown.until + 1)).toBe(0);
140
+ });
141
+
142
+ it("keeps the two hosts on separate budgets", () => {
143
+ // They are separate buckets upstream. Blocking raw because the API is
144
+ // exhausted would stop work that would have succeeded.
145
+ recordRateLimit(
146
+ "api.github.com",
147
+ new Headers({ "retry-after": "60" }),
148
+ NOW,
149
+ );
150
+ expect(cooldownFor("api.github.com", NOW)).not.toBeNull();
151
+ expect(cooldownFor("raw.githubusercontent.com", NOW)).toBeNull();
152
+ });
153
+
154
+ it("clears the cooldown and the strike count on success", () => {
155
+ // Without resetting strikes, one bad afternoon pins the backoff at ten
156
+ // minutes for the rest of the session even though calls are succeeding.
157
+ recordRateLimit("raw.githubusercontent.com", undefined, NOW);
158
+ recordRateLimit("raw.githubusercontent.com", undefined, NOW);
159
+ recordSuccess("raw.githubusercontent.com");
160
+ expect(cooldownFor("raw.githubusercontent.com", NOW)).toBeNull();
161
+
162
+ const afterRecovery = recordRateLimit(
163
+ "raw.githubusercontent.com",
164
+ undefined,
165
+ NOW,
166
+ );
167
+ expect(afterRecovery.strikes).toBe(1);
168
+ });
169
+
170
+ it("lists active cooldowns soonest first", () => {
171
+ recordRateLimit(
172
+ "api.github.com",
173
+ new Headers({ "retry-after": "600" }),
174
+ NOW,
175
+ );
176
+ recordRateLimit(
177
+ "raw.githubusercontent.com",
178
+ new Headers({ "retry-after": "30" }),
179
+ NOW,
180
+ );
181
+ const active = activeCooldowns(NOW);
182
+ expect(active.map((c) => c.host)).toEqual([
183
+ "raw.githubusercontent.com",
184
+ "api.github.com",
185
+ ]);
186
+ });
181
187
  });
182
188
 
183
189
  describe("remainingFromHeaders", () => {
184
- it("reads the budget so a caller can stop before tripping the limit", () => {
185
- expect(
186
- remainingFromHeaders(new Headers({ "x-ratelimit-remaining": "51" })),
187
- ).toBe(51);
188
- expect(remainingFromHeaders(new Headers())).toBeNull();
189
- });
190
+ it("reads the budget so a caller can stop before tripping the limit", () => {
191
+ expect(
192
+ remainingFromHeaders(new Headers({ "x-ratelimit-remaining": "51" })),
193
+ ).toBe(51);
194
+ expect(remainingFromHeaders(new Headers())).toBeNull();
195
+ });
190
196
  });
191
197
 
192
198
  describe("formatWait", () => {
193
- it("reads naturally at every scale", () => {
194
- expect(formatWait(0)).toBe("0s");
195
- expect(formatWait(45_000)).toBe("45s");
196
- expect(formatWait(90_000)).toBe("2m");
197
- expect(formatWait(3_600_000)).toBe("1h 0m");
198
- expect(formatWait(4_320_000)).toBe("1h 12m");
199
- });
199
+ it("reads naturally at every scale", () => {
200
+ expect(formatWait(0)).toBe("0s");
201
+ expect(formatWait(45_000)).toBe("45s");
202
+ expect(formatWait(90_000)).toBe("2m");
203
+ expect(formatWait(3_600_000)).toBe("1h 0m");
204
+ expect(formatWait(4_320_000)).toBe("1h 12m");
205
+ });
200
206
  });