@selesai/code 0.10.1 → 0.11.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 +23 -0
- package/dist/config.d.ts +5 -3
- package/dist/config.js +6 -4
- package/dist/core/skills.d.ts +2 -0
- package/dist/core/skills.js +1 -0
- package/dist/defaults/settings.json +0 -3
- package/dist/extensions/auto-session-name.test.ts +157 -0
- package/dist/extensions/auto-session-name.ts +96 -0
- package/dist/extensions/cost-reconcile.test.ts +76 -0
- package/dist/extensions/cost-reconcile.ts +278 -0
- package/dist/extensions/enable-readonly-tools.test.ts +11 -0
- package/dist/extensions/enable-readonly-tools.ts +7 -3
- package/dist/extensions/package.json +3 -0
- package/dist/extensions/pi-tool-display/CHANGELOG.md +329 -0
- package/dist/extensions/pi-tool-display/LICENSE +21 -0
- package/dist/extensions/pi-tool-display/README.md +417 -0
- package/dist/extensions/pi-tool-display/config/config.example.json +40 -0
- package/dist/extensions/pi-tool-display/index.ts +3 -0
- package/dist/extensions/pi-tool-display/package-lock.json +2555 -0
- package/dist/extensions/pi-tool-display/package.json +86 -0
- package/dist/extensions/pi-tool-display/src/agent-dir.ts +35 -0
- package/dist/extensions/pi-tool-display/src/ansi-utils.ts +116 -0
- package/dist/extensions/pi-tool-display/src/bash-display.ts +213 -0
- package/dist/extensions/pi-tool-display/src/capabilities.ts +88 -0
- package/dist/extensions/pi-tool-display/src/config-modal.ts +541 -0
- package/dist/extensions/pi-tool-display/src/config-store.ts +304 -0
- package/dist/extensions/pi-tool-display/src/debug-logger.ts +150 -0
- package/dist/extensions/pi-tool-display/src/diff-presentation.ts +72 -0
- package/dist/extensions/pi-tool-display/src/diff-renderer.ts +2517 -0
- package/dist/extensions/pi-tool-display/src/disposable.ts +34 -0
- package/dist/extensions/pi-tool-display/src/extension-lifecycle.ts +14 -0
- package/dist/extensions/pi-tool-display/src/index.ts +109 -0
- package/dist/extensions/pi-tool-display/src/line-width-safety.ts +93 -0
- package/dist/extensions/pi-tool-display/src/modal-icons.ts +57 -0
- package/dist/extensions/pi-tool-display/src/pending-diff-preview.ts +370 -0
- package/dist/extensions/pi-tool-display/src/presets.ts +117 -0
- package/dist/extensions/pi-tool-display/src/render-utils.ts +186 -0
- package/dist/extensions/pi-tool-display/src/settings-inspector-modal.ts +475 -0
- package/dist/extensions/pi-tool-display/src/thinking-label.ts +343 -0
- package/dist/extensions/pi-tool-display/src/tool-call-box.ts +181 -0
- package/dist/extensions/pi-tool-display/src/tool-metadata.ts +139 -0
- package/dist/extensions/pi-tool-display/src/tool-overrides.ts +2168 -0
- package/dist/extensions/pi-tool-display/src/types.ts +109 -0
- package/dist/extensions/pi-tool-display/src/user-message-box-markdown.ts +64 -0
- package/dist/extensions/pi-tool-display/src/user-message-box-native.ts +66 -0
- package/dist/extensions/pi-tool-display/src/user-message-box-patch.ts +72 -0
- package/dist/extensions/pi-tool-display/src/user-message-box-renderer.ts +419 -0
- package/dist/extensions/pi-tool-display/src/user-message-box-utils.ts +106 -0
- package/dist/extensions/pi-tool-display/src/write-display-utils.ts +31 -0
- package/dist/extensions/pi-tool-display/src/zellij-modal.ts +1027 -0
- package/dist/extensions/pi-tool-display/tests/active-backlog-red.test.ts +65 -0
- package/dist/extensions/pi-tool-display/tests/ansi-utils.test.ts +345 -0
- package/dist/extensions/pi-tool-display/tests/bash-display.test.ts +442 -0
- package/dist/extensions/pi-tool-display/tests/capabilities-edge.test.ts +360 -0
- package/dist/extensions/pi-tool-display/tests/capabilities.test.ts +54 -0
- package/dist/extensions/pi-tool-display/tests/config-modal.test.ts +360 -0
- package/dist/extensions/pi-tool-display/tests/config-store.test.ts +87 -0
- package/dist/extensions/pi-tool-display/tests/custom-tool-overrides.test.ts +415 -0
- package/dist/extensions/pi-tool-display/tests/debug-logger-edge.test.ts +630 -0
- package/dist/extensions/pi-tool-display/tests/debug-logger.test.ts +62 -0
- package/dist/extensions/pi-tool-display/tests/diff-renderer-ansi.test.ts +55 -0
- package/dist/extensions/pi-tool-display/tests/diff-renderer-edge.test.ts +886 -0
- package/dist/extensions/pi-tool-display/tests/diff-renderer-numbering.test.ts +135 -0
- package/dist/extensions/pi-tool-display/tests/diff-renderer-width.test.ts +454 -0
- package/dist/extensions/pi-tool-display/tests/diff-renderer-wrap.test.ts +169 -0
- package/dist/extensions/pi-tool-display/tests/index-integration.test.ts +354 -0
- package/dist/extensions/pi-tool-display/tests/pending-diff-preview.test.ts +79 -0
- package/dist/extensions/pi-tool-display/tests/presets-edge.test.ts +229 -0
- package/dist/extensions/pi-tool-display/tests/presets.test.ts +26 -0
- package/dist/extensions/pi-tool-display/tests/reload-behavior.test.ts +1241 -0
- package/dist/extensions/pi-tool-display/tests/render-utils.test.ts +710 -0
- package/dist/extensions/pi-tool-display/tests/settings-inspector.test.ts +450 -0
- package/dist/extensions/pi-tool-display/tests/thinking-label-edge.test.ts +904 -0
- package/dist/extensions/pi-tool-display/tests/thinking-label.test.ts +72 -0
- package/dist/extensions/pi-tool-display/tests/tool-call-box.test.ts +105 -0
- package/dist/extensions/pi-tool-display/tests/tool-overrides-config.test.ts +613 -0
- package/dist/extensions/pi-tool-display/tests/tool-overrides-mcp.test.ts +499 -0
- package/dist/extensions/pi-tool-display/tests/tool-overrides-registration.test.ts +315 -0
- package/dist/extensions/pi-tool-display/tests/tool-overrides-write-meta.test.ts +68 -0
- package/dist/extensions/pi-tool-display/tests/tool-ui-utils.test.ts +413 -0
- package/dist/extensions/pi-tool-display/tests/user-message-box-performance-red.test.ts +176 -0
- package/dist/extensions/pi-tool-display/tests/user-message-box.test.ts +908 -0
- package/dist/extensions/pi-tool-display/tests/zellij-modal.test.ts +848 -0
- package/dist/extensions/pi-tool-display/tool-display-api-consumer.d.ts +30 -0
- package/dist/extensions/pi-tool-display/tool-display-api-consumer.js +40 -0
- package/dist/extensions/pi-zentui/extensions/zentui/format.ts +46 -1
- package/dist/extensions/pi-zentui/extensions/zentui/index.ts +10 -0
- package/dist/extensions/pi-zentui/extensions/zentui/user-message-styles.ts +1 -1
- package/dist/extensions/pi-zentui/test/extension-compliance.test.ts +3 -3
- package/dist/extensions/pi-zentui/test/format.test.ts +50 -2
- package/dist/modes/interactive/components/startup-box.d.ts +38 -0
- package/dist/modes/interactive/components/startup-box.js +90 -0
- package/dist/modes/interactive/interactive-mode.d.ts +3 -0
- package/dist/modes/interactive/interactive-mode.js +85 -122
- package/dist/skills/agent-browser/SKILL.md +1 -0
- package/dist/skills/batch-grill-me/SKILL.md +1 -0
- package/dist/skills/brandkit/SKILL.md +1 -0
- package/dist/skills/design-references/SKILL.md +1 -0
- package/dist/skills/design-taste-frontend/SKILL.md +1 -0
- package/dist/skills/design-taste-frontend-v1/SKILL.md +1 -0
- package/dist/skills/full-output-enforcement/SKILL.md +1 -0
- package/dist/skills/gpt-taste/SKILL.md +1 -0
- package/dist/skills/grill-me/SKILL.md +1 -0
- package/dist/skills/handoff/SKILL.md +1 -0
- package/dist/skills/handoff-text/SKILL.md +1 -0
- package/dist/skills/high-end-visual-design/SKILL.md +1 -0
- package/dist/skills/image-to-code/SKILL.md +1 -0
- package/dist/skills/imagegen-frontend-mobile/SKILL.md +1 -0
- package/dist/skills/imagegen-frontend-web/SKILL.md +1 -0
- package/dist/skills/implanger/SKILL.md +1 -0
- package/dist/skills/improve-codebase/SKILL.md +1 -0
- package/dist/skills/industrial-brutalist-ui/SKILL.md +1 -0
- package/dist/skills/minimalist-ui/SKILL.md +1 -0
- package/dist/skills/pi-subagents/SKILL.md +1 -0
- package/dist/skills/planger/SKILL.md +1 -0
- package/dist/skills/ponytail/SKILL.md +1 -0
- package/dist/skills/ponytail-audit/SKILL.md +1 -0
- package/dist/skills/ponytail-debt/SKILL.md +1 -0
- package/dist/skills/ponytail-gain/SKILL.md +1 -0
- package/dist/skills/ponytail-help/SKILL.md +1 -0
- package/dist/skills/ponytail-review/SKILL.md +1 -0
- package/dist/skills/redesign-existing-projects/SKILL.md +1 -0
- package/dist/skills/selesai-handoff/SKILL.md +1 -0
- package/dist/skills/stitch-design-taste/SKILL.md +1 -0
- package/dist/skills/unlazy/SKILL.md +1 -0
- package/dist/skills/web-design-guidelines/SKILL.md +1 -0
- package/dist/skills/workflow/SKILL.md +1 -0
- package/package.json +3 -3
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
import {
|
|
5
|
+
buildCollapsedDiffHintText,
|
|
6
|
+
type WidthMeasurementOps,
|
|
7
|
+
} from "../src/line-width-safety.ts";
|
|
8
|
+
import {
|
|
9
|
+
compactOutputLines,
|
|
10
|
+
countNonEmptyLines,
|
|
11
|
+
extractTextOutput,
|
|
12
|
+
isLikelyQuietCommand,
|
|
13
|
+
pluralize,
|
|
14
|
+
previewLines,
|
|
15
|
+
shortenPath,
|
|
16
|
+
splitLines,
|
|
17
|
+
} from "../src/render-utils.ts";
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Helpers
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
const codePointWidthOps: WidthMeasurementOps = {
|
|
24
|
+
measure: (text: string): number => [...text].length,
|
|
25
|
+
truncate: (text: string, maxWidth: number): string =>
|
|
26
|
+
[...text].slice(0, Math.max(0, maxWidth)).join(""),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
interface ThemeLike {
|
|
30
|
+
fg(color: string, text: string): string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function formatExpandHint(theme: ThemeLike): string {
|
|
34
|
+
return theme.fg("muted", " • Ctrl+O to expand");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// formatExpandHint (replicates production logic from tool-overrides.ts)
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
test("formatExpandHint: pass-through theme returns plain hint string", () => {
|
|
42
|
+
const theme: ThemeLike = { fg: (_color, text) => text };
|
|
43
|
+
assert.equal(formatExpandHint(theme), " • Ctrl+O to expand");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("formatExpandHint: ANSI-wrapping theme wraps with color codes", () => {
|
|
47
|
+
const theme: ThemeLike = {
|
|
48
|
+
fg: (color, text) => `\x1b[38;2;200;200;200m[${color}]${text}\x1b[0m`,
|
|
49
|
+
};
|
|
50
|
+
const result = formatExpandHint(theme);
|
|
51
|
+
assert.match(result, /^\x1b\[38;2;200;200;200m\[muted\]/);
|
|
52
|
+
assert.match(result, /Ctrl\+O to expand/);
|
|
53
|
+
assert.match(result, /\x1b\[0m$/);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("formatExpandHint: theme returning empty string", () => {
|
|
57
|
+
const theme: ThemeLike = { fg: () => "" };
|
|
58
|
+
assert.equal(formatExpandHint(theme), "");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("formatExpandHint: theme returning custom prefix", () => {
|
|
62
|
+
const theme: ThemeLike = { fg: (_color, text) => `[[${text}]]` };
|
|
63
|
+
assert.equal(formatExpandHint(theme), "[[ • Ctrl+O to expand]]");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// shortenPath
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
test("shortenPath: undefined returns empty string", () => {
|
|
71
|
+
assert.equal(shortenPath(undefined), "");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("shortenPath: empty string returns empty string", () => {
|
|
75
|
+
assert.equal(shortenPath(""), "");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("shortenPath: absolute path within homedir is shortened with tilde", () => {
|
|
79
|
+
const home = homedir();
|
|
80
|
+
const result = shortenPath(`${home}/projects/my-app`);
|
|
81
|
+
assert.equal(result, "~/projects/my-app");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("shortenPath: absolute path outside homedir is unchanged", () => {
|
|
85
|
+
const result = shortenPath("/opt/app/config.json");
|
|
86
|
+
assert.equal(result, "/opt/app/config.json");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("shortenPath: relative path is unchanged", () => {
|
|
90
|
+
assert.equal(shortenPath("src/utils/helper.ts"), "src/utils/helper.ts");
|
|
91
|
+
assert.equal(shortenPath("./local/file.txt"), "./local/file.txt");
|
|
92
|
+
assert.equal(shortenPath("../sibling/file.ts"), "../sibling/file.ts");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("shortenPath: deeply nested path within homedir is shortened", () => {
|
|
96
|
+
const home = homedir();
|
|
97
|
+
const result = shortenPath(`${home}/a/b/c/d/e/f/g/file.ts`);
|
|
98
|
+
assert.equal(result, "~/a/b/c/d/e/f/g/file.ts");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("shortenPath: Windows-style C:\\ path is unchanged", () => {
|
|
102
|
+
const result = shortenPath("C:\\Users\\Name\\Projects\\app");
|
|
103
|
+
// Should not match homedir (unless running on Windows with matching homedir)
|
|
104
|
+
// On Windows homedir is typically C:\Users\<username>
|
|
105
|
+
// This test ensures the path is not incorrectly shortened
|
|
106
|
+
const home = homedir();
|
|
107
|
+
if (home.startsWith("C:\\Users\\")) {
|
|
108
|
+
// If path IS within homedir, it should be shortened
|
|
109
|
+
const result2 = shortenPath(`${home}\\Projects\\app`);
|
|
110
|
+
assert.match(result2, /^~/);
|
|
111
|
+
} else {
|
|
112
|
+
// Otherwise path outside homedir stays unchanged
|
|
113
|
+
assert.equal(result, "C:\\Users\\Name\\Projects\\app");
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("shortenPath: UNC path is unchanged", () => {
|
|
118
|
+
const result = shortenPath("\\\\server\\share\\path\\file.txt");
|
|
119
|
+
// UNC paths don't start with homedir, so they pass through
|
|
120
|
+
assert.equal(result, "\\\\server\\share\\path\\file.txt");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("shortenPath: homedir itself returns tilde", () => {
|
|
124
|
+
const home = homedir();
|
|
125
|
+
const result = shortenPath(home);
|
|
126
|
+
assert.equal(result, "~");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("shortenPath: path exactly matching homedir with trailing slash returns tilde", () => {
|
|
130
|
+
const home = homedir();
|
|
131
|
+
const result = shortenPath(`${home}/`);
|
|
132
|
+
assert.equal(result, "~/");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// extractTextOutput
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
test("extractTextOutput: input with null content returns empty string", () => {
|
|
140
|
+
assert.equal(extractTextOutput({ content: null } as never), "");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("extractTextOutput: missing content field returns empty string", () => {
|
|
144
|
+
assert.equal(extractTextOutput({}), "");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("extractTextOutput: empty content array returns empty string", () => {
|
|
148
|
+
assert.equal(extractTextOutput({ content: [] }), "");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("extractTextOutput: single text block returns its text", () => {
|
|
152
|
+
const result = extractTextOutput({
|
|
153
|
+
content: [{ type: "text", text: "hello world" }],
|
|
154
|
+
});
|
|
155
|
+
assert.equal(result, "hello world");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("extractTextOutput: multiple text blocks joined by newline", () => {
|
|
159
|
+
const result = extractTextOutput({
|
|
160
|
+
content: [
|
|
161
|
+
{ type: "text", text: "line1" },
|
|
162
|
+
{ type: "text", text: "line2" },
|
|
163
|
+
],
|
|
164
|
+
});
|
|
165
|
+
assert.equal(result, "line1\nline2");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("extractTextOutput: non-text type blocks are filtered out", () => {
|
|
169
|
+
const result = extractTextOutput({
|
|
170
|
+
content: [
|
|
171
|
+
{ type: "tool_result", text: "skip me" },
|
|
172
|
+
{ type: "text", text: "keep me" },
|
|
173
|
+
],
|
|
174
|
+
});
|
|
175
|
+
assert.equal(result, "keep me");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("extractTextOutput: text block missing text field returns empty", () => {
|
|
179
|
+
const result = extractTextOutput({
|
|
180
|
+
content: [{ type: "text" }],
|
|
181
|
+
});
|
|
182
|
+
assert.equal(result, "");
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("extractTextOutput: text block with non-string text field is filtered", () => {
|
|
186
|
+
const result = extractTextOutput({
|
|
187
|
+
content: [{ type: "text", text: 123 } as never],
|
|
188
|
+
});
|
|
189
|
+
assert.equal(result, "");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("extractTextOutput: null and undefined items in array are filtered", () => {
|
|
193
|
+
const result = extractTextOutput({
|
|
194
|
+
content: [
|
|
195
|
+
null,
|
|
196
|
+
undefined,
|
|
197
|
+
{ type: "text", text: "surviving" },
|
|
198
|
+
] as never,
|
|
199
|
+
});
|
|
200
|
+
assert.equal(result, "surviving");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("extractTextOutput: nested content object (non-array) returns empty", () => {
|
|
204
|
+
const result = extractTextOutput({
|
|
205
|
+
content: { type: "text", text: "nested" },
|
|
206
|
+
} as never);
|
|
207
|
+
assert.equal(result, "");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("extractTextOutput: content as string returns empty", () => {
|
|
211
|
+
const result = extractTextOutput({
|
|
212
|
+
content: "raw string",
|
|
213
|
+
} as never);
|
|
214
|
+
assert.equal(result, "");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("extractTextOutput: content with mixed valid/invalid blocks", () => {
|
|
218
|
+
const result = extractTextOutput({
|
|
219
|
+
content: [
|
|
220
|
+
{ type: "image", source: "..." },
|
|
221
|
+
{ type: "text", text: "valid text" },
|
|
222
|
+
{ type: "text" },
|
|
223
|
+
{ type: "text", text: "" },
|
|
224
|
+
],
|
|
225
|
+
});
|
|
226
|
+
assert.equal(result, "valid text\n");
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
// splitLines
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
test("splitLines: empty string returns empty array", () => {
|
|
234
|
+
assert.deepEqual(splitLines(""), []);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("splitLines: LF line endings", () => {
|
|
238
|
+
assert.deepEqual(splitLines("line1\nline2\nline3"), [
|
|
239
|
+
"line1",
|
|
240
|
+
"line2",
|
|
241
|
+
"line3",
|
|
242
|
+
]);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("splitLines: CRLF line endings are normalized", () => {
|
|
246
|
+
assert.deepEqual(splitLines("line1\r\nline2\r\nline3"), [
|
|
247
|
+
"line1",
|
|
248
|
+
"line2",
|
|
249
|
+
"line3",
|
|
250
|
+
]);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("splitLines: CR-only line endings are removed (not treated as line separator)", () => {
|
|
254
|
+
// splitLines only removes \r; it does NOT split on CR.
|
|
255
|
+
assert.deepEqual(splitLines("line1\rline2\rline3"), ["line1line2line3"]);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("splitLines: mixed line endings — CR removed, \n splits; lone CR concatenates", () => {
|
|
259
|
+
// "a\r\nb\nc\rd" → remove \r → "a\nb\ncd" → split \n → ["a", "b", "cd"]
|
|
260
|
+
assert.deepEqual(splitLines("a\r\nb\nc\rd"), ["a", "b", "cd"]);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("splitLines: trailing newline produces trailing empty string", () => {
|
|
264
|
+
assert.deepEqual(splitLines("line1\nline2\n"), ["line1", "line2", ""]);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("splitLines: no newline returns single-element array", () => {
|
|
268
|
+
assert.deepEqual(splitLines("single line"), ["single line"]);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("splitLines: empty lines in middle are preserved", () => {
|
|
272
|
+
assert.deepEqual(splitLines("a\n\n\nb"), ["a", "", "", "b"]);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("splitLines: tabs are expanded to 4 spaces", () => {
|
|
276
|
+
assert.deepEqual(splitLines("a\tb\nc\td"), ["a b", "c d"]);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("splitLines: only newlines returns array of empty strings (split semantics)", () => {
|
|
280
|
+
assert.deepEqual(splitLines("\n\n"), ["", "", ""]);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
// compactOutputLines
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
|
|
287
|
+
test("compactOutputLines: expanded mode trims trailing empty lines", () => {
|
|
288
|
+
const result = compactOutputLines(["a", "b", "", "c", "", ""], {
|
|
289
|
+
expanded: true,
|
|
290
|
+
});
|
|
291
|
+
assert.deepEqual(result, ["a", "b", "", "c"]);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("compactOutputLines: expanded mode preserves all non-trailing content", () => {
|
|
295
|
+
const result = compactOutputLines(["a", "", "b"], { expanded: true });
|
|
296
|
+
assert.deepEqual(result, ["a", "", "b"]);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test("compactOutputLines: collapsed mode defaults to max 1 consecutive empty line", () => {
|
|
300
|
+
const result = compactOutputLines(["a", "", "", "b", "", "", "", "c"], {
|
|
301
|
+
expanded: false,
|
|
302
|
+
});
|
|
303
|
+
assert.deepEqual(result, ["a", "", "b", "", "c"]);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test("compactOutputLines: collapsed mode with maxCollapsedConsecutiveEmptyLines = 0 removes all empty lines", () => {
|
|
307
|
+
const result = compactOutputLines(["a", "", "", "b", "", "c"], {
|
|
308
|
+
expanded: false,
|
|
309
|
+
maxCollapsedConsecutiveEmptyLines: 0,
|
|
310
|
+
});
|
|
311
|
+
assert.deepEqual(result, ["a", "b", "c"]);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test("compactOutputLines: collapsed mode with maxCollapsedConsecutiveEmptyLines = 2 allows up to 2 consecutive empties", () => {
|
|
315
|
+
const result = compactOutputLines(["a", "", "", "", "b", "", "c"], {
|
|
316
|
+
expanded: false,
|
|
317
|
+
maxCollapsedConsecutiveEmptyLines: 2,
|
|
318
|
+
});
|
|
319
|
+
assert.deepEqual(result, ["a", "", "", "b", "", "c"]);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test("compactOutputLines: all trailing empty lines are trimmed before collapse", () => {
|
|
323
|
+
const result = compactOutputLines(["a", "", "b", "", ""], {
|
|
324
|
+
expanded: false,
|
|
325
|
+
});
|
|
326
|
+
assert.deepEqual(result, ["a", "", "b"]);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test("compactOutputLines: all empty lines collapses to empty array", () => {
|
|
330
|
+
const result = compactOutputLines(["", "", ""], { expanded: false });
|
|
331
|
+
assert.deepEqual(result, []);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test("compactOutputLines: empty input returns empty array", () => {
|
|
335
|
+
const result = compactOutputLines([], { expanded: false });
|
|
336
|
+
assert.deepEqual(result, []);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("compactOutputLines: single line stays unchanged", () => {
|
|
340
|
+
const result = compactOutputLines(["hello"], { expanded: true });
|
|
341
|
+
assert.deepEqual(result, ["hello"]);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// ---------------------------------------------------------------------------
|
|
345
|
+
// isLikelyQuietCommand
|
|
346
|
+
// ---------------------------------------------------------------------------
|
|
347
|
+
|
|
348
|
+
test("isLikelyQuietCommand: undefined returns false", () => {
|
|
349
|
+
assert.equal(isLikelyQuietCommand(undefined), false);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test("isLikelyQuietCommand: empty string returns false", () => {
|
|
353
|
+
assert.equal(isLikelyQuietCommand(""), false);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test("isLikelyQuietCommand: 'cd /tmp' is quiet", () => {
|
|
357
|
+
assert.equal(isLikelyQuietCommand("cd /tmp"), true);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test("isLikelyQuietCommand: 'git add file.txt' is quiet", () => {
|
|
361
|
+
assert.equal(isLikelyQuietCommand("git add file.txt"), true);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test("isLikelyQuietCommand: 'git checkout main' is quiet", () => {
|
|
365
|
+
assert.equal(isLikelyQuietCommand("git checkout main"), true);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
test("isLikelyQuietCommand: 'npm install' is quiet", () => {
|
|
369
|
+
assert.equal(isLikelyQuietCommand("npm install"), true);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test("isLikelyQuietCommand: 'npm install express' is quiet", () => {
|
|
373
|
+
assert.equal(isLikelyQuietCommand("npm install express"), true);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
test("isLikelyQuietCommand: 'pip install pytest' is quiet", () => {
|
|
377
|
+
assert.equal(isLikelyQuietCommand("pip install pytest"), true);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test("isLikelyQuietCommand: 'echo hello' is NOT quiet (not in list)", () => {
|
|
381
|
+
assert.equal(isLikelyQuietCommand("echo hello"), false);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test("isLikelyQuietCommand: 'cat file.txt' is NOT quiet (not in list)", () => {
|
|
385
|
+
assert.equal(isLikelyQuietCommand("cat file.txt"), false);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test("isLikelyQuietCommand: 'rm -rf node_modules' is quiet", () => {
|
|
389
|
+
assert.equal(isLikelyQuietCommand("rm -rf node_modules"), true);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test("isLikelyQuietCommand: 'mkdir -p dist' is quiet", () => {
|
|
393
|
+
assert.equal(isLikelyQuietCommand("mkdir -p dist"), true);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test("isLikelyQuietCommand: 'mv old new' is quiet", () => {
|
|
397
|
+
assert.equal(isLikelyQuietCommand("mv old new"), true);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
test("isLikelyQuietCommand: 'cp src dest' is quiet", () => {
|
|
401
|
+
assert.equal(isLikelyQuietCommand("cp src dest"), true);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test("isLikelyQuietCommand: 'touch index.ts' is quiet", () => {
|
|
405
|
+
assert.equal(isLikelyQuietCommand("touch index.ts"), true);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
test("isLikelyQuietCommand: whitespace-padded quiet command is detected", () => {
|
|
409
|
+
assert.equal(isLikelyQuietCommand(" cd /tmp "), true);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
test("isLikelyQuietCommand: 'npx jest' is NOT quiet", () => {
|
|
413
|
+
assert.equal(isLikelyQuietCommand("npx jest"), false);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
test("isLikelyQuietCommand: compound command matches first segment", () => {
|
|
417
|
+
assert.equal(isLikelyQuietCommand("cd /tmp && npm test"), true);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test("isLikelyQuietCommand: 'git commit' is NOT quiet (only specific git subcommands)", () => {
|
|
421
|
+
assert.equal(isLikelyQuietCommand("git commit -m 'fix'"), false);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
test("isLikelyQuietCommand: 'chmod 755 script.sh' is quiet", () => {
|
|
425
|
+
assert.equal(isLikelyQuietCommand("chmod 755 script.sh"), true);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
test("isLikelyQuietCommand: 'chown user:group file' is quiet", () => {
|
|
429
|
+
assert.equal(isLikelyQuietCommand("chown user:group file"), true);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
test("isLikelyQuietCommand: 'pnpm install' is quiet", () => {
|
|
433
|
+
assert.equal(isLikelyQuietCommand("pnpm install"), true);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
test("isLikelyQuietCommand: 'yarn install' is quiet", () => {
|
|
437
|
+
assert.equal(isLikelyQuietCommand("yarn install"), true);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
test("isLikelyQuietCommand: 'bun install' is quiet", () => {
|
|
441
|
+
assert.equal(isLikelyQuietCommand("bun install"), true);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
test("isLikelyQuietCommand: 'pip install' alone is quiet", () => {
|
|
445
|
+
assert.equal(isLikelyQuietCommand("pip install"), true);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
test("isLikelyQuietCommand: 'Set-Location C:\\' is quiet", () => {
|
|
449
|
+
assert.equal(isLikelyQuietCommand("Set-Location C:\\"), true);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
test("isLikelyQuietCommand: 'New-Item file.txt' is quiet", () => {
|
|
453
|
+
assert.equal(isLikelyQuietCommand("New-Item file.txt"), true);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
test("isLikelyQuietCommand: 'Remove-Item file' is quiet", () => {
|
|
457
|
+
assert.equal(isLikelyQuietCommand("Remove-Item file.txt"), true);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
test("isLikelyQuietCommand: 'git reset HEAD~1' is quiet", () => {
|
|
461
|
+
assert.equal(isLikelyQuietCommand("git reset HEAD~1"), true);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
test("isLikelyQuietCommand: 'git clean -fd' is quiet", () => {
|
|
465
|
+
assert.equal(isLikelyQuietCommand("git clean -fd"), true);
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
test("isLikelyQuietCommand: 'git switch feature-branch' is quiet", () => {
|
|
469
|
+
assert.equal(isLikelyQuietCommand("git switch feature-branch"), true);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
test("isLikelyQuietCommand: 'git restore .' is quiet", () => {
|
|
473
|
+
assert.equal(isLikelyQuietCommand("git restore ."), true);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
test("isLikelyQuietCommand: 'poetry install' is quiet", () => {
|
|
477
|
+
assert.equal(isLikelyQuietCommand("poetry install"), true);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
test("isLikelyQuietCommand: 'cargo fetch' is quiet", () => {
|
|
481
|
+
assert.equal(isLikelyQuietCommand("cargo fetch"), true);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
test("isLikelyQuietCommand: 'go mod tidy' is quiet", () => {
|
|
485
|
+
assert.equal(isLikelyQuietCommand("go mod tidy"), true);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
test("isLikelyQuietCommand: 'Copy-Item src dest' is quiet", () => {
|
|
489
|
+
assert.equal(isLikelyQuietCommand("Copy-Item src dest"), true);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
test("isLikelyQuietCommand: 'Move-Item old new' is quiet", () => {
|
|
493
|
+
assert.equal(isLikelyQuietCommand("Move-Item old new"), true);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// ---------------------------------------------------------------------------
|
|
497
|
+
// countNonEmptyLines
|
|
498
|
+
// ---------------------------------------------------------------------------
|
|
499
|
+
|
|
500
|
+
test("countNonEmptyLines: empty array returns 0", () => {
|
|
501
|
+
assert.equal(countNonEmptyLines([]), 0);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
test("countNonEmptyLines: all whitespace lines return 0", () => {
|
|
505
|
+
assert.equal(countNonEmptyLines(["", " ", "\t", " \t "]), 0);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
test("countNonEmptyLines: mixed empty and non-empty", () => {
|
|
509
|
+
assert.equal(countNonEmptyLines(["a", "", "b", " ", "c"]), 3);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
test("countNonEmptyLines: all non-empty returns count", () => {
|
|
513
|
+
assert.equal(countNonEmptyLines(["a", "b", "c"]), 3);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
test("countNonEmptyLines: single non-empty line", () => {
|
|
517
|
+
assert.equal(countNonEmptyLines([" hello "]), 1);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
test("countNonEmptyLines: single whitespace-only line returns 0", () => {
|
|
521
|
+
assert.equal(countNonEmptyLines([" "]), 0);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
// ---------------------------------------------------------------------------
|
|
525
|
+
// pluralize
|
|
526
|
+
// ---------------------------------------------------------------------------
|
|
527
|
+
|
|
528
|
+
test("pluralize: 0 count returns plural", () => {
|
|
529
|
+
assert.equal(pluralize(0, "line"), "lines");
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
test("pluralize: 1 count returns singular", () => {
|
|
533
|
+
assert.equal(pluralize(1, "line"), "line");
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
test("pluralize: 2 count returns plural", () => {
|
|
537
|
+
assert.equal(pluralize(2, "line"), "lines");
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
test("pluralize: explicit plural form for 0", () => {
|
|
541
|
+
assert.equal(pluralize(0, "hunk", "hunks"), "hunks");
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
test("pluralize: explicit plural form for 1", () => {
|
|
545
|
+
assert.equal(pluralize(1, "hunk", "hunks"), "hunk");
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
test("pluralize: explicit plural form for many", () => {
|
|
549
|
+
assert.equal(pluralize(5, "hunk", "hunks"), "hunks");
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
test("pluralize: negative count uses plural", () => {
|
|
553
|
+
assert.equal(pluralize(-1, "line"), "lines");
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
test("pluralize: large count uses plural", () => {
|
|
557
|
+
assert.equal(pluralize(1000, "file"), "files");
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
test("pluralize: custom plurals for diff contexts", () => {
|
|
561
|
+
assert.equal(pluralize(1, "diff", "diffs"), "diff");
|
|
562
|
+
assert.equal(pluralize(2, "diff", "diffs"), "diffs");
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
// ---------------------------------------------------------------------------
|
|
566
|
+
// previewLines
|
|
567
|
+
// ---------------------------------------------------------------------------
|
|
568
|
+
|
|
569
|
+
test("previewLines: maxLines 0 returns empty shown, all remaining", () => {
|
|
570
|
+
const result = previewLines(["a", "b", "c"], 0);
|
|
571
|
+
assert.deepEqual(result, { shown: [], remaining: 3 });
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
test("previewLines: maxLines 1 returns first line, remaining", () => {
|
|
575
|
+
const result = previewLines(["a", "b", "c"], 1);
|
|
576
|
+
assert.deepEqual(result, { shown: ["a"], remaining: 2 });
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
test("previewLines: maxLines larger than array returns all, 0 remaining", () => {
|
|
580
|
+
const result = previewLines(["a", "b"], 10);
|
|
581
|
+
assert.deepEqual(result, { shown: ["a", "b"], remaining: 0 });
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
test("previewLines: empty array returns empty shown, 0 remaining", () => {
|
|
585
|
+
const result = previewLines([], 5);
|
|
586
|
+
assert.deepEqual(result, { shown: [], remaining: 0 });
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
test("previewLines: negative maxLines treated as 0", () => {
|
|
590
|
+
const result = previewLines(["a", "b"], -5);
|
|
591
|
+
assert.deepEqual(result, { shown: [], remaining: 2 });
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
test("previewLines: maxLines exactly equals array length", () => {
|
|
595
|
+
const result = previewLines(["a", "b", "c"], 3);
|
|
596
|
+
assert.deepEqual(result, { shown: ["a", "b", "c"], remaining: 0 });
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
test("previewLines: single element with maxLines 0 returns empty", () => {
|
|
600
|
+
const result = previewLines(["only"], 0);
|
|
601
|
+
assert.deepEqual(result, { shown: [], remaining: 1 });
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
test("previewLines: Infinity-like large value is clamped to array", () => {
|
|
605
|
+
const result = previewLines(["x"], Number.POSITIVE_INFINITY);
|
|
606
|
+
assert.deepEqual(result, { shown: ["x"], remaining: 0 });
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
// ---------------------------------------------------------------------------
|
|
610
|
+
// buildCollapsedDiffHintText
|
|
611
|
+
// ---------------------------------------------------------------------------
|
|
612
|
+
|
|
613
|
+
test("buildCollapsedDiffHintText: wide width shows full hint with lines and hunks", () => {
|
|
614
|
+
const hint = buildCollapsedDiffHintText(
|
|
615
|
+
{ remainingLines: 42, hiddenHunks: 3 },
|
|
616
|
+
80,
|
|
617
|
+
codePointWidthOps,
|
|
618
|
+
);
|
|
619
|
+
assert.equal(
|
|
620
|
+
hint,
|
|
621
|
+
"… (42 more diff lines • 3 more hunks • Ctrl+O to expand)",
|
|
622
|
+
);
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
test("buildCollapsedDiffHintText: remaining lines only, no hunks", () => {
|
|
626
|
+
const hint = buildCollapsedDiffHintText(
|
|
627
|
+
{ remainingLines: 10, hiddenHunks: 0 },
|
|
628
|
+
80,
|
|
629
|
+
codePointWidthOps,
|
|
630
|
+
);
|
|
631
|
+
assert.equal(hint, "… (10 more diff lines • Ctrl+O to expand)");
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
test("buildCollapsedDiffHintText: zero remaining lines and zero hunks", () => {
|
|
635
|
+
const hint = buildCollapsedDiffHintText(
|
|
636
|
+
{ remainingLines: 0, hiddenHunks: 0 },
|
|
637
|
+
80,
|
|
638
|
+
codePointWidthOps,
|
|
639
|
+
);
|
|
640
|
+
// "0 more diff lines" passed to pluralize → "0 more diff lines"
|
|
641
|
+
assert.equal(hint, "… (0 more diff lines • Ctrl+O to expand)");
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
test("buildCollapsedDiffHintText: narrow width forces shorter variants", () => {
|
|
645
|
+
const hint = buildCollapsedDiffHintText(
|
|
646
|
+
{ remainingLines: 3970, hiddenHunks: 1 },
|
|
647
|
+
40,
|
|
648
|
+
codePointWidthOps,
|
|
649
|
+
);
|
|
650
|
+
assert.ok(codePointWidthOps.measure(hint) <= 40);
|
|
651
|
+
assert.match(hint, /3970/);
|
|
652
|
+
// With ~40 chars, it should drop the Ctrl+O hint
|
|
653
|
+
assert.doesNotMatch(hint, /Ctrl\+O/);
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
test("buildCollapsedDiffHintText: very narrow width forces compact format", () => {
|
|
657
|
+
const hint = buildCollapsedDiffHintText(
|
|
658
|
+
{ remainingLines: 3970, hiddenHunks: 1 },
|
|
659
|
+
20,
|
|
660
|
+
codePointWidthOps,
|
|
661
|
+
);
|
|
662
|
+
assert.ok(codePointWidthOps.measure(hint) <= 20);
|
|
663
|
+
// At 20 chars, it likely shows the compact "+N • +Mh" format
|
|
664
|
+
assert.match(hint, /\+/);
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
test("buildCollapsedDiffHintText: extremely narrow width returns just '…'", () => {
|
|
668
|
+
const hint = buildCollapsedDiffHintText(
|
|
669
|
+
{ remainingLines: 100, hiddenHunks: 5 },
|
|
670
|
+
1,
|
|
671
|
+
codePointWidthOps,
|
|
672
|
+
);
|
|
673
|
+
// The ellipsis "…" is 1 code point wide
|
|
674
|
+
assert.ok(codePointWidthOps.measure(hint) <= 1);
|
|
675
|
+
assert.equal(hint, "…");
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
test("buildCollapsedDiffHintText: zero width returns empty string", () => {
|
|
679
|
+
const hint = buildCollapsedDiffHintText(
|
|
680
|
+
{ remainingLines: 100, hiddenHunks: 2 },
|
|
681
|
+
0,
|
|
682
|
+
codePointWidthOps,
|
|
683
|
+
);
|
|
684
|
+
assert.equal(hint, "");
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
test("buildCollapsedDiffHintText: many hunks at narrow width", () => {
|
|
688
|
+
const hint = buildCollapsedDiffHintText(
|
|
689
|
+
{ remainingLines: 12, hiddenHunks: 2 },
|
|
690
|
+
35,
|
|
691
|
+
codePointWidthOps,
|
|
692
|
+
);
|
|
693
|
+
assert.ok(codePointWidthOps.measure(hint) <= 35);
|
|
694
|
+
// Should mention both lines and hunks in some form
|
|
695
|
+
assert.match(hint, /12/);
|
|
696
|
+
assert.match(hint, /2/);
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
test("buildCollapsedDiffHintText: negative values are handled gracefully", () => {
|
|
700
|
+
// buildCollapsedDiffHintText doesn't clamp negative values internally,
|
|
701
|
+
// but pluralize handles negative counts (returns plural)
|
|
702
|
+
const hint = buildCollapsedDiffHintText(
|
|
703
|
+
{ remainingLines: -1, hiddenHunks: -2 },
|
|
704
|
+
80,
|
|
705
|
+
codePointWidthOps,
|
|
706
|
+
);
|
|
707
|
+
// Should not throw and produce some string
|
|
708
|
+
assert.ok(typeof hint === "string");
|
|
709
|
+
assert.ok(hint.length > 0);
|
|
710
|
+
});
|