magus-cli 7.4.0 → 7.4.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magus-cli",
3
- "version": "7.4.0",
3
+ "version": "7.4.1",
4
4
  "description": "TUI tool for managing Claude Code plugins, MCPs, and configuration",
5
5
  "type": "module",
6
6
  "main": "src/main.tsx",
@@ -64,8 +64,8 @@
64
64
  "typescript": "^5.6.3"
65
65
  },
66
66
  "optionalDependencies": {
67
- "magus-cli-darwin-arm64": "7.4.0",
68
- "magus-cli-darwin-x64": "7.4.0",
69
- "magus-cli-linux-x64": "7.4.0"
67
+ "magus-cli-darwin-arm64": "7.4.1",
68
+ "magus-cli-darwin-x64": "7.4.1",
69
+ "magus-cli-linux-x64": "7.4.1"
70
70
  }
71
71
  }
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Blind tests — contract C19 (requirement R4.9).
3
+ *
4
+ * Written from requirements.md and architecture.md only. A terminal delivers a
5
+ * paste as ONE event, not as keys, and C19 routes that event through the same
6
+ * layer order as a keypress. The layers here are stubs that record every
7
+ * handlePaste() call and the exact text each one received, so "a lower layer
8
+ * never sees the text" and "the text is passed through unchanged" are observed
9
+ * directly rather than inferred.
10
+ */
11
+ import { describe, expect, test } from "bun:test";
12
+
13
+ const load = () => import("../../ui/input/key-dispatch.js");
14
+
15
+ type LayerId = "system" | "modal" | "input" | "screen" | "scroll" | "global";
16
+
17
+ const ORDER: LayerId[] = [
18
+ "system",
19
+ "modal",
20
+ "input",
21
+ "screen",
22
+ "scroll",
23
+ "global",
24
+ ];
25
+
26
+ interface Recorder {
27
+ /** Layer ids in the order their handlePaste() was called. */
28
+ calls: LayerId[];
29
+ /** Every (layer, text) pair a handler actually received. */
30
+ seen: { id: LayerId; text: string }[];
31
+ layers: {
32
+ id: LayerId;
33
+ active: boolean;
34
+ handlePaste(text: string): boolean;
35
+ }[];
36
+ }
37
+
38
+ /** Build one paste layer per id. `consumes[id]` decides; absent means "never consumes". */
39
+ function stack(
40
+ consumes: Partial<Record<LayerId, (text: string) => boolean>>,
41
+ active: Partial<Record<LayerId, boolean>> = {},
42
+ order: LayerId[] = ORDER,
43
+ ): Recorder {
44
+ const calls: LayerId[] = [];
45
+ const seen: { id: LayerId; text: string }[] = [];
46
+ const layers = order.map((id) => ({
47
+ id,
48
+ active: active[id] ?? true,
49
+ handlePaste(text: string): boolean {
50
+ calls.push(id);
51
+ seen.push({ id, text });
52
+ return consumes[id]?.(text) ?? false;
53
+ },
54
+ }));
55
+ return { calls, seen, layers };
56
+ }
57
+
58
+ const all = () => true;
59
+ const TEXT = "pasted search term";
60
+
61
+ describe("C19 — dispatchPaste: layer order", () => {
62
+ test("R4.9 a paste is offered to layers in LAYER_ORDER regardless of array order", async () => {
63
+ const { dispatchPaste, LAYER_ORDER } = await load();
64
+ expect([...LAYER_ORDER]).toEqual(ORDER);
65
+
66
+ const shuffled: LayerId[] = [
67
+ "global",
68
+ "screen",
69
+ "system",
70
+ "scroll",
71
+ "input",
72
+ "modal",
73
+ ];
74
+ const r = stack({}, {}, shuffled);
75
+ expect(dispatchPaste(r.layers, TEXT)).toBeNull();
76
+ expect(r.calls).toEqual(ORDER);
77
+ });
78
+
79
+ test("R4.9 with every layer consuming in reversed array order, the topmost layer wins alone", async () => {
80
+ const { dispatchPaste } = await load();
81
+ const r = stack(
82
+ {
83
+ system: all,
84
+ modal: all,
85
+ input: all,
86
+ screen: all,
87
+ scroll: all,
88
+ global: all,
89
+ },
90
+ {},
91
+ [...ORDER].reverse(),
92
+ );
93
+ expect(dispatchPaste(r.layers, TEXT)).toBe("system");
94
+ expect(r.calls).toEqual(["system"]);
95
+ });
96
+
97
+ for (const [i, consumer] of ORDER.entries()) {
98
+ test(`R4.9 when ${consumer} consumes, dispatchPaste returns "${consumer}" and no lower layer sees the text`, async () => {
99
+ const { dispatchPaste } = await load();
100
+ // Every layer from the consumer down would also consume — only order decides.
101
+ const consumes: Partial<Record<LayerId, (text: string) => boolean>> = {};
102
+ for (const id of ORDER.slice(i)) consumes[id] = all;
103
+ const r = stack(consumes);
104
+ expect(dispatchPaste(r.layers, TEXT)).toBe(consumer);
105
+ expect(r.calls).toEqual(ORDER.slice(0, i + 1));
106
+ expect(r.seen.map((s) => s.id)).toEqual(ORDER.slice(0, i + 1));
107
+ });
108
+ }
109
+
110
+ test("R4.9 inactive layers are skipped: their handlePaste() is never called", async () => {
111
+ const { dispatchPaste } = await load();
112
+ const r = stack(
113
+ { modal: all, input: all, global: all },
114
+ { modal: false, input: false },
115
+ );
116
+ expect(dispatchPaste(r.layers, TEXT)).toBe("global");
117
+ expect(r.calls).toEqual(["system", "screen", "scroll", "global"]);
118
+ });
119
+
120
+ test("R4.9 an inactive consumer does not stop the paste: the next active consumer wins", async () => {
121
+ const { dispatchPaste } = await load();
122
+ const r = stack({ input: all, screen: all }, { input: false });
123
+ expect(dispatchPaste(r.layers, TEXT)).toBe("screen");
124
+ expect(r.calls).toEqual(["system", "modal", "screen"]);
125
+ });
126
+
127
+ test("R4.9 dispatchPaste returns null when no layer consumes, calling each active layer once", async () => {
128
+ const { dispatchPaste } = await load();
129
+ const r = stack({});
130
+ expect(dispatchPaste(r.layers, TEXT)).toBeNull();
131
+ expect(r.calls).toEqual(ORDER);
132
+ });
133
+
134
+ test("R4.9 missing layers are fine: a stack of only screen and global", async () => {
135
+ const { dispatchPaste } = await load();
136
+ const r = stack({ global: all }, {}, ["global", "screen"]);
137
+ expect(dispatchPaste(r.layers, TEXT)).toBe("global");
138
+ expect(r.calls).toEqual(["screen", "global"]);
139
+ });
140
+
141
+ test("R4.9 an empty layer list consumes nothing", async () => {
142
+ const { dispatchPaste } = await load();
143
+ expect(dispatchPaste([], TEXT)).toBeNull();
144
+ });
145
+ });
146
+
147
+ describe("C19 — dispatchPaste: a focused search box takes the paste", () => {
148
+ test("R4.9 a focused input consumes the paste and the screen below never sees the text", async () => {
149
+ const { dispatchPaste } = await load();
150
+ const typed: string[] = [];
151
+ const r = stack({
152
+ input: (text) => {
153
+ typed.push(text);
154
+ return true;
155
+ },
156
+ screen: all,
157
+ global: all,
158
+ });
159
+ expect(dispatchPaste(r.layers, TEXT)).toBe("input");
160
+ expect(typed).toEqual([TEXT]);
161
+ expect(r.calls).toEqual(["system", "modal", "input"]);
162
+ expect(r.seen.some((s) => s.id === "screen")).toBe(false);
163
+ expect(r.seen.some((s) => s.id === "global")).toBe(false);
164
+ });
165
+
166
+ test("R4.9 with no active input layer, the paste reaches the lower layers in order", async () => {
167
+ const { dispatchPaste } = await load();
168
+ const r = stack({ input: all }, { input: false });
169
+ expect(dispatchPaste(r.layers, TEXT)).toBeNull();
170
+ expect(r.calls).toEqual(["system", "modal", "screen", "scroll", "global"]);
171
+ expect(r.seen.every((s) => s.text === TEXT)).toBe(true);
172
+ });
173
+
174
+ test("R4.9 with no input layer in the stack at all, nothing consumes the paste", async () => {
175
+ const { dispatchPaste } = await load();
176
+ const r = stack({}, {}, ["system", "modal", "screen", "scroll", "global"]);
177
+ expect(dispatchPaste(r.layers, TEXT)).toBeNull();
178
+ expect(r.calls).toEqual(["system", "modal", "screen", "scroll", "global"]);
179
+ });
180
+
181
+ test("R4.9 a modal above the input takes the paste first", async () => {
182
+ const { dispatchPaste } = await load();
183
+ const r = stack({ modal: all, input: all });
184
+ expect(dispatchPaste(r.layers, TEXT)).toBe("modal");
185
+ expect(r.calls).toEqual(["system", "modal"]);
186
+ });
187
+ });
188
+
189
+ describe("C19 — dispatchPaste: empty text", () => {
190
+ test("R4.9 empty text consumes nothing and returns null", async () => {
191
+ const { dispatchPaste } = await load();
192
+ const r = stack({
193
+ system: all,
194
+ modal: all,
195
+ input: all,
196
+ screen: all,
197
+ scroll: all,
198
+ global: all,
199
+ });
200
+ expect(dispatchPaste(r.layers, "")).toBeNull();
201
+ });
202
+
203
+ test("R4.9 empty text calls no handler, not even a focused input", async () => {
204
+ const { dispatchPaste } = await load();
205
+ const r = stack({ input: all });
206
+ dispatchPaste(r.layers, "");
207
+ expect(r.calls).toEqual([]);
208
+ expect(r.seen).toEqual([]);
209
+ });
210
+ });
211
+
212
+ describe("C19 — dispatchPaste: the text passes through unchanged", () => {
213
+ const PAYLOADS: [string, string][] = [
214
+ ["a plain term", "node_modules"],
215
+ ["leading and trailing spaces", " spaced term "],
216
+ ["a single newline", "first line\nsecond line"],
217
+ ["a trailing newline", "term\n"],
218
+ ["CRLF line endings", "one\r\ntwo\r\n"],
219
+ ["several lines", "a\nb\nc\nd"],
220
+ ["a tab", "left\tright"],
221
+ ["unicode accents", "café naïve"],
222
+ ["CJK", "日本語のテキスト"],
223
+ ["emoji", "🚀 ship it 🎉"],
224
+ ["combining marks", "égalité"],
225
+ ["a path with spaces", "/tmp/some dir/file.json"],
226
+ ["JSON-ish punctuation", '{"key": "value", "n": 1}'],
227
+ ["a single space", " "],
228
+ ["a lone newline", "\n"],
229
+ ];
230
+
231
+ for (const [label, text] of PAYLOADS) {
232
+ test(`R4.9 ${label} reaches the consuming layer byte-for-byte`, async () => {
233
+ const { dispatchPaste } = await load();
234
+ const got: string[] = [];
235
+ const r = stack({
236
+ input: (t) => {
237
+ got.push(t);
238
+ return true;
239
+ },
240
+ });
241
+ expect(dispatchPaste(r.layers, text)).toBe("input");
242
+ expect(got).toEqual([text]);
243
+ });
244
+
245
+ test(`R4.9 ${label} reaches every layer it is offered to unchanged`, async () => {
246
+ const { dispatchPaste } = await load();
247
+ const r = stack({});
248
+ expect(dispatchPaste(r.layers, text)).toBeNull();
249
+ expect(r.seen).toEqual(ORDER.map((id) => ({ id, text })));
250
+ });
251
+ }
252
+ });