claudeup 6.2.1 → 6.3.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.
- package/README.md +22 -0
- package/bin/claudeup.js +6 -1
- package/package.json +6 -6
- package/src/__tests__/alias-adopt.test.ts +331 -329
- package/src/__tests__/alias-parser.test.ts +384 -376
- package/src/__tests__/alias-shell-writer.test.ts +638 -625
- package/src/__tests__/alias-store.test.ts +143 -143
- package/src/__tests__/catalog-cache-store.test.ts +265 -265
- package/src/__tests__/catalog-notice.test.ts +123 -123
- package/src/__tests__/cli-apply-seams.test.ts +12 -8
- package/src/__tests__/cli-router.test.ts +132 -3
- package/src/__tests__/cli-update-view.test.ts +27 -8
- package/src/__tests__/community-staleness.test.ts +0 -2
- package/src/__tests__/component-badge.test.ts +4 -4
- package/src/__tests__/content-drift.test.ts +5 -1
- package/src/__tests__/conventions-integration.test.ts +774 -689
- package/src/__tests__/conventions-manager.test.ts +1216 -1152
- package/src/__tests__/doctor-bins.test.ts +14 -6
- package/src/__tests__/doctor.test.ts +438 -425
- package/src/__tests__/dual-write-prevention.test.ts +277 -282
- package/src/__tests__/enabled-not-installed.test.ts +71 -63
- package/src/__tests__/file-locking.test.ts +196 -196
- package/src/__tests__/gap-fill-versions.test.ts +318 -311
- package/src/__tests__/github-budget.test.ts +170 -164
- package/src/__tests__/gitignore-detector.test.ts +160 -152
- package/src/__tests__/gitignore-fixer.test.ts +285 -247
- package/src/__tests__/gitignore-prerun.test.ts +57 -57
- package/src/__tests__/gitignore-resolver.test.ts +173 -146
- package/src/__tests__/gitignore-service.test.ts +121 -119
- package/src/__tests__/install-command.test.ts +3 -3
- package/src/__tests__/install-plan.test.ts +13 -7
- package/src/__tests__/manifest.test.ts +16 -5
- package/src/__tests__/markdown-renderer.test.ts +0 -1
- package/src/__tests__/marketplace-badge.test.ts +111 -90
- package/src/__tests__/marketplace-version.test.ts +64 -61
- package/src/__tests__/plugin-manager-fallback.test.ts +249 -239
- package/src/__tests__/plugin-requires.test.ts +18 -4
- package/src/__tests__/plugin-setup.test.ts +12 -12
- package/src/__tests__/profile-command.test.ts +1 -7
- package/src/__tests__/profile-materializer.test.ts +31 -8
- package/src/__tests__/profile-sync.test.ts +23 -5
- package/src/__tests__/registry-version-resolution.test.ts +61 -47
- package/src/__tests__/resolve-executable.test.ts +1 -1
- package/src/__tests__/resolver.test.ts +40 -15
- package/src/__tests__/scope-action.test.ts +52 -50
- package/src/__tests__/scope-squares.test.tsx +4 -5
- package/src/__tests__/skill-install-files.test.ts +111 -103
- package/src/__tests__/symlink-manager.test.ts +27 -10
- package/src/__tests__/theme-env.test.ts +281 -0
- package/src/__tests__/theme-resolve.test.ts +380 -0
- package/src/__tests__/update-apply.test.ts +1 -3
- package/src/__tests__/useGitignoreModal.test.ts +69 -61
- package/src/__tests__/version-snapshot.test.ts +181 -165
- package/src/cli/ansi.ts +23 -15
- package/src/cli/doctor.ts +10 -9
- package/src/cli/install.ts +3 -3
- package/src/cli/profile.ts +5 -4
- package/src/cli/prompt.ts +0 -1
- package/src/cli/router.ts +69 -2
- package/src/cli/update-view.ts +11 -6
- package/src/cli/update.ts +9 -4
- package/src/cli/upgrade.ts +6 -4
- package/src/data/alias-flags.ts +260 -260
- package/src/data/gitignore-defaults.ts +20 -20
- package/src/data/gitignore-reasons.ts +86 -92
- package/src/data/gitignore-templates.ts +14 -14
- package/src/data/predefined-profiles.ts +8 -9
- package/src/data/skill-repos.ts +43 -35
- package/src/main.tsx +29 -6
- package/src/opentui.d.ts +1 -3
- package/src/prerunner/index.ts +7 -8
- package/src/services/alias-settings.ts +20 -23
- package/src/services/alias-shell-writer.ts +800 -803
- package/src/services/alias-store.ts +77 -77
- package/src/services/claude-settings.ts +27 -18
- package/src/services/content-drift.ts +13 -3
- package/src/services/conventions-manager.ts +692 -617
- package/src/services/doctor.ts +464 -403
- package/src/services/gitignore-detector.ts +113 -103
- package/src/services/gitignore-fixer.ts +204 -189
- package/src/services/gitignore-prerun.ts +26 -26
- package/src/services/gitignore-resolver.ts +162 -144
- package/src/services/gitignore-service.ts +120 -117
- package/src/services/install-plan.ts +4 -4
- package/src/services/manifest.ts +1 -1
- package/src/services/marketplace-catalog-git.ts +5 -2
- package/src/services/marketplace-fetcher.ts +241 -235
- package/src/services/plugin-manager.ts +17 -20
- package/src/services/plugin-mcp-config.ts +2 -2
- package/src/services/plugin-requires.ts +9 -6
- package/src/services/plugin-setup.ts +7 -9
- package/src/services/plugin-version-check.ts +4 -4
- package/src/services/profile-materializer.ts +9 -5
- package/src/services/profile-sync.ts +12 -4
- package/src/services/profiles.ts +3 -3
- package/src/services/resolver.ts +10 -6
- package/src/services/settings-manager.ts +82 -46
- package/src/services/skills-manager.ts +11 -9
- package/src/services/symlink-manager.ts +2 -5
- package/src/services/toolchain.ts +3 -3
- package/src/services/update-cache.ts +1 -1
- package/src/services/update-engine.ts +4 -4
- package/src/services/update-plan.ts +9 -11
- package/src/services/version-snapshot.ts +88 -88
- package/src/types/gitignore.ts +28 -28
- package/src/ui/App.tsx +36 -36
- package/src/ui/adapters/settingsAdapter.ts +2 -2
- package/src/ui/components/CategoryHeader.tsx +1 -1
- package/src/ui/components/FlagDetailEditor.tsx +0 -0
- package/src/ui/components/ScopeIndicator.tsx +3 -1
- package/src/ui/components/ScrollableList.tsx +2 -1
- package/src/ui/components/StyledText.tsx +1 -1
- package/src/ui/components/layout/FooterHints.tsx +5 -3
- package/src/ui/components/layout/Panel.tsx +2 -2
- package/src/ui/components/layout/ScreenLayout.tsx +5 -4
- package/src/ui/components/modals/InputModal.tsx +7 -1
- package/src/ui/components/modals/ModalContainer.tsx +3 -3
- package/src/ui/components/modals/SelectModal.tsx +1 -1
- package/src/ui/components/primitives/DetailSection.tsx +1 -1
- package/src/ui/components/primitives/KeyValueLine.tsx +1 -1
- package/src/ui/components/primitives/ScopeSquares.tsx +2 -2
- package/src/ui/hooks/index.ts +0 -1
- package/src/ui/hooks/useGitignoreModal.ts +1 -5
- package/src/ui/hooks/useMismatchModal.ts +4 -10
- package/src/ui/renderers/cliToolRenderers.tsx +2 -2
- package/src/ui/renderers/gitignoreRenderers.tsx +2 -2
- package/src/ui/renderers/mcpRenderers.tsx +1 -1
- package/src/ui/renderers/pluginRenderers.tsx +3 -9
- package/src/ui/renderers/profileRenderers.tsx +9 -8
- package/src/ui/renderers/settingsRenderers.tsx +4 -4
- package/src/ui/renderers/skillRenderers.tsx +14 -14
- package/src/ui/screens/AliasScreen.tsx +31 -34
- package/src/ui/screens/CliToolsScreen.tsx +14 -13
- package/src/ui/screens/EnvVarsScreen.tsx +8 -8
- package/src/ui/screens/GitignoreScreen.tsx +15 -14
- package/src/ui/screens/McpRegistryScreen.tsx +10 -7
- package/src/ui/screens/McpScreen.tsx +10 -10
- package/src/ui/screens/PluginsScreen.tsx +17 -21
- package/src/ui/screens/ProfilesScreen.tsx +25 -24
- package/src/ui/screens/SkillsScreen.tsx +16 -14
- package/src/ui/state/AppContext.tsx +4 -8
- package/src/ui/state/DimensionsContext.tsx +3 -2
- package/src/ui/theme-mode.ts +20 -38
- package/src/ui/theme-resolve.ts +148 -0
- package/src/utils/clipboard.ts +5 -5
- package/src/utils/command-utils.ts +2 -2
- package/src/utils/config-dir.ts +1 -3
- package/src/utils/file-locking.ts +88 -86
- package/src/ui/hooks/useAsyncData.ts +0 -127
- package/src/ui/state/AnimationContext.tsx +0 -76
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { ThemeMode } from "@opentui/core";
|
|
3
|
+
import {
|
|
4
|
+
type ResolveThemeInput,
|
|
5
|
+
parseColorFgBg,
|
|
6
|
+
parseThemeWord,
|
|
7
|
+
resolveTheme,
|
|
8
|
+
} from "../ui/theme-resolve.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The resolver is a six-step, first-answer-wins list. These tests pin every
|
|
12
|
+
* step, every adjacent precedence pair, and the "no opinion" rule: a word that
|
|
13
|
+
* is not exactly `light` or `dark` says nothing and the next step runs.
|
|
14
|
+
*
|
|
15
|
+
* The probe is a hand-rolled recorder rather than `spyOn` — `spyOn` calls
|
|
16
|
+
* through to the original by default, and the property under test is whether
|
|
17
|
+
* the probe was reached at all.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
function probeReturning(value: ThemeMode | null) {
|
|
21
|
+
let calls = 0;
|
|
22
|
+
const probe = async () => {
|
|
23
|
+
calls += 1;
|
|
24
|
+
return value;
|
|
25
|
+
};
|
|
26
|
+
return { probe, calls: () => calls };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function input(overrides: Partial<ResolveThemeInput> = {}): ResolveThemeInput {
|
|
30
|
+
return {
|
|
31
|
+
env: {},
|
|
32
|
+
isInteractive: true,
|
|
33
|
+
probe: async () => null,
|
|
34
|
+
...overrides,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe("each step answers on its own", () => {
|
|
39
|
+
test("T-step-1: the flag wins and the probe is never called", async () => {
|
|
40
|
+
const p = probeReturning("dark");
|
|
41
|
+
|
|
42
|
+
const r = await resolveTheme(input({ flag: "light", probe: p.probe }));
|
|
43
|
+
|
|
44
|
+
expect(r).toEqual({ mode: "light", source: "flag" });
|
|
45
|
+
expect(p.calls()).toBe(0);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("T-step-2: CLAUDEUP_THEME answers without the probe", async () => {
|
|
49
|
+
const p = probeReturning("light");
|
|
50
|
+
|
|
51
|
+
const r = await resolveTheme(
|
|
52
|
+
input({ env: { CLAUDEUP_THEME: "dark" }, probe: p.probe }),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
expect(r).toEqual({ mode: "dark", source: "variable" });
|
|
56
|
+
expect(p.calls()).toBe(0);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("T-step-3: TERM_THEME answers and the probe is skipped entirely", async () => {
|
|
60
|
+
const p = probeReturning("dark");
|
|
61
|
+
|
|
62
|
+
const r = await resolveTheme(
|
|
63
|
+
input({ env: { TERM_THEME: "light" }, probe: p.probe }),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
expect(r).toEqual({ mode: "light", source: "TERM_THEME" });
|
|
67
|
+
expect(p.calls()).toBe(0);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("T-step-4: an interactive tty probes exactly once", async () => {
|
|
71
|
+
const p = probeReturning("light");
|
|
72
|
+
|
|
73
|
+
const r = await resolveTheme(input({ probe: p.probe }));
|
|
74
|
+
|
|
75
|
+
expect(r).toEqual({ mode: "light", source: "osc11" });
|
|
76
|
+
expect(p.calls()).toBe(1);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("T-step-5: an unanswered probe falls to COLORFGBG", async () => {
|
|
80
|
+
const r = await resolveTheme(
|
|
81
|
+
input({ env: { COLORFGBG: "15;7" }, probe: async () => null }),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
expect(r).toEqual({ mode: "light", source: "COLORFGBG" });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("T-step-6: nothing answers → dark by decision, no throw", async () => {
|
|
88
|
+
const r = await resolveTheme(input());
|
|
89
|
+
|
|
90
|
+
expect(r).toEqual({ mode: "dark", source: "default" });
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("precedence between adjacent steps", () => {
|
|
95
|
+
test("T-prec-flag>var: the flag beats CLAUDEUP_THEME", async () => {
|
|
96
|
+
const r = await resolveTheme(
|
|
97
|
+
input({ flag: "light", env: { CLAUDEUP_THEME: "dark" } }),
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
expect(r.mode).toBe("light");
|
|
101
|
+
expect(r.source).toBe("flag");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("T-prec-var>TERM: CLAUDEUP_THEME beats TERM_THEME", async () => {
|
|
105
|
+
const r = await resolveTheme(
|
|
106
|
+
input({ env: { CLAUDEUP_THEME: "light", TERM_THEME: "dark" } }),
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
expect(r.mode).toBe("light");
|
|
110
|
+
expect(r.source).toBe("variable");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("T-prec-TERM>probe: TERM_THEME beats a probe that would disagree", async () => {
|
|
114
|
+
const p = probeReturning("light");
|
|
115
|
+
|
|
116
|
+
const r = await resolveTheme(
|
|
117
|
+
input({ env: { TERM_THEME: "dark" }, probe: p.probe }),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
expect(r.mode).toBe("dark");
|
|
121
|
+
expect(p.calls()).toBe(0);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("T-prec-probe>CFB: the probe beats COLORFGBG", async () => {
|
|
125
|
+
const r = await resolveTheme(
|
|
126
|
+
input({ env: { COLORFGBG: "15;0" }, probe: async () => "light" }),
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
expect(r).toEqual({ mode: "light", source: "osc11" });
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("T-prec-CFB>default: a dark COLORFGBG is attributed to COLORFGBG, not default", async () => {
|
|
133
|
+
const r = await resolveTheme(input({ env: { COLORFGBG: "0;0" } }));
|
|
134
|
+
|
|
135
|
+
expect(r).toEqual({ mode: "dark", source: "COLORFGBG" });
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe("precedence across non-adjacent steps", () => {
|
|
140
|
+
test("T-prec-flag>all: the flag beats every later step, and none of them is consulted", async () => {
|
|
141
|
+
const p = probeReturning("light");
|
|
142
|
+
|
|
143
|
+
const r = await resolveTheme(
|
|
144
|
+
input({
|
|
145
|
+
flag: "dark",
|
|
146
|
+
env: {
|
|
147
|
+
CLAUDEUP_THEME: "light",
|
|
148
|
+
TERM_THEME: "light",
|
|
149
|
+
COLORFGBG: "0;15",
|
|
150
|
+
},
|
|
151
|
+
probe: p.probe,
|
|
152
|
+
}),
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
expect(r).toEqual({ mode: "dark", source: "flag" });
|
|
156
|
+
expect(p.calls()).toBe(0);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("T-prec-var>probe: CLAUDEUP_THEME beats a probe that would disagree, and skips it", async () => {
|
|
160
|
+
const p = probeReturning("light");
|
|
161
|
+
|
|
162
|
+
const r = await resolveTheme(
|
|
163
|
+
input({
|
|
164
|
+
env: { CLAUDEUP_THEME: "dark", COLORFGBG: "0;15" },
|
|
165
|
+
probe: p.probe,
|
|
166
|
+
}),
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
expect(r).toEqual({ mode: "dark", source: "variable" });
|
|
170
|
+
expect(p.calls()).toBe(0);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("T-prec-TERM>CFB: TERM_THEME beats COLORFGBG when there is no tty to probe", async () => {
|
|
174
|
+
const r = await resolveTheme(
|
|
175
|
+
input({
|
|
176
|
+
env: { TERM_THEME: "light", COLORFGBG: "15;0" },
|
|
177
|
+
isInteractive: false,
|
|
178
|
+
}),
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
expect(r).toEqual({ mode: "light", source: "TERM_THEME" });
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("T-step-4b: a probe that answers dark is attributed to osc11, not default", async () => {
|
|
185
|
+
const r = await resolveTheme(
|
|
186
|
+
input({ env: { COLORFGBG: "0;15" }, probe: async () => "dark" }),
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
expect(r).toEqual({ mode: "dark", source: "osc11" });
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe("no opinion falls through", () => {
|
|
194
|
+
test("T-noop-auto: TERM_THEME=auto reaches the probe", async () => {
|
|
195
|
+
const p = probeReturning("light");
|
|
196
|
+
|
|
197
|
+
const r = await resolveTheme(
|
|
198
|
+
input({ env: { TERM_THEME: "auto" }, probe: p.probe }),
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
expect(r).toEqual({ mode: "light", source: "osc11" });
|
|
202
|
+
expect(p.calls()).toBe(1);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("T-noop-case: Light and DARK are not the exact words and are ignored", async () => {
|
|
206
|
+
const r = await resolveTheme(
|
|
207
|
+
input({
|
|
208
|
+
env: { TERM_THEME: "Light", CLAUDEUP_THEME: "DARK" },
|
|
209
|
+
isInteractive: false,
|
|
210
|
+
}),
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
expect(r).toEqual({ mode: "dark", source: "default" });
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("T-noop-empty: an empty TERM_THEME falls through", async () => {
|
|
217
|
+
const r = await resolveTheme(
|
|
218
|
+
input({ env: { TERM_THEME: "", COLORFGBG: "0;15" } }),
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
expect(r).toEqual({ mode: "light", source: "COLORFGBG" });
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("T-noop-auto-notty: TERM_THEME=auto never means dark — without a tty it reaches COLORFGBG", async () => {
|
|
225
|
+
const p = probeReturning("dark");
|
|
226
|
+
|
|
227
|
+
const r = await resolveTheme(
|
|
228
|
+
input({
|
|
229
|
+
env: { TERM_THEME: "auto", COLORFGBG: "0;15" },
|
|
230
|
+
isInteractive: false,
|
|
231
|
+
probe: p.probe,
|
|
232
|
+
}),
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
expect(r).toEqual({ mode: "light", source: "COLORFGBG" });
|
|
236
|
+
expect(p.calls()).toBe(0);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("T-noop-auto-default: TERM_THEME=auto with nothing else is the DEFAULT dark, not a TERM_THEME answer", async () => {
|
|
240
|
+
const r = await resolveTheme(input({ env: { TERM_THEME: "auto" } }));
|
|
241
|
+
|
|
242
|
+
expect(r).toEqual({ mode: "dark", source: "default" });
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test.each(["auto", "purple", "", "Dark"])(
|
|
246
|
+
"T-noop-var: CLAUDEUP_THEME=%p is no opinion and TERM_THEME answers instead",
|
|
247
|
+
async (word) => {
|
|
248
|
+
const p = probeReturning("dark");
|
|
249
|
+
|
|
250
|
+
const r = await resolveTheme(
|
|
251
|
+
input({
|
|
252
|
+
env: { CLAUDEUP_THEME: word, TERM_THEME: "light" },
|
|
253
|
+
probe: p.probe,
|
|
254
|
+
}),
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
expect(r).toEqual({ mode: "light", source: "TERM_THEME" });
|
|
258
|
+
expect(p.calls()).toBe(0);
|
|
259
|
+
},
|
|
260
|
+
);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe("R8: resolving is silent", () => {
|
|
264
|
+
// The dark fallback is a decision, not a failure: no step, including step 6
|
|
265
|
+
// and a probe that fails, may print a "could not detect" line.
|
|
266
|
+
test("T-quiet: neither the default path nor a failing probe writes to the console", async () => {
|
|
267
|
+
const written: string[] = [];
|
|
268
|
+
const record = (...a: unknown[]) => {
|
|
269
|
+
written.push(a.join(" "));
|
|
270
|
+
};
|
|
271
|
+
const original = {
|
|
272
|
+
log: console.log,
|
|
273
|
+
warn: console.warn,
|
|
274
|
+
error: console.error,
|
|
275
|
+
};
|
|
276
|
+
console.log = record;
|
|
277
|
+
console.warn = record;
|
|
278
|
+
console.error = record;
|
|
279
|
+
try {
|
|
280
|
+
await resolveTheme(input());
|
|
281
|
+
await resolveTheme(
|
|
282
|
+
input({
|
|
283
|
+
probe: async () => {
|
|
284
|
+
throw new Error("no reply");
|
|
285
|
+
},
|
|
286
|
+
}),
|
|
287
|
+
);
|
|
288
|
+
await resolveTheme(
|
|
289
|
+
input({ env: { TERM_THEME: "auto" }, isInteractive: false }),
|
|
290
|
+
);
|
|
291
|
+
} finally {
|
|
292
|
+
console.log = original.log;
|
|
293
|
+
console.warn = original.warn;
|
|
294
|
+
console.error = original.error;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
expect(written).toEqual([]);
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
describe("the probe is guarded", () => {
|
|
302
|
+
test("T-tty: a non-interactive process never probes", async () => {
|
|
303
|
+
const p = probeReturning("light");
|
|
304
|
+
|
|
305
|
+
const fromEnv = await resolveTheme(
|
|
306
|
+
input({
|
|
307
|
+
isInteractive: false,
|
|
308
|
+
env: { COLORFGBG: "0;15" },
|
|
309
|
+
probe: p.probe,
|
|
310
|
+
}),
|
|
311
|
+
);
|
|
312
|
+
const fromDefault = await resolveTheme(
|
|
313
|
+
input({ isInteractive: false, probe: p.probe }),
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
expect(fromEnv).toEqual({ mode: "light", source: "COLORFGBG" });
|
|
317
|
+
expect(fromDefault).toEqual({ mode: "dark", source: "default" });
|
|
318
|
+
expect(p.calls()).toBe(0);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test("T-probe-throws: a rejecting probe is treated as null", async () => {
|
|
322
|
+
const r = await resolveTheme(
|
|
323
|
+
input({
|
|
324
|
+
env: { COLORFGBG: "0;7" },
|
|
325
|
+
probe: async () => {
|
|
326
|
+
throw new Error("terminal hung up");
|
|
327
|
+
},
|
|
328
|
+
}),
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
expect(r).toEqual({ mode: "light", source: "COLORFGBG" });
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
describe("T-cfb-parse: parseColorFgBg reads the LAST field as the background", () => {
|
|
336
|
+
test.each<[string | undefined, ThemeMode | null]>([
|
|
337
|
+
["15;0", "dark"],
|
|
338
|
+
["0;15", "light"],
|
|
339
|
+
["0;7", "light"],
|
|
340
|
+
["12;3;7", "light"],
|
|
341
|
+
["0;default;15", "light"],
|
|
342
|
+
["default;default", null],
|
|
343
|
+
["7", null],
|
|
344
|
+
["15", null],
|
|
345
|
+
["x;16", null],
|
|
346
|
+
["0;255", null],
|
|
347
|
+
// Zero-padded fields are outside the statusline's literal `case` set, so
|
|
348
|
+
// they are no opinion here too — Number("07") would have said light.
|
|
349
|
+
["0;07", null],
|
|
350
|
+
["0;015", null],
|
|
351
|
+
["0;00", null],
|
|
352
|
+
["0;", null],
|
|
353
|
+
["", null],
|
|
354
|
+
[undefined, null],
|
|
355
|
+
])("%p → %p", (value, expected) => {
|
|
356
|
+
expect(parseColorFgBg(value)).toBe(expected);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test("every background 0-14 except 7 is dark", () => {
|
|
360
|
+
for (let bg = 0; bg <= 14; bg += 1) {
|
|
361
|
+
expect(parseColorFgBg(`0;${bg}`)).toBe(bg === 7 ? "light" : "dark");
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
describe("T-word-parse: parseThemeWord is exact, lowercase, untrimmed", () => {
|
|
367
|
+
test.each<[string | undefined, ThemeMode | null]>([
|
|
368
|
+
["light", "light"],
|
|
369
|
+
["dark", "dark"],
|
|
370
|
+
["auto", null],
|
|
371
|
+
["Light", null],
|
|
372
|
+
["DARK", null],
|
|
373
|
+
[" dark", null],
|
|
374
|
+
["dark ", null],
|
|
375
|
+
["", null],
|
|
376
|
+
[undefined, null],
|
|
377
|
+
])("%p → %p", (value, expected) => {
|
|
378
|
+
expect(parseThemeWord(value)).toBe(expected);
|
|
379
|
+
});
|
|
380
|
+
});
|
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
10
|
+
import type { PluginScope } from "../services/claude-cli.js";
|
|
10
11
|
import {
|
|
11
12
|
type PluginApplyDeps,
|
|
12
13
|
applyPlugins,
|
|
13
14
|
installPluginInScope,
|
|
14
15
|
updatePluginInScope,
|
|
15
16
|
} from "../services/update-engine.js";
|
|
16
|
-
import type { PluginScope } from "../services/claude-cli.js";
|
|
17
17
|
import type { PluginUpdateItem } from "../services/update-plan.js";
|
|
18
18
|
|
|
19
19
|
const PROJECT = "/tmp/project";
|
|
@@ -67,9 +67,7 @@ function item(over: Partial<PluginUpdateItem>): PluginUpdateItem {
|
|
|
67
67
|
let logs: string[];
|
|
68
68
|
beforeEach(() => {
|
|
69
69
|
logs = [];
|
|
70
|
-
// biome-ignore lint/suspicious/noExplicitAny: test double
|
|
71
70
|
console.log = ((...a: unknown[]) => logs.push(a.join(" "))) as any;
|
|
72
|
-
// biome-ignore lint/suspicious/noExplicitAny: test double
|
|
73
71
|
console.warn = ((...a: unknown[]) => logs.push(a.join(" "))) as any;
|
|
74
72
|
});
|
|
75
73
|
|
|
@@ -1,71 +1,79 @@
|
|
|
1
|
-
import { describe,
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import { buildGitignoreModal } from "../ui/hooks/useGitignoreModal";
|
|
3
3
|
|
|
4
4
|
describe("buildGitignoreModal", () => {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
5
|
+
it("builds a select modal with three options", () => {
|
|
6
|
+
let dismissed = false;
|
|
7
|
+
let fixed = false;
|
|
8
|
+
let opened = false;
|
|
9
|
+
const modal = buildGitignoreModal({
|
|
10
|
+
violationCount: 5,
|
|
11
|
+
safeCount: 3,
|
|
12
|
+
onFixSafe: () => {
|
|
13
|
+
fixed = true;
|
|
14
|
+
},
|
|
15
|
+
onOpenTab: () => {
|
|
16
|
+
opened = true;
|
|
17
|
+
},
|
|
18
|
+
onDismiss: () => {
|
|
19
|
+
dismissed = true;
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
expect(modal.type).toBe("select");
|
|
23
|
+
if (modal.type !== "select") throw new Error("type guard");
|
|
24
|
+
expect(modal.options.length).toBe(3);
|
|
25
|
+
expect(modal.options[0].value).toBe("fix-safe");
|
|
26
|
+
expect(modal.options[1].value).toBe("open-tab");
|
|
27
|
+
expect(modal.options[2].value).toBe("dismiss");
|
|
28
|
+
expect(modal.options[0].label).toContain("3");
|
|
23
29
|
|
|
24
|
-
|
|
25
|
-
|
|
30
|
+
modal.onSelect("fix-safe");
|
|
31
|
+
expect(fixed).toBe(true);
|
|
26
32
|
|
|
27
|
-
|
|
28
|
-
|
|
33
|
+
modal.onSelect("open-tab");
|
|
34
|
+
expect(opened).toBe(true);
|
|
29
35
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
modal.onSelect("dismiss");
|
|
37
|
+
expect(dismissed).toBe(true);
|
|
38
|
+
});
|
|
33
39
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
it("shows no missing entries label when safeCount is 0", () => {
|
|
41
|
+
const modal = buildGitignoreModal({
|
|
42
|
+
violationCount: 2,
|
|
43
|
+
safeCount: 0,
|
|
44
|
+
onFixSafe: () => {},
|
|
45
|
+
onOpenTab: () => {},
|
|
46
|
+
onDismiss: () => {},
|
|
47
|
+
});
|
|
48
|
+
if (modal.type !== "select") throw new Error("type guard");
|
|
49
|
+
expect(modal.options[0].label).toContain("no missing .gitignore");
|
|
50
|
+
});
|
|
45
51
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
it("includes violation count in the message", () => {
|
|
53
|
+
const modal = buildGitignoreModal({
|
|
54
|
+
violationCount: 7,
|
|
55
|
+
safeCount: 2,
|
|
56
|
+
onFixSafe: () => {},
|
|
57
|
+
onOpenTab: () => {},
|
|
58
|
+
onDismiss: () => {},
|
|
59
|
+
});
|
|
60
|
+
if (modal.type !== "select") throw new Error("type guard");
|
|
61
|
+
expect(modal.message).toContain("7");
|
|
62
|
+
});
|
|
57
63
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
64
|
+
it("calls onCancel as dismiss", () => {
|
|
65
|
+
let dismissed = false;
|
|
66
|
+
const modal = buildGitignoreModal({
|
|
67
|
+
violationCount: 1,
|
|
68
|
+
safeCount: 0,
|
|
69
|
+
onFixSafe: () => {},
|
|
70
|
+
onOpenTab: () => {},
|
|
71
|
+
onDismiss: () => {
|
|
72
|
+
dismissed = true;
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
if (modal.type !== "select") throw new Error("type guard");
|
|
76
|
+
modal.onCancel();
|
|
77
|
+
expect(dismissed).toBe(true);
|
|
78
|
+
});
|
|
71
79
|
});
|