@mrpatronz/nexusflow 2.8.1 → 2.10.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 (732) hide show
  1. package/README.md +172 -113
  2. package/dist/agent/AcpCliAdapter.d.ts +2 -0
  3. package/dist/agent/AcpCliAdapter.d.ts.map +1 -1
  4. package/dist/agent/AcpCliAdapter.js +50 -4
  5. package/dist/agent/AcpCliAdapter.js.map +1 -1
  6. package/dist/agent/AntigravityCliAdapter.d.ts +44 -2
  7. package/dist/agent/AntigravityCliAdapter.d.ts.map +1 -1
  8. package/dist/agent/AntigravityCliAdapter.js +363 -16
  9. package/dist/agent/AntigravityCliAdapter.js.map +1 -1
  10. package/dist/agent/ClaudeCliAdapter.d.ts +3 -0
  11. package/dist/agent/ClaudeCliAdapter.d.ts.map +1 -1
  12. package/dist/agent/ClaudeCliAdapter.js +37 -10
  13. package/dist/agent/ClaudeCliAdapter.js.map +1 -1
  14. package/dist/agent/ClaudeSdkAdapter.d.ts +1 -0
  15. package/dist/agent/ClaudeSdkAdapter.d.ts.map +1 -1
  16. package/dist/agent/ClaudeSdkAdapter.js +38 -11
  17. package/dist/agent/ClaudeSdkAdapter.js.map +1 -1
  18. package/dist/agent/CliAdapterBase.d.ts +2 -0
  19. package/dist/agent/CliAdapterBase.d.ts.map +1 -1
  20. package/dist/agent/CliAdapterBase.js +5 -1
  21. package/dist/agent/CliAdapterBase.js.map +1 -1
  22. package/dist/agent/CodexCliAdapter.d.ts +1 -1
  23. package/dist/agent/CodexCliAdapter.d.ts.map +1 -1
  24. package/dist/agent/CodexCliAdapter.js +5 -4
  25. package/dist/agent/CodexCliAdapter.js.map +1 -1
  26. package/dist/agent/CodexSdkAdapter.d.ts.map +1 -1
  27. package/dist/agent/CodexSdkAdapter.js +4 -1
  28. package/dist/agent/CodexSdkAdapter.js.map +1 -1
  29. package/dist/agent/CopilotAcpAdapter.d.ts.map +1 -1
  30. package/dist/agent/CopilotAcpAdapter.js +2 -2
  31. package/dist/agent/CopilotAcpAdapter.js.map +1 -1
  32. package/dist/agent/ProviderRegistry.d.ts +8 -1
  33. package/dist/agent/ProviderRegistry.d.ts.map +1 -1
  34. package/dist/agent/ProviderRegistry.js.map +1 -1
  35. package/dist/agent/TurnSessionManager.d.ts +65 -0
  36. package/dist/agent/TurnSessionManager.d.ts.map +1 -0
  37. package/dist/agent/TurnSessionManager.js +330 -0
  38. package/dist/agent/TurnSessionManager.js.map +1 -0
  39. package/dist/agent/adapters.d.ts.map +1 -1
  40. package/dist/agent/adapters.js +23 -6
  41. package/dist/agent/adapters.js.map +1 -1
  42. package/dist/agent/cliAvailability.d.ts.map +1 -1
  43. package/dist/agent/cliAvailability.js +12 -0
  44. package/dist/agent/cliAvailability.js.map +1 -1
  45. package/dist/agent/mcp-config.d.ts +0 -5
  46. package/dist/agent/mcp-config.d.ts.map +1 -1
  47. package/dist/agent/mcp-config.js +2 -1
  48. package/dist/agent/mcp-config.js.map +1 -1
  49. package/dist/agent/models.d.ts +13 -0
  50. package/dist/agent/models.d.ts.map +1 -1
  51. package/dist/agent/models.js +262 -8
  52. package/dist/agent/models.js.map +1 -1
  53. package/dist/agent/nativeTools.d.ts.map +1 -1
  54. package/dist/agent/nativeTools.js +1 -0
  55. package/dist/agent/nativeTools.js.map +1 -1
  56. package/dist/agent/orchestrator.d.ts.map +1 -1
  57. package/dist/agent/orchestrator.js +3 -2
  58. package/dist/agent/orchestrator.js.map +1 -1
  59. package/dist/agent/session.d.ts +1 -0
  60. package/dist/agent/session.d.ts.map +1 -1
  61. package/dist/agent/session.js.map +1 -1
  62. package/dist/analyzers/detect-deps.js +16 -1
  63. package/dist/analyzers/detect-deps.js.map +1 -1
  64. package/dist/analyzers/run-analyzer.d.ts.map +1 -1
  65. package/dist/analyzers/run-analyzer.js.map +1 -1
  66. package/dist/commands/adapter.d.ts.map +1 -1
  67. package/dist/commands/adapter.js +15 -14
  68. package/dist/commands/adapter.js.map +1 -1
  69. package/dist/commands/add-repo.d.ts.map +1 -1
  70. package/dist/commands/add-repo.js +6 -5
  71. package/dist/commands/add-repo.js.map +1 -1
  72. package/dist/commands/commit.js +6 -8
  73. package/dist/commands/commit.js.map +1 -1
  74. package/dist/commands/config.d.ts.map +1 -1
  75. package/dist/commands/config.js +2 -1
  76. package/dist/commands/config.js.map +1 -1
  77. package/dist/commands/create.d.ts +4 -4
  78. package/dist/commands/create.d.ts.map +1 -1
  79. package/dist/commands/create.js +8 -7
  80. package/dist/commands/create.js.map +1 -1
  81. package/dist/commands/desktop.d.ts +44 -3
  82. package/dist/commands/desktop.d.ts.map +1 -1
  83. package/dist/commands/desktop.js +246 -7
  84. package/dist/commands/desktop.js.map +1 -1
  85. package/dist/commands/diff.d.ts.map +1 -1
  86. package/dist/commands/diff.js +3 -2
  87. package/dist/commands/diff.js.map +1 -1
  88. package/dist/commands/doctor.d.ts.map +1 -1
  89. package/dist/commands/doctor.js +3 -2
  90. package/dist/commands/doctor.js.map +1 -1
  91. package/dist/commands/finish.js +2 -1
  92. package/dist/commands/finish.js.map +1 -1
  93. package/dist/commands/handoff.d.ts.map +1 -1
  94. package/dist/commands/handoff.js +12 -12
  95. package/dist/commands/handoff.js.map +1 -1
  96. package/dist/commands/init.d.ts +2 -2
  97. package/dist/commands/init.d.ts.map +1 -1
  98. package/dist/commands/init.js +8 -7
  99. package/dist/commands/init.js.map +1 -1
  100. package/dist/commands/isolate.d.ts.map +1 -1
  101. package/dist/commands/isolate.js +2 -1
  102. package/dist/commands/isolate.js.map +1 -1
  103. package/dist/commands/knowledge.d.ts.map +1 -1
  104. package/dist/commands/knowledge.js +3 -2
  105. package/dist/commands/knowledge.js.map +1 -1
  106. package/dist/commands/list.d.ts.map +1 -1
  107. package/dist/commands/list.js +3 -2
  108. package/dist/commands/list.js.map +1 -1
  109. package/dist/commands/logs.js +3 -3
  110. package/dist/commands/logs.js.map +1 -1
  111. package/dist/commands/mcp.d.ts +1 -2
  112. package/dist/commands/mcp.d.ts.map +1 -1
  113. package/dist/commands/mcp.js +19 -12
  114. package/dist/commands/mcp.js.map +1 -1
  115. package/dist/commands/migrate.d.ts +12 -0
  116. package/dist/commands/migrate.d.ts.map +1 -0
  117. package/dist/commands/migrate.js +128 -0
  118. package/dist/commands/migrate.js.map +1 -0
  119. package/dist/commands/open.d.ts.map +1 -1
  120. package/dist/commands/open.js +2 -1
  121. package/dist/commands/open.js.map +1 -1
  122. package/dist/commands/progress.d.ts.map +1 -1
  123. package/dist/commands/progress.js +2 -1
  124. package/dist/commands/progress.js.map +1 -1
  125. package/dist/commands/project.d.ts.map +1 -1
  126. package/dist/commands/project.js +7 -6
  127. package/dist/commands/project.js.map +1 -1
  128. package/dist/commands/refresh.d.ts.map +1 -1
  129. package/dist/commands/refresh.js +8 -11
  130. package/dist/commands/refresh.js.map +1 -1
  131. package/dist/commands/remove.d.ts.map +1 -1
  132. package/dist/commands/remove.js +2 -1
  133. package/dist/commands/remove.js.map +1 -1
  134. package/dist/commands/schedule.js +16 -18
  135. package/dist/commands/schedule.js.map +1 -1
  136. package/dist/commands/start.d.ts.map +1 -1
  137. package/dist/commands/start.js +9 -8
  138. package/dist/commands/start.js.map +1 -1
  139. package/dist/commands/status.d.ts.map +1 -1
  140. package/dist/commands/status.js +2 -1
  141. package/dist/commands/status.js.map +1 -1
  142. package/dist/commands/stop.d.ts.map +1 -1
  143. package/dist/commands/stop.js +2 -1
  144. package/dist/commands/stop.js.map +1 -1
  145. package/dist/commands/strategy.d.ts.map +1 -1
  146. package/dist/commands/strategy.js +6 -5
  147. package/dist/commands/strategy.js.map +1 -1
  148. package/dist/commands/sync.js +8 -10
  149. package/dist/commands/sync.js.map +1 -1
  150. package/dist/commands/tui.d.ts.map +1 -1
  151. package/dist/commands/tui.js +11 -10
  152. package/dist/commands/tui.js.map +1 -1
  153. package/dist/commands/ui.d.ts.map +1 -1
  154. package/dist/commands/ui.js +4 -3
  155. package/dist/commands/ui.js.map +1 -1
  156. package/dist/core/adapters/local-storage.d.ts +3 -1
  157. package/dist/core/adapters/local-storage.d.ts.map +1 -1
  158. package/dist/core/adapters/local-storage.js +62 -4
  159. package/dist/core/adapters/local-storage.js.map +1 -1
  160. package/dist/core/analysis-cache.d.ts.map +1 -1
  161. package/dist/core/analysis-cache.js +8 -7
  162. package/dist/core/analysis-cache.js.map +1 -1
  163. package/dist/core/brand-config.d.ts +261 -0
  164. package/dist/core/brand-config.d.ts.map +1 -0
  165. package/dist/core/brand-config.js +514 -0
  166. package/dist/core/brand-config.js.map +1 -0
  167. package/dist/core/config.d.ts +1 -1
  168. package/dist/core/config.d.ts.map +1 -1
  169. package/dist/core/config.js +18 -8
  170. package/dist/core/config.js.map +1 -1
  171. package/dist/core/constants.d.ts +88 -0
  172. package/dist/core/constants.d.ts.map +1 -0
  173. package/dist/core/constants.js +103 -0
  174. package/dist/core/constants.js.map +1 -0
  175. package/dist/core/doctor.d.ts.map +1 -1
  176. package/dist/core/doctor.js +11 -3
  177. package/dist/core/doctor.js.map +1 -1
  178. package/dist/core/generation-lock.d.ts +2 -2
  179. package/dist/core/generation-lock.d.ts.map +1 -1
  180. package/dist/core/generation-lock.js +49 -22
  181. package/dist/core/generation-lock.js.map +1 -1
  182. package/dist/core/knowledge.d.ts +19 -0
  183. package/dist/core/knowledge.d.ts.map +1 -1
  184. package/dist/core/knowledge.js +150 -24
  185. package/dist/core/knowledge.js.map +1 -1
  186. package/dist/core/migrate.d.ts +53 -0
  187. package/dist/core/migrate.d.ts.map +1 -0
  188. package/dist/core/migrate.js +607 -0
  189. package/dist/core/migrate.js.map +1 -0
  190. package/dist/core/new-repo.js +1 -1
  191. package/dist/core/new-repo.js.map +1 -1
  192. package/dist/core/projects.d.ts.map +1 -1
  193. package/dist/core/projects.js +13 -4
  194. package/dist/core/projects.js.map +1 -1
  195. package/dist/core/refresh.d.ts.map +1 -1
  196. package/dist/core/refresh.js +6 -4
  197. package/dist/core/refresh.js.map +1 -1
  198. package/dist/core/scheduler.d.ts +1 -1
  199. package/dist/core/scheduler.d.ts.map +1 -1
  200. package/dist/core/scheduler.js +11 -9
  201. package/dist/core/scheduler.js.map +1 -1
  202. package/dist/core/sync.js +1 -1
  203. package/dist/core/sync.js.map +1 -1
  204. package/dist/core/workspace-git.d.ts.map +1 -1
  205. package/dist/core/workspace-git.js +34 -10
  206. package/dist/core/workspace-git.js.map +1 -1
  207. package/dist/core/workspace-state.d.ts +4 -0
  208. package/dist/core/workspace-state.d.ts.map +1 -1
  209. package/dist/core/workspace-state.js +8 -8
  210. package/dist/core/workspace-state.js.map +1 -1
  211. package/dist/core/workspace.d.ts +3 -4
  212. package/dist/core/workspace.d.ts.map +1 -1
  213. package/dist/core/workspace.js +62 -30
  214. package/dist/core/workspace.js.map +1 -1
  215. package/dist/desktop-server.js +6 -3
  216. package/dist/desktop-server.js.map +1 -1
  217. package/dist/generators/base.d.ts.map +1 -1
  218. package/dist/generators/base.js +7 -5
  219. package/dist/generators/base.js.map +1 -1
  220. package/dist/generators/claude.d.ts.map +1 -1
  221. package/dist/generators/claude.js +2 -1
  222. package/dist/generators/claude.js.map +1 -1
  223. package/dist/generators/cursor.d.ts +1 -1
  224. package/dist/generators/cursor.d.ts.map +1 -1
  225. package/dist/generators/cursor.js +9 -4
  226. package/dist/generators/cursor.js.map +1 -1
  227. package/dist/generators/index.d.ts +4 -8
  228. package/dist/generators/index.d.ts.map +1 -1
  229. package/dist/generators/index.js +34 -69
  230. package/dist/generators/index.js.map +1 -1
  231. package/dist/generators/plan-generator.d.ts.map +1 -1
  232. package/dist/generators/plan-generator.js +20 -11
  233. package/dist/generators/plan-generator.js.map +1 -1
  234. package/dist/generators/skills-generator.d.ts.map +1 -1
  235. package/dist/generators/skills-generator.js +26 -11
  236. package/dist/generators/skills-generator.js.map +1 -1
  237. package/dist/gui/app_logo.png +0 -0
  238. package/dist/gui/assets/AgentsPage-CnXzZFo1.js +3 -0
  239. package/dist/gui/assets/DashboardPage-rm3hKgsw.js +1 -0
  240. package/dist/gui/assets/GettingStartedPage-BKeuSHZQ.js +1 -0
  241. package/dist/gui/assets/NullStore-DuSDhGOe.js +1 -0
  242. package/dist/gui/assets/ProjectsPage-CioaA4uf.js +1 -0
  243. package/dist/gui/assets/ScaffoldRepoInline-DKZ1rPmE.js +1 -0
  244. package/dist/gui/assets/Separator-BgWyHDtg.js +1 -0
  245. package/dist/gui/assets/SettingsPage-BevO8oge.js +1 -0
  246. package/dist/gui/assets/SkillsPage-D2stJRWy.js +6 -0
  247. package/dist/gui/assets/StartWorkPage-C8a6eN90.js +1 -0
  248. package/dist/gui/assets/{StrategiesPage-C36TZgDX.js → StrategiesPage-BRQMly_d.js} +2 -2
  249. package/dist/gui/assets/WorkroomsPage-KqBkRD-x.js +4 -0
  250. package/dist/gui/assets/WorkspacesPage-DcA2i5HN.js +2 -0
  251. package/dist/gui/assets/alert-QnMXzwpU.js +1 -0
  252. package/dist/gui/assets/alert-dialog-BHEUQVyS.js +1 -0
  253. package/dist/gui/assets/badge-DTBzXOWn.js +1 -0
  254. package/dist/gui/assets/brand-hHByyyzq.js +1 -0
  255. package/dist/gui/assets/button-CREn1mup.js +1 -0
  256. package/dist/gui/assets/card-yrg6ov8B.js +1 -0
  257. package/dist/gui/assets/chevron-down-B_Mwbim8.js +1 -0
  258. package/dist/gui/assets/{circle-plus-lmU1yNJd.js → circle-plus--zM3-3r3.js} +1 -1
  259. package/dist/gui/assets/{code-xml-jFAa3-TT.js → code-xml-DH-38Tt7.js} +1 -1
  260. package/dist/gui/assets/{git-branch-Cfj9D8yW.js → git-branch-Ce0I2663.js} +1 -1
  261. package/dist/gui/assets/index-BB4BuHBQ.css +2 -0
  262. package/dist/gui/assets/index-BTe-_1Mu.js +57 -0
  263. package/dist/gui/assets/isElementDisabled-BrwmCB1d.js +1 -0
  264. package/dist/gui/assets/label-DC730X8s.js +1 -0
  265. package/dist/gui/assets/layers-j_brOuoa.js +1 -0
  266. package/dist/gui/assets/menu-d_G9km1-.js +1 -0
  267. package/dist/gui/assets/shield-check-Dd1Yv3l2.js +1 -0
  268. package/dist/gui/assets/square-check-big-BSIiYXPI.js +1 -0
  269. package/dist/gui/assets/tabs-BHwTt4t1.js +1 -0
  270. package/dist/gui/assets/{trash-2-CtR35_eF.js → trash-2-CMdv3eD3.js} +1 -1
  271. package/dist/gui/assets/useAriaLabelledBy-BmJErI5a.js +1 -0
  272. package/dist/gui/assets/useValueChanged-WS99hTmm.js +1 -0
  273. package/dist/gui/assets/{vsc-DFh0-53h.js → vsc-eMRaEwN3.js} +1 -1
  274. package/dist/gui/favicon-aurora.svg +8 -0
  275. package/dist/gui/favicon.ico +0 -0
  276. package/dist/gui/favicon.svg +8 -1
  277. package/dist/gui/index.html +21 -12
  278. package/dist/harness/claude.d.ts.map +1 -1
  279. package/dist/harness/claude.js +97 -7
  280. package/dist/harness/claude.js.map +1 -1
  281. package/dist/harness/codex.d.ts +1 -0
  282. package/dist/harness/codex.d.ts.map +1 -1
  283. package/dist/harness/codex.js +51 -34
  284. package/dist/harness/codex.js.map +1 -1
  285. package/dist/index.js +57 -27
  286. package/dist/index.js.map +1 -1
  287. package/dist/mcp/server.d.ts +12 -1
  288. package/dist/mcp/server.d.ts.map +1 -1
  289. package/dist/mcp/server.js +24 -6
  290. package/dist/mcp/server.js.map +1 -1
  291. package/dist/mcp/tools.d.ts +4 -2
  292. package/dist/mcp/tools.d.ts.map +1 -1
  293. package/dist/mcp/tools.js +372 -17
  294. package/dist/mcp/tools.js.map +1 -1
  295. package/dist/orchestration/detect.d.ts.map +1 -1
  296. package/dist/orchestration/detect.js +12 -5
  297. package/dist/orchestration/detect.js.map +1 -1
  298. package/dist/orchestration/log-tail.js +17 -17
  299. package/dist/orchestration/log-tail.js.map +1 -1
  300. package/dist/orchestration/runner.d.ts +1 -1
  301. package/dist/orchestration/runner.d.ts.map +1 -1
  302. package/dist/orchestration/runner.js +13 -8
  303. package/dist/orchestration/runner.js.map +1 -1
  304. package/dist/resources/agents-catalog.d.ts +3 -1
  305. package/dist/resources/agents-catalog.d.ts.map +1 -1
  306. package/dist/resources/agents-catalog.js +12 -6
  307. package/dist/resources/agents-catalog.js.map +1 -1
  308. package/dist/resources/contracts.d.ts +7 -0
  309. package/dist/resources/contracts.d.ts.map +1 -1
  310. package/dist/resources/contracts.js +8 -1
  311. package/dist/resources/contracts.js.map +1 -1
  312. package/dist/resources/fs-safety.d.ts +7 -1
  313. package/dist/resources/fs-safety.d.ts.map +1 -1
  314. package/dist/resources/fs-safety.js +44 -1
  315. package/dist/resources/fs-safety.js.map +1 -1
  316. package/dist/resources/materializer.d.ts +1 -0
  317. package/dist/resources/materializer.d.ts.map +1 -1
  318. package/dist/resources/materializer.js +48 -17
  319. package/dist/resources/materializer.js.map +1 -1
  320. package/dist/resources/service.d.ts.map +1 -1
  321. package/dist/resources/service.js +3 -2
  322. package/dist/resources/service.js.map +1 -1
  323. package/dist/server.d.ts +2 -19
  324. package/dist/server.d.ts.map +1 -1
  325. package/dist/server.js +1158 -318
  326. package/dist/server.js.map +1 -1
  327. package/dist/static-resources/workflows/cost-aware-sol-luna.md +95 -0
  328. package/dist/storage/db.d.ts +94 -0
  329. package/dist/storage/db.d.ts.map +1 -0
  330. package/dist/storage/db.js +609 -0
  331. package/dist/storage/db.js.map +1 -0
  332. package/dist/types.d.ts +19 -0
  333. package/dist/types.d.ts.map +1 -1
  334. package/dist/utils/detect-ai.d.ts.map +1 -1
  335. package/dist/utils/detect-ai.js +1 -3
  336. package/dist/utils/detect-ai.js.map +1 -1
  337. package/dist/utils/multi-git.d.ts +0 -8
  338. package/dist/utils/multi-git.d.ts.map +1 -1
  339. package/dist/utils/multi-git.js +12 -4
  340. package/dist/utils/multi-git.js.map +1 -1
  341. package/dist/utils/pr.d.ts.map +1 -1
  342. package/dist/utils/pr.js +10 -5
  343. package/dist/utils/pr.js.map +1 -1
  344. package/dist/utils/resolve-workspace.d.ts +1 -1
  345. package/dist/utils/resolve-workspace.d.ts.map +1 -1
  346. package/dist/utils/resolve-workspace.js +10 -3
  347. package/dist/utils/resolve-workspace.js.map +1 -1
  348. package/dist/utils/session-finder.d.ts +12 -1
  349. package/dist/utils/session-finder.d.ts.map +1 -1
  350. package/dist/utils/session-finder.js +183 -0
  351. package/dist/utils/session-finder.js.map +1 -1
  352. package/dist/utils/skills-catalog.d.ts +5 -1
  353. package/dist/utils/skills-catalog.d.ts.map +1 -1
  354. package/dist/utils/skills-catalog.js +131 -75
  355. package/dist/utils/skills-catalog.js.map +1 -1
  356. package/dist/utils/terminal-launch.d.ts +4 -0
  357. package/dist/utils/terminal-launch.d.ts.map +1 -1
  358. package/dist/utils/terminal-launch.js +71 -12
  359. package/dist/utils/terminal-launch.js.map +1 -1
  360. package/dist/utils/update-check.d.ts +4 -1
  361. package/dist/utils/update-check.d.ts.map +1 -1
  362. package/dist/utils/update-check.js +66 -19
  363. package/dist/utils/update-check.js.map +1 -1
  364. package/dist/utils/workflows.d.ts +4 -1
  365. package/dist/utils/workflows.d.ts.map +1 -1
  366. package/dist/utils/workflows.js +51 -21
  367. package/dist/utils/workflows.js.map +1 -1
  368. package/dist/workrooms/client.d.ts +54 -0
  369. package/dist/workrooms/client.d.ts.map +1 -0
  370. package/dist/workrooms/client.js +191 -0
  371. package/dist/workrooms/client.js.map +1 -0
  372. package/dist/workrooms/contracts.d.ts +579 -0
  373. package/dist/workrooms/contracts.d.ts.map +1 -0
  374. package/dist/workrooms/contracts.js +231 -0
  375. package/dist/workrooms/contracts.js.map +1 -0
  376. package/dist/workrooms/crypto.d.ts +30 -0
  377. package/dist/workrooms/crypto.d.ts.map +1 -0
  378. package/dist/workrooms/crypto.js +72 -0
  379. package/dist/workrooms/crypto.js.map +1 -0
  380. package/dist/workrooms/host.d.ts +61 -0
  381. package/dist/workrooms/host.d.ts.map +1 -0
  382. package/dist/workrooms/host.js +747 -0
  383. package/dist/workrooms/host.js.map +1 -0
  384. package/dist/workrooms/local-resources.d.ts +26 -0
  385. package/dist/workrooms/local-resources.d.ts.map +1 -0
  386. package/dist/workrooms/local-resources.js +367 -0
  387. package/dist/workrooms/local-resources.js.map +1 -0
  388. package/dist/workrooms/manager.d.ts +228 -0
  389. package/dist/workrooms/manager.d.ts.map +1 -0
  390. package/dist/workrooms/manager.js +768 -0
  391. package/dist/workrooms/manager.js.map +1 -0
  392. package/dist/workrooms/milestones.d.ts +42 -0
  393. package/dist/workrooms/milestones.d.ts.map +1 -0
  394. package/dist/workrooms/milestones.js +94 -0
  395. package/dist/workrooms/milestones.js.map +1 -0
  396. package/dist/workrooms/portable.d.ts +21 -0
  397. package/dist/workrooms/portable.d.ts.map +1 -0
  398. package/dist/workrooms/portable.js +201 -0
  399. package/dist/workrooms/portable.js.map +1 -0
  400. package/dist/workrooms/resource-package.d.ts +7 -0
  401. package/dist/workrooms/resource-package.d.ts.map +1 -0
  402. package/dist/workrooms/resource-package.js +260 -0
  403. package/dist/workrooms/resource-package.js.map +1 -0
  404. package/dist/workrooms/service.d.ts +92 -0
  405. package/dist/workrooms/service.d.ts.map +1 -0
  406. package/dist/workrooms/service.js +788 -0
  407. package/dist/workrooms/service.js.map +1 -0
  408. package/dist/workrooms/sqlite-store.d.ts +31 -0
  409. package/dist/workrooms/sqlite-store.d.ts.map +1 -0
  410. package/dist/workrooms/sqlite-store.js +511 -0
  411. package/dist/workrooms/sqlite-store.js.map +1 -0
  412. package/dist/workrooms/store.d.ts +542 -0
  413. package/dist/workrooms/store.d.ts.map +1 -0
  414. package/dist/workrooms/store.js +71 -0
  415. package/dist/workrooms/store.js.map +1 -0
  416. package/package.json +16 -10
  417. package/resources/workflows/cost-aware-sol-luna.md +95 -0
  418. package/dist/agent/AcpCliAdapter.test.d.ts +0 -2
  419. package/dist/agent/AcpCliAdapter.test.d.ts.map +0 -1
  420. package/dist/agent/AcpCliAdapter.test.js +0 -235
  421. package/dist/agent/AcpCliAdapter.test.js.map +0 -1
  422. package/dist/agent/AntigravityCliAdapter.test.d.ts +0 -2
  423. package/dist/agent/AntigravityCliAdapter.test.d.ts.map +0 -1
  424. package/dist/agent/AntigravityCliAdapter.test.js +0 -222
  425. package/dist/agent/AntigravityCliAdapter.test.js.map +0 -1
  426. package/dist/agent/ClaudeCliAdapter.test.d.ts +0 -2
  427. package/dist/agent/ClaudeCliAdapter.test.d.ts.map +0 -1
  428. package/dist/agent/ClaudeCliAdapter.test.js +0 -320
  429. package/dist/agent/ClaudeCliAdapter.test.js.map +0 -1
  430. package/dist/agent/ClaudeSdkAdapter.test.d.ts +0 -2
  431. package/dist/agent/ClaudeSdkAdapter.test.d.ts.map +0 -1
  432. package/dist/agent/ClaudeSdkAdapter.test.js +0 -155
  433. package/dist/agent/ClaudeSdkAdapter.test.js.map +0 -1
  434. package/dist/agent/CliAdapterBase.test.d.ts +0 -2
  435. package/dist/agent/CliAdapterBase.test.d.ts.map +0 -1
  436. package/dist/agent/CliAdapterBase.test.js +0 -240
  437. package/dist/agent/CliAdapterBase.test.js.map +0 -1
  438. package/dist/agent/CodexCliAdapter.test.d.ts +0 -2
  439. package/dist/agent/CodexCliAdapter.test.d.ts.map +0 -1
  440. package/dist/agent/CodexCliAdapter.test.js +0 -331
  441. package/dist/agent/CodexCliAdapter.test.js.map +0 -1
  442. package/dist/agent/CodexSdkAdapter.test.d.ts +0 -2
  443. package/dist/agent/CodexSdkAdapter.test.d.ts.map +0 -1
  444. package/dist/agent/CodexSdkAdapter.test.js +0 -76
  445. package/dist/agent/CodexSdkAdapter.test.js.map +0 -1
  446. package/dist/agent/ProviderRegistry.test.d.ts +0 -2
  447. package/dist/agent/ProviderRegistry.test.d.ts.map +0 -1
  448. package/dist/agent/ProviderRegistry.test.js +0 -129
  449. package/dist/agent/ProviderRegistry.test.js.map +0 -1
  450. package/dist/agent/adapters.test.d.ts +0 -2
  451. package/dist/agent/adapters.test.d.ts.map +0 -1
  452. package/dist/agent/adapters.test.js +0 -71
  453. package/dist/agent/adapters.test.js.map +0 -1
  454. package/dist/agent/cliAvailability.test.d.ts +0 -2
  455. package/dist/agent/cliAvailability.test.d.ts.map +0 -1
  456. package/dist/agent/cliAvailability.test.js +0 -217
  457. package/dist/agent/cliAvailability.test.js.map +0 -1
  458. package/dist/agent/mcp-config.test.d.ts +0 -2
  459. package/dist/agent/mcp-config.test.d.ts.map +0 -1
  460. package/dist/agent/mcp-config.test.js +0 -22
  461. package/dist/agent/mcp-config.test.js.map +0 -1
  462. package/dist/agent/models.test.d.ts +0 -2
  463. package/dist/agent/models.test.d.ts.map +0 -1
  464. package/dist/agent/models.test.js +0 -40
  465. package/dist/agent/models.test.js.map +0 -1
  466. package/dist/agent/nativeAgent.test.d.ts +0 -2
  467. package/dist/agent/nativeAgent.test.d.ts.map +0 -1
  468. package/dist/agent/nativeAgent.test.js +0 -35
  469. package/dist/agent/nativeAgent.test.js.map +0 -1
  470. package/dist/agent/nativeTools.test.d.ts +0 -2
  471. package/dist/agent/nativeTools.test.d.ts.map +0 -1
  472. package/dist/agent/nativeTools.test.js +0 -67
  473. package/dist/agent/nativeTools.test.js.map +0 -1
  474. package/dist/agent/orchestrator.test.d.ts +0 -2
  475. package/dist/agent/orchestrator.test.d.ts.map +0 -1
  476. package/dist/agent/orchestrator.test.js +0 -284
  477. package/dist/agent/orchestrator.test.js.map +0 -1
  478. package/dist/agent/session.test.d.ts +0 -2
  479. package/dist/agent/session.test.d.ts.map +0 -1
  480. package/dist/agent/session.test.js +0 -74
  481. package/dist/agent/session.test.js.map +0 -1
  482. package/dist/agent/smoke-chat.test.d.ts +0 -2
  483. package/dist/agent/smoke-chat.test.d.ts.map +0 -1
  484. package/dist/agent/smoke-chat.test.js +0 -166
  485. package/dist/agent/smoke-chat.test.js.map +0 -1
  486. package/dist/commands/add-repo.test.d.ts +0 -2
  487. package/dist/commands/add-repo.test.d.ts.map +0 -1
  488. package/dist/commands/add-repo.test.js +0 -30
  489. package/dist/commands/add-repo.test.js.map +0 -1
  490. package/dist/commands/commands.test.d.ts +0 -2
  491. package/dist/commands/commands.test.d.ts.map +0 -1
  492. package/dist/commands/commands.test.js +0 -156
  493. package/dist/commands/commands.test.js.map +0 -1
  494. package/dist/commands/commit.test.d.ts +0 -2
  495. package/dist/commands/commit.test.d.ts.map +0 -1
  496. package/dist/commands/commit.test.js +0 -54
  497. package/dist/commands/commit.test.js.map +0 -1
  498. package/dist/commands/isolate.test.d.ts +0 -2
  499. package/dist/commands/isolate.test.d.ts.map +0 -1
  500. package/dist/commands/isolate.test.js +0 -59
  501. package/dist/commands/isolate.test.js.map +0 -1
  502. package/dist/commands/knowledge.test.d.ts +0 -2
  503. package/dist/commands/knowledge.test.d.ts.map +0 -1
  504. package/dist/commands/knowledge.test.js +0 -90
  505. package/dist/commands/knowledge.test.js.map +0 -1
  506. package/dist/commands/status.test.d.ts +0 -2
  507. package/dist/commands/status.test.d.ts.map +0 -1
  508. package/dist/commands/status.test.js +0 -31
  509. package/dist/commands/status.test.js.map +0 -1
  510. package/dist/commands/ui.test.d.ts +0 -2
  511. package/dist/commands/ui.test.d.ts.map +0 -1
  512. package/dist/commands/ui.test.js +0 -58
  513. package/dist/commands/ui.test.js.map +0 -1
  514. package/dist/core/adapters/storage.test.d.ts +0 -2
  515. package/dist/core/adapters/storage.test.d.ts.map +0 -1
  516. package/dist/core/adapters/storage.test.js +0 -65
  517. package/dist/core/adapters/storage.test.js.map +0 -1
  518. package/dist/core/analysis-cache.test.d.ts +0 -2
  519. package/dist/core/analysis-cache.test.d.ts.map +0 -1
  520. package/dist/core/analysis-cache.test.js +0 -163
  521. package/dist/core/analysis-cache.test.js.map +0 -1
  522. package/dist/core/config.test.d.ts +0 -2
  523. package/dist/core/config.test.d.ts.map +0 -1
  524. package/dist/core/config.test.js +0 -90
  525. package/dist/core/config.test.js.map +0 -1
  526. package/dist/core/finish.test.d.ts +0 -2
  527. package/dist/core/finish.test.d.ts.map +0 -1
  528. package/dist/core/finish.test.js +0 -110
  529. package/dist/core/finish.test.js.map +0 -1
  530. package/dist/core/generation-lock.test.d.ts +0 -2
  531. package/dist/core/generation-lock.test.d.ts.map +0 -1
  532. package/dist/core/generation-lock.test.js +0 -134
  533. package/dist/core/generation-lock.test.js.map +0 -1
  534. package/dist/core/isolate.test.d.ts +0 -2
  535. package/dist/core/isolate.test.d.ts.map +0 -1
  536. package/dist/core/isolate.test.js +0 -320
  537. package/dist/core/isolate.test.js.map +0 -1
  538. package/dist/core/knowledge.test.d.ts +0 -2
  539. package/dist/core/knowledge.test.d.ts.map +0 -1
  540. package/dist/core/knowledge.test.js +0 -287
  541. package/dist/core/knowledge.test.js.map +0 -1
  542. package/dist/core/locks.test.d.ts +0 -2
  543. package/dist/core/locks.test.d.ts.map +0 -1
  544. package/dist/core/locks.test.js +0 -143
  545. package/dist/core/locks.test.js.map +0 -1
  546. package/dist/core/new-repo.test.d.ts +0 -2
  547. package/dist/core/new-repo.test.d.ts.map +0 -1
  548. package/dist/core/new-repo.test.js +0 -78
  549. package/dist/core/new-repo.test.js.map +0 -1
  550. package/dist/core/progress.test.d.ts +0 -2
  551. package/dist/core/progress.test.d.ts.map +0 -1
  552. package/dist/core/progress.test.js +0 -52
  553. package/dist/core/progress.test.js.map +0 -1
  554. package/dist/core/projects.test.d.ts +0 -2
  555. package/dist/core/projects.test.d.ts.map +0 -1
  556. package/dist/core/projects.test.js +0 -165
  557. package/dist/core/projects.test.js.map +0 -1
  558. package/dist/core/refresh.test.d.ts +0 -2
  559. package/dist/core/refresh.test.d.ts.map +0 -1
  560. package/dist/core/refresh.test.js +0 -93
  561. package/dist/core/refresh.test.js.map +0 -1
  562. package/dist/core/scanner.test.d.ts +0 -2
  563. package/dist/core/scanner.test.d.ts.map +0 -1
  564. package/dist/core/scanner.test.js +0 -53
  565. package/dist/core/scanner.test.js.map +0 -1
  566. package/dist/core/scheduler.test.d.ts +0 -2
  567. package/dist/core/scheduler.test.d.ts.map +0 -1
  568. package/dist/core/scheduler.test.js +0 -169
  569. package/dist/core/scheduler.test.js.map +0 -1
  570. package/dist/core/sync.test.d.ts +0 -2
  571. package/dist/core/sync.test.d.ts.map +0 -1
  572. package/dist/core/sync.test.js +0 -128
  573. package/dist/core/sync.test.js.map +0 -1
  574. package/dist/core/workspace-git.test.d.ts +0 -2
  575. package/dist/core/workspace-git.test.d.ts.map +0 -1
  576. package/dist/core/workspace-git.test.js +0 -52
  577. package/dist/core/workspace-git.test.js.map +0 -1
  578. package/dist/core/workspace-skills-workflow.integration.test.d.ts +0 -2
  579. package/dist/core/workspace-skills-workflow.integration.test.d.ts.map +0 -1
  580. package/dist/core/workspace-skills-workflow.integration.test.js +0 -177
  581. package/dist/core/workspace-skills-workflow.integration.test.js.map +0 -1
  582. package/dist/core/workspace-state.test.d.ts +0 -2
  583. package/dist/core/workspace-state.test.d.ts.map +0 -1
  584. package/dist/core/workspace-state.test.js +0 -84
  585. package/dist/core/workspace-state.test.js.map +0 -1
  586. package/dist/core/workspace.create.test.d.ts +0 -2
  587. package/dist/core/workspace.create.test.d.ts.map +0 -1
  588. package/dist/core/workspace.create.test.js +0 -224
  589. package/dist/core/workspace.create.test.js.map +0 -1
  590. package/dist/core/workspace.test.d.ts +0 -2
  591. package/dist/core/workspace.test.d.ts.map +0 -1
  592. package/dist/core/workspace.test.js +0 -53
  593. package/dist/core/workspace.test.js.map +0 -1
  594. package/dist/core/worktree.test.d.ts +0 -2
  595. package/dist/core/worktree.test.d.ts.map +0 -1
  596. package/dist/core/worktree.test.js +0 -153
  597. package/dist/core/worktree.test.js.map +0 -1
  598. package/dist/generators/base.test.d.ts +0 -2
  599. package/dist/generators/base.test.d.ts.map +0 -1
  600. package/dist/generators/base.test.js +0 -287
  601. package/dist/generators/base.test.js.map +0 -1
  602. package/dist/generators/knowledge-migration.test.d.ts +0 -2
  603. package/dist/generators/knowledge-migration.test.d.ts.map +0 -1
  604. package/dist/generators/knowledge-migration.test.js +0 -22
  605. package/dist/generators/knowledge-migration.test.js.map +0 -1
  606. package/dist/generators/plan-generator.test.d.ts +0 -2
  607. package/dist/generators/plan-generator.test.d.ts.map +0 -1
  608. package/dist/generators/plan-generator.test.js +0 -220
  609. package/dist/generators/plan-generator.test.js.map +0 -1
  610. package/dist/generators/skills-generator.test.d.ts +0 -2
  611. package/dist/generators/skills-generator.test.d.ts.map +0 -1
  612. package/dist/generators/skills-generator.test.js +0 -79
  613. package/dist/generators/skills-generator.test.js.map +0 -1
  614. package/dist/gui/assets/AgentsPage-B4fx43rd.js +0 -3
  615. package/dist/gui/assets/CSPContext-DBUu9tE0.js +0 -1
  616. package/dist/gui/assets/DashboardPage-DH52G8IG.js +0 -1
  617. package/dist/gui/assets/GettingStartedPage-BlTrQhvi.js +0 -1
  618. package/dist/gui/assets/ProjectsPage-BPVSLPXv.js +0 -1
  619. package/dist/gui/assets/ScaffoldRepoInline-ywcpq9PX.js +0 -1
  620. package/dist/gui/assets/SettingsPage-RQr8SalV.js +0 -1
  621. package/dist/gui/assets/SkillsPage-CYoSRrwx.js +0 -6
  622. package/dist/gui/assets/StartWorkPage-DBKK4lFi.js +0 -1
  623. package/dist/gui/assets/WorkspacesPage-DxEONvSq.js +0 -6
  624. package/dist/gui/assets/alert-dialog-CQAX2yRx.js +0 -1
  625. package/dist/gui/assets/badge-CAohJGj6.js +0 -1
  626. package/dist/gui/assets/bot-C2HWrAft.js +0 -1
  627. package/dist/gui/assets/button-BDDZnIHx.js +0 -1
  628. package/dist/gui/assets/card-BKXzd_5L.js +0 -1
  629. package/dist/gui/assets/chevron-down-qTL8HPug.js +0 -1
  630. package/dist/gui/assets/cpu-Cdwd84Jz.js +0 -1
  631. package/dist/gui/assets/empty-BdlgbifD.js +0 -1
  632. package/dist/gui/assets/index-BF6pre6G.css +0 -2
  633. package/dist/gui/assets/index-C_rwnBzF.js +0 -52
  634. package/dist/gui/assets/isElementDisabled-uy_EOitH.js +0 -1
  635. package/dist/gui/assets/label-CG5exjag.js +0 -1
  636. package/dist/gui/assets/square-DlIYnlSq.js +0 -1
  637. package/dist/gui/assets/textarea-ByfoCb54.js +0 -1
  638. package/dist/gui/assets/useAriaLabelledBy-ClesgjMe.js +0 -1
  639. package/dist/gui/assets/useValueChanged-DeYvGnKs.js +0 -1
  640. package/dist/gui/assets/zap-CWG_Lvki.js +0 -1
  641. package/dist/harness/contract.test.d.ts +0 -2
  642. package/dist/harness/contract.test.d.ts.map +0 -1
  643. package/dist/harness/contract.test.js +0 -426
  644. package/dist/harness/contract.test.js.map +0 -1
  645. package/dist/harness/harness.test.d.ts +0 -2
  646. package/dist/harness/harness.test.d.ts.map +0 -1
  647. package/dist/harness/harness.test.js +0 -215
  648. package/dist/harness/harness.test.js.map +0 -1
  649. package/dist/mcp/tools.test.d.ts +0 -2
  650. package/dist/mcp/tools.test.d.ts.map +0 -1
  651. package/dist/mcp/tools.test.js +0 -194
  652. package/dist/mcp/tools.test.js.map +0 -1
  653. package/dist/orchestration/log-tail.test.d.ts +0 -2
  654. package/dist/orchestration/log-tail.test.d.ts.map +0 -1
  655. package/dist/orchestration/log-tail.test.js +0 -72
  656. package/dist/orchestration/log-tail.test.js.map +0 -1
  657. package/dist/orchestration/orchestrator.test.d.ts +0 -2
  658. package/dist/orchestration/orchestrator.test.d.ts.map +0 -1
  659. package/dist/orchestration/orchestrator.test.js +0 -85
  660. package/dist/orchestration/orchestrator.test.js.map +0 -1
  661. package/dist/orchestration/runner.test.d.ts +0 -2
  662. package/dist/orchestration/runner.test.d.ts.map +0 -1
  663. package/dist/orchestration/runner.test.js +0 -233
  664. package/dist/orchestration/runner.test.js.map +0 -1
  665. package/dist/resources/agents-catalog.test.d.ts +0 -2
  666. package/dist/resources/agents-catalog.test.d.ts.map +0 -1
  667. package/dist/resources/agents-catalog.test.js +0 -50
  668. package/dist/resources/agents-catalog.test.js.map +0 -1
  669. package/dist/resources/codex-agent.test.d.ts +0 -2
  670. package/dist/resources/codex-agent.test.d.ts.map +0 -1
  671. package/dist/resources/codex-agent.test.js +0 -27
  672. package/dist/resources/codex-agent.test.js.map +0 -1
  673. package/dist/resources/materializer.test.d.ts +0 -2
  674. package/dist/resources/materializer.test.d.ts.map +0 -1
  675. package/dist/resources/materializer.test.js +0 -149
  676. package/dist/resources/materializer.test.js.map +0 -1
  677. package/dist/resources/service.test.d.ts +0 -2
  678. package/dist/resources/service.test.d.ts.map +0 -1
  679. package/dist/resources/service.test.js +0 -58
  680. package/dist/resources/service.test.js.map +0 -1
  681. package/dist/server.test.d.ts +0 -2
  682. package/dist/server.test.d.ts.map +0 -1
  683. package/dist/server.test.js +0 -1697
  684. package/dist/server.test.js.map +0 -1
  685. package/dist/utils/detect-ai.test.d.ts +0 -2
  686. package/dist/utils/detect-ai.test.d.ts.map +0 -1
  687. package/dist/utils/detect-ai.test.js +0 -70
  688. package/dist/utils/detect-ai.test.js.map +0 -1
  689. package/dist/utils/detect-editors.test.d.ts +0 -2
  690. package/dist/utils/detect-editors.test.d.ts.map +0 -1
  691. package/dist/utils/detect-editors.test.js +0 -85
  692. package/dist/utils/detect-editors.test.js.map +0 -1
  693. package/dist/utils/git.test.d.ts +0 -2
  694. package/dist/utils/git.test.d.ts.map +0 -1
  695. package/dist/utils/git.test.js +0 -125
  696. package/dist/utils/git.test.js.map +0 -1
  697. package/dist/utils/multi-git.test.d.ts +0 -2
  698. package/dist/utils/multi-git.test.d.ts.map +0 -1
  699. package/dist/utils/multi-git.test.js +0 -330
  700. package/dist/utils/multi-git.test.js.map +0 -1
  701. package/dist/utils/pr.test.d.ts +0 -2
  702. package/dist/utils/pr.test.d.ts.map +0 -1
  703. package/dist/utils/pr.test.js +0 -47
  704. package/dist/utils/pr.test.js.map +0 -1
  705. package/dist/utils/session-finder.test.d.ts +0 -2
  706. package/dist/utils/session-finder.test.d.ts.map +0 -1
  707. package/dist/utils/session-finder.test.js +0 -304
  708. package/dist/utils/session-finder.test.js.map +0 -1
  709. package/dist/utils/skills-catalog.test.d.ts +0 -2
  710. package/dist/utils/skills-catalog.test.d.ts.map +0 -1
  711. package/dist/utils/skills-catalog.test.js +0 -281
  712. package/dist/utils/skills-catalog.test.js.map +0 -1
  713. package/dist/utils/system-scanner.test.d.ts +0 -2
  714. package/dist/utils/system-scanner.test.d.ts.map +0 -1
  715. package/dist/utils/system-scanner.test.js +0 -73
  716. package/dist/utils/system-scanner.test.js.map +0 -1
  717. package/dist/utils/terminal-launch.test.d.ts +0 -2
  718. package/dist/utils/terminal-launch.test.d.ts.map +0 -1
  719. package/dist/utils/terminal-launch.test.js +0 -117
  720. package/dist/utils/terminal-launch.test.js.map +0 -1
  721. package/dist/utils/update-check.test.d.ts +0 -2
  722. package/dist/utils/update-check.test.d.ts.map +0 -1
  723. package/dist/utils/update-check.test.js +0 -30
  724. package/dist/utils/update-check.test.js.map +0 -1
  725. package/dist/utils/workflows.test.d.ts +0 -2
  726. package/dist/utils/workflows.test.d.ts.map +0 -1
  727. package/dist/utils/workflows.test.js +0 -86
  728. package/dist/utils/workflows.test.js.map +0 -1
  729. package/dist/utils/workspace-launch.test.d.ts +0 -2
  730. package/dist/utils/workspace-launch.test.d.ts.map +0 -1
  731. package/dist/utils/workspace-launch.test.js +0 -128
  732. package/dist/utils/workspace-launch.test.js.map +0 -1
@@ -1,1697 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import * as fs from 'node:fs/promises';
3
- import * as path from 'node:path';
4
- import { execa } from 'execa';
5
- import { AgentTurnGate, app, canOfferClaudeDesktopTransfer, dispatchAgentInput, isAllowedUpdateUrl } from './server.js';
6
- import * as workspace from './core/workspace.js';
7
- import * as config from './core/config.js';
8
- import * as analyzers from './analyzers/index.js';
9
- import * as generators from './generators/index.js';
10
- import * as workflows from './utils/workflows.js';
11
- import * as skillsCatalog from './utils/skills-catalog.js';
12
- import * as agentsCatalog from './resources/agents-catalog.js';
13
- import * as resourceService from './resources/service.js';
14
- import * as detectAi from './utils/detect-ai.js';
15
- import * as newRepo from './core/new-repo.js';
16
- import * as orchestration from './orchestration/index.js';
17
- import * as sessionFinder from './utils/session-finder.js';
18
- import { ProviderRegistry } from './agent/ProviderRegistry.js';
19
- // Mock dependencies
20
- vi.mock('node:fs/promises');
21
- vi.mock('execa');
22
- vi.mock('./core/workspace.js');
23
- vi.mock('./core/config.js');
24
- vi.mock('./utils/system-scanner.js');
25
- vi.mock('./utils/update-check.js');
26
- vi.mock('./analyzers/index.js');
27
- vi.mock('./generators/index.js');
28
- vi.mock('./utils/workflows.js');
29
- vi.mock('./utils/skills-catalog.js');
30
- vi.mock('./resources/agents-catalog.js');
31
- vi.mock('./resources/service.js', async (importOriginal) => {
32
- const actual = await importOriginal();
33
- return {
34
- ...actual,
35
- withResourceAdministrationLock: vi.fn((operation) => operation()),
36
- validateResourceSelections: vi.fn().mockResolvedValue(undefined),
37
- };
38
- });
39
- vi.mock('./utils/detect-ai.js', () => ({
40
- detectAIAssistants: vi.fn().mockResolvedValue([])
41
- }));
42
- vi.mock('./utils/terminal-launch.js', async (importOriginal) => {
43
- const actual = await importOriginal();
44
- return {
45
- ...actual,
46
- launchWorkspaceTerminal: vi.fn().mockResolvedValue({
47
- success: true,
48
- command: 'claude --resume 0199a213-81c0-7800-8aa1-bbab2a035a54',
49
- }),
50
- };
51
- });
52
- vi.mock('./core/new-repo.js', async (importOriginal) => {
53
- const actual = await importOriginal();
54
- return {
55
- ...actual,
56
- createNewRepo: vi.fn(),
57
- };
58
- });
59
- vi.mock('./orchestration/index.js');
60
- describe('Server API Endpoints Unit Tests', () => {
61
- beforeEach(() => {
62
- vi.clearAllMocks();
63
- });
64
- it('rejects an invalid transcript session id at the HTTP boundary', async () => {
65
- const response = await app.request('/api/session/codex/53/transcript');
66
- expect(response.status).toBe(400);
67
- await expect(response.json()).resolves.toEqual({ error: 'Invalid session UUID format.' });
68
- });
69
- describe('embedded turn admission', () => {
70
- it('rejects overlapping input until the active turn settles', () => {
71
- const gate = new AgentTurnGate();
72
- expect(gate.tryBegin()).toBe(true);
73
- expect(gate.isActive()).toBe(true);
74
- expect(gate.tryBegin()).toBe(false);
75
- gate.settle();
76
- expect(gate.isActive()).toBe(false);
77
- expect(gate.tryBegin()).toBe(true);
78
- });
79
- });
80
- describe('CLI adapter status probes', () => {
81
- it('keeps the legacy GET endpoint side-effect-free', async () => {
82
- const status = vi.spyOn(ProviderRegistry, 'getAllStatus').mockReturnValue([]);
83
- const response = await app.request('/api/adapters/status', {
84
- headers: { Origin: 'https://evil.example' },
85
- });
86
- expect(response.status).toBe(405);
87
- expect(status).not.toHaveBeenCalled();
88
- status.mockRestore();
89
- });
90
- it('rejects a hostile initial status POST before running CLI detectors', async () => {
91
- const status = vi.spyOn(ProviderRegistry, 'getAllStatus').mockReturnValue([]);
92
- const response = await app.request('/api/adapters/status', {
93
- method: 'POST',
94
- headers: { 'Content-Type': 'application/json', Origin: 'https://evil.example' },
95
- body: '{}',
96
- });
97
- expect(response.status).toBe(403);
98
- expect(status).not.toHaveBeenCalled();
99
- status.mockRestore();
100
- });
101
- it('allows a local-origin initial status POST', async () => {
102
- const status = vi.spyOn(ProviderRegistry, 'getAllStatus').mockReturnValue([]);
103
- const response = await app.request('/api/adapters/status', {
104
- method: 'POST',
105
- headers: { 'Content-Type': 'application/json', Origin: 'http://localhost:4173' },
106
- body: '{}',
107
- });
108
- expect(response.status).toBe(200);
109
- expect(response.headers.get('access-control-allow-origin')).toBe('http://localhost:4173');
110
- expect(status).toHaveBeenCalledWith();
111
- status.mockRestore();
112
- });
113
- it('rejects a hostile browser origin before refreshing a CLI detector', async () => {
114
- const status = vi.spyOn(ProviderRegistry, 'getAllStatus').mockReturnValue([]);
115
- const response = await app.request('/api/adapters/status/refresh', {
116
- method: 'POST',
117
- headers: { 'Content-Type': 'application/json', Origin: 'https://evil.example' },
118
- body: JSON.stringify({ providerId: 'claude-cli' }),
119
- });
120
- expect(response.status).toBe(403);
121
- expect(status).not.toHaveBeenCalled();
122
- status.mockRestore();
123
- });
124
- it('refreshes an allowlisted harness provider for a same-origin request', async () => {
125
- const status = vi.spyOn(ProviderRegistry, 'getAllStatus').mockReturnValue([]);
126
- const response = await app.request('/api/adapters/status/refresh', {
127
- method: 'POST',
128
- headers: { 'Content-Type': 'application/json', Origin: 'http://localhost:4173' },
129
- body: JSON.stringify({ providerId: 'codex-cli' }),
130
- });
131
- expect(response.status).toBe(200);
132
- expect(response.headers.get('access-control-allow-origin')).toBe('http://localhost:4173');
133
- expect(status).toHaveBeenCalledWith({ refreshProviderId: 'codex-cli' });
134
- status.mockRestore();
135
- });
136
- it('refreshes an allowlisted first-party SDK status', async () => {
137
- const status = vi.spyOn(ProviderRegistry, 'getAllStatus').mockReturnValue([]);
138
- const response = await app.request('/api/adapters/status/refresh', {
139
- method: 'POST',
140
- headers: { 'Content-Type': 'application/json', Origin: 'http://localhost:4173' },
141
- body: JSON.stringify({ providerId: 'codex-sdk' }),
142
- });
143
- expect(response.status).toBe(200);
144
- expect(status).toHaveBeenCalledWith({ refreshProviderId: 'codex-sdk' });
145
- status.mockRestore();
146
- });
147
- it('rejects attempts to refresh other providers', async () => {
148
- const status = vi.spyOn(ProviderRegistry, 'getAllStatus').mockReturnValue([]);
149
- const response = await app.request('/api/adapters/status/refresh', {
150
- method: 'POST',
151
- headers: { 'Content-Type': 'application/json', Origin: 'http://127.0.0.1:4173' },
152
- body: JSON.stringify({ providerId: 'copilot-cli' }),
153
- });
154
- expect(response.status).toBe(400);
155
- expect(status).not.toHaveBeenCalled();
156
- status.mockRestore();
157
- });
158
- });
159
- describe('embedded turn execution profiles', () => {
160
- const provider = {
161
- id: 'profiled-test',
162
- name: 'Profiled test',
163
- capabilities: { transport: 'cli-print', sessionIdentity: 'none', workspaceAccess: 'harness-managed' },
164
- executionProfiles: [
165
- { id: 'review', label: 'Review only', description: 'Read-only.' },
166
- { id: 'workspace-write', label: 'Edit workspace', description: 'May edit.' },
167
- ],
168
- defaultExecutionProfile: 'review',
169
- isConfigured: () => true,
170
- getStatusMessage: () => undefined,
171
- createInstance: () => harness(),
172
- };
173
- function harness() {
174
- return {
175
- start: vi.fn(),
176
- send: vi.fn().mockResolvedValue(undefined),
177
- stop: vi.fn(),
178
- on: vi.fn().mockReturnThis(),
179
- off: vi.fn().mockReturnThis(),
180
- };
181
- }
182
- it.each([undefined, 'danger-full-access', false])('rejects a missing or unsupported profile %j before harness send', (executionProfile) => {
183
- const agent = harness();
184
- expect(dispatchAgentInput(agent, provider, { input: 'Do work', executionProfile }))
185
- .toMatch(/supported execution profile/i);
186
- expect(agent.send).not.toHaveBeenCalled();
187
- });
188
- it('passes a validated profile to the harness', () => {
189
- const agent = harness();
190
- expect(dispatchAgentInput(agent, provider, {
191
- input: 'Inspect only',
192
- executionProfile: 'review',
193
- })).toBeNull();
194
- expect(agent.send).toHaveBeenCalledWith('Inspect only', 'review');
195
- });
196
- });
197
- describe('POST /api/open-editor', () => {
198
- const workspacesDir = path.resolve('mock-workspaces');
199
- const workspacePath = path.join(workspacesDir, 'test-workspace');
200
- it('rejects cross-origin requests to open-editor', async () => {
201
- const response = await app.request('/api/open-editor', {
202
- method: 'POST',
203
- headers: {
204
- 'Content-Type': 'application/json',
205
- Origin: 'http://malicious-site.example.com',
206
- },
207
- body: JSON.stringify({ workspacePath, command: 'code' }),
208
- });
209
- expect(response.status).toBe(403);
210
- await expect(response.json()).resolves.toEqual({ error: 'Forbidden cross-origin request.' });
211
- });
212
- it('should return 400 for forbidden editor commands', async () => {
213
- const response = await app.request('/api/open-editor', {
214
- method: 'POST',
215
- headers: { 'Content-Type': 'application/json' },
216
- body: JSON.stringify({
217
- workspacePath: '/mock/workspace/path',
218
- command: 'rm -rf /'
219
- })
220
- });
221
- expect(response.status).toBe(400);
222
- const data = await response.json();
223
- expect(data.error).toBe('Forbidden editor command');
224
- });
225
- it('explicitly rejects legacy interactive terminal editors', async () => {
226
- const response = await app.request('/api/open-editor', {
227
- method: 'POST',
228
- headers: { 'Content-Type': 'application/json' },
229
- body: JSON.stringify({ workspacePath, command: 'nvim' }),
230
- });
231
- expect(response.status).toBe(400);
232
- await expect(response.json()).resolves.toEqual({ error: 'Forbidden editor command' });
233
- });
234
- it('should return 400 if workspace path does not exist', async () => {
235
- vi.spyOn(config, 'loadConfig').mockResolvedValue({ workspacesDir });
236
- vi.spyOn(fs, 'stat').mockRejectedValue(new Error('File not found'));
237
- const response = await app.request('/api/open-editor', {
238
- method: 'POST',
239
- headers: { 'Content-Type': 'application/json' },
240
- body: JSON.stringify({
241
- workspacePath,
242
- command: 'code-insiders'
243
- })
244
- });
245
- expect(response.status).toBe(400);
246
- const data = await response.json();
247
- expect(data.error).toBe('Workspace path does not exist');
248
- });
249
- it('should return 400 if workspace path is not a directory', async () => {
250
- vi.spyOn(config, 'loadConfig').mockResolvedValue({ workspacesDir });
251
- vi.spyOn(fs, 'stat').mockResolvedValue({
252
- isDirectory: () => false
253
- });
254
- const response = await app.request('/api/open-editor', {
255
- method: 'POST',
256
- headers: { 'Content-Type': 'application/json' },
257
- body: JSON.stringify({
258
- workspacePath,
259
- command: 'code-insiders'
260
- })
261
- });
262
- expect(response.status).toBe(400);
263
- const data = await response.json();
264
- expect(data.error).toBe('Workspace path is not a directory');
265
- });
266
- it('should launch a detected editor through the server-owned target', async () => {
267
- vi.spyOn(config, 'loadConfig').mockResolvedValue({ workspacesDir });
268
- vi.spyOn(fs, 'stat').mockResolvedValue({
269
- isDirectory: () => true
270
- });
271
- vi.spyOn(fs, 'access').mockRejectedValue(new Error('No generated workspace file'));
272
- vi.spyOn(fs, 'realpath').mockImplementation(async (candidate) => path.resolve(String(candidate)));
273
- vi.spyOn(workspace, 'loadWorkspaceManifest').mockResolvedValue({
274
- id: 'test-workspace',
275
- workspacePath,
276
- });
277
- vi.mocked(execa).mockImplementation((async (command, args) => ({
278
- exitCode: args?.[0] === '--version' && command === 'code-insiders' ? 0 : 1,
279
- })));
280
- const response = await app.request('/api/open-editor', {
281
- method: 'POST',
282
- headers: { 'Content-Type': 'application/json' },
283
- body: JSON.stringify({
284
- workspacePath,
285
- command: 'code-insiders'
286
- })
287
- });
288
- expect(response.status).toBe(200);
289
- const data = await response.json();
290
- expect(data.success).toBe(true);
291
- expect(execa).toHaveBeenCalledWith('code-insiders', [process.platform === 'win32' ? `"${workspacePath}"` : workspacePath], {
292
- stdio: 'ignore',
293
- shell: process.platform === 'win32',
294
- });
295
- });
296
- it('rejects an existing directory outside the configured workspace root', async () => {
297
- vi.spyOn(config, 'loadConfig').mockResolvedValue({ workspacesDir });
298
- const response = await app.request('/api/open-editor', {
299
- method: 'POST',
300
- headers: { 'Content-Type': 'application/json' },
301
- body: JSON.stringify({
302
- workspacePath: path.resolve('outside-workspace'),
303
- command: 'code',
304
- }),
305
- });
306
- expect(response.status).toBe(400);
307
- await expect(response.json()).resolves.toEqual({ error: 'Invalid workspace path' });
308
- });
309
- });
310
- describe('workspace launch targets', () => {
311
- const workspacesDir = path.resolve('launch-workspaces');
312
- const workspacePath = path.join(workspacesDir, 'safe-workspace');
313
- beforeEach(() => {
314
- vi.spyOn(config, 'loadConfig').mockResolvedValue({ workspacesDir });
315
- vi.spyOn(fs, 'realpath').mockImplementation(async (candidate) => path.resolve(String(candidate)));
316
- vi.spyOn(workspace, 'loadWorkspaceManifest').mockResolvedValue({
317
- id: 'safe-workspace',
318
- description: 'Improve the local Desktop handoff',
319
- repos: [],
320
- workspacePath,
321
- });
322
- });
323
- it('returns stable ids and never exposes executable commands', async () => {
324
- vi.mocked(execa).mockResolvedValue({ exitCode: 1 });
325
- const response = await app.request('/api/workspace-launch-targets');
326
- const targets = await response.json();
327
- expect(response.status).toBe(200);
328
- expect(targets.map((target) => target.id)).toContain('codex-desktop');
329
- expect(targets.map((target) => target.id)).toContain('vscode-insiders');
330
- expect(targets.every((target) => !('command' in target) && !('uri' in target))).toBe(true);
331
- });
332
- it('rejects a client-supplied target outside the closed catalog', async () => {
333
- vi.mocked(execa).mockResolvedValue({ exitCode: 1 });
334
- const response = await app.request('/api/workspace/safe-workspace/launch', {
335
- method: 'POST',
336
- headers: { 'Content-Type': 'application/json' },
337
- body: JSON.stringify({ targetId: 'editor:../../calc.exe' }),
338
- });
339
- expect(response.status).toBe(400);
340
- await expect(response.json()).resolves.toEqual({ error: 'Unknown workspace launch target.' });
341
- });
342
- it('rejects a workspace whose canonical path escapes through a symlink', async () => {
343
- const outsidePath = path.resolve('outside-launch-workspace');
344
- vi.mocked(fs.realpath).mockImplementation(async (candidate) => path.resolve(String(candidate)) === workspacePath
345
- ? outsidePath
346
- : path.resolve(String(candidate)));
347
- const response = await app.request('/api/workspace/safe-workspace/launch', {
348
- method: 'POST',
349
- headers: { 'Content-Type': 'application/json' },
350
- body: JSON.stringify({ targetId: 'codex-desktop' }),
351
- });
352
- expect(response.status).toBe(400);
353
- await expect(response.json()).resolves.toEqual({ error: 'Invalid workspace path' });
354
- expect(execa).not.toHaveBeenCalled();
355
- });
356
- it('rejects a child path that only inherits a parent workspace manifest', async () => {
357
- vi.mocked(workspace.loadWorkspaceManifest).mockResolvedValue(null);
358
- vi.mocked(workspace.loadFeatureConfig).mockResolvedValue({
359
- id: 'parent-workspace',
360
- workspacePath: workspacesDir,
361
- });
362
- const response = await app.request('/api/workspace/safe-workspace/launch', {
363
- method: 'POST',
364
- headers: { 'Content-Type': 'application/json' },
365
- body: JSON.stringify({ targetId: 'codex-desktop' }),
366
- });
367
- expect(response.status).toBe(404);
368
- await expect(response.json()).resolves.toEqual({ error: 'Workspace configuration not found.' });
369
- expect(execa).not.toHaveBeenCalled();
370
- });
371
- it('returns a conflict when the selected app is unavailable', async () => {
372
- vi.mocked(execa).mockResolvedValue({ exitCode: 1 });
373
- const response = await app.request('/api/workspace/safe-workspace/launch', {
374
- method: 'POST',
375
- headers: { 'Content-Type': 'application/json' },
376
- body: JSON.stringify({ targetId: 'codex-desktop' }),
377
- });
378
- expect(response.status).toBe(409);
379
- expect((await response.json()).error).toMatch(/not installed|unavailable|supported/i);
380
- });
381
- it('launches Codex with a once-encoded validated workspace path', async () => {
382
- const platform = vi.spyOn(process, 'platform', 'get').mockReturnValue('win32');
383
- vi.mocked(execa).mockImplementation((async (command) => ({
384
- exitCode: command === 'reg.exe' || command === 'cmd.exe' ? 0 : 1,
385
- })));
386
- try {
387
- const response = await app.request('/api/workspace/safe-workspace/launch', {
388
- method: 'POST',
389
- headers: { 'Content-Type': 'application/json' },
390
- body: JSON.stringify({ targetId: 'codex-desktop' }),
391
- });
392
- expect(response.status).toBe(200);
393
- const prompt = 'Read the workspace instructions and implementation plan, inspect the repository state, then begin the task described for this workspace. Ask before making a decision that materially changes scope.\n\nWorkspace task: Improve the local Desktop handoff';
394
- expect(execa).toHaveBeenCalledWith('cmd.exe', ['/d', '/v:off', '/s', '/c', 'start "" "%NEXUSFLOW_DESKTOP_URI%"'], {
395
- env: {
396
- NEXUSFLOW_DESKTOP_URI: `codex://threads/new?path=${encodeURIComponent(workspacePath)}&prompt=${encodeURIComponent(prompt)}`,
397
- },
398
- shell: false,
399
- windowsHide: true,
400
- windowsVerbatimArguments: true,
401
- });
402
- }
403
- finally {
404
- platform.mockRestore();
405
- }
406
- });
407
- it('opens only a Codex thread recorded for the selected workspace', async () => {
408
- const platform = vi.spyOn(process, 'platform', 'get').mockReturnValue('win32');
409
- const sessionId = '0199a213-81c0-7800-8aa1-bbab2a035a53';
410
- const authorizeSession = vi.spyOn(sessionFinder, 'canOpenCodexSessionInWorkspace')
411
- .mockResolvedValue(true);
412
- vi.mocked(execa).mockResolvedValue({ exitCode: 0 });
413
- try {
414
- const response = await app.request('/api/workspace/safe-workspace/launch', {
415
- method: 'POST',
416
- headers: { 'Content-Type': 'application/json' },
417
- body: JSON.stringify({ targetId: 'codex-desktop', action: 'resume', sessionId }),
418
- });
419
- expect(response.status).toBe(200);
420
- expect(execa).toHaveBeenCalledWith('cmd.exe', ['/d', '/v:off', '/s', '/c', 'start "" "%NEXUSFLOW_DESKTOP_URI%"'], {
421
- env: { NEXUSFLOW_DESKTOP_URI: `codex://threads/${sessionId}` },
422
- shell: false,
423
- windowsHide: true,
424
- windowsVerbatimArguments: true,
425
- });
426
- }
427
- finally {
428
- authorizeSession.mockRestore();
429
- platform.mockRestore();
430
- }
431
- });
432
- it('does not open a Codex thread owned by another workspace', async () => {
433
- const platform = vi.spyOn(process, 'platform', 'get').mockReturnValue('win32');
434
- const authorizeSession = vi.spyOn(sessionFinder, 'canOpenCodexSessionInWorkspace')
435
- .mockResolvedValue(false);
436
- vi.mocked(execa).mockResolvedValue({ exitCode: 0 });
437
- try {
438
- const response = await app.request('/api/workspace/safe-workspace/launch', {
439
- method: 'POST',
440
- headers: { 'Content-Type': 'application/json' },
441
- body: JSON.stringify({
442
- targetId: 'codex-desktop',
443
- action: 'resume',
444
- sessionId: '0199a213-81c0-7800-8aa1-bbab2a035a53',
445
- }),
446
- });
447
- expect(response.status).toBe(404);
448
- await expect(response.json()).resolves.toEqual({
449
- error: 'Codex session not found in this workspace.',
450
- });
451
- expect(execa).not.toHaveBeenCalledWith('cmd.exe', expect.anything(), expect.anything());
452
- }
453
- finally {
454
- authorizeSession.mockRestore();
455
- platform.mockRestore();
456
- }
457
- });
458
- it('launches a terminal for a valid workspace', async () => {
459
- vi.spyOn(config, 'loadConfig').mockResolvedValue({ workspacesDir });
460
- vi.mocked(fs.realpath).mockImplementation(async (candidate) => path.resolve(String(candidate)));
461
- vi.mocked(workspace.loadWorkspaceManifest).mockResolvedValue({
462
- id: 'safe-workspace',
463
- repos: [path.join(workspacePath, 'repo')],
464
- workspacePath,
465
- });
466
- const response = await app.request('/api/workspace/safe-workspace/terminal', {
467
- method: 'POST',
468
- headers: { 'Content-Type': 'application/json' },
469
- body: JSON.stringify({
470
- assistant: 'claude',
471
- sessionId: '0199a213-81c0-7800-8aa1-bbab2a035a54',
472
- }),
473
- });
474
- expect(response.status).toBe(200);
475
- const data = await response.json();
476
- expect(data.success).toBe(true);
477
- expect(data.command).toBe('claude --resume 0199a213-81c0-7800-8aa1-bbab2a035a54');
478
- });
479
- it('rejects cross-origin requests to terminal launch', async () => {
480
- const response = await app.request('/api/workspace/safe-workspace/terminal', {
481
- method: 'POST',
482
- headers: {
483
- 'Content-Type': 'application/json',
484
- Origin: 'http://malicious-site.example.com',
485
- },
486
- body: JSON.stringify({
487
- assistant: 'claude',
488
- }),
489
- });
490
- expect(response.status).toBe(403);
491
- const data = await response.json();
492
- expect(data.error).toBe('Forbidden cross-origin request.');
493
- });
494
- it('rejects cross-origin requests to workspace launch', async () => {
495
- const response = await app.request('/api/workspace/safe-workspace/launch', {
496
- method: 'POST',
497
- headers: {
498
- 'Content-Type': 'application/json',
499
- Origin: 'http://malicious-site.example.com',
500
- },
501
- body: JSON.stringify({ targetId: 'codex-desktop' }),
502
- });
503
- expect(response.status).toBe(403);
504
- await expect(response.json()).resolves.toEqual({ error: 'Forbidden cross-origin request.' });
505
- });
506
- it('rejects invalid assistant and malformed session UUID', async () => {
507
- vi.spyOn(config, 'loadConfig').mockResolvedValue({ workspacesDir });
508
- vi.mocked(fs.realpath).mockImplementation(async (candidate) => path.resolve(String(candidate)));
509
- vi.mocked(workspace.loadWorkspaceManifest).mockResolvedValue({
510
- id: 'safe-workspace',
511
- repos: [path.join(workspacePath, 'repo')],
512
- workspacePath,
513
- });
514
- const invalidAssistantRes = await app.request('/api/workspace/safe-workspace/terminal', {
515
- method: 'POST',
516
- headers: { 'Content-Type': 'application/json' },
517
- body: JSON.stringify({
518
- assistant: 'unknown-harness-name',
519
- }),
520
- });
521
- expect(invalidAssistantRes.status).toBe(400);
522
- const invalidUuidRes = await app.request('/api/workspace/safe-workspace/terminal', {
523
- method: 'POST',
524
- headers: { 'Content-Type': 'application/json' },
525
- body: JSON.stringify({
526
- assistant: 'claude',
527
- sessionId: 'not-a-valid-uuid',
528
- }),
529
- });
530
- expect(invalidUuidRes.status).toBe(400);
531
- const invalidTitleRes = await app.request('/api/workspace/safe-workspace/terminal', {
532
- method: 'POST',
533
- headers: { 'Content-Type': 'application/json' },
534
- body: JSON.stringify({
535
- assistant: 'claude',
536
- title: 12345,
537
- }),
538
- });
539
- expect(invalidTitleRes.status).toBe(400);
540
- await expect(invalidTitleRes.json()).resolves.toEqual({ error: 'Title must be a string.' });
541
- });
542
- });
543
- describe('workspace recent session handoffs', () => {
544
- const workspacesDir = path.resolve('session-workspaces');
545
- const workspacePath = path.join(workspacesDir, 'safe-workspace');
546
- const codexId = '0199a213-81c0-7800-8aa1-bbab2a035a53';
547
- const claudeId = '0199a213-81c0-7800-8aa1-bbab2a035a54';
548
- const unauthorizedCodexId = '0199a213-81c0-7800-8aa1-bbab2a035a55';
549
- it('returns only server-authorized Desktop handoff capabilities', async () => {
550
- vi.spyOn(config, 'loadConfig').mockResolvedValue({ workspacesDir });
551
- vi.mocked(fs.realpath).mockImplementation(async (candidate) => path.resolve(String(candidate)));
552
- vi.mocked(workspace.loadWorkspaceManifest).mockResolvedValue({
553
- id: 'safe-workspace',
554
- repos: [path.join(workspacePath, 'repo')],
555
- workspacePath,
556
- });
557
- const find = vi.spyOn(sessionFinder, 'findSessions').mockResolvedValue([
558
- {
559
- id: 'copilot-session', assistant: 'copilot', title: 'Newer Copilot task',
560
- createdAt: '2026-08-16T03:00:00.000Z', updatedAt: '2026-08-16T03:00:00.000Z',
561
- messageCount: 2, workspacePath,
562
- },
563
- {
564
- id: 'antigravity-session', assistant: 'antigravity', title: 'Newer Antigravity task',
565
- createdAt: '2026-08-16T02:00:00.000Z', updatedAt: '2026-08-16T02:00:00.000Z',
566
- messageCount: 2, workspacePath,
567
- },
568
- {
569
- id: unauthorizedCodexId, assistant: 'codex', title: 'Fuzzy Codex task',
570
- createdAt: '2026-08-16T01:00:00.000Z', updatedAt: '2026-08-16T01:00:00.000Z',
571
- messageCount: 2, workspacePath,
572
- },
573
- {
574
- id: codexId, assistant: 'codex', title: 'Codex task',
575
- createdAt: '2026-08-15T00:00:00.000Z', updatedAt: '2026-08-16T00:00:00.000Z',
576
- messageCount: 4, workspacePath,
577
- },
578
- {
579
- id: claudeId, assistant: 'claude', title: 'Claude task',
580
- createdAt: '2026-08-14T00:00:00.000Z', updatedAt: '2026-08-15T00:00:00.000Z',
581
- messageCount: 2, workspacePath,
582
- },
583
- ]);
584
- const codexAuth = vi.spyOn(sessionFinder, 'canOpenCodexSessionInWorkspace')
585
- .mockImplementation(async (_workspace, _repos, sessionId) => sessionId === codexId);
586
- const claudeAuth = vi.spyOn(sessionFinder, 'canTransferClaudeSessionInWorkspace').mockResolvedValue(true);
587
- const platform = vi.spyOn(process, 'platform', 'get').mockReturnValue('win32');
588
- const architecture = vi.spyOn(process, 'arch', 'get').mockReturnValue('x64');
589
- vi.stubEnv('ANTHROPIC_API_KEY', '');
590
- vi.stubEnv('ANTHROPIC_AUTH_TOKEN', '');
591
- vi.stubEnv('CLAUDE_CODE_USE_BEDROCK', '');
592
- vi.stubEnv('CLAUDE_CODE_USE_VERTEX', '');
593
- vi.stubEnv('CLAUDE_CODE_USE_FOUNDRY', '');
594
- const provider = vi.spyOn(ProviderRegistry, 'getProvider').mockReturnValue({
595
- isConfigured: () => true,
596
- });
597
- try {
598
- const response = await app.request('/api/workspace/safe-workspace/sessions?limit=2&desktopHandoffOnly=true');
599
- const body = await response.json();
600
- expect(response.status).toBe(200);
601
- expect(body.sessions).toHaveLength(2);
602
- expect(body.sessions[0].desktopHandoff).toEqual({ targetId: 'codex-desktop', method: 'direct' });
603
- expect(body.sessions[1].desktopHandoff).toEqual({ targetId: 'claude-desktop', method: 'guided' });
604
- expect(codexAuth).toHaveBeenCalledWith(workspacePath, [path.join(workspacePath, 'repo')], codexId);
605
- expect(claudeAuth).toHaveBeenCalledWith(workspacePath, [path.join(workspacePath, 'repo')], claudeId);
606
- }
607
- finally {
608
- find.mockRestore();
609
- codexAuth.mockRestore();
610
- claudeAuth.mockRestore();
611
- provider.mockRestore();
612
- platform.mockRestore();
613
- architecture.mockRestore();
614
- vi.unstubAllEnvs();
615
- }
616
- });
617
- it('omits a discovered session when strong authorization fails', async () => {
618
- vi.spyOn(config, 'loadConfig').mockResolvedValue({ workspacesDir });
619
- vi.mocked(fs.realpath).mockImplementation(async (candidate) => path.resolve(String(candidate)));
620
- vi.mocked(workspace.loadWorkspaceManifest).mockResolvedValue({
621
- id: 'safe-workspace', repos: [], workspacePath,
622
- });
623
- const find = vi.spyOn(sessionFinder, 'findSessions').mockResolvedValue([{
624
- id: codexId, assistant: 'codex', title: 'Fuzzy match only',
625
- createdAt: '2026-08-15T00:00:00.000Z', updatedAt: '2026-08-16T00:00:00.000Z',
626
- messageCount: 2, workspacePath,
627
- }]);
628
- const codexAuth = vi.spyOn(sessionFinder, 'canOpenCodexSessionInWorkspace').mockResolvedValue(false);
629
- try {
630
- const response = await app.request('/api/workspace/safe-workspace/sessions?limit=3&desktopHandoffOnly=true');
631
- expect(response.status).toBe(200);
632
- await expect(response.json()).resolves.toEqual({ sessions: [] });
633
- }
634
- finally {
635
- find.mockRestore();
636
- codexAuth.mockRestore();
637
- }
638
- });
639
- it('rejects an excessive recent-session limit', async () => {
640
- const response = await app.request('/api/workspace/safe-workspace/sessions?limit=200');
641
- expect(response.status).toBe(400);
642
- await expect(response.json()).resolves.toEqual({ error: 'Session limit must be an integer from 1 to 20.' });
643
- });
644
- it('gates Claude Desktop transfer to supported subscription CLI environments', () => {
645
- const configured = () => true;
646
- expect(canOfferClaudeDesktopTransfer('win32', 'x64', {}, configured)).toBe(true);
647
- expect(canOfferClaudeDesktopTransfer('darwin', 'arm64', {}, configured)).toBe(true);
648
- expect(canOfferClaudeDesktopTransfer('linux', 'x64', {}, configured)).toBe(false);
649
- expect(canOfferClaudeDesktopTransfer('win32', 'arm64', {}, configured)).toBe(false);
650
- expect(canOfferClaudeDesktopTransfer('win32', 'x64', { ANTHROPIC_API_KEY: 'configured' }, configured)).toBe(false);
651
- expect(canOfferClaudeDesktopTransfer('win32', 'x64', {}, () => false)).toBe(false);
652
- });
653
- it('falls back gracefully when ProviderRegistry has no claude-cli', () => {
654
- const getProvider = vi.spyOn(ProviderRegistry, 'getProvider').mockReturnValue(undefined);
655
- try {
656
- expect(canOfferClaudeDesktopTransfer('win32', 'x64', {})).toBe(false);
657
- }
658
- finally {
659
- getProvider.mockRestore();
660
- }
661
- });
662
- });
663
- describe('POST /api/workspace/:id/resume', () => {
664
- it('rejects cross-origin requests to resume', async () => {
665
- const response = await app.request('/api/workspace/test-ws/resume', {
666
- method: 'POST',
667
- headers: {
668
- 'Content-Type': 'application/json',
669
- Origin: 'http://malicious-site.example.com',
670
- },
671
- body: JSON.stringify({ assistant: 'antigravity' }),
672
- });
673
- expect(response.status).toBe(403);
674
- await expect(response.json()).resolves.toEqual({ error: 'Forbidden cross-origin request.' });
675
- });
676
- it('rejects invalid sessionId UUID on resume', async () => {
677
- vi.spyOn(config, 'loadConfig').mockResolvedValue({ workspacesDir: '/mock/workspaces' });
678
- vi.spyOn(workspace, 'loadFeatureConfig').mockResolvedValue({
679
- id: 'test-ws',
680
- repos: [],
681
- assistants: ['antigravity'],
682
- });
683
- const response = await app.request('/api/workspace/test-ws/resume', {
684
- method: 'POST',
685
- headers: { 'Content-Type': 'application/json' },
686
- body: JSON.stringify({
687
- assistant: 'antigravity',
688
- sessionId: 'malicious-uuid-attempt',
689
- }),
690
- });
691
- expect(response.status).toBe(400);
692
- await expect(response.json()).resolves.toEqual({ error: 'Invalid session UUID format.' });
693
- });
694
- it('should fail with 400 if command is forbidden', async () => {
695
- vi.spyOn(config, 'loadConfig').mockResolvedValue({
696
- workspacesDir: '/mock/workspaces'
697
- });
698
- vi.spyOn(workspace, 'loadFeatureConfig').mockResolvedValue({
699
- id: 'test-ws',
700
- repos: [],
701
- assistants: ['antigravity']
702
- });
703
- const response = await app.request('/api/workspace/test-ws/resume', {
704
- method: 'POST',
705
- headers: { 'Content-Type': 'application/json' },
706
- body: JSON.stringify({
707
- command: 'malicious-editor',
708
- assistant: 'antigravity'
709
- })
710
- });
711
- expect(response.status).toBe(400);
712
- const data = await response.json();
713
- expect(data.error).toBe('Forbidden editor command');
714
- });
715
- it('should spawn the editor with proper options when resuming', async () => {
716
- vi.spyOn(config, 'loadConfig').mockResolvedValue({
717
- workspacesDir: '/mock/workspaces'
718
- });
719
- vi.spyOn(workspace, 'loadFeatureConfig').mockResolvedValue({
720
- id: 'test-ws',
721
- repos: [],
722
- assistants: ['antigravity']
723
- });
724
- const dummyChild = {
725
- unref: vi.fn(),
726
- catch: vi.fn().mockReturnThis()
727
- };
728
- vi.mocked(execa).mockReturnValue(dummyChild);
729
- const response = await app.request('/api/workspace/test-ws/resume', {
730
- method: 'POST',
731
- headers: { 'Content-Type': 'application/json' },
732
- body: JSON.stringify({
733
- command: 'code',
734
- assistant: 'antigravity'
735
- })
736
- });
737
- expect(response.status).toBe(200);
738
- const data = await response.json();
739
- expect(data.success).toBe(true);
740
- const isWin = process.platform === 'win32';
741
- expect(execa).toHaveBeenCalledWith('code', [expect.any(String)], {
742
- stdio: 'ignore',
743
- shell: isWin,
744
- });
745
- });
746
- });
747
- describe('GET /api/config', () => {
748
- it('should load configuration successfully', async () => {
749
- vi.spyOn(config, 'getConfigDir').mockReturnValue('/mock/config-dir');
750
- vi.spyOn(config, 'loadConfig').mockResolvedValue({
751
- devDir: '/mock/dev',
752
- scanDepth: 2
753
- });
754
- vi.spyOn(fs, 'access').mockResolvedValue();
755
- const response = await app.request('/api/config');
756
- expect(response.status).toBe(200);
757
- const data = await response.json();
758
- expect(data.config.devDir).toBe('/mock/dev');
759
- expect(data.exists).toBe(true);
760
- });
761
- });
762
- describe('GET /api/adapters', () => {
763
- it('should return all registered storage adapters with meta', async () => {
764
- const response = await app.request('/api/adapters');
765
- expect(response.status).toBe(200);
766
- const data = await response.json();
767
- expect(data.adapters).toBeDefined();
768
- expect(Array.isArray(data.adapters)).toBe(true);
769
- // `local` is the only built-in backend: a backend that relocates the
770
- // generated files out of the workspace cannot work, since assistants read
771
- // AGENTS.md and CLAUDE.md from the root. Plugins may still register more.
772
- const names = data.adapters.map((a) => a.name);
773
- expect(names).toContain('local');
774
- expect(names).not.toContain('central-vault');
775
- });
776
- });
777
- describe('GET /api/repos/branches', () => {
778
- it('should return 400 when the path parameter is missing', async () => {
779
- const response = await app.request('/api/repos/branches');
780
- expect(response.status).toBe(400);
781
- });
782
- it('should return 400 when the path escapes devDir', async () => {
783
- vi.mocked(config.loadConfig).mockResolvedValue({ devDir: '/mock/dev' });
784
- const response = await app.request(`/api/repos/branches?path=${encodeURIComponent('/mock/dev/../../etc')}`);
785
- expect(response.status).toBe(400);
786
- });
787
- it('should list local and origin branches for a repo inside devDir', async () => {
788
- vi.mocked(config.loadConfig).mockResolvedValue({ devDir: '/mock/dev' });
789
- vi.mocked(execa).mockResolvedValue({
790
- stdout: 'main\nfeature/x\norigin/HEAD\norigin/main\norigin/remote-only',
791
- });
792
- const response = await app.request(`/api/repos/branches?path=${encodeURIComponent('/mock/dev/repo1')}`);
793
- expect(response.status).toBe(200);
794
- const data = await response.json();
795
- expect(data.local).toEqual(['main', 'feature/x']);
796
- expect(data.remote).toEqual(['main', 'remote-only']);
797
- });
798
- });
799
- describe('POST /api/repos/new', () => {
800
- it('should return 400 when the name is missing', async () => {
801
- const response = await app.request('/api/repos/new', {
802
- method: 'POST',
803
- headers: { 'Content-Type': 'application/json' },
804
- body: JSON.stringify({}),
805
- });
806
- expect(response.status).toBe(400);
807
- });
808
- it('should scaffold a repo in devDir and return it', async () => {
809
- vi.mocked(config.loadConfig).mockResolvedValue({ devDir: '/mock/dev' });
810
- const repo = { name: 'newproj', path: '/mock/dev/newproj', defaultBranch: 'main' };
811
- vi.mocked(newRepo.createNewRepo).mockResolvedValue(repo);
812
- const response = await app.request('/api/repos/new', {
813
- method: 'POST',
814
- headers: { 'Content-Type': 'application/json' },
815
- body: JSON.stringify({ name: 'newproj' }),
816
- });
817
- expect(response.status).toBe(200);
818
- const data = await response.json();
819
- expect(data.success).toBe(true);
820
- expect(data.repo).toEqual(repo);
821
- expect(newRepo.createNewRepo).toHaveBeenCalledWith('/mock/dev', 'newproj');
822
- });
823
- });
824
- describe('GET and PUT /api/workspace/:id/knowledge', () => {
825
- it('should get workspace knowledge content', async () => {
826
- vi.spyOn(config, 'loadConfig').mockResolvedValue({
827
- workspacesDir: '/mock/workspaces'
828
- });
829
- vi.spyOn(fs, 'readFile').mockResolvedValue('# Mock Knowledge File content');
830
- const response = await app.request('/api/workspace/test-ws/knowledge');
831
- expect(response.status).toBe(200);
832
- const data = await response.json();
833
- expect(data.content).toBe('# Mock Knowledge File content');
834
- });
835
- it('should write workspace knowledge content', async () => {
836
- vi.spyOn(config, 'loadConfig').mockResolvedValue({
837
- workspacesDir: '/mock/workspaces'
838
- });
839
- vi.spyOn(fs, 'writeFile').mockResolvedValue();
840
- const response = await app.request('/api/workspace/test-ws/knowledge', {
841
- method: 'PUT',
842
- headers: { 'Content-Type': 'application/json' },
843
- body: JSON.stringify({ content: '# Updated Content' })
844
- });
845
- expect(response.status).toBe(200);
846
- const data = await response.json();
847
- expect(data.success).toBe(true);
848
- });
849
- it('returns a compatibility-friendly 400 when a structured entry omits its required title', async () => {
850
- const response = await app.request('/api/workspace/test-ws/knowledge/entry', {
851
- method: 'POST',
852
- headers: { 'Content-Type': 'application/json' },
853
- body: JSON.stringify({ type: 'decision', message: 'Use explicit titles.' }),
854
- });
855
- expect(response.status).toBe(400);
856
- await expect(response.json()).resolves.toEqual({ error: 'A short knowledge title is required.' });
857
- });
858
- it('rejects a base-knowledge repo traversal at the HTTP boundary', async () => {
859
- vi.spyOn(config, 'loadConfig').mockResolvedValue({ workspacesDir: '/mock/workspaces' });
860
- vi.mocked(workspace.loadFeatureConfig).mockResolvedValue({
861
- id: 'test-ws',
862
- repos: ['/mock/workspaces/test-ws/api'],
863
- });
864
- const response = await app.request('/api/workspace/test-ws/knowledge/entry', {
865
- method: 'POST',
866
- headers: { 'Content-Type': 'application/json' },
867
- body: JSON.stringify({
868
- type: 'gotcha', title: 'Traversal', message: 'must stay contained', repo: '../outside',
869
- }),
870
- });
871
- expect(response.status).toBe(400);
872
- await expect(response.json()).resolves.toMatchObject({ error: expect.stringMatching(/Invalid repository name/) });
873
- });
874
- });
875
- describe('GET /api/workspace/:id/plan', () => {
876
- it('should get workspace plan content', async () => {
877
- vi.spyOn(config, 'loadConfig').mockResolvedValue({
878
- workspacesDir: '/mock/workspaces'
879
- });
880
- vi.spyOn(fs, 'readFile').mockResolvedValue('# Mock Plan content');
881
- const response = await app.request('/api/workspace/test-ws/plan');
882
- expect(response.status).toBe(200);
883
- const data = await response.json();
884
- expect(data.content).toBe('# Mock Plan content');
885
- });
886
- });
887
- describe('POST /api/updates/install', () => {
888
- it('should fail if tool not found', async () => {
889
- const response = await app.request('/api/updates/install', {
890
- method: 'POST',
891
- headers: { 'Content-Type': 'application/json' },
892
- body: JSON.stringify({ toolId: 'invalid-tool' })
893
- });
894
- expect(response.status).toBe(404);
895
- const data = await response.json();
896
- expect(data.error).toBe('Tool not found');
897
- });
898
- it('should install tool successfully if execa exits with 0', async () => {
899
- vi.mocked(execa).mockResolvedValue({ exitCode: 0, stdout: 'Successfully updated' });
900
- const response = await app.request('/api/updates/install', {
901
- method: 'POST',
902
- headers: { 'Content-Type': 'application/json' },
903
- body: JSON.stringify({ toolId: 'nexusflow' })
904
- });
905
- expect(response.status).toBe(200);
906
- const data = await response.json();
907
- expect(data.success).toBe(true);
908
- expect(data.output).toBe('Successfully updated');
909
- expect(execa).toHaveBeenCalledWith('npm', ['install', '-g', '@mrpatronz/nexusflow'], expect.any(Object));
910
- });
911
- });
912
- describe('POST /api/workspace', () => {
913
- it('should complete workspace creation successfully', async () => {
914
- vi.spyOn(config, 'loadConfig').mockResolvedValue({
915
- devDir: '/mock',
916
- workspacesDir: '/mock/workspaces',
917
- storageProvider: 'local'
918
- });
919
- vi.spyOn(workspace, 'createWorkspace').mockResolvedValue('/mock/workspaces/test-ws-creation-no-pack');
920
- vi.spyOn(analyzers, 'analyzeAllRepos').mockResolvedValue(new Map());
921
- vi.spyOn(generators, 'generateContextFiles').mockResolvedValue(undefined);
922
- const response = await app.request('/api/workspace', {
923
- method: 'POST',
924
- headers: { 'Content-Type': 'application/json' },
925
- body: JSON.stringify({
926
- branchName: 'test-ws-creation-no-pack',
927
- description: 'A test workspace',
928
- repos: [{ name: 'repo-1', path: '/mock/repo-1' }],
929
- assistants: ['antigravity']
930
- })
931
- });
932
- expect(response.status).toBe(200);
933
- const data = await response.json();
934
- expect(data.success).toBe(true);
935
- expect(data.jobId).toBe('test-ws-creation-no-pack');
936
- // Wait a brief tick for the background job to execute
937
- await new Promise((resolve) => setTimeout(resolve, 50));
938
- expect(workspace.createWorkspace).toHaveBeenCalled();
939
- expect(analyzers.analyzeAllRepos).toHaveBeenCalled();
940
- expect(generators.generateContextFiles).toHaveBeenCalled();
941
- // Read status via SSE stream route
942
- const streamResponse = await app.request('/api/workspace/create-stream/test-ws-creation-no-pack');
943
- expect(streamResponse.status).toBe(200);
944
- const text = await streamResponse.text();
945
- expect(text).toContain('"status":"completed"');
946
- expect(text).toContain('"progress":100');
947
- });
948
- it('saves enabledSkills and enabledAgents during workspace creation', async () => {
949
- vi.spyOn(config, 'loadConfig').mockResolvedValue({
950
- devDir: '/mock',
951
- workspacesDir: '/mock/workspaces',
952
- storageProvider: 'local'
953
- });
954
- vi.spyOn(workspace, 'createWorkspace').mockResolvedValue('/mock/workspaces/skills-ws');
955
- vi.spyOn(analyzers, 'analyzeAllRepos').mockResolvedValue(new Map());
956
- vi.spyOn(generators, 'generateContextFiles').mockResolvedValue(undefined);
957
- vi.spyOn(skillsCatalog, 'saveWorkspaceSkillsConfig').mockResolvedValue(undefined);
958
- const response = await app.request('/api/workspace', {
959
- method: 'POST',
960
- headers: { 'Content-Type': 'application/json' },
961
- body: JSON.stringify({
962
- branchName: 'skills-ws',
963
- description: 'Skills workspace',
964
- repos: [{ name: 'repo-1', path: '/mock/repo-1' }],
965
- assistants: ['antigravity'],
966
- enabledSkills: ['pr-review-toolkit', 'unit-test-coverage'],
967
- enabledAgents: ['my-agent'],
968
- })
969
- });
970
- expect(response.status).toBe(200);
971
- const data = await response.json();
972
- expect(data.success).toBe(true);
973
- await new Promise((resolve) => setTimeout(resolve, 50));
974
- expect(skillsCatalog.saveWorkspaceSkillsConfig).toHaveBeenCalledWith(expect.stringContaining('skills-ws'), {
975
- enabledSkills: ['pr-review-toolkit', 'unit-test-coverage'],
976
- enabledAgents: ['my-agent'],
977
- enabledCategories: [],
978
- });
979
- });
980
- it('rejects workspace creation when repo path escapes devDir', async () => {
981
- vi.spyOn(config, 'loadConfig').mockResolvedValue({
982
- devDir: '/mock/dev',
983
- workspacesDir: '/mock/workspaces',
984
- storageProvider: 'local',
985
- });
986
- const response = await app.request('/api/workspace', {
987
- method: 'POST',
988
- headers: { 'Content-Type': 'application/json' },
989
- body: JSON.stringify({
990
- branchName: 'escaped-ws',
991
- description: 'Path escape test',
992
- repos: [{ name: 'repo-1', path: '/etc/shadow' }],
993
- assistants: ['antigravity'],
994
- }),
995
- });
996
- expect(response.status).toBe(400);
997
- const data = await response.json();
998
- expect(data.error).toBe('Invalid workspace path');
999
- });
1000
- // XML context packing tests removed.
1001
- it('rejects in-place creation without a name', async () => {
1002
- const response = await app.request('/api/workspace', {
1003
- method: 'POST',
1004
- headers: { 'Content-Type': 'application/json' },
1005
- body: JSON.stringify({
1006
- mode: 'in-place',
1007
- description: 'nameless',
1008
- repos: [{ name: 'repo-1', path: '/mock/repo-1' }],
1009
- assistants: ['claude']
1010
- })
1011
- });
1012
- expect(response.status).toBe(400);
1013
- expect((await response.json()).error).toContain('name');
1014
- });
1015
- it('rejects worktree creation without a branch name', async () => {
1016
- const response = await app.request('/api/workspace', {
1017
- method: 'POST',
1018
- headers: { 'Content-Type': 'application/json' },
1019
- body: JSON.stringify({
1020
- description: 'branchless',
1021
- repos: [{ name: 'repo-1', path: '/mock/repo-1' }],
1022
- assistants: ['claude']
1023
- })
1024
- });
1025
- expect(response.status).toBe(400);
1026
- expect((await response.json()).error).toContain('branchName');
1027
- });
1028
- it('creates an in-place workspace against the source repos (no worktree remap)', async () => {
1029
- vi.spyOn(config, 'loadConfig').mockResolvedValue({
1030
- devDir: '/mock',
1031
- workspacesDir: '/mock/workspaces',
1032
- storageProvider: 'local'
1033
- });
1034
- vi.spyOn(workspace, 'createWorkspace').mockResolvedValue('/mock/workspaces/my-quick-fix');
1035
- vi.spyOn(analyzers, 'analyzeAllRepos').mockResolvedValue(new Map());
1036
- vi.spyOn(generators, 'generateContextFiles').mockResolvedValue(undefined);
1037
- const response = await app.request('/api/workspace', {
1038
- method: 'POST',
1039
- headers: { 'Content-Type': 'application/json' },
1040
- body: JSON.stringify({
1041
- mode: 'in-place',
1042
- name: 'My Quick Fix',
1043
- projectId: 'billing',
1044
- description: 'in-place workspace',
1045
- repos: [{ name: 'repo-1', path: '/mock/repo-1', defaultBranch: 'main' }],
1046
- assistants: ['claude']
1047
- })
1048
- });
1049
- expect(response.status).toBe(200);
1050
- const data = await response.json();
1051
- // The name is slugified into the job/workspace id.
1052
- expect(data.jobId).toBe('my-quick-fix');
1053
- await new Promise((resolve) => setTimeout(resolve, 50));
1054
- const feature = vi.mocked(workspace.createWorkspace).mock.calls[0][0];
1055
- expect(feature.mode).toBe('in-place');
1056
- expect(feature.id).toBe('my-quick-fix');
1057
- expect(feature.projectId).toBe('billing');
1058
- // Repos stay at their source paths — no join(workspacePath, name) remap.
1059
- expect(feature.repos).toEqual(['/mock/repo-1']);
1060
- // Analysis also runs against the source repos.
1061
- const analyzed = vi.mocked(analyzers.analyzeAllRepos).mock.calls[0][0];
1062
- expect(analyzed[0].path).toBe('/mock/repo-1');
1063
- // The SSE stream reports the in-place step set.
1064
- const streamResponse = await app.request('/api/workspace/create-stream/my-quick-fix');
1065
- const text = await streamResponse.text();
1066
- expect(text).toContain('"status":"completed"');
1067
- expect(text).toContain('Register Workspace');
1068
- expect(text).not.toContain('Create Git Worktrees');
1069
- });
1070
- });
1071
- describe('Workflows Templates API', () => {
1072
- it('GET /api/workflows/templates should return list of templates', async () => {
1073
- vi.spyOn(workflows, 'getWorkflowTemplates').mockResolvedValue([
1074
- { id: 'test-id', name: 'Test Name', description: 'Test Desc', content: 'Test Content', custom: false }
1075
- ]);
1076
- const response = await app.request('/api/workflows/templates');
1077
- expect(response.status).toBe(200);
1078
- const data = await response.json();
1079
- expect(data).toEqual({
1080
- templates: [
1081
- { id: 'test-id', name: 'Test Name', description: 'Test Desc', content: 'Test Content', custom: false }
1082
- ]
1083
- });
1084
- });
1085
- it('POST /api/workflows/templates should create or update template', async () => {
1086
- vi.spyOn(workflows, 'saveWorkflowTemplate').mockResolvedValue({
1087
- id: 'test-id',
1088
- name: 'Test Name',
1089
- description: 'Test Desc',
1090
- content: 'Test Content',
1091
- custom: true
1092
- });
1093
- const response = await app.request('/api/workflows/templates', {
1094
- method: 'POST',
1095
- headers: { 'Content-Type': 'application/json' },
1096
- body: JSON.stringify({ id: 'old-id', name: 'Test Name', content: 'Test Content' })
1097
- });
1098
- expect(response.status).toBe(200);
1099
- const data = await response.json();
1100
- expect(data.success).toBe(true);
1101
- expect(data.template.id).toBe('test-id');
1102
- expect(workflows.saveWorkflowTemplate).toHaveBeenCalledWith('Test Name', 'Test Content', 'old-id');
1103
- });
1104
- it('DELETE /api/workflows/templates/:id should delete template', async () => {
1105
- vi.spyOn(workflows, 'getWorkflowTemplates').mockResolvedValue([
1106
- { id: 'test-id', name: 'Test Name', description: 'Test Desc', content: 'Test Content', custom: true }
1107
- ]);
1108
- vi.spyOn(workflows, 'deleteWorkflowTemplate').mockResolvedValue(undefined);
1109
- const response = await app.request('/api/workflows/templates/test-id', {
1110
- method: 'DELETE'
1111
- });
1112
- expect(response.status).toBe(200);
1113
- const data = await response.json();
1114
- expect(data.success).toBe(true);
1115
- expect(workflows.deleteWorkflowTemplate).toHaveBeenCalledWith('test-id');
1116
- });
1117
- it('POST /api/workflows/templates/:id/analyze should run inspection with comment', async () => {
1118
- vi.mocked(detectAi.detectAIAssistants).mockResolvedValue([
1119
- { name: 'antigravity', displayName: 'Antigravity', detected: true, command: 'agy' }
1120
- ]);
1121
- vi.mocked(execa).mockResolvedValue({
1122
- exitCode: 0,
1123
- stdout: 'Review result: Success\n=== SUGGESTED IMPROVEMENT START ===\n# Refined Strategy\n=== SUGGESTED IMPROVEMENT END ==='
1124
- });
1125
- const response = await app.request('/api/workflows/templates/test-id/analyze', {
1126
- method: 'POST',
1127
- headers: { 'Content-Type': 'application/json' },
1128
- body: JSON.stringify({
1129
- content: 'My Strategy guidelines',
1130
- assistant: 'antigravity',
1131
- comment: 'Check for timeouts'
1132
- })
1133
- });
1134
- expect(response.status).toBe(200);
1135
- const data = await response.json();
1136
- expect(data.analysis).toContain('Review result: Success');
1137
- expect(data.suggestedImprovement).toBe('# Refined Strategy');
1138
- expect(execa).toHaveBeenCalled();
1139
- const calledArgs = vi.mocked(execa).mock.calls[0][1];
1140
- expect(JSON.stringify(calledArgs)).toContain('Check for timeouts');
1141
- });
1142
- it('uses non-interactive codex exec with the prompt on stdin', async () => {
1143
- vi.mocked(detectAi.detectAIAssistants).mockResolvedValue([
1144
- { name: 'codex', displayName: 'OpenAI Codex', detected: true, command: 'codex' }
1145
- ]);
1146
- vi.mocked(execa).mockResolvedValue({
1147
- exitCode: 0,
1148
- stdout: 'Review result'
1149
- });
1150
- const response = await app.request('/api/workflows/templates/test-id/analyze', {
1151
- method: 'POST',
1152
- headers: { 'Content-Type': 'application/json' },
1153
- body: JSON.stringify({ content: 'Rules', assistant: 'codex' })
1154
- });
1155
- expect(response.status).toBe(200);
1156
- const [command, args, options] = vi.mocked(execa).mock.calls[0];
1157
- expect(command).toBe('codex');
1158
- expect(args).toEqual(['exec', '--color', 'never', '-']);
1159
- expect(options?.input).toContain('Rules');
1160
- });
1161
- describe('POST /api/workspace/suggest-workflow', () => {
1162
- it('should suggest a workflow using heuristics when local LLM is disabled', async () => {
1163
- vi.spyOn(config, 'loadConfig').mockResolvedValue({
1164
- version: '1.0',
1165
- devDir: '/dev',
1166
- workspacesDir: '/dev/workspaces',
1167
- defaultAssistant: null,
1168
- scanDepth: 2
1169
- });
1170
- const response = await app.request('/api/workspace/suggest-workflow', {
1171
- method: 'POST',
1172
- headers: { 'Content-Type': 'application/json' },
1173
- body: JSON.stringify({
1174
- description: 'Fix a typo in README.md and update comments',
1175
- repos: [{ name: 'my-project', path: '/dev/my-project', defaultBranch: 'main' }]
1176
- })
1177
- });
1178
- expect(response.status).toBe(200);
1179
- const data = await response.json();
1180
- expect(data.success).toBe(true);
1181
- expect(data.difficulty).toBe('simple');
1182
- expect(data.suggestedWorkflowId).toBe('solo-developer');
1183
- expect(data.customInstructions).toContain('Solo Developer');
1184
- });
1185
- it('should suggest a complex workflow when description contains complex keywords', async () => {
1186
- vi.spyOn(config, 'loadConfig').mockResolvedValue({
1187
- version: '1.0',
1188
- devDir: '/dev',
1189
- workspacesDir: '/dev/workspaces',
1190
- defaultAssistant: null,
1191
- scanDepth: 2
1192
- });
1193
- const response = await app.request('/api/workspace/suggest-workflow', {
1194
- method: 'POST',
1195
- headers: { 'Content-Type': 'application/json' },
1196
- body: JSON.stringify({
1197
- description: 'Refactor database schema and migrate data to postgres',
1198
- repos: [{ name: 'my-project', path: '/dev/my-project', defaultBranch: 'main' }]
1199
- })
1200
- });
1201
- expect(response.status).toBe(200);
1202
- const data = await response.json();
1203
- expect(data.success).toBe(true);
1204
- expect(data.difficulty).toBe('complex');
1205
- expect(data.suggestedWorkflowId).toBe('plan-implement-review');
1206
- expect(data.customInstructions).toContain('Plan, Implement, Review');
1207
- });
1208
- it('POST /api/workspace/:id/isolate isolates a repository', async () => {
1209
- vi.spyOn(config, 'loadConfig').mockResolvedValue({
1210
- version: '1.0',
1211
- devDir: '/dev',
1212
- workspacesDir: '/dev/workspaces',
1213
- defaultAssistant: null,
1214
- scanDepth: 2,
1215
- });
1216
- vi.mocked(workspace.isolateWorkspaceRepo).mockResolvedValue({
1217
- repoName: 'my-repo',
1218
- sourcePath: '/dev/my-repo',
1219
- worktreePath: '/dev/workspaces/my-ws/my-repo',
1220
- branchName: 'feat/my-repo-ws',
1221
- baseBranch: 'main',
1222
- alreadyIsolated: false,
1223
- });
1224
- const response = await app.request('/api/workspace/my-ws/isolate', {
1225
- method: 'POST',
1226
- headers: { 'Content-Type': 'application/json' },
1227
- body: JSON.stringify({ repo: 'my-repo', branchName: 'feat/my-repo-ws' }),
1228
- });
1229
- expect(response.status).toBe(200);
1230
- const data = await response.json();
1231
- expect(data.success).toBe(true);
1232
- expect(data.repoName).toBe('my-repo');
1233
- expect(data.branchName).toBe('feat/my-repo-ws');
1234
- expect(workspace.isolateWorkspaceRepo).toHaveBeenCalledWith(path.resolve('/dev/workspaces', 'my-ws'), 'my-repo', { branchName: 'feat/my-repo-ws', baseBranch: undefined });
1235
- });
1236
- it('POST /api/workspace/:id/isolate requires repo parameter', async () => {
1237
- vi.spyOn(config, 'loadConfig').mockResolvedValue({
1238
- version: '1.0',
1239
- devDir: '/dev',
1240
- workspacesDir: '/dev/workspaces',
1241
- defaultAssistant: null,
1242
- scanDepth: 2,
1243
- });
1244
- const response = await app.request('/api/workspace/my-ws/isolate', {
1245
- method: 'POST',
1246
- headers: { 'Content-Type': 'application/json' },
1247
- body: JSON.stringify({}),
1248
- });
1249
- expect(response.status).toBe(400);
1250
- const data = await response.json();
1251
- expect(data.error).toContain('Missing "repo" parameter');
1252
- });
1253
- it('POST /api/workspace/:id/isolate rejects whitespace repo and path traversal ids', async () => {
1254
- vi.spyOn(config, 'loadConfig').mockResolvedValue({
1255
- version: '1.0',
1256
- devDir: '/dev',
1257
- workspacesDir: '/dev/workspaces',
1258
- defaultAssistant: null,
1259
- scanDepth: 2,
1260
- });
1261
- const res1 = await app.request('/api/workspace/my-ws/isolate', {
1262
- method: 'POST',
1263
- headers: { 'Content-Type': 'application/json' },
1264
- body: JSON.stringify({ repo: ' ' }),
1265
- });
1266
- expect(res1.status).toBe(400);
1267
- const res2 = await app.request('/api/workspace/..%2f..%2fevil/isolate', {
1268
- method: 'POST',
1269
- headers: { 'Content-Type': 'application/json' },
1270
- body: JSON.stringify({ repo: 'my-repo' }),
1271
- });
1272
- expect(res2.status).toBe(400);
1273
- });
1274
- });
1275
- });
1276
- describe('Schedules API', () => {
1277
- beforeEach(() => {
1278
- vi.spyOn(config, 'getConfigDir').mockReturnValue('/mock/home/.nexusflow');
1279
- vi.spyOn(config, 'ensureConfigDir').mockResolvedValue(undefined);
1280
- vi.mocked(fs.mkdir).mockResolvedValue(undefined);
1281
- vi.mocked(fs.unlink).mockResolvedValue(undefined);
1282
- vi.mocked(fs.stat).mockResolvedValue({ mtimeMs: Date.now() });
1283
- vi.mocked(fs.open).mockResolvedValue({
1284
- writeFile: vi.fn().mockResolvedValue(undefined),
1285
- close: vi.fn().mockResolvedValue(undefined),
1286
- });
1287
- vi.mocked(fs.writeFile).mockResolvedValue(undefined);
1288
- });
1289
- it('GET /api/schedules should return jobs with a computed nextDueAt', async () => {
1290
- vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify({
1291
- version: 1,
1292
- jobs: [{
1293
- id: 'sync-ws-abc',
1294
- workspacePath: '/mock/ws',
1295
- task: 'sync',
1296
- intervalMinutes: 60,
1297
- enabled: true,
1298
- createdAt: '2026-01-01T00:00:00.000Z',
1299
- lastRunAt: '2026-01-02T10:00:00.000Z',
1300
- }],
1301
- }));
1302
- const response = await app.request('/api/schedules');
1303
- expect(response.status).toBe(200);
1304
- const data = await response.json();
1305
- expect(data.jobs).toHaveLength(1);
1306
- expect(data.jobs[0].id).toBe('sync-ws-abc');
1307
- expect(data.jobs[0].nextDueAt).toBe('2026-01-02T11:00:00.000Z');
1308
- });
1309
- it('POST /api/schedules should reject an unknown task', async () => {
1310
- const response = await app.request('/api/schedules', {
1311
- method: 'POST',
1312
- headers: { 'Content-Type': 'application/json' },
1313
- body: JSON.stringify({ task: 'deploy', every: '2h', workspacePath: '/mock/ws' }),
1314
- });
1315
- expect(response.status).toBe(400);
1316
- const data = await response.json();
1317
- expect(data.error).toContain('task must be');
1318
- });
1319
- it('POST /api/schedules should create a job for a valid workspace', async () => {
1320
- vi.mocked(fs.readFile).mockRejectedValue(new Error('ENOENT')); // empty store
1321
- vi.spyOn(config, 'loadConfig').mockResolvedValue({ workspacesDir: '/mock/workspaces' });
1322
- vi.spyOn(workspace, 'loadFeatureConfig').mockResolvedValue({
1323
- id: 'ws-1',
1324
- branchName: 'ws-1',
1325
- description: '',
1326
- repos: [],
1327
- assistants: [],
1328
- workspacePath: '/mock/workspaces/ws-1',
1329
- createdAt: '2026-01-01T00:00:00.000Z',
1330
- });
1331
- const response = await app.request('/api/schedules', {
1332
- method: 'POST',
1333
- headers: { 'Content-Type': 'application/json' },
1334
- body: JSON.stringify({ task: 'refresh', every: '2h', workspaceId: 'ws-1' }),
1335
- });
1336
- expect(response.status).toBe(201);
1337
- const data = await response.json();
1338
- expect(data.job.task).toBe('refresh');
1339
- expect(data.job.intervalMinutes).toBe(120);
1340
- expect(data.job.enabled).toBe(true);
1341
- expect(fs.writeFile).toHaveBeenCalled();
1342
- });
1343
- it('DELETE /api/schedules/:id should 404 for an unknown job', async () => {
1344
- vi.mocked(fs.readFile).mockRejectedValue(new Error('ENOENT'));
1345
- const response = await app.request('/api/schedules/nope', { method: 'DELETE' });
1346
- expect(response.status).toBe(404);
1347
- });
1348
- });
1349
- describe('Security hardening', () => {
1350
- describe('workspace path traversal (A2.2)', () => {
1351
- it('DELETE /api/workspace/:id rejects a traversal id without touching the filesystem', async () => {
1352
- vi.spyOn(config, 'loadConfig').mockResolvedValue({
1353
- workspacesDir: '/mock/workspaces',
1354
- });
1355
- const deleteSpy = vi
1356
- .spyOn(workspace, 'deleteWorkspace')
1357
- .mockResolvedValue(undefined);
1358
- const response = await app.request(`/api/workspace/${encodeURIComponent('../../evil')}`, { method: 'DELETE' });
1359
- expect(response.status).toBe(400);
1360
- expect(deleteSpy).not.toHaveBeenCalled();
1361
- });
1362
- it('GET /api/workspace/:id/knowledge rejects a traversal id without reading the file', async () => {
1363
- vi.spyOn(config, 'loadConfig').mockResolvedValue({
1364
- workspacesDir: '/mock/workspaces',
1365
- });
1366
- const readSpy = vi.spyOn(fs, 'readFile');
1367
- const response = await app.request(`/api/workspace/${encodeURIComponent('../../../etc/passwd')}/knowledge`);
1368
- expect(response.status).toBe(400);
1369
- expect(readSpy).not.toHaveBeenCalled();
1370
- });
1371
- it('accepts a normal workspace id', async () => {
1372
- vi.spyOn(config, 'loadConfig').mockResolvedValue({
1373
- workspacesDir: '/mock/workspaces',
1374
- });
1375
- vi.mocked(fs.readFile).mockResolvedValue('# Knowledge');
1376
- const response = await app.request('/api/workspace/my-feature/knowledge');
1377
- expect(response.status).toBe(200);
1378
- });
1379
- });
1380
- describe('diff repo containment (A2.3)', () => {
1381
- it('rejects a sibling-prefix repo escape', async () => {
1382
- vi.spyOn(config, 'loadConfig').mockResolvedValue({
1383
- workspacesDir: '/mock/workspaces',
1384
- });
1385
- const execSpy = vi.mocked(execa);
1386
- // Workspace "feat"; sibling "feat-secret" shares the name prefix.
1387
- const response = await app.request(`/api/workspace/feat/changes/diff?repo=${encodeURIComponent('../feat-secret/repo')}&file=x.ts`);
1388
- expect(response.status).toBe(400);
1389
- expect(execSpy).not.toHaveBeenCalled();
1390
- });
1391
- });
1392
- describe('CORS is restricted to localhost (A2.1)', () => {
1393
- it('does not echo a non-localhost Origin', async () => {
1394
- vi.spyOn(config, 'loadConfig').mockResolvedValue({
1395
- workspacesDir: '/mock/workspaces',
1396
- });
1397
- const response = await app.request('/api/config', {
1398
- headers: { Origin: 'http://evil.example.com' },
1399
- });
1400
- expect(response.headers.get('access-control-allow-origin')).not.toBe('http://evil.example.com');
1401
- });
1402
- it('allows a localhost Origin', async () => {
1403
- vi.spyOn(config, 'loadConfig').mockResolvedValue({
1404
- workspacesDir: '/mock/workspaces',
1405
- });
1406
- const response = await app.request('/api/config', {
1407
- headers: { Origin: 'http://localhost:5173' },
1408
- });
1409
- expect(response.headers.get('access-control-allow-origin')).toBe('http://localhost:5173');
1410
- });
1411
- });
1412
- describe('update download URL allow-list (A2.1)', () => {
1413
- it('accepts GitHub release hosts over HTTPS', () => {
1414
- expect(isAllowedUpdateUrl('https://github.com/mrpatronz/nexusflow/releases/download/v1.3.0/Setup.exe')).toBe(true);
1415
- expect(isAllowedUpdateUrl('https://objects.githubusercontent.com/foo/Setup.exe')).toBe(true);
1416
- });
1417
- it('rejects non-GitHub hosts and non-HTTPS schemes', () => {
1418
- expect(isAllowedUpdateUrl('https://evil.example.com/Setup.exe')).toBe(false);
1419
- expect(isAllowedUpdateUrl('http://github.com/Setup.exe')).toBe(false);
1420
- expect(isAllowedUpdateUrl('file:///C:/Setup.exe')).toBe(false);
1421
- expect(isAllowedUpdateUrl('not a url')).toBe(false);
1422
- });
1423
- it('POST /api/updates/download rejects a disallowed host', async () => {
1424
- const response = await app.request('/api/updates/download', {
1425
- method: 'POST',
1426
- headers: { 'Content-Type': 'application/json' },
1427
- body: JSON.stringify({ downloadUrl: 'https://evil.example.com/Setup.exe' }),
1428
- });
1429
- expect(response.status).toBe(400);
1430
- });
1431
- });
1432
- });
1433
- describe('Services & orchestration endpoints', () => {
1434
- beforeEach(() => {
1435
- vi.spyOn(config, 'loadConfig').mockResolvedValue({ workspacesDir: '/mock/workspaces' });
1436
- });
1437
- it('POST /services/start re-detects server-side and ignores the request body', async () => {
1438
- vi.mocked(orchestration.detectAllServices).mockResolvedValue([
1439
- { name: 'api', command: 'npm', args: ['run', 'dev'], cwd: '/mock', source: 'package.json' },
1440
- ]);
1441
- vi.mocked(orchestration.startServices).mockResolvedValue(undefined);
1442
- const response = await app.request('/api/workspace/ws/services/start', {
1443
- method: 'POST',
1444
- headers: { 'Content-Type': 'application/json' },
1445
- body: JSON.stringify({ services: [{ name: 'evil', command: 'rm', args: ['-rf', '/'], cwd: '/', source: 'x' }] }),
1446
- });
1447
- expect(response.status).toBe(200);
1448
- expect(orchestration.detectAllServices).toHaveBeenCalled();
1449
- // The started services are the DETECTED ones, not the client's payload.
1450
- const started = vi.mocked(orchestration.startServices).mock.calls[0]?.[0];
1451
- expect(started).toEqual([{ name: 'api', command: 'npm', args: ['run', 'dev'], cwd: '/mock', source: 'package.json' }]);
1452
- });
1453
- it('POST /services/:name/start 404s for an unknown service', async () => {
1454
- vi.mocked(orchestration.detectAllServices).mockResolvedValue([]);
1455
- const response = await app.request('/api/workspace/ws/services/ghost/start', { method: 'POST' });
1456
- expect(response.status).toBe(404);
1457
- });
1458
- it('POST /orchestrators/start rejects an unknown detection id', async () => {
1459
- vi.mocked(orchestration.detectOrchestrationTools).mockResolvedValue([]);
1460
- const response = await app.request('/api/workspace/ws/orchestrators/start', {
1461
- method: 'POST',
1462
- headers: { 'Content-Type': 'application/json' },
1463
- body: JSON.stringify({ id: 'docker-compose:nope.yml' }),
1464
- });
1465
- expect(response.status).toBe(404);
1466
- });
1467
- it('GET /services/logs/:name rejects a traversal service name without reading outside the log dir', async () => {
1468
- const response = await app.request(`/api/workspace/ws/services/logs/${encodeURIComponent('../../secret')}`);
1469
- expect(response.status).toBe(400);
1470
- });
1471
- });
1472
- describe('Skills & Categories Endpoints', () => {
1473
- it('GET /api/skills/categories returns default categories', async () => {
1474
- vi.mocked(skillsCatalog.getSkillCategories).mockResolvedValue([
1475
- {
1476
- id: 'pull-requests',
1477
- name: 'Pull Requests & Review',
1478
- description: 'PR review skills',
1479
- icon: 'git-pull-request',
1480
- color: '#3b82f6',
1481
- custom: false,
1482
- isTemplate: true,
1483
- },
1484
- ]);
1485
- const response = await app.request('/api/skills/categories');
1486
- expect(response.status).toBe(200);
1487
- const data = await response.json();
1488
- expect(data.categories).toBeDefined();
1489
- expect(data.categories.length).toBe(1);
1490
- expect(data.categories[0].id).toBe('pull-requests');
1491
- expect(data.categories[0].isTemplate).toBe(true);
1492
- });
1493
- it('POST /api/skills/categories creates a custom category', async () => {
1494
- vi.mocked(skillsCatalog.saveSkillCategory).mockResolvedValue({
1495
- id: 'serverless-workflows',
1496
- name: 'Serverless Workflows',
1497
- description: 'AWS Lambda and Cloudflare workers',
1498
- icon: 'zap',
1499
- color: '#ec4899',
1500
- custom: true,
1501
- isTemplate: false,
1502
- });
1503
- const response = await app.request('/api/skills/categories', {
1504
- method: 'POST',
1505
- headers: { 'Content-Type': 'application/json' },
1506
- body: JSON.stringify({
1507
- name: 'Serverless Workflows',
1508
- description: 'AWS Lambda and Cloudflare workers',
1509
- icon: 'zap',
1510
- color: '#ec4899',
1511
- }),
1512
- });
1513
- expect(response.status).toBe(200);
1514
- const data = await response.json();
1515
- expect(data.success).toBe(true);
1516
- expect(data.category.id).toBe('serverless-workflows');
1517
- expect(data.category.custom).toBe(true);
1518
- });
1519
- it('GET /api/skills returns all skills', async () => {
1520
- vi.mocked(skillsCatalog.getAllSkills).mockResolvedValue([
1521
- {
1522
- id: 'pr-review-toolkit',
1523
- name: 'pr-review-toolkit',
1524
- title: 'PR Review Toolkit',
1525
- category: 'pull-requests',
1526
- description: 'Review pull requests',
1527
- custom: false,
1528
- content: '# PR Review',
1529
- },
1530
- ]);
1531
- const response = await app.request('/api/skills');
1532
- expect(response.status).toBe(200);
1533
- const data = await response.json();
1534
- expect(data.skills).toBeDefined();
1535
- expect(data.skills.length).toBe(1);
1536
- expect(data.skills[0].id).toBe('pr-review-toolkit');
1537
- });
1538
- it('POST /api/skills creates a new skill package', async () => {
1539
- vi.mocked(skillsCatalog.saveSkill).mockResolvedValue({
1540
- id: 'graphql-linter',
1541
- name: 'graphql-linter',
1542
- title: 'GraphQL Schema Linter',
1543
- category: 'database-migrations',
1544
- description: 'Validates GraphQL schemas for deprecated fields',
1545
- custom: true,
1546
- content: '# GraphQL Schema Linter\n\nRun graphql-inspector validate.',
1547
- });
1548
- const response = await app.request('/api/skills', {
1549
- method: 'POST',
1550
- headers: { 'Content-Type': 'application/json' },
1551
- body: JSON.stringify({
1552
- name: 'graphql-linter',
1553
- title: 'GraphQL Schema Linter',
1554
- category: 'database-migrations',
1555
- description: 'Validates GraphQL schemas for deprecated fields',
1556
- content: '# GraphQL Schema Linter\n\nRun graphql-inspector validate.',
1557
- }),
1558
- });
1559
- expect(response.status).toBe(200);
1560
- const data = await response.json();
1561
- expect(data.success).toBe(true);
1562
- expect(data.skill.id).toBe('graphql-linter');
1563
- expect(data.skill.custom).toBe(true);
1564
- });
1565
- it('GET and POST /api/agents administer Codex-native agents', async () => {
1566
- const agent = {
1567
- id: 'reviewer',
1568
- name: 'reviewer',
1569
- category: 'general',
1570
- description: 'Review a fixed diff.',
1571
- developerInstructions: 'Review correctness and security.',
1572
- sandboxMode: 'read-only',
1573
- custom: true,
1574
- };
1575
- vi.mocked(agentsCatalog.getAllAgents).mockResolvedValue([agent]);
1576
- vi.mocked(agentsCatalog.saveAgent).mockResolvedValue(agent);
1577
- const listResponse = await app.request('/api/agents');
1578
- expect(listResponse.status).toBe(200);
1579
- await expect(listResponse.json()).resolves.toEqual({ agents: [agent] });
1580
- const saveResponse = await app.request('/api/agents', {
1581
- method: 'POST',
1582
- headers: { 'Content-Type': 'application/json' },
1583
- body: JSON.stringify(agent),
1584
- });
1585
- expect(saveResponse.status).toBe(200);
1586
- expect(agentsCatalog.saveAgent).toHaveBeenCalledWith(agent);
1587
- });
1588
- it('blocks deleting resources that are still assigned to a workspace', async () => {
1589
- const workspacePath = path.resolve('/workspaces/example');
1590
- vi.mocked(config.loadConfig).mockResolvedValue({ workspacesDir: path.resolve('/workspaces') });
1591
- vi.mocked(workspace.listWorkspaces).mockResolvedValue([{ id: 'example', workspacePath }]);
1592
- vi.mocked(fs.realpath).mockImplementation(async (candidate) => path.resolve(String(candidate)));
1593
- vi.mocked(workspace.loadWorkspaceManifest).mockResolvedValue({ workspacePath });
1594
- vi.mocked(skillsCatalog.getWorkspaceSkillsConfig).mockResolvedValue({
1595
- schemaVersion: 1,
1596
- revision: 1,
1597
- enabledSkills: ['pr-review-toolkit'],
1598
- enabledAgents: ['reviewer'],
1599
- enabledCategories: [],
1600
- });
1601
- const skillResponse = await app.request('/api/skills/pr-review-toolkit', { method: 'DELETE' });
1602
- expect(skillResponse.status).toBe(409);
1603
- await expect(skillResponse.json()).resolves.toMatchObject({ workspaces: ['example'] });
1604
- expect(skillsCatalog.deleteSkill).not.toHaveBeenCalled();
1605
- const agentResponse = await app.request('/api/agents/reviewer', { method: 'DELETE' });
1606
- expect(agentResponse.status).toBe(409);
1607
- await expect(agentResponse.json()).resolves.toMatchObject({ workspaces: ['example'] });
1608
- expect(agentsCatalog.deleteAgent).not.toHaveBeenCalled();
1609
- });
1610
- it('requires an exact existing workspace before reading resource assignments', async () => {
1611
- vi.mocked(config.loadConfig).mockResolvedValue({ workspacesDir: '/workspaces' });
1612
- vi.mocked(fs.realpath).mockImplementation(async (candidate) => {
1613
- if (String(candidate).endsWith('missing')) {
1614
- throw Object.assign(new Error('missing'), { code: 'ENOENT' });
1615
- }
1616
- return String(candidate);
1617
- });
1618
- const response = await app.request('/api/skills/workspace/missing');
1619
- expect(response.status).toBe(404);
1620
- expect(skillsCatalog.getWorkspaceSkillsConfig).not.toHaveBeenCalled();
1621
- });
1622
- it('saves versioned skill and agent assignments for an exact workspace', async () => {
1623
- const workspacePath = path.resolve('/workspaces/example');
1624
- vi.mocked(config.loadConfig).mockResolvedValue({ workspacesDir: path.resolve('/workspaces') });
1625
- vi.mocked(fs.realpath).mockImplementation(async (candidate) => path.resolve(String(candidate)));
1626
- vi.mocked(workspace.loadWorkspaceManifest).mockResolvedValue({ workspacePath });
1627
- vi.mocked(skillsCatalog.saveWorkspaceSkillsConfig).mockResolvedValue({
1628
- schemaVersion: 1,
1629
- revision: 4,
1630
- enabledSkills: ['pr-review-toolkit'],
1631
- enabledAgents: ['reviewer'],
1632
- enabledCategories: [],
1633
- });
1634
- const response = await app.request('/api/skills/workspace/example/assign', {
1635
- method: 'POST',
1636
- headers: { 'Content-Type': 'application/json' },
1637
- body: JSON.stringify({
1638
- expectedRevision: 3,
1639
- enabledSkills: ['pr-review-toolkit'],
1640
- enabledAgents: ['reviewer'],
1641
- enabledCategories: [],
1642
- }),
1643
- });
1644
- expect(response.status).toBe(200);
1645
- expect(skillsCatalog.saveWorkspaceSkillsConfig).toHaveBeenCalledWith(workspacePath, expect.objectContaining({ enabledSkills: ['pr-review-toolkit'], enabledAgents: ['reviewer'] }), 3);
1646
- });
1647
- it('rejects workspace assignments that reference missing catalog resources', async () => {
1648
- const workspacePath = path.resolve('/workspaces/example');
1649
- vi.mocked(config.loadConfig).mockResolvedValue({ workspacesDir: path.resolve('/workspaces') });
1650
- vi.mocked(fs.realpath).mockImplementation(async (candidate) => path.resolve(String(candidate)));
1651
- vi.mocked(workspace.loadWorkspaceManifest).mockResolvedValue({ workspacePath });
1652
- vi.mocked(resourceService.validateResourceSelections).mockRejectedValueOnce(new resourceService.ResourceSelectionError(['missing-skill'], ['missing_agent']));
1653
- const response = await app.request('/api/skills/workspace/example/assign', {
1654
- method: 'POST',
1655
- headers: { 'Content-Type': 'application/json' },
1656
- body: JSON.stringify({
1657
- expectedRevision: 0,
1658
- enabledSkills: ['missing-skill'],
1659
- enabledAgents: ['missing_agent'],
1660
- enabledCategories: [],
1661
- }),
1662
- });
1663
- expect(response.status).toBe(400);
1664
- await expect(response.json()).resolves.toMatchObject({
1665
- missingSkills: ['missing-skill'],
1666
- missingAgents: ['missing_agent'],
1667
- });
1668
- expect(skillsCatalog.saveWorkspaceSkillsConfig).not.toHaveBeenCalled();
1669
- });
1670
- it('requires an assignment revision and maps stale revisions to a conflict', async () => {
1671
- const missingRevision = await app.request('/api/skills/workspace/example/assign', {
1672
- method: 'POST',
1673
- headers: { 'Content-Type': 'application/json' },
1674
- body: JSON.stringify({ enabledSkills: [], enabledAgents: [] }),
1675
- });
1676
- expect(missingRevision.status).toBe(400);
1677
- expect(config.loadConfig).not.toHaveBeenCalled();
1678
- const workspacePath = path.resolve('/workspaces/example');
1679
- vi.mocked(config.loadConfig).mockResolvedValue({ workspacesDir: path.resolve('/workspaces') });
1680
- vi.mocked(fs.realpath).mockImplementation(async (candidate) => path.resolve(String(candidate)));
1681
- vi.mocked(workspace.loadWorkspaceManifest).mockResolvedValue({ workspacePath });
1682
- const revisionError = new skillsCatalog.WorkspaceResourceRevisionError(2, 3);
1683
- revisionError.message = 'Workspace resource configuration changed (expected revision 2, current 3).';
1684
- vi.mocked(skillsCatalog.saveWorkspaceSkillsConfig).mockRejectedValueOnce(revisionError);
1685
- const staleRevision = await app.request('/api/skills/workspace/example/assign', {
1686
- method: 'POST',
1687
- headers: { 'Content-Type': 'application/json' },
1688
- body: JSON.stringify({ expectedRevision: 2, enabledSkills: [], enabledAgents: [] }),
1689
- });
1690
- expect(staleRevision.status).toBe(409);
1691
- await expect(staleRevision.json()).resolves.toMatchObject({
1692
- error: expect.stringContaining('expected revision 2, current 3'),
1693
- });
1694
- });
1695
- });
1696
- });
1697
- //# sourceMappingURL=server.test.js.map