@bevel-software/platform-core-backend 0.29.0 → 0.31.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 (307) hide show
  1. package/agent-guide/access-control.md +49 -4
  2. package/agent-guide/html-views.md +76 -2
  3. package/dist/__tests__/test-repositories.d.ts +3 -0
  4. package/dist/__tests__/test-repositories.d.ts.map +1 -0
  5. package/dist/__tests__/test-repositories.js +18 -0
  6. package/dist/__tests__/test-repositories.js.map +1 -0
  7. package/dist/core/catalog-revision.js +3 -3
  8. package/dist/core/create-core-server.d.ts.map +1 -1
  9. package/dist/core/create-core-server.js +3 -2
  10. package/dist/core/create-core-server.js.map +1 -1
  11. package/dist/core/create-core-services.d.ts +3 -0
  12. package/dist/core/create-core-services.d.ts.map +1 -1
  13. package/dist/core/create-core-services.js +35 -5
  14. package/dist/core/create-core-services.js.map +1 -1
  15. package/dist/core/lifecycle.d.ts +8 -0
  16. package/dist/core/lifecycle.d.ts.map +1 -1
  17. package/dist/core/lifecycle.js +4 -0
  18. package/dist/core/lifecycle.js.map +1 -1
  19. package/dist/modules/access/access-control.interface.d.ts +20 -0
  20. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  21. package/dist/modules/access/access-control.service.d.ts +32 -1
  22. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  23. package/dist/modules/access/access-control.service.js +135 -17
  24. package/dist/modules/access/access-control.service.js.map +1 -1
  25. package/dist/modules/access/access.routes.d.ts.map +1 -1
  26. package/dist/modules/access/access.routes.js +20 -6
  27. package/dist/modules/access/access.routes.js.map +1 -1
  28. package/dist/modules/agent-instructions/shared-file-rules.d.ts +1 -1
  29. package/dist/modules/agent-instructions/shared-file-rules.js +1 -1
  30. package/dist/modules/audit/agent-audit.service.d.ts.map +1 -1
  31. package/dist/modules/audit/agent-audit.service.js +66 -6
  32. package/dist/modules/audit/agent-audit.service.js.map +1 -1
  33. package/dist/modules/audit/audit.contract.d.ts +52 -8
  34. package/dist/modules/audit/audit.contract.d.ts.map +1 -1
  35. package/dist/modules/audit/audit.contract.js +37 -1
  36. package/dist/modules/audit/audit.contract.js.map +1 -1
  37. package/dist/modules/audit/audit.routes.d.ts +1 -1
  38. package/dist/modules/audit/audit.routes.d.ts.map +1 -1
  39. package/dist/modules/audit/audit.routes.js +15 -13
  40. package/dist/modules/audit/audit.routes.js.map +1 -1
  41. package/dist/modules/audit/request-audit.d.ts +21 -10
  42. package/dist/modules/audit/request-audit.d.ts.map +1 -1
  43. package/dist/modules/audit/request-audit.js +26 -10
  44. package/dist/modules/audit/request-audit.js.map +1 -1
  45. package/dist/modules/database/core-schema.d.ts +22 -3
  46. package/dist/modules/database/core-schema.d.ts.map +1 -1
  47. package/dist/modules/database/core-schema.js +15 -6
  48. package/dist/modules/database/core-schema.js.map +1 -1
  49. package/dist/modules/diff/diff.service.d.ts.map +1 -1
  50. package/dist/modules/diff/diff.service.js +7 -1
  51. package/dist/modules/diff/diff.service.js.map +1 -1
  52. package/dist/modules/kb-fs/git-dir.d.ts +21 -0
  53. package/dist/modules/kb-fs/git-dir.d.ts.map +1 -0
  54. package/dist/modules/kb-fs/git-dir.js +58 -0
  55. package/dist/modules/kb-fs/git-dir.js.map +1 -0
  56. package/dist/modules/kb-fs/index.d.ts +3 -0
  57. package/dist/modules/kb-fs/index.d.ts.map +1 -1
  58. package/dist/modules/kb-fs/index.js +3 -1
  59. package/dist/modules/kb-fs/index.js.map +1 -1
  60. package/dist/modules/kb-fs/locking-filesystem.d.ts.map +1 -1
  61. package/dist/modules/kb-fs/locking-filesystem.js +3 -12
  62. package/dist/modules/kb-fs/locking-filesystem.js.map +1 -1
  63. package/dist/modules/kb-fs/mutex.d.ts +11 -0
  64. package/dist/modules/kb-fs/mutex.d.ts.map +1 -1
  65. package/dist/modules/kb-fs/mutex.js +14 -0
  66. package/dist/modules/kb-fs/mutex.js.map +1 -1
  67. package/dist/modules/kb-fs/ref-files.d.ts +63 -0
  68. package/dist/modules/kb-fs/ref-files.d.ts.map +1 -0
  69. package/dist/modules/kb-fs/ref-files.js +249 -0
  70. package/dist/modules/kb-fs/ref-files.js.map +1 -0
  71. package/dist/modules/mcp/mcp-auth.middleware.d.ts +14 -0
  72. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
  73. package/dist/modules/mcp/mcp-auth.middleware.js +4 -0
  74. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
  75. package/dist/modules/mcp/mcp.routes.d.ts +5 -0
  76. package/dist/modules/mcp/mcp.routes.d.ts.map +1 -1
  77. package/dist/modules/mcp/mcp.routes.js +67 -0
  78. package/dist/modules/mcp/mcp.routes.js.map +1 -1
  79. package/dist/modules/mcp/mcp.service.d.ts +107 -27
  80. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  81. package/dist/modules/mcp/mcp.service.js +257 -50
  82. package/dist/modules/mcp/mcp.service.js.map +1 -1
  83. package/dist/modules/mcp/request-context.d.ts +25 -0
  84. package/dist/modules/mcp/request-context.d.ts.map +1 -0
  85. package/dist/modules/mcp/request-context.js +11 -0
  86. package/dist/modules/mcp/request-context.js.map +1 -0
  87. package/dist/modules/mcp/shared-definitions.d.ts +56 -0
  88. package/dist/modules/mcp/shared-definitions.d.ts.map +1 -0
  89. package/dist/modules/mcp/shared-definitions.js +117 -0
  90. package/dist/modules/mcp/shared-definitions.js.map +1 -0
  91. package/dist/modules/mcp/surface-cache.d.ts +70 -0
  92. package/dist/modules/mcp/surface-cache.d.ts.map +1 -0
  93. package/dist/modules/mcp/surface-cache.js +104 -0
  94. package/dist/modules/mcp/surface-cache.js.map +1 -0
  95. package/dist/modules/plugins/teams.routes.d.ts +4 -0
  96. package/dist/modules/plugins/teams.routes.d.ts.map +1 -1
  97. package/dist/modules/plugins/teams.routes.js +9 -1
  98. package/dist/modules/plugins/teams.routes.js.map +1 -1
  99. package/dist/modules/tool-auth/internal-token.service.d.ts +1 -1
  100. package/dist/modules/tool-auth/tool-auth.middleware.d.ts +1 -2
  101. package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
  102. package/dist/modules/tool-auth/tool-auth.middleware.js +1 -2
  103. package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
  104. package/dist/modules/tool-helpers/tool-context.d.ts.map +1 -1
  105. package/dist/modules/tool-helpers/tool-context.js +33 -12
  106. package/dist/modules/tool-helpers/tool-context.js.map +1 -1
  107. package/dist/modules/tool-helpers/tool-handler.d.ts +2 -2
  108. package/dist/modules/tool-helpers/tool.contract.d.ts +10 -2
  109. package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
  110. package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
  111. package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts.map +1 -1
  112. package/dist/modules/workflow/agent-tools/change-request-read.tools.js +3 -14
  113. package/dist/modules/workflow/agent-tools/change-request-read.tools.js.map +1 -1
  114. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts +18 -1
  115. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  116. package/dist/modules/workflow/agent-tools/workflow.tools.js +453 -2
  117. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  118. package/dist/modules/workflow/git/git.service.d.ts +142 -4
  119. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  120. package/dist/modules/workflow/git/git.service.js +645 -128
  121. package/dist/modules/workflow/git/git.service.js.map +1 -1
  122. package/dist/modules/workflow/read-on-branch.d.ts +35 -0
  123. package/dist/modules/workflow/read-on-branch.d.ts.map +1 -0
  124. package/dist/modules/workflow/read-on-branch.js +58 -0
  125. package/dist/modules/workflow/read-on-branch.js.map +1 -0
  126. package/dist/modules/workflow/workflow-hooks.d.ts +1 -1
  127. package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
  128. package/dist/modules/workflow/workflow.routes.js +22 -41
  129. package/dist/modules/workflow/workflow.routes.js.map +1 -1
  130. package/dist/modules/workflow/workflow.service.d.ts +48 -8
  131. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  132. package/dist/modules/workflow/workflow.service.js +163 -16
  133. package/dist/modules/workflow/workflow.service.js.map +1 -1
  134. package/dist/modules/workspace/agent-access.gate.d.ts +2 -2
  135. package/dist/modules/workspace/agent-access.gate.js +2 -2
  136. package/dist/modules/workspace/bounded-matcher.d.ts +79 -0
  137. package/dist/modules/workspace/bounded-matcher.d.ts.map +1 -0
  138. package/dist/modules/workspace/bounded-matcher.js +261 -0
  139. package/dist/modules/workspace/bounded-matcher.js.map +1 -0
  140. package/dist/modules/workspace/fetch-instant.d.ts +9 -0
  141. package/dist/modules/workspace/fetch-instant.d.ts.map +1 -0
  142. package/dist/modules/workspace/fetch-instant.js +13 -0
  143. package/dist/modules/workspace/fetch-instant.js.map +1 -0
  144. package/dist/modules/workspace/index.d.ts +9 -0
  145. package/dist/modules/workspace/index.d.ts.map +1 -1
  146. package/dist/modules/workspace/index.js +10 -0
  147. package/dist/modules/workspace/index.js.map +1 -1
  148. package/dist/modules/workspace/kb-repository.contract.d.ts +124 -0
  149. package/dist/modules/workspace/kb-repository.contract.d.ts.map +1 -0
  150. package/dist/modules/workspace/kb-repository.contract.js +19 -0
  151. package/dist/modules/workspace/kb-repository.contract.js.map +1 -0
  152. package/dist/modules/workspace/kb-repository.d.ts +236 -0
  153. package/dist/modules/workspace/kb-repository.d.ts.map +1 -0
  154. package/dist/modules/workspace/kb-repository.js +811 -0
  155. package/dist/modules/workspace/kb-repository.js.map +1 -0
  156. package/dist/modules/workspace/pattern-matcher.contract.d.ts +50 -0
  157. package/dist/modules/workspace/pattern-matcher.contract.d.ts.map +1 -0
  158. package/dist/modules/workspace/pattern-matcher.contract.js +27 -0
  159. package/dist/modules/workspace/pattern-matcher.contract.js.map +1 -0
  160. package/dist/modules/workspace/routine-write-policy.d.ts +0 -7
  161. package/dist/modules/workspace/routine-write-policy.d.ts.map +1 -1
  162. package/dist/modules/workspace/routine-write-policy.js +0 -5
  163. package/dist/modules/workspace/routine-write-policy.js.map +1 -1
  164. package/dist/modules/workspace/set-aside-clone.d.ts +40 -0
  165. package/dist/modules/workspace/set-aside-clone.d.ts.map +1 -1
  166. package/dist/modules/workspace/set-aside-clone.js +139 -0
  167. package/dist/modules/workspace/set-aside-clone.js.map +1 -1
  168. package/dist/modules/workspace/startup/kb-startup-runner.d.ts +99 -0
  169. package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
  170. package/dist/modules/workspace/startup/kb-startup-runner.js +407 -29
  171. package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
  172. package/dist/modules/workspace/workspace.service.d.ts +68 -32
  173. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  174. package/dist/modules/workspace/workspace.service.js +240 -161
  175. package/dist/modules/workspace/workspace.service.js.map +1 -1
  176. package/dist/modules/workspace/workspace.tools.d.ts +13 -4
  177. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  178. package/dist/modules/workspace/workspace.tools.js +162 -336
  179. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  180. package/dist/shared/git.contract.d.ts +7 -0
  181. package/dist/shared/git.contract.d.ts.map +1 -1
  182. package/dist/shared/git.contract.js +9 -0
  183. package/dist/shared/git.contract.js.map +1 -1
  184. package/dist/shared/no-link-below.d.ts +10 -0
  185. package/dist/shared/no-link-below.d.ts.map +1 -0
  186. package/dist/shared/no-link-below.js +30 -0
  187. package/dist/shared/no-link-below.js.map +1 -0
  188. package/dist/test-setup.js +4 -0
  189. package/dist/test-setup.js.map +1 -1
  190. package/migrations/0018_agent_events_page.sql +11 -0
  191. package/migrations/meta/0018_snapshot.json +2429 -0
  192. package/migrations/meta/_journal.json +8 -1
  193. package/package.json +3 -3
  194. package/src/__tests__/test-repositories.ts +22 -0
  195. package/src/__tests__/workspace-service.ts +69 -0
  196. package/src/core/__tests__/lifecycle.test.ts +16 -0
  197. package/src/core/catalog-revision.ts +3 -3
  198. package/src/core/create-core-server.ts +3 -2
  199. package/src/core/create-core-services.ts +39 -4
  200. package/src/core/lifecycle.ts +10 -0
  201. package/src/modules/access/__tests__/access-control.atref-cache.test.ts +144 -4
  202. package/src/modules/access/__tests__/access-control.group-read.test.ts +73 -0
  203. package/src/modules/access/__tests__/access.routes.group-grant.test.ts +66 -0
  204. package/src/modules/access/__tests__/access.routes.unknown-email.test.ts +2 -0
  205. package/src/modules/access/access-control.interface.ts +22 -0
  206. package/src/modules/access/access-control.service.ts +138 -15
  207. package/src/modules/access/access.routes.ts +22 -6
  208. package/src/modules/agent-guide/__tests__/agent-guide.test.ts +22 -1
  209. package/src/modules/agent-instructions/shared-file-rules.ts +1 -1
  210. package/src/modules/audit/__tests__/agent-audit.service.test.ts +78 -1
  211. package/src/modules/audit/__tests__/audit.routes.test.ts +43 -5
  212. package/src/modules/audit/agent-audit.service.ts +66 -6
  213. package/src/modules/audit/audit.contract.ts +62 -10
  214. package/src/modules/audit/audit.routes.ts +14 -12
  215. package/src/modules/audit/request-audit.ts +28 -13
  216. package/src/modules/database/core-schema.ts +15 -6
  217. package/src/modules/diff/__tests__/diff.service.seed-atomicity.test.ts +2 -1
  218. package/src/modules/diff/__tests__/diff.service.test.ts +2 -1
  219. package/src/modules/diff/__tests__/diff.service.worktree.test.ts +76 -0
  220. package/src/modules/diff/diff.service.ts +6 -4
  221. package/src/modules/kb-fs/__tests__/git-dir.test.ts +77 -0
  222. package/src/modules/kb-fs/__tests__/mutex.canonical.test.ts +57 -0
  223. package/src/modules/kb-fs/__tests__/ref-files.test.ts +338 -0
  224. package/src/modules/kb-fs/git-dir.ts +53 -0
  225. package/src/modules/kb-fs/index.ts +3 -0
  226. package/src/modules/kb-fs/locking-filesystem.ts +3 -12
  227. package/src/modules/kb-fs/mutex.ts +16 -0
  228. package/src/modules/kb-fs/ref-files.ts +279 -0
  229. package/src/modules/mcp/__tests__/mcp-routes-harness.ts +7 -1
  230. package/src/modules/mcp/__tests__/mcp.routes.page.test.ts +115 -0
  231. package/src/modules/mcp/__tests__/mcp.surface-cache.test.ts +521 -0
  232. package/src/modules/mcp/__tests__/shared-definitions.test.ts +28 -0
  233. package/src/modules/mcp/__tests__/surface-cache.test.ts +127 -0
  234. package/src/modules/mcp/mcp-auth.middleware.ts +21 -0
  235. package/src/modules/mcp/mcp.routes.ts +68 -0
  236. package/src/modules/mcp/mcp.service.ts +315 -51
  237. package/src/modules/mcp/request-context.ts +34 -0
  238. package/src/modules/mcp/shared-definitions.ts +121 -0
  239. package/src/modules/mcp/surface-cache.ts +154 -0
  240. package/src/modules/plugins/__tests__/teams.routes.test.ts +55 -0
  241. package/src/modules/plugins/teams.routes.ts +14 -1
  242. package/src/modules/settings/__tests__/setup.routes.layout-phase.test.ts +2 -2
  243. package/src/modules/settings/__tests__/setup.routes.managed-phase.test.ts +2 -2
  244. package/src/modules/tool-auth/internal-token.service.ts +1 -1
  245. package/src/modules/tool-auth/tool-auth.middleware.ts +1 -2
  246. package/src/modules/tool-helpers/__tests__/branch-declaration.test.ts +2 -2
  247. package/src/modules/tool-helpers/tool-context.ts +37 -18
  248. package/src/modules/tool-helpers/tool-handler.ts +2 -2
  249. package/src/modules/tool-helpers/tool.contract.ts +10 -2
  250. package/src/modules/tool-registry/__tests__/own-tool-schemas.test.ts +1 -1
  251. package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +7 -2
  252. package/src/modules/workflow/__tests__/workflow.routes.history-read-gate.test.ts +23 -12
  253. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +250 -4
  254. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +464 -4
  255. package/src/modules/workflow/agent-tools/change-request-read.tools.ts +4 -22
  256. package/src/modules/workflow/agent-tools/workflow.tools.ts +462 -1
  257. package/src/modules/workflow/git/__tests__/git.service.changedPathsMemo.test.ts +175 -0
  258. package/src/modules/workflow/git/__tests__/git.service.diffFileBetweenBranches.test.ts +76 -11
  259. package/src/modules/workflow/git/__tests__/git.service.mergeIntoBranch.test.ts +721 -0
  260. package/src/modules/workflow/git/__tests__/git.service.worktrees.test.ts +284 -0
  261. package/src/modules/workflow/git/git.service.ts +719 -126
  262. package/src/modules/workflow/read-on-branch.ts +79 -0
  263. package/src/modules/workflow/workflow-hooks.ts +1 -1
  264. package/src/modules/workflow/workflow.routes.ts +21 -39
  265. package/src/modules/workflow/workflow.service.ts +203 -16
  266. package/src/modules/workspace/__tests__/agent-downloads.test.ts +2 -2
  267. package/src/modules/workspace/__tests__/bounded-matcher.test.ts +213 -0
  268. package/src/modules/workspace/__tests__/git-internals.security.test.ts +22 -7
  269. package/src/modules/workspace/__tests__/kb-repository.test.ts +481 -0
  270. package/src/modules/workspace/__tests__/rest-tool-arguments.test.ts +14 -0
  271. package/src/modules/workspace/__tests__/routine-write-policy.test.ts +0 -11
  272. package/src/modules/workspace/__tests__/workspace-paths-inside-the-repo.test.ts +2 -2
  273. package/src/modules/workspace/__tests__/workspace.routes.folder-zip-access.test.ts +2 -2
  274. package/src/modules/workspace/__tests__/workspace.routes.folders.test.ts +4 -3
  275. package/src/modules/workspace/__tests__/workspace.service.any-workspace-credentials.test.ts +2 -2
  276. package/src/modules/workspace/__tests__/workspace.service.ensure-remotes-fetched.test.ts +2 -2
  277. package/src/modules/workspace/__tests__/workspace.service.forget-clone-races.test.ts +12 -5
  278. package/src/modules/workspace/__tests__/workspace.service.list-cloned.test.ts +6 -6
  279. package/src/modules/workspace/__tests__/workspace.service.move-never-overwrites.test.ts +2 -1
  280. package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +2 -1
  281. package/src/modules/workspace/__tests__/workspace.service.replaced-repository.test.ts +2 -1
  282. package/src/modules/workspace/__tests__/workspace.service.test.ts +24 -23
  283. package/src/modules/workspace/__tests__/workspace.service.unknown-branch.test.ts +2 -1
  284. package/src/modules/workspace/__tests__/workspace.service.worktrees.test.ts +620 -0
  285. package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +15 -1
  286. package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +71 -5
  287. package/src/modules/workspace/__tests__/workspace.tools.test.ts +153 -298
  288. package/src/modules/workspace/agent-access.gate.ts +2 -2
  289. package/src/modules/workspace/bounded-matcher.ts +328 -0
  290. package/src/modules/workspace/fetch-instant.ts +13 -0
  291. package/src/modules/workspace/index.ts +23 -0
  292. package/src/modules/workspace/kb-repository.contract.ts +124 -0
  293. package/src/modules/workspace/kb-repository.ts +844 -0
  294. package/src/modules/workspace/pattern-matcher.contract.ts +69 -0
  295. package/src/modules/workspace/routine-write-policy.ts +0 -15
  296. package/src/modules/workspace/set-aside-clone.ts +140 -0
  297. package/src/modules/workspace/startup/__tests__/kb-startup-runner.degraded.test.ts +3 -2
  298. package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +12 -10
  299. package/src/modules/workspace/startup/__tests__/kb-startup-runner.worktrees.test.ts +613 -0
  300. package/src/modules/workspace/startup/kb-startup-runner.ts +422 -29
  301. package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +4 -3
  302. package/src/modules/workspace/workspace.service.ts +247 -169
  303. package/src/modules/workspace/workspace.tools.ts +179 -365
  304. package/src/shared/__tests__/no-link-below.test.ts +45 -0
  305. package/src/shared/git.contract.ts +10 -0
  306. package/src/shared/no-link-below.ts +29 -0
  307. package/src/test-setup.ts +5 -0
@@ -0,0 +1,284 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { testKbContext } from '../../../../__tests__/kb-context.js';
3
+ import { execFile } from 'node:child_process';
4
+ import { promisify } from 'node:util';
5
+ import fs from 'node:fs/promises';
6
+ import path from 'node:path';
7
+ import os from 'node:os';
8
+ import type { AuthUser } from '@bevel-software/platform-shared';
9
+ import { NodeFs } from '../../../kb-fs/node-fs.js';
10
+ import { WorkspaceMutex } from '../../../kb-fs/mutex.js';
11
+ import { WorkspaceService } from '../../../workspace/workspace.service.js';
12
+ import { KbRepository, repositoryDirFor } from '../../../workspace/kb-repository.js';
13
+ import { WorkflowHooks } from '../../workflow-hooks.js';
14
+ import { GitService } from '../git.service.js';
15
+ import { NodeGitRunner } from '../node-git-runner.js';
16
+ import { workspaceIdForBranch } from '../../../../shared/workspace-id.js';
17
+ import { newWorkspaceService } from '../../../../__tests__/workspace-service.js';
18
+
19
+ /**
20
+ * The git layer over a knowledge base of WORKTREES of one repository: the
21
+ * same answers as over clones of the same remote, two worktrees committing at
22
+ * once, and a branch deleted while its own worktree is open. Real git.
23
+ */
24
+
25
+ const execFileAsync = promisify(execFile);
26
+ const TARGET = 'current-company-state';
27
+ const ALICE: AuthUser = { id: 'u-alice', email: 'alice@example.com', name: 'Alice' };
28
+
29
+ async function git(cwd: string, args: string[]): Promise<string> {
30
+ const { stdout } = await execFileAsync('git', args, {
31
+ cwd,
32
+ env: {
33
+ ...process.env,
34
+ GIT_AUTHOR_NAME: 'Test', GIT_AUTHOR_EMAIL: 't@x.com',
35
+ GIT_COMMITTER_NAME: 'Test', GIT_COMMITTER_EMAIL: 't@x.com',
36
+ },
37
+ });
38
+ return stdout.toString();
39
+ }
40
+
41
+ let root: string;
42
+ let upstream: string;
43
+ let workspacesRoot: string;
44
+ let workspaces: WorkspaceService;
45
+ let gitService: GitService;
46
+
47
+ beforeEach(async () => {
48
+ root = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-git-worktrees-'));
49
+ workspacesRoot = path.join(root, 'workspaces');
50
+ await fs.mkdir(workspacesRoot, { recursive: true });
51
+ upstream = path.join(root, 'upstream.git');
52
+ await git(root, ['init', '--bare', '-b', TARGET, upstream]);
53
+ const seed = path.join(root, '.seed');
54
+ await fs.mkdir(seed);
55
+ await git(seed, ['init', '-b', TARGET]);
56
+ await fs.writeFile(path.join(seed, 'base.md'), 'base\n');
57
+ await git(seed, ['add', '-A']);
58
+ await git(seed, ['commit', '-m', 'init']);
59
+ for (const draft of ['alice/a', 'bob/b']) {
60
+ await git(seed, ['checkout', '-q', '-b', draft, TARGET]);
61
+ await fs.writeFile(path.join(seed, `${draft.split('/')[0]}.md`), `${draft}\n`);
62
+ await git(seed, ['add', '-A']);
63
+ await git(seed, ['commit', '-m', `on ${draft}`]);
64
+ }
65
+ await git(seed, ['push', '-q', upstream, TARGET, 'alice/a', 'bob/b']);
66
+
67
+ const mutex = new WorkspaceMutex();
68
+ const runner = new NodeGitRunner();
69
+ const repository = new KbRepository({
70
+ dir: repositoryDirFor(workspacesRoot, 'knowledge-base'),
71
+ kbRepoUrl: () => upstream,
72
+ gitRunner: runner,
73
+ });
74
+ workspaces = newWorkspaceService(
75
+ workspacesRoot, upstream, testKbContext(), new NodeFs(), runner, undefined, undefined, repository,
76
+ );
77
+ gitService = new GitService(workspaces, new WorkflowHooks(), testKbContext(), mutex, null, runner);
78
+ });
79
+
80
+ afterEach(async () => {
81
+ await fs.rm(root, { recursive: true, force: true });
82
+ });
83
+
84
+ const id = (branch: string) => workspaceIdForBranch(branch);
85
+ const checkoutOf = (branch: string) => path.join(workspacesRoot, id(branch), 'knowledge-base');
86
+
87
+ describe('GitService over worktrees', () => {
88
+ it('lands two commits requested at the same time in two worktrees, and pushes both', async () => {
89
+ await workspaces.getOrCreateForBranch('alice/a');
90
+ await workspaces.getOrCreateForBranch('bob/b');
91
+ await fs.writeFile(path.join(checkoutOf('alice/a'), 'alice.md'), 'alice edits\n');
92
+ await fs.writeFile(path.join(checkoutOf('bob/b'), 'bob.md'), 'bob edits\n');
93
+
94
+ const [a, b] = await Promise.all([
95
+ gitService.commitFile(id('alice/a'), ALICE, 'knowledge-base/alice.md', 'alice edits'),
96
+ gitService.commitFile(id('bob/b'), ALICE, 'knowledge-base/bob.md', 'bob edits'),
97
+ ]);
98
+ expect(a).not.toBeNull();
99
+ expect(b).not.toBeNull();
100
+ await Promise.all([gitService.push(id('alice/a'), ALICE), gitService.push(id('bob/b'), ALICE)]);
101
+
102
+ expect(await git(upstream, ['show', 'alice/a:alice.md'])).toBe('alice edits\n');
103
+ expect(await git(upstream, ['show', 'bob/b:bob.md'])).toBe('bob edits\n');
104
+ // Each push left its branch tracking its own remote branch.
105
+ expect((await git(checkoutOf('alice/a'), ['rev-parse', '--abbrev-ref', '@{upstream}'])).trim()).toBe('origin/alice/a');
106
+ expect((await git(checkoutOf('bob/b'), ['status', '--porcelain'])).trim()).toBe('');
107
+ });
108
+
109
+ it("answers a change request's files and merges it exactly as a clone of the same remote does", async () => {
110
+ await workspaces.getOrCreateForBranch(TARGET);
111
+ await workspaces.getOrCreateForBranch('alice/a');
112
+ const onWorktrees = await gitService.changedFilesForPr(id(TARGET), TARGET, 'alice/a');
113
+
114
+ // The same question asked of a full clone of the same remote.
115
+ const cloneRoot = path.join(root, 'clones');
116
+ const clone = path.join(cloneRoot, id(TARGET), 'knowledge-base');
117
+ await fs.mkdir(path.dirname(clone), { recursive: true });
118
+ await git(root, ['clone', '-q', '-b', TARGET, upstream, clone]);
119
+ const clones = newWorkspaceService(cloneRoot, upstream, testKbContext(), new NodeFs());
120
+ const onClones = await new GitService(clones, new WorkflowHooks(), testKbContext()).changedFilesForPr(
121
+ id(TARGET), TARGET, 'alice/a',
122
+ );
123
+ expect(onWorktrees).toEqual(onClones);
124
+
125
+ await gitService.mergeChangeRequest(
126
+ id(TARGET), 'alice/a', TARGET, { subject: 'Merge alice/a (#1)', body: '' }, ALICE,
127
+ );
128
+ expect(await git(upstream, ['show', `${TARGET}:alice.md`])).toBe('alice/a\n');
129
+ });
130
+
131
+ it('deletes a branch whose own worktree is open, and removing that workspace takes the worktree and its ref', async () => {
132
+ await workspaces.getOrCreateForBranch(TARGET);
133
+ await workspaces.getOrCreateForBranch('alice/a');
134
+
135
+ await gitService.deleteBranch(id(TARGET), 'alice/a', ALICE, { systemCleanup: true });
136
+ await workspaces.deleteWorkspace(id('alice/a'));
137
+
138
+ await expect(git(upstream, ['rev-parse', '--verify', 'refs/heads/alice/a'])).rejects.toThrow();
139
+ await expect(git(workspaces.repository.dir, ['rev-parse', '--verify', 'refs/heads/alice/a'])).rejects.toThrow();
140
+ await expect(fs.access(checkoutOf('alice/a'))).rejects.toThrow();
141
+ expect((await git(checkoutOf(TARGET), ['status', '--porcelain'])).trim()).toBe('');
142
+ });
143
+
144
+ it("finishes a branch's first open raced by git operations on that branch, and a fetch joined from inside one", async () => {
145
+ // The first request for a branch fans out: the page opens the workspace
146
+ // while the git layer asks for its status — inside the branch's
147
+ // reservation, resolving the same workspace — and a listing fetches.
148
+ // The worktree add waits for running git operations, and they wait for
149
+ // the open: nothing may wait for itself.
150
+ await workspaces.getOrCreateForBranch(TARGET);
151
+ const all = Promise.all([
152
+ workspaces.getOrCreateForBranch('bob/b'),
153
+ gitService.status(id('bob/b')),
154
+ gitService.listBranches(id(TARGET), { freshFetch: true }),
155
+ gitService.status(id('bob/b')),
156
+ ]);
157
+ let timeout: ReturnType<typeof setTimeout> | undefined;
158
+ try {
159
+ await Promise.race([
160
+ all,
161
+ new Promise((_, reject) => {
162
+ timeout = setTimeout(() => reject(new Error('deadlocked')), 20_000);
163
+ }),
164
+ ]);
165
+ } finally {
166
+ if (timeout) clearTimeout(timeout);
167
+ }
168
+ expect((await git(checkoutOf('bob/b'), ['rev-parse', '--abbrev-ref', 'HEAD'])).trim()).toBe('bob/b');
169
+ });
170
+
171
+ it('lists branches from one fetch of the repository, strictly', async () => {
172
+ await workspaces.getOrCreateForBranch(TARGET);
173
+ const branches = await gitService.listBranches(id(TARGET), { freshFetch: true, strictFetch: true });
174
+ expect(branches.map((b) => b.name).sort()).toEqual(['alice/a', 'bob/b', TARGET].sort());
175
+ expect(gitService.lastRemoteContact()?.ok).toBe(true);
176
+ });
177
+
178
+ it("does not list another worktree's local branch once origin no longer has it, so the orphan sweep can take it", async () => {
179
+ await workspaces.getOrCreateForBranch(TARGET);
180
+ await workspaces.getOrCreateForBranch('bob/b');
181
+ // Deleted on the host while its worktree stays open: the repository
182
+ // still holds bob/b's local ref, as bob's clone held it.
183
+ await git(upstream, ['branch', '-D', 'bob/b']);
184
+
185
+ const fromTarget = await gitService.listBranches(id(TARGET), { freshFetch: true, strictFetch: true });
186
+ expect(fromTarget.map((b) => b.name).sort()).toEqual(['alice/a', TARGET].sort());
187
+ // A clone never saw another branch's local ref either: listed from its
188
+ // own worktree, a branch is its own.
189
+ const fromBob = await gitService.listBranches(id('bob/b'), { freshFetch: true });
190
+ expect(fromBob.find((b) => b.name === 'bob/b')?.hasRemote).toBe(false);
191
+
192
+ const { removed } = await workspaces.sweepOrphanedWorkspaces(fromTarget.map((b) => b.name));
193
+ expect(removed).toEqual([id('bob/b')]);
194
+ await expect(git(workspaces.repository.dir, ['rev-parse', '--verify', 'refs/heads/bob/b'])).rejects.toThrow();
195
+ });
196
+ });
197
+
198
+ describe('GitService and the repository administrator', () => {
199
+ /** The repository, noting whether its administrator's section is held while git runs. */
200
+ class RecordingRepository extends KbRepository {
201
+ depth = 0;
202
+ override exclusive<T>(fn: () => Promise<T>): Promise<T> {
203
+ return super.exclusive(async () => {
204
+ this.depth += 1;
205
+ try { return await fn(); } finally { this.depth -= 1; }
206
+ });
207
+ }
208
+ }
209
+
210
+ it('writes the shared config as the administrator, and runs everything else as itself', async () => {
211
+ const mutex = new WorkspaceMutex();
212
+ const runner = new NodeGitRunner();
213
+ const repository = new RecordingRepository({
214
+ dir: repositoryDirFor(workspacesRoot, 'knowledge-base'),
215
+ kbRepoUrl: () => upstream,
216
+ gitRunner: runner,
217
+ });
218
+ const recorded = newWorkspaceService(
219
+ workspacesRoot, upstream, testKbContext(), new NodeFs(), runner, undefined, undefined, repository,
220
+ );
221
+ const calls: { args: string[]; held: boolean }[] = [];
222
+ const recordingRunner = {
223
+ defaultTimeoutMs: runner.defaultTimeoutMs,
224
+ credentials: runner.credentials,
225
+ run: ((cwd: string, args: string[], opts?: never) => {
226
+ calls.push({ args, held: repository.depth > 0 });
227
+ return runner.run(cwd, args, opts);
228
+ }) as NodeGitRunner['run'],
229
+ };
230
+ const service = new GitService(recorded, new WorkflowHooks(), testKbContext(), mutex, null, recordingRunner);
231
+ await recorded.getOrCreateForBranch(TARGET);
232
+ await recorded.getOrCreateForBranch('alice/a');
233
+
234
+ await service.fetch(id(TARGET));
235
+ await service.changedFilesForPr(id(TARGET), TARGET, 'alice/a');
236
+ await fs.writeFile(path.join(checkoutOf('alice/a'), 'alice.md'), 'alice edits\n');
237
+ await service.commitFile(id('alice/a'), ALICE, 'knowledge-base/alice.md', 'alice edits');
238
+ await service.push(id('alice/a'), ALICE);
239
+ await service.status(id('alice/a'));
240
+ // A merge stamps the committer's name and email into the config.
241
+ const merged = await service.mergeChangeRequest(
242
+ id(TARGET), 'alice/a', TARGET, { subject: 'alice edits (#1)', body: '' }, ALICE,
243
+ );
244
+ expect(merged.kind).toBe('merged');
245
+ await service.deleteBranch(id(TARGET), 'alice/a', ALICE, { systemCleanup: true });
246
+
247
+ // The subcommand, past leading `-c key=value` / `-C dir` options.
248
+ const sub = (args: string[]) => {
249
+ let i = 0;
250
+ while (args[i] === '-c' || args[i] === '-C') i += 2;
251
+ return { name: args[i], rest: args.slice(i + 1) };
252
+ };
253
+ const of = (pred: (s: { name: string; rest: string[] }) => boolean) => calls.filter((c) => pred(sub(c.args)));
254
+
255
+ // The config is the one file git refuses a second writer of: every write
256
+ // to it (the upstream stamp, the committer's name and email, a branch
257
+ // delete that drops the branch's section) is the administrator's. The
258
+ // refs, packed-refs and the objects git locks itself, so a fetch, a
259
+ // commit, a push, a merge and a read run as any worktree's git would.
260
+ const configRead = (rest: string[]) => rest.some((a) => /^(--get|--get-all|--get-regexp|--list|-l)$/.test(a));
261
+ const shared = [
262
+ ...of((s) => s.name === 'config' && !configRead(s.rest)),
263
+ ...of((s) => s.name === 'branch' && s.rest.some((a) => a === '-D' || a === '-d')),
264
+ ];
265
+ const own = [
266
+ ...of((s) => s.name === 'fetch'),
267
+ ...of((s) => s.name === 'commit'),
268
+ ...of((s) => s.name === 'merge'),
269
+ ...of((s) => s.name === 'push' && !s.rest.includes('-u')),
270
+ ...of((s) => s.name === 'status'),
271
+ ...of((s) => s.name === 'config' && configRead(s.rest)),
272
+ ];
273
+ for (const name of ['fetch', 'config', 'commit', 'merge', 'push', 'status']) {
274
+ expect(calls.some((c) => sub(c.args).name === name), name).toBe(true);
275
+ }
276
+ expect(of((s) => s.name === 'config' && s.rest.includes('--replace-all')).length).toBeGreaterThan(0);
277
+ expect(of((s) => s.name === 'config' && s.rest[0] === 'user.name').length).toBeGreaterThan(0);
278
+ expect(of((s) => s.name === 'config' && s.rest[0] === 'user.email').length).toBeGreaterThan(0);
279
+ expect(of((s) => s.name === 'branch' && s.rest.some((a) => a === '-D' || a === '-d')).length).toBeGreaterThan(0);
280
+ expect(shared.length).toBeGreaterThan(3);
281
+ for (const c of shared) expect(c.held, c.args.join(' ')).toBe(true);
282
+ for (const c of own) expect(c.held, c.args.join(' ')).toBe(false);
283
+ });
284
+ });