@bevel-software/platform-core-backend 0.22.0 → 0.23.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 (325) hide show
  1. package/dist/core/core-ports.d.ts +11 -1
  2. package/dist/core/core-ports.d.ts.map +1 -1
  3. package/dist/core/core-ports.js.map +1 -1
  4. package/dist/core/create-core-server.d.ts +3 -3
  5. package/dist/core/create-core-server.d.ts.map +1 -1
  6. package/dist/core/create-core-server.js +30 -11
  7. package/dist/core/create-core-server.js.map +1 -1
  8. package/dist/core/create-core-services.d.ts +9 -2
  9. package/dist/core/create-core-services.d.ts.map +1 -1
  10. package/dist/core/create-core-services.js +112 -23
  11. package/dist/core/create-core-services.js.map +1 -1
  12. package/dist/core/lifecycle.d.ts +34 -1
  13. package/dist/core/lifecycle.d.ts.map +1 -1
  14. package/dist/core/lifecycle.js +89 -13
  15. package/dist/core/lifecycle.js.map +1 -1
  16. package/dist/core-config.d.ts +0 -12
  17. package/dist/core-config.d.ts.map +1 -1
  18. package/dist/core-config.js +11 -13
  19. package/dist/core-config.js.map +1 -1
  20. package/dist/index.d.ts +3 -2
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +2 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/modules/access/access-control.interface.d.ts +42 -12
  25. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  26. package/dist/modules/access/access-control.service.d.ts +67 -10
  27. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  28. package/dist/modules/access/access-control.service.js +214 -35
  29. package/dist/modules/access/access-control.service.js.map +1 -1
  30. package/dist/modules/access/access.routes.d.ts.map +1 -1
  31. package/dist/modules/access/access.routes.js +13 -15
  32. package/dist/modules/access/access.routes.js.map +1 -1
  33. package/dist/modules/auth/account-admission.d.ts +58 -7
  34. package/dist/modules/auth/account-admission.d.ts.map +1 -1
  35. package/dist/modules/auth/account-admission.js +44 -1
  36. package/dist/modules/auth/account-admission.js.map +1 -1
  37. package/dist/modules/auth/account.routes.d.ts +1 -1
  38. package/dist/modules/auth/account.routes.d.ts.map +1 -1
  39. package/dist/modules/auth/account.routes.js +61 -5
  40. package/dist/modules/auth/account.routes.js.map +1 -1
  41. package/dist/modules/auth/auth.middleware.d.ts +1 -1
  42. package/dist/modules/auth/auth.middleware.d.ts.map +1 -1
  43. package/dist/modules/auth/auth.middleware.js +18 -7
  44. package/dist/modules/auth/auth.middleware.js.map +1 -1
  45. package/dist/modules/auth/auth.routes.d.ts.map +1 -1
  46. package/dist/modules/auth/auth.routes.js +8 -0
  47. package/dist/modules/auth/auth.routes.js.map +1 -1
  48. package/dist/modules/auth/auth.service.d.ts +70 -2
  49. package/dist/modules/auth/auth.service.d.ts.map +1 -1
  50. package/dist/modules/auth/auth.service.js +176 -7
  51. package/dist/modules/auth/auth.service.js.map +1 -1
  52. package/dist/modules/auth/oidc-auth-provider.d.ts.map +1 -1
  53. package/dist/modules/auth/oidc-auth-provider.js +7 -2
  54. package/dist/modules/auth/oidc-auth-provider.js.map +1 -1
  55. package/dist/modules/database/core-schema.d.ts +41 -24
  56. package/dist/modules/database/core-schema.d.ts.map +1 -1
  57. package/dist/modules/database/core-schema.js +35 -27
  58. package/dist/modules/database/core-schema.js.map +1 -1
  59. package/dist/modules/kb-fs/branch-name.d.ts.map +1 -1
  60. package/dist/modules/kb-fs/branch-name.js +12 -2
  61. package/dist/modules/kb-fs/branch-name.js.map +1 -1
  62. package/dist/modules/kb-fs/repo-path.d.ts +11 -16
  63. package/dist/modules/kb-fs/repo-path.d.ts.map +1 -1
  64. package/dist/modules/kb-fs/repo-path.js +79 -0
  65. package/dist/modules/kb-fs/repo-path.js.map +1 -1
  66. package/dist/modules/kb-sync/kb-sync.routes.d.ts +2 -2
  67. package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -1
  68. package/dist/modules/kb-sync/kb-sync.routes.js +14 -3
  69. package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -1
  70. package/dist/modules/kb-sync/sync-auth.d.ts +3 -1
  71. package/dist/modules/kb-sync/sync-auth.d.ts.map +1 -1
  72. package/dist/modules/kb-sync/sync-auth.js +1 -1
  73. package/dist/modules/kb-sync/sync-auth.js.map +1 -1
  74. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
  75. package/dist/modules/mcp/mcp-auth.middleware.js +21 -6
  76. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
  77. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  78. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +3 -2
  79. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  80. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  81. package/dist/modules/settings/deployment-settings.service.js +8 -3
  82. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  83. package/dist/modules/settings/setup.routes.d.ts +52 -1
  84. package/dist/modules/settings/setup.routes.d.ts.map +1 -1
  85. package/dist/modules/settings/setup.routes.js +207 -19
  86. package/dist/modules/settings/setup.routes.js.map +1 -1
  87. package/dist/modules/skills/skills.contract.d.ts +90 -18
  88. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  89. package/dist/modules/skills/skills.contract.js +4 -1
  90. package/dist/modules/skills/skills.contract.js.map +1 -1
  91. package/dist/modules/skills/skills.service.d.ts +98 -9
  92. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  93. package/dist/modules/skills/skills.service.js +239 -37
  94. package/dist/modules/skills/skills.service.js.map +1 -1
  95. package/dist/modules/skills/skills.tools.d.ts +7 -0
  96. package/dist/modules/skills/skills.tools.d.ts.map +1 -1
  97. package/dist/modules/skills/skills.tools.js +71 -7
  98. package/dist/modules/skills/skills.tools.js.map +1 -1
  99. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  100. package/dist/modules/tool-auth/external-api-key.service.js +4 -2
  101. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  102. package/dist/modules/tool-auth/internal-token.service.d.ts +3 -3
  103. package/dist/modules/tool-auth/tool-auth.middleware.d.ts +16 -7
  104. package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
  105. package/dist/modules/tool-auth/tool-auth.middleware.js +34 -12
  106. package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
  107. package/dist/modules/tool-helpers/tool-handler.d.ts +2 -1
  108. package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
  109. package/dist/modules/tool-helpers/tool-handler.js +25 -4
  110. package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
  111. package/dist/modules/tool-helpers/tool.contract.d.ts +3 -2
  112. package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
  113. package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
  114. package/dist/modules/tool-helpers/validate-token.js +1 -1
  115. package/dist/modules/tool-helpers/validate-token.js.map +1 -1
  116. package/dist/modules/workflow/agent-tools/change-request-summary.d.ts +98 -0
  117. package/dist/modules/workflow/agent-tools/change-request-summary.d.ts.map +1 -0
  118. package/dist/modules/workflow/agent-tools/change-request-summary.js +81 -0
  119. package/dist/modules/workflow/agent-tools/change-request-summary.js.map +1 -0
  120. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  121. package/dist/modules/workflow/agent-tools/workflow.tools.js +75 -35
  122. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  123. package/dist/modules/workflow/file-lock.service.d.ts +24 -0
  124. package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
  125. package/dist/modules/workflow/file-lock.service.js +30 -0
  126. package/dist/modules/workflow/file-lock.service.js.map +1 -1
  127. package/dist/modules/workflow/git/git.service.d.ts +69 -0
  128. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  129. package/dist/modules/workflow/git/git.service.js +153 -2
  130. package/dist/modules/workflow/git/git.service.js.map +1 -1
  131. package/dist/modules/workflow/pending-commits.service.d.ts +38 -0
  132. package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
  133. package/dist/modules/workflow/pending-commits.service.js +55 -0
  134. package/dist/modules/workflow/pending-commits.service.js.map +1 -1
  135. package/dist/modules/workflow/workflow-hooks.d.ts +54 -32
  136. package/dist/modules/workflow/workflow-hooks.d.ts.map +1 -1
  137. package/dist/modules/workflow/workflow-hooks.js +16 -1
  138. package/dist/modules/workflow/workflow-hooks.js.map +1 -1
  139. package/dist/modules/workflow/workflow.service.d.ts +60 -6
  140. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  141. package/dist/modules/workflow/workflow.service.js +111 -4
  142. package/dist/modules/workflow/workflow.service.js.map +1 -1
  143. package/dist/modules/workspace/agent-access.gate.d.ts +94 -0
  144. package/dist/modules/workspace/agent-access.gate.d.ts.map +1 -0
  145. package/dist/modules/workspace/agent-access.gate.js +123 -0
  146. package/dist/modules/workspace/agent-access.gate.js.map +1 -0
  147. package/dist/modules/workspace/routine-write-policy.d.ts +5 -6
  148. package/dist/modules/workspace/routine-write-policy.d.ts.map +1 -1
  149. package/dist/modules/workspace/routine-write-policy.js +5 -6
  150. package/dist/modules/workspace/routine-write-policy.js.map +1 -1
  151. package/dist/modules/workspace/session-sink.d.ts +5 -5
  152. package/dist/modules/workspace/set-aside-clone.d.ts +46 -0
  153. package/dist/modules/workspace/set-aside-clone.d.ts.map +1 -0
  154. package/dist/modules/workspace/set-aside-clone.js +92 -0
  155. package/dist/modules/workspace/set-aside-clone.js.map +1 -0
  156. package/dist/modules/workspace/startup/kb-startup-runner.d.ts +59 -9
  157. package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
  158. package/dist/modules/workspace/startup/kb-startup-runner.js +65 -24
  159. package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
  160. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  161. package/dist/modules/workspace/workspace.routes.js +92 -3
  162. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  163. package/dist/modules/workspace/workspace.service.d.ts +115 -5
  164. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  165. package/dist/modules/workspace/workspace.service.js +255 -25
  166. package/dist/modules/workspace/workspace.service.js.map +1 -1
  167. package/dist/modules/workspace/workspace.tools.d.ts +2 -2
  168. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  169. package/dist/modules/workspace/workspace.tools.js +309 -115
  170. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  171. package/dist/modules/write-access/write-access.d.ts +60 -0
  172. package/dist/modules/write-access/write-access.d.ts.map +1 -0
  173. package/dist/modules/write-access/write-access.js +129 -0
  174. package/dist/modules/write-access/write-access.js.map +1 -0
  175. package/dist/shared/domain-errors.d.ts +25 -0
  176. package/dist/shared/domain-errors.d.ts.map +1 -1
  177. package/dist/shared/domain-errors.js +28 -0
  178. package/dist/shared/domain-errors.js.map +1 -1
  179. package/dist/shared/git.contract.d.ts +20 -0
  180. package/dist/shared/git.contract.d.ts.map +1 -1
  181. package/dist/shared/git.contract.js +26 -0
  182. package/dist/shared/git.contract.js.map +1 -1
  183. package/dist/tenancy/static-tenant-source.d.ts +0 -1
  184. package/dist/tenancy/static-tenant-source.d.ts.map +1 -1
  185. package/dist/tenancy/static-tenant-source.js +0 -2
  186. package/dist/tenancy/static-tenant-source.js.map +1 -1
  187. package/migrations/0014_change_request_closed_reason.sql +1 -0
  188. package/migrations/0015_account_deactivation.sql +3 -0
  189. package/migrations/meta/0014_snapshot.json +2265 -0
  190. package/migrations/meta/0015_snapshot.json +2271 -0
  191. package/migrations/meta/_journal.json +14 -0
  192. package/package.json +3 -3
  193. package/src/__tests__/kb-layout-config.test.ts +0 -2
  194. package/src/__tests__/retired-settings.test.ts +96 -0
  195. package/src/core/__tests__/gated-boot.test.ts +132 -0
  196. package/src/core/__tests__/lifecycle.test.ts +170 -1
  197. package/src/core/__tests__/set-aside-root-is-one-place.test.ts +63 -0
  198. package/src/core/core-ports.ts +11 -1
  199. package/src/core/create-core-server.ts +32 -14
  200. package/src/core/create-core-services.ts +129 -23
  201. package/src/core/lifecycle.ts +98 -13
  202. package/src/core-config.ts +13 -14
  203. package/src/index.ts +12 -1
  204. package/src/modules/access/__tests__/access-control.preview-relocation.test.ts +385 -0
  205. package/src/modules/access/__tests__/access-control.prospective.test.ts +94 -18
  206. package/src/modules/access/__tests__/access.routes.prospective.test.ts +6 -7
  207. package/src/modules/access/access-control.interface.ts +43 -12
  208. package/src/modules/access/access-control.service.ts +234 -36
  209. package/src/modules/access/access.routes.ts +13 -15
  210. package/src/modules/agent-instructions/__tests__/agent-instructions.route.test.ts +3 -1
  211. package/src/modules/auth/__tests__/account-deactivation.test.ts +252 -0
  212. package/src/modules/auth/__tests__/account.routes.test.ts +81 -3
  213. package/src/modules/auth/__tests__/auth.middleware.test.ts +45 -22
  214. package/src/modules/auth/__tests__/auth.service.test.ts +1 -1
  215. package/src/modules/auth/account-admission.ts +78 -9
  216. package/src/modules/auth/account.routes.ts +63 -7
  217. package/src/modules/auth/auth.middleware.ts +19 -8
  218. package/src/modules/auth/auth.routes.ts +8 -0
  219. package/src/modules/auth/auth.service.ts +179 -6
  220. package/src/modules/auth/oidc-auth-provider.ts +7 -2
  221. package/src/modules/database/core-schema.ts +35 -27
  222. package/src/modules/kb-fs/__tests__/branch-name.test.ts +10 -0
  223. package/src/modules/kb-fs/__tests__/repo-path.test.ts +123 -0
  224. package/src/modules/kb-fs/branch-name.ts +14 -1
  225. package/src/modules/kb-fs/repo-path.ts +85 -0
  226. package/src/modules/kb-sync/__tests__/kb-sync.routes.test.ts +26 -1
  227. package/src/modules/kb-sync/kb-sync.routes.ts +14 -4
  228. package/src/modules/kb-sync/sync-auth.ts +2 -2
  229. package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +60 -3
  230. package/src/modules/mcp/__tests__/mcp.service.test.ts +68 -6
  231. package/src/modules/mcp/mcp-auth.middleware.ts +20 -6
  232. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +3 -1
  233. package/src/modules/plugins/__tests__/plugins.tools.test.ts +2 -1
  234. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +16 -0
  235. package/src/modules/settings/__tests__/setup.routes.git-mode.test.ts +91 -58
  236. package/src/modules/settings/__tests__/setup.routes.github-app.test.ts +25 -8
  237. package/src/modules/settings/__tests__/setup.routes.managed-phase.test.ts +14 -11
  238. package/src/modules/settings/__tests__/setup.routes.repository-change.test.ts +390 -0
  239. package/src/modules/settings/__tests__/setup.routes.test.ts +3 -0
  240. package/src/modules/settings/deployment-settings.service.ts +8 -3
  241. package/src/modules/settings/setup.routes.ts +263 -19
  242. package/src/modules/skills/__tests__/allowed-tools-warn.tools.test.ts +2 -1
  243. package/src/modules/skills/__tests__/branch-skills.tools.test.ts +218 -0
  244. package/src/modules/skills/__tests__/skills.service.test.ts +229 -5
  245. package/src/modules/skills/skills.contract.ts +91 -18
  246. package/src/modules/skills/skills.service.ts +278 -41
  247. package/src/modules/skills/skills.tools.ts +80 -8
  248. package/src/modules/tool-auth/__tests__/manual-auth.middleware.test.ts +14 -1
  249. package/src/modules/tool-auth/external-api-key.service.ts +4 -2
  250. package/src/modules/tool-auth/internal-token.service.ts +3 -3
  251. package/src/modules/tool-auth/tool-auth.middleware.ts +34 -11
  252. package/src/modules/tool-helpers/__tests__/agent-roles-write.test.ts +5 -3
  253. package/src/modules/tool-helpers/__tests__/phase4-tools.test.ts +3 -3
  254. package/src/modules/tool-helpers/__tests__/validate-token.test.ts +11 -1
  255. package/src/modules/tool-helpers/tool-handler.ts +24 -4
  256. package/src/modules/tool-helpers/tool.contract.ts +3 -2
  257. package/src/modules/tool-helpers/validate-token.ts +1 -1
  258. package/src/modules/workflow/__tests__/pending-commits.repository-replaced.test.ts +111 -0
  259. package/src/modules/workflow/__tests__/workflow.routes.history-read-gate.test.ts +25 -0
  260. package/src/modules/workflow/__tests__/workflow.service.repository-replaced.test.ts +249 -0
  261. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +264 -6
  262. package/src/modules/workflow/agent-tools/change-request-summary.ts +182 -0
  263. package/src/modules/workflow/agent-tools/workflow.tools.ts +86 -35
  264. package/src/modules/workflow/file-lock.service.ts +31 -0
  265. package/src/modules/workflow/git/__tests__/git.service.fileBytesAtCommit.test.ts +260 -0
  266. package/src/modules/workflow/git/git.service.ts +168 -0
  267. package/src/modules/workflow/pending-commits.service.ts +59 -1
  268. package/src/modules/workflow/workflow-hooks.ts +64 -26
  269. package/src/modules/workflow/workflow.service.ts +121 -4
  270. package/src/modules/workspace/__tests__/agent-access.gate.test.ts +208 -0
  271. package/src/modules/workspace/__tests__/escape-sequences.routes.test.ts +384 -0
  272. package/src/modules/workspace/__tests__/file-stat-access.test.ts +2 -1
  273. package/src/modules/workspace/__tests__/git-internals.security.test.ts +2 -1
  274. package/src/modules/workspace/__tests__/set-aside-clone.test.ts +52 -0
  275. package/src/modules/workspace/__tests__/workspace.routes.at-ref.test.ts +305 -0
  276. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +2 -0
  277. package/src/modules/workspace/__tests__/workspace.service.forget-clone-races.test.ts +147 -0
  278. package/src/modules/workspace/__tests__/workspace.service.replaced-repository.test.ts +326 -0
  279. package/src/modules/workspace/__tests__/workspace.service.test.ts +20 -9
  280. package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +2 -3
  281. package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +2 -3
  282. package/src/modules/workspace/__tests__/workspace.tools.test.ts +685 -14
  283. package/src/modules/workspace/agent-access.gate.ts +164 -0
  284. package/src/modules/workspace/routine-write-policy.ts +5 -6
  285. package/src/modules/workspace/session-sink.ts +5 -5
  286. package/src/modules/workspace/set-aside-clone.ts +96 -0
  287. package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +86 -0
  288. package/src/modules/workspace/startup/kb-startup-runner.ts +115 -34
  289. package/src/modules/workspace/workspace.routes.ts +95 -3
  290. package/src/modules/workspace/workspace.service.ts +256 -24
  291. package/src/modules/workspace/workspace.tools.ts +338 -119
  292. package/src/modules/write-access/__tests__/write-access.test.ts +248 -0
  293. package/src/modules/write-access/write-access.ts +153 -0
  294. package/src/shared/domain-errors.ts +31 -0
  295. package/src/shared/git.contract.ts +27 -0
  296. package/src/tenancy/__tests__/static-tenant-source.test.ts +0 -1
  297. package/src/tenancy/static-tenant-source.ts +0 -3
  298. package/dist/modules/workflow/session-ontology.policy.d.ts +0 -52
  299. package/dist/modules/workflow/session-ontology.policy.d.ts.map +0 -1
  300. package/dist/modules/workflow/session-ontology.policy.js +0 -62
  301. package/dist/modules/workflow/session-ontology.policy.js.map +0 -1
  302. package/dist/modules/workflow/session-ontology.service.d.ts +0 -105
  303. package/dist/modules/workflow/session-ontology.service.d.ts.map +0 -1
  304. package/dist/modules/workflow/session-ontology.service.js +0 -147
  305. package/dist/modules/workflow/session-ontology.service.js.map +0 -1
  306. package/dist/modules/workspace/session-ontology.gate.d.ts +0 -114
  307. package/dist/modules/workspace/session-ontology.gate.d.ts.map +0 -1
  308. package/dist/modules/workspace/session-ontology.gate.js +0 -161
  309. package/dist/modules/workspace/session-ontology.gate.js.map +0 -1
  310. package/dist/shared/kb-layout.d.ts +0 -39
  311. package/dist/shared/kb-layout.d.ts.map +0 -1
  312. package/dist/shared/kb-layout.js +0 -103
  313. package/dist/shared/kb-layout.js.map +0 -1
  314. package/dist/shared/kb-layout.test.d.ts +0 -2
  315. package/dist/shared/kb-layout.test.d.ts.map +0 -1
  316. package/dist/shared/kb-layout.test.js +0 -75
  317. package/dist/shared/kb-layout.test.js.map +0 -1
  318. package/src/modules/workflow/__tests__/session-ontology.policy.test.ts +0 -62
  319. package/src/modules/workflow/__tests__/session-ontology.service.test.ts +0 -201
  320. package/src/modules/workflow/session-ontology.policy.ts +0 -70
  321. package/src/modules/workflow/session-ontology.service.ts +0 -183
  322. package/src/modules/workspace/__tests__/session-ontology.gate.test.ts +0 -239
  323. package/src/modules/workspace/session-ontology.gate.ts +0 -191
  324. package/src/shared/kb-layout.test.ts +0 -98
  325. package/src/shared/kb-layout.ts +0 -102
@@ -0,0 +1,384 @@
1
+ import type { Server as HttpServer } from 'node:http';
2
+ import type { AddressInfo } from 'node:net';
3
+ import { mkdtemp, readFile, rm } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import express, { type RequestHandler } from 'express';
7
+ import { afterEach, describe, expect, it } from 'vitest';
8
+ import { LocalFilesystem } from '@mastra/core/workspace';
9
+ import { ToolRegistry } from '../../tool-registry/tool-registry.js';
10
+ import { createManualRoutes } from '../../tool-registry/manual.routes.js';
11
+ import { createToolHandlerFactory } from '../../tool-helpers/tool-handler.js';
12
+ import type { ToolContext } from '../../tool-helpers/tool.contract.js';
13
+ import type { ToolAuth } from '../../tool-auth/tool-auth.middleware.js';
14
+ import { registerWorkspaceTools } from '../workspace.tools.js';
15
+ import { RoutineWritePolicyService } from '../routine-write-policy.js';
16
+ import { WorkflowHooks } from '../../workflow/workflow-hooks.js';
17
+ import { ToolDescriptionNotes } from '../agent-access.gate.js';
18
+ import { SpillStore } from '../spill-store.js';
19
+ import { DocExtractService } from '../file-readers/doc-extract.service.js';
20
+ import type { IAccessControl } from '../../access/access-control.interface.js';
21
+ import { createMcpRoutes } from '../../mcp/mcp.routes.js';
22
+ import { McpService } from '../../mcp/mcp.service.js';
23
+ import { testKbContext } from '../../../__tests__/kb-context.js';
24
+
25
+ /**
26
+ * Who decodes escape sequences in written content a second time — settled on
27
+ * all three routes an agent can write through, with RAW request bodies and a
28
+ * BYTE-level read of what landed:
29
+ *
30
+ * 1. the tool route POST /api/agent/tools/<name>
31
+ * 2. the MCP endpoint POST /api/mcp (JSON-RPC `tools/call`)
32
+ * 3. `call_tool_chain` the same endpoint, content as a JavaScript
33
+ * string literal inside the chain's code
34
+ *
35
+ * Route 2 and 3 run the real `McpService` proxy over the real Streamable-HTTP
36
+ * transport, and dispatch back to route 1 over loopback through UTCP — so the
37
+ * three cases exercise one, two and three serialization hops respectively.
38
+ *
39
+ * Nothing in this file is written as an escape sequence. Every character of
40
+ * the payload is built from its code point, so neither an editor nor the tool
41
+ * that wrote this file can decode the thing under test and leave a broken
42
+ * assertion looking green. What goes on the wire is asserted too
43
+ * ({@link WIRE_ESC_A}), so a payload that was already decoded before it was
44
+ * sent cannot pass as a payload that survived the trip.
45
+ */
46
+
47
+ const BACKSLASH = String.fromCharCode(92);
48
+ const QUOTE = String.fromCharCode(34);
49
+ const KB_DIR = 'knowledge-base';
50
+ const BRANCH = 'main';
51
+
52
+ /** The six characters backslash, `u`, `0`, `0`, `4`, `1` — the ticket's ESC-A. */
53
+ const ESC_A = `${BACKSLASH}u0041`;
54
+ /** ESC-A as it must appear in the JSON *text* of a request: the backslash doubled. */
55
+ const WIRE_ESC_A = `${BACKSLASH}${BACKSLASH}u0041`;
56
+
57
+ /**
58
+ * One payload carrying an escape for a letter (ESC-A), a quote, a backslash
59
+ * and a line break — each meant to stay TEXT — plus a real em dash, which is
60
+ * meant to stay the character it is. Nine distinct characters of interest and
61
+ * no accident: if any hop decodes once more than it should, this string is
62
+ * stored shorter than it was sent.
63
+ */
64
+ const PAYLOAD = [
65
+ ESC_A,
66
+ `${BACKSLASH}${QUOTE}`,
67
+ `${BACKSLASH}${BACKSLASH}`,
68
+ `${BACKSLASH}n`,
69
+ String.fromCharCode(0x2014),
70
+ ].join('|');
71
+
72
+ /** What `PAYLOAD` must be on disk, as bytes. */
73
+ const EXPECTED = Buffer.from(PAYLOAD, 'utf8');
74
+
75
+ const BEARER = 'bevel_key_user_a';
76
+
77
+ /** Sets both surfaces' auth stamps: `toolAuth` for the tool routes, `userId` for MCP. */
78
+ const fakeAuth: RequestHandler = (req, _res, next) => {
79
+ req.toolAuth = { source: 'internal', userId: 'u', scope: 'write' };
80
+ req.userId = 'u';
81
+ req.externalApiKeyId = 'tok-a';
82
+ next();
83
+ };
84
+ const passthrough: RequestHandler = (_req, _res, next) => next();
85
+
86
+ interface Platform {
87
+ baseUrl: string;
88
+ /** The bytes of a stored workspace file, read straight off disk. */
89
+ bytesOf(path: string): Promise<Buffer>;
90
+ /** Tool name → the description an agent reads, off the served catalog. */
91
+ descriptions(): Promise<Record<string, string>>;
92
+ stop(): Promise<void>;
93
+ }
94
+
95
+ const running: Platform[] = [];
96
+
97
+ async function startPlatform(): Promise<Platform> {
98
+ const tempDir = await mkdtemp(join(tmpdir(), 'escape-seq-'));
99
+ const fs = new LocalFilesystem({ basePath: tempDir, contained: true });
100
+ // `write_files` lands its batch through the locking filesystem's `writeFiles`
101
+ // (re-judge every path under the lock, then write what is kept). This plain
102
+ // filesystem has none, so the same contract is honoured by hand.
103
+ Object.assign(fs, {
104
+ writeFiles: async (
105
+ writes: { path: string; content: string }[],
106
+ _summary: string,
107
+ _deletes: string[],
108
+ check: (pending: readonly { path: string; content: string }[]) => Promise<{ path: string; content: string }[]>,
109
+ ) => {
110
+ for (const w of await check(writes)) await fs.writeFile(w.path, w.content);
111
+ },
112
+ });
113
+
114
+ const registry = new ToolRegistry();
115
+ const resolve = async (auth: ToolAuth, signal: AbortSignal, sessionId?: string): Promise<ToolContext> => ({
116
+ user: { id: 'u', email: 'e@x', name: 'N' },
117
+ scope: auth.scope,
118
+ source: auth.source,
119
+ sessionId,
120
+ abortSignal: signal,
121
+ workspaceService: {} as never,
122
+ workflowService: {} as never,
123
+ events: {} as never,
124
+ getFilesystem: async () => fs,
125
+ });
126
+ const allowAll = { canRead: async () => true } as unknown as IAccessControl;
127
+
128
+ const app = express();
129
+ app.use(express.json());
130
+ const router = express.Router();
131
+ registerWorkspaceTools(
132
+ registry,
133
+ router,
134
+ fakeAuth,
135
+ createToolHandlerFactory(resolve),
136
+ new SpillStore(join(tempDir, 'spills')),
137
+ new DocExtractService(join(tempDir, 'doc-extract')),
138
+ allowAll,
139
+ testKbContext({ kbDirName: KB_DIR }),
140
+ { recoveryBotEmail: 'recovery-bot@bevel.local', hooks: new WorkflowHooks(), notes: new ToolDescriptionNotes() },
141
+ new RoutineWritePolicyService(),
142
+ {} as never,
143
+ );
144
+ app.use('/api', router);
145
+
146
+ const httpServer = await new Promise<HttpServer>((res) => {
147
+ const s = app.listen(0, '127.0.0.1', () => res(s));
148
+ });
149
+ const { port } = httpServer.address() as AddressInfo;
150
+ const baseUrl = `http://127.0.0.1:${port}`;
151
+
152
+ // The catalog the MCP proxy discovers: this process's own tool manual.
153
+ app.get('/api/agent/all-tools', (_req, res) => {
154
+ res.json({
155
+ manuals: [
156
+ {
157
+ name: 'KNOWLEDGE_BASE',
158
+ call_template_type: 'http',
159
+ http_method: 'GET',
160
+ url: '${API_URL}/api/agent/utcp',
161
+ content_type: 'application/json',
162
+ headers: { Authorization: 'Bearer ${CONNECTION_KEY}' },
163
+ },
164
+ ],
165
+ });
166
+ });
167
+ const service = new McpService({
168
+ loopbackBaseUrl: baseUrl,
169
+ manualName: 'KNOWLEDGE_BASE',
170
+ spillStore: new SpillStore(join(tempDir, 'spills')),
171
+ publicFrontendUrl: 'http://localhost:5173',
172
+ });
173
+ const stub = {} as never;
174
+ app.use('/api', createMcpRoutes(service, stub, fakeAuth, fakeAuth, stub, stub, stub, ''));
175
+ app.use('/api', createManualRoutes(registry, passthrough));
176
+
177
+ let stopped = false;
178
+ const platform: Platform = {
179
+ baseUrl,
180
+ bytesOf: (path) => readFile(join(tempDir, path)),
181
+ async descriptions() {
182
+ const res = await fetch(`${baseUrl}/api/agent/utcp`, { headers: { Authorization: `Bearer ${BEARER}` } });
183
+ const manual = (await res.json()) as { tools: Array<{ name: string; description: string }> };
184
+ return Object.fromEntries(manual.tools.map((t) => [t.name, t.description]));
185
+ },
186
+ async stop() {
187
+ if (stopped) return;
188
+ stopped = true;
189
+ service.onSecretsChanged(null);
190
+ httpServer.closeAllConnections();
191
+ await new Promise<void>((r) => httpServer.close(() => r()));
192
+ await rm(tempDir, { recursive: true, force: true });
193
+ },
194
+ };
195
+ running.push(platform);
196
+ return platform;
197
+ }
198
+
199
+ /** POST a raw body string — no object is handed to `fetch`, only text. */
200
+ async function postRaw(url: string, body: string, accept = 'application/json'): Promise<string> {
201
+ const res = await fetch(url, {
202
+ method: 'POST',
203
+ headers: {
204
+ 'Content-Type': 'application/json',
205
+ Accept: `${accept}, text/event-stream`,
206
+ Authorization: `Bearer ${BEARER}`,
207
+ },
208
+ body,
209
+ });
210
+ return res.text();
211
+ }
212
+
213
+ /** The JSON-RPC messages out of a `/api/mcp` answer, JSON or SSE alike. */
214
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- raw JSON-RPC, read by shape
215
+ function messagesOf(text: string): Array<{ result?: any; error?: any }> {
216
+ if (!text.trim()) return [];
217
+ if (!text.startsWith('event:') && !text.startsWith('data:')) return [JSON.parse(text)];
218
+ return text
219
+ .split('\n')
220
+ .filter((line) => line.startsWith('data:'))
221
+ .map((line) => JSON.parse(line.slice('data:'.length)));
222
+ }
223
+
224
+ /** `tools/call` over the MCP endpoint, from raw JSON text. */
225
+ async function callTool(baseUrl: string, name: string, args: unknown): Promise<string> {
226
+ const body = JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name, arguments: args } });
227
+ // What goes on the wire is the six characters, escaped once for JSON.
228
+ expect(body).toContain(WIRE_ESC_A);
229
+ const out = messagesOf(await postRaw(`${baseUrl}/api/mcp`, body));
230
+ const result = out[out.length - 1]?.result;
231
+ expect(out[out.length - 1]?.error, `tools/call ${name} failed`).toBeUndefined();
232
+ expect(result?.isError, JSON.stringify(result)).toBeFalsy();
233
+ return (result.content as Array<{ text: string }>)[0].text;
234
+ }
235
+
236
+ afterEach(async () => {
237
+ for (const p of running.splice(0)) await p.stop().catch(() => {});
238
+ });
239
+
240
+ describe('escape sequences survive a write — the tool route', () => {
241
+ it('stores `write_file` content byte for byte', async () => {
242
+ const { baseUrl, bytesOf } = await startPlatform();
243
+ const path = `${KB_DIR}/esc/route-rest.md`;
244
+ const body = JSON.stringify({ branch: BRANCH, path, content: PAYLOAD });
245
+ expect(body).toContain(WIRE_ESC_A);
246
+
247
+ const answer = JSON.parse(await postRaw(`${baseUrl}/api/agent/tools/write_file`, body));
248
+ expect(answer.outcome).toBe('created');
249
+ expect(await bytesOf(path)).toEqual(EXPECTED);
250
+ });
251
+
252
+ it('stores `write_files` content byte for byte', async () => {
253
+ const { baseUrl, bytesOf } = await startPlatform();
254
+ const path = `${KB_DIR}/esc/route-rest-batch.md`;
255
+ const body = JSON.stringify({ branch: BRANCH, files: [{ path, content: PAYLOAD }] });
256
+ expect(body).toContain(WIRE_ESC_A);
257
+
258
+ const answer = JSON.parse(await postRaw(`${baseUrl}/api/agent/tools/write_files`, body));
259
+ expect(answer.count).toBe(1);
260
+ expect(await bytesOf(path)).toEqual(EXPECTED);
261
+ });
262
+
263
+ it('stores an `edit_file` `new_string` byte for byte', async () => {
264
+ const { baseUrl, bytesOf } = await startPlatform();
265
+ const path = `${KB_DIR}/esc/route-rest-edit.md`;
266
+ await postRaw(
267
+ `${baseUrl}/api/agent/tools/write_file`,
268
+ JSON.stringify({ branch: BRANCH, path, content: 'PLACEHOLDER' }),
269
+ );
270
+ const body = JSON.stringify({ branch: BRANCH, path, old_string: 'PLACEHOLDER', new_string: PAYLOAD });
271
+ expect(body).toContain(WIRE_ESC_A);
272
+
273
+ const answer = JSON.parse(await postRaw(`${baseUrl}/api/agent/tools/edit_file`, body));
274
+ expect(answer.replaced).toBe(1);
275
+ expect(await bytesOf(path)).toEqual(EXPECTED);
276
+ });
277
+ });
278
+
279
+ describe('escape sequences survive a write — the MCP endpoint', () => {
280
+ it('stores `write_file` content byte for byte', async () => {
281
+ const { baseUrl, bytesOf } = await startPlatform();
282
+ const path = `${KB_DIR}/esc/route-mcp.md`;
283
+ await callTool(baseUrl, 'write_file', { body: { branch: BRANCH, path, content: PAYLOAD } });
284
+ expect(await bytesOf(path)).toEqual(EXPECTED);
285
+ });
286
+
287
+ it('stores `write_files` content byte for byte', async () => {
288
+ const { baseUrl, bytesOf } = await startPlatform();
289
+ const path = `${KB_DIR}/esc/route-mcp-batch.md`;
290
+ await callTool(baseUrl, 'write_files', { body: { branch: BRANCH, files: [{ path, content: PAYLOAD }] } });
291
+ expect(await bytesOf(path)).toEqual(EXPECTED);
292
+ });
293
+
294
+ it('stores an `edit_file` `new_string` byte for byte', async () => {
295
+ const { baseUrl, bytesOf } = await startPlatform();
296
+ const path = `${KB_DIR}/esc/route-mcp-edit.md`;
297
+ await postRaw(
298
+ `${baseUrl}/api/agent/tools/write_file`,
299
+ JSON.stringify({ branch: BRANCH, path, content: 'PLACEHOLDER' }),
300
+ );
301
+ await callTool(baseUrl, 'edit_file', {
302
+ body: { branch: BRANCH, path, old_string: 'PLACEHOLDER', new_string: PAYLOAD },
303
+ });
304
+ expect(await bytesOf(path)).toEqual(EXPECTED);
305
+ });
306
+ });
307
+
308
+ describe('escape sequences survive a write — call_tool_chain', () => {
309
+ /**
310
+ * The chain's code with `PAYLOAD` as a JavaScript string LITERAL — the shape
311
+ * the ticket describes, and the one that crosses the isolate boundary twice
312
+ * (source in, arguments out). `JSON.stringify` of the payload is a valid JS
313
+ * literal for it, so the literal is produced rather than hand-escaped.
314
+ */
315
+ const chainCode = (expr: string) => `return KNOWLEDGE_BASE.${expr};`;
316
+
317
+ it('stores `write_file` content byte for byte', async () => {
318
+ const { baseUrl, bytesOf } = await startPlatform();
319
+ const path = `${KB_DIR}/esc/route-chain.md`;
320
+ const code = chainCode(
321
+ `write_file({ body: { branch: ${JSON.stringify(BRANCH)}, path: ${JSON.stringify(path)}, content: ${JSON.stringify(PAYLOAD)} } })`,
322
+ );
323
+ // The literal inside the chain's own source is the six characters, written
324
+ // as a JS escape — two backslashes, exactly as on the JSON wire.
325
+ expect(code).toContain(WIRE_ESC_A);
326
+
327
+ const text = await callTool(baseUrl, 'call_tool_chain', { code });
328
+ expect(JSON.parse(text).success, text).toBe(true);
329
+ expect(await bytesOf(path)).toEqual(EXPECTED);
330
+ });
331
+
332
+ it('stores `write_files` content byte for byte', async () => {
333
+ const { baseUrl, bytesOf } = await startPlatform();
334
+ const path = `${KB_DIR}/esc/route-chain-batch.md`;
335
+ const code = chainCode(
336
+ `write_files({ body: { branch: ${JSON.stringify(BRANCH)}, files: [{ path: ${JSON.stringify(path)}, content: ${JSON.stringify(PAYLOAD)} }] } })`,
337
+ );
338
+ expect(code).toContain(WIRE_ESC_A);
339
+
340
+ const text = await callTool(baseUrl, 'call_tool_chain', { code });
341
+ expect(JSON.parse(text).success, text).toBe(true);
342
+ expect(await bytesOf(path)).toEqual(EXPECTED);
343
+ });
344
+
345
+ it('stores an `edit_file` `new_string` byte for byte', async () => {
346
+ const { baseUrl, bytesOf } = await startPlatform();
347
+ const path = `${KB_DIR}/esc/route-chain-edit.md`;
348
+ await postRaw(
349
+ `${baseUrl}/api/agent/tools/write_file`,
350
+ JSON.stringify({ branch: BRANCH, path, content: 'PLACEHOLDER' }),
351
+ );
352
+ const code = chainCode(
353
+ `edit_file({ body: { branch: ${JSON.stringify(BRANCH)}, path: ${JSON.stringify(path)}, old_string: ${JSON.stringify('PLACEHOLDER')}, new_string: ${JSON.stringify(PAYLOAD)} } })`,
354
+ );
355
+ expect(code).toContain(WIRE_ESC_A);
356
+
357
+ const text = await callTool(baseUrl, 'call_tool_chain', { code });
358
+ expect(JSON.parse(text).success, text).toBe(true);
359
+ expect(await bytesOf(path)).toEqual(EXPECTED);
360
+ });
361
+ });
362
+
363
+ describe('what the write tools tell an agent about escape sequences', () => {
364
+ it('warns on write_file, write_files and edit_file, and names the upload route', async () => {
365
+ const { descriptions } = await startPlatform();
366
+ const served = await descriptions();
367
+
368
+ for (const name of ['write_file', 'write_files', 'edit_file']) {
369
+ const d = served[name];
370
+ expect(d, name).toBeDefined();
371
+ // Some clients decode before sending …
372
+ expect(d, name).toContain('some clients decode them in arguments before sending');
373
+ // … and the route that lands such content unchanged.
374
+ expect(d, name).toContain('request_upload_token');
375
+ expect(d, name).toContain('lands it unchanged');
376
+ }
377
+ // Said on the three tools that take content as a JSON string, and nowhere else.
378
+ const carrying = Object.entries(served)
379
+ .filter(([, d]) => d.includes('some clients decode them in arguments before sending'))
380
+ .map(([n]) => n)
381
+ .sort();
382
+ expect(carrying).toEqual(['edit_file', 'write_file', 'write_files']);
383
+ });
384
+ });
@@ -13,6 +13,7 @@ import type { ToolAuth } from '../../tool-auth/tool-auth.middleware.js';
13
13
  import { registerWorkspaceTools } from '../workspace.tools.js';
14
14
  import { RoutineWritePolicyService } from '../routine-write-policy.js';
15
15
  import { WorkflowHooks } from '../../workflow/workflow-hooks.js';
16
+ import { ToolDescriptionNotes } from '../../workspace/agent-access.gate.js';
16
17
  import { SpillStore } from '../spill-store.js';
17
18
  import { DocExtractService } from '../file-readers/doc-extract.service.js';
18
19
  import { AccessControlService } from '../../access/access-control.service.js';
@@ -88,7 +89,7 @@ beforeAll(async () => {
88
89
  new DocExtractService(docCache),
89
90
  service,
90
91
  testKbContext({ kbDirName: KB }),
91
- { service: {} as never, enabled: false, kb: testKbContext({ kbDirName: KB }), recoveryBotEmail: 'recovery-bot@bevel.local', hooks: new WorkflowHooks() },
92
+ { recoveryBotEmail: 'recovery-bot@bevel.local', hooks: new WorkflowHooks(), notes: new ToolDescriptionNotes() },
92
93
  new RoutineWritePolicyService(),
93
94
  {} as never,
94
95
  );
@@ -22,6 +22,7 @@ import type { ICreatorAccess } from '../../access-model/creator.js';
22
22
  import type { IAdminAccessService } from '../../admin/admin.interface.js';
23
23
  import type { WorkflowEventBus } from '../../workflow/event-bus.js';
24
24
  import { WorkflowHooks } from '../../workflow/workflow-hooks.js';
25
+ import { ToolDescriptionNotes } from '../../workspace/agent-access.gate.js';
25
26
  import type { AuthService } from '../../auth/auth.service.js';
26
27
  import { registerWorkspaceTools } from '../workspace.tools.js';
27
28
  import { RoutineWritePolicyService } from '../routine-write-policy.js';
@@ -277,7 +278,7 @@ describe('workspace tools refuse the git folder', () => {
277
278
  new DocExtractService(join(root, 'doc-cache')),
278
279
  allowAll,
279
280
  testKbContext({ kbDirName: KB }),
280
- { service: {} as never, enabled: false, kb: testKbContext({ kbDirName: KB }), recoveryBotEmail: 'recovery-bot@bevel.local', hooks: new WorkflowHooks() },
281
+ { recoveryBotEmail: 'recovery-bot@bevel.local', hooks: new WorkflowHooks(), notes: new ToolDescriptionNotes() },
281
282
  new RoutineWritePolicyService(),
282
283
  {} as never,
283
284
  );
@@ -0,0 +1,52 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2
+ import fs from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { setAsideClone } from '../set-aside-clone.js';
6
+
7
+ /**
8
+ * Setting a working copy aside is the one thing between a replaced repository
9
+ * and the loss of whatever was only ever committed in that copy. So the
10
+ * destination is never somebody else's: a folder already there holds an
11
+ * EARLIER set-aside, and this one must neither land in it nor clean it up.
12
+ */
13
+
14
+ let root: string;
15
+
16
+ beforeEach(async () => {
17
+ root = await fs.mkdtemp(path.join(os.tmpdir(), 'set-aside-clone-'));
18
+ });
19
+
20
+ afterEach(async () => {
21
+ await fs.rm(root, { recursive: true, force: true });
22
+ });
23
+
24
+ async function folderWith(name: string, file: string, content: string): Promise<string> {
25
+ const dir = path.join(root, name);
26
+ await fs.mkdir(dir, { recursive: true });
27
+ await fs.writeFile(path.join(dir, file), content, 'utf8');
28
+ return dir;
29
+ }
30
+
31
+ describe('setAsideClone', () => {
32
+ it('moves the working copy, whole, to a destination that is not there yet', async () => {
33
+ const copy = await folderWith('workspaces/main/knowledge-base', 'unpushed.md', 'local work');
34
+ const dest = path.join(root, 'replaced-working-copies', 'stamp', 'main');
35
+
36
+ await setAsideClone(copy, dest);
37
+
38
+ expect(await fs.readFile(path.join(dest, 'unpushed.md'), 'utf8')).toBe('local work');
39
+ expect(await fs.access(copy).then(() => true, () => false)).toBe(false);
40
+ });
41
+
42
+ it('refuses a destination that already holds a set-aside copy, and touches neither', async () => {
43
+ const copy = await folderWith('workspaces/main/knowledge-base', 'unpushed.md', 'this copy');
44
+ const dest = await folderWith('replaced-working-copies/stamp/main', 'unpushed.md', 'an earlier copy');
45
+
46
+ await expect(setAsideClone(copy, dest)).rejects.toThrow(/already holds one/);
47
+
48
+ // The earlier one is whole, and so is the one that was to be moved.
49
+ expect(await fs.readFile(path.join(dest, 'unpushed.md'), 'utf8')).toBe('an earlier copy');
50
+ expect(await fs.readFile(path.join(copy, 'unpushed.md'), 'utf8')).toBe('this copy');
51
+ });
52
+ });