@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,150 +1,150 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import fs from 'node:fs/promises';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import os from 'node:os';
|
|
5
|
-
import type { AuthUser, IWorkflowService } from '@bevel-software/platform-shared';
|
|
6
|
-
import type { IDiffService } from '../diff.interface.js';
|
|
7
|
-
import { workspaceIdForBranch } from '../../workspace/workspace.service.js';
|
|
8
|
-
import { WorkflowValidationError } from '../../workflow/workflow.errors.js';
|
|
9
|
-
import { rejectPathsLocked } from '../diff.routes.js';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* `rejectPathsLocked` — the BATCH reject. All paths go through ONE
|
|
13
|
-
* `LockingFilesystem.writeFiles` cycle: every lock acquired before the first
|
|
14
|
-
* disk mutation, baselines restored / agent-added files deleted, then a single
|
|
15
|
-
* `commitChanges` and lock release. Fail-closed: contention or a commit error
|
|
16
|
-
* aborts with nothing committed.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
const USER: AuthUser = { id: 'user-1', email: 'alice@example.com', name: 'Alice' };
|
|
20
|
-
const BRANCH = 'alice/revert-test';
|
|
21
|
-
const WORKSPACE_ID = workspaceIdForBranch(BRANCH);
|
|
22
|
-
|
|
23
|
-
interface Harness {
|
|
24
|
-
workflow: IWorkflowService;
|
|
25
|
-
diff: IDiffService;
|
|
26
|
-
calls: string[];
|
|
27
|
-
workspaceDir: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
async function makeHarness(overrides: {
|
|
31
|
-
/** Per-path acquire outcome. Default: every path acquires cleanly. */
|
|
32
|
-
acquire?: (relPath: string) => { acquired: boolean; lock: { holderName: string } };
|
|
33
|
-
/** Throw from commitChanges to simulate a commit failure. */
|
|
34
|
-
commit?: () => void;
|
|
35
|
-
} = {}): Promise<Harness> {
|
|
36
|
-
const calls: string[] = [];
|
|
37
|
-
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-reject-'));
|
|
38
|
-
// Baseline-carrying file (backup exists → revert = write baseline bytes)
|
|
39
|
-
// and an agent-added file (no backup → revert = delete).
|
|
40
|
-
await fs.mkdir(path.join(workspaceDir, 'knowledge-base'), { recursive: true });
|
|
41
|
-
await fs.writeFile(path.join(workspaceDir, 'knowledge-base/a.md'), 'agent version\n');
|
|
42
|
-
await fs.writeFile(path.join(workspaceDir, 'knowledge-base/added.md'), 'agent-added\n');
|
|
43
|
-
|
|
44
|
-
const acquireLock = vi.fn(async (_ws: string, _branch: string, relPath: string) => {
|
|
45
|
-
const result = overrides.acquire
|
|
46
|
-
? overrides.acquire(relPath)
|
|
47
|
-
: { acquired: true, lock: { holderName: 'Alice' } };
|
|
48
|
-
calls.push(`acquire:${relPath}:${result.acquired}`);
|
|
49
|
-
return result;
|
|
50
|
-
});
|
|
51
|
-
const releaseLockNoCommit = vi.fn(async (_ws: string, _branch: string, relPath: string) => {
|
|
52
|
-
calls.push(`releaseNoCommit:${relPath}`);
|
|
53
|
-
});
|
|
54
|
-
const commitChanges = vi.fn(async (_ws: string, _user: AuthUser, summary: string) => {
|
|
55
|
-
calls.push(`commit:${summary}`);
|
|
56
|
-
overrides.commit?.();
|
|
57
|
-
return { sha: 'abc', summary } as never;
|
|
58
|
-
});
|
|
59
|
-
const workflow = { acquireLock, releaseLockNoCommit, commitChanges } as unknown as IWorkflowService;
|
|
60
|
-
|
|
61
|
-
const revertPlan = vi.fn(async (_ws: string, paths: string[]) => ({
|
|
62
|
-
workspaceDir,
|
|
63
|
-
writes: paths
|
|
64
|
-
.filter((p) => p.endsWith('a.md'))
|
|
65
|
-
.map((p) => ({ path: p, content: Buffer.from('baseline\n') })),
|
|
66
|
-
deletes: paths.filter((p) => p.endsWith('added.md')),
|
|
67
|
-
}));
|
|
68
|
-
const diff = { revertPlan } as unknown as IDiffService;
|
|
69
|
-
return { workflow, diff, calls, workspaceDir };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
describe('rejectPathsLocked', () => {
|
|
73
|
-
let workspaceDir: string | undefined;
|
|
74
|
-
beforeEach(() => {
|
|
75
|
-
workspaceDir = undefined;
|
|
76
|
-
});
|
|
77
|
-
afterEach(async () => {
|
|
78
|
-
if (workspaceDir) await fs.rm(workspaceDir, { recursive: true, force: true });
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('acquires EVERY lock before mutating, restores + deletes, then commits ONCE and releases', async () => {
|
|
82
|
-
const h = await makeHarness();
|
|
83
|
-
workspaceDir = h.workspaceDir;
|
|
84
|
-
|
|
85
|
-
await rejectPathsLocked(h.workflow, h.diff, WORKSPACE_ID, USER, [
|
|
86
|
-
'knowledge-base/a.md',
|
|
87
|
-
'knowledge-base/added.md',
|
|
88
|
-
]);
|
|
89
|
-
|
|
90
|
-
// All locks first (sorted), ONE commit, then per-path release (no per-file commit).
|
|
91
|
-
expect(h.calls).toEqual([
|
|
92
|
-
'acquire:knowledge-base/a.md:true',
|
|
93
|
-
'acquire:knowledge-base/added.md:true',
|
|
94
|
-
'commit:Revert agent changes (2 files)',
|
|
95
|
-
'releaseNoCommit:knowledge-base/a.md',
|
|
96
|
-
'releaseNoCommit:knowledge-base/added.md',
|
|
97
|
-
]);
|
|
98
|
-
// Disk: baseline restored, agent-added file gone.
|
|
99
|
-
expect(await fs.readFile(path.join(h.workspaceDir, 'knowledge-base/a.md'), 'utf-8')).toBe('baseline\n');
|
|
100
|
-
await expect(fs.access(path.join(h.workspaceDir, 'knowledge-base/added.md'))).rejects.toThrow();
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('is a no-op for an empty path list', async () => {
|
|
104
|
-
const h = await makeHarness();
|
|
105
|
-
workspaceDir = h.workspaceDir;
|
|
106
|
-
await expect(
|
|
107
|
-
rejectPathsLocked(h.workflow, h.diff, WORKSPACE_ID, USER, []),
|
|
108
|
-
).resolves.toBeUndefined();
|
|
109
|
-
expect(h.calls).toEqual([]);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it('fails CLOSED on a contended lock: nothing mutated, nothing committed', async () => {
|
|
113
|
-
const h = await makeHarness({
|
|
114
|
-
acquire: (relPath) =>
|
|
115
|
-
relPath.endsWith('added.md')
|
|
116
|
-
? { acquired: false, lock: { holderName: 'Bob' } }
|
|
117
|
-
: { acquired: true, lock: { holderName: 'Alice' } },
|
|
118
|
-
});
|
|
119
|
-
workspaceDir = h.workspaceDir;
|
|
120
|
-
|
|
121
|
-
await expect(
|
|
122
|
-
rejectPathsLocked(h.workflow, h.diff, WORKSPACE_ID, USER, [
|
|
123
|
-
'knowledge-base/a.md',
|
|
124
|
-
'knowledge-base/added.md',
|
|
125
|
-
]),
|
|
126
|
-
).rejects.toBeInstanceOf(WorkflowValidationError);
|
|
127
|
-
|
|
128
|
-
// No commit landed and the disk is untouched — the batch is atomic.
|
|
129
|
-
expect(h.calls.some((c) => c.startsWith('commit:'))).toBe(false);
|
|
130
|
-
expect(await fs.readFile(path.join(h.workspaceDir, 'knowledge-base/a.md'), 'utf-8')).toBe('agent version\n');
|
|
131
|
-
await expect(fs.access(path.join(h.workspaceDir, 'knowledge-base/added.md'))).resolves.toBeUndefined();
|
|
132
|
-
}, 15_000);
|
|
133
|
-
|
|
134
|
-
it('surfaces a commit failure as a WorkflowValidationError and releases without committing', async () => {
|
|
135
|
-
const h = await makeHarness({
|
|
136
|
-
commit: () => {
|
|
137
|
-
throw new Error('push rejected');
|
|
138
|
-
},
|
|
139
|
-
});
|
|
140
|
-
workspaceDir = h.workspaceDir;
|
|
141
|
-
|
|
142
|
-
await expect(
|
|
143
|
-
rejectPathsLocked(h.workflow, h.diff, WORKSPACE_ID, USER, [
|
|
144
|
-
'knowledge-base/a.md',
|
|
145
|
-
]),
|
|
146
|
-
).rejects.toBeInstanceOf(WorkflowValidationError);
|
|
147
|
-
// Locks were dropped via the no-commit path after the failure.
|
|
148
|
-
expect(h.calls.filter((c) => c.startsWith('releaseNoCommit:'))).toHaveLength(1);
|
|
149
|
-
});
|
|
150
|
-
});
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import type { AuthUser, IWorkflowService } from '@bevel-software/platform-shared';
|
|
6
|
+
import type { IDiffService } from '../diff.interface.js';
|
|
7
|
+
import { workspaceIdForBranch } from '../../workspace/workspace.service.js';
|
|
8
|
+
import { WorkflowValidationError } from '../../workflow/workflow.errors.js';
|
|
9
|
+
import { rejectPathsLocked } from '../diff.routes.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* `rejectPathsLocked` — the BATCH reject. All paths go through ONE
|
|
13
|
+
* `LockingFilesystem.writeFiles` cycle: every lock acquired before the first
|
|
14
|
+
* disk mutation, baselines restored / agent-added files deleted, then a single
|
|
15
|
+
* `commitChanges` and lock release. Fail-closed: contention or a commit error
|
|
16
|
+
* aborts with nothing committed.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const USER: AuthUser = { id: 'user-1', email: 'alice@example.com', name: 'Alice' };
|
|
20
|
+
const BRANCH = 'alice/revert-test';
|
|
21
|
+
const WORKSPACE_ID = workspaceIdForBranch(BRANCH);
|
|
22
|
+
|
|
23
|
+
interface Harness {
|
|
24
|
+
workflow: IWorkflowService;
|
|
25
|
+
diff: IDiffService;
|
|
26
|
+
calls: string[];
|
|
27
|
+
workspaceDir: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function makeHarness(overrides: {
|
|
31
|
+
/** Per-path acquire outcome. Default: every path acquires cleanly. */
|
|
32
|
+
acquire?: (relPath: string) => { acquired: boolean; lock: { holderName: string } };
|
|
33
|
+
/** Throw from commitChanges to simulate a commit failure. */
|
|
34
|
+
commit?: () => void;
|
|
35
|
+
} = {}): Promise<Harness> {
|
|
36
|
+
const calls: string[] = [];
|
|
37
|
+
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-reject-'));
|
|
38
|
+
// Baseline-carrying file (backup exists → revert = write baseline bytes)
|
|
39
|
+
// and an agent-added file (no backup → revert = delete).
|
|
40
|
+
await fs.mkdir(path.join(workspaceDir, 'knowledge-base'), { recursive: true });
|
|
41
|
+
await fs.writeFile(path.join(workspaceDir, 'knowledge-base/a.md'), 'agent version\n');
|
|
42
|
+
await fs.writeFile(path.join(workspaceDir, 'knowledge-base/added.md'), 'agent-added\n');
|
|
43
|
+
|
|
44
|
+
const acquireLock = vi.fn(async (_ws: string, _branch: string, relPath: string) => {
|
|
45
|
+
const result = overrides.acquire
|
|
46
|
+
? overrides.acquire(relPath)
|
|
47
|
+
: { acquired: true, lock: { holderName: 'Alice' } };
|
|
48
|
+
calls.push(`acquire:${relPath}:${result.acquired}`);
|
|
49
|
+
return result;
|
|
50
|
+
});
|
|
51
|
+
const releaseLockNoCommit = vi.fn(async (_ws: string, _branch: string, relPath: string) => {
|
|
52
|
+
calls.push(`releaseNoCommit:${relPath}`);
|
|
53
|
+
});
|
|
54
|
+
const commitChanges = vi.fn(async (_ws: string, _user: AuthUser, summary: string) => {
|
|
55
|
+
calls.push(`commit:${summary}`);
|
|
56
|
+
overrides.commit?.();
|
|
57
|
+
return { sha: 'abc', summary } as never;
|
|
58
|
+
});
|
|
59
|
+
const workflow = { acquireLock, releaseLockNoCommit, commitChanges } as unknown as IWorkflowService;
|
|
60
|
+
|
|
61
|
+
const revertPlan = vi.fn(async (_ws: string, paths: string[]) => ({
|
|
62
|
+
workspaceDir,
|
|
63
|
+
writes: paths
|
|
64
|
+
.filter((p) => p.endsWith('a.md'))
|
|
65
|
+
.map((p) => ({ path: p, content: Buffer.from('baseline\n') })),
|
|
66
|
+
deletes: paths.filter((p) => p.endsWith('added.md')),
|
|
67
|
+
}));
|
|
68
|
+
const diff = { revertPlan } as unknown as IDiffService;
|
|
69
|
+
return { workflow, diff, calls, workspaceDir };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
describe('rejectPathsLocked', () => {
|
|
73
|
+
let workspaceDir: string | undefined;
|
|
74
|
+
beforeEach(() => {
|
|
75
|
+
workspaceDir = undefined;
|
|
76
|
+
});
|
|
77
|
+
afterEach(async () => {
|
|
78
|
+
if (workspaceDir) await fs.rm(workspaceDir, { recursive: true, force: true });
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('acquires EVERY lock before mutating, restores + deletes, then commits ONCE and releases', async () => {
|
|
82
|
+
const h = await makeHarness();
|
|
83
|
+
workspaceDir = h.workspaceDir;
|
|
84
|
+
|
|
85
|
+
await rejectPathsLocked(h.workflow, h.diff, WORKSPACE_ID, USER, [
|
|
86
|
+
'knowledge-base/a.md',
|
|
87
|
+
'knowledge-base/added.md',
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
// All locks first (sorted), ONE commit, then per-path release (no per-file commit).
|
|
91
|
+
expect(h.calls).toEqual([
|
|
92
|
+
'acquire:knowledge-base/a.md:true',
|
|
93
|
+
'acquire:knowledge-base/added.md:true',
|
|
94
|
+
'commit:Revert agent changes (2 files)',
|
|
95
|
+
'releaseNoCommit:knowledge-base/a.md',
|
|
96
|
+
'releaseNoCommit:knowledge-base/added.md',
|
|
97
|
+
]);
|
|
98
|
+
// Disk: baseline restored, agent-added file gone.
|
|
99
|
+
expect(await fs.readFile(path.join(h.workspaceDir, 'knowledge-base/a.md'), 'utf-8')).toBe('baseline\n');
|
|
100
|
+
await expect(fs.access(path.join(h.workspaceDir, 'knowledge-base/added.md'))).rejects.toThrow();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('is a no-op for an empty path list', async () => {
|
|
104
|
+
const h = await makeHarness();
|
|
105
|
+
workspaceDir = h.workspaceDir;
|
|
106
|
+
await expect(
|
|
107
|
+
rejectPathsLocked(h.workflow, h.diff, WORKSPACE_ID, USER, []),
|
|
108
|
+
).resolves.toBeUndefined();
|
|
109
|
+
expect(h.calls).toEqual([]);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('fails CLOSED on a contended lock: nothing mutated, nothing committed', async () => {
|
|
113
|
+
const h = await makeHarness({
|
|
114
|
+
acquire: (relPath) =>
|
|
115
|
+
relPath.endsWith('added.md')
|
|
116
|
+
? { acquired: false, lock: { holderName: 'Bob' } }
|
|
117
|
+
: { acquired: true, lock: { holderName: 'Alice' } },
|
|
118
|
+
});
|
|
119
|
+
workspaceDir = h.workspaceDir;
|
|
120
|
+
|
|
121
|
+
await expect(
|
|
122
|
+
rejectPathsLocked(h.workflow, h.diff, WORKSPACE_ID, USER, [
|
|
123
|
+
'knowledge-base/a.md',
|
|
124
|
+
'knowledge-base/added.md',
|
|
125
|
+
]),
|
|
126
|
+
).rejects.toBeInstanceOf(WorkflowValidationError);
|
|
127
|
+
|
|
128
|
+
// No commit landed and the disk is untouched — the batch is atomic.
|
|
129
|
+
expect(h.calls.some((c) => c.startsWith('commit:'))).toBe(false);
|
|
130
|
+
expect(await fs.readFile(path.join(h.workspaceDir, 'knowledge-base/a.md'), 'utf-8')).toBe('agent version\n');
|
|
131
|
+
await expect(fs.access(path.join(h.workspaceDir, 'knowledge-base/added.md'))).resolves.toBeUndefined();
|
|
132
|
+
}, 15_000);
|
|
133
|
+
|
|
134
|
+
it('surfaces a commit failure as a WorkflowValidationError and releases without committing', async () => {
|
|
135
|
+
const h = await makeHarness({
|
|
136
|
+
commit: () => {
|
|
137
|
+
throw new Error('push rejected');
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
workspaceDir = h.workspaceDir;
|
|
141
|
+
|
|
142
|
+
await expect(
|
|
143
|
+
rejectPathsLocked(h.workflow, h.diff, WORKSPACE_ID, USER, [
|
|
144
|
+
'knowledge-base/a.md',
|
|
145
|
+
]),
|
|
146
|
+
).rejects.toBeInstanceOf(WorkflowValidationError);
|
|
147
|
+
// Locks were dropped via the no-commit path after the failure.
|
|
148
|
+
expect(h.calls.filter((c) => c.startsWith('releaseNoCommit:'))).toHaveLength(1);
|
|
149
|
+
});
|
|
150
|
+
});
|