@openhands/extensions 0.24.0 → 0.26.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.
Files changed (133) hide show
  1. package/.claude-plugin/marketplace.json +30 -0
  2. package/.codex-plugin/marketplace.json +30 -0
  3. package/.github/pull_request_template.md +17 -2
  4. package/.github/scripts/check_pr_description.py +64 -5
  5. package/.github/scripts/markdown_sections.py +95 -0
  6. package/.github/workflows/build-vscode-app.yml +30 -0
  7. package/.github/workflows/check-extensions.yml +3 -0
  8. package/.github/workflows/tests.yml +26 -0
  9. package/.plugin/marketplace.json +30 -0
  10. package/.release-please-manifest.json +1 -1
  11. package/AGENTS.md +3 -2
  12. package/README.md +10 -3
  13. package/apps/README.md +20 -0
  14. package/apps/catalog.json +10 -0
  15. package/apps/index.d.ts +11 -0
  16. package/apps/index.js +5 -0
  17. package/apps/vscode/README.md +13 -0
  18. package/apps/vscode/canvas-extension.json +19 -0
  19. package/apps/vscode/extension.js +114 -0
  20. package/apps/vscode/scripts/package.mjs +84 -0
  21. package/automations/bundle-index.js +18 -14
  22. package/automations/catalog/github-agents-md-maintainer/manifest.json +1 -1
  23. package/automations/catalog/github-pr-reviewer/manifest.json +19 -8
  24. package/automations/catalog/github-stale-ci-pr-closer/manifest.json +71 -0
  25. package/automations/catalog/gitlab-issue-to-mr/manifest.json +1 -1
  26. package/automations/catalog/news-digest/manifest.json +2 -2
  27. package/automations/catalog-index.js +2 -0
  28. package/index.d.ts +2 -0
  29. package/index.js +1 -0
  30. package/marketplaces/openhands-extensions.json +30 -0
  31. package/package.json +9 -1
  32. package/plugins/openhands/skills/openhands-api/README.md +1 -1
  33. package/plugins/openhands/skills/openhands-api/SKILL.md +3 -3
  34. package/plugins/openhands/skills/openhands-api/scripts/openhands_api.py +13 -0
  35. package/plugins/openhands/skills/openhands-automation/SKILL.md +66 -4
  36. package/plugins/openhands/skills/openhands-automation/references/custom-automation.md +21 -7
  37. package/plugins/openhands/skills/openhands-sdk/SKILL.md +1 -0
  38. package/plugins/pr-review/skills/github-pr-review/SKILL.md +12 -2
  39. package/plugins/qa-changes/skills/github-pr-review/SKILL.md +12 -2
  40. package/plugins/runtime-api-configs/.claude-plugin/plugin.json +20 -0
  41. package/plugins/runtime-api-configs/.codex-plugin/plugin.json +20 -0
  42. package/plugins/runtime-api-configs/.plugin/plugin.json +20 -0
  43. package/plugins/runtime-api-configs/README.md +59 -0
  44. package/plugins/runtime-api-configs/SKILL.md +240 -0
  45. package/plugins/runtime-api-configs/scripts/warm_runtime_configs.py +331 -0
  46. package/plugins/runtime-api-configs/tests/test_warm_runtime_configs.py +338 -0
  47. package/plugins/vulnerability-remediation/skills/github/scripts/agent_conversation.py +223 -43
  48. package/plugins/vulnerability-remediation/skills/github/scripts/github_client.py +183 -15
  49. package/pyproject.toml +1 -1
  50. package/python/openhands_extensions/_version.py +1 -1
  51. package/scripts/check_plugin_resources.py +116 -0
  52. package/scripts/sync_extensions.py +32 -5
  53. package/skills/canvas-extension-api/SKILL.md +34 -3
  54. package/skills/canvas-extension-api/references/backend-safety.md +14 -0
  55. package/skills/canvas-extension-api/references/connections.md +24 -0
  56. package/skills/canvas-extension-api/references/sidecar-pattern.md +49 -10
  57. package/skills/canvas-extension-api/references/v1-contract.md +53 -3
  58. package/skills/canvas-extension-api/scripts/validate-extension.mjs +52 -0
  59. package/skills/canvas-extension-api/scripts/validate-extension.test.mjs +192 -1
  60. package/skills/github/scripts/agent_conversation.py +223 -43
  61. package/skills/github/scripts/github_client.py +183 -15
  62. package/skills/github-agents-md-maintainer/README.md +7 -0
  63. package/skills/github-agents-md-maintainer/SKILL.md +13 -6
  64. package/skills/github-agents-md-maintainer/scripts/github_client.py +183 -15
  65. package/skills/github-agents-md-maintainer/scripts/main.py +199 -29
  66. package/skills/github-delivery-watchdog/scripts/github_client.py +183 -15
  67. package/skills/github-issue-to-pr/scripts/agent_conversation.py +223 -43
  68. package/skills/github-issue-to-pr/scripts/github_client.py +183 -15
  69. package/skills/github-issue-to-pr/scripts/worker.py +10 -1
  70. package/skills/github-issue-triage/scripts/agent_conversation.py +223 -43
  71. package/skills/github-issue-triage/scripts/github_client.py +183 -15
  72. package/skills/github-pr-review/SKILL.md +12 -2
  73. package/skills/github-pr-reviewer/README.md +36 -0
  74. package/skills/github-pr-reviewer/SKILL.md +147 -4
  75. package/skills/github-pr-reviewer/scripts/agent_conversation.py +223 -43
  76. package/skills/github-pr-reviewer/scripts/github_client.py +183 -15
  77. package/skills/github-pr-reviewer/scripts/main.py +17 -1
  78. package/skills/github-pr-reviewer/scripts/worker.py +842 -57
  79. package/skills/github-repo-monitor/scripts/github_client.py +183 -15
  80. package/skills/github-repo-monitor/scripts/main.py +32 -1
  81. package/skills/github-stale-ci-pr-closer/.claude-plugin/plugin.json +9 -0
  82. package/skills/github-stale-ci-pr-closer/.codex-plugin/plugin.json +9 -0
  83. package/skills/github-stale-ci-pr-closer/.plugin/plugin.json +9 -0
  84. package/skills/github-stale-ci-pr-closer/README.md +9 -0
  85. package/skills/github-stale-ci-pr-closer/SKILL.md +24 -0
  86. package/skills/github-stale-ci-pr-closer/commands/github-stale-ci-pr-closer.md +8 -0
  87. package/skills/github-stale-ci-pr-closer/scripts/worker.py +508 -0
  88. package/skills/github-stale-ci-pr-closer/tests/test_worker.py +353 -0
  89. package/skills/gitlab-issue-to-mr/README.md +12 -0
  90. package/skills/gitlab-issue-to-mr/SKILL.md +13 -6
  91. package/skills/gitlab-issue-to-mr/scripts/main.py +217 -42
  92. package/skills/incident-retrospective/SKILL.md +5 -5
  93. package/skills/index.js +34 -16
  94. package/skills/jira-issue-to-pr/README.md +12 -4
  95. package/skills/jira-issue-to-pr/SKILL.md +129 -18
  96. package/skills/jira-issue-to-pr/references/setup.md +8 -0
  97. package/skills/jira-issue-to-pr/scripts/main.py +181 -54
  98. package/skills/jira-issue-to-pr/scripts/setup_webhook.py +290 -0
  99. package/skills/linear-triage/SKILL.md +5 -5
  100. package/skills/news-digest/README.md +6 -0
  101. package/skills/news-digest/SKILL.md +19 -9
  102. package/skills/news-digest/commands/news-digest-setup.md +1 -1
  103. package/skills/news-digest/scripts/main.py +133 -15
  104. package/skills/openhands-api/README.md +1 -1
  105. package/skills/openhands-api/SKILL.md +3 -3
  106. package/skills/openhands-api/scripts/openhands_api.py +13 -0
  107. package/skills/openhands-automation/SKILL.md +66 -4
  108. package/skills/openhands-automation/references/custom-automation.md +21 -7
  109. package/skills/openhands-sdk/SKILL.md +1 -0
  110. package/skills/research-brief/SKILL.md +5 -5
  111. package/skills/slack-standup-digest/SKILL.md +5 -5
  112. package/skills/ticket-to-code-change/SKILL.md +6 -3
  113. package/skills/use-jev/.claude-plugin/plugin.json +8 -0
  114. package/skills/use-jev/.codex-plugin/plugin.json +8 -0
  115. package/skills/use-jev/.plugin/plugin.json +8 -0
  116. package/skills/use-jev/README.md +66 -0
  117. package/skills/use-jev/SKILL.md +66 -0
  118. package/tests/fixtures/automations/github-pr-reviewer.json +39 -24
  119. package/tests/test_agent_conversation_dispatch.py +212 -0
  120. package/tests/test_apps_catalog.py +26 -0
  121. package/tests/test_automation_setup.py +44 -0
  122. package/tests/test_check_pr_description.py +89 -6
  123. package/tests/test_github_automation_foundation.py +158 -0
  124. package/tests/test_github_repo_monitor.py +107 -0
  125. package/tests/test_github_reviewer_delivery.py +2247 -6
  126. package/tests/test_github_stale_ci_pr_closer.py +56 -0
  127. package/tests/test_jira_issue_to_pr.py +312 -0
  128. package/tests/test_jira_setup_webhook.py +309 -0
  129. package/tests/test_openhands_api_python.py +37 -0
  130. package/tests/test_skill_plugin_loading.py +25 -15
  131. package/tests/test_sync_extensions.py +102 -0
  132. package/tests/test_vscode_app.py +33 -0
  133. package/tests/vscode-app.test.mjs +43 -0
@@ -150,3 +150,40 @@ def test_legacy_alias_still_exists(monkeypatch):
150
150
 
151
151
  api = mod.OpenHandsV1API(api_key="k")
152
152
  assert isinstance(api, mod.OpenHandsAPI)
153
+
154
+
155
+ def _api_with_conversations(monkeypatch, conversations, base_url="https://example.com"):
156
+ """Client whose app server answers GET /app-conversations with `conversations`."""
157
+ mod = _load_openhands_api_module()
158
+ real_client = mod.httpx.Client
159
+
160
+ def handler(request):
161
+ assert request.url.path == "/api/v1/app-conversations"
162
+ return mod.httpx.Response(200, json=conversations)
163
+
164
+ monkeypatch.setattr(
165
+ mod.httpx,
166
+ "Client",
167
+ lambda **kwargs: real_client(transport=mod.httpx.MockTransport(handler), **kwargs),
168
+ )
169
+ return mod.OpenHandsAPI(api_key="k", base_url=base_url)
170
+
171
+
172
+ def test_app_conversation_ui_url_uses_server_provided_link(monkeypatch):
173
+ server_link = "https://app.example.com/canvas/conversations/conv-1"
174
+ api = _api_with_conversations(
175
+ monkeypatch, [{"id": "conv-1", "conversation_ui_url": server_link}]
176
+ )
177
+
178
+ assert api.app_conversation_ui_url("conv-1") == server_link
179
+
180
+
181
+ def test_app_conversation_ui_url_falls_back_to_agent_canvas_route(monkeypatch):
182
+ # Servers that predate `conversation_ui_url` only return the agent-server API URL.
183
+ api = _api_with_conversations(
184
+ monkeypatch,
185
+ [{"id": "conv-1", "conversation_url": "https://runtime.example.com/api/conversations/conv-1"}],
186
+ base_url="https://example.com/",
187
+ )
188
+
189
+ assert api.app_conversation_ui_url("conv-1") == "https://example.com/canvas/conversations/conv-1"
@@ -1,8 +1,10 @@
1
1
  """Test that skills listed in marketplaces can be loaded as Codex/Claude plugins.
2
2
 
3
3
  Every marketplace entry that references a ``skills/`` directory needs a
4
- ``.plugin/plugin.json`` manifest and vendor symlinks (``.codex-plugin``,
4
+ ``.plugin/plugin.json`` manifest and vendor manifests (``.codex-plugin``,
5
5
  ``.claude-plugin``) so that Codex and Claude Code can discover and load them.
6
+ A vendor manifest is either a symlink to ``.plugin/`` or a real directory
7
+ mirroring ``plugin.json`` (Codex install drops symlinked dirs; see issue #257).
6
8
 
7
9
  Regression test for: https://github.com/OpenHands/extensions/issues/201
8
10
  """
@@ -61,18 +63,17 @@ class TestAllMarketplaceSkillsHaveManifests:
61
63
  f"{', '.join(missing)}"
62
64
  )
63
65
 
64
- def test_all_marketplace_skills_have_vendor_symlinks(self):
65
- """Every marketplace skill with a manifest must have vendor symlinks."""
66
+ def test_all_marketplace_skills_have_vendor_manifests(self):
67
+ """Every marketplace skill with a manifest must have vendor manifests."""
66
68
  problems = []
67
69
  for name, path in _marketplace_skill_entries():
68
70
  if not (path / ".plugin" / "plugin.json").exists():
69
71
  continue
70
72
  for vendor in VENDOR_SYMLINKS:
71
- link = path / vendor
72
- if not link.is_symlink():
73
+ if not _vendor_manifest_ok(path, vendor):
73
74
  problems.append(f"{name}/{vendor}")
74
75
  assert not problems, (
75
- f"Missing vendor symlinks: {', '.join(problems)}"
76
+ f"Missing vendor manifests: {', '.join(problems)}"
76
77
  )
77
78
 
78
79
  def test_all_manifests_have_required_fields(self):
@@ -127,8 +128,20 @@ class TestIteratePluginLoading:
127
128
  assert "verify" in command_names
128
129
 
129
130
 
131
+ def _vendor_manifest_ok(directory: Path, vendor: str) -> bool:
132
+ """A vendor manifest is a symlink to .plugin/ or a real dir mirror."""
133
+ link = directory / vendor
134
+ if link.is_symlink():
135
+ return link.resolve() == (directory / ".plugin").resolve()
136
+ if link.is_dir():
137
+ src = directory / ".plugin" / "plugin.json"
138
+ dst = link / "plugin.json"
139
+ return src.is_file() and dst.is_file() and src.read_bytes() == dst.read_bytes()
140
+ return False
141
+
142
+
130
143
  class TestVendorSymlinksForManifests:
131
- """Every directory with .plugin/ must have vendor symlinks."""
144
+ """Every directory with .plugin/ must have vendor manifests."""
132
145
 
133
146
  @pytest.fixture(
134
147
  params=list(_all_dirs_with_plugin_manifest()),
@@ -137,13 +150,10 @@ class TestVendorSymlinksForManifests:
137
150
  def dir_with_manifest(self, request):
138
151
  return request.param
139
152
 
140
- def test_has_vendor_symlinks(self, dir_with_manifest):
141
- """Directories with .plugin/ must have .claude-plugin and .codex-plugin symlinks."""
153
+ def test_has_vendor_manifests(self, dir_with_manifest):
154
+ """Directories with .plugin/ must have .claude-plugin and .codex-plugin manifests."""
142
155
  for vendor in VENDOR_SYMLINKS:
143
- link = dir_with_manifest / vendor
144
- assert link.is_symlink(), (
145
- f"{link.relative_to(REPO_ROOT)} must be a symlink to .plugin"
146
- )
147
- assert link.resolve() == (dir_with_manifest / ".plugin").resolve(), (
148
- f"{link.relative_to(REPO_ROOT)} must point to .plugin"
156
+ assert _vendor_manifest_ok(dir_with_manifest, vendor), (
157
+ f"{dir_with_manifest / vendor} must be a symlink to .plugin "
158
+ "or a real directory mirroring plugin.json"
149
159
  )
@@ -1,5 +1,6 @@
1
1
  """Tests for scripts/sync_extensions.py core functions."""
2
2
 
3
+ import shutil
3
4
  import sys
4
5
  from pathlib import Path
5
6
 
@@ -18,6 +19,7 @@ from sync_extensions import (
18
19
  parse_frontmatter,
19
20
  slash_triggers,
20
21
  sync_commands,
22
+ sync_symlinks,
21
23
  )
22
24
 
23
25
 
@@ -288,6 +290,106 @@ class TestSyncCommands:
288
290
  assert any("manually-edited" in p for p in problems)
289
291
 
290
292
 
293
+ # ── vendor manifests ─────────────────────────────────────────────────
294
+
295
+ def _make_plugin_skill(root: Path, name: str = "demo") -> Path:
296
+ """Create a fake skill dir with a canonical .plugin/plugin.json."""
297
+ skill = root / "skills" / name
298
+ (skill / ".plugin").mkdir(parents=True)
299
+ (skill / ".plugin" / "plugin.json").write_text('{"name": "demo"}\n')
300
+ return skill
301
+
302
+
303
+ def _make_real_vendor_mirror(skill: Path, vendor: str = ".codex-plugin") -> Path:
304
+ path = skill / vendor
305
+ path.mkdir(exist_ok=True)
306
+ (path / "plugin.json").write_text('{"name": "demo"}\n')
307
+ return path
308
+
309
+
310
+ def _point_sync_at(tmp_path, monkeypatch):
311
+ monkeypatch.setattr("sync_extensions.SKILL_DIRS", [tmp_path / "skills"])
312
+ monkeypatch.setattr("sync_extensions.REPO_ROOT", tmp_path)
313
+
314
+
315
+ class TestVendorManifests:
316
+ def test_iterate_vendor_dirs_are_real_and_mirror_canonical(self):
317
+ """Codex drops symlinked dirs on install, so iterate ships real ones."""
318
+ iterate = REPO_ROOT / "skills" / "iterate"
319
+ canon = (iterate / ".plugin" / "plugin.json").read_bytes()
320
+ for vendor in (".codex-plugin", ".claude-plugin"):
321
+ path = iterate / vendor
322
+ assert not path.is_symlink(), f"{vendor} must not be a symlink"
323
+ assert path.is_dir(), f"{vendor} must be a real directory"
324
+ assert (path / "plugin.json").read_bytes() == canon
325
+
326
+ def test_symlink_to_canonical_passes(self, tmp_path, monkeypatch):
327
+ skill = _make_plugin_skill(tmp_path)
328
+ try:
329
+ for vendor in (".codex-plugin", ".claude-plugin"):
330
+ (skill / vendor).symlink_to(".plugin", target_is_directory=True)
331
+ except OSError:
332
+ pytest.skip("symlinks need privileges on this platform")
333
+ _point_sync_at(tmp_path, monkeypatch)
334
+
335
+ assert sync_symlinks(check=True) == []
336
+
337
+ def test_real_dir_mirror_passes_and_stale_copy_is_flagged(self, tmp_path, monkeypatch):
338
+ skill = _make_plugin_skill(tmp_path)
339
+ vendor = _make_real_vendor_mirror(skill)
340
+ _make_real_vendor_mirror(skill, ".claude-plugin")
341
+ _point_sync_at(tmp_path, monkeypatch)
342
+
343
+ assert sync_symlinks(check=True) == []
344
+
345
+ (vendor / "plugin.json").write_text('{"name": "stale"}\n')
346
+ problems = sync_symlinks(check=True)
347
+ assert any("stale manifest copy" in p for p in problems)
348
+
349
+ def test_fix_mode_refreshes_stale_copy(self, tmp_path, monkeypatch):
350
+ skill = _make_plugin_skill(tmp_path)
351
+ vendor = _make_real_vendor_mirror(skill)
352
+ _make_real_vendor_mirror(skill, ".claude-plugin")
353
+ (vendor / "plugin.json").write_text('{"name": "stale"}\n')
354
+ _point_sync_at(tmp_path, monkeypatch)
355
+
356
+ problems = sync_symlinks(check=False)
357
+ assert any("stale manifest copy" in p for p in problems)
358
+ assert (vendor / "plugin.json").read_text() == '{"name": "demo"}\n'
359
+ assert sync_symlinks(check=True) == []
360
+
361
+ def test_lossy_install_copy_keeps_codex_manifest(self, tmp_path):
362
+ """Mimic Codex `plugin add`, which drops symlinked directories.
363
+
364
+ The installed cache must still contain
365
+ `.codex-plugin/plugin.json` for the plugin to load.
366
+ """
367
+ src = REPO_ROOT / "skills" / "iterate"
368
+ dst = tmp_path / "iterate"
369
+ shutil.copytree(
370
+ src,
371
+ dst,
372
+ ignore=lambda d, names: [
373
+ n for n in names if (Path(d) / n).is_symlink()
374
+ ],
375
+ )
376
+ manifest = dst / ".codex-plugin" / "plugin.json"
377
+ assert manifest.is_file(), "Codex install lost .codex-plugin/plugin.json"
378
+ assert manifest.read_bytes() == (src / ".plugin" / "plugin.json").read_bytes()
379
+
380
+ def test_missing_vendor_still_gets_symlink(self, tmp_path, monkeypatch):
381
+ skill = _make_plugin_skill(tmp_path)
382
+ _point_sync_at(tmp_path, monkeypatch)
383
+ try:
384
+ sync_symlinks(check=False)
385
+ except OSError:
386
+ pytest.skip("symlinks need privileges on this platform")
387
+
388
+ assert (skill / ".codex-plugin").is_symlink()
389
+ assert (skill / ".claude-plugin").is_symlink()
390
+ assert sync_symlinks(check=True) == []
391
+
392
+
291
393
  # ── marketplace source paths ─────────────────────────────────────────
292
394
 
293
395
  class TestMarketplaceSourcePaths:
@@ -0,0 +1,33 @@
1
+ import json
2
+ import subprocess
3
+ from pathlib import Path
4
+
5
+ ROOT = Path(__file__).resolve().parents[1]
6
+ APP = ROOT / "apps" / "vscode"
7
+
8
+
9
+ def test_vscode_app_manifest_has_contained_pinned_dual_arch_artifacts():
10
+ manifest = json.loads((APP / "canvas-extension.json").read_text())
11
+ backend = manifest["backend"]
12
+ assert set(backend["artifacts"]) == {"linux-amd64", "linux-arm64"}
13
+ for artifact in backend["artifacts"].values():
14
+ assert artifact["path"].startswith("backend/")
15
+ assert len(artifact["sha256"]) == 64
16
+ assert "HOME" not in backend["inherit_environment"]
17
+ assert manifest["entrypoint"] == "extension.js"
18
+
19
+
20
+ def test_ready_entrypoint_is_self_contained_and_full_height():
21
+ source = (APP / "extension.js").read_text()
22
+ assert "export function activate" in source
23
+ assert "host.appBackendView.mount" in source
24
+ assert "query: { folder }" in source
25
+ assert "localStorage" in source
26
+ assert "height:100vh" in source
27
+ assert "<iframe" not in source
28
+ assert "import " not in source
29
+ subprocess.run(["node", "--check", str(APP / "extension.js")], check=True)
30
+
31
+
32
+ def test_release_binaries_are_not_committed():
33
+ assert not list(APP.glob("backend/*"))
@@ -0,0 +1,43 @@
1
+ import assert from "node:assert/strict";
2
+ import { activate } from "../apps/vscode/extension.js";
3
+
4
+ class Element {
5
+ constructor(tag) { this.tagName = tag; this.children = []; this.listeners = {}; this.style = {}; }
6
+ append(...children) { this.children.push(...children); }
7
+ replaceChildren(...children) { this.children = children; }
8
+ setAttribute() {}
9
+ addEventListener(name, fn) { this.listeners[name] = fn; }
10
+ async click() { await this.listeners.click(); }
11
+ }
12
+ class Document { createElement(tag) { return new Element(tag); } createTextNode(text) { return { textContent: text }; } }
13
+ const document = new Document();
14
+ globalThis.document = document;
15
+ globalThis.localStorage = { values: new Map(), getItem(k) { return this.values.get(k) ?? null; }, setItem(k, v) { this.values.set(k, v); }, removeItem(k) { this.values.delete(k); } };
16
+
17
+ const requests = [];
18
+ let status = { name: "vscode", state: "stopped", revision: "artifact-revision" };
19
+ const host = {
20
+ backend: { id: "backend-a", orgId: "org-a" }, extension: { name: "vscode" },
21
+ agentServer: { request: async ({ method, path, body }) => {
22
+ requests.push({ method, path, body });
23
+ if (method === "GET") return status;
24
+ if (path.endsWith("/prepare")) return status;
25
+ if (path.endsWith("/start")) { status = { ...status, state: "ready" }; return status; }
26
+ return status;
27
+ } },
28
+ appBackendView: { mount: ({ container, query }) => { container.mountedQuery = query; return () => { container.disposed = true; }; } },
29
+ registerPage: (_id, mount) => { host.mount = mount; return () => {}; },
30
+ };
31
+ await activate(host);
32
+ const container = new Element("main");
33
+ const dispose = await host.mount({ container });
34
+ assert.equal(container.children[0].children[0].children[0].textContent, "VS Code setup");
35
+ await container.children[0].children[0].children[3].children[0].click();
36
+ assert.deepEqual(requests.slice(0, 3).map(({ method, path, body }) => [method, path, body]), [
37
+ ["GET", "/api/canvas-extensions/installed/vscode/backend", undefined],
38
+ ["POST", "/api/canvas-extensions/installed/vscode/backend/prepare", { revision: "artifact-revision" }],
39
+ ["POST", "/api/canvas-extensions/installed/vscode/backend/start", { revision: "artifact-revision" }],
40
+ ]);
41
+ assert.equal(container.children[0].children[0].mountedQuery, undefined);
42
+ dispose();
43
+ console.log("VS Code App lifecycle behavior passed");