gsd-pi 2.45.0 → 2.46.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 (239) hide show
  1. package/dist/help-text.js +1 -1
  2. package/dist/loader.js +34 -0
  3. package/dist/resources/extensions/gsd/auto/phases.js +27 -42
  4. package/dist/resources/extensions/gsd/auto/run-unit.js +6 -3
  5. package/dist/resources/extensions/gsd/auto/session.js +0 -11
  6. package/dist/resources/extensions/gsd/auto-artifact-paths.js +112 -0
  7. package/dist/resources/extensions/gsd/auto-post-unit.js +25 -96
  8. package/dist/resources/extensions/gsd/auto-start.js +2 -3
  9. package/dist/resources/extensions/gsd/auto-worktree.js +5 -4
  10. package/dist/resources/extensions/gsd/auto.js +12 -57
  11. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +15 -12
  12. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +18 -0
  13. package/dist/resources/extensions/gsd/commands/context.js +0 -4
  14. package/dist/resources/extensions/gsd/commands/handlers/parallel.js +1 -1
  15. package/dist/resources/extensions/gsd/crash-recovery.js +2 -4
  16. package/dist/resources/extensions/gsd/dashboard-overlay.js +0 -44
  17. package/dist/resources/extensions/gsd/db-writer.js +9 -9
  18. package/dist/resources/extensions/gsd/doctor-checks.js +167 -2
  19. package/dist/resources/extensions/gsd/doctor.js +5 -3
  20. package/dist/resources/extensions/gsd/gsd-db.js +16 -3
  21. package/dist/resources/extensions/gsd/guided-flow.js +1 -2
  22. package/dist/resources/extensions/gsd/parallel-merge.js +1 -1
  23. package/dist/resources/extensions/gsd/parallel-orchestrator.js +5 -18
  24. package/dist/resources/extensions/gsd/preferences-types.js +2 -2
  25. package/dist/resources/extensions/gsd/preferences.js +8 -4
  26. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +21 -8
  27. package/dist/resources/extensions/gsd/prompts/complete-slice.md +10 -23
  28. package/dist/resources/extensions/gsd/prompts/discuss.md +2 -2
  29. package/dist/resources/extensions/gsd/prompts/execute-task.md +5 -15
  30. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
  31. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
  32. package/dist/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -1
  33. package/dist/resources/extensions/gsd/prompts/guided-research-slice.md +1 -1
  34. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  35. package/dist/resources/extensions/gsd/prompts/plan-slice.md +4 -2
  36. package/dist/resources/extensions/gsd/prompts/queue.md +2 -2
  37. package/dist/resources/extensions/gsd/prompts/quick-task.md +2 -0
  38. package/dist/resources/extensions/gsd/prompts/reactive-execute.md +1 -1
  39. package/dist/resources/extensions/gsd/prompts/research-slice.md +3 -3
  40. package/dist/resources/extensions/gsd/prompts/rethink.md +7 -2
  41. package/dist/resources/extensions/gsd/prompts/system.md +1 -1
  42. package/dist/resources/extensions/gsd/session-lock.js +1 -3
  43. package/dist/resources/extensions/gsd/state.js +7 -0
  44. package/dist/resources/extensions/gsd/sync-lock.js +89 -0
  45. package/dist/resources/extensions/gsd/tools/complete-milestone.js +61 -11
  46. package/dist/resources/extensions/gsd/tools/complete-slice.js +56 -11
  47. package/dist/resources/extensions/gsd/tools/complete-task.js +50 -2
  48. package/dist/resources/extensions/gsd/tools/plan-milestone.js +37 -1
  49. package/dist/resources/extensions/gsd/tools/plan-slice.js +30 -1
  50. package/dist/resources/extensions/gsd/tools/plan-task.js +27 -1
  51. package/dist/resources/extensions/gsd/tools/reassess-roadmap.js +32 -2
  52. package/dist/resources/extensions/gsd/tools/reopen-slice.js +86 -0
  53. package/dist/resources/extensions/gsd/tools/reopen-task.js +90 -0
  54. package/dist/resources/extensions/gsd/tools/replan-slice.js +32 -2
  55. package/dist/resources/extensions/gsd/unit-ownership.js +85 -0
  56. package/dist/resources/extensions/gsd/workflow-events.js +102 -0
  57. package/dist/resources/extensions/gsd/workflow-logger.js +193 -0
  58. package/dist/resources/extensions/gsd/workflow-manifest.js +244 -0
  59. package/dist/resources/extensions/gsd/workflow-migration.js +280 -0
  60. package/dist/resources/extensions/gsd/workflow-projections.js +373 -0
  61. package/dist/resources/extensions/gsd/workflow-reconcile.js +411 -0
  62. package/dist/resources/extensions/gsd/worktree-manager.js +4 -3
  63. package/dist/resources/extensions/gsd/worktree-resolver.js +37 -0
  64. package/dist/resources/extensions/gsd/write-intercept.js +84 -0
  65. package/dist/resources/extensions/voice/index.js +11 -16
  66. package/dist/resources/extensions/voice/linux-ready.js +67 -0
  67. package/dist/web/standalone/.next/BUILD_ID +1 -1
  68. package/dist/web/standalone/.next/app-path-routes-manifest.json +9 -9
  69. package/dist/web/standalone/.next/build-manifest.json +2 -2
  70. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  71. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  72. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  76. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  77. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  78. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  79. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  80. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  81. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  82. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  83. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  84. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  85. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  86. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  87. package/dist/web/standalone/.next/server/app/index.html +1 -1
  88. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  89. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  90. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  91. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  92. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  93. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  94. package/dist/web/standalone/.next/server/app-paths-manifest.json +9 -9
  95. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  96. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  97. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  98. package/package.json +2 -1
  99. package/packages/pi-coding-agent/dist/core/compaction-orchestrator.d.ts.map +1 -1
  100. package/packages/pi-coding-agent/dist/core/compaction-orchestrator.js +2 -0
  101. package/packages/pi-coding-agent/dist/core/compaction-orchestrator.js.map +1 -1
  102. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +2 -1
  103. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  104. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  105. package/packages/pi-coding-agent/dist/core/lifecycle-hooks.d.ts +4 -0
  106. package/packages/pi-coding-agent/dist/core/lifecycle-hooks.d.ts.map +1 -1
  107. package/packages/pi-coding-agent/dist/core/lifecycle-hooks.js +10 -5
  108. package/packages/pi-coding-agent/dist/core/lifecycle-hooks.js.map +1 -1
  109. package/packages/pi-coding-agent/dist/core/lifecycle-hooks.test.d.ts +2 -0
  110. package/packages/pi-coding-agent/dist/core/lifecycle-hooks.test.d.ts.map +1 -0
  111. package/packages/pi-coding-agent/dist/core/lifecycle-hooks.test.js +185 -0
  112. package/packages/pi-coding-agent/dist/core/lifecycle-hooks.test.js.map +1 -0
  113. package/packages/pi-coding-agent/dist/core/model-registry-auth-mode.test.js +239 -10
  114. package/packages/pi-coding-agent/dist/core/model-registry-auth-mode.test.js.map +1 -1
  115. package/packages/pi-coding-agent/dist/core/model-registry.d.ts +2 -1
  116. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  117. package/packages/pi-coding-agent/dist/core/model-registry.js +20 -2
  118. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  119. package/packages/pi-coding-agent/dist/core/package-commands.test.js +206 -195
  120. package/packages/pi-coding-agent/dist/core/package-commands.test.js.map +1 -1
  121. package/packages/pi-coding-agent/package.json +1 -1
  122. package/packages/pi-coding-agent/src/core/compaction-orchestrator.ts +2 -0
  123. package/packages/pi-coding-agent/src/core/extensions/types.ts +2 -1
  124. package/packages/pi-coding-agent/src/core/lifecycle-hooks.test.ts +227 -0
  125. package/packages/pi-coding-agent/src/core/lifecycle-hooks.ts +11 -5
  126. package/packages/pi-coding-agent/src/core/model-registry-auth-mode.test.ts +297 -11
  127. package/packages/pi-coding-agent/src/core/model-registry.ts +30 -3
  128. package/packages/pi-coding-agent/src/core/package-commands.test.ts +227 -205
  129. package/pkg/package.json +1 -1
  130. package/src/resources/extensions/gsd/auto/loop-deps.ts +0 -19
  131. package/src/resources/extensions/gsd/auto/phases.ts +24 -44
  132. package/src/resources/extensions/gsd/auto/run-unit.ts +6 -3
  133. package/src/resources/extensions/gsd/auto/session.ts +0 -18
  134. package/src/resources/extensions/gsd/auto-artifact-paths.ts +131 -0
  135. package/src/resources/extensions/gsd/auto-dashboard.ts +0 -1
  136. package/src/resources/extensions/gsd/auto-post-unit.ts +25 -106
  137. package/src/resources/extensions/gsd/auto-start.ts +1 -3
  138. package/src/resources/extensions/gsd/auto-worktree.ts +8 -5
  139. package/src/resources/extensions/gsd/auto.ts +7 -83
  140. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +15 -12
  141. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +22 -0
  142. package/src/resources/extensions/gsd/commands/context.ts +0 -5
  143. package/src/resources/extensions/gsd/commands/handlers/parallel.ts +1 -1
  144. package/src/resources/extensions/gsd/crash-recovery.ts +1 -5
  145. package/src/resources/extensions/gsd/dashboard-overlay.ts +0 -50
  146. package/src/resources/extensions/gsd/db-writer.ts +9 -17
  147. package/src/resources/extensions/gsd/doctor-checks.ts +180 -2
  148. package/src/resources/extensions/gsd/doctor-types.ts +7 -1
  149. package/src/resources/extensions/gsd/doctor.ts +6 -3
  150. package/src/resources/extensions/gsd/gsd-db.ts +16 -3
  151. package/src/resources/extensions/gsd/guided-flow.ts +1 -2
  152. package/src/resources/extensions/gsd/journal.ts +6 -1
  153. package/src/resources/extensions/gsd/parallel-merge.ts +1 -1
  154. package/src/resources/extensions/gsd/parallel-orchestrator.ts +5 -21
  155. package/src/resources/extensions/gsd/preferences-types.ts +2 -2
  156. package/src/resources/extensions/gsd/preferences.ts +7 -3
  157. package/src/resources/extensions/gsd/prompts/complete-milestone.md +21 -8
  158. package/src/resources/extensions/gsd/prompts/complete-slice.md +10 -23
  159. package/src/resources/extensions/gsd/prompts/discuss.md +2 -2
  160. package/src/resources/extensions/gsd/prompts/execute-task.md +5 -15
  161. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
  162. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
  163. package/src/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -1
  164. package/src/resources/extensions/gsd/prompts/guided-research-slice.md +1 -1
  165. package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  166. package/src/resources/extensions/gsd/prompts/plan-slice.md +4 -2
  167. package/src/resources/extensions/gsd/prompts/queue.md +2 -2
  168. package/src/resources/extensions/gsd/prompts/quick-task.md +2 -0
  169. package/src/resources/extensions/gsd/prompts/reactive-execute.md +1 -1
  170. package/src/resources/extensions/gsd/prompts/research-slice.md +3 -3
  171. package/src/resources/extensions/gsd/prompts/rethink.md +7 -2
  172. package/src/resources/extensions/gsd/prompts/system.md +1 -1
  173. package/src/resources/extensions/gsd/session-lock.ts +0 -4
  174. package/src/resources/extensions/gsd/state.ts +8 -0
  175. package/src/resources/extensions/gsd/sync-lock.ts +94 -0
  176. package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +5 -13
  177. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +6 -10
  178. package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +96 -0
  179. package/src/resources/extensions/gsd/tests/complete-slice.test.ts +264 -228
  180. package/src/resources/extensions/gsd/tests/complete-task.test.ts +317 -250
  181. package/src/resources/extensions/gsd/tests/crash-recovery.test.ts +2 -8
  182. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +0 -3
  183. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +1 -1
  184. package/src/resources/extensions/gsd/tests/idle-recovery.test.ts +1 -1
  185. package/src/resources/extensions/gsd/tests/integration-proof.test.ts +15 -24
  186. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +0 -3
  187. package/src/resources/extensions/gsd/tests/md-importer.test.ts +1 -1
  188. package/src/resources/extensions/gsd/tests/memory-store.test.ts +2 -2
  189. package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +1 -1
  190. package/src/resources/extensions/gsd/tests/milestone-transition-state-rebuild.test.ts +8 -9
  191. package/src/resources/extensions/gsd/tests/none-mode-gates.test.ts +42 -3
  192. package/src/resources/extensions/gsd/tests/parallel-budget-atomicity.test.ts +0 -1
  193. package/src/resources/extensions/gsd/tests/parallel-crash-recovery.test.ts +0 -7
  194. package/src/resources/extensions/gsd/tests/parallel-merge.test.ts +7 -8
  195. package/src/resources/extensions/gsd/tests/parallel-orchestration.test.ts +20 -24
  196. package/src/resources/extensions/gsd/tests/parallel-worker-monitoring.test.ts +0 -2
  197. package/src/resources/extensions/gsd/tests/plan-milestone.test.ts +9 -6
  198. package/src/resources/extensions/gsd/tests/post-mutation-hook.test.ts +171 -0
  199. package/src/resources/extensions/gsd/tests/preferences.test.ts +7 -9
  200. package/src/resources/extensions/gsd/tests/projection-regression.test.ts +174 -0
  201. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +15 -14
  202. package/src/resources/extensions/gsd/tests/reopen-slice.test.ts +155 -0
  203. package/src/resources/extensions/gsd/tests/reopen-task.test.ts +165 -0
  204. package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +1 -4
  205. package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +2 -3
  206. package/src/resources/extensions/gsd/tests/sync-lock.test.ts +122 -0
  207. package/src/resources/extensions/gsd/tests/unit-ownership.test.ts +175 -0
  208. package/src/resources/extensions/gsd/tests/workflow-events.test.ts +205 -0
  209. package/src/resources/extensions/gsd/tests/workflow-logger.test.ts +275 -0
  210. package/src/resources/extensions/gsd/tests/workflow-manifest.test.ts +186 -0
  211. package/src/resources/extensions/gsd/tests/workflow-projections.test.ts +171 -0
  212. package/src/resources/extensions/gsd/tests/worktree-journal-events.test.ts +220 -0
  213. package/src/resources/extensions/gsd/tests/write-intercept.test.ts +76 -0
  214. package/src/resources/extensions/gsd/tools/complete-milestone.ts +74 -11
  215. package/src/resources/extensions/gsd/tools/complete-slice.ts +68 -11
  216. package/src/resources/extensions/gsd/tools/complete-task.ts +63 -1
  217. package/src/resources/extensions/gsd/tools/plan-milestone.ts +45 -0
  218. package/src/resources/extensions/gsd/tools/plan-slice.ts +38 -0
  219. package/src/resources/extensions/gsd/tools/plan-task.ts +35 -1
  220. package/src/resources/extensions/gsd/tools/reassess-roadmap.ts +39 -1
  221. package/src/resources/extensions/gsd/tools/reopen-slice.ts +125 -0
  222. package/src/resources/extensions/gsd/tools/reopen-task.ts +129 -0
  223. package/src/resources/extensions/gsd/tools/replan-slice.ts +38 -1
  224. package/src/resources/extensions/gsd/types.ts +8 -0
  225. package/src/resources/extensions/gsd/unit-ownership.ts +104 -0
  226. package/src/resources/extensions/gsd/workflow-events.ts +154 -0
  227. package/src/resources/extensions/gsd/workflow-logger.ts +243 -0
  228. package/src/resources/extensions/gsd/workflow-manifest.ts +334 -0
  229. package/src/resources/extensions/gsd/workflow-migration.ts +345 -0
  230. package/src/resources/extensions/gsd/workflow-projections.ts +425 -0
  231. package/src/resources/extensions/gsd/workflow-reconcile.ts +503 -0
  232. package/src/resources/extensions/gsd/worktree-manager.ts +4 -9
  233. package/src/resources/extensions/gsd/worktree-resolver.ts +37 -0
  234. package/src/resources/extensions/gsd/write-intercept.ts +90 -0
  235. package/src/resources/extensions/voice/index.ts +11 -21
  236. package/src/resources/extensions/voice/linux-ready.ts +87 -0
  237. package/src/resources/extensions/voice/tests/linux-ready.test.ts +124 -0
  238. /package/dist/web/standalone/.next/static/{wUzEX1U3CmFcMry2SUDJn → 8zT99piZz8u3xAU3Omz2g}/_buildManifest.js +0 -0
  239. /package/dist/web/standalone/.next/static/{wUzEX1U3CmFcMry2SUDJn → 8zT99piZz8u3xAU3Omz2g}/_ssgManifest.js +0 -0
@@ -0,0 +1,185 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync } from "node:fs";
3
+ import { homedir, tmpdir } from "node:os";
4
+ import { join, resolve } from "node:path";
5
+ import { describe, it } from "node:test";
6
+ import { readManifestRuntimeDeps, collectRuntimeDependencies, verifyRuntimeDependencies, resolveLocalSourcePath, } from "./lifecycle-hooks.js";
7
+ function tmpDir(prefix, t) {
8
+ const dir = mkdtempSync(join(tmpdir(), `pi-lh-${prefix}-`));
9
+ t.after(() => rmSync(dir, { recursive: true, force: true }));
10
+ return dir;
11
+ }
12
+ // ─── readManifestRuntimeDeps ──────────────────────────────────────────────────
13
+ describe("readManifestRuntimeDeps", () => {
14
+ it("returns empty array when manifest file is missing", (t) => {
15
+ const dir = tmpDir("no-manifest", t);
16
+ assert.deepEqual(readManifestRuntimeDeps(dir), []);
17
+ });
18
+ it("returns empty array for malformed JSON", (t) => {
19
+ const dir = tmpDir("bad-json", t);
20
+ writeFileSync(join(dir, "extension-manifest.json"), "not json{{{", "utf-8");
21
+ assert.deepEqual(readManifestRuntimeDeps(dir), []);
22
+ });
23
+ it("returns runtime deps from valid manifest", (t) => {
24
+ const dir = tmpDir("valid", t);
25
+ writeFileSync(join(dir, "extension-manifest.json"), JSON.stringify({
26
+ dependencies: { runtime: ["claude", "node"] },
27
+ }), "utf-8");
28
+ assert.deepEqual(readManifestRuntimeDeps(dir), ["claude", "node"]);
29
+ });
30
+ it("returns empty array when dependencies exists but runtime is missing", (t) => {
31
+ const dir = tmpDir("no-runtime", t);
32
+ writeFileSync(join(dir, "extension-manifest.json"), JSON.stringify({
33
+ dependencies: {},
34
+ }), "utf-8");
35
+ assert.deepEqual(readManifestRuntimeDeps(dir), []);
36
+ });
37
+ it("returns empty array when runtime is empty", (t) => {
38
+ const dir = tmpDir("empty-runtime", t);
39
+ writeFileSync(join(dir, "extension-manifest.json"), JSON.stringify({
40
+ dependencies: { runtime: [] },
41
+ }), "utf-8");
42
+ assert.deepEqual(readManifestRuntimeDeps(dir), []);
43
+ });
44
+ it("filters out non-string entries in runtime array", (t) => {
45
+ const dir = tmpDir("mixed-types", t);
46
+ writeFileSync(join(dir, "extension-manifest.json"), JSON.stringify({
47
+ dependencies: { runtime: [123, null, "node", false, "python"] },
48
+ }), "utf-8");
49
+ assert.deepEqual(readManifestRuntimeDeps(dir), ["node", "python"]);
50
+ });
51
+ it("returns empty array when no dependencies field at all", (t) => {
52
+ const dir = tmpDir("no-deps-field", t);
53
+ writeFileSync(join(dir, "extension-manifest.json"), JSON.stringify({
54
+ id: "test",
55
+ name: "Test",
56
+ }), "utf-8");
57
+ assert.deepEqual(readManifestRuntimeDeps(dir), []);
58
+ });
59
+ });
60
+ // ─── collectRuntimeDependencies ───────────────────────────────────────────────
61
+ describe("collectRuntimeDependencies", () => {
62
+ it("aggregates deps from installedPath manifest", (t) => {
63
+ const dir = tmpDir("collect-installed", t);
64
+ writeFileSync(join(dir, "extension-manifest.json"), JSON.stringify({
65
+ dependencies: { runtime: ["claude"] },
66
+ }), "utf-8");
67
+ assert.deepEqual(collectRuntimeDependencies(dir, []), ["claude"]);
68
+ });
69
+ it("aggregates deps from entry path directory manifests", (t) => {
70
+ const root = tmpDir("collect-entry", t);
71
+ const installedDir = join(root, "installed");
72
+ const entryDir = join(root, "entry");
73
+ mkdirSync(installedDir, { recursive: true });
74
+ mkdirSync(entryDir, { recursive: true });
75
+ writeFileSync(join(entryDir, "extension-manifest.json"), JSON.stringify({
76
+ dependencies: { runtime: ["python"] },
77
+ }), "utf-8");
78
+ const deps = collectRuntimeDependencies(installedDir, [join(entryDir, "index.ts")]);
79
+ assert.deepEqual(deps, ["python"]);
80
+ });
81
+ it("deduplicates across multiple directories", (t) => {
82
+ const root = tmpDir("collect-dedup", t);
83
+ const dir1 = join(root, "dir1");
84
+ const dir2 = join(root, "dir2");
85
+ mkdirSync(dir1, { recursive: true });
86
+ mkdirSync(dir2, { recursive: true });
87
+ writeFileSync(join(dir1, "extension-manifest.json"), JSON.stringify({
88
+ dependencies: { runtime: ["node", "python"] },
89
+ }), "utf-8");
90
+ writeFileSync(join(dir2, "extension-manifest.json"), JSON.stringify({
91
+ dependencies: { runtime: ["python", "claude"] },
92
+ }), "utf-8");
93
+ const deps = collectRuntimeDependencies(dir1, [join(dir2, "index.ts")]);
94
+ assert.equal(deps.length, 3);
95
+ assert.ok(deps.includes("node"));
96
+ assert.ok(deps.includes("python"));
97
+ assert.ok(deps.includes("claude"));
98
+ });
99
+ it("returns empty when no directories have manifests", (t) => {
100
+ const dir = tmpDir("collect-empty", t);
101
+ assert.deepEqual(collectRuntimeDependencies(dir, []), []);
102
+ });
103
+ });
104
+ // ─── verifyRuntimeDependencies ────────────────────────────────────────────────
105
+ describe("verifyRuntimeDependencies", () => {
106
+ it("does not throw for empty deps array", () => {
107
+ assert.doesNotThrow(() => verifyRuntimeDependencies([], "test-source", "pi"));
108
+ });
109
+ it("does not throw when all deps are present", () => {
110
+ assert.doesNotThrow(() => verifyRuntimeDependencies(["node"], "test-source", "pi"));
111
+ });
112
+ it("throws for missing dep with 'Missing runtime dependencies' message", () => {
113
+ assert.throws(() => verifyRuntimeDependencies(["__nonexistent_dep_for_test__"], "test-source", "pi"), (err) => {
114
+ assert.ok(err.message.includes("Missing runtime dependencies"));
115
+ assert.ok(err.message.includes("__nonexistent_dep_for_test__"));
116
+ return true;
117
+ });
118
+ });
119
+ it("lists all missing deps in error message", () => {
120
+ assert.throws(() => verifyRuntimeDependencies(["__missing_1__", "__missing_2__"], "test-source", "pi"), (err) => {
121
+ assert.ok(err.message.includes("__missing_1__"));
122
+ assert.ok(err.message.includes("__missing_2__"));
123
+ return true;
124
+ });
125
+ });
126
+ it("includes appName and source in error for retry hint", () => {
127
+ assert.throws(() => verifyRuntimeDependencies(["__missing__"], "github:user/repo", "gsd"), (err) => {
128
+ assert.ok(err.message.includes("gsd"));
129
+ assert.ok(err.message.includes("github:user/repo"));
130
+ return true;
131
+ });
132
+ });
133
+ });
134
+ // ─── resolveLocalSourcePath ───────────────────────────────────────────────────
135
+ describe("resolveLocalSourcePath", () => {
136
+ it("returns undefined for empty string", () => {
137
+ assert.equal(resolveLocalSourcePath("", "/tmp"), undefined);
138
+ });
139
+ it("returns undefined for npm: source", () => {
140
+ assert.equal(resolveLocalSourcePath("npm:@foo/bar", "/tmp"), undefined);
141
+ });
142
+ it("returns undefined for git URL", () => {
143
+ assert.equal(resolveLocalSourcePath("git:github.com/user/repo", "/tmp"), undefined);
144
+ });
145
+ it("returns undefined for https git URL", () => {
146
+ assert.equal(resolveLocalSourcePath("https://github.com/user/repo", "/tmp"), undefined);
147
+ });
148
+ it("resolves ~ to homedir", () => {
149
+ const result = resolveLocalSourcePath("~", "/tmp");
150
+ if (existsSync(homedir())) {
151
+ assert.equal(result, homedir());
152
+ }
153
+ else {
154
+ assert.equal(result, undefined);
155
+ }
156
+ });
157
+ it("resolves ~/path relative to homedir", () => {
158
+ const result = resolveLocalSourcePath("~/", "/tmp");
159
+ if (existsSync(homedir())) {
160
+ assert.equal(result, homedir());
161
+ }
162
+ else {
163
+ assert.equal(result, undefined);
164
+ }
165
+ });
166
+ it("resolves relative path that exists", (t) => {
167
+ const dir = tmpDir("resolve-rel", t);
168
+ const sub = join(dir, "myext");
169
+ mkdirSync(sub, { recursive: true });
170
+ const result = resolveLocalSourcePath("myext", dir);
171
+ assert.equal(result, resolve(dir, "myext"));
172
+ });
173
+ it("returns undefined for relative path that does not exist", (t) => {
174
+ const dir = tmpDir("resolve-noexist", t);
175
+ assert.equal(resolveLocalSourcePath("nonexistent", dir), undefined);
176
+ });
177
+ it("resolves absolute path that exists", (t) => {
178
+ const dir = tmpDir("resolve-abs", t);
179
+ assert.equal(resolveLocalSourcePath(dir, "/irrelevant"), dir);
180
+ });
181
+ it("returns undefined for absolute path that does not exist", () => {
182
+ assert.equal(resolveLocalSourcePath("/tmp/__nonexistent_path_for_test__", "/tmp"), undefined);
183
+ });
184
+ });
185
+ //# sourceMappingURL=lifecycle-hooks.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lifecycle-hooks.test.js","sourceRoot":"","sources":["../../src/core/lifecycle-hooks.test.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from \"node:assert/strict\";\nimport { mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync } from \"node:fs\";\nimport { homedir, tmpdir } from \"node:os\";\nimport { join, resolve } from \"node:path\";\nimport { describe, it } from \"node:test\";\nimport {\n\treadManifestRuntimeDeps,\n\tcollectRuntimeDependencies,\n\tverifyRuntimeDependencies,\n\tresolveLocalSourcePath,\n} from \"./lifecycle-hooks.js\";\n\nfunction tmpDir(prefix: string, t: { after: (fn: () => void) => void }): string {\n\tconst dir = mkdtempSync(join(tmpdir(), `pi-lh-${prefix}-`));\n\tt.after(() => rmSync(dir, { recursive: true, force: true }));\n\treturn dir;\n}\n\n// ─── readManifestRuntimeDeps ──────────────────────────────────────────────────\n\ndescribe(\"readManifestRuntimeDeps\", () => {\n\tit(\"returns empty array when manifest file is missing\", (t) => {\n\t\tconst dir = tmpDir(\"no-manifest\", t);\n\t\tassert.deepEqual(readManifestRuntimeDeps(dir), []);\n\t});\n\n\tit(\"returns empty array for malformed JSON\", (t) => {\n\t\tconst dir = tmpDir(\"bad-json\", t);\n\t\twriteFileSync(join(dir, \"extension-manifest.json\"), \"not json{{{\", \"utf-8\");\n\t\tassert.deepEqual(readManifestRuntimeDeps(dir), []);\n\t});\n\n\tit(\"returns runtime deps from valid manifest\", (t) => {\n\t\tconst dir = tmpDir(\"valid\", t);\n\t\twriteFileSync(join(dir, \"extension-manifest.json\"), JSON.stringify({\n\t\t\tdependencies: { runtime: [\"claude\", \"node\"] },\n\t\t}), \"utf-8\");\n\t\tassert.deepEqual(readManifestRuntimeDeps(dir), [\"claude\", \"node\"]);\n\t});\n\n\tit(\"returns empty array when dependencies exists but runtime is missing\", (t) => {\n\t\tconst dir = tmpDir(\"no-runtime\", t);\n\t\twriteFileSync(join(dir, \"extension-manifest.json\"), JSON.stringify({\n\t\t\tdependencies: {},\n\t\t}), \"utf-8\");\n\t\tassert.deepEqual(readManifestRuntimeDeps(dir), []);\n\t});\n\n\tit(\"returns empty array when runtime is empty\", (t) => {\n\t\tconst dir = tmpDir(\"empty-runtime\", t);\n\t\twriteFileSync(join(dir, \"extension-manifest.json\"), JSON.stringify({\n\t\t\tdependencies: { runtime: [] },\n\t\t}), \"utf-8\");\n\t\tassert.deepEqual(readManifestRuntimeDeps(dir), []);\n\t});\n\n\tit(\"filters out non-string entries in runtime array\", (t) => {\n\t\tconst dir = tmpDir(\"mixed-types\", t);\n\t\twriteFileSync(join(dir, \"extension-manifest.json\"), JSON.stringify({\n\t\t\tdependencies: { runtime: [123, null, \"node\", false, \"python\"] },\n\t\t}), \"utf-8\");\n\t\tassert.deepEqual(readManifestRuntimeDeps(dir), [\"node\", \"python\"]);\n\t});\n\n\tit(\"returns empty array when no dependencies field at all\", (t) => {\n\t\tconst dir = tmpDir(\"no-deps-field\", t);\n\t\twriteFileSync(join(dir, \"extension-manifest.json\"), JSON.stringify({\n\t\t\tid: \"test\",\n\t\t\tname: \"Test\",\n\t\t}), \"utf-8\");\n\t\tassert.deepEqual(readManifestRuntimeDeps(dir), []);\n\t});\n});\n\n// ─── collectRuntimeDependencies ───────────────────────────────────────────────\n\ndescribe(\"collectRuntimeDependencies\", () => {\n\tit(\"aggregates deps from installedPath manifest\", (t) => {\n\t\tconst dir = tmpDir(\"collect-installed\", t);\n\t\twriteFileSync(join(dir, \"extension-manifest.json\"), JSON.stringify({\n\t\t\tdependencies: { runtime: [\"claude\"] },\n\t\t}), \"utf-8\");\n\t\tassert.deepEqual(collectRuntimeDependencies(dir, []), [\"claude\"]);\n\t});\n\n\tit(\"aggregates deps from entry path directory manifests\", (t) => {\n\t\tconst root = tmpDir(\"collect-entry\", t);\n\t\tconst installedDir = join(root, \"installed\");\n\t\tconst entryDir = join(root, \"entry\");\n\t\tmkdirSync(installedDir, { recursive: true });\n\t\tmkdirSync(entryDir, { recursive: true });\n\t\twriteFileSync(join(entryDir, \"extension-manifest.json\"), JSON.stringify({\n\t\t\tdependencies: { runtime: [\"python\"] },\n\t\t}), \"utf-8\");\n\t\tconst deps = collectRuntimeDependencies(installedDir, [join(entryDir, \"index.ts\")]);\n\t\tassert.deepEqual(deps, [\"python\"]);\n\t});\n\n\tit(\"deduplicates across multiple directories\", (t) => {\n\t\tconst root = tmpDir(\"collect-dedup\", t);\n\t\tconst dir1 = join(root, \"dir1\");\n\t\tconst dir2 = join(root, \"dir2\");\n\t\tmkdirSync(dir1, { recursive: true });\n\t\tmkdirSync(dir2, { recursive: true });\n\t\twriteFileSync(join(dir1, \"extension-manifest.json\"), JSON.stringify({\n\t\t\tdependencies: { runtime: [\"node\", \"python\"] },\n\t\t}), \"utf-8\");\n\t\twriteFileSync(join(dir2, \"extension-manifest.json\"), JSON.stringify({\n\t\t\tdependencies: { runtime: [\"python\", \"claude\"] },\n\t\t}), \"utf-8\");\n\t\tconst deps = collectRuntimeDependencies(dir1, [join(dir2, \"index.ts\")]);\n\t\tassert.equal(deps.length, 3);\n\t\tassert.ok(deps.includes(\"node\"));\n\t\tassert.ok(deps.includes(\"python\"));\n\t\tassert.ok(deps.includes(\"claude\"));\n\t});\n\n\tit(\"returns empty when no directories have manifests\", (t) => {\n\t\tconst dir = tmpDir(\"collect-empty\", t);\n\t\tassert.deepEqual(collectRuntimeDependencies(dir, []), []);\n\t});\n});\n\n// ─── verifyRuntimeDependencies ────────────────────────────────────────────────\n\ndescribe(\"verifyRuntimeDependencies\", () => {\n\tit(\"does not throw for empty deps array\", () => {\n\t\tassert.doesNotThrow(() => verifyRuntimeDependencies([], \"test-source\", \"pi\"));\n\t});\n\n\tit(\"does not throw when all deps are present\", () => {\n\t\tassert.doesNotThrow(() => verifyRuntimeDependencies([\"node\"], \"test-source\", \"pi\"));\n\t});\n\n\tit(\"throws for missing dep with 'Missing runtime dependencies' message\", () => {\n\t\tassert.throws(\n\t\t\t() => verifyRuntimeDependencies([\"__nonexistent_dep_for_test__\"], \"test-source\", \"pi\"),\n\t\t\t(err: Error) => {\n\t\t\t\tassert.ok(err.message.includes(\"Missing runtime dependencies\"));\n\t\t\t\tassert.ok(err.message.includes(\"__nonexistent_dep_for_test__\"));\n\t\t\t\treturn true;\n\t\t\t},\n\t\t);\n\t});\n\n\tit(\"lists all missing deps in error message\", () => {\n\t\tassert.throws(\n\t\t\t() => verifyRuntimeDependencies([\"__missing_1__\", \"__missing_2__\"], \"test-source\", \"pi\"),\n\t\t\t(err: Error) => {\n\t\t\t\tassert.ok(err.message.includes(\"__missing_1__\"));\n\t\t\t\tassert.ok(err.message.includes(\"__missing_2__\"));\n\t\t\t\treturn true;\n\t\t\t},\n\t\t);\n\t});\n\n\tit(\"includes appName and source in error for retry hint\", () => {\n\t\tassert.throws(\n\t\t\t() => verifyRuntimeDependencies([\"__missing__\"], \"github:user/repo\", \"gsd\"),\n\t\t\t(err: Error) => {\n\t\t\t\tassert.ok(err.message.includes(\"gsd\"));\n\t\t\t\tassert.ok(err.message.includes(\"github:user/repo\"));\n\t\t\t\treturn true;\n\t\t\t},\n\t\t);\n\t});\n});\n\n// ─── resolveLocalSourcePath ───────────────────────────────────────────────────\n\ndescribe(\"resolveLocalSourcePath\", () => {\n\tit(\"returns undefined for empty string\", () => {\n\t\tassert.equal(resolveLocalSourcePath(\"\", \"/tmp\"), undefined);\n\t});\n\n\tit(\"returns undefined for npm: source\", () => {\n\t\tassert.equal(resolveLocalSourcePath(\"npm:@foo/bar\", \"/tmp\"), undefined);\n\t});\n\n\tit(\"returns undefined for git URL\", () => {\n\t\tassert.equal(resolveLocalSourcePath(\"git:github.com/user/repo\", \"/tmp\"), undefined);\n\t});\n\n\tit(\"returns undefined for https git URL\", () => {\n\t\tassert.equal(resolveLocalSourcePath(\"https://github.com/user/repo\", \"/tmp\"), undefined);\n\t});\n\n\tit(\"resolves ~ to homedir\", () => {\n\t\tconst result = resolveLocalSourcePath(\"~\", \"/tmp\");\n\t\tif (existsSync(homedir())) {\n\t\t\tassert.equal(result, homedir());\n\t\t} else {\n\t\t\tassert.equal(result, undefined);\n\t\t}\n\t});\n\n\tit(\"resolves ~/path relative to homedir\", () => {\n\t\tconst result = resolveLocalSourcePath(\"~/\", \"/tmp\");\n\t\tif (existsSync(homedir())) {\n\t\t\tassert.equal(result, homedir());\n\t\t} else {\n\t\t\tassert.equal(result, undefined);\n\t\t}\n\t});\n\n\tit(\"resolves relative path that exists\", (t) => {\n\t\tconst dir = tmpDir(\"resolve-rel\", t);\n\t\tconst sub = join(dir, \"myext\");\n\t\tmkdirSync(sub, { recursive: true });\n\t\tconst result = resolveLocalSourcePath(\"myext\", dir);\n\t\tassert.equal(result, resolve(dir, \"myext\"));\n\t});\n\n\tit(\"returns undefined for relative path that does not exist\", (t) => {\n\t\tconst dir = tmpDir(\"resolve-noexist\", t);\n\t\tassert.equal(resolveLocalSourcePath(\"nonexistent\", dir), undefined);\n\t});\n\n\tit(\"resolves absolute path that exists\", (t) => {\n\t\tconst dir = tmpDir(\"resolve-abs\", t);\n\t\tassert.equal(resolveLocalSourcePath(dir, \"/irrelevant\"), dir);\n\t});\n\n\tit(\"returns undefined for absolute path that does not exist\", () => {\n\t\tassert.equal(resolveLocalSourcePath(\"/tmp/__nonexistent_path_for_test__\", \"/tmp\"), undefined);\n\t});\n});\n"]}
@@ -1,5 +1,6 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { describe, it } from "node:test";
3
+ import { getApiProvider } from "@gsd/pi-ai";
3
4
  import { ModelRegistry } from "./model-registry.js";
4
5
  function createRegistry(hasAuthFn) {
5
6
  const authStorage = {
@@ -12,11 +13,11 @@ function createRegistry(hasAuthFn) {
12
13
  };
13
14
  return new ModelRegistry(authStorage, undefined);
14
15
  }
15
- function createProviderModel(id) {
16
+ function createProviderModel(id, api) {
16
17
  return {
17
18
  id,
18
19
  name: id,
19
- api: "openai-completions",
20
+ api: (api ?? "openai-completions"),
20
21
  reasoning: false,
21
22
  input: ["text"],
22
23
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
@@ -27,31 +28,79 @@ function createProviderModel(id) {
27
28
  function findModel(registry, provider, id) {
28
29
  return registry.getAvailable().find((m) => m.provider === provider && m.id === id);
29
30
  }
31
+ function makeModel(provider, id, api) {
32
+ return {
33
+ id,
34
+ name: id,
35
+ api: api,
36
+ provider,
37
+ baseUrl: `${provider}:`,
38
+ reasoning: false,
39
+ input: ["text"],
40
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
41
+ contextWindow: 128000,
42
+ maxTokens: 16384,
43
+ };
44
+ }
45
+ function makeContext() {
46
+ return {
47
+ systemPrompt: "test",
48
+ messages: [{ role: "user", content: "hello", timestamp: Date.now() }],
49
+ };
50
+ }
51
+ /** No-op streamSimple for tests that need one to pass validation but don't inspect it. */
52
+ const noopStreamSimple = (_model, _context, _options) => {
53
+ return {
54
+ [Symbol.asyncIterator]() { return { next: async () => ({ value: undefined, done: true }) }; },
55
+ result: () => Promise.resolve({ role: "assistant", content: [], api: "test", provider: "test", model: "test", usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } }, stopReason: "stop", timestamp: Date.now() }),
56
+ push: () => { },
57
+ end: () => { },
58
+ };
59
+ };
60
+ /** Create a spy streamSimple that captures the options it receives and returns a stub stream. */
61
+ function createStreamSpy() {
62
+ let capturedOptions;
63
+ const streamSimple = (_model, _context, options) => {
64
+ capturedOptions = options;
65
+ // Return a minimal stub that satisfies AssistantMessageEventStream
66
+ return {
67
+ [Symbol.asyncIterator]() { return { next: async () => ({ value: undefined, done: true }) }; },
68
+ result: () => Promise.resolve({ role: "assistant", content: [], api: "test", provider: "test", model: "test", usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } }, stopReason: "stop", timestamp: Date.now() }),
69
+ push: () => { },
70
+ end: () => { },
71
+ };
72
+ };
73
+ return { streamSimple, getCapturedOptions: () => capturedOptions };
74
+ }
30
75
  // ─── Registration ─────────────────────────────────────────────────────────────
31
76
  describe("ModelRegistry authMode — registration", () => {
32
- it("registers externalCli provider without apiKey/oauth", () => {
77
+ it("registers externalCli provider with streamSimple and without apiKey/oauth", () => {
33
78
  const registry = createRegistry();
79
+ const spy = createStreamSpy();
34
80
  assert.doesNotThrow(() => {
35
81
  registry.registerProvider("cli-provider", {
36
82
  authMode: "externalCli",
37
83
  baseUrl: "https://cli.local",
38
84
  api: "openai-completions",
85
+ streamSimple: spy.streamSimple,
39
86
  models: [createProviderModel("cli-model")],
40
87
  });
41
88
  });
42
89
  });
43
- it("registers none provider without apiKey/oauth", () => {
90
+ it("registers none provider with streamSimple and without apiKey/oauth", () => {
44
91
  const registry = createRegistry();
92
+ const spy = createStreamSpy();
45
93
  assert.doesNotThrow(() => {
46
94
  registry.registerProvider("none-provider", {
47
95
  authMode: "none",
48
96
  baseUrl: "http://localhost:11434",
49
97
  api: "openai-completions",
98
+ streamSimple: spy.streamSimple,
50
99
  models: [createProviderModel("local-model")],
51
100
  });
52
101
  });
53
102
  });
54
- it("rejects apiKey provider without apiKey or oauth", () => {
103
+ it("rejects apiKey provider without apiKey or oauth — message mentions authMode", () => {
55
104
  const registry = createRegistry();
56
105
  assert.throws(() => {
57
106
  registry.registerProvider("apikey-provider", {
@@ -60,6 +109,10 @@ describe("ModelRegistry authMode — registration", () => {
60
109
  api: "openai-completions",
61
110
  models: [createProviderModel("model")],
62
111
  });
112
+ }, (err) => {
113
+ assert.ok(err.message.includes("authMode"), "error message must mention authMode");
114
+ assert.ok(err.message.includes("externalCli"), "error message must suggest externalCli");
115
+ return true;
63
116
  });
64
117
  });
65
118
  it("rejects provider with no authMode and no apiKey/oauth (defaults to apiKey)", () => {
@@ -70,6 +123,75 @@ describe("ModelRegistry authMode — registration", () => {
70
123
  api: "openai-completions",
71
124
  models: [createProviderModel("model")],
72
125
  });
126
+ }, (err) => {
127
+ assert.ok(err.message.includes("authMode"), "error message must mention authMode");
128
+ return true;
129
+ });
130
+ });
131
+ it("rejects externalCli provider without streamSimple", () => {
132
+ const registry = createRegistry();
133
+ assert.throws(() => {
134
+ registry.registerProvider("cli-no-stream", {
135
+ authMode: "externalCli",
136
+ baseUrl: "https://cli.local",
137
+ api: "openai-completions",
138
+ models: [createProviderModel("model")],
139
+ });
140
+ }, (err) => {
141
+ assert.ok(err.message.includes("streamSimple"), "error message must mention streamSimple");
142
+ assert.ok(err.message.includes("externalCli"), "error message must mention authMode");
143
+ return true;
144
+ });
145
+ });
146
+ it("rejects none provider without streamSimple", () => {
147
+ const registry = createRegistry();
148
+ assert.throws(() => {
149
+ registry.registerProvider("none-no-stream", {
150
+ authMode: "none",
151
+ baseUrl: "http://localhost:11434",
152
+ api: "openai-completions",
153
+ models: [createProviderModel("model")],
154
+ });
155
+ }, (err) => {
156
+ assert.ok(err.message.includes("streamSimple"), "error message must mention streamSimple");
157
+ assert.ok(err.message.includes("none"), "error message must mention authMode");
158
+ return true;
159
+ });
160
+ });
161
+ it("rejects externalCli provider that also sets apiKey", () => {
162
+ const registry = createRegistry();
163
+ const spy = createStreamSpy();
164
+ assert.throws(() => {
165
+ registry.registerProvider("cli-with-key", {
166
+ authMode: "externalCli",
167
+ baseUrl: "https://cli.local",
168
+ api: "openai-completions",
169
+ apiKey: "SHOULD_NOT_EXIST",
170
+ streamSimple: spy.streamSimple,
171
+ models: [createProviderModel("model")],
172
+ });
173
+ }, (err) => {
174
+ assert.ok(err.message.includes("apiKey"), "error message must mention apiKey");
175
+ assert.ok(err.message.includes("externalCli"), "error message must mention authMode");
176
+ return true;
177
+ });
178
+ });
179
+ it("rejects none provider that also sets apiKey", () => {
180
+ const registry = createRegistry();
181
+ const spy = createStreamSpy();
182
+ assert.throws(() => {
183
+ registry.registerProvider("none-with-key", {
184
+ authMode: "none",
185
+ baseUrl: "http://localhost:11434",
186
+ api: "openai-completions",
187
+ apiKey: "SHOULD_NOT_EXIST",
188
+ streamSimple: spy.streamSimple,
189
+ models: [createProviderModel("model")],
190
+ });
191
+ }, (err) => {
192
+ assert.ok(err.message.includes("apiKey"), "error message must mention apiKey");
193
+ assert.ok(err.message.includes("none"), "error message must mention authMode");
194
+ return true;
73
195
  });
74
196
  });
75
197
  });
@@ -85,6 +207,7 @@ describe("ModelRegistry authMode — getProviderAuthMode", () => {
85
207
  authMode: "externalCli",
86
208
  baseUrl: "https://cli.local",
87
209
  api: "openai-completions",
210
+ streamSimple: noopStreamSimple,
88
211
  models: [createProviderModel("m")],
89
212
  });
90
213
  assert.equal(registry.getProviderAuthMode("cli"), "externalCli");
@@ -95,6 +218,7 @@ describe("ModelRegistry authMode — getProviderAuthMode", () => {
95
218
  authMode: "none",
96
219
  baseUrl: "http://localhost:11434",
97
220
  api: "openai-completions",
221
+ streamSimple: noopStreamSimple,
98
222
  models: [createProviderModel("m")],
99
223
  });
100
224
  assert.equal(registry.getProviderAuthMode("local"), "none");
@@ -108,6 +232,7 @@ describe("ModelRegistry authMode — isProviderRequestReady", () => {
108
232
  authMode: "externalCli",
109
233
  baseUrl: "https://cli.local",
110
234
  api: "openai-completions",
235
+ streamSimple: noopStreamSimple,
111
236
  models: [createProviderModel("m")],
112
237
  });
113
238
  assert.equal(registry.isProviderRequestReady("cli"), true);
@@ -118,6 +243,7 @@ describe("ModelRegistry authMode — isProviderRequestReady", () => {
118
243
  authMode: "none",
119
244
  baseUrl: "http://localhost:11434",
120
245
  api: "openai-completions",
246
+ streamSimple: noopStreamSimple,
121
247
  models: [createProviderModel("m")],
122
248
  });
123
249
  assert.equal(registry.isProviderRequestReady("local"), true);
@@ -139,6 +265,7 @@ describe("ModelRegistry authMode — isReady callback", () => {
139
265
  authMode: "externalCli",
140
266
  baseUrl: "https://cli.local",
141
267
  api: "openai-completions",
268
+ streamSimple: noopStreamSimple,
142
269
  isReady: () => false,
143
270
  models: [createProviderModel("m")],
144
271
  });
@@ -161,6 +288,7 @@ describe("ModelRegistry authMode — isReady callback", () => {
161
288
  authMode: "externalCli",
162
289
  baseUrl: "https://cli.local",
163
290
  api: "openai-completions",
291
+ streamSimple: noopStreamSimple,
164
292
  isReady: () => true,
165
293
  models: [createProviderModel("m")],
166
294
  });
@@ -172,6 +300,7 @@ describe("ModelRegistry authMode — isReady callback", () => {
172
300
  authMode: "externalCli",
173
301
  baseUrl: "https://cli.local",
174
302
  api: "openai-completions",
303
+ streamSimple: noopStreamSimple,
175
304
  models: [createProviderModel("m")],
176
305
  });
177
306
  // externalCli without isReady → true (default)
@@ -186,6 +315,7 @@ describe("ModelRegistry authMode — getAvailable", () => {
186
315
  authMode: "externalCli",
187
316
  baseUrl: "https://cli.local",
188
317
  api: "openai-completions",
318
+ streamSimple: noopStreamSimple,
189
319
  models: [createProviderModel("cli-model")],
190
320
  });
191
321
  assert.ok(findModel(registry, "cli", "cli-model"));
@@ -196,6 +326,7 @@ describe("ModelRegistry authMode — getAvailable", () => {
196
326
  authMode: "none",
197
327
  baseUrl: "http://localhost:11434",
198
328
  api: "openai-completions",
329
+ streamSimple: noopStreamSimple,
199
330
  models: [createProviderModel("local-model")],
200
331
  });
201
332
  assert.ok(findModel(registry, "local", "local-model"));
@@ -206,6 +337,7 @@ describe("ModelRegistry authMode — getAvailable", () => {
206
337
  authMode: "externalCli",
207
338
  baseUrl: "https://cli.local",
208
339
  api: "openai-completions",
340
+ streamSimple: noopStreamSimple,
209
341
  isReady: () => false,
210
342
  models: [createProviderModel("m")],
211
343
  });
@@ -213,10 +345,7 @@ describe("ModelRegistry authMode — getAvailable", () => {
213
345
  });
214
346
  it("excludes apiKey models without stored auth", () => {
215
347
  const registry = createRegistry(() => false);
216
- // Built-in providers have no registeredProviders entry, so authMode defaults to apiKey
217
- // getAvailable filters by isProviderRequestReady → hasAuth → false
218
348
  const available = registry.getAvailable();
219
- // No models should be available since hasAuth returns false for everything
220
349
  assert.equal(available.length, 0);
221
350
  });
222
351
  });
@@ -228,6 +357,7 @@ describe("ModelRegistry authMode — getApiKey", () => {
228
357
  authMode: "externalCli",
229
358
  baseUrl: "https://cli.local",
230
359
  api: "openai-completions",
360
+ streamSimple: noopStreamSimple,
231
361
  models: [createProviderModel("m")],
232
362
  });
233
363
  const model = registry.getAll().find((m) => m.provider === "cli");
@@ -239,6 +369,7 @@ describe("ModelRegistry authMode — getApiKey", () => {
239
369
  authMode: "none",
240
370
  baseUrl: "http://localhost:11434",
241
371
  api: "openai-completions",
372
+ streamSimple: noopStreamSimple,
242
373
  models: [createProviderModel("m")],
243
374
  });
244
375
  const model = registry.getAll().find((m) => m.provider === "local");
@@ -246,10 +377,108 @@ describe("ModelRegistry authMode — getApiKey", () => {
246
377
  });
247
378
  it("delegates to authStorage for apiKey provider", async () => {
248
379
  const registry = createRegistry();
249
- // authStorage.getApiKey returns undefined (no key configured)
250
- // For apiKey providers this is an expected "no key" response, not early exit
251
380
  const key = await registry.getApiKeyForProvider("anthropic");
252
381
  assert.equal(key, undefined);
253
382
  });
254
383
  });
384
+ // ─── streamSimple apiKey stripping ────────────────────────────────────────────
385
+ describe("ModelRegistry authMode — streamSimple apiKey boundary", () => {
386
+ it("strips apiKey from options for externalCli provider", () => {
387
+ const registry = createRegistry();
388
+ const spy = createStreamSpy();
389
+ const apiType = `ext-cli-strip-${Date.now()}`;
390
+ registry.registerProvider("cli-strip", {
391
+ authMode: "externalCli",
392
+ baseUrl: "https://cli.local",
393
+ api: apiType,
394
+ streamSimple: spy.streamSimple,
395
+ models: [createProviderModel("m", apiType)],
396
+ });
397
+ const provider = getApiProvider(apiType);
398
+ assert.ok(provider, "provider must be registered in api registry");
399
+ provider.streamSimple(makeModel("cli-strip", "m", apiType), makeContext(), { apiKey: "should-be-stripped", maxTokens: 1024 });
400
+ const captured = spy.getCapturedOptions();
401
+ assert.ok(captured, "streamSimple must have been called");
402
+ assert.equal("apiKey" in captured, false, "apiKey must not exist in options for externalCli provider");
403
+ assert.equal(captured.maxTokens, 1024, "other options must pass through");
404
+ });
405
+ it("strips apiKey from options for none provider", () => {
406
+ const registry = createRegistry();
407
+ const spy = createStreamSpy();
408
+ const apiType = `none-strip-${Date.now()}`;
409
+ registry.registerProvider("none-strip", {
410
+ authMode: "none",
411
+ baseUrl: "http://localhost:11434",
412
+ api: apiType,
413
+ streamSimple: spy.streamSimple,
414
+ models: [createProviderModel("m", apiType)],
415
+ });
416
+ const provider = getApiProvider(apiType);
417
+ assert.ok(provider, "provider must be registered in api registry");
418
+ provider.streamSimple(makeModel("none-strip", "m", apiType), makeContext(), { apiKey: "should-be-stripped", maxTokens: 2048 });
419
+ const captured = spy.getCapturedOptions();
420
+ assert.ok(captured, "streamSimple must have been called");
421
+ assert.equal("apiKey" in captured, false, "apiKey must not exist in options for none provider");
422
+ assert.equal(captured.maxTokens, 2048, "other options must pass through");
423
+ });
424
+ it("preserves apiKey in options for apiKey provider", () => {
425
+ const registry = createRegistry();
426
+ const spy = createStreamSpy();
427
+ const apiType = `apikey-preserve-${Date.now()}`;
428
+ registry.registerProvider("apikey-preserve", {
429
+ apiKey: "MY_KEY",
430
+ baseUrl: "https://api.local",
431
+ api: apiType,
432
+ streamSimple: spy.streamSimple,
433
+ models: [createProviderModel("m", apiType)],
434
+ });
435
+ const provider = getApiProvider(apiType);
436
+ assert.ok(provider, "provider must be registered in api registry");
437
+ provider.streamSimple(makeModel("apikey-preserve", "m", apiType), makeContext(), { apiKey: "sk-real-key", maxTokens: 4096 });
438
+ const captured = spy.getCapturedOptions();
439
+ assert.ok(captured, "streamSimple must have been called");
440
+ assert.equal(captured.apiKey, "sk-real-key", "apiKey must be preserved for apiKey provider");
441
+ assert.equal(captured.maxTokens, 4096, "other options must pass through");
442
+ });
443
+ it("handles undefined options for externalCli provider", () => {
444
+ const registry = createRegistry();
445
+ const spy = createStreamSpy();
446
+ const apiType = `ext-cli-undef-${Date.now()}`;
447
+ registry.registerProvider("cli-undef", {
448
+ authMode: "externalCli",
449
+ baseUrl: "https://cli.local",
450
+ api: apiType,
451
+ streamSimple: spy.streamSimple,
452
+ models: [createProviderModel("m", apiType)],
453
+ });
454
+ const provider = getApiProvider(apiType);
455
+ assert.ok(provider, "provider must be registered in api registry");
456
+ provider.streamSimple(makeModel("cli-undef", "m", apiType), makeContext(), undefined);
457
+ const captured = spy.getCapturedOptions();
458
+ assert.ok(captured !== undefined, "streamSimple must have been called");
459
+ assert.equal("apiKey" in captured, false, "apiKey must not exist even when options is undefined");
460
+ });
461
+ it("strips apiKey but preserves signal and other fields for externalCli", () => {
462
+ const registry = createRegistry();
463
+ const spy = createStreamSpy();
464
+ const apiType = `ext-cli-fields-${Date.now()}`;
465
+ const abortController = new AbortController();
466
+ registry.registerProvider("cli-fields", {
467
+ authMode: "externalCli",
468
+ baseUrl: "https://cli.local",
469
+ api: apiType,
470
+ streamSimple: spy.streamSimple,
471
+ models: [createProviderModel("m", apiType)],
472
+ });
473
+ const provider = getApiProvider(apiType);
474
+ assert.ok(provider, "provider must be registered in api registry");
475
+ provider.streamSimple(makeModel("cli-fields", "m", apiType), makeContext(), { apiKey: "strip-me", maxTokens: 8192, signal: abortController.signal, reasoning: "high" });
476
+ const captured = spy.getCapturedOptions();
477
+ assert.ok(captured, "streamSimple must have been called");
478
+ assert.equal("apiKey" in captured, false, "apiKey must be stripped");
479
+ assert.equal(captured.maxTokens, 8192, "maxTokens must pass through");
480
+ assert.equal(captured.signal, abortController.signal, "signal must pass through");
481
+ assert.equal(captured.reasoning, "high", "reasoning must pass through");
482
+ });
483
+ });
255
484
  //# sourceMappingURL=model-registry-auth-mode.test.js.map