@openvcs/sdk 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +39 -3
  2. package/lib/build.d.ts +8 -0
  3. package/lib/build.js +81 -2
  4. package/lib/dist.js +1 -0
  5. package/lib/init.d.ts +2 -0
  6. package/lib/init.js +7 -4
  7. package/lib/runtime/contracts.d.ts +45 -0
  8. package/lib/runtime/contracts.js +4 -0
  9. package/lib/runtime/dispatcher.d.ts +16 -0
  10. package/lib/runtime/dispatcher.js +133 -0
  11. package/lib/runtime/errors.d.ts +5 -0
  12. package/lib/runtime/errors.js +26 -0
  13. package/lib/runtime/factory.d.ts +3 -0
  14. package/lib/runtime/factory.js +153 -0
  15. package/lib/runtime/host.d.ts +10 -0
  16. package/lib/runtime/host.js +48 -0
  17. package/lib/runtime/index.d.ts +10 -0
  18. package/lib/runtime/index.js +23 -0
  19. package/lib/runtime/registration.d.ts +39 -0
  20. package/lib/runtime/registration.js +37 -0
  21. package/lib/runtime/transport.d.ts +14 -0
  22. package/lib/runtime/transport.js +72 -0
  23. package/lib/types/host.d.ts +57 -0
  24. package/lib/types/host.js +4 -0
  25. package/lib/types/index.d.ts +4 -0
  26. package/lib/types/index.js +22 -0
  27. package/lib/types/plugin.d.ts +56 -0
  28. package/lib/types/plugin.js +4 -0
  29. package/lib/types/protocol.d.ts +77 -0
  30. package/lib/types/protocol.js +13 -0
  31. package/lib/types/vcs.d.ts +459 -0
  32. package/lib/types/vcs.js +4 -0
  33. package/package.json +14 -1
  34. package/src/lib/build.ts +104 -2
  35. package/src/lib/dist.ts +2 -0
  36. package/src/lib/init.ts +7 -4
  37. package/src/lib/runtime/contracts.ts +52 -0
  38. package/src/lib/runtime/dispatcher.ts +185 -0
  39. package/src/lib/runtime/errors.ts +27 -0
  40. package/src/lib/runtime/factory.ts +182 -0
  41. package/src/lib/runtime/host.ts +72 -0
  42. package/src/lib/runtime/index.ts +36 -0
  43. package/src/lib/runtime/registration.ts +93 -0
  44. package/src/lib/runtime/transport.ts +93 -0
  45. package/src/lib/types/host.ts +71 -0
  46. package/src/lib/types/index.ts +7 -0
  47. package/src/lib/types/plugin.ts +110 -0
  48. package/src/lib/types/protocol.ts +97 -0
  49. package/src/lib/types/vcs.ts +579 -0
  50. package/test/build.test.js +147 -6
  51. package/test/cli.test.js +5 -3
  52. package/test/dist.test.js +27 -18
  53. package/test/init.test.js +29 -0
  54. package/test/runtime.test.js +235 -0
@@ -3,9 +3,35 @@ const fs = require("node:fs");
3
3
  const path = require("node:path");
4
4
  const test = require("node:test");
5
5
 
6
- const { buildPluginAssets, parseBuildArgs, readManifest, validateDeclaredModuleExec } = require("../lib/build");
6
+ const {
7
+ buildPluginAssets,
8
+ parseBuildArgs,
9
+ readManifest,
10
+ validateDeclaredModuleExec,
11
+ validateGeneratedBootstrapTargets,
12
+ } = require("../lib/build");
7
13
  const { cleanupTempDir, makeTempDir, writeJson, writeText } = require("./helpers");
8
14
 
15
+ test("renderGeneratedBootstrap creates ESM code", () => {
16
+ const output = require("../lib/build").renderGeneratedBootstrap("./plugin.js", true);
17
+ assert.match(output, /^#!/);
18
+ assert.match(output, /import \{ bootstrapPluginModule \}/);
19
+ assert.match(output, /import\('\.\/plugin\.js'\)/);
20
+ });
21
+
22
+ test("renderGeneratedBootstrap creates CJS code", () => {
23
+ const output = require("../lib/build").renderGeneratedBootstrap("./plugin.js", false);
24
+ assert.match(output, /^#!/);
25
+ assert.match(output, /require\('@openvcs\/sdk\/runtime'\)/);
26
+ assert.match(output, /require\('\.\/plugin\.js'\)/);
27
+ assert.match(output, /\(\s*async\s*\(\s*\)\s*=>/);
28
+ });
29
+
30
+ test("renderGeneratedBootstrap handles subdirectory import paths", () => {
31
+ const output = require("../lib/build").renderGeneratedBootstrap("./subdir/plugin.js", true);
32
+ assert.match(output, /import\('\.\/subdir\/plugin\.js'\)/);
33
+ });
34
+
9
35
  test("parseBuildArgs uses defaults", () => {
10
36
  const parsed = parseBuildArgs([]);
11
37
  assert.equal(parsed.pluginDir, process.cwd());
@@ -39,7 +65,7 @@ test("buildPluginAssets requires package.json for code plugins", () => {
39
65
 
40
66
  writeJson(path.join(pluginDir, "openvcs.plugin.json"), {
41
67
  id: "missing-package",
42
- module: { exec: "plugin.js" },
68
+ module: { exec: "openvcs-plugin.js" },
43
69
  });
44
70
 
45
71
  assert.throws(
@@ -56,7 +82,7 @@ test("buildPluginAssets runs build:plugin and validates output", () => {
56
82
 
57
83
  writeJson(path.join(pluginDir, "openvcs.plugin.json"), {
58
84
  id: "builder",
59
- module: { exec: "plugin.js" },
85
+ module: { exec: "openvcs-plugin.js" },
60
86
  });
61
87
  writeJson(path.join(pluginDir, "package.json"), {
62
88
  name: "builder",
@@ -74,6 +100,11 @@ test("buildPluginAssets runs build:plugin and validates output", () => {
74
100
 
75
101
  assert.equal(manifest.pluginId, "builder");
76
102
  assert.equal(fs.existsSync(path.join(pluginDir, "bin", "plugin.js")), true);
103
+ assert.equal(fs.existsSync(path.join(pluginDir, "bin", "openvcs-plugin.js")), true);
104
+ assert.match(
105
+ fs.readFileSync(path.join(pluginDir, "bin", "openvcs-plugin.js"), "utf8"),
106
+ /bootstrapPluginModule/
107
+ );
77
108
  cleanupTempDir(root);
78
109
  });
79
110
 
@@ -83,13 +114,123 @@ test("readManifest and validateDeclaredModuleExec stay reusable", () => {
83
114
 
84
115
  writeJson(path.join(pluginDir, "openvcs.plugin.json"), {
85
116
  id: "reusable",
86
- module: { exec: "plugin.js" },
117
+ module: { exec: "openvcs-plugin.js" },
87
118
  });
88
- writeText(path.join(pluginDir, "bin", "plugin.js"), "export {};\n");
119
+ writeText(path.join(pluginDir, "bin", "plugin.js"), "export function OnPluginStart() {}\n");
120
+ writeText(path.join(pluginDir, "bin", "openvcs-plugin.js"), "export {};\n");
89
121
 
90
122
  const manifest = readManifest(pluginDir);
91
- assert.equal(manifest.moduleExec, "plugin.js");
123
+ assert.equal(manifest.moduleExec, "openvcs-plugin.js");
124
+ assert.doesNotThrow(() => validateGeneratedBootstrapTargets(pluginDir, manifest.moduleExec));
92
125
  assert.doesNotThrow(() => validateDeclaredModuleExec(pluginDir, manifest.moduleExec));
93
126
 
94
127
  cleanupTempDir(root);
95
128
  });
129
+
130
+ test("validateGeneratedBootstrapTargets rejects module.exec collisions", () => {
131
+ const root = makeTempDir("openvcs-sdk-test");
132
+ const pluginDir = path.join(root, "plugin");
133
+
134
+ writeText(path.join(pluginDir, "bin", "plugin.js"), "export function OnPluginStart() {}\n");
135
+
136
+ assert.throws(
137
+ () => validateGeneratedBootstrapTargets(pluginDir, "plugin.js"),
138
+ /must not be plugin\.js/
139
+ );
140
+
141
+ cleanupTempDir(root);
142
+ });
143
+
144
+ test("validateGeneratedBootstrapTargets rejects case-insensitive collisions", () => {
145
+ const root = makeTempDir("openvcs-sdk-test");
146
+ const pluginDir = path.join(root, "plugin");
147
+
148
+ writeText(path.join(pluginDir, "bin", "plugin.js"), "export function OnPluginStart() {}\n");
149
+
150
+ assert.throws(
151
+ () => validateGeneratedBootstrapTargets(pluginDir, "Plugin.js"),
152
+ /must not be plugin\.js/
153
+ );
154
+ assert.throws(
155
+ () => validateGeneratedBootstrapTargets(pluginDir, "PLUGIN.JS"),
156
+ /must not be plugin\.js/
157
+ );
158
+
159
+ cleanupTempDir(root);
160
+ });
161
+
162
+ test("generateModuleBootstrap handles subdirectory module.exec paths", () => {
163
+ const root = makeTempDir("openvcs-sdk-test");
164
+ const pluginDir = path.join(root, "plugin");
165
+
166
+ writeJson(path.join(pluginDir, "openvcs.plugin.json"), {
167
+ id: "subdir-plugin",
168
+ module: { exec: "subdir/openvcs-plugin.js" },
169
+ });
170
+ writeJson(path.join(pluginDir, "package.json"), {
171
+ name: "subdir-plugin",
172
+ type: "module",
173
+ private: true,
174
+ scripts: { "build:plugin": "node ./scripts/build.js" },
175
+ });
176
+ writeText(path.join(pluginDir, "bin", "plugin.js"), "export function OnPluginStart() {}\n");
177
+ writeText(path.join(pluginDir, "bin", "subdir", "openvcs-plugin.js"), "export {};\n");
178
+
179
+ const { generateModuleBootstrap } = require("../lib/build");
180
+ generateModuleBootstrap(pluginDir, "subdir/openvcs-plugin.js");
181
+
182
+ const bootstrapContent = fs.readFileSync(
183
+ path.join(pluginDir, "bin", "subdir", "openvcs-plugin.js"),
184
+ "utf8"
185
+ );
186
+ assert.match(bootstrapContent, /\.\.\/plugin\.js/);
187
+ cleanupTempDir(root);
188
+ });
189
+
190
+ test("detectEsmMode returns true for package.json type: module", () => {
191
+ const root = makeTempDir("openvcs-sdk-test");
192
+ const pluginDir = path.join(root, "plugin");
193
+
194
+ writeJson(path.join(pluginDir, "openvcs.plugin.json"), {
195
+ id: "esm-plugin",
196
+ module: { exec: "bootstrap.js" },
197
+ });
198
+ writeJson(path.join(pluginDir, "package.json"), {
199
+ name: "esm-plugin",
200
+ type: "module",
201
+ });
202
+ writeText(path.join(pluginDir, "bin", "plugin.js"), "export function OnPluginStart() {}\n");
203
+ writeText(path.join(pluginDir, "bin", "bootstrap.js"), "export {};\n");
204
+
205
+ const { generateModuleBootstrap } = require("../lib/build");
206
+ generateModuleBootstrap(pluginDir, "bootstrap.js");
207
+
208
+ const bootstrapContent = fs.readFileSync(path.join(pluginDir, "bin", "bootstrap.js"), "utf8");
209
+ assert.match(bootstrapContent, /^#!/);
210
+ assert.match(bootstrapContent, /^import\s*{/m);
211
+ cleanupTempDir(root);
212
+ });
213
+
214
+ test("detectEsmMode returns false for package.json type: commonjs", () => {
215
+ const root = makeTempDir("openvcs-sdk-test");
216
+ const pluginDir = path.join(root, "plugin");
217
+
218
+ writeJson(path.join(pluginDir, "openvcs.plugin.json"), {
219
+ id: "cjs-plugin",
220
+ module: { exec: "bootstrap.js" },
221
+ });
222
+ writeJson(path.join(pluginDir, "package.json"), {
223
+ name: "cjs-plugin",
224
+ type: "commonjs",
225
+ });
226
+ writeText(path.join(pluginDir, "bin", "plugin.js"), "export function OnPluginStart() {}\n");
227
+ writeText(path.join(pluginDir, "bin", "bootstrap.js"), "export {};\n");
228
+
229
+ const { generateModuleBootstrap } = require("../lib/build");
230
+ generateModuleBootstrap(pluginDir, "bootstrap.js");
231
+
232
+ const bootstrapContent = fs.readFileSync(path.join(pluginDir, "bin", "bootstrap.js"), "utf8");
233
+ assert.match(bootstrapContent, /require\(/);
234
+ assert.match(bootstrapContent, /\(\s*async\s*\(\s*\)\s*=>/);
235
+ cleanupTempDir(root);
236
+ });
package/test/cli.test.js CHANGED
@@ -67,9 +67,10 @@ test("openvcs dist command creates bundle", () => {
67
67
 
68
68
  writeJson(path.join(pluginDir, "openvcs.plugin.json"), {
69
69
  id: "cli-plugin",
70
- module: { exec: "plugin.js" },
70
+ module: { exec: "openvcs-plugin.js" },
71
71
  });
72
- writeText(path.join(pluginDir, "bin", "plugin.js"), "export {};\n");
72
+ writeText(path.join(pluginDir, "bin", "plugin.js"), "export function OnPluginStart() {}\n");
73
+ writeText(path.join(pluginDir, "bin", "openvcs-plugin.js"), "export {};\n");
73
74
 
74
75
  const result = runCli([
75
76
  "dist",
@@ -100,7 +101,7 @@ test("openvcs build command builds code plugin assets", () => {
100
101
 
101
102
  writeJson(path.join(pluginDir, "openvcs.plugin.json"), {
102
103
  id: "build-plugin",
103
- module: { exec: "plugin.js" },
104
+ module: { exec: "openvcs-plugin.js" },
104
105
  });
105
106
  writeJson(path.join(pluginDir, "package.json"), {
106
107
  name: "build-plugin",
@@ -119,6 +120,7 @@ test("openvcs build command builds code plugin assets", () => {
119
120
  assert.equal(result.status, 0);
120
121
  assert.equal(result.stdout.trim(), "build-plugin");
121
122
  assert.equal(fs.existsSync(path.join(pluginDir, "bin", "plugin.js")), true);
123
+ assert.equal(fs.existsSync(path.join(pluginDir, "bin", "openvcs-plugin.js")), true);
122
124
 
123
125
  cleanupTempDir(root);
124
126
  });
package/test/dist.test.js CHANGED
@@ -108,7 +108,7 @@ test("readManifest rejects id with path separators", () => {
108
108
  test("validateDeclaredModuleExec accepts .js/.mjs/.cjs", () => {
109
109
  const root = makeTempDir("openvcs-sdk-test");
110
110
  const pluginDir = path.join(root, "plugin");
111
- writeText(path.join(pluginDir, "bin", "plugin.js"), "export {};\n");
111
+ writeText(path.join(pluginDir, "bin", "plugin.js"), "export function OnPluginStart() {}\n");
112
112
  writeText(path.join(pluginDir, "bin", "plugin.mjs"), "export {};\n");
113
113
  writeText(path.join(pluginDir, "bin", "plugin.cjs"), "module.exports = {};\n");
114
114
 
@@ -133,7 +133,7 @@ test("validateDeclaredModuleExec rejects non-node extension", () => {
133
133
  test("validateDeclaredModuleExec rejects absolute path", () => {
134
134
  const root = makeTempDir("openvcs-sdk-test");
135
135
  const pluginDir = path.join(root, "plugin");
136
- writeText(path.join(pluginDir, "bin", "plugin.js"), "export {};\n");
136
+ writeText(path.join(pluginDir, "bin", "plugin.js"), "export function OnPluginStart() {}\n");
137
137
  assert.throws(
138
138
  () => __private.validateDeclaredModuleExec(pluginDir, path.resolve(pluginDir, "bin", "plugin.js")),
139
139
  /must be a relative path under bin/
@@ -279,6 +279,7 @@ test("bundlePlugin trims module.exec and includes extra bin files", async () =>
279
279
  id: "x",
280
280
  module: { exec: " module.mjs " },
281
281
  });
282
+ writeText(path.join(pluginDir, "bin", "plugin.js"), "export function OnPluginStart() {}\n");
282
283
  writeText(path.join(pluginDir, "bin", "module.mjs"), "export {};\n");
283
284
  writeText(path.join(pluginDir, "bin", "helpers", "util.mjs"), "export const x = 1;\n");
284
285
 
@@ -291,6 +292,7 @@ test("bundlePlugin trims module.exec and includes extra bin files", async () =>
291
292
  });
292
293
  const entries = await readBundleEntries(outPath);
293
294
 
295
+ assert.equal(entries.has("x/bin/plugin.js"), true);
294
296
  assert.equal(entries.has("x/bin/module.mjs"), true);
295
297
  assert.equal(entries.has("x/bin/helpers/util.mjs"), true);
296
298
  cleanupTempDir(root);
@@ -303,7 +305,7 @@ test("bundlePlugin builds code plugins before packaging", async () => {
303
305
 
304
306
  writeJson(path.join(pluginDir, "openvcs.plugin.json"), {
305
307
  id: "builder",
306
- module: { exec: "plugin.js" },
308
+ module: { exec: "openvcs-plugin.js" },
307
309
  });
308
310
  writeJson(path.join(pluginDir, "package.json"), {
309
311
  name: "builder",
@@ -327,6 +329,7 @@ test("bundlePlugin builds code plugins before packaging", async () => {
327
329
  const entries = await readBundleEntries(outPath);
328
330
 
329
331
  assert.equal(entries.has("builder/bin/plugin.js"), true);
332
+ assert.equal(entries.has("builder/bin/openvcs-plugin.js"), true);
330
333
  cleanupTempDir(root);
331
334
  });
332
335
 
@@ -337,12 +340,12 @@ test("bundlePlugin with no-build requires prebuilt module entrypoint", async ()
337
340
 
338
341
  writeJson(path.join(pluginDir, "openvcs.plugin.json"), {
339
342
  id: "prebuilt",
340
- module: { exec: "plugin.js" },
343
+ module: { exec: "openvcs-plugin.js" },
341
344
  });
342
345
 
343
346
  await assert.rejects(
344
347
  () => bundlePlugin({ pluginDir, outDir, verbose: false, noBuild: true, noNpmDeps: true }),
345
- /module entrypoint not found/
348
+ /compiled plugin module not found/
346
349
  );
347
350
 
348
351
  cleanupTempDir(root);
@@ -355,7 +358,7 @@ test("bundlePlugin errors when code plugin lacks build:plugin", async () => {
355
358
 
356
359
  writeJson(path.join(pluginDir, "openvcs.plugin.json"), {
357
360
  id: "missing-script",
358
- module: { exec: "plugin.js" },
361
+ module: { exec: "openvcs-plugin.js" },
359
362
  });
360
363
  writeJson(path.join(pluginDir, "package.json"), {
361
364
  name: "missing-script",
@@ -416,9 +419,10 @@ test("bundlePlugin rejects plugin id with path separators", async () => {
416
419
 
417
420
  writeJson(path.join(pluginDir, "openvcs.plugin.json"), {
418
421
  id: "bad/id",
419
- module: { exec: "plugin.js" },
422
+ module: { exec: "openvcs-plugin.js" },
420
423
  });
421
- writeText(path.join(pluginDir, "bin", "plugin.js"), "export {};\n");
424
+ writeText(path.join(pluginDir, "bin", "plugin.js"), "export function OnPluginStart() {}\n");
425
+ writeText(path.join(pluginDir, "bin", "openvcs-plugin.js"), "export {};\n");
422
426
 
423
427
  await assert.rejects(
424
428
  () => bundlePlugin({ pluginDir, outDir, verbose: false, noBuild: true, noNpmDeps: true }),
@@ -439,9 +443,10 @@ test("bundlePlugin rejects symlink in bin", async () => {
439
443
 
440
444
  writeJson(path.join(pluginDir, "openvcs.plugin.json"), {
441
445
  id: "x",
442
- module: { exec: "plugin.js" },
446
+ module: { exec: "openvcs-plugin.js" },
443
447
  });
444
- writeText(path.join(pluginDir, "bin", "plugin.js"), "export {};\n");
448
+ writeText(path.join(pluginDir, "bin", "plugin.js"), "export function OnPluginStart() {}\n");
449
+ writeText(path.join(pluginDir, "bin", "openvcs-plugin.js"), "export {};\n");
445
450
  writeText(path.join(pluginDir, "bin", "target.js"), "export {};\n");
446
451
  fs.symlinkSync(path.join(pluginDir, "bin", "target.js"), path.join(pluginDir, "bin", "link.js"));
447
452
 
@@ -460,9 +465,10 @@ test("bundlePlugin output archive keeps plugin-id root directory", async () => {
460
465
 
461
466
  writeJson(path.join(pluginDir, "openvcs.plugin.json"), {
462
467
  id: "root-check",
463
- module: { exec: "plugin.js" },
468
+ module: { exec: "openvcs-plugin.js" },
464
469
  });
465
- writeText(path.join(pluginDir, "bin", "plugin.js"), "export {};\n");
470
+ writeText(path.join(pluginDir, "bin", "plugin.js"), "export function OnPluginStart() {}\n");
471
+ writeText(path.join(pluginDir, "bin", "openvcs-plugin.js"), "export {};\n");
466
472
 
467
473
  const outPath = await bundlePlugin({
468
474
  pluginDir,
@@ -487,9 +493,10 @@ test("bundlePlugin overwrites existing bundle file", async () => {
487
493
 
488
494
  writeJson(path.join(pluginDir, "openvcs.plugin.json"), {
489
495
  id: "replace",
490
- module: { exec: "plugin.js" },
496
+ module: { exec: "openvcs-plugin.js" },
491
497
  });
492
- writeText(path.join(pluginDir, "bin", "plugin.js"), "export {};\n");
498
+ writeText(path.join(pluginDir, "bin", "plugin.js"), "export function OnPluginStart() {}\n");
499
+ writeText(path.join(pluginDir, "bin", "openvcs-plugin.js"), "export {};\n");
493
500
 
494
501
  const existingPath = path.join(outDir, "replace.ovcsp");
495
502
  writeText(existingPath, "not-a-tar");
@@ -514,9 +521,10 @@ test("bundlePlugin generates package-lock in staging, not pluginDir", async () =
514
521
 
515
522
  writeJson(path.join(pluginDir, "openvcs.plugin.json"), {
516
523
  id: "npm-plugin",
517
- module: { exec: "plugin.js" },
524
+ module: { exec: "openvcs-plugin.js" },
518
525
  });
519
- writeText(path.join(pluginDir, "bin", "plugin.js"), "export {};\n");
526
+ writeText(path.join(pluginDir, "bin", "plugin.js"), "export function OnPluginStart() {}\n");
527
+ writeText(path.join(pluginDir, "bin", "openvcs-plugin.js"), "export {};\n");
520
528
  writeJson(path.join(pluginDir, "package.json"), {
521
529
  name: "npm-plugin",
522
530
  version: "0.1.0",
@@ -546,9 +554,10 @@ test("bundlePlugin with --no-npm-deps does not generate lockfile", async () => {
546
554
 
547
555
  writeJson(path.join(pluginDir, "openvcs.plugin.json"), {
548
556
  id: "no-npm",
549
- module: { exec: "plugin.js" },
557
+ module: { exec: "openvcs-plugin.js" },
550
558
  });
551
- writeText(path.join(pluginDir, "bin", "plugin.js"), "export {};\n");
559
+ writeText(path.join(pluginDir, "bin", "plugin.js"), "export function OnPluginStart() {}\n");
560
+ writeText(path.join(pluginDir, "bin", "openvcs-plugin.js"), "export {};\n");
552
561
  writeJson(path.join(pluginDir, "package.json"), {
553
562
  name: "no-npm",
554
563
  version: "0.1.0",
package/test/init.test.js CHANGED
@@ -1,8 +1,10 @@
1
1
  const assert = require("node:assert/strict");
2
+ const fs = require("node:fs");
2
3
  const test = require("node:test");
3
4
  const path = require("node:path");
4
5
 
5
6
  const { __private } = require("../lib/init");
7
+ const { cleanupTempDir, makeTempDir } = require("./helpers");
6
8
 
7
9
  test("validatePluginId accepts regular ids", () => {
8
10
  assert.equal(__private.validatePluginId("my.plugin"), undefined);
@@ -63,3 +65,30 @@ test("collectAnswers re-prompts invalid plugin id", async () => {
63
65
  assert.equal(answers.runNpmInstall, false);
64
66
  assert.equal(messages.some((message) => message.includes("must not contain path separators")), true);
65
67
  });
68
+
69
+ test("writeModuleTemplate scaffolds SDK runtime entrypoint", () => {
70
+ const root = makeTempDir("openvcs-sdk-test");
71
+ const targetDir = path.join(root, "plugin");
72
+
73
+ __private.writeModuleTemplate({
74
+ targetDir,
75
+ kind: "module",
76
+ pluginId: "example.plugin",
77
+ pluginName: "Example Plugin",
78
+ pluginVersion: "0.1.0",
79
+ defaultEnabled: true,
80
+ runNpmInstall: false,
81
+ });
82
+
83
+ const pluginSource = fs.readFileSync(path.join(targetDir, "src", "plugin.ts"), "utf8");
84
+ const manifest = JSON.parse(
85
+ fs.readFileSync(path.join(targetDir, "openvcs.plugin.json"), "utf8")
86
+ );
87
+
88
+ assert.equal(manifest.module.exec, "openvcs-plugin.js");
89
+ assert.match(pluginSource, /OnPluginStart/);
90
+ assert.match(pluginSource, /PluginDefinition/);
91
+ assert.match(pluginSource, /context\.host\.info\('OpenVCS plugin started'\)/);
92
+
93
+ cleanupTempDir(root);
94
+ });
@@ -0,0 +1,235 @@
1
+ const assert = require("node:assert/strict");
2
+ const { EventEmitter } = require("node:events");
3
+ const test = require("node:test");
4
+
5
+ const {
6
+ bootstrapPluginModule,
7
+ createPluginRuntime,
8
+ } = require("../lib/runtime");
9
+ const {
10
+ parseFramedMessages,
11
+ serializeFramedMessage,
12
+ } = require("../lib/runtime/transport");
13
+
14
+ function createRuntimeHarness(options) {
15
+ const stdin = new EventEmitter();
16
+ const chunks = [];
17
+ const stdout = {
18
+ write(chunk) {
19
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
20
+ return true;
21
+ },
22
+ };
23
+ const runtime = createPluginRuntime(options);
24
+ runtime.start({ stdin, stdout });
25
+
26
+ return {
27
+ async request(message) {
28
+ stdin.emit("data", serializeFramedMessage(message));
29
+ await new Promise((resolve) => setImmediate(resolve));
30
+ const parsed = parseFramedMessages(Buffer.concat(chunks));
31
+ chunks.length = 0;
32
+ return parsed.messages;
33
+ },
34
+ };
35
+ }
36
+
37
+ test("createPluginRuntime answers plugin.initialize with inferred capabilities", async () => {
38
+ const harness = createRuntimeHarness({});
39
+
40
+ const messages = await harness.request({
41
+ jsonrpc: "2.0",
42
+ id: 1,
43
+ method: "plugin.initialize",
44
+ params: {},
45
+ });
46
+
47
+ assert.deepEqual(messages, [
48
+ {
49
+ jsonrpc: "2.0",
50
+ id: 1,
51
+ result: {
52
+ protocol_version: 1,
53
+ implements: {
54
+ plugin: true,
55
+ vcs: false,
56
+ },
57
+ },
58
+ },
59
+ ]);
60
+ });
61
+
62
+ test("createPluginRuntime uses plugin defaults and host notifications", async () => {
63
+ const harness = createRuntimeHarness({
64
+ plugin: {
65
+ async "plugin.init"(_params, context) {
66
+ context.host.info("ready");
67
+ return null;
68
+ },
69
+ },
70
+ });
71
+
72
+ const messages = await harness.request({
73
+ jsonrpc: "2.0",
74
+ id: 2,
75
+ method: "plugin.init",
76
+ params: {},
77
+ });
78
+
79
+ assert.deepEqual(messages, [
80
+ {
81
+ jsonrpc: "2.0",
82
+ method: "host.log",
83
+ params: {
84
+ level: "info",
85
+ target: "openvcs.plugin",
86
+ message: "ready",
87
+ },
88
+ },
89
+ {
90
+ jsonrpc: "2.0",
91
+ id: 2,
92
+ result: null,
93
+ },
94
+ ]);
95
+ });
96
+
97
+ test("createPluginRuntime reports missing methods as plugin failures", async () => {
98
+ const harness = createRuntimeHarness({});
99
+
100
+ const messages = await harness.request({
101
+ jsonrpc: "2.0",
102
+ id: 3,
103
+ method: "vcs.get_caps",
104
+ params: {},
105
+ });
106
+
107
+ assert.deepEqual(messages, [
108
+ {
109
+ jsonrpc: "2.0",
110
+ id: 3,
111
+ error: {
112
+ code: -32001,
113
+ message: "method 'vcs.get_caps' is not implemented",
114
+ data: {
115
+ code: "rpc-method-not-found",
116
+ message: "method 'vcs.get_caps' is not implemented",
117
+ },
118
+ },
119
+ },
120
+ ]);
121
+ });
122
+
123
+ test("bootstrapPluginModule runs OnPluginStart before starting runtime", async () => {
124
+ const stdin = new EventEmitter();
125
+ const chunks = [];
126
+ const stdout = {
127
+ write(chunk) {
128
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
129
+ return true;
130
+ },
131
+ };
132
+
133
+ await bootstrapPluginModule({
134
+ modulePath: "./plugin.js",
135
+ transport: { stdin, stdout },
136
+ async importPluginModule() {
137
+ return {
138
+ PluginDefinition: {
139
+ plugin: {
140
+ async "plugin.init"(_params, context) {
141
+ context.host.info("booted");
142
+ return null;
143
+ },
144
+ },
145
+ vcs: {
146
+ async "vcs.get_caps"() {
147
+ return { commits: true };
148
+ },
149
+ },
150
+ },
151
+ OnPluginStart() {
152
+ },
153
+ };
154
+ },
155
+ });
156
+
157
+ stdin.emit(
158
+ "data",
159
+ serializeFramedMessage({
160
+ jsonrpc: "2.0",
161
+ id: 4,
162
+ method: "plugin.initialize",
163
+ params: {},
164
+ })
165
+ );
166
+ stdin.emit(
167
+ "data",
168
+ serializeFramedMessage({
169
+ jsonrpc: "2.0",
170
+ id: 5,
171
+ method: "plugin.init",
172
+ params: {},
173
+ })
174
+ );
175
+ await new Promise((resolve) => setImmediate(resolve));
176
+
177
+ const parsed = parseFramedMessages(Buffer.concat(chunks));
178
+ assert.deepEqual(parsed.messages, [
179
+ {
180
+ jsonrpc: "2.0",
181
+ id: 4,
182
+ result: {
183
+ protocol_version: 1,
184
+ implements: {
185
+ plugin: true,
186
+ vcs: true,
187
+ },
188
+ },
189
+ },
190
+ {
191
+ jsonrpc: "2.0",
192
+ method: "host.log",
193
+ params: {
194
+ level: "info",
195
+ target: "openvcs.plugin",
196
+ message: "booted",
197
+ },
198
+ },
199
+ {
200
+ jsonrpc: "2.0",
201
+ id: 5,
202
+ result: null,
203
+ },
204
+ ]);
205
+ });
206
+
207
+ test("bootstrapPluginModule rejects modules without OnPluginStart", async () => {
208
+ await assert.rejects(
209
+ () =>
210
+ bootstrapPluginModule({
211
+ modulePath: "./plugin.js",
212
+ async importPluginModule() {
213
+ return {};
214
+ },
215
+ }),
216
+ /must export OnPluginStart/
217
+ );
218
+ });
219
+
220
+ test("bootstrapPluginModule rejects when OnPluginStart throws", async () => {
221
+ await assert.rejects(
222
+ () =>
223
+ bootstrapPluginModule({
224
+ modulePath: "./plugin.js",
225
+ async importPluginModule() {
226
+ return {
227
+ OnPluginStart() {
228
+ throw new Error("startup failure");
229
+ },
230
+ };
231
+ },
232
+ }),
233
+ /plugin startup failed/
234
+ );
235
+ });