@bevel-software/platform-core-backend 0.21.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 (434) 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 +62 -13
  7. package/dist/core/create-core-server.js.map +1 -1
  8. package/dist/core/create-core-services.d.ts +22 -3
  9. package/dist/core/create-core-services.d.ts.map +1 -1
  10. package/dist/core/create-core-services.js +158 -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-requests.contract.d.ts +75 -0
  31. package/dist/modules/access/access-requests.contract.d.ts.map +1 -0
  32. package/dist/modules/access/access-requests.contract.js +20 -0
  33. package/dist/modules/access/access-requests.contract.js.map +1 -0
  34. package/dist/modules/access/access-requests.routes.d.ts +35 -0
  35. package/dist/modules/access/access-requests.routes.d.ts.map +1 -0
  36. package/dist/modules/access/access-requests.routes.js +237 -0
  37. package/dist/modules/access/access-requests.routes.js.map +1 -0
  38. package/dist/modules/access/access-requests.service.d.ts +123 -0
  39. package/dist/modules/access/access-requests.service.d.ts.map +1 -0
  40. package/dist/modules/access/access-requests.service.js +337 -0
  41. package/dist/modules/access/access-requests.service.js.map +1 -0
  42. package/dist/modules/access/access.routes.d.ts.map +1 -1
  43. package/dist/modules/access/access.routes.js +13 -15
  44. package/dist/modules/access/access.routes.js.map +1 -1
  45. package/dist/modules/access-model/access-grammar.d.ts +12 -0
  46. package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
  47. package/dist/modules/access-model/access-grammar.js +24 -0
  48. package/dist/modules/access-model/access-grammar.js.map +1 -1
  49. package/dist/modules/auth/account-admission.d.ts +58 -7
  50. package/dist/modules/auth/account-admission.d.ts.map +1 -1
  51. package/dist/modules/auth/account-admission.js +44 -1
  52. package/dist/modules/auth/account-admission.js.map +1 -1
  53. package/dist/modules/auth/account.routes.d.ts +1 -1
  54. package/dist/modules/auth/account.routes.d.ts.map +1 -1
  55. package/dist/modules/auth/account.routes.js +61 -5
  56. package/dist/modules/auth/account.routes.js.map +1 -1
  57. package/dist/modules/auth/auth.middleware.d.ts +1 -1
  58. package/dist/modules/auth/auth.middleware.d.ts.map +1 -1
  59. package/dist/modules/auth/auth.middleware.js +18 -7
  60. package/dist/modules/auth/auth.middleware.js.map +1 -1
  61. package/dist/modules/auth/auth.routes.d.ts.map +1 -1
  62. package/dist/modules/auth/auth.routes.js +8 -0
  63. package/dist/modules/auth/auth.routes.js.map +1 -1
  64. package/dist/modules/auth/auth.service.d.ts +96 -3
  65. package/dist/modules/auth/auth.service.d.ts.map +1 -1
  66. package/dist/modules/auth/auth.service.js +187 -9
  67. package/dist/modules/auth/auth.service.js.map +1 -1
  68. package/dist/modules/auth/oidc-auth-provider.d.ts.map +1 -1
  69. package/dist/modules/auth/oidc-auth-provider.js +7 -2
  70. package/dist/modules/auth/oidc-auth-provider.js.map +1 -1
  71. package/dist/modules/database/core-schema.d.ts +41 -24
  72. package/dist/modules/database/core-schema.d.ts.map +1 -1
  73. package/dist/modules/database/core-schema.js +35 -27
  74. package/dist/modules/database/core-schema.js.map +1 -1
  75. package/dist/modules/github-app/github-app.client.d.ts +101 -0
  76. package/dist/modules/github-app/github-app.client.d.ts.map +1 -0
  77. package/dist/modules/github-app/github-app.client.js +222 -0
  78. package/dist/modules/github-app/github-app.client.js.map +1 -0
  79. package/dist/modules/github-app/github-app.connection.d.ts +102 -0
  80. package/dist/modules/github-app/github-app.connection.d.ts.map +1 -0
  81. package/dist/modules/github-app/github-app.connection.js +185 -0
  82. package/dist/modules/github-app/github-app.connection.js.map +1 -0
  83. package/dist/modules/github-app/github-app.routes.d.ts +59 -0
  84. package/dist/modules/github-app/github-app.routes.d.ts.map +1 -0
  85. package/dist/modules/github-app/github-app.routes.js +323 -0
  86. package/dist/modules/github-app/github-app.routes.js.map +1 -0
  87. package/dist/modules/github-app/index.d.ts +4 -0
  88. package/dist/modules/github-app/index.d.ts.map +1 -0
  89. package/dist/modules/github-app/index.js +4 -0
  90. package/dist/modules/github-app/index.js.map +1 -0
  91. package/dist/modules/kb-fs/branch-name.d.ts.map +1 -1
  92. package/dist/modules/kb-fs/branch-name.js +12 -2
  93. package/dist/modules/kb-fs/branch-name.js.map +1 -1
  94. package/dist/modules/kb-fs/remote-url.d.ts +22 -0
  95. package/dist/modules/kb-fs/remote-url.d.ts.map +1 -0
  96. package/dist/modules/kb-fs/remote-url.js +35 -0
  97. package/dist/modules/kb-fs/remote-url.js.map +1 -0
  98. package/dist/modules/kb-fs/repo-path.d.ts +11 -16
  99. package/dist/modules/kb-fs/repo-path.d.ts.map +1 -1
  100. package/dist/modules/kb-fs/repo-path.js +79 -0
  101. package/dist/modules/kb-fs/repo-path.js.map +1 -1
  102. package/dist/modules/kb-sync/kb-sync.routes.d.ts +2 -2
  103. package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -1
  104. package/dist/modules/kb-sync/kb-sync.routes.js +14 -3
  105. package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -1
  106. package/dist/modules/kb-sync/sync-auth.d.ts +3 -1
  107. package/dist/modules/kb-sync/sync-auth.d.ts.map +1 -1
  108. package/dist/modules/kb-sync/sync-auth.js +1 -1
  109. package/dist/modules/kb-sync/sync-auth.js.map +1 -1
  110. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
  111. package/dist/modules/mcp/mcp-auth.middleware.js +21 -6
  112. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
  113. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  114. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +3 -2
  115. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  116. package/dist/modules/plugins/join-proposals.d.ts +17 -5
  117. package/dist/modules/plugins/join-proposals.d.ts.map +1 -1
  118. package/dist/modules/plugins/join-proposals.js +76 -22
  119. package/dist/modules/plugins/join-proposals.js.map +1 -1
  120. package/dist/modules/plugins/join-requests.service.d.ts +111 -18
  121. package/dist/modules/plugins/join-requests.service.d.ts.map +1 -1
  122. package/dist/modules/plugins/join-requests.service.js +173 -29
  123. package/dist/modules/plugins/join-requests.service.js.map +1 -1
  124. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
  125. package/dist/modules/plugins/plugins.routes.js +3 -2
  126. package/dist/modules/plugins/plugins.routes.js.map +1 -1
  127. package/dist/modules/settings/deployment-settings.service.d.ts +22 -1
  128. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  129. package/dist/modules/settings/deployment-settings.service.js +101 -7
  130. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  131. package/dist/modules/settings/managed-repository.d.ts +43 -0
  132. package/dist/modules/settings/managed-repository.d.ts.map +1 -0
  133. package/dist/modules/settings/managed-repository.js +60 -0
  134. package/dist/modules/settings/managed-repository.js.map +1 -0
  135. package/dist/modules/settings/repository-source.d.ts +128 -0
  136. package/dist/modules/settings/repository-source.d.ts.map +1 -0
  137. package/dist/modules/settings/repository-source.js +150 -0
  138. package/dist/modules/settings/repository-source.js.map +1 -0
  139. package/dist/modules/settings/setup.routes.d.ts +78 -4
  140. package/dist/modules/settings/setup.routes.d.ts.map +1 -1
  141. package/dist/modules/settings/setup.routes.js +379 -17
  142. package/dist/modules/settings/setup.routes.js.map +1 -1
  143. package/dist/modules/skills/skill-access-requests.routes.d.ts +6 -8
  144. package/dist/modules/skills/skill-access-requests.routes.d.ts.map +1 -1
  145. package/dist/modules/skills/skill-access-requests.routes.js +24 -63
  146. package/dist/modules/skills/skill-access-requests.routes.js.map +1 -1
  147. package/dist/modules/skills/skills.contract.d.ts +90 -18
  148. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  149. package/dist/modules/skills/skills.contract.js +4 -1
  150. package/dist/modules/skills/skills.contract.js.map +1 -1
  151. package/dist/modules/skills/skills.service.d.ts +98 -9
  152. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  153. package/dist/modules/skills/skills.service.js +239 -37
  154. package/dist/modules/skills/skills.service.js.map +1 -1
  155. package/dist/modules/skills/skills.tools.d.ts +7 -0
  156. package/dist/modules/skills/skills.tools.d.ts.map +1 -1
  157. package/dist/modules/skills/skills.tools.js +71 -7
  158. package/dist/modules/skills/skills.tools.js.map +1 -1
  159. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  160. package/dist/modules/tool-auth/external-api-key.service.js +4 -2
  161. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  162. package/dist/modules/tool-auth/internal-token.service.d.ts +3 -3
  163. package/dist/modules/tool-auth/tool-auth.middleware.d.ts +16 -7
  164. package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
  165. package/dist/modules/tool-auth/tool-auth.middleware.js +34 -12
  166. package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
  167. package/dist/modules/tool-helpers/tool-context.d.ts.map +1 -1
  168. package/dist/modules/tool-helpers/tool-context.js +15 -0
  169. package/dist/modules/tool-helpers/tool-context.js.map +1 -1
  170. package/dist/modules/tool-helpers/tool-handler.d.ts +2 -1
  171. package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
  172. package/dist/modules/tool-helpers/tool-handler.js +25 -4
  173. package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
  174. package/dist/modules/tool-helpers/tool.contract.d.ts +3 -2
  175. package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
  176. package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
  177. package/dist/modules/tool-helpers/validate-token.js +1 -1
  178. package/dist/modules/tool-helpers/validate-token.js.map +1 -1
  179. package/dist/modules/workflow/agent-tools/change-request-summary.d.ts +98 -0
  180. package/dist/modules/workflow/agent-tools/change-request-summary.d.ts.map +1 -0
  181. package/dist/modules/workflow/agent-tools/change-request-summary.js +81 -0
  182. package/dist/modules/workflow/agent-tools/change-request-summary.js.map +1 -0
  183. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  184. package/dist/modules/workflow/agent-tools/workflow.tools.js +113 -37
  185. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  186. package/dist/modules/workflow/file-lock.service.d.ts +24 -0
  187. package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
  188. package/dist/modules/workflow/file-lock.service.js +30 -0
  189. package/dist/modules/workflow/file-lock.service.js.map +1 -1
  190. package/dist/modules/workflow/git/git.service.d.ts +72 -1
  191. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  192. package/dist/modules/workflow/git/git.service.js +170 -6
  193. package/dist/modules/workflow/git/git.service.js.map +1 -1
  194. package/dist/modules/workflow/git/node-git-runner.d.ts.map +1 -1
  195. package/dist/modules/workflow/git/node-git-runner.js +5 -0
  196. package/dist/modules/workflow/git/node-git-runner.js.map +1 -1
  197. package/dist/modules/workflow/git/pull-request.service.d.ts +45 -0
  198. package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
  199. package/dist/modules/workflow/git/pull-request.service.js +99 -0
  200. package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
  201. package/dist/modules/workflow/pending-commits.service.d.ts +38 -0
  202. package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
  203. package/dist/modules/workflow/pending-commits.service.js +55 -0
  204. package/dist/modules/workflow/pending-commits.service.js.map +1 -1
  205. package/dist/modules/workflow/workflow-hooks.d.ts +54 -32
  206. package/dist/modules/workflow/workflow-hooks.d.ts.map +1 -1
  207. package/dist/modules/workflow/workflow-hooks.js +16 -1
  208. package/dist/modules/workflow/workflow-hooks.js.map +1 -1
  209. package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
  210. package/dist/modules/workflow/workflow.routes.js +3 -1
  211. package/dist/modules/workflow/workflow.routes.js.map +1 -1
  212. package/dist/modules/workflow/workflow.service.d.ts +94 -13
  213. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  214. package/dist/modules/workflow/workflow.service.js +258 -51
  215. package/dist/modules/workflow/workflow.service.js.map +1 -1
  216. package/dist/modules/workspace/agent-access.gate.d.ts +94 -0
  217. package/dist/modules/workspace/agent-access.gate.d.ts.map +1 -0
  218. package/dist/modules/workspace/agent-access.gate.js +123 -0
  219. package/dist/modules/workspace/agent-access.gate.js.map +1 -0
  220. package/dist/modules/workspace/routine-write-policy.d.ts +5 -6
  221. package/dist/modules/workspace/routine-write-policy.d.ts.map +1 -1
  222. package/dist/modules/workspace/routine-write-policy.js +5 -6
  223. package/dist/modules/workspace/routine-write-policy.js.map +1 -1
  224. package/dist/modules/workspace/session-sink.d.ts +5 -5
  225. package/dist/modules/workspace/set-aside-clone.d.ts +46 -0
  226. package/dist/modules/workspace/set-aside-clone.d.ts.map +1 -0
  227. package/dist/modules/workspace/set-aside-clone.js +92 -0
  228. package/dist/modules/workspace/set-aside-clone.js.map +1 -0
  229. package/dist/modules/workspace/startup/kb-startup-runner.d.ts +117 -1
  230. package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
  231. package/dist/modules/workspace/startup/kb-startup-runner.js +190 -1
  232. package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
  233. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  234. package/dist/modules/workspace/workspace.routes.js +92 -3
  235. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  236. package/dist/modules/workspace/workspace.service.d.ts +115 -5
  237. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  238. package/dist/modules/workspace/workspace.service.js +272 -26
  239. package/dist/modules/workspace/workspace.service.js.map +1 -1
  240. package/dist/modules/workspace/workspace.tools.d.ts +2 -2
  241. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  242. package/dist/modules/workspace/workspace.tools.js +337 -117
  243. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  244. package/dist/modules/write-access/write-access.d.ts +60 -0
  245. package/dist/modules/write-access/write-access.d.ts.map +1 -0
  246. package/dist/modules/write-access/write-access.js +129 -0
  247. package/dist/modules/write-access/write-access.js.map +1 -0
  248. package/dist/shared/domain-errors.d.ts +64 -0
  249. package/dist/shared/domain-errors.d.ts.map +1 -1
  250. package/dist/shared/domain-errors.js +83 -0
  251. package/dist/shared/domain-errors.js.map +1 -1
  252. package/dist/shared/git.contract.d.ts +30 -0
  253. package/dist/shared/git.contract.d.ts.map +1 -1
  254. package/dist/shared/git.contract.js +26 -0
  255. package/dist/shared/git.contract.js.map +1 -1
  256. package/dist/tenancy/static-tenant-source.d.ts +0 -1
  257. package/dist/tenancy/static-tenant-source.d.ts.map +1 -1
  258. package/dist/tenancy/static-tenant-source.js +0 -2
  259. package/dist/tenancy/static-tenant-source.js.map +1 -1
  260. package/migrations/0014_change_request_closed_reason.sql +1 -0
  261. package/migrations/0015_account_deactivation.sql +3 -0
  262. package/migrations/meta/0014_snapshot.json +2265 -0
  263. package/migrations/meta/0015_snapshot.json +2271 -0
  264. package/migrations/meta/_journal.json +14 -0
  265. package/package.json +3 -3
  266. package/src/__tests__/kb-layout-config.test.ts +0 -2
  267. package/src/__tests__/retired-settings.test.ts +96 -0
  268. package/src/core/__tests__/gated-boot.test.ts +132 -0
  269. package/src/core/__tests__/lifecycle.test.ts +170 -1
  270. package/src/core/__tests__/set-aside-root-is-one-place.test.ts +63 -0
  271. package/src/core/core-ports.ts +11 -1
  272. package/src/core/create-core-server.ts +69 -15
  273. package/src/core/create-core-services.ts +190 -27
  274. package/src/core/lifecycle.ts +98 -13
  275. package/src/core-config.ts +13 -14
  276. package/src/index.ts +12 -1
  277. package/src/modules/access/__tests__/access-control.preview-relocation.test.ts +385 -0
  278. package/src/modules/access/__tests__/access-control.prospective.test.ts +94 -18
  279. package/src/modules/access/__tests__/access-requests.recut.test.ts +134 -0
  280. package/src/modules/access/__tests__/access-requests.routes.test.ts +610 -0
  281. package/src/modules/access/__tests__/access.routes.prospective.test.ts +6 -7
  282. package/src/modules/access/access-control.interface.ts +43 -12
  283. package/src/modules/access/access-control.service.ts +234 -36
  284. package/src/modules/access/access-requests.contract.ts +93 -0
  285. package/src/modules/access/access-requests.routes.ts +286 -0
  286. package/src/modules/access/access-requests.service.ts +420 -0
  287. package/src/modules/access/access.routes.ts +13 -15
  288. package/src/modules/access-model/access-grammar.ts +22 -0
  289. package/src/modules/agent-instructions/__tests__/agent-instructions.route.test.ts +3 -1
  290. package/src/modules/auth/__tests__/account-deactivation.test.ts +252 -0
  291. package/src/modules/auth/__tests__/account.routes.test.ts +81 -3
  292. package/src/modules/auth/__tests__/auth.middleware.test.ts +45 -22
  293. package/src/modules/auth/__tests__/auth.service.test.ts +39 -1
  294. package/src/modules/auth/account-admission.ts +78 -9
  295. package/src/modules/auth/account.routes.ts +63 -7
  296. package/src/modules/auth/auth.middleware.ts +19 -8
  297. package/src/modules/auth/auth.routes.ts +8 -0
  298. package/src/modules/auth/auth.service.ts +207 -7
  299. package/src/modules/auth/oidc-auth-provider.ts +7 -2
  300. package/src/modules/database/core-schema.ts +35 -27
  301. package/src/modules/github-app/__tests__/github-app.test.ts +848 -0
  302. package/src/modules/github-app/github-app.client.ts +268 -0
  303. package/src/modules/github-app/github-app.connection.ts +204 -0
  304. package/src/modules/github-app/github-app.routes.ts +359 -0
  305. package/src/modules/github-app/index.ts +19 -0
  306. package/src/modules/kb-fs/__tests__/branch-name.test.ts +10 -0
  307. package/src/modules/kb-fs/__tests__/remote-url.test.ts +38 -0
  308. package/src/modules/kb-fs/__tests__/repo-path.test.ts +123 -0
  309. package/src/modules/kb-fs/branch-name.ts +14 -1
  310. package/src/modules/kb-fs/remote-url.ts +35 -0
  311. package/src/modules/kb-fs/repo-path.ts +85 -0
  312. package/src/modules/kb-sync/__tests__/kb-sync.routes.test.ts +26 -1
  313. package/src/modules/kb-sync/kb-sync.routes.ts +14 -4
  314. package/src/modules/kb-sync/sync-auth.ts +2 -2
  315. package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +60 -3
  316. package/src/modules/mcp/__tests__/mcp.service.test.ts +68 -6
  317. package/src/modules/mcp/mcp-auth.middleware.ts +20 -6
  318. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +3 -1
  319. package/src/modules/plugins/__tests__/join-proposals.test.ts +100 -19
  320. package/src/modules/plugins/__tests__/join-requests.service.test.ts +27 -11
  321. package/src/modules/plugins/__tests__/join-requests.settlement.test.ts +371 -0
  322. package/src/modules/plugins/__tests__/plugins.routes.test.ts +1 -1
  323. package/src/modules/plugins/__tests__/plugins.tools.test.ts +2 -1
  324. package/src/modules/plugins/join-proposals.ts +87 -19
  325. package/src/modules/plugins/join-requests.service.ts +199 -39
  326. package/src/modules/plugins/plugins.routes.ts +3 -2
  327. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +16 -0
  328. package/src/modules/settings/__tests__/repository-source.test.ts +191 -0
  329. package/src/modules/settings/__tests__/setup.routes.git-mode.test.ts +386 -0
  330. package/src/modules/settings/__tests__/setup.routes.github-app.test.ts +299 -0
  331. package/src/modules/settings/__tests__/setup.routes.managed-phase.test.ts +236 -0
  332. package/src/modules/settings/__tests__/setup.routes.repository-change.test.ts +390 -0
  333. package/src/modules/settings/__tests__/setup.routes.test.ts +3 -0
  334. package/src/modules/settings/deployment-settings.service.ts +117 -6
  335. package/src/modules/settings/managed-repository.ts +68 -0
  336. package/src/modules/settings/repository-source.ts +197 -0
  337. package/src/modules/settings/setup.routes.ts +463 -15
  338. package/src/modules/skills/__tests__/allowed-tools-warn.tools.test.ts +2 -1
  339. package/src/modules/skills/__tests__/branch-skills.tools.test.ts +218 -0
  340. package/src/modules/skills/__tests__/skill-access-requests.routes.test.ts +5 -1
  341. package/src/modules/skills/__tests__/skills.service.test.ts +229 -5
  342. package/src/modules/skills/skill-access-requests.routes.ts +25 -75
  343. package/src/modules/skills/skills.contract.ts +91 -18
  344. package/src/modules/skills/skills.service.ts +278 -41
  345. package/src/modules/skills/skills.tools.ts +80 -8
  346. package/src/modules/tool-auth/__tests__/manual-auth.middleware.test.ts +14 -1
  347. package/src/modules/tool-auth/external-api-key.service.ts +4 -2
  348. package/src/modules/tool-auth/internal-token.service.ts +3 -3
  349. package/src/modules/tool-auth/tool-auth.middleware.ts +34 -11
  350. package/src/modules/tool-helpers/__tests__/agent-roles-write.test.ts +5 -3
  351. package/src/modules/tool-helpers/__tests__/phase4-tools.test.ts +3 -3
  352. package/src/modules/tool-helpers/__tests__/validate-token.test.ts +11 -1
  353. package/src/modules/tool-helpers/tool-context.ts +15 -0
  354. package/src/modules/tool-helpers/tool-handler.ts +24 -4
  355. package/src/modules/tool-helpers/tool.contract.ts +3 -2
  356. package/src/modules/tool-helpers/validate-token.ts +1 -1
  357. package/src/modules/workflow/__tests__/apply-failure.test.ts +9 -1
  358. package/src/modules/workflow/__tests__/pending-commits.repository-replaced.test.ts +111 -0
  359. package/src/modules/workflow/__tests__/workflow.routes.history-read-gate.test.ts +25 -0
  360. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +159 -8
  361. package/src/modules/workflow/__tests__/workflow.service.repository-replaced.test.ts +249 -0
  362. package/src/modules/workflow/__tests__/workflow.service.update-from-target.test.ts +146 -40
  363. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +405 -7
  364. package/src/modules/workflow/agent-tools/change-request-summary.ts +182 -0
  365. package/src/modules/workflow/agent-tools/workflow.tools.ts +135 -38
  366. package/src/modules/workflow/file-lock.service.ts +31 -0
  367. package/src/modules/workflow/git/__tests__/git.service.fileBytesAtCommit.test.ts +260 -0
  368. package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +121 -0
  369. package/src/modules/workflow/git/__tests__/pull-request.service.getPrDetail.test.ts +145 -2
  370. package/src/modules/workflow/git/__tests__/pull-request.service.viewer-can-delete.test.ts +166 -0
  371. package/src/modules/workflow/git/git.service.ts +193 -4
  372. package/src/modules/workflow/git/node-git-runner.ts +6 -0
  373. package/src/modules/workflow/git/pull-request.service.ts +119 -0
  374. package/src/modules/workflow/pending-commits.service.ts +59 -1
  375. package/src/modules/workflow/workflow-hooks.ts +64 -26
  376. package/src/modules/workflow/workflow.routes.ts +3 -1
  377. package/src/modules/workflow/workflow.service.ts +290 -55
  378. package/src/modules/workspace/__tests__/agent-access.gate.test.ts +208 -0
  379. package/src/modules/workspace/__tests__/escape-sequences.routes.test.ts +384 -0
  380. package/src/modules/workspace/__tests__/file-stat-access.test.ts +2 -1
  381. package/src/modules/workspace/__tests__/git-internals.security.test.ts +2 -1
  382. package/src/modules/workspace/__tests__/set-aside-clone.test.ts +52 -0
  383. package/src/modules/workspace/__tests__/workspace.routes.at-ref.test.ts +305 -0
  384. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +2 -0
  385. package/src/modules/workspace/__tests__/workspace.service.forget-clone-races.test.ts +147 -0
  386. package/src/modules/workspace/__tests__/workspace.service.replaced-repository.test.ts +326 -0
  387. package/src/modules/workspace/__tests__/workspace.service.test.ts +20 -9
  388. package/src/modules/workspace/__tests__/workspace.service.unknown-branch.test.ts +43 -0
  389. package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +2 -3
  390. package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +235 -10
  391. package/src/modules/workspace/__tests__/workspace.tools.test.ts +720 -17
  392. package/src/modules/workspace/agent-access.gate.ts +164 -0
  393. package/src/modules/workspace/routine-write-policy.ts +5 -6
  394. package/src/modules/workspace/session-sink.ts +5 -5
  395. package/src/modules/workspace/set-aside-clone.ts +96 -0
  396. package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +297 -0
  397. package/src/modules/workspace/startup/kb-startup-runner.ts +242 -2
  398. package/src/modules/workspace/workspace.routes.ts +95 -3
  399. package/src/modules/workspace/workspace.service.ts +273 -25
  400. package/src/modules/workspace/workspace.tools.ts +373 -119
  401. package/src/modules/write-access/__tests__/write-access.test.ts +248 -0
  402. package/src/modules/write-access/write-access.ts +153 -0
  403. package/src/shared/domain-errors.ts +89 -0
  404. package/src/shared/git.contract.ts +37 -0
  405. package/src/tenancy/__tests__/static-tenant-source.test.ts +0 -1
  406. package/src/tenancy/static-tenant-source.ts +0 -3
  407. package/dist/modules/workflow/session-ontology.policy.d.ts +0 -52
  408. package/dist/modules/workflow/session-ontology.policy.d.ts.map +0 -1
  409. package/dist/modules/workflow/session-ontology.policy.js +0 -62
  410. package/dist/modules/workflow/session-ontology.policy.js.map +0 -1
  411. package/dist/modules/workflow/session-ontology.service.d.ts +0 -105
  412. package/dist/modules/workflow/session-ontology.service.d.ts.map +0 -1
  413. package/dist/modules/workflow/session-ontology.service.js +0 -147
  414. package/dist/modules/workflow/session-ontology.service.js.map +0 -1
  415. package/dist/modules/workspace/session-ontology.gate.d.ts +0 -114
  416. package/dist/modules/workspace/session-ontology.gate.d.ts.map +0 -1
  417. package/dist/modules/workspace/session-ontology.gate.js +0 -161
  418. package/dist/modules/workspace/session-ontology.gate.js.map +0 -1
  419. package/dist/shared/kb-layout.d.ts +0 -39
  420. package/dist/shared/kb-layout.d.ts.map +0 -1
  421. package/dist/shared/kb-layout.js +0 -103
  422. package/dist/shared/kb-layout.js.map +0 -1
  423. package/dist/shared/kb-layout.test.d.ts +0 -2
  424. package/dist/shared/kb-layout.test.d.ts.map +0 -1
  425. package/dist/shared/kb-layout.test.js +0 -75
  426. package/dist/shared/kb-layout.test.js.map +0 -1
  427. package/src/modules/workflow/__tests__/session-ontology.policy.test.ts +0 -62
  428. package/src/modules/workflow/__tests__/session-ontology.service.test.ts +0 -201
  429. package/src/modules/workflow/session-ontology.policy.ts +0 -70
  430. package/src/modules/workflow/session-ontology.service.ts +0 -183
  431. package/src/modules/workspace/__tests__/session-ontology.gate.test.ts +0 -239
  432. package/src/modules/workspace/session-ontology.gate.ts +0 -191
  433. package/src/shared/kb-layout.test.ts +0 -98
  434. package/src/shared/kb-layout.ts +0 -102
@@ -353,3 +353,124 @@ describe('GitService.pull', () => {
353
353
  await expect(svc.hasUnpushedCommits(workspaceId)).resolves.toBe(false);
354
354
  });
355
355
  });
356
+
357
+ /**
358
+ * `preserveMerges` — why an unpushed MERGE must survive the pull's rebase.
359
+ *
360
+ * The change-request Update merges the target into the proposal branch and
361
+ * pushes. When that push fails, the merge sits in the clone, and the NEXT
362
+ * update pulls first. A plain `git rebase` replays `--no-merges
363
+ * upstream..HEAD`, which turns that merge commit into cherry-picks of the
364
+ * commits it merged: the target stops being a parent, so the branch never
365
+ * contains the target's head however many times the update runs, and the
366
+ * request stays behind for good.
367
+ */
368
+ describe('GitService.pull — an unpushed merge commit', () => {
369
+ let root: string;
370
+ const workspaceId = 'alice%2Fdeal';
371
+
372
+ beforeEach(async () => {
373
+ root = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-git-pull-merges-'));
374
+ });
375
+
376
+ afterEach(async () => {
377
+ await fs.rm(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
378
+ });
379
+
380
+ /**
381
+ * A clone of `alice/deal` holding a merge of `target-company-state` that
382
+ * origin has not seen, with origin's own `alice/deal` one commit further on
383
+ * so the pull has something to rebase ONTO.
384
+ */
385
+ async function seedUnpushedMerge(): Promise<{ repo: string; targetTip: string }> {
386
+ const upstream = path.join(root, 'upstream.git');
387
+ await runGit(root, ['init', '--bare', '-b', 'target-company-state', upstream]);
388
+
389
+ const seed = path.join(root, '.seed');
390
+ await fs.mkdir(seed);
391
+ await runGit(seed, ['init', '-b', 'target-company-state']);
392
+ await runGit(seed, ['remote', 'add', 'origin', upstream]);
393
+ await fs.writeFile(path.join(seed, 'shared.txt'), 'one\n');
394
+ await runGit(seed, ['add', '.']);
395
+ await runGit(seed, ['commit', '-m', 'init']);
396
+ await runGit(seed, ['push', 'origin', 'target-company-state']);
397
+ // The proposal branch, forked here.
398
+ await runGit(seed, ['checkout', '-b', 'alice/deal']);
399
+ await fs.writeFile(path.join(seed, 'proposal.txt'), 'alice proposes\n');
400
+ await runGit(seed, ['add', '.']);
401
+ await runGit(seed, ['commit', '-m', 'propose']);
402
+ await runGit(seed, ['push', 'origin', 'alice/deal']);
403
+ // The target moves on, in a file the proposal does not touch.
404
+ await runGit(seed, ['checkout', 'target-company-state']);
405
+ await fs.writeFile(path.join(seed, 'elsewhere.txt'), 'someone else\n');
406
+ await runGit(seed, ['add', '.']);
407
+ await runGit(seed, ['commit', '-m', 'target moves']);
408
+ await runGit(seed, ['push', 'origin', 'target-company-state']);
409
+ const targetTip = await gitOut(seed, ['rev-parse', 'HEAD']);
410
+
411
+ const workspaceDir = path.join(root, workspaceId);
412
+ const repo = path.join(workspaceDir, 'knowledge-base');
413
+ await fs.mkdir(workspaceDir, { recursive: true });
414
+ await runGit(root, ['clone', '-b', 'alice/deal', upstream, repo]);
415
+ await runGit(repo, ['config', 'user.email', 'workspace@bevel.test']);
416
+ await runGit(repo, ['config', 'user.name', 'bevel Workspace']);
417
+ await runGit(repo, ['config', 'core.autocrlf', 'false']);
418
+
419
+ // The Update that merged and could not push: a real merge commit in the
420
+ // clone, with the target's tip as its second parent.
421
+ await runGit(repo, ['fetch', 'origin']);
422
+ await runGit(repo, [
423
+ 'merge', '--no-ff', '-m', 'Refresh alice/deal', 'origin/target-company-state',
424
+ ]);
425
+ expect(await gitOut(repo, ['rev-list', '--count', '--merges', 'HEAD'])).toBe('1');
426
+
427
+ // Origin's alice/deal moves on too (the author saved from another client),
428
+ // so the pull has a real rebase to do rather than nothing.
429
+ await runGit(seed, ['checkout', 'alice/deal']);
430
+ await fs.writeFile(path.join(seed, 'proposal.txt'), 'alice proposes\nand adds\n');
431
+ await runGit(seed, ['add', '.']);
432
+ await runGit(seed, ['commit', '-m', 'another save']);
433
+ await runGit(seed, ['push', 'origin', 'alice/deal']);
434
+
435
+ return { repo, targetTip };
436
+ }
437
+
438
+ function svcFor(): GitService {
439
+ return new GitService(
440
+ stubWorkspaceService({ [workspaceId]: path.join(root, workspaceId) }),
441
+ stubWorkflowHooks(),
442
+ testKbContext(),
443
+ );
444
+ }
445
+
446
+ it('is flattened by a plain pull — the branch stops containing the target', async () => {
447
+ // The bug, stated as the behaviour that causes it. Kept as a test so the
448
+ // option below is never "simplified" away as redundant.
449
+ const { repo, targetTip } = await seedUnpushedMerge();
450
+ await svcFor().pull(workspaceId);
451
+
452
+ expect(await gitOut(repo, ['rev-list', '--count', '--merges', 'HEAD'])).toBe('0');
453
+ const contains = await gitOut(repo, [
454
+ 'merge-base', '--is-ancestor', targetTip, 'HEAD',
455
+ ]).then(() => true, () => false);
456
+ expect(contains).toBe(false);
457
+ });
458
+
459
+ it('survives `preserveMerges`, and the branch still contains the target', async () => {
460
+ const { repo, targetTip } = await seedUnpushedMerge();
461
+ await svcFor().pull(workspaceId, { preserveMerges: true });
462
+
463
+ // The merge is still a merge...
464
+ expect(await gitOut(repo, ['rev-list', '--count', '--merges', 'HEAD'])).toBe('1');
465
+ // ...so the target's head is an ancestor of the branch, which is exactly
466
+ // what "this request is no longer behind" means.
467
+ const contains = await gitOut(repo, [
468
+ 'merge-base', '--is-ancestor', targetTip, 'HEAD',
469
+ ]).then(() => true, () => false);
470
+ expect(contains).toBe(true);
471
+ // And the save origin had that the clone did not is replayed on top.
472
+ expect(await fs.readFile(path.join(repo, 'proposal.txt'), 'utf8')).toBe(
473
+ 'alice proposes\nand adds\n',
474
+ );
475
+ });
476
+ });
@@ -49,7 +49,13 @@ describe('PullRequestService.getPrDetail — git work per read', () => {
49
49
  const resolvePrShas = vi.fn(async () => ({ baseSha: BASE, headSha: HEAD }));
50
50
  const changedFilesForPr = vi.fn(async () => []);
51
51
  const forkPointForPr = vi.fn(async () => ({ mergeBaseSha: FORK, behind: true }));
52
- const git = { resolvePrShas, changedFilesForPr, forkPointForPr } as unknown as GitService;
52
+ const pathsChangedBetween = vi.fn(async () => []);
53
+ const git = {
54
+ resolvePrShas,
55
+ changedFilesForPr,
56
+ forkPointForPr,
57
+ pathsChangedBetween,
58
+ } as unknown as GitService;
53
59
  const workspace = {
54
60
  findAnyWorkspaceId: async () => 'ws-main',
55
61
  ensureRemotesFetched: async () => undefined,
@@ -63,7 +69,14 @@ describe('PullRequestService.getPrDetail — git work per read', () => {
63
69
  getApprovalStates,
64
70
  evaluateMergeGate: () => ({ mergeable: false, reasons: [], warnings: [] }),
65
71
  });
66
- return { svc, resolvePrShas, changedFilesForPr, forkPointForPr, getApprovalStates };
72
+ return {
73
+ svc,
74
+ resolvePrShas,
75
+ changedFilesForPr,
76
+ forkPointForPr,
77
+ pathsChangedBetween,
78
+ getApprovalStates,
79
+ };
67
80
  }
68
81
 
69
82
  it('pins the file list to the SHAs it just resolved, and keeps patches for a client read', async () => {
@@ -179,3 +192,133 @@ describe('PullRequestService — change request link', () => {
179
192
  expect(detail?.urlNote).toBe('Set PUBLIC_FRONTEND_URL to get absolute links.');
180
193
  });
181
194
  });
195
+
196
+ /**
197
+ * `needsUpdate` — the answer the dialog acts on, as distinct from `behind`.
198
+ *
199
+ * `behind` is the honest git fact that the two branches have diverged. On a
200
+ * knowledge base where every shared save commits to the default branch it is
201
+ * true again minutes after every update, and acting on it made opening a
202
+ * request pay for a merge on almost every open — for changes to files the
203
+ * request does not contain. `needsUpdate` adds the half that matters: the
204
+ * target changed a file THIS request also changes.
205
+ */
206
+ describe('PullRequestService.getPrDetail — needsUpdate', () => {
207
+ const FILE = 'Sales/Deal.md';
208
+ const OTHER = 'Ops/Runbook.md';
209
+
210
+ function harness(opts: {
211
+ behind?: boolean;
212
+ mergeBaseSha?: string | null;
213
+ files?: { path: string; previousPath?: string }[];
214
+ changedOnTarget?: string[];
215
+ changedOnTargetError?: Error;
216
+ state?: string;
217
+ } = {}) {
218
+ const resolvePrShas = vi.fn(async () => ({ baseSha: BASE, headSha: HEAD }));
219
+ const changedFilesForPr = vi.fn(async () => (opts.files ?? [{ path: FILE }]) as never);
220
+ const forkPointForPr = vi.fn(async () => ({
221
+ mergeBaseSha: opts.mergeBaseSha === undefined ? FORK : opts.mergeBaseSha,
222
+ behind: opts.behind ?? true,
223
+ }));
224
+ const pathsChangedBetween = opts.changedOnTargetError
225
+ ? vi.fn().mockRejectedValue(opts.changedOnTargetError)
226
+ : vi.fn(async () => opts.changedOnTarget ?? []);
227
+ const git = {
228
+ resolvePrShas,
229
+ changedFilesForPr,
230
+ forkPointForPr,
231
+ pathsChangedBetween,
232
+ } as unknown as GitService;
233
+ const workspace = {
234
+ findAnyWorkspaceId: async () => 'ws-main',
235
+ ensureRemotesFetched: async () => undefined,
236
+ } as unknown as WorkspaceService;
237
+ const access = { canWriteAtRef: async () => false } as unknown as IAccessControl;
238
+ const db = {
239
+ select: () => ({
240
+ from: () => ({
241
+ where: () => ({ limit: async () => [{ ...ROW, state: opts.state ?? 'open' }] }),
242
+ }),
243
+ }),
244
+ } as unknown as Database;
245
+ const svc = new PullRequestService(db, workspace, access, git);
246
+ svc.setDetailEnricher({
247
+ listComments: async () => [],
248
+ getApprovalStates: async () => [],
249
+ evaluateMergeGate: () => ({ mergeable: false, reasons: [], warnings: [] }),
250
+ });
251
+ return { svc, pathsChangedBetween };
252
+ }
253
+
254
+ it('is false when the target moved in files this request does not change', async () => {
255
+ // The case that made the message appear on essentially every open: a
256
+ // shared save somewhere else in the knowledge base.
257
+ const h = harness({ files: [{ path: FILE }], changedOnTarget: [OTHER] });
258
+ const detail = await h.svc.getPrDetail(7, { fresh: true });
259
+ expect(detail?.behind).toBe(true);
260
+ expect(detail?.needsUpdate).toBe(false);
261
+ });
262
+
263
+ it('is true when the target changed a file this request also changes', async () => {
264
+ const h = harness({ files: [{ path: FILE }], changedOnTarget: [OTHER, FILE] });
265
+ const detail = await h.svc.getPrDetail(7, { fresh: true });
266
+ expect(detail?.needsUpdate).toBe(true);
267
+ // Intersected over the fork point → the target's tip: the same two
268
+ // commits the rest of the detail is pinned to.
269
+ expect(h.pathsChangedBetween).toHaveBeenCalledWith('ws-main', FORK, BASE);
270
+ });
271
+
272
+ it('asks git nothing at all when the request is not behind', async () => {
273
+ // The common case, and the one that has to stay free.
274
+ const h = harness({ behind: false });
275
+ const detail = await h.svc.getPrDetail(7, { fresh: true });
276
+ expect(detail?.needsUpdate).toBe(false);
277
+ expect(h.pathsChangedBetween).not.toHaveBeenCalled();
278
+ });
279
+
280
+ it('asks git nothing when the request changes no files', async () => {
281
+ const h = harness({ files: [], changedOnTarget: [FILE] });
282
+ const detail = await h.svc.getPrDetail(7, { fresh: true });
283
+ expect(detail?.needsUpdate).toBe(false);
284
+ expect(h.pathsChangedBetween).not.toHaveBeenCalled();
285
+ });
286
+
287
+ it('counts a file the target RENAMED, under either of its names', async () => {
288
+ // `pathsChangedBetween` runs without rename detection, so the target's
289
+ // rename reports the path it left as well as the one it arrived at — and
290
+ // a request still editing the old name is affected.
291
+ const h = harness({
292
+ files: [{ path: FILE }],
293
+ changedOnTarget: [FILE, 'Sales/Deal-2026.md'],
294
+ });
295
+ expect((await h.svc.getPrDetail(7, { fresh: true }))?.needsUpdate).toBe(true);
296
+ });
297
+
298
+ it('counts a file THIS request renamed, under the name it came from', async () => {
299
+ const h = harness({
300
+ files: [{ path: 'Sales/Deal-final.md', previousPath: FILE }],
301
+ changedOnTarget: [FILE],
302
+ });
303
+ expect((await h.svc.getPrDetail(7, { fresh: true }))?.needsUpdate).toBe(true);
304
+ });
305
+
306
+ it('is true when the branches share no history — there is nothing to intersect', async () => {
307
+ const h = harness({ mergeBaseSha: null });
308
+ const detail = await h.svc.getPrDetail(7, { fresh: true });
309
+ expect(detail?.needsUpdate).toBe(true);
310
+ expect(h.pathsChangedBetween).not.toHaveBeenCalled();
311
+ });
312
+
313
+ it('is true when the comparison itself fails — an infra error is not "this is fine"', async () => {
314
+ const h = harness({ changedOnTargetError: new Error('git exploded') });
315
+ expect((await h.svc.getPrDetail(7, { fresh: true }))?.needsUpdate).toBe(true);
316
+ });
317
+
318
+ it('is false for a request that is not open, whatever git says', async () => {
319
+ const h = harness({ state: 'merged', changedOnTarget: [FILE] });
320
+ const detail = await h.svc.getPrDetail(7, { fresh: true });
321
+ expect(detail?.needsUpdate).toBe(false);
322
+ expect(detail?.behind).toBe(false);
323
+ });
324
+ });
@@ -0,0 +1,166 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { computeViewerCanDelete } from '../pull-request.service.js';
3
+ import { hashEmail } from '../../../../shared/email-identity.js';
4
+
5
+ const EMAIL = 'juan@bevel.software';
6
+ const AUTHOR_HASH = hashEmail(EMAIL);
7
+ const OTHER_AUTHOR_HASH = hashEmail('someone-else@bevel.software');
8
+
9
+ /**
10
+ * The truth table for who may delete a request outright — close it AND retire
11
+ * its branch. It must mirror `deleteChangeRequest`'s own check, which is why
12
+ * the interesting rows here are the ones where it DIFFERS from its siblings:
13
+ * an owner of every changed file is absent (they decline, they do not
14
+ * destroy), and a closed request still says yes (its leftover branch is still
15
+ * the server's to retire).
16
+ */
17
+ describe('computeViewerCanDelete', () => {
18
+ it('lets the author delete their own open request', () => {
19
+ expect(
20
+ computeViewerCanDelete({
21
+ state: 'open',
22
+ authorId: AUTHOR_HASH,
23
+ viewerEmail: EMAIL,
24
+ viewerCanBypassMerge: false,
25
+ }),
26
+ ).toBe(true);
27
+ });
28
+
29
+ it('lets an admin delete someone else’s open request', () => {
30
+ expect(
31
+ computeViewerCanDelete({
32
+ state: 'open',
33
+ authorId: OTHER_AUTHOR_HASH,
34
+ viewerEmail: EMAIL,
35
+ viewerCanBypassMerge: true,
36
+ }),
37
+ ).toBe(true);
38
+ });
39
+
40
+ it('says yes once for a viewer who is both author and admin', () => {
41
+ expect(
42
+ computeViewerCanDelete({
43
+ state: 'open',
44
+ authorId: AUTHOR_HASH,
45
+ viewerEmail: EMAIL,
46
+ viewerCanBypassMerge: true,
47
+ }),
48
+ ).toBe(true);
49
+ });
50
+
51
+ it('refuses a signed-in user who is neither author nor admin', () => {
52
+ expect(
53
+ computeViewerCanDelete({
54
+ state: 'open',
55
+ authorId: OTHER_AUTHOR_HASH,
56
+ viewerEmail: EMAIL,
57
+ viewerCanBypassMerge: false,
58
+ }),
59
+ ).toBe(false);
60
+ });
61
+
62
+ it('fails closed with no viewer, even on one’s own-looking request', () => {
63
+ expect(
64
+ computeViewerCanDelete({
65
+ state: 'open',
66
+ authorId: AUTHOR_HASH,
67
+ viewerEmail: undefined,
68
+ viewerCanBypassMerge: false,
69
+ }),
70
+ ).toBe(false);
71
+ });
72
+
73
+ it('fails closed for an anonymous admin-flagged read', () => {
74
+ expect(
75
+ computeViewerCanDelete({
76
+ state: 'open',
77
+ authorId: OTHER_AUTHOR_HASH,
78
+ viewerEmail: undefined,
79
+ viewerCanBypassMerge: true,
80
+ }),
81
+ ).toBe(false);
82
+ });
83
+
84
+ it('refuses an APPLIED request to its author — applied history is nobody’s to delete', () => {
85
+ expect(
86
+ computeViewerCanDelete({
87
+ state: 'merged',
88
+ authorId: AUTHOR_HASH,
89
+ viewerEmail: EMAIL,
90
+ viewerCanBypassMerge: false,
91
+ }),
92
+ ).toBe(false);
93
+ });
94
+
95
+ it('refuses an APPLIED request to an admin too', () => {
96
+ expect(
97
+ computeViewerCanDelete({
98
+ state: 'merged',
99
+ authorId: OTHER_AUTHOR_HASH,
100
+ viewerEmail: EMAIL,
101
+ viewerCanBypassMerge: true,
102
+ }),
103
+ ).toBe(false);
104
+ });
105
+
106
+ it('still says yes on a CLOSED request: the leftover branch is the delete’s job', () => {
107
+ // Withdrawn in another tab. The server's delete still succeeds there and
108
+ // removes the branch the withdraw left behind, so the hint must not go
109
+ // quiet the moment the row flips — unlike `viewerCanCancel`, which is
110
+ // open-only because there is nothing left to cancel.
111
+ expect(
112
+ computeViewerCanDelete({
113
+ state: 'closed',
114
+ authorId: AUTHOR_HASH,
115
+ viewerEmail: EMAIL,
116
+ viewerCanBypassMerge: false,
117
+ }),
118
+ ).toBe(true);
119
+ expect(
120
+ computeViewerCanDelete({
121
+ state: 'closed',
122
+ authorId: OTHER_AUTHOR_HASH,
123
+ viewerEmail: EMAIL,
124
+ viewerCanBypassMerge: true,
125
+ }),
126
+ ).toBe(true);
127
+ });
128
+
129
+ it('refuses a request with no stored author to a non-admin (opened outside this backend)', () => {
130
+ expect(
131
+ computeViewerCanDelete({
132
+ state: 'open',
133
+ authorId: undefined,
134
+ viewerEmail: EMAIL,
135
+ viewerCanBypassMerge: false,
136
+ }),
137
+ ).toBe(false);
138
+ });
139
+
140
+ it('leaves normalization to hashEmail, exactly as its siblings do', () => {
141
+ expect(
142
+ computeViewerCanDelete({
143
+ state: 'open',
144
+ authorId: AUTHOR_HASH,
145
+ viewerEmail: ' Juan@Bevel.Software ',
146
+ viewerCanBypassMerge: false,
147
+ }),
148
+ ).toBe(true);
149
+ });
150
+
151
+ it('does NOT grant the owner of every changed file — it takes no such input', () => {
152
+ // Decision 4. The owner of every changed file is exactly the person
153
+ // `computeViewerCanCancel` says yes to and this predicate must not: they
154
+ // may decline the request, which leaves the author's branch to rework.
155
+ // There is no input here to express that grant, and the row a
156
+ // file-owning non-admin produces reads false.
157
+ expect(
158
+ computeViewerCanDelete({
159
+ state: 'open',
160
+ authorId: OTHER_AUTHOR_HASH,
161
+ viewerEmail: EMAIL,
162
+ viewerCanBypassMerge: false,
163
+ }),
164
+ ).toBe(false);
165
+ });
166
+ });