@openvcs/sdk 0.2.2 → 0.2.4

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 (51) hide show
  1. package/README.md +76 -7
  2. package/lib/build.d.ts +28 -0
  3. package/lib/build.js +188 -0
  4. package/lib/cli.js +21 -2
  5. package/lib/dist.d.ts +4 -7
  6. package/lib/dist.js +67 -113
  7. package/lib/init.d.ts +2 -0
  8. package/lib/init.js +13 -8
  9. package/lib/runtime/contracts.d.ts +45 -0
  10. package/lib/runtime/contracts.js +4 -0
  11. package/lib/runtime/dispatcher.d.ts +16 -0
  12. package/lib/runtime/dispatcher.js +133 -0
  13. package/lib/runtime/errors.d.ts +5 -0
  14. package/lib/runtime/errors.js +26 -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 +9 -0
  18. package/lib/runtime/index.js +166 -0
  19. package/lib/runtime/transport.d.ts +14 -0
  20. package/lib/runtime/transport.js +72 -0
  21. package/lib/types/host.d.ts +57 -0
  22. package/lib/types/host.js +4 -0
  23. package/lib/types/index.d.ts +4 -0
  24. package/lib/types/index.js +22 -0
  25. package/lib/types/plugin.d.ts +56 -0
  26. package/lib/types/plugin.js +4 -0
  27. package/lib/types/protocol.d.ts +77 -0
  28. package/lib/types/protocol.js +13 -0
  29. package/lib/types/vcs.d.ts +459 -0
  30. package/lib/types/vcs.js +4 -0
  31. package/package.json +16 -3
  32. package/src/lib/build.ts +229 -0
  33. package/src/lib/cli.ts +21 -2
  34. package/src/lib/dist.ts +76 -128
  35. package/src/lib/init.ts +13 -8
  36. package/src/lib/runtime/contracts.ts +52 -0
  37. package/src/lib/runtime/dispatcher.ts +185 -0
  38. package/src/lib/runtime/errors.ts +27 -0
  39. package/src/lib/runtime/host.ts +72 -0
  40. package/src/lib/runtime/index.ts +201 -0
  41. package/src/lib/runtime/transport.ts +93 -0
  42. package/src/lib/types/host.ts +71 -0
  43. package/src/lib/types/index.ts +7 -0
  44. package/src/lib/types/plugin.ts +110 -0
  45. package/src/lib/types/protocol.ts +97 -0
  46. package/src/lib/types/vcs.ts +579 -0
  47. package/test/build.test.js +95 -0
  48. package/test/cli.test.js +37 -0
  49. package/test/dist.test.js +239 -15
  50. package/test/init.test.js +25 -0
  51. package/test/runtime.test.js +118 -0
package/test/cli.test.js CHANGED
@@ -27,6 +27,7 @@ test("openvcs --help prints usage", () => {
27
27
  const result = runCli(["--help"]);
28
28
  assert.equal(result.status, 0);
29
29
  assert.match(result.stdout, /Usage: openvcs <command>/);
30
+ assert.match(result.stdout, /build \[args\]/);
30
31
  });
31
32
 
32
33
  test("openvcs with no args exits non-zero", () => {
@@ -41,6 +42,12 @@ test("openvcs dist --help prints dist usage", () => {
41
42
  assert.match(result.stdout, /openvcs dist \[args\]/);
42
43
  });
43
44
 
45
+ test("openvcs build --help prints build usage", () => {
46
+ const result = runCli(["build", "--help"]);
47
+ assert.equal(result.status, 0);
48
+ assert.match(result.stdout, /openvcs build \[args\]/);
49
+ });
50
+
44
51
  test("openvcs init --help prints init usage", () => {
45
52
  const result = runCli(["init", "--help"]);
46
53
  assert.equal(result.status, 0);
@@ -70,6 +77,7 @@ test("openvcs dist command creates bundle", () => {
70
77
  pluginDir,
71
78
  "--out",
72
79
  outDir,
80
+ "--no-build",
73
81
  "--no-npm-deps",
74
82
  ]);
75
83
 
@@ -85,3 +93,32 @@ test("openvcs dist reports argument errors", () => {
85
93
  assert.equal(result.status, 1);
86
94
  assert.match(result.stderr, /missing value for --plugin-dir/);
87
95
  });
96
+
97
+ test("openvcs build command builds code plugin assets", () => {
98
+ const root = makeTempDir("openvcs-sdk-test");
99
+ const pluginDir = path.join(root, "plugin");
100
+
101
+ writeJson(path.join(pluginDir, "openvcs.plugin.json"), {
102
+ id: "build-plugin",
103
+ module: { exec: "plugin.js" },
104
+ });
105
+ writeJson(path.join(pluginDir, "package.json"), {
106
+ name: "build-plugin",
107
+ private: true,
108
+ scripts: {
109
+ "build:plugin": "node ./scripts/build-plugin.js",
110
+ },
111
+ });
112
+ writeText(
113
+ path.join(pluginDir, "scripts", "build-plugin.js"),
114
+ "const fs = require('node:fs');\nconst path = require('node:path');\nconst out = path.join(process.cwd(), 'bin', 'plugin.js');\nfs.mkdirSync(path.dirname(out), { recursive: true });\nfs.writeFileSync(out, 'export {};\\n', 'utf8');\n"
115
+ );
116
+
117
+ const result = runCli(["build", "--plugin-dir", pluginDir]);
118
+
119
+ assert.equal(result.status, 0);
120
+ assert.equal(result.stdout.trim(), "build-plugin");
121
+ assert.equal(fs.existsSync(path.join(pluginDir, "bin", "plugin.js")), true);
122
+
123
+ cleanupTempDir(root);
124
+ });
package/test/dist.test.js CHANGED
@@ -26,11 +26,13 @@ test("parseDistArgs parses known flags", () => {
26
26
  "some/plugin",
27
27
  "--out",
28
28
  "some/out",
29
+ "--no-build",
29
30
  "--no-npm-deps",
30
31
  "--verbose",
31
32
  ]);
32
33
  assert.equal(parsed.pluginDir, path.resolve("some/plugin"));
33
34
  assert.equal(parsed.outDir, path.resolve("some/out"));
35
+ assert.equal(parsed.noBuild, true);
34
36
  assert.equal(parsed.noNpmDeps, true);
35
37
  assert.equal(parsed.verbose, true);
36
38
  });
@@ -237,7 +239,13 @@ test("bundlePlugin writes a gzip .ovcsp for themes-only plugin", async () => {
237
239
  writeText(path.join(pluginDir, "themes", "default", "theme.json"), '{"name":"test"}\n');
238
240
  writeText(path.join(pluginDir, "icon.png"), "icon-bytes");
239
241
 
240
- const outPath = await bundlePlugin({ pluginDir, outDir, verbose: false, noNpmDeps: true });
242
+ const outPath = await bundlePlugin({
243
+ pluginDir,
244
+ outDir,
245
+ verbose: false,
246
+ noBuild: true,
247
+ noNpmDeps: true,
248
+ });
241
249
  const entries = await readBundleEntries(outPath);
242
250
 
243
251
  assert.equal(path.basename(outPath), "ui-only.ovcsp");
@@ -248,15 +256,15 @@ test("bundlePlugin writes a gzip .ovcsp for themes-only plugin", async () => {
248
256
  cleanupTempDir(root);
249
257
  });
250
258
 
251
- test("bundlePlugin rejects manifest with no module and no themes", async () => {
259
+ test("bundlePlugin rejects manifest with no module, entry, or themes", async () => {
252
260
  const root = makeTempDir("openvcs-sdk-test");
253
261
  const pluginDir = path.join(root, "plugin");
254
262
  const outDir = path.join(root, "out");
255
263
  writeJson(path.join(pluginDir, "openvcs.plugin.json"), { id: "x" });
256
264
 
257
265
  await assert.rejects(
258
- () => bundlePlugin({ pluginDir, outDir, verbose: false, noNpmDeps: true }),
259
- /manifest has no module\.exec or themes\//
266
+ () => bundlePlugin({ pluginDir, outDir, verbose: false, noBuild: true, noNpmDeps: true }),
267
+ /manifest has no module\.exec, entry, or themes\//
260
268
  );
261
269
 
262
270
  cleanupTempDir(root);
@@ -274,7 +282,13 @@ test("bundlePlugin trims module.exec and includes extra bin files", async () =>
274
282
  writeText(path.join(pluginDir, "bin", "module.mjs"), "export {};\n");
275
283
  writeText(path.join(pluginDir, "bin", "helpers", "util.mjs"), "export const x = 1;\n");
276
284
 
277
- const outPath = await bundlePlugin({ pluginDir, outDir, verbose: false, noNpmDeps: true });
285
+ const outPath = await bundlePlugin({
286
+ pluginDir,
287
+ outDir,
288
+ verbose: false,
289
+ noBuild: true,
290
+ noNpmDeps: true,
291
+ });
278
292
  const entries = await readBundleEntries(outPath);
279
293
 
280
294
  assert.equal(entries.has("x/bin/module.mjs"), true);
@@ -282,6 +296,81 @@ test("bundlePlugin trims module.exec and includes extra bin files", async () =>
282
296
  cleanupTempDir(root);
283
297
  });
284
298
 
299
+ test("bundlePlugin builds code plugins before packaging", async () => {
300
+ const root = makeTempDir("openvcs-sdk-test");
301
+ const pluginDir = path.join(root, "plugin");
302
+ const outDir = path.join(root, "out");
303
+
304
+ writeJson(path.join(pluginDir, "openvcs.plugin.json"), {
305
+ id: "builder",
306
+ module: { exec: "plugin.js" },
307
+ });
308
+ writeJson(path.join(pluginDir, "package.json"), {
309
+ name: "builder",
310
+ private: true,
311
+ scripts: {
312
+ "build:plugin": "node ./scripts/build-plugin.js",
313
+ },
314
+ });
315
+ writeText(
316
+ path.join(pluginDir, "scripts", "build-plugin.js"),
317
+ "const fs = require('node:fs');\nconst path = require('node:path');\nconst out = path.join(process.cwd(), 'bin', 'plugin.js');\nfs.mkdirSync(path.dirname(out), { recursive: true });\nfs.writeFileSync(out, 'export {};\\n', 'utf8');\n"
318
+ );
319
+
320
+ const outPath = await bundlePlugin({
321
+ pluginDir,
322
+ outDir,
323
+ verbose: false,
324
+ noBuild: false,
325
+ noNpmDeps: true,
326
+ });
327
+ const entries = await readBundleEntries(outPath);
328
+
329
+ assert.equal(entries.has("builder/bin/plugin.js"), true);
330
+ cleanupTempDir(root);
331
+ });
332
+
333
+ test("bundlePlugin with no-build requires prebuilt module entrypoint", async () => {
334
+ const root = makeTempDir("openvcs-sdk-test");
335
+ const pluginDir = path.join(root, "plugin");
336
+ const outDir = path.join(root, "out");
337
+
338
+ writeJson(path.join(pluginDir, "openvcs.plugin.json"), {
339
+ id: "prebuilt",
340
+ module: { exec: "plugin.js" },
341
+ });
342
+
343
+ await assert.rejects(
344
+ () => bundlePlugin({ pluginDir, outDir, verbose: false, noBuild: true, noNpmDeps: true }),
345
+ /module entrypoint not found/
346
+ );
347
+
348
+ cleanupTempDir(root);
349
+ });
350
+
351
+ test("bundlePlugin errors when code plugin lacks build:plugin", async () => {
352
+ const root = makeTempDir("openvcs-sdk-test");
353
+ const pluginDir = path.join(root, "plugin");
354
+ const outDir = path.join(root, "out");
355
+
356
+ writeJson(path.join(pluginDir, "openvcs.plugin.json"), {
357
+ id: "missing-script",
358
+ module: { exec: "plugin.js" },
359
+ });
360
+ writeJson(path.join(pluginDir, "package.json"), {
361
+ name: "missing-script",
362
+ private: true,
363
+ scripts: {},
364
+ });
365
+
366
+ await assert.rejects(
367
+ () => bundlePlugin({ pluginDir, outDir, verbose: false, noBuild: false, noNpmDeps: true }),
368
+ /build:plugin/
369
+ );
370
+
371
+ cleanupTempDir(root);
372
+ });
373
+
285
374
  test("bundlePlugin rejects module.exec path traversal", async () => {
286
375
  const root = makeTempDir("openvcs-sdk-test");
287
376
  const pluginDir = path.join(root, "plugin");
@@ -294,7 +383,7 @@ test("bundlePlugin rejects module.exec path traversal", async () => {
294
383
  writeText(path.join(pluginDir, "secret.js"), "console.log('secret')\n");
295
384
 
296
385
  await assert.rejects(
297
- () => bundlePlugin({ pluginDir, outDir, verbose: false, noNpmDeps: true }),
386
+ () => bundlePlugin({ pluginDir, outDir, verbose: false, noBuild: true, noNpmDeps: true }),
298
387
  /module\.exec/
299
388
  );
300
389
 
@@ -313,7 +402,7 @@ test("bundlePlugin rejects non-node module.exec extension", async () => {
313
402
  writeText(path.join(pluginDir, "bin", "plugin.ts"), "export {};\n");
314
403
 
315
404
  await assert.rejects(
316
- () => bundlePlugin({ pluginDir, outDir, verbose: false, noNpmDeps: true }),
405
+ () => bundlePlugin({ pluginDir, outDir, verbose: false, noBuild: true, noNpmDeps: true }),
317
406
  /must end with .js\/.mjs\/.cjs/
318
407
  );
319
408
 
@@ -332,7 +421,7 @@ test("bundlePlugin rejects plugin id with path separators", async () => {
332
421
  writeText(path.join(pluginDir, "bin", "plugin.js"), "export {};\n");
333
422
 
334
423
  await assert.rejects(
335
- () => bundlePlugin({ pluginDir, outDir, verbose: false, noNpmDeps: true }),
424
+ () => bundlePlugin({ pluginDir, outDir, verbose: false, noBuild: true, noNpmDeps: true }),
336
425
  /must not contain path separators/
337
426
  );
338
427
 
@@ -357,7 +446,7 @@ test("bundlePlugin rejects symlink in bin", async () => {
357
446
  fs.symlinkSync(path.join(pluginDir, "bin", "target.js"), path.join(pluginDir, "bin", "link.js"));
358
447
 
359
448
  await assert.rejects(
360
- () => bundlePlugin({ pluginDir, outDir, verbose: false, noNpmDeps: true }),
449
+ () => bundlePlugin({ pluginDir, outDir, verbose: false, noBuild: true, noNpmDeps: true }),
361
450
  /symlink/
362
451
  );
363
452
 
@@ -375,7 +464,13 @@ test("bundlePlugin output archive keeps plugin-id root directory", async () => {
375
464
  });
376
465
  writeText(path.join(pluginDir, "bin", "plugin.js"), "export {};\n");
377
466
 
378
- const outPath = await bundlePlugin({ pluginDir, outDir, verbose: false, noNpmDeps: true });
467
+ const outPath = await bundlePlugin({
468
+ pluginDir,
469
+ outDir,
470
+ verbose: false,
471
+ noBuild: true,
472
+ noNpmDeps: true,
473
+ });
379
474
  const entries = await readBundleEntries(outPath);
380
475
  for (const key of entries.keys()) {
381
476
  assert.equal(key.startsWith("root-check/"), true);
@@ -399,14 +494,20 @@ test("bundlePlugin overwrites existing bundle file", async () => {
399
494
  const existingPath = path.join(outDir, "replace.ovcsp");
400
495
  writeText(existingPath, "not-a-tar");
401
496
 
402
- const outPath = await bundlePlugin({ pluginDir, outDir, verbose: false, noNpmDeps: true });
497
+ const outPath = await bundlePlugin({
498
+ pluginDir,
499
+ outDir,
500
+ verbose: false,
501
+ noBuild: true,
502
+ noNpmDeps: true,
503
+ });
403
504
  const entries = await readBundleEntries(outPath);
404
505
  assert.equal(entries.has("replace/openvcs.plugin.json"), true);
405
506
 
406
507
  cleanupTempDir(root);
407
508
  });
408
509
 
409
- test("bundlePlugin generates package-lock when package.json exists", async () => {
510
+ test("bundlePlugin generates package-lock in staging, not pluginDir", async () => {
410
511
  const root = makeTempDir("openvcs-sdk-test");
411
512
  const pluginDir = path.join(root, "plugin");
412
513
  const outDir = path.join(root, "out");
@@ -422,10 +523,16 @@ test("bundlePlugin generates package-lock when package.json exists", async () =>
422
523
  private: true,
423
524
  });
424
525
 
425
- const outPath = await bundlePlugin({ pluginDir, outDir, verbose: false, noNpmDeps: false });
526
+ const outPath = await bundlePlugin({
527
+ pluginDir,
528
+ outDir,
529
+ verbose: false,
530
+ noBuild: true,
531
+ noNpmDeps: false,
532
+ });
426
533
  const entries = await readBundleEntries(outPath);
427
534
 
428
- assert.equal(fs.existsSync(path.join(pluginDir, "package-lock.json")), true);
535
+ assert.equal(fs.existsSync(path.join(pluginDir, "package-lock.json")), false);
429
536
  assert.equal(entries.has("npm-plugin/package.json"), true);
430
537
  assert.equal(entries.has("npm-plugin/package-lock.json"), true);
431
538
 
@@ -448,9 +555,126 @@ test("bundlePlugin with --no-npm-deps does not generate lockfile", async () => {
448
555
  private: true,
449
556
  });
450
557
 
451
- await bundlePlugin({ pluginDir, outDir, verbose: false, noNpmDeps: true });
558
+ await bundlePlugin({ pluginDir, outDir, verbose: false, noBuild: true, noNpmDeps: true });
452
559
 
453
560
  assert.equal(fs.existsSync(path.join(pluginDir, "package-lock.json")), false);
454
561
 
455
562
  cleanupTempDir(root);
456
563
  });
564
+
565
+ test("bundlePlugin bundles manifest entry file with sibling assets", async () => {
566
+ const root = makeTempDir("openvcs-sdk-test");
567
+ const pluginDir = path.join(root, "plugin");
568
+ const outDir = path.join(root, "out");
569
+
570
+ writeJson(path.join(pluginDir, "openvcs.plugin.json"), {
571
+ id: "ui-plugin",
572
+ entry: "ui/index.html",
573
+ });
574
+ writeText(path.join(pluginDir, "ui", "index.html"), "<html></html>\n");
575
+ writeText(path.join(pluginDir, "ui", "app.js"), "console.log('ui');\n");
576
+ writeText(path.join(pluginDir, "ui", "styles.css"), "body {}\n");
577
+ writeText(path.join(pluginDir, "icon.png"), "icon-bytes");
578
+
579
+ const outPath = await bundlePlugin({
580
+ pluginDir,
581
+ outDir,
582
+ verbose: false,
583
+ noBuild: true,
584
+ noNpmDeps: true,
585
+ });
586
+ const entries = await readBundleEntries(outPath);
587
+
588
+ assert.equal(entries.has("ui-plugin/openvcs.plugin.json"), true);
589
+ assert.equal(entries.has("ui-plugin/ui/index.html"), true);
590
+ assert.equal(entries.has("ui-plugin/ui/app.js"), true);
591
+ assert.equal(entries.has("ui-plugin/ui/styles.css"), true);
592
+ assert.equal(entries.has("ui-plugin/icon.png"), true);
593
+
594
+ cleanupTempDir(root);
595
+ });
596
+
597
+ test("bundlePlugin bundles root-level entry with sibling assets", async () => {
598
+ const root = makeTempDir("openvcs-sdk-test");
599
+ const pluginDir = path.join(root, "plugin");
600
+ const outDir = path.join(root, "out");
601
+
602
+ writeJson(path.join(pluginDir, "openvcs.plugin.json"), {
603
+ id: "root-ui",
604
+ entry: "index.html",
605
+ });
606
+ writeText(path.join(pluginDir, "index.html"), "<html></html>\n");
607
+ writeText(path.join(pluginDir, "app.js"), "console.log('ui');\n");
608
+ writeText(path.join(pluginDir, "styles.css"), "body {}\n");
609
+
610
+ const outPath = await bundlePlugin({
611
+ pluginDir,
612
+ outDir,
613
+ verbose: false,
614
+ noBuild: true,
615
+ noNpmDeps: true,
616
+ });
617
+ const entries = await readBundleEntries(outPath);
618
+
619
+ assert.equal(entries.has("root-ui/openvcs.plugin.json"), true);
620
+ assert.equal(entries.has("root-ui/index.html"), true);
621
+ assert.equal(entries.has("root-ui/app.js"), true);
622
+ assert.equal(entries.has("root-ui/styles.css"), true);
623
+
624
+ cleanupTempDir(root);
625
+ });
626
+
627
+ test("bundlePlugin rejects manifest entry path traversal", async () => {
628
+ const root = makeTempDir("openvcs-sdk-test");
629
+ const pluginDir = path.join(root, "plugin");
630
+ const outDir = path.join(root, "out");
631
+
632
+ writeJson(path.join(pluginDir, "openvcs.plugin.json"), {
633
+ id: "bad-entry",
634
+ entry: "../secret.txt",
635
+ });
636
+ writeText(path.join(pluginDir, "secret.txt"), "secret");
637
+
638
+ await assert.rejects(
639
+ () => bundlePlugin({ pluginDir, outDir, verbose: false, noBuild: true, noNpmDeps: true }),
640
+ /manifest entry must point to a file under the plugin directory/
641
+ );
642
+
643
+ cleanupTempDir(root);
644
+ });
645
+
646
+ test("bundlePlugin rejects manifest entry absolute path", async () => {
647
+ const root = makeTempDir("openvcs-sdk-test");
648
+ const pluginDir = path.join(root, "plugin");
649
+ const outDir = path.join(root, "out");
650
+
651
+ writeJson(path.join(pluginDir, "openvcs.plugin.json"), {
652
+ id: "abs-entry",
653
+ entry: "/tmp/secret.txt",
654
+ });
655
+
656
+ await assert.rejects(
657
+ () => bundlePlugin({ pluginDir, outDir, verbose: false, noBuild: true, noNpmDeps: true }),
658
+ /manifest entry must be a relative path/
659
+ );
660
+
661
+ cleanupTempDir(root);
662
+ });
663
+
664
+ test("bundlePlugin rejects missing manifest entry file", async () => {
665
+ const root = makeTempDir("openvcs-sdk-test");
666
+ const pluginDir = path.join(root, "plugin");
667
+ const outDir = path.join(root, "out");
668
+
669
+ writeJson(path.join(pluginDir, "openvcs.plugin.json"), {
670
+ id: "missing-entry",
671
+ entry: "nonexistent.html",
672
+ });
673
+
674
+ await assert.rejects(
675
+ () => bundlePlugin({ pluginDir, outDir, verbose: false, noBuild: true, noNpmDeps: true }),
676
+ /manifest entry file not found/
677
+ );
678
+
679
+ cleanupTempDir(root);
680
+ });
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,26 @@ 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
+
85
+ assert.match(pluginSource, /createPluginRuntime/);
86
+ assert.match(pluginSource, /startPluginRuntime/);
87
+ assert.match(pluginSource, /context\.host\.info\('OpenVCS plugin started'\)/);
88
+
89
+ cleanupTempDir(root);
90
+ });
@@ -0,0 +1,118 @@
1
+ const assert = require("node:assert/strict");
2
+ const { EventEmitter } = require("node:events");
3
+ const test = require("node:test");
4
+
5
+ const { createPluginRuntime } = require("../lib/runtime");
6
+ const {
7
+ parseFramedMessages,
8
+ serializeFramedMessage,
9
+ } = require("../lib/runtime/transport");
10
+
11
+ function createRuntimeHarness(options) {
12
+ const stdin = new EventEmitter();
13
+ const chunks = [];
14
+ const stdout = {
15
+ write(chunk) {
16
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
17
+ return true;
18
+ },
19
+ };
20
+ const runtime = createPluginRuntime(options);
21
+ runtime.start({ stdin, stdout });
22
+
23
+ return {
24
+ async request(message) {
25
+ stdin.emit("data", serializeFramedMessage(message));
26
+ await new Promise((resolve) => setImmediate(resolve));
27
+ const parsed = parseFramedMessages(Buffer.concat(chunks));
28
+ chunks.length = 0;
29
+ return parsed.messages;
30
+ },
31
+ };
32
+ }
33
+
34
+ test("createPluginRuntime answers plugin.initialize with inferred capabilities", async () => {
35
+ const harness = createRuntimeHarness({});
36
+
37
+ const messages = await harness.request({
38
+ jsonrpc: "2.0",
39
+ id: 1,
40
+ method: "plugin.initialize",
41
+ params: {},
42
+ });
43
+
44
+ assert.deepEqual(messages, [
45
+ {
46
+ jsonrpc: "2.0",
47
+ id: 1,
48
+ result: {
49
+ protocol_version: 1,
50
+ implements: {
51
+ plugin: true,
52
+ vcs: false,
53
+ },
54
+ },
55
+ },
56
+ ]);
57
+ });
58
+
59
+ test("createPluginRuntime uses plugin defaults and host notifications", async () => {
60
+ const harness = createRuntimeHarness({
61
+ plugin: {
62
+ async "plugin.init"(_params, context) {
63
+ context.host.info("ready");
64
+ return null;
65
+ },
66
+ },
67
+ });
68
+
69
+ const messages = await harness.request({
70
+ jsonrpc: "2.0",
71
+ id: 2,
72
+ method: "plugin.init",
73
+ params: {},
74
+ });
75
+
76
+ assert.deepEqual(messages, [
77
+ {
78
+ jsonrpc: "2.0",
79
+ method: "host.log",
80
+ params: {
81
+ level: "info",
82
+ target: "openvcs.plugin",
83
+ message: "ready",
84
+ },
85
+ },
86
+ {
87
+ jsonrpc: "2.0",
88
+ id: 2,
89
+ result: null,
90
+ },
91
+ ]);
92
+ });
93
+
94
+ test("createPluginRuntime reports missing methods as plugin failures", async () => {
95
+ const harness = createRuntimeHarness({});
96
+
97
+ const messages = await harness.request({
98
+ jsonrpc: "2.0",
99
+ id: 3,
100
+ method: "vcs.get_caps",
101
+ params: {},
102
+ });
103
+
104
+ assert.deepEqual(messages, [
105
+ {
106
+ jsonrpc: "2.0",
107
+ id: 3,
108
+ error: {
109
+ code: -32001,
110
+ message: "method 'vcs.get_caps' is not implemented",
111
+ data: {
112
+ code: "rpc-method-not-found",
113
+ message: "method 'vcs.get_caps' is not implemented",
114
+ },
115
+ },
116
+ },
117
+ ]);
118
+ });