@bevel-software/platform-core-backend 0.7.4 → 0.8.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 (246) hide show
  1. package/LICENSE +202 -202
  2. package/THIRD-PARTY-NOTICES.md +428 -454
  3. package/dist/core/core-ports.d.ts +1 -1
  4. package/dist/core/create-core-server.d.ts.map +1 -1
  5. package/dist/core/create-core-server.js +51 -7
  6. package/dist/core/create-core-server.js.map +1 -1
  7. package/dist/core/create-core-services.d.ts +5 -3
  8. package/dist/core/create-core-services.d.ts.map +1 -1
  9. package/dist/core/create-core-services.js +38 -21
  10. package/dist/core/create-core-services.js.map +1 -1
  11. package/dist/modules/access/access-control.interface.d.ts +8 -7
  12. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  13. package/dist/modules/access/access-control.service.d.ts +1 -1
  14. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  15. package/dist/modules/access/access-control.service.js +6 -6
  16. package/dist/modules/access/access-control.service.js.map +1 -1
  17. package/dist/modules/access/access-declarations.d.ts +5 -5
  18. package/dist/modules/access/access-declarations.js +3 -3
  19. package/dist/modules/access/access-mutation.service.d.ts +3 -3
  20. package/dist/modules/access/access-mutation.service.d.ts.map +1 -1
  21. package/dist/modules/access/access-mutation.service.js +3 -3
  22. package/dist/modules/access/access-mutation.service.js.map +1 -1
  23. package/dist/modules/access/access-splice.js +4 -4
  24. package/dist/modules/access/access-splice.js.map +1 -1
  25. package/dist/modules/access/access.routes.js +19 -19
  26. package/dist/modules/access/access.routes.js.map +1 -1
  27. package/dist/modules/access/creator-access.d.ts +2 -2
  28. package/dist/modules/access/creator-access.js +5 -5
  29. package/dist/modules/access/creator-access.js.map +1 -1
  30. package/dist/modules/access/roles-admin.service.d.ts +18 -4
  31. package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
  32. package/dist/modules/access/roles-admin.service.js +14 -4
  33. package/dist/modules/access/roles-admin.service.js.map +1 -1
  34. package/dist/modules/code-mode/code-mode-names.d.ts +5 -13
  35. package/dist/modules/code-mode/code-mode-names.d.ts.map +1 -1
  36. package/dist/modules/code-mode/code-mode-names.js +5 -27
  37. package/dist/modules/code-mode/code-mode-names.js.map +1 -1
  38. package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
  39. package/dist/modules/code-mode/code-mode.tool.js +29 -7
  40. package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
  41. package/dist/modules/mcp/mcp.service.d.ts +11 -52
  42. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  43. package/dist/modules/mcp/mcp.service.js +33 -395
  44. package/dist/modules/mcp/mcp.service.js.map +1 -1
  45. package/dist/modules/plugins/index.d.ts +7 -0
  46. package/dist/modules/plugins/index.d.ts.map +1 -0
  47. package/dist/modules/plugins/index.js +6 -0
  48. package/dist/modules/plugins/index.js.map +1 -0
  49. package/dist/modules/plugins/join-proposals.d.ts +53 -0
  50. package/dist/modules/plugins/join-proposals.d.ts.map +1 -0
  51. package/dist/modules/plugins/join-proposals.js +67 -0
  52. package/dist/modules/plugins/join-proposals.js.map +1 -0
  53. package/dist/modules/plugins/join-requests.service.d.ts +81 -0
  54. package/dist/modules/plugins/join-requests.service.d.ts.map +1 -0
  55. package/dist/modules/plugins/join-requests.service.js +135 -0
  56. package/dist/modules/plugins/join-requests.service.js.map +1 -0
  57. package/dist/modules/plugins/plugin-provision.service.d.ts +134 -0
  58. package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -0
  59. package/dist/modules/plugins/plugin-provision.service.js +344 -0
  60. package/dist/modules/plugins/plugin-provision.service.js.map +1 -0
  61. package/dist/modules/plugins/plugins.contract.d.ts +106 -0
  62. package/dist/modules/plugins/plugins.contract.d.ts.map +1 -0
  63. package/dist/modules/plugins/plugins.contract.js +36 -0
  64. package/dist/modules/plugins/plugins.contract.js.map +1 -0
  65. package/dist/modules/plugins/plugins.routes.d.ts +42 -0
  66. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -0
  67. package/dist/modules/plugins/plugins.routes.js +379 -0
  68. package/dist/modules/plugins/plugins.routes.js.map +1 -0
  69. package/dist/modules/plugins/plugins.service.d.ts +60 -0
  70. package/dist/modules/plugins/plugins.service.d.ts.map +1 -0
  71. package/dist/modules/plugins/plugins.service.js +172 -0
  72. package/dist/modules/plugins/plugins.service.js.map +1 -0
  73. package/dist/modules/skills/pending-skills.service.d.ts +2 -2
  74. package/dist/modules/skills/pending-skills.service.js +7 -7
  75. package/dist/modules/skills/pending-skills.service.js.map +1 -1
  76. package/dist/modules/skills/skills.contract.d.ts +4 -4
  77. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  78. package/dist/modules/skills/skills.contract.js +1 -1
  79. package/dist/modules/skills/skills.service.js +5 -5
  80. package/dist/modules/skills/skills.service.js.map +1 -1
  81. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +65 -0
  82. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -0
  83. package/dist/modules/tool-manuals/mcp-json-discovery.js +276 -0
  84. package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -0
  85. package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts +92 -0
  86. package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts.map +1 -0
  87. package/dist/modules/tool-manuals/mcp-server-edit.service.js +328 -0
  88. package/dist/modules/tool-manuals/mcp-server-edit.service.js.map +1 -0
  89. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +38 -12
  90. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
  91. package/dist/modules/tool-manuals/tool-manuals.contract.js +1 -1
  92. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +13 -2
  93. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
  94. package/dist/modules/tool-manuals/tool-manuals.routes.js +233 -2
  95. package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
  96. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  97. package/dist/modules/tool-manuals/tool-manuals.service.js +74 -37
  98. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  99. package/dist/modules/tool-manuals/tool-manuals.tools.js +6 -3
  100. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  101. package/dist/modules/workflow/git/git.service.js +2 -2
  102. package/dist/modules/workflow/git/git.service.js.map +1 -1
  103. package/dist/modules/workflow/pending-commits.service.d.ts +11 -0
  104. package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
  105. package/dist/modules/workflow/pending-commits.service.js +37 -9
  106. package/dist/modules/workflow/pending-commits.service.js.map +1 -1
  107. package/dist/modules/workflow/pending-commits.worker.d.ts +17 -4
  108. package/dist/modules/workflow/pending-commits.worker.d.ts.map +1 -1
  109. package/dist/modules/workflow/pending-commits.worker.js +63 -9
  110. package/dist/modules/workflow/pending-commits.worker.js.map +1 -1
  111. package/dist/modules/workflow/workflow.service.d.ts +1 -0
  112. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  113. package/dist/modules/workflow/workflow.service.js +6 -1
  114. package/dist/modules/workflow/workflow.service.js.map +1 -1
  115. package/dist/modules/workspace/kb-seed.service.d.ts +2 -2
  116. package/dist/modules/workspace/kb-seed.service.d.ts.map +1 -1
  117. package/dist/modules/workspace/kb-seed.service.js +43 -10
  118. package/dist/modules/workspace/kb-seed.service.js.map +1 -1
  119. package/dist/modules/workspace/plugins-migration.d.ts +50 -0
  120. package/dist/modules/workspace/plugins-migration.d.ts.map +1 -0
  121. package/dist/modules/workspace/plugins-migration.js +379 -0
  122. package/dist/modules/workspace/plugins-migration.js.map +1 -0
  123. package/dist/modules/workspace/workspace.routes.js +3 -3
  124. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  125. package/dist/modules/workspace/workspace.service.d.ts +26 -0
  126. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  127. package/dist/modules/workspace/workspace.service.js +83 -12
  128. package/dist/modules/workspace/workspace.service.js.map +1 -1
  129. package/dist/shared/kb-layout.test.js +3 -3
  130. package/dist/shared/kb-layout.test.js.map +1 -1
  131. package/dist/shared/utcp-namespace.d.ts +6 -27
  132. package/dist/shared/utcp-namespace.d.ts.map +1 -1
  133. package/dist/shared/utcp-namespace.js +6 -63
  134. package/dist/shared/utcp-namespace.js.map +1 -1
  135. package/dist/shared/variable-refs.d.ts +42 -0
  136. package/dist/shared/variable-refs.d.ts.map +1 -0
  137. package/dist/shared/variable-refs.js +60 -0
  138. package/dist/shared/variable-refs.js.map +1 -0
  139. package/kb-template/.bevelignore +1 -1
  140. package/kb-template/AGENTS.md +88 -35
  141. package/kb-template/KnowledgeBase/How to get started.md +10 -10
  142. package/kb-template/access.md +36 -36
  143. package/migrations/meta/0000_snapshot.json +1479 -1479
  144. package/package.json +5 -4
  145. package/src/assets.ts +25 -25
  146. package/src/core/core-ports.ts +106 -106
  147. package/src/core/create-core-server.ts +55 -9
  148. package/src/core/create-core-services.ts +48 -24
  149. package/src/index.ts +69 -69
  150. package/src/modules/access/__tests__/access-control.atref-batch.test.ts +98 -98
  151. package/src/modules/access/__tests__/access-declarations.test.ts +28 -28
  152. package/src/modules/access/__tests__/access-md-format.test.ts +18 -18
  153. package/src/modules/access/__tests__/access-mutation.service.test.ts +5 -5
  154. package/src/modules/access/__tests__/access-splice.test.ts +2 -2
  155. package/src/modules/access/__tests__/access.routes.overrides.test.ts +16 -16
  156. package/src/modules/access/__tests__/grant-sources.test.ts +12 -12
  157. package/src/modules/access/__tests__/roles-admin.service.test.ts +13 -1
  158. package/src/modules/access/access-control.interface.ts +8 -7
  159. package/src/modules/access/access-control.service.ts +7 -7
  160. package/src/modules/access/access-declarations.ts +5 -5
  161. package/src/modules/access/access-mutation.service.ts +3 -3
  162. package/src/modules/access/access-splice.ts +4 -4
  163. package/src/modules/access/access.routes.ts +20 -20
  164. package/src/modules/access/creator-access.ts +5 -5
  165. package/src/modules/access/roles-admin.service.ts +13 -2
  166. package/src/modules/admin/admin-access.routes.ts +29 -29
  167. package/src/modules/auth/__tests__/auth.routes.test.ts +91 -91
  168. package/src/modules/auth/__tests__/rate-limit.test.ts +36 -36
  169. package/src/modules/auth/rate-limit.ts +45 -45
  170. package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +67 -0
  171. package/src/modules/code-mode/code-mode-names.ts +10 -36
  172. package/src/modules/code-mode/code-mode.tool.ts +27 -7
  173. package/src/modules/database/connection.ts +15 -15
  174. package/src/modules/database/schema.ts +11 -11
  175. package/src/modules/diff/__tests__/diff.routes.rejectPathsLocked.test.ts +150 -150
  176. package/src/modules/mcp/mcp.service.ts +57 -435
  177. package/src/modules/{groups → plugins}/__tests__/join-proposals.test.ts +1 -1
  178. package/src/modules/{groups → plugins}/__tests__/join-requests.service.test.ts +7 -7
  179. package/src/modules/{groups/__tests__/group-index.service.test.ts → plugins/__tests__/plugin-index.service.test.ts} +41 -41
  180. package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +312 -0
  181. package/src/modules/{groups/__tests__/groups.routes.test.ts → plugins/__tests__/plugins.routes.test.ts} +100 -100
  182. package/src/modules/plugins/index.ts +17 -0
  183. package/src/modules/{groups → plugins}/join-proposals.ts +2 -2
  184. package/src/modules/{groups → plugins}/join-requests.service.ts +8 -8
  185. package/src/modules/{groups/group-provision.service.ts → plugins/plugin-provision.service.ts} +139 -69
  186. package/src/modules/{groups/groups.contract.ts → plugins/plugins.contract.ts} +26 -26
  187. package/src/modules/{groups/groups.routes.ts → plugins/plugins.routes.ts} +102 -102
  188. package/src/modules/{groups/groups.service.ts → plugins/plugins.service.ts} +43 -43
  189. package/src/modules/secrets-vault/__tests__/tool-owner-gate.route.test.ts +143 -143
  190. package/src/modules/skills/__tests__/pending-skills.service.test.ts +14 -14
  191. package/src/modules/skills/__tests__/skills.service.test.ts +13 -13
  192. package/src/modules/skills/pending-skills.service.ts +7 -7
  193. package/src/modules/skills/skills.contract.ts +4 -4
  194. package/src/modules/skills/skills.service.ts +5 -5
  195. package/src/modules/tool-auth/llm-usage-meter.ts +19 -19
  196. package/src/modules/tool-manuals/__tests__/mcp-json-discovery.test.ts +198 -0
  197. package/src/modules/tool-manuals/__tests__/mcp-server-edit.service.test.ts +346 -0
  198. package/src/modules/tool-manuals/__tests__/tool-manuals.archive.route.test.ts +101 -0
  199. package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +3 -3
  200. package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +2 -2
  201. package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +27 -27
  202. package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +3 -3
  203. package/src/modules/tool-manuals/mcp-json-discovery.ts +328 -0
  204. package/src/modules/tool-manuals/mcp-server-edit.service.ts +434 -0
  205. package/src/modules/tool-manuals/tool-manuals.contract.ts +35 -12
  206. package/src/modules/tool-manuals/tool-manuals.routes.ts +222 -1
  207. package/src/modules/tool-manuals/tool-manuals.service.ts +82 -42
  208. package/src/modules/tool-manuals/tool-manuals.tools.ts +6 -3
  209. package/src/modules/workflow/__tests__/pending-commits.worker.test.ts +118 -0
  210. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +1 -1
  211. package/src/modules/workflow/git/__tests__/branch-name.test.ts +3 -3
  212. package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +3 -3
  213. package/src/modules/workflow/git/__tests__/git.service.commitFile.test.ts +13 -8
  214. package/src/modules/workflow/git/__tests__/git.service.deleteBranch.test.ts +1 -1
  215. package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +1 -1
  216. package/src/modules/workflow/git/git.service.ts +2 -2
  217. package/src/modules/workflow/pending-commits.service.ts +48 -12
  218. package/src/modules/workflow/pending-commits.worker.ts +64 -8
  219. package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +1 -1
  220. package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +1 -1
  221. package/src/modules/workflow/workflow-hooks.ts +101 -101
  222. package/src/modules/workflow/workflow.service.ts +13 -2
  223. package/src/modules/workspace/__tests__/kb-seed.service.test.ts +81 -13
  224. package/src/modules/workspace/__tests__/plugins-migration.test.ts +427 -0
  225. package/src/modules/workspace/__tests__/session-ontology.gate.test.ts +237 -237
  226. package/src/modules/workspace/__tests__/workspace.routes.create-grant.test.ts +236 -236
  227. package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +179 -179
  228. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +320 -320
  229. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +337 -337
  230. package/src/modules/workspace/__tests__/workspace.service.test.ts +116 -0
  231. package/src/modules/workspace/bevel-ignore.ts +66 -66
  232. package/src/modules/workspace/kb-seed.service.ts +38 -9
  233. package/src/modules/workspace/plugins-migration.ts +479 -0
  234. package/src/modules/workspace/session-sink.ts +25 -25
  235. package/src/modules/workspace/workspace.routes.ts +3 -3
  236. package/src/modules/workspace/workspace.service.ts +85 -14
  237. package/src/modules/workspace/workspace.tools.ts +922 -922
  238. package/src/shared/__tests__/join-request.test.ts +13 -13
  239. package/src/shared/__tests__/kb-layout.plugin.test.ts +45 -0
  240. package/src/shared/kb-layout.test.ts +3 -3
  241. package/src/shared/utcp-namespace.ts +10 -68
  242. package/src/shared/variable-refs.ts +64 -0
  243. package/src/modules/groups/__tests__/group-provision.service.test.ts +0 -247
  244. package/src/modules/groups/index.ts +0 -17
  245. package/src/shared/__tests__/kb-layout.group.test.ts +0 -45
  246. /package/kb-template/{Groups → Plugins}/.gitkeep +0 -0
@@ -1,179 +1,179 @@
1
- import type { Server } from 'node:http';
2
- import type { AddressInfo } from 'node:net';
3
- import fs from 'node:fs/promises';
4
- import path from 'node:path';
5
- import os from 'node:os';
6
- import express from 'express';
7
- import { describe, it, expect, afterEach, vi } from 'vitest';
8
- import type { IWorkflowService } from '@bevel-software/platform-shared';
9
- import type { IAccessControl } from '../../access/access-control.interface.js';
10
- import type { WorkflowEventBus } from '../../workflow/event-bus.js';
11
- import type { AuthService } from '../../auth/auth.service.js';
12
- import type { IAdminAccessService } from '../../admin/admin.interface.js';
13
- import { createWorkspaceRoutes } from '../workspace.routes.js';
14
- import type { ICreatorAccess } from '../../access/creator-access.js';
15
- import type { WorkspaceService } from '../workspace.service.js';
16
-
17
- const stubCreatorAccess: ICreatorAccess = {
18
- planForCreate: async () => null,
19
- grantInExtractedFile: async () => null,
20
- noteAccessFileWritten: () => {},
21
- };
22
-
23
- /**
24
- * Contract test for DELETE /workspace/:id/file on a *directory* target —
25
- * the recursive folder-delete branch.
26
- *
27
- * BEVA-132: deleting a folder that contains sub-folders left the (now-empty)
28
- * sub-directory shells on disk, so the explorer (which lists on-disk
29
- * directories, not just tracked files) kept showing the folder and it looked
30
- * undeletable. The route must sweep the whole empty subtree, not just the top
31
- * directory.
32
- */
33
-
34
- const USER_ID = 'user-1';
35
- const USER = { id: USER_ID, email: 'alice@example.com', name: 'Alice' };
36
- const WORKSPACE_ID = 'feature-branch';
37
-
38
- interface Harness {
39
- server: Server;
40
- baseUrl: string;
41
- workspaceDir: string;
42
- }
43
-
44
- async function makeHarness(): Promise<Harness> {
45
- const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), 'beva132-'));
46
-
47
- const accessControl = {
48
- canWrite: vi.fn(),
49
- canWriteBatch: vi.fn(),
50
- canDownload: vi.fn(),
51
- eligibleWriters: vi.fn(),
52
- eligibleWriterEmails: vi.fn(),
53
- invalidate: vi.fn(),
54
- findEmailByHash: vi.fn(),
55
- canWriteAtRef: vi.fn(),
56
- canWriteBatchAtRef: vi.fn(),
57
- eligibleWritersAtRef: vi.fn(),
58
- eligibleWritersForPathsAtRef: vi.fn(),
59
- } as unknown as IAccessControl;
60
-
61
- // deleteFile just removes the file from disk — the lock/commit machinery is
62
- // mocked out, so the on-disk effect of a per-file delete is all that the
63
- // recursive-cleanup logic under test reacts to.
64
- const workspaceServiceMock: Partial<WorkspaceService> = {
65
- getWorkspacePath: vi.fn(async () => workspaceDir),
66
- deleteFile: vi.fn(async (_id: string, relPath: string) => {
67
- await fs.rm(path.resolve(workspaceDir, relPath));
68
- }),
69
- };
70
- const workspaceService = workspaceServiceMock as WorkspaceService;
71
-
72
- const workflowServiceMock: Partial<IWorkflowService> = {
73
- getLock: vi.fn(async () => null),
74
- acquireLock: vi.fn(async () => ({ acquired: true, lock: {} as never })),
75
- releaseLock: vi.fn(async () => undefined as never),
76
- releaseLockNoCommit: vi.fn(async () => undefined as never),
77
- };
78
- const workflowService = workflowServiceMock as unknown as IWorkflowService;
79
-
80
- const authServiceMock: Partial<AuthService> = {
81
- getUserById: vi.fn(async () => USER),
82
- };
83
- const authService = authServiceMock as AuthService;
84
-
85
- const eventBus = { emit: vi.fn() } as unknown as WorkflowEventBus;
86
-
87
- const app = express();
88
- app.use(express.json());
89
- app.use('/api', (req, _res, next) => {
90
- (req as any).userId = USER_ID;
91
- next();
92
- });
93
- app.use('/api', createWorkspaceRoutes(
94
- workspaceService,
95
- authService,
96
- workflowService,
97
- eventBus,
98
- accessControl,
99
- 'knowledge-base',
100
- stubCreatorAccess,
101
- // Not exercised here — only `.bevelignore`'s tree visibility consults it.
102
- { isAdmin: async () => false } as unknown as IAdminAccessService,
103
- ));
104
-
105
- const server = await new Promise<Server>((resolve) => {
106
- const s = app.listen(0, () => resolve(s));
107
- });
108
- const addr = server.address();
109
- if (addr === null || typeof addr === 'string') {
110
- throw new Error(`Unexpected server.address() shape: ${JSON.stringify(addr)}`);
111
- }
112
- const port = (addr as AddressInfo).port;
113
- return { server, baseUrl: `http://127.0.0.1:${port}`, workspaceDir };
114
- }
115
-
116
- async function closeServer(server: Server): Promise<void> {
117
- await new Promise<void>((resolve, reject) =>
118
- server.close((err) => (err ? reject(err) : resolve())),
119
- );
120
- }
121
-
122
- async function exists(p: string): Promise<boolean> {
123
- try {
124
- await fs.stat(p);
125
- return true;
126
- } catch {
127
- return false;
128
- }
129
- }
130
-
131
- describe('DELETE /workspace/:id/file — recursive folder delete (BEVA-132)', () => {
132
- let h: Harness | null = null;
133
- afterEach(async () => {
134
- if (h) {
135
- await closeServer(h.server);
136
- await fs.rm(h.workspaceDir, { recursive: true, force: true });
137
- }
138
- h = null;
139
- });
140
-
141
- it('removes a folder whose children are sub-folders (nested files) off disk', async () => {
142
- h = await makeHarness();
143
- // parent/
144
- // sub/a.md
145
- // sub/deep/b.md
146
- // other/c.md
147
- await fs.mkdir(path.join(h.workspaceDir, 'parent/sub/deep'), { recursive: true });
148
- await fs.mkdir(path.join(h.workspaceDir, 'parent/other'), { recursive: true });
149
- await fs.writeFile(path.join(h.workspaceDir, 'parent/sub/a.md'), 'a');
150
- await fs.writeFile(path.join(h.workspaceDir, 'parent/sub/deep/b.md'), 'b');
151
- await fs.writeFile(path.join(h.workspaceDir, 'parent/other/c.md'), 'c');
152
-
153
- const res = await fetch(
154
- `${h.baseUrl}/api/workspace/${WORKSPACE_ID}/file?path=${encodeURIComponent('parent')}`,
155
- { method: 'DELETE' },
156
- );
157
- expect(res.status).toBe(200);
158
- const body = (await res.json()) as { status: string; count: number };
159
- expect(body.status).toBe('deleted');
160
- expect(body.count).toBe(3);
161
- // The whole subtree — including the empty sub-folder shells — is gone.
162
- expect(await exists(path.join(h.workspaceDir, 'parent'))).toBe(false);
163
- });
164
-
165
- it('removes a folder that contains only empty sub-folders (no tracked files)', async () => {
166
- h = await makeHarness();
167
- await fs.mkdir(path.join(h.workspaceDir, 'parent/emptyA/nested'), { recursive: true });
168
- await fs.mkdir(path.join(h.workspaceDir, 'parent/emptyB'), { recursive: true });
169
-
170
- const res = await fetch(
171
- `${h.baseUrl}/api/workspace/${WORKSPACE_ID}/file?path=${encodeURIComponent('parent')}`,
172
- { method: 'DELETE' },
173
- );
174
- expect(res.status).toBe(200);
175
- const body = (await res.json()) as { status: string; count: number };
176
- expect(body.count).toBe(0);
177
- expect(await exists(path.join(h.workspaceDir, 'parent'))).toBe(false);
178
- });
179
- });
1
+ import type { Server } from 'node:http';
2
+ import type { AddressInfo } from 'node:net';
3
+ import fs from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import os from 'node:os';
6
+ import express from 'express';
7
+ import { describe, it, expect, afterEach, vi } from 'vitest';
8
+ import type { IWorkflowService } from '@bevel-software/platform-shared';
9
+ import type { IAccessControl } from '../../access/access-control.interface.js';
10
+ import type { WorkflowEventBus } from '../../workflow/event-bus.js';
11
+ import type { AuthService } from '../../auth/auth.service.js';
12
+ import type { IAdminAccessService } from '../../admin/admin.interface.js';
13
+ import { createWorkspaceRoutes } from '../workspace.routes.js';
14
+ import type { ICreatorAccess } from '../../access/creator-access.js';
15
+ import type { WorkspaceService } from '../workspace.service.js';
16
+
17
+ const stubCreatorAccess: ICreatorAccess = {
18
+ planForCreate: async () => null,
19
+ grantInExtractedFile: async () => null,
20
+ noteAccessFileWritten: () => {},
21
+ };
22
+
23
+ /**
24
+ * Contract test for DELETE /workspace/:id/file on a *directory* target —
25
+ * the recursive folder-delete branch.
26
+ *
27
+ * BEVA-132: deleting a folder that contains sub-folders left the (now-empty)
28
+ * sub-directory shells on disk, so the explorer (which lists on-disk
29
+ * directories, not just tracked files) kept showing the folder and it looked
30
+ * undeletable. The route must sweep the whole empty subtree, not just the top
31
+ * directory.
32
+ */
33
+
34
+ const USER_ID = 'user-1';
35
+ const USER = { id: USER_ID, email: 'alice@example.com', name: 'Alice' };
36
+ const WORKSPACE_ID = 'feature-branch';
37
+
38
+ interface Harness {
39
+ server: Server;
40
+ baseUrl: string;
41
+ workspaceDir: string;
42
+ }
43
+
44
+ async function makeHarness(): Promise<Harness> {
45
+ const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), 'beva132-'));
46
+
47
+ const accessControl = {
48
+ canWrite: vi.fn(),
49
+ canWriteBatch: vi.fn(),
50
+ canDownload: vi.fn(),
51
+ eligibleWriters: vi.fn(),
52
+ eligibleWriterEmails: vi.fn(),
53
+ invalidate: vi.fn(),
54
+ findEmailByHash: vi.fn(),
55
+ canWriteAtRef: vi.fn(),
56
+ canWriteBatchAtRef: vi.fn(),
57
+ eligibleWritersAtRef: vi.fn(),
58
+ eligibleWritersForPathsAtRef: vi.fn(),
59
+ } as unknown as IAccessControl;
60
+
61
+ // deleteFile just removes the file from disk — the lock/commit machinery is
62
+ // mocked out, so the on-disk effect of a per-file delete is all that the
63
+ // recursive-cleanup logic under test reacts to.
64
+ const workspaceServiceMock: Partial<WorkspaceService> = {
65
+ getWorkspacePath: vi.fn(async () => workspaceDir),
66
+ deleteFile: vi.fn(async (_id: string, relPath: string) => {
67
+ await fs.rm(path.resolve(workspaceDir, relPath));
68
+ }),
69
+ };
70
+ const workspaceService = workspaceServiceMock as WorkspaceService;
71
+
72
+ const workflowServiceMock: Partial<IWorkflowService> = {
73
+ getLock: vi.fn(async () => null),
74
+ acquireLock: vi.fn(async () => ({ acquired: true, lock: {} as never })),
75
+ releaseLock: vi.fn(async () => undefined as never),
76
+ releaseLockNoCommit: vi.fn(async () => undefined as never),
77
+ };
78
+ const workflowService = workflowServiceMock as unknown as IWorkflowService;
79
+
80
+ const authServiceMock: Partial<AuthService> = {
81
+ getUserById: vi.fn(async () => USER),
82
+ };
83
+ const authService = authServiceMock as AuthService;
84
+
85
+ const eventBus = { emit: vi.fn() } as unknown as WorkflowEventBus;
86
+
87
+ const app = express();
88
+ app.use(express.json());
89
+ app.use('/api', (req, _res, next) => {
90
+ (req as any).userId = USER_ID;
91
+ next();
92
+ });
93
+ app.use('/api', createWorkspaceRoutes(
94
+ workspaceService,
95
+ authService,
96
+ workflowService,
97
+ eventBus,
98
+ accessControl,
99
+ 'knowledge-base',
100
+ stubCreatorAccess,
101
+ // Not exercised here — only `.bevelignore`'s tree visibility consults it.
102
+ { isAdmin: async () => false } as unknown as IAdminAccessService,
103
+ ));
104
+
105
+ const server = await new Promise<Server>((resolve) => {
106
+ const s = app.listen(0, () => resolve(s));
107
+ });
108
+ const addr = server.address();
109
+ if (addr === null || typeof addr === 'string') {
110
+ throw new Error(`Unexpected server.address() shape: ${JSON.stringify(addr)}`);
111
+ }
112
+ const port = (addr as AddressInfo).port;
113
+ return { server, baseUrl: `http://127.0.0.1:${port}`, workspaceDir };
114
+ }
115
+
116
+ async function closeServer(server: Server): Promise<void> {
117
+ await new Promise<void>((resolve, reject) =>
118
+ server.close((err) => (err ? reject(err) : resolve())),
119
+ );
120
+ }
121
+
122
+ async function exists(p: string): Promise<boolean> {
123
+ try {
124
+ await fs.stat(p);
125
+ return true;
126
+ } catch {
127
+ return false;
128
+ }
129
+ }
130
+
131
+ describe('DELETE /workspace/:id/file — recursive folder delete (BEVA-132)', () => {
132
+ let h: Harness | null = null;
133
+ afterEach(async () => {
134
+ if (h) {
135
+ await closeServer(h.server);
136
+ await fs.rm(h.workspaceDir, { recursive: true, force: true });
137
+ }
138
+ h = null;
139
+ });
140
+
141
+ it('removes a folder whose children are sub-folders (nested files) off disk', async () => {
142
+ h = await makeHarness();
143
+ // parent/
144
+ // sub/a.md
145
+ // sub/deep/b.md
146
+ // other/c.md
147
+ await fs.mkdir(path.join(h.workspaceDir, 'parent/sub/deep'), { recursive: true });
148
+ await fs.mkdir(path.join(h.workspaceDir, 'parent/other'), { recursive: true });
149
+ await fs.writeFile(path.join(h.workspaceDir, 'parent/sub/a.md'), 'a');
150
+ await fs.writeFile(path.join(h.workspaceDir, 'parent/sub/deep/b.md'), 'b');
151
+ await fs.writeFile(path.join(h.workspaceDir, 'parent/other/c.md'), 'c');
152
+
153
+ const res = await fetch(
154
+ `${h.baseUrl}/api/workspace/${WORKSPACE_ID}/file?path=${encodeURIComponent('parent')}`,
155
+ { method: 'DELETE' },
156
+ );
157
+ expect(res.status).toBe(200);
158
+ const body = (await res.json()) as { status: string; count: number };
159
+ expect(body.status).toBe('deleted');
160
+ expect(body.count).toBe(3);
161
+ // The whole subtree — including the empty sub-folder shells — is gone.
162
+ expect(await exists(path.join(h.workspaceDir, 'parent'))).toBe(false);
163
+ });
164
+
165
+ it('removes a folder that contains only empty sub-folders (no tracked files)', async () => {
166
+ h = await makeHarness();
167
+ await fs.mkdir(path.join(h.workspaceDir, 'parent/emptyA/nested'), { recursive: true });
168
+ await fs.mkdir(path.join(h.workspaceDir, 'parent/emptyB'), { recursive: true });
169
+
170
+ const res = await fetch(
171
+ `${h.baseUrl}/api/workspace/${WORKSPACE_ID}/file?path=${encodeURIComponent('parent')}`,
172
+ { method: 'DELETE' },
173
+ );
174
+ expect(res.status).toBe(200);
175
+ const body = (await res.json()) as { status: string; count: number };
176
+ expect(body.count).toBe(0);
177
+ expect(await exists(path.join(h.workspaceDir, 'parent'))).toBe(false);
178
+ });
179
+ });