@openvcs/sdk 0.4.0 → 0.4.1-beta.128
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 +21 -6
- package/lib/build.d.ts +5 -8
- package/lib/build.js +42 -25
- package/lib/init.d.ts +4 -0
- package/lib/init.js +5 -14
- package/lib/npm-runner.d.ts +11 -0
- package/lib/npm-runner.js +62 -0
- package/lib/runtime/dispatcher.js +19 -11
- package/lib/runtime/vcs-delegate-base.d.ts +3 -1
- package/lib/runtime/vcs-delegate-base.js +4 -0
- package/lib/runtime/vcs-delegate-metadata.d.ts +2 -0
- package/lib/runtime/vcs-delegate-metadata.js +1 -0
- package/lib/types/vcs.d.ts +34 -10
- package/package.json +6 -3
- package/src/lib/build.ts +52 -25
- package/src/lib/init.ts +6 -17
- package/src/lib/npm-runner.ts +47 -0
- package/src/lib/runtime/dispatcher.ts +19 -11
- package/src/lib/runtime/vcs-delegate-base.ts +9 -1
- package/src/lib/runtime/vcs-delegate-metadata.ts +2 -0
- package/src/lib/types/vcs.ts +40 -11
- package/test/build.test.js +153 -2
- package/test/cli-direct.test.js +57 -0
- package/test/dispatcher.test.js +104 -0
- package/test/fs-utils.test.js +57 -0
- package/test/host.test.js +39 -0
- package/test/init.test.js +209 -1
- package/test/menu.test.js +147 -0
- package/test/modal.test.js +74 -0
- package/test/npm-runner.test.js +89 -0
- package/test/runtime.test.js +143 -0
- package/test/transport.test.js +71 -0
- package/test/vcs-delegate-base.test.js +87 -0
package/test/init.test.js
CHANGED
|
@@ -1,11 +1,42 @@
|
|
|
1
1
|
const assert = require("node:assert/strict");
|
|
2
|
+
const childProcess = require("node:child_process");
|
|
2
3
|
const fs = require("node:fs");
|
|
4
|
+
const readline = require("node:readline/promises");
|
|
3
5
|
const test = require("node:test");
|
|
4
6
|
const path = require("node:path");
|
|
5
7
|
|
|
6
|
-
const { __private } = require("../lib/init");
|
|
8
|
+
const { __private, initUsage, isUsageError, runInitCommand } = require("../lib/init");
|
|
7
9
|
const { cleanupTempDir, makeTempDir } = require("./helpers");
|
|
8
10
|
|
|
11
|
+
async function withMockReadline(answers, run) {
|
|
12
|
+
const originalCreateInterface = readline.createInterface;
|
|
13
|
+
const prompts = [];
|
|
14
|
+
readline.createInterface = () => ({
|
|
15
|
+
async question(prompt) {
|
|
16
|
+
prompts.push(prompt);
|
|
17
|
+
return answers.shift() ?? "";
|
|
18
|
+
},
|
|
19
|
+
close() {},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
return await run(prompts);
|
|
24
|
+
} finally {
|
|
25
|
+
readline.createInterface = originalCreateInterface;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function withMockSpawnSync(result, run) {
|
|
30
|
+
const originalSpawnSync = childProcess.spawnSync;
|
|
31
|
+
childProcess.spawnSync = () => result;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
return await run();
|
|
35
|
+
} finally {
|
|
36
|
+
childProcess.spawnSync = originalSpawnSync;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
9
40
|
test("validatePluginId accepts regular ids", () => {
|
|
10
41
|
assert.equal(__private.validatePluginId("my.plugin"), undefined);
|
|
11
42
|
assert.equal(__private.validatePluginId("my-plugin_1"), undefined);
|
|
@@ -25,6 +56,97 @@ test("validatePluginId rejects path separators", () => {
|
|
|
25
56
|
assert.match(__private.validatePluginId("bad\\id"), /path separators/);
|
|
26
57
|
});
|
|
27
58
|
|
|
59
|
+
test("init helpers derive stable defaults", () => {
|
|
60
|
+
assert.equal(__private.sanitizeIdToken(" My Plugin!! "), "my-plugin");
|
|
61
|
+
assert.equal(__private.defaultPluginIdFromDir(path.join("tmp", "My Plugin")), "my-plugin");
|
|
62
|
+
assert.equal(__private.defaultPluginIdFromDir(path.join("tmp", "!!!")), "openvcs.plugin");
|
|
63
|
+
assert.equal(__private.defaultPluginNameFromId("my-plugin.name"), "My Plugin Name");
|
|
64
|
+
assert.equal(__private.defaultPluginNameFromId("---"), "OpenVCS Plugin");
|
|
65
|
+
assert.match(initUsage("sdk"), /sdk init/);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("runInitCommand validates args before prompting", async () => {
|
|
69
|
+
await assert.rejects(() => runInitCommand(["--bad"]), /unknown argument for init/);
|
|
70
|
+
await assert.rejects(() => runInitCommand(["one", "two"]), /at most one target directory/);
|
|
71
|
+
|
|
72
|
+
let usageError;
|
|
73
|
+
try {
|
|
74
|
+
await runInitCommand(["--help"]);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
usageError = error;
|
|
77
|
+
}
|
|
78
|
+
assert.equal(isUsageError(usageError), true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("runInitCommand rejects when target path is a file", async () => {
|
|
82
|
+
const root = makeTempDir("openvcs-sdk-test");
|
|
83
|
+
const targetPath = path.join(root, "plugin");
|
|
84
|
+
fs.writeFileSync(targetPath, "not a directory", "utf8");
|
|
85
|
+
|
|
86
|
+
await assert.rejects(
|
|
87
|
+
() =>
|
|
88
|
+
withMockReadline([
|
|
89
|
+
"",
|
|
90
|
+
"module",
|
|
91
|
+
"",
|
|
92
|
+
"",
|
|
93
|
+
"",
|
|
94
|
+
"",
|
|
95
|
+
"n",
|
|
96
|
+
], () => runInitCommand([targetPath])),
|
|
97
|
+
/target path exists but is not a directory/
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
cleanupTempDir(root);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("runInitCommand writes a module template after confirming overwrite", async () => {
|
|
104
|
+
const root = makeTempDir("openvcs-sdk-test");
|
|
105
|
+
const targetDir = path.join(root, "plugin");
|
|
106
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
107
|
+
fs.writeFileSync(path.join(targetDir, "README.md"), "keep", "utf8");
|
|
108
|
+
|
|
109
|
+
const created = await withMockReadline([
|
|
110
|
+
"",
|
|
111
|
+
"module",
|
|
112
|
+
"",
|
|
113
|
+
"",
|
|
114
|
+
"",
|
|
115
|
+
"",
|
|
116
|
+
"n",
|
|
117
|
+
"y",
|
|
118
|
+
], () => runInitCommand([targetDir]));
|
|
119
|
+
|
|
120
|
+
assert.equal(created, targetDir);
|
|
121
|
+
assert.equal(fs.existsSync(path.join(targetDir, "package.json")), true);
|
|
122
|
+
assert.equal(fs.existsSync(path.join(targetDir, "src", "plugin.ts")), true);
|
|
123
|
+
assert.equal(fs.existsSync(path.join(targetDir, ".gitignore")), true);
|
|
124
|
+
|
|
125
|
+
cleanupTempDir(root);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("runInitCommand spawns npm install when requested", async () => {
|
|
129
|
+
const root = makeTempDir("openvcs-sdk-test");
|
|
130
|
+
const targetDir = path.join(root, "fresh-plugin");
|
|
131
|
+
|
|
132
|
+
await withMockSpawnSync({ status: 0 }, async () => {
|
|
133
|
+
const created = await withMockReadline([
|
|
134
|
+
targetDir,
|
|
135
|
+
"module",
|
|
136
|
+
"fresh.plugin",
|
|
137
|
+
"Fresh Plugin",
|
|
138
|
+
"0.1.0",
|
|
139
|
+
"y",
|
|
140
|
+
"y",
|
|
141
|
+
], () => runInitCommand([]));
|
|
142
|
+
|
|
143
|
+
assert.equal(created, path.resolve(targetDir));
|
|
144
|
+
assert.equal(fs.existsSync(path.join(targetDir, "package.json")), true);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
cleanupTempDir(root);
|
|
148
|
+
});
|
|
149
|
+
|
|
28
150
|
test("collectAnswers re-prompts invalid plugin id", async () => {
|
|
29
151
|
const prompts = [
|
|
30
152
|
"plugin-dir",
|
|
@@ -66,6 +188,67 @@ test("collectAnswers re-prompts invalid plugin id", async () => {
|
|
|
66
188
|
assert.equal(messages.some((message) => message.includes("must not contain path separators")), true);
|
|
67
189
|
});
|
|
68
190
|
|
|
191
|
+
test("collectAnswers handles theme mode, invalid kind, blank defaults, and boolean retries", async () => {
|
|
192
|
+
const prompts = [
|
|
193
|
+
"",
|
|
194
|
+
"bad-kind",
|
|
195
|
+
"t",
|
|
196
|
+
"",
|
|
197
|
+
"",
|
|
198
|
+
"",
|
|
199
|
+
];
|
|
200
|
+
const booleans = [false, true];
|
|
201
|
+
const messages = [];
|
|
202
|
+
const promptDriver = {
|
|
203
|
+
async promptText(_label, defaultValue) {
|
|
204
|
+
const value = prompts.shift();
|
|
205
|
+
return value === "" ? defaultValue : value;
|
|
206
|
+
},
|
|
207
|
+
async promptBoolean() {
|
|
208
|
+
return booleans.shift();
|
|
209
|
+
},
|
|
210
|
+
close() {
|
|
211
|
+
messages.push("closed");
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
const output = { write(message) { messages.push(message); } };
|
|
215
|
+
|
|
216
|
+
const answers = await __private.collectAnswers(
|
|
217
|
+
{ forceTheme: false, targetHint: "theme-dir" },
|
|
218
|
+
promptDriver,
|
|
219
|
+
output,
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
assert.equal(answers.kind, "theme");
|
|
223
|
+
assert.equal(answers.pluginId, "theme-dir");
|
|
224
|
+
assert.equal(answers.pluginName, "Theme Dir");
|
|
225
|
+
assert.equal(answers.pluginVersion, "0.1.0");
|
|
226
|
+
assert.equal(answers.defaultEnabled, false);
|
|
227
|
+
assert.equal(answers.runNpmInstall, true);
|
|
228
|
+
assert.equal(messages.some((message) => String(message).includes("Please choose")), true);
|
|
229
|
+
assert.equal(messages.includes("closed"), true);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("collectAnswers skips kind prompt when theme is forced", async () => {
|
|
233
|
+
const prompts = ["forced-dir", "forced.theme", "Forced Theme", "1.0.0"];
|
|
234
|
+
const labels = [];
|
|
235
|
+
const promptDriver = {
|
|
236
|
+
async promptText(label) {
|
|
237
|
+
labels.push(label);
|
|
238
|
+
return prompts.shift();
|
|
239
|
+
},
|
|
240
|
+
async promptBoolean() {
|
|
241
|
+
return false;
|
|
242
|
+
},
|
|
243
|
+
close() {},
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const answers = await __private.collectAnswers({ forceTheme: true }, promptDriver, { write() {} });
|
|
247
|
+
|
|
248
|
+
assert.equal(answers.kind, "theme");
|
|
249
|
+
assert.equal(labels.includes("Template type (module/theme)"), false);
|
|
250
|
+
});
|
|
251
|
+
|
|
69
252
|
test("writeModuleTemplate scaffolds SDK runtime entrypoint", () => {
|
|
70
253
|
const root = makeTempDir("openvcs-sdk-test");
|
|
71
254
|
const targetDir = path.join(root, "plugin");
|
|
@@ -92,3 +275,28 @@ test("writeModuleTemplate scaffolds SDK runtime entrypoint", () => {
|
|
|
92
275
|
|
|
93
276
|
cleanupTempDir(root);
|
|
94
277
|
});
|
|
278
|
+
|
|
279
|
+
test("writeThemeTemplate scaffolds theme package without module runtime", () => {
|
|
280
|
+
const root = makeTempDir("openvcs-sdk-test");
|
|
281
|
+
const targetDir = path.join(root, "theme-plugin");
|
|
282
|
+
|
|
283
|
+
__private.writeThemeTemplate({
|
|
284
|
+
targetDir,
|
|
285
|
+
kind: "theme",
|
|
286
|
+
pluginId: "example.theme",
|
|
287
|
+
pluginName: "Example Theme",
|
|
288
|
+
pluginVersion: "0.3.0",
|
|
289
|
+
defaultEnabled: false,
|
|
290
|
+
runNpmInstall: false,
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(targetDir, "package.json"), "utf8"));
|
|
294
|
+
const themeJson = JSON.parse(fs.readFileSync(path.join(targetDir, "themes", "default", "theme.json"), "utf8"));
|
|
295
|
+
|
|
296
|
+
assert.equal(packageJson.openvcs.module, undefined);
|
|
297
|
+
assert.equal(packageJson.openvcs.default_enabled, false);
|
|
298
|
+
assert.equal(themeJson.name, "Example Theme");
|
|
299
|
+
assert.equal(themeJson.tokens.accent, "#2a7fff");
|
|
300
|
+
|
|
301
|
+
cleanupTempDir(root);
|
|
302
|
+
});
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
const assert = require("node:assert/strict");
|
|
2
|
+
const test = require("node:test");
|
|
3
|
+
|
|
4
|
+
const {
|
|
5
|
+
addMenuItem,
|
|
6
|
+
createMenu,
|
|
7
|
+
getMenu,
|
|
8
|
+
hasRegisteredAction,
|
|
9
|
+
hideMenu,
|
|
10
|
+
invoke,
|
|
11
|
+
notify,
|
|
12
|
+
registerAction,
|
|
13
|
+
removeMenu,
|
|
14
|
+
resetMenuRegistry,
|
|
15
|
+
runRegisteredAction,
|
|
16
|
+
showMenu,
|
|
17
|
+
} = require("../lib/runtime/menu");
|
|
18
|
+
const { createRegisteredPluginRuntime } = require("../lib/runtime");
|
|
19
|
+
const { EventEmitter } = require("node:events");
|
|
20
|
+
const { parseFramedMessages, serializeFramedMessage } = require("../lib/runtime/transport");
|
|
21
|
+
|
|
22
|
+
function menuResult() {
|
|
23
|
+
const stdin = new EventEmitter();
|
|
24
|
+
const chunks = [];
|
|
25
|
+
const stdout = {
|
|
26
|
+
write(chunk) {
|
|
27
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
28
|
+
return true;
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
createRegisteredPluginRuntime({}).start({ stdin, stdout });
|
|
32
|
+
stdin.emit("data", serializeFramedMessage({ jsonrpc: "2.0", id: 1, method: "plugin.get_menus", params: {} }));
|
|
33
|
+
return new Promise((resolve) => setImmediate(() => resolve(parseFramedMessages(Buffer.concat(chunks)).messages[0].result)));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
test("menu ordering supports before and after placement", async () => {
|
|
37
|
+
resetMenuRegistry();
|
|
38
|
+
createMenu("middle", "Middle", { surface: "menubar" });
|
|
39
|
+
createMenu("after", "After", { surface: "settings", after: "middle" });
|
|
40
|
+
createMenu("before", "Before", { surface: "menubar", before: "middle" });
|
|
41
|
+
|
|
42
|
+
const menus = await menuResult();
|
|
43
|
+
|
|
44
|
+
assert.deepEqual(menus.map((menu) => menu.id), ["before", "middle", "after"]);
|
|
45
|
+
assert.deepEqual(menus.map((menu) => menu.surface), ["menubar", "menubar", "settings"]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("menu ordering falls back when requested anchors are missing", async () => {
|
|
49
|
+
resetMenuRegistry();
|
|
50
|
+
createMenu("first", "First", { surface: "menubar", after: "missing" });
|
|
51
|
+
createMenu("second", "Second", { surface: "menubar", before: "missing" });
|
|
52
|
+
createMenu("first", "First Updated", { surface: "settings" });
|
|
53
|
+
|
|
54
|
+
const menus = await menuResult();
|
|
55
|
+
|
|
56
|
+
assert.deepEqual(menus.map((menu) => menu.id), ["second", "first"]);
|
|
57
|
+
assert.equal(menus[1].label, "First Updated");
|
|
58
|
+
assert.equal(menus[1].surface, "settings");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("menu item insertion supports before anchors and ignores invalid items", async () => {
|
|
62
|
+
resetMenuRegistry();
|
|
63
|
+
const menu = createMenu("insert", "Insert", { surface: "menubar" });
|
|
64
|
+
menu.addItem({ label: "Last", action: "last" });
|
|
65
|
+
menu.addItem({ label: "First", action: "first", before: "last" });
|
|
66
|
+
menu.addItem({ label: "", action: "ignored" });
|
|
67
|
+
menu.addItem({ label: "Ignored", action: "" });
|
|
68
|
+
|
|
69
|
+
const menus = await menuResult();
|
|
70
|
+
|
|
71
|
+
assert.deepEqual(menus[0].elements, [
|
|
72
|
+
{ type: "button", id: "first", label: "First" },
|
|
73
|
+
{ type: "button", id: "last", label: "Last" },
|
|
74
|
+
]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("menu handle can remove, hide, and show individual items", async () => {
|
|
78
|
+
resetMenuRegistry();
|
|
79
|
+
const menu = createMenu("tools", "Tools", { surface: "menubar" });
|
|
80
|
+
menu.addItem({ label: "A", action: "a" });
|
|
81
|
+
menu.addItem({ label: "B", action: "b" });
|
|
82
|
+
menu.hideItem("a");
|
|
83
|
+
|
|
84
|
+
let menus = await menuResult();
|
|
85
|
+
assert.deepEqual(menus[0].elements, [{ type: "button", id: "b", label: "B" }]);
|
|
86
|
+
|
|
87
|
+
menu.showItem("a");
|
|
88
|
+
menu.removeItem("b");
|
|
89
|
+
menus = await menuResult();
|
|
90
|
+
|
|
91
|
+
assert.deepEqual(menus[0].elements, [{ type: "button", id: "a", label: "A" }]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("registered actions report presence and ignore blank ids", async () => {
|
|
95
|
+
resetMenuRegistry();
|
|
96
|
+
registerAction(" save ", () => "saved");
|
|
97
|
+
|
|
98
|
+
assert.equal(hasRegisteredAction("save"), true);
|
|
99
|
+
assert.equal(hasRegisteredAction(""), false);
|
|
100
|
+
assert.equal(await runRegisteredAction("save"), "saved");
|
|
101
|
+
assert.equal(await runRegisteredAction("missing"), null);
|
|
102
|
+
assert.equal(await runRegisteredAction(""), null);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("removeMenu deletes menu and order entry", async () => {
|
|
106
|
+
resetMenuRegistry();
|
|
107
|
+
createMenu("gone", "Gone", { surface: "menubar" });
|
|
108
|
+
createMenu("stay", "Stay", { surface: "menubar" });
|
|
109
|
+
removeMenu("gone");
|
|
110
|
+
hideMenu("missing");
|
|
111
|
+
showMenu("missing");
|
|
112
|
+
|
|
113
|
+
assert.equal(getMenu("gone"), null);
|
|
114
|
+
assert.deepEqual((await menuResult()).map((menu) => menu.id), ["stay"]);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("invoke and notify use OpenVCS host helper when available", async () => {
|
|
118
|
+
const previous = globalThis.OpenVCS;
|
|
119
|
+
const calls = [];
|
|
120
|
+
globalThis.OpenVCS = {
|
|
121
|
+
async invoke(cmd, args) {
|
|
122
|
+
calls.push({ cmd, args });
|
|
123
|
+
return "ok";
|
|
124
|
+
},
|
|
125
|
+
notify(msg) {
|
|
126
|
+
calls.push({ msg });
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
try {
|
|
130
|
+
assert.equal(await invoke("repo.open", { path: "/tmp" }), "ok");
|
|
131
|
+
notify("done");
|
|
132
|
+
assert.deepEqual(calls, [{ cmd: "repo.open", args: { path: "/tmp" } }, { msg: "done" }]);
|
|
133
|
+
} finally {
|
|
134
|
+
globalThis.OpenVCS = previous;
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("invoke and notify fail when OpenVCS host helper is missing", async () => {
|
|
139
|
+
const previous = globalThis.OpenVCS;
|
|
140
|
+
delete globalThis.OpenVCS;
|
|
141
|
+
try {
|
|
142
|
+
await assert.rejects(() => invoke("missing"), /host is not available/);
|
|
143
|
+
assert.throws(() => notify("missing"), /host is not available/);
|
|
144
|
+
} finally {
|
|
145
|
+
globalThis.OpenVCS = previous;
|
|
146
|
+
}
|
|
147
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const assert = require("node:assert/strict");
|
|
2
|
+
const test = require("node:test");
|
|
3
|
+
|
|
4
|
+
const { ModalBuilder } = require("../lib/runtime");
|
|
5
|
+
|
|
6
|
+
test("ModalBuilder serializes all supported content item types", async () => {
|
|
7
|
+
const modal = new ModalBuilder(" Settings ")
|
|
8
|
+
.text("Hello", { title: "Greeting", align: "center" })
|
|
9
|
+
.separator()
|
|
10
|
+
.button(" save ", "Save", { title: "Persist", variant: "primary", align: "end", payload: { ok: true } })
|
|
11
|
+
.input(" name ", " Name ", { kind: "text", value: "OpenVCS", placeholder: "Name", required: true, align: "start" })
|
|
12
|
+
.select("theme", "Theme", { value: "dark", align: "center", options: [{ value: "dark", label: "Dark" }] })
|
|
13
|
+
.list("files", { label: "Files", emptyText: "None", align: "start", items: [{ id: "a", label: "A" }] })
|
|
14
|
+
.build();
|
|
15
|
+
|
|
16
|
+
assert.equal(modal.title, "Settings");
|
|
17
|
+
assert.deepEqual(modal.content.map((item) => item.type), [
|
|
18
|
+
"text",
|
|
19
|
+
"separator",
|
|
20
|
+
"button",
|
|
21
|
+
"input",
|
|
22
|
+
"select",
|
|
23
|
+
"list",
|
|
24
|
+
]);
|
|
25
|
+
assert.deepEqual(modal.content[2].payload, { ok: true });
|
|
26
|
+
assert.equal(modal.content[3].required, true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("ModalBuilder omits optional fields when not provided", () => {
|
|
30
|
+
const modal = new ModalBuilder(" ")
|
|
31
|
+
.text("Plain")
|
|
32
|
+
.button(" save ", "Save")
|
|
33
|
+
.input(" name ", " Name ")
|
|
34
|
+
.select("theme", "Theme", { options: [] })
|
|
35
|
+
.list("files", { items: [] })
|
|
36
|
+
.horizontalBox([{ type: "text", content: "child" }])
|
|
37
|
+
.verticalBox([{ type: "text", content: "child" }])
|
|
38
|
+
.grid([{ type: "text", content: "child" }], { columns: " 1fr " })
|
|
39
|
+
.build();
|
|
40
|
+
|
|
41
|
+
assert.equal(modal.title, "");
|
|
42
|
+
assert.equal(modal.content[0].title, undefined);
|
|
43
|
+
assert.equal(modal.content[1].variant, undefined);
|
|
44
|
+
assert.equal(modal.content[2].kind, undefined);
|
|
45
|
+
assert.equal(modal.content[3].value, undefined);
|
|
46
|
+
assert.equal(modal.content[4].label, undefined);
|
|
47
|
+
assert.equal(modal.content[5].gap, undefined);
|
|
48
|
+
assert.equal(modal.content[6].gap, undefined);
|
|
49
|
+
assert.equal(modal.content[7].gap, undefined);
|
|
50
|
+
assert.equal(modal.content[7].columns, "1fr");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("ModalBuilder clones nested container content before returning definitions", () => {
|
|
54
|
+
const nested = [{ type: "text", content: "inside" }];
|
|
55
|
+
const builder = new ModalBuilder("Nested")
|
|
56
|
+
.horizontalBox(nested, { gap: "4px", align: "center", wrap: true })
|
|
57
|
+
.verticalBox(nested, { gap: "8px" })
|
|
58
|
+
.grid(nested, { columns: "1fr 1fr", gap: "2px" });
|
|
59
|
+
|
|
60
|
+
const first = builder.build();
|
|
61
|
+
first.content[0].content[0].content = "mutated";
|
|
62
|
+
nested[0].content = "source mutated";
|
|
63
|
+
const second = builder.build();
|
|
64
|
+
|
|
65
|
+
assert.equal(second.content[0].content[0].content, "inside");
|
|
66
|
+
assert.equal(second.content[1].content[0].content, "inside");
|
|
67
|
+
assert.equal(second.content[2].columns, "1fr 1fr");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("ModalBuilder.open returns the same payload shape as build", async () => {
|
|
71
|
+
const builder = new ModalBuilder("Open").text("Body");
|
|
72
|
+
|
|
73
|
+
assert.deepEqual(await builder.open(), builder.build());
|
|
74
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const assert = require("node:assert/strict");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const test = require("node:test");
|
|
4
|
+
|
|
5
|
+
const { npmArgsPrefix, npmCommand, npmExecutable, resolveNpmCli } = require("../lib/npm-runner");
|
|
6
|
+
|
|
7
|
+
test("npmCommand uses npm directly on non-Windows platforms", () => {
|
|
8
|
+
const command = npmCommand("linux", "/usr/bin/node");
|
|
9
|
+
|
|
10
|
+
assert.deepEqual(command, { program: "npm", argsPrefix: [] });
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("npmExecutable and npmArgsPrefix expose the default npm command", () => {
|
|
14
|
+
if (process.platform === "win32") {
|
|
15
|
+
assert.equal(npmExecutable(), process.execPath);
|
|
16
|
+
assert.ok(npmArgsPrefix().length > 0);
|
|
17
|
+
} else {
|
|
18
|
+
assert.equal(npmExecutable(), "npm");
|
|
19
|
+
assert.deepEqual(npmArgsPrefix(), []);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("npmCommand runs npm through node.exe on Windows without cmd.exe", () => {
|
|
24
|
+
const execPath = "C:\\Program Files\\nodejs\\node.exe";
|
|
25
|
+
const localCli = path.join(path.dirname(execPath), "node_modules", "npm", "bin", "npm-cli.js");
|
|
26
|
+
|
|
27
|
+
const command = npmCommand("win32", execPath, (candidate) => candidate === localCli);
|
|
28
|
+
|
|
29
|
+
assert.equal(command.program, execPath);
|
|
30
|
+
assert.deepEqual(command.argsPrefix, [localCli]);
|
|
31
|
+
assert.notEqual(command.program.toLowerCase(), "cmd.exe");
|
|
32
|
+
assert.notEqual(path.basename(command.argsPrefix[0]).toLowerCase(), "npm.cmd");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("resolveNpmCli falls back to require.resolve when local npm cli is unavailable", () => {
|
|
36
|
+
const resolved = resolveNpmCli(
|
|
37
|
+
"C:\\Program Files\\nodejs\\node.exe",
|
|
38
|
+
() => false,
|
|
39
|
+
(specifier) => `resolved:${specifier}`,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
assert.equal(resolved, "resolved:npm/bin/npm-cli.js");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("resolveNpmCli prefers npm cli beside node.exe before require.resolve", () => {
|
|
46
|
+
const execPath = "C:\\Tools With Spaces\\nodejs\\node.exe";
|
|
47
|
+
const localCli = path.join(path.dirname(execPath), "node_modules", "npm", "bin", "npm-cli.js");
|
|
48
|
+
let fallbackCalled = false;
|
|
49
|
+
|
|
50
|
+
const resolved = resolveNpmCli(
|
|
51
|
+
execPath,
|
|
52
|
+
(candidate) => candidate === localCli,
|
|
53
|
+
() => {
|
|
54
|
+
fallbackCalled = true;
|
|
55
|
+
return "fallback";
|
|
56
|
+
},
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
assert.equal(resolved, localCli);
|
|
60
|
+
assert.equal(fallbackCalled, false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("npmCommand keeps Windows paths unquoted because spawn receives argv array", () => {
|
|
64
|
+
const execPath = "C:\\Program Files\\nodejs\\node.exe";
|
|
65
|
+
const cliPath = "C:\\Program Files\\nodejs\\node_modules\\npm\\bin\\npm-cli.js";
|
|
66
|
+
const command = npmCommand("win32", execPath, () => false, () => cliPath);
|
|
67
|
+
|
|
68
|
+
assert.equal(command.program, execPath);
|
|
69
|
+
assert.equal(command.argsPrefix[0], cliPath);
|
|
70
|
+
assert.equal(command.program.startsWith('"'), false);
|
|
71
|
+
assert.equal(command.argsPrefix[0].startsWith('"'), false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("npmCommand returns a fresh argsPrefix array per Windows call", () => {
|
|
75
|
+
const execPath = "C:\\nodejs\\node.exe";
|
|
76
|
+
const cliPath = "C:\\nodejs\\node_modules\\npm\\bin\\npm-cli.js";
|
|
77
|
+
const first = npmCommand("win32", execPath, () => false, () => cliPath);
|
|
78
|
+
const second = npmCommand("win32", execPath, () => false, () => cliPath);
|
|
79
|
+
|
|
80
|
+
first.argsPrefix.push("mutated");
|
|
81
|
+
|
|
82
|
+
assert.deepEqual(second.argsPrefix, [cliPath]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("SDK build module no longer exposes shell-wrapper helper", () => {
|
|
86
|
+
const buildModule = require("../lib/build");
|
|
87
|
+
|
|
88
|
+
assert.equal(Object.hasOwn(buildModule, "shouldUseWindowsShell"), false);
|
|
89
|
+
});
|