@bevel-software/platform-core-backend 0.4.1 → 0.5.1

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 (305) hide show
  1. package/THIRD-PARTY-NOTICES.md +6118 -0
  2. package/dist/core/core-ports.d.ts +15 -0
  3. package/dist/core/core-ports.d.ts.map +1 -1
  4. package/dist/core/core-ports.js.map +1 -1
  5. package/dist/core/create-core-server.d.ts.map +1 -1
  6. package/dist/core/create-core-server.js +40 -6
  7. package/dist/core/create-core-server.js.map +1 -1
  8. package/dist/core/create-core-services.d.ts +17 -1
  9. package/dist/core/create-core-services.d.ts.map +1 -1
  10. package/dist/core/create-core-services.js +153 -29
  11. package/dist/core/create-core-services.js.map +1 -1
  12. package/dist/core-config.d.ts +70 -21
  13. package/dist/core-config.d.ts.map +1 -1
  14. package/dist/core-config.js +134 -58
  15. package/dist/core-config.js.map +1 -1
  16. package/dist/modules/access/access-control.interface.d.ts +6 -0
  17. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  18. package/dist/modules/access/access-control.service.d.ts +74 -5
  19. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  20. package/dist/modules/access/access-control.service.js +116 -15
  21. package/dist/modules/access/access-control.service.js.map +1 -1
  22. package/dist/modules/access/access-declarations.d.ts +93 -0
  23. package/dist/modules/access/access-declarations.d.ts.map +1 -0
  24. package/dist/modules/access/access-declarations.js +168 -0
  25. package/dist/modules/access/access-declarations.js.map +1 -0
  26. package/dist/modules/access/access-mutation.service.js +4 -4
  27. package/dist/modules/access/access-mutation.service.js.map +1 -1
  28. package/dist/modules/access/access-splice.d.ts +16 -1
  29. package/dist/modules/access/access-splice.d.ts.map +1 -1
  30. package/dist/modules/access/access-splice.js +40 -21
  31. package/dist/modules/access/access-splice.js.map +1 -1
  32. package/dist/modules/access/access.routes.d.ts.map +1 -1
  33. package/dist/modules/access/access.routes.js +51 -0
  34. package/dist/modules/access/access.routes.js.map +1 -1
  35. package/dist/modules/access/creator-access.d.ts +8 -0
  36. package/dist/modules/access/creator-access.d.ts.map +1 -1
  37. package/dist/modules/access/creator-access.js +21 -2
  38. package/dist/modules/access/creator-access.js.map +1 -1
  39. package/dist/modules/admin/admin-access.service.d.ts +1 -1
  40. package/dist/modules/admin/admin-access.service.d.ts.map +1 -1
  41. package/dist/modules/admin/admin-access.service.js +3 -2
  42. package/dist/modules/admin/admin-access.service.js.map +1 -1
  43. package/dist/modules/auth/auth.routes.d.ts.map +1 -1
  44. package/dist/modules/auth/auth.routes.js +50 -1
  45. package/dist/modules/auth/auth.routes.js.map +1 -1
  46. package/dist/modules/auth/auth.service.d.ts +39 -10
  47. package/dist/modules/auth/auth.service.d.ts.map +1 -1
  48. package/dist/modules/auth/auth.service.js +38 -16
  49. package/dist/modules/auth/auth.service.js.map +1 -1
  50. package/dist/modules/database/core-schema.d.ts +124 -0
  51. package/dist/modules/database/core-schema.d.ts.map +1 -1
  52. package/dist/modules/database/core-schema.js +45 -0
  53. package/dist/modules/database/core-schema.js.map +1 -1
  54. package/dist/modules/groups/group-provision.service.d.ts +130 -0
  55. package/dist/modules/groups/group-provision.service.d.ts.map +1 -0
  56. package/dist/modules/groups/group-provision.service.js +288 -0
  57. package/dist/modules/groups/group-provision.service.js.map +1 -0
  58. package/dist/modules/groups/groups.contract.d.ts +106 -0
  59. package/dist/modules/groups/groups.contract.d.ts.map +1 -0
  60. package/dist/modules/groups/groups.contract.js +36 -0
  61. package/dist/modules/groups/groups.contract.js.map +1 -0
  62. package/dist/modules/groups/groups.routes.d.ts +42 -0
  63. package/dist/modules/groups/groups.routes.d.ts.map +1 -0
  64. package/dist/modules/groups/groups.routes.js +379 -0
  65. package/dist/modules/groups/groups.routes.js.map +1 -0
  66. package/dist/modules/groups/groups.service.d.ts +60 -0
  67. package/dist/modules/groups/groups.service.d.ts.map +1 -0
  68. package/dist/modules/groups/groups.service.js +172 -0
  69. package/dist/modules/groups/groups.service.js.map +1 -0
  70. package/dist/modules/groups/index.d.ts +7 -0
  71. package/dist/modules/groups/index.d.ts.map +1 -0
  72. package/dist/modules/groups/index.js +6 -0
  73. package/dist/modules/groups/index.js.map +1 -0
  74. package/dist/modules/groups/join-proposals.d.ts +53 -0
  75. package/dist/modules/groups/join-proposals.d.ts.map +1 -0
  76. package/dist/modules/groups/join-proposals.js +67 -0
  77. package/dist/modules/groups/join-proposals.js.map +1 -0
  78. package/dist/modules/groups/join-requests.service.d.ts +81 -0
  79. package/dist/modules/groups/join-requests.service.d.ts.map +1 -0
  80. package/dist/modules/groups/join-requests.service.js +135 -0
  81. package/dist/modules/groups/join-requests.service.js.map +1 -0
  82. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts +20 -0
  83. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -1
  84. package/dist/modules/secrets-vault/secrets-vault.routes.js +59 -11
  85. package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -1
  86. package/dist/modules/settings/deployment-settings.service.d.ts +129 -0
  87. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -0
  88. package/dist/modules/settings/deployment-settings.service.js +368 -0
  89. package/dist/modules/settings/deployment-settings.service.js.map +1 -0
  90. package/dist/modules/settings/setup.routes.d.ts +49 -0
  91. package/dist/modules/settings/setup.routes.d.ts.map +1 -0
  92. package/dist/modules/settings/setup.routes.js +292 -0
  93. package/dist/modules/settings/setup.routes.js.map +1 -0
  94. package/dist/modules/skills/index.d.ts +2 -1
  95. package/dist/modules/skills/index.d.ts.map +1 -1
  96. package/dist/modules/skills/index.js +1 -0
  97. package/dist/modules/skills/index.js.map +1 -1
  98. package/dist/modules/skills/pending-skills.service.d.ts +40 -0
  99. package/dist/modules/skills/pending-skills.service.d.ts.map +1 -0
  100. package/dist/modules/skills/pending-skills.service.js +147 -0
  101. package/dist/modules/skills/pending-skills.service.js.map +1 -0
  102. package/dist/modules/skills/skills.contract.d.ts +32 -3
  103. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  104. package/dist/modules/skills/skills.contract.js +1 -1
  105. package/dist/modules/skills/skills.routes.d.ts +3 -2
  106. package/dist/modules/skills/skills.routes.d.ts.map +1 -1
  107. package/dist/modules/skills/skills.routes.js +18 -1
  108. package/dist/modules/skills/skills.routes.js.map +1 -1
  109. package/dist/modules/skills/skills.service.d.ts +22 -0
  110. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  111. package/dist/modules/skills/skills.service.js +24 -12
  112. package/dist/modules/skills/skills.service.js.map +1 -1
  113. package/dist/modules/tool-manuals/index.d.ts +1 -1
  114. package/dist/modules/tool-manuals/index.d.ts.map +1 -1
  115. package/dist/modules/tool-manuals/index.js.map +1 -1
  116. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +47 -2
  117. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
  118. package/dist/modules/tool-manuals/tool-manuals.contract.js +1 -1
  119. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +4 -0
  120. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
  121. package/dist/modules/tool-manuals/tool-manuals.routes.js +23 -0
  122. package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
  123. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +3 -1
  124. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  125. package/dist/modules/tool-manuals/tool-manuals.service.js +84 -17
  126. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  127. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
  128. package/dist/modules/tool-manuals/tool-manuals.tools.js +6 -2
  129. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  130. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  131. package/dist/modules/workflow/agent-tools/workflow.tools.js +4 -2
  132. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  133. package/dist/modules/workflow/event-bus.d.ts +11 -0
  134. package/dist/modules/workflow/event-bus.d.ts.map +1 -1
  135. package/dist/modules/workflow/event-bus.js +22 -0
  136. package/dist/modules/workflow/event-bus.js.map +1 -1
  137. package/dist/modules/workflow/file-lock.service.d.ts +9 -0
  138. package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
  139. package/dist/modules/workflow/file-lock.service.js +16 -0
  140. package/dist/modules/workflow/file-lock.service.js.map +1 -1
  141. package/dist/modules/workflow/git/branch-name.d.ts +1 -1
  142. package/dist/modules/workflow/git/branch-name.d.ts.map +1 -1
  143. package/dist/modules/workflow/git/branch-name.js +1 -1
  144. package/dist/modules/workflow/git/branch-name.js.map +1 -1
  145. package/dist/modules/workflow/git/git.service.d.ts +31 -2
  146. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  147. package/dist/modules/workflow/git/git.service.js +144 -52
  148. package/dist/modules/workflow/git/git.service.js.map +1 -1
  149. package/dist/modules/workflow/git/pull-request.service.d.ts +12 -6
  150. package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
  151. package/dist/modules/workflow/git/pull-request.service.js +19 -9
  152. package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
  153. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
  154. package/dist/modules/workflow/review-workflow/review-workflow.service.js +12 -3
  155. package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
  156. package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
  157. package/dist/modules/workflow/workflow.routes.js +95 -14
  158. package/dist/modules/workflow/workflow.routes.js.map +1 -1
  159. package/dist/modules/workflow/workflow.service.d.ts +92 -3
  160. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  161. package/dist/modules/workflow/workflow.service.js +381 -108
  162. package/dist/modules/workflow/workflow.service.js.map +1 -1
  163. package/dist/modules/workspace/bevel-ignore.d.ts +5 -0
  164. package/dist/modules/workspace/bevel-ignore.d.ts.map +1 -1
  165. package/dist/modules/workspace/bevel-ignore.js +5 -1
  166. package/dist/modules/workspace/bevel-ignore.js.map +1 -1
  167. package/dist/modules/workspace/kb-seed.interface.d.ts +1 -1
  168. package/dist/modules/workspace/kb-seed.service.d.ts +60 -5
  169. package/dist/modules/workspace/kb-seed.service.d.ts.map +1 -1
  170. package/dist/modules/workspace/kb-seed.service.js +228 -51
  171. package/dist/modules/workspace/kb-seed.service.js.map +1 -1
  172. package/dist/modules/workspace/workspace.routes.d.ts +2 -1
  173. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  174. package/dist/modules/workspace/workspace.routes.js +51 -18
  175. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  176. package/dist/modules/workspace/workspace.service.d.ts +16 -4
  177. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  178. package/dist/modules/workspace/workspace.service.js +36 -9
  179. package/dist/modules/workspace/workspace.service.js.map +1 -1
  180. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  181. package/dist/modules/workspace/workspace.tools.js +8 -3
  182. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  183. package/dist/shared/kb-layout.test.js +2 -2
  184. package/dist/test-setup.d.ts +2 -0
  185. package/dist/test-setup.d.ts.map +1 -0
  186. package/dist/test-setup.js +18 -0
  187. package/dist/test-setup.js.map +1 -0
  188. package/kb-template/.bevelignore +6 -2
  189. package/kb-template/AGENTS.md +272 -0
  190. package/kb-template/KnowledgeBase/How to get started.md +65 -0
  191. package/kb-template/access.md +36 -36
  192. package/migrations/0002_user_onboarding_done.sql +1 -0
  193. package/migrations/0003_deployment_settings.sql +9 -0
  194. package/migrations/meta/0001_snapshot.json +1486 -0
  195. package/migrations/meta/0002_snapshot.json +1493 -0
  196. package/migrations/meta/0003_snapshot.json +1552 -0
  197. package/migrations/meta/_journal.json +15 -1
  198. package/package.json +5 -4
  199. package/src/__tests__/branch-model.test.ts +79 -0
  200. package/src/__tests__/core-config.admin.test.ts +114 -0
  201. package/src/__tests__/database-url.test.ts +70 -0
  202. package/src/core/core-ports.ts +15 -0
  203. package/src/core/create-core-server.ts +55 -6
  204. package/src/core/create-core-services.ts +201 -36
  205. package/src/core-config.ts +139 -59
  206. package/src/modules/access/__tests__/access-declarations.test.ts +195 -0
  207. package/src/modules/access/__tests__/access-md-format.test.ts +191 -0
  208. package/src/modules/access/__tests__/access-splice.test.ts +21 -0
  209. package/src/modules/access/__tests__/access.routes.overrides.test.ts +214 -0
  210. package/src/modules/access/access-control.interface.ts +11 -0
  211. package/src/modules/access/access-control.service.ts +113 -15
  212. package/src/modules/access/access-declarations.ts +251 -0
  213. package/src/modules/access/access-mutation.service.ts +4 -4
  214. package/src/modules/access/access-splice.ts +64 -21
  215. package/src/modules/access/access.routes.ts +61 -0
  216. package/src/modules/access/creator-access.ts +22 -2
  217. package/src/modules/admin/admin-access.service.ts +4 -3
  218. package/src/modules/auth/__tests__/auth.service.test.ts +57 -0
  219. package/src/modules/auth/__tests__/onboarding-done.route.test.ts +147 -0
  220. package/src/modules/auth/auth.routes.ts +50 -1
  221. package/src/modules/auth/auth.service.ts +55 -17
  222. package/src/modules/database/core-schema.ts +46 -0
  223. package/src/modules/groups/__tests__/group-index.service.test.ts +225 -0
  224. package/src/modules/groups/__tests__/group-provision.service.test.ts +247 -0
  225. package/src/modules/groups/__tests__/groups.routes.test.ts +493 -0
  226. package/src/modules/groups/__tests__/join-proposals.test.ts +101 -0
  227. package/src/modules/groups/__tests__/join-requests.service.test.ts +175 -0
  228. package/src/modules/groups/group-provision.service.ts +328 -0
  229. package/src/modules/groups/groups.contract.ts +107 -0
  230. package/src/modules/groups/groups.routes.ts +433 -0
  231. package/src/modules/groups/groups.service.ts +193 -0
  232. package/src/modules/groups/index.ts +17 -0
  233. package/src/modules/groups/join-proposals.ts +114 -0
  234. package/src/modules/groups/join-requests.service.ts +185 -0
  235. package/src/modules/secrets-vault/__tests__/oauth-return-to.route.test.ts +283 -0
  236. package/src/modules/secrets-vault/secrets-vault.routes.ts +68 -12
  237. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +256 -0
  238. package/src/modules/settings/__tests__/setup.routes.test.ts +201 -0
  239. package/src/modules/settings/deployment-settings.service.ts +419 -0
  240. package/src/modules/settings/setup.routes.ts +317 -0
  241. package/src/modules/skills/__tests__/pending-skills.service.test.ts +240 -0
  242. package/src/modules/skills/__tests__/skills.service.test.ts +13 -13
  243. package/src/modules/skills/index.ts +3 -0
  244. package/src/modules/skills/pending-skills.service.ts +168 -0
  245. package/src/modules/skills/skills.contract.ts +34 -3
  246. package/src/modules/skills/skills.routes.ts +23 -2
  247. package/src/modules/skills/skills.service.ts +24 -16
  248. package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +262 -0
  249. package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +2 -2
  250. package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +61 -23
  251. package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +156 -156
  252. package/src/modules/tool-manuals/index.ts +2 -0
  253. package/src/modules/tool-manuals/tool-manuals.contract.ts +50 -2
  254. package/src/modules/tool-manuals/tool-manuals.routes.ts +21 -0
  255. package/src/modules/tool-manuals/tool-manuals.service.ts +86 -17
  256. package/src/modules/tool-manuals/tool-manuals.tools.ts +206 -202
  257. package/src/modules/workflow/__tests__/event-bus.test.ts +35 -0
  258. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +251 -5
  259. package/src/modules/workflow/agent-tools/workflow.tools.ts +4 -2
  260. package/src/modules/workflow/event-bus.ts +25 -0
  261. package/src/modules/workflow/file-lock.service.ts +17 -0
  262. package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +9 -49
  263. package/src/modules/workflow/git/__tests__/git.service.changedFilesForPr.test.ts +101 -0
  264. package/src/modules/workflow/git/__tests__/git.service.commitFile.test.ts +219 -211
  265. package/src/modules/workflow/git/__tests__/git.service.fileContentsAtCommit.test.ts +132 -0
  266. package/src/modules/workflow/git/__tests__/git.service.logForFile.test.ts +4 -2
  267. package/src/modules/workflow/git/__tests__/git.service.postMergePull.test.ts +275 -272
  268. package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +5 -0
  269. package/src/modules/workflow/git/__tests__/pull-request.service.viewer-can-cancel.test.ts +44 -0
  270. package/src/modules/workflow/git/branch-name.ts +5 -1
  271. package/src/modules/workflow/git/git.service.ts +159 -62
  272. package/src/modules/workflow/git/pull-request.service.ts +22 -11
  273. package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +4 -1
  274. package/src/modules/workflow/review-workflow/review-workflow.service.ts +12 -3
  275. package/src/modules/workflow/workflow.routes.ts +91 -13
  276. package/src/modules/workflow/workflow.service.ts +353 -13
  277. package/src/modules/workspace/__tests__/kb-seed.service.test.ts +255 -24
  278. package/src/modules/workspace/__tests__/workspace.routes.create-grant.test.ts +236 -233
  279. package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +179 -176
  280. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +320 -303
  281. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +337 -276
  282. package/src/modules/workspace/__tests__/workspace.tools.test.ts +14 -6
  283. package/src/modules/workspace/bevel-ignore.ts +66 -62
  284. package/src/modules/workspace/kb-seed.interface.ts +1 -1
  285. package/src/modules/workspace/kb-seed.service.ts +247 -53
  286. package/src/modules/workspace/workspace.routes.ts +54 -18
  287. package/src/modules/workspace/workspace.service.ts +45 -7
  288. package/src/modules/workspace/workspace.tools.ts +922 -917
  289. package/src/shared/__tests__/frontmatter-id.test.ts +53 -0
  290. package/src/shared/__tests__/join-request.test.ts +96 -0
  291. package/src/shared/__tests__/kb-layout.group.test.ts +45 -0
  292. package/src/shared/kb-layout.test.ts +2 -2
  293. package/src/test-setup.ts +18 -0
  294. package/kb-template/Agents/README.md +0 -38
  295. package/kb-template/CLAUDE.md +0 -419
  296. package/kb-template/Data/.gitkeep +0 -0
  297. package/kb-template/Data/README.md +0 -39
  298. package/kb-template/KnowledgeBase/README.md +0 -37
  299. package/kb-template/Pipelines/.gitkeep +0 -0
  300. package/kb-template/Pipelines/README.md +0 -38
  301. package/kb-template/Skills/.gitkeep +0 -0
  302. package/kb-template/Skills/README.md +0 -32
  303. package/kb-template/Tools/.gitkeep +0 -0
  304. package/kb-template/Tools/README.md +0 -42
  305. /package/kb-template/{Agents → Groups}/.gitkeep +0 -0
@@ -1,211 +1,219 @@
1
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
- import { execFile } from 'node:child_process';
3
- import { promisify } from 'node:util';
4
- import fs from 'node:fs/promises';
5
- import path from 'node:path';
6
- import os from 'node:os';
7
- import type { WorkspaceService } from '../../../workspace/workspace.service.js';
8
- import { WorkflowHooks } from '../../workflow-hooks.js';
9
- import { GitService } from '../git.service.js';
10
-
11
- /**
12
- * `GitService.commitFile` — the `skipValidator` flag. The reject flow passes
13
- * `skipValidator: true` because it re-commits a pre-agent baseline that was
14
- * already validated when first written; running the (advisory) KB validator
15
- * there is pure latency.
16
- */
17
-
18
- const execFileAsync = promisify(execFile);
19
- const PROCESS_MAP_DIR = 'knowledge-base';
20
- const USER = { id: 'u-alice', name: 'Alice', email: 'alice@bevel.software' };
21
-
22
- async function runGit(cwd: string, args: string[]): Promise<void> {
23
- await execFileAsync('git', args, {
24
- cwd,
25
- env: {
26
- ...process.env,
27
- GIT_AUTHOR_NAME: 'Test',
28
- GIT_AUTHOR_EMAIL: 't@x.com',
29
- GIT_COMMITTER_NAME: 'Test',
30
- GIT_COMMITTER_EMAIL: 't@x.com',
31
- },
32
- });
33
- }
34
-
35
- async function seedWorkspace(
36
- root: string,
37
- workspaceId: string,
38
- ): Promise<{ workspaceDir: string; repo: string }> {
39
- const workspaceDir = path.join(root, workspaceId);
40
- const repo = path.join(workspaceDir, PROCESS_MAP_DIR);
41
- await fs.mkdir(repo, { recursive: true });
42
- // A feature branch — not a protected branch — so `commitFile` skips the
43
- // protected-branch access gate; this test is only about the validator.
44
- await runGit(repo, ['init', '-b', 'feature-test']);
45
- await runGit(repo, ['config', 'user.email', 'test@bevel.local']);
46
- await runGit(repo, ['config', 'user.name', 'Test Runner']);
47
- // An initial commit so HEAD exists.
48
- await fs.writeFile(path.join(repo, 'seed.md'), 'seed\n');
49
- await runGit(repo, ['add', 'seed.md']);
50
- await runGit(repo, ['commit', '-m', 'seed']);
51
- return { workspaceDir, repo };
52
- }
53
-
54
- function stubWorkspaceService(workspaceId: string, workspaceDir: string): WorkspaceService {
55
- return {
56
- getWorkspacePath: async (id: string) => {
57
- if (id !== workspaceId) throw new Error(`unexpected workspace ${id}`);
58
- return workspaceDir;
59
- },
60
- } as unknown as WorkspaceService;
61
- }
62
-
63
- function makeValidator(): { hooks: WorkflowHooks; runValidation: ReturnType<typeof vi.fn> } {
64
- const report = { ok: true, mustFix: [], warnings: [], rawOutput: '' };
65
- const runValidation = vi.fn(async () => report);
66
- // Registered as a commit-validation hook — the seam GitService consults
67
- // where it used to call the injected validator.
68
- const hooks = new WorkflowHooks();
69
- hooks.onCommitValidation(() => runValidation());
70
- return { hooks, runValidation };
71
- }
72
-
73
- describe('GitService.commitFile — skipValidator', () => {
74
- let root: string;
75
- const workspaceId = 'ws-commitfile-1';
76
-
77
- beforeEach(async () => {
78
- root = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-git-commitfile-'));
79
- });
80
- afterEach(async () => {
81
- await fs.rm(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
82
- });
83
-
84
- it('skipValidator=true commits the file WITHOUT running the KB validator', async () => {
85
- const { workspaceDir, repo } = await seedWorkspace(root, workspaceId);
86
- const { hooks, runValidation } = makeValidator();
87
- const svc = new GitService(
88
- stubWorkspaceService(workspaceId, workspaceDir),
89
- hooks,
90
- 'knowledge-base',
91
- );
92
-
93
- await fs.writeFile(path.join(repo, 'Foo.md'), 'reverted content\n');
94
- const change = await svc.commitFile(workspaceId, USER, 'Foo.md', 'Revert agent change', true);
95
-
96
- expect(runValidation).not.toHaveBeenCalled();
97
- expect(change?.sha).toBeTruthy();
98
- expect(change?.subject).toBe('Revert agent change');
99
- });
100
-
101
- it('runs the KB validator when skipValidator is omitted', async () => {
102
- const { workspaceDir, repo } = await seedWorkspace(root, workspaceId);
103
- const { hooks, runValidation } = makeValidator();
104
- const svc = new GitService(
105
- stubWorkspaceService(workspaceId, workspaceDir),
106
- hooks,
107
- 'knowledge-base',
108
- );
109
-
110
- await fs.writeFile(path.join(repo, 'Bar.md'), 'new content\n');
111
- const change = await svc.commitFile(workspaceId, USER, 'Bar.md', 'normal commit');
112
-
113
- expect(runValidation).toHaveBeenCalledTimes(1);
114
- expect(change?.sha).toBeTruthy();
115
- });
116
-
117
- // Regression: filenames with bracketed prefixes (`[Approved] …`, `[New] …`,
118
- // `[Updated 03.09.2025] …`) used to be rejected at `assertValidRelativePath`
119
- // because their `[` / `]` characters would be interpreted by `git add -- <path>`
120
- // as pathspec globs. The fix is `GIT_LITERAL_PATHSPECS=1` on the subprocess
121
- // env (set in `GitService.git()`), which forces literal matching. This test
122
- // commits one of the user-reported offenders end-to-end through real git so
123
- // a regression in the env-var plumbing would surface as a subprocess failure
124
- // here rather than as a 400 on the upload route.
125
- it('commits a filename containing pathspec glob characters (regression: [Approved] / [New] prefixes)', async () => {
126
- const { workspaceDir, repo } = await seedWorkspace(root, workspaceId);
127
- const { hooks } = makeValidator();
128
- const svc = new GitService(
129
- stubWorkspaceService(workspaceId, workspaceDir),
130
- hooks,
131
- 'knowledge-base',
132
- );
133
-
134
- const relPath = 'Purchasing/[Updated 03.09.2025] Purchasing_How to search_Product detail page.md';
135
- await fs.mkdir(path.join(repo, 'Purchasing'), { recursive: true });
136
- await fs.writeFile(path.join(repo, relPath), 'content\n');
137
-
138
- const change = await svc.commitFile(workspaceId, USER, relPath);
139
-
140
- expect(change?.sha).toBeTruthy();
141
- // Working tree must end up clean — `git add -- <path>` literally matched
142
- // the bracketed name, so there's no leftover untracked file ghost on disk.
143
- const { stdout: status } = await execFileAsync('git', ['-C', repo, 'status', '--porcelain'], {
144
- env: { ...process.env, GIT_LITERAL_PATHSPECS: '1' },
145
- });
146
- expect(status.trim()).toBe('');
147
- });
148
-
149
- // Regression: deep KB paths (≥194 chars) made the default `Update <path>`
150
- // subject exceed the 200-char limit, so the lock-release auto-commit
151
- // propagated `commit summary must be ≤ 200 characters` back through the
152
- // agent's write tool. The default now falls back to the basename when the
153
- // full path doesn't fit.
154
- it('truncates the default subject when the path makes "Update <path>" exceed 200 chars', async () => {
155
- const { workspaceDir, repo } = await seedWorkspace(root, workspaceId);
156
- const { hooks } = makeValidator();
157
- const svc = new GitService(
158
- stubWorkspaceService(workspaceId, workspaceDir),
159
- hooks,
160
- 'knowledge-base',
161
- );
162
-
163
- const deepDir = path.join(
164
- 'Product',
165
- 'Knowledge',
166
- '1. Service Commitment Management',
167
- '1.1 In - Architecture Layering (AL)',
168
- '0-Governance',
169
- '0.5-Layer-Group-Specific-Governance-Principles',
170
- );
171
- const fileName = '0.5.5-Buyer-Side-Commercial-Agreement-Governance.md';
172
- const relPath = path.posix.join(deepDir.split(path.sep).join('/'), fileName);
173
- expect(`Update ${relPath}`.length).toBeGreaterThan(200);
174
-
175
- await fs.mkdir(path.join(repo, deepDir), { recursive: true });
176
- await fs.writeFile(path.join(repo, deepDir, fileName), 'content\n');
177
-
178
- const change = await svc.commitFile(workspaceId, USER, relPath);
179
-
180
- expect(change?.subject).toBe(`Update ${fileName}`);
181
- expect(change?.subject.length).toBeLessThanOrEqual(200);
182
- });
183
-
184
- // Regression: pathspec literal handling. `git checkout HEAD -- '[Approved] x.md'`
185
- // and `git ls-files --error-unmatch -- '[Approved] x.md'` would interpret the
186
- // brackets as a character-class glob, so discardPath would either silently
187
- // no-op or fail to revert the on-disk content. Belongs alongside the
188
- // commitFile regression because no dedicated discardPath test file exists
189
- // yet — its full surface is covered transitively elsewhere.
190
- it('discardPath reverts an unstaged modification to a bracketed-name file', async () => {
191
- const { workspaceDir, repo } = await seedWorkspace(root, workspaceId);
192
- const { hooks } = makeValidator();
193
- const svc = new GitService(
194
- stubWorkspaceService(workspaceId, workspaceDir),
195
- hooks,
196
- 'knowledge-base',
197
- );
198
-
199
- const relPath = '[Approved] config.md';
200
- const committed = 'committed content\n';
201
- await fs.writeFile(path.join(repo, relPath), committed);
202
- await svc.commitFile(workspaceId, USER, relPath, 'add bracketed config');
203
-
204
- // Dirty the working tree.
205
- await fs.writeFile(path.join(repo, relPath), 'dirty content\n');
206
- await svc.discardPath(workspaceId, relPath);
207
-
208
- const restored = await fs.readFile(path.join(repo, relPath), 'utf-8');
209
- expect(restored).toBe(committed);
210
- });
211
- });
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { execFile } from 'node:child_process';
3
+ import { promisify } from 'node:util';
4
+ import fs from 'node:fs/promises';
5
+ import path from 'node:path';
6
+ import os from 'node:os';
7
+ import type { WorkspaceService } from '../../../workspace/workspace.service.js';
8
+ import { WorkflowHooks } from '../../workflow-hooks.js';
9
+ import { GitService } from '../git.service.js';
10
+
11
+ /**
12
+ * `GitService.commitFile` — the `skipValidator` flag. The reject flow passes
13
+ * `skipValidator: true` because it re-commits a pre-agent baseline that was
14
+ * already validated when first written; running the (advisory) KB validator
15
+ * there is pure latency.
16
+ */
17
+
18
+ const execFileAsync = promisify(execFile);
19
+ const PROCESS_MAP_DIR = 'knowledge-base';
20
+ const USER = { id: 'u-alice', name: 'Alice', email: 'alice@bevel.software' };
21
+
22
+ async function runGit(cwd: string, args: string[]): Promise<void> {
23
+ await execFileAsync('git', args, {
24
+ cwd,
25
+ env: {
26
+ ...process.env,
27
+ GIT_AUTHOR_NAME: 'Test',
28
+ GIT_AUTHOR_EMAIL: 't@x.com',
29
+ GIT_COMMITTER_NAME: 'Test',
30
+ GIT_COMMITTER_EMAIL: 't@x.com',
31
+ },
32
+ });
33
+ }
34
+
35
+ async function seedWorkspace(
36
+ root: string,
37
+ workspaceId: string,
38
+ ): Promise<{ workspaceDir: string; repo: string }> {
39
+ const workspaceDir = path.join(root, workspaceId);
40
+ const repo = path.join(workspaceDir, PROCESS_MAP_DIR);
41
+ await fs.mkdir(repo, { recursive: true });
42
+ // A feature branch — not a protected branch — so `commitFile` skips the
43
+ // protected-branch access gate; this test is only about the validator.
44
+ await runGit(repo, ['init', '-b', 'feature-test']);
45
+ await runGit(repo, ['config', 'user.email', 'test@bevel.local']);
46
+ await runGit(repo, ['config', 'user.name', 'Test Runner']);
47
+ // Repo-local so GitService's own git children (which run with the ambient
48
+ // env) see them too: autocrlf off keeps `git checkout` (discardPath) from
49
+ // rewriting the LF fixtures as CRLF under Git-for-Windows' system-level
50
+ // `core.autocrlf=true`; longpaths lifts Windows' 260-char MAX_PATH, which
51
+ // the deep-KB-path subject-truncation test exceeds. Both are no-ops on
52
+ // Linux/CI — they pin the repo to the behaviour the assertions assume.
53
+ await runGit(repo, ['config', 'core.autocrlf', 'false']);
54
+ await runGit(repo, ['config', 'core.longpaths', 'true']);
55
+ // An initial commit so HEAD exists.
56
+ await fs.writeFile(path.join(repo, 'seed.md'), 'seed\n');
57
+ await runGit(repo, ['add', 'seed.md']);
58
+ await runGit(repo, ['commit', '-m', 'seed']);
59
+ return { workspaceDir, repo };
60
+ }
61
+
62
+ function stubWorkspaceService(workspaceId: string, workspaceDir: string): WorkspaceService {
63
+ return {
64
+ getWorkspacePath: async (id: string) => {
65
+ if (id !== workspaceId) throw new Error(`unexpected workspace ${id}`);
66
+ return workspaceDir;
67
+ },
68
+ } as unknown as WorkspaceService;
69
+ }
70
+
71
+ function makeValidator(): { hooks: WorkflowHooks; runValidation: ReturnType<typeof vi.fn> } {
72
+ const report = { ok: true, mustFix: [], warnings: [], rawOutput: '' };
73
+ const runValidation = vi.fn(async () => report);
74
+ // Registered as a commit-validation hook — the seam GitService consults
75
+ // where it used to call the injected validator.
76
+ const hooks = new WorkflowHooks();
77
+ hooks.onCommitValidation(() => runValidation());
78
+ return { hooks, runValidation };
79
+ }
80
+
81
+ describe('GitService.commitFile — skipValidator', () => {
82
+ let root: string;
83
+ const workspaceId = 'ws-commitfile-1';
84
+
85
+ beforeEach(async () => {
86
+ root = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-git-commitfile-'));
87
+ });
88
+ afterEach(async () => {
89
+ await fs.rm(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
90
+ });
91
+
92
+ it('skipValidator=true commits the file WITHOUT running the KB validator', async () => {
93
+ const { workspaceDir, repo } = await seedWorkspace(root, workspaceId);
94
+ const { hooks, runValidation } = makeValidator();
95
+ const svc = new GitService(
96
+ stubWorkspaceService(workspaceId, workspaceDir),
97
+ hooks,
98
+ 'knowledge-base',
99
+ );
100
+
101
+ await fs.writeFile(path.join(repo, 'Foo.md'), 'reverted content\n');
102
+ const change = await svc.commitFile(workspaceId, USER, 'Foo.md', 'Revert agent change', true);
103
+
104
+ expect(runValidation).not.toHaveBeenCalled();
105
+ expect(change?.sha).toBeTruthy();
106
+ expect(change?.subject).toBe('Revert agent change');
107
+ });
108
+
109
+ it('runs the KB validator when skipValidator is omitted', async () => {
110
+ const { workspaceDir, repo } = await seedWorkspace(root, workspaceId);
111
+ const { hooks, runValidation } = makeValidator();
112
+ const svc = new GitService(
113
+ stubWorkspaceService(workspaceId, workspaceDir),
114
+ hooks,
115
+ 'knowledge-base',
116
+ );
117
+
118
+ await fs.writeFile(path.join(repo, 'Bar.md'), 'new content\n');
119
+ const change = await svc.commitFile(workspaceId, USER, 'Bar.md', 'normal commit');
120
+
121
+ expect(runValidation).toHaveBeenCalledTimes(1);
122
+ expect(change?.sha).toBeTruthy();
123
+ });
124
+
125
+ // Regression: filenames with bracketed prefixes (`[Approved] …`, `[New] …`,
126
+ // `[Updated 03.09.2025] …`) used to be rejected at `assertValidRelativePath`
127
+ // because their `[` / `]` characters would be interpreted by `git add -- <path>`
128
+ // as pathspec globs. The fix is `GIT_LITERAL_PATHSPECS=1` on the subprocess
129
+ // env (set in `GitService.git()`), which forces literal matching. This test
130
+ // commits one of the user-reported offenders end-to-end through real git so
131
+ // a regression in the env-var plumbing would surface as a subprocess failure
132
+ // here rather than as a 400 on the upload route.
133
+ it('commits a filename containing pathspec glob characters (regression: [Approved] / [New] prefixes)', async () => {
134
+ const { workspaceDir, repo } = await seedWorkspace(root, workspaceId);
135
+ const { hooks } = makeValidator();
136
+ const svc = new GitService(
137
+ stubWorkspaceService(workspaceId, workspaceDir),
138
+ hooks,
139
+ 'knowledge-base',
140
+ );
141
+
142
+ const relPath = 'Purchasing/[Updated 03.09.2025] Purchasing_How to search_Product detail page.md';
143
+ await fs.mkdir(path.join(repo, 'Purchasing'), { recursive: true });
144
+ await fs.writeFile(path.join(repo, relPath), 'content\n');
145
+
146
+ const change = await svc.commitFile(workspaceId, USER, relPath);
147
+
148
+ expect(change?.sha).toBeTruthy();
149
+ // Working tree must end up clean — `git add -- <path>` literally matched
150
+ // the bracketed name, so there's no leftover untracked file ghost on disk.
151
+ const { stdout: status } = await execFileAsync('git', ['-C', repo, 'status', '--porcelain'], {
152
+ env: { ...process.env, GIT_LITERAL_PATHSPECS: '1' },
153
+ });
154
+ expect(status.trim()).toBe('');
155
+ });
156
+
157
+ // Regression: deep KB paths (≥194 chars) made the default `Update <path>`
158
+ // subject exceed the 200-char limit, so the lock-release auto-commit
159
+ // propagated `commit summary must be ≤ 200 characters` back through the
160
+ // agent's write tool. The default now falls back to the basename when the
161
+ // full path doesn't fit.
162
+ it('truncates the default subject when the path makes "Update <path>" exceed 200 chars', async () => {
163
+ const { workspaceDir, repo } = await seedWorkspace(root, workspaceId);
164
+ const { hooks } = makeValidator();
165
+ const svc = new GitService(
166
+ stubWorkspaceService(workspaceId, workspaceDir),
167
+ hooks,
168
+ 'knowledge-base',
169
+ );
170
+
171
+ const deepDir = path.join(
172
+ 'Product',
173
+ 'Knowledge',
174
+ '1. Service Commitment Management',
175
+ '1.1 In - Architecture Layering (AL)',
176
+ '0-Governance',
177
+ '0.5-Layer-Group-Specific-Governance-Principles',
178
+ );
179
+ const fileName = '0.5.5-Buyer-Side-Commercial-Agreement-Governance.md';
180
+ const relPath = path.posix.join(deepDir.split(path.sep).join('/'), fileName);
181
+ expect(`Update ${relPath}`.length).toBeGreaterThan(200);
182
+
183
+ await fs.mkdir(path.join(repo, deepDir), { recursive: true });
184
+ await fs.writeFile(path.join(repo, deepDir, fileName), 'content\n');
185
+
186
+ const change = await svc.commitFile(workspaceId, USER, relPath);
187
+
188
+ expect(change?.subject).toBe(`Update ${fileName}`);
189
+ expect(change?.subject.length).toBeLessThanOrEqual(200);
190
+ });
191
+
192
+ // Regression: pathspec literal handling. `git checkout HEAD -- '[Approved] x.md'`
193
+ // and `git ls-files --error-unmatch -- '[Approved] x.md'` would interpret the
194
+ // brackets as a character-class glob, so discardPath would either silently
195
+ // no-op or fail to revert the on-disk content. Belongs alongside the
196
+ // commitFile regression because no dedicated discardPath test file exists
197
+ // yet — its full surface is covered transitively elsewhere.
198
+ it('discardPath reverts an unstaged modification to a bracketed-name file', async () => {
199
+ const { workspaceDir, repo } = await seedWorkspace(root, workspaceId);
200
+ const { hooks } = makeValidator();
201
+ const svc = new GitService(
202
+ stubWorkspaceService(workspaceId, workspaceDir),
203
+ hooks,
204
+ 'knowledge-base',
205
+ );
206
+
207
+ const relPath = '[Approved] config.md';
208
+ const committed = 'committed content\n';
209
+ await fs.writeFile(path.join(repo, relPath), committed);
210
+ await svc.commitFile(workspaceId, USER, relPath, 'add bracketed config');
211
+
212
+ // Dirty the working tree.
213
+ await fs.writeFile(path.join(repo, relPath), 'dirty content\n');
214
+ await svc.discardPath(workspaceId, relPath);
215
+
216
+ const restored = await fs.readFile(path.join(repo, relPath), 'utf-8');
217
+ expect(restored).toBe(committed);
218
+ });
219
+ });
@@ -0,0 +1,132 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { execFile } from 'node:child_process';
3
+ import { promisify } from 'node:util';
4
+ import fs from 'node:fs/promises';
5
+ import path from 'node:path';
6
+ import os from 'node:os';
7
+ import type { WorkspaceService } from '../../../workspace/workspace.service.js';
8
+ import { WorkflowHooks } from '../../workflow-hooks.js';
9
+ import { GitService } from '../git.service.js';
10
+
11
+ const execFileAsync = promisify(execFile);
12
+ const PROCESS_MAP_DIR = 'knowledge-base';
13
+
14
+ async function mkTmpRoot(): Promise<string> {
15
+ return fs.mkdtemp(path.join(os.tmpdir(), 'bevel-git-file-at-commit-'));
16
+ }
17
+
18
+ async function runGit(cwd: string, args: string[]): Promise<void> {
19
+ await execFileAsync('git', args, {
20
+ cwd,
21
+ env: {
22
+ ...process.env,
23
+ GIT_AUTHOR_NAME: 'Test',
24
+ GIT_AUTHOR_EMAIL: 't@x.com',
25
+ GIT_COMMITTER_NAME: 'Test',
26
+ GIT_COMMITTER_EMAIL: 't@x.com',
27
+ },
28
+ });
29
+ }
30
+
31
+ async function gitStdout(cwd: string, args: string[]): Promise<string> {
32
+ const { stdout } = await execFileAsync('git', args, { cwd });
33
+ return stdout.toString().trim();
34
+ }
35
+
36
+ async function seedWorkspace(
37
+ root: string,
38
+ workspaceId: string,
39
+ ): Promise<{ workspaceDir: string; repo: string }> {
40
+ const workspaceDir = path.join(root, workspaceId);
41
+ const repo = path.join(workspaceDir, PROCESS_MAP_DIR);
42
+ await fs.mkdir(repo, { recursive: true });
43
+ await runGit(repo, ['init', '-b', 'current-company-state']);
44
+ await runGit(repo, ['config', 'user.email', 't@x.com']);
45
+ await runGit(repo, ['config', 'user.name', 'Test']);
46
+ return { workspaceDir, repo };
47
+ }
48
+
49
+ async function commitFile(
50
+ repo: string,
51
+ relativePath: string,
52
+ contents: string,
53
+ subject: string,
54
+ ): Promise<string> {
55
+ const absolutePath = path.join(repo, relativePath);
56
+ await fs.mkdir(path.dirname(absolutePath), { recursive: true });
57
+ await fs.writeFile(absolutePath, contents);
58
+ await runGit(repo, ['add', relativePath]);
59
+ await runGit(repo, ['commit', '-m', subject]);
60
+ return gitStdout(repo, ['rev-parse', 'HEAD']);
61
+ }
62
+
63
+ function stubWorkspaceService(workspaceId: string, workspaceDir: string): WorkspaceService {
64
+ return {
65
+ getWorkspacePath: async (id: string) => {
66
+ if (id !== workspaceId) throw new Error(`unexpected workspace ${id}`);
67
+ return workspaceDir;
68
+ },
69
+ } as unknown as WorkspaceService;
70
+ }
71
+
72
+ describe('GitService.fileContentsAtCommit', () => {
73
+ let root: string;
74
+ const workspaceId = 'ws-file-at-commit-1';
75
+
76
+ beforeEach(async () => {
77
+ root = await mkTmpRoot();
78
+ });
79
+
80
+ afterEach(async () => {
81
+ await fs.rm(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
82
+ });
83
+
84
+ it('covers root-commit add, modify, later add, and delete in one history', async () => {
85
+ const { workspaceDir, repo } = await seedWorkspace(root, workspaceId);
86
+ const svc = new GitService(
87
+ stubWorkspaceService(workspaceId, workspaceDir),
88
+ new WorkflowHooks(),
89
+ 'knowledge-base',
90
+ );
91
+
92
+ const rootAdd = await commitFile(repo, 'Knowledge/Foo.md', '# one\n', 'add foo');
93
+ const modify = await commitFile(repo, 'Knowledge/Foo.md', '# two\n', 'edit foo');
94
+ const laterAdd = await commitFile(repo, 'Knowledge/Bar.md', '# bar\n', 'add bar');
95
+ await runGit(repo, ['rm', 'Knowledge/Foo.md']);
96
+ await runGit(repo, ['commit', '-m', 'delete foo']);
97
+ const del = await gitStdout(repo, ['rev-parse', 'HEAD']);
98
+
99
+ // Root commit: no parent side at all → baseline null.
100
+ expect(await svc.fileContentsAtCommit(workspaceId, 'Knowledge/Foo.md', rootAdd)).toEqual({
101
+ baseline: null,
102
+ current: '# one\n',
103
+ });
104
+ // Plain modification: both sides present.
105
+ expect(await svc.fileContentsAtCommit(workspaceId, 'Knowledge/Foo.md', modify)).toEqual({
106
+ baseline: '# one\n',
107
+ current: '# two\n',
108
+ });
109
+ // File added mid-history: parent exists, file absent there → baseline null.
110
+ expect(await svc.fileContentsAtCommit(workspaceId, 'Knowledge/Bar.md', laterAdd)).toEqual({
111
+ baseline: null,
112
+ current: '# bar\n',
113
+ });
114
+ // Deletion: absent at the commit itself → current null.
115
+ expect(await svc.fileContentsAtCommit(workspaceId, 'Knowledge/Foo.md', del)).toEqual({
116
+ baseline: '# two\n',
117
+ current: null,
118
+ });
119
+ });
120
+
121
+ it('rejects an invalid sha', async () => {
122
+ const { workspaceDir } = await seedWorkspace(root, workspaceId);
123
+ const svc = new GitService(
124
+ stubWorkspaceService(workspaceId, workspaceDir),
125
+ new WorkflowHooks(),
126
+ 'knowledge-base',
127
+ );
128
+ await expect(
129
+ svc.fileContentsAtCommit(workspaceId, 'Knowledge/Foo.md', 'not-a-sha'),
130
+ ).rejects.toThrow(/invalid commit sha/);
131
+ });
132
+ });
@@ -191,8 +191,10 @@ describe('GitService.logForFile', () => {
191
191
  const limited = await svc.logForFile(workspaceId, 'Knowledge/Foo.md', 5);
192
192
  expect(limited).toHaveLength(5);
193
193
  // Timeout is generous because Windows process-spawn dominates here — on
194
- // Linux CI this finishes in ~3 s.
195
- }, 60000);
194
+ // Linux CI this finishes in ~3 s. Under a full-suite parallel run on
195
+ // Windows the 220 spawns contend with every other git suite, so the cap
196
+ // has to absorb that load, not just the isolated cost.
197
+ }, 180_000);
196
198
 
197
199
  // Regression: pathspec literal handling. `git log -- '[Approved] foo.md'`
198
200
  // would otherwise interpret `[Approved]` as a character-class glob and