@bevel-software/platform-core-backend 0.7.5 → 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.
- package/LICENSE +202 -202
- package/THIRD-PARTY-NOTICES.md +428 -454
- package/dist/core/core-ports.d.ts +1 -1
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +51 -7
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +5 -3
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +30 -18
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +8 -7
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +1 -1
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +6 -6
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/access-declarations.d.ts +5 -5
- package/dist/modules/access/access-declarations.js +3 -3
- package/dist/modules/access/access-mutation.service.d.ts +3 -3
- package/dist/modules/access/access-mutation.service.d.ts.map +1 -1
- package/dist/modules/access/access-mutation.service.js +3 -3
- package/dist/modules/access/access-mutation.service.js.map +1 -1
- package/dist/modules/access/access-splice.js +4 -4
- package/dist/modules/access/access-splice.js.map +1 -1
- package/dist/modules/access/access.routes.js +19 -19
- package/dist/modules/access/access.routes.js.map +1 -1
- package/dist/modules/access/creator-access.d.ts +2 -2
- package/dist/modules/access/creator-access.js +5 -5
- package/dist/modules/access/creator-access.js.map +1 -1
- package/dist/modules/access/roles-admin.service.d.ts +18 -4
- package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
- package/dist/modules/access/roles-admin.service.js +14 -4
- package/dist/modules/access/roles-admin.service.js.map +1 -1
- package/dist/modules/code-mode/code-mode-names.d.ts +5 -13
- package/dist/modules/code-mode/code-mode-names.d.ts.map +1 -1
- package/dist/modules/code-mode/code-mode-names.js +5 -27
- package/dist/modules/code-mode/code-mode-names.js.map +1 -1
- package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
- package/dist/modules/code-mode/code-mode.tool.js +29 -7
- package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
- package/dist/modules/mcp/mcp.service.d.ts +11 -52
- package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.service.js +33 -395
- package/dist/modules/mcp/mcp.service.js.map +1 -1
- package/dist/modules/plugins/index.d.ts +7 -0
- package/dist/modules/plugins/index.d.ts.map +1 -0
- package/dist/modules/plugins/index.js +6 -0
- package/dist/modules/plugins/index.js.map +1 -0
- package/dist/modules/plugins/join-proposals.d.ts +53 -0
- package/dist/modules/plugins/join-proposals.d.ts.map +1 -0
- package/dist/modules/plugins/join-proposals.js +67 -0
- package/dist/modules/plugins/join-proposals.js.map +1 -0
- package/dist/modules/plugins/join-requests.service.d.ts +81 -0
- package/dist/modules/plugins/join-requests.service.d.ts.map +1 -0
- package/dist/modules/plugins/join-requests.service.js +135 -0
- package/dist/modules/plugins/join-requests.service.js.map +1 -0
- package/dist/modules/plugins/plugin-provision.service.d.ts +134 -0
- package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -0
- package/dist/modules/plugins/plugin-provision.service.js +344 -0
- package/dist/modules/plugins/plugin-provision.service.js.map +1 -0
- package/dist/modules/plugins/plugins.contract.d.ts +106 -0
- package/dist/modules/plugins/plugins.contract.d.ts.map +1 -0
- package/dist/modules/plugins/plugins.contract.js +36 -0
- package/dist/modules/plugins/plugins.contract.js.map +1 -0
- package/dist/modules/plugins/plugins.routes.d.ts +42 -0
- package/dist/modules/plugins/plugins.routes.d.ts.map +1 -0
- package/dist/modules/plugins/plugins.routes.js +379 -0
- package/dist/modules/plugins/plugins.routes.js.map +1 -0
- package/dist/modules/plugins/plugins.service.d.ts +60 -0
- package/dist/modules/plugins/plugins.service.d.ts.map +1 -0
- package/dist/modules/plugins/plugins.service.js +172 -0
- package/dist/modules/plugins/plugins.service.js.map +1 -0
- package/dist/modules/skills/pending-skills.service.d.ts +2 -2
- package/dist/modules/skills/pending-skills.service.js +7 -7
- package/dist/modules/skills/pending-skills.service.js.map +1 -1
- package/dist/modules/skills/skills.contract.d.ts +4 -4
- package/dist/modules/skills/skills.contract.d.ts.map +1 -1
- package/dist/modules/skills/skills.contract.js +1 -1
- package/dist/modules/skills/skills.service.js +5 -5
- package/dist/modules/skills/skills.service.js.map +1 -1
- package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +65 -0
- package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -0
- package/dist/modules/tool-manuals/mcp-json-discovery.js +276 -0
- package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -0
- package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts +92 -0
- package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts.map +1 -0
- package/dist/modules/tool-manuals/mcp-server-edit.service.js +328 -0
- package/dist/modules/tool-manuals/mcp-server-edit.service.js.map +1 -0
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +38 -12
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.contract.js +1 -1
- package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +13 -2
- package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.routes.js +233 -2
- package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.js +74 -37
- package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.js +6 -3
- package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
- package/dist/modules/workflow/git/git.service.js +2 -2
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workspace/kb-seed.service.d.ts +2 -2
- package/dist/modules/workspace/kb-seed.service.d.ts.map +1 -1
- package/dist/modules/workspace/kb-seed.service.js +43 -10
- package/dist/modules/workspace/kb-seed.service.js.map +1 -1
- package/dist/modules/workspace/plugins-migration.d.ts +50 -0
- package/dist/modules/workspace/plugins-migration.d.ts.map +1 -0
- package/dist/modules/workspace/plugins-migration.js +379 -0
- package/dist/modules/workspace/plugins-migration.js.map +1 -0
- package/dist/modules/workspace/workspace.routes.js +3 -3
- package/dist/modules/workspace/workspace.routes.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +26 -0
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +83 -12
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/shared/kb-layout.test.js +3 -3
- package/dist/shared/kb-layout.test.js.map +1 -1
- package/dist/shared/utcp-namespace.d.ts +6 -27
- package/dist/shared/utcp-namespace.d.ts.map +1 -1
- package/dist/shared/utcp-namespace.js +6 -63
- package/dist/shared/utcp-namespace.js.map +1 -1
- package/dist/shared/variable-refs.d.ts +42 -0
- package/dist/shared/variable-refs.d.ts.map +1 -0
- package/dist/shared/variable-refs.js +60 -0
- package/dist/shared/variable-refs.js.map +1 -0
- package/kb-template/.bevelignore +1 -1
- package/kb-template/AGENTS.md +88 -35
- package/kb-template/KnowledgeBase/How to get started.md +10 -10
- package/kb-template/access.md +36 -36
- package/migrations/meta/0000_snapshot.json +1479 -1479
- package/package.json +5 -4
- package/src/assets.ts +25 -25
- package/src/core/core-ports.ts +106 -106
- package/src/core/create-core-server.ts +55 -9
- package/src/core/create-core-services.ts +40 -20
- package/src/index.ts +69 -69
- package/src/modules/access/__tests__/access-control.atref-batch.test.ts +98 -98
- package/src/modules/access/__tests__/access-declarations.test.ts +28 -28
- package/src/modules/access/__tests__/access-md-format.test.ts +18 -18
- package/src/modules/access/__tests__/access-mutation.service.test.ts +5 -5
- package/src/modules/access/__tests__/access-splice.test.ts +2 -2
- package/src/modules/access/__tests__/access.routes.overrides.test.ts +16 -16
- package/src/modules/access/__tests__/grant-sources.test.ts +12 -12
- package/src/modules/access/__tests__/roles-admin.service.test.ts +13 -1
- package/src/modules/access/access-control.interface.ts +8 -7
- package/src/modules/access/access-control.service.ts +7 -7
- package/src/modules/access/access-declarations.ts +5 -5
- package/src/modules/access/access-mutation.service.ts +3 -3
- package/src/modules/access/access-splice.ts +4 -4
- package/src/modules/access/access.routes.ts +20 -20
- package/src/modules/access/creator-access.ts +5 -5
- package/src/modules/access/roles-admin.service.ts +13 -2
- package/src/modules/admin/admin-access.routes.ts +29 -29
- package/src/modules/auth/__tests__/auth.routes.test.ts +91 -91
- package/src/modules/auth/__tests__/rate-limit.test.ts +36 -36
- package/src/modules/auth/rate-limit.ts +45 -45
- package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +67 -0
- package/src/modules/code-mode/code-mode-names.ts +10 -36
- package/src/modules/code-mode/code-mode.tool.ts +27 -7
- package/src/modules/database/connection.ts +15 -15
- package/src/modules/database/schema.ts +11 -11
- package/src/modules/diff/__tests__/diff.routes.rejectPathsLocked.test.ts +150 -150
- package/src/modules/mcp/mcp.service.ts +57 -435
- package/src/modules/{groups → plugins}/__tests__/join-proposals.test.ts +1 -1
- package/src/modules/{groups → plugins}/__tests__/join-requests.service.test.ts +7 -7
- package/src/modules/{groups/__tests__/group-index.service.test.ts → plugins/__tests__/plugin-index.service.test.ts} +41 -41
- package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +312 -0
- package/src/modules/{groups/__tests__/groups.routes.test.ts → plugins/__tests__/plugins.routes.test.ts} +100 -100
- package/src/modules/plugins/index.ts +17 -0
- package/src/modules/{groups → plugins}/join-proposals.ts +2 -2
- package/src/modules/{groups → plugins}/join-requests.service.ts +8 -8
- package/src/modules/{groups/group-provision.service.ts → plugins/plugin-provision.service.ts} +139 -69
- package/src/modules/{groups/groups.contract.ts → plugins/plugins.contract.ts} +26 -26
- package/src/modules/{groups/groups.routes.ts → plugins/plugins.routes.ts} +102 -102
- package/src/modules/{groups/groups.service.ts → plugins/plugins.service.ts} +43 -43
- package/src/modules/secrets-vault/__tests__/tool-owner-gate.route.test.ts +143 -143
- package/src/modules/skills/__tests__/pending-skills.service.test.ts +14 -14
- package/src/modules/skills/__tests__/skills.service.test.ts +13 -13
- package/src/modules/skills/pending-skills.service.ts +7 -7
- package/src/modules/skills/skills.contract.ts +4 -4
- package/src/modules/skills/skills.service.ts +5 -5
- package/src/modules/tool-auth/llm-usage-meter.ts +19 -19
- package/src/modules/tool-manuals/__tests__/mcp-json-discovery.test.ts +198 -0
- package/src/modules/tool-manuals/__tests__/mcp-server-edit.service.test.ts +346 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.archive.route.test.ts +101 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +3 -3
- package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +2 -2
- package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +27 -27
- package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +3 -3
- package/src/modules/tool-manuals/mcp-json-discovery.ts +328 -0
- package/src/modules/tool-manuals/mcp-server-edit.service.ts +434 -0
- package/src/modules/tool-manuals/tool-manuals.contract.ts +35 -12
- package/src/modules/tool-manuals/tool-manuals.routes.ts +222 -1
- package/src/modules/tool-manuals/tool-manuals.service.ts +82 -42
- package/src/modules/tool-manuals/tool-manuals.tools.ts +6 -3
- package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +1 -1
- package/src/modules/workflow/git/__tests__/branch-name.test.ts +3 -3
- package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +3 -3
- package/src/modules/workflow/git/__tests__/git.service.commitFile.test.ts +13 -8
- package/src/modules/workflow/git/__tests__/git.service.deleteBranch.test.ts +1 -1
- package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +1 -1
- package/src/modules/workflow/git/git.service.ts +2 -2
- package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +1 -1
- package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +1 -1
- package/src/modules/workflow/workflow-hooks.ts +101 -101
- package/src/modules/workspace/__tests__/kb-seed.service.test.ts +81 -13
- package/src/modules/workspace/__tests__/plugins-migration.test.ts +427 -0
- package/src/modules/workspace/__tests__/session-ontology.gate.test.ts +237 -237
- package/src/modules/workspace/__tests__/workspace.routes.create-grant.test.ts +236 -236
- package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +179 -179
- package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +320 -320
- package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +337 -337
- package/src/modules/workspace/__tests__/workspace.service.test.ts +116 -0
- package/src/modules/workspace/bevel-ignore.ts +66 -66
- package/src/modules/workspace/kb-seed.service.ts +38 -9
- package/src/modules/workspace/plugins-migration.ts +479 -0
- package/src/modules/workspace/session-sink.ts +25 -25
- package/src/modules/workspace/workspace.routes.ts +3 -3
- package/src/modules/workspace/workspace.service.ts +85 -14
- package/src/modules/workspace/workspace.tools.ts +922 -922
- package/src/shared/__tests__/join-request.test.ts +13 -13
- package/src/shared/__tests__/kb-layout.plugin.test.ts +45 -0
- package/src/shared/kb-layout.test.ts +3 -3
- package/src/shared/utcp-namespace.ts +10 -68
- package/src/shared/variable-refs.ts +64 -0
- package/src/modules/groups/__tests__/group-provision.service.test.ts +0 -247
- package/src/modules/groups/index.ts +0 -17
- package/src/shared/__tests__/kb-layout.group.test.ts +0 -45
- /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
|
+
});
|