@bevel-software/platform-core-backend 0.28.1 → 0.30.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/agent-guide/access-control.md +15 -0
- package/agent-guide/skills.md +8 -0
- package/dist/__tests__/test-repositories.d.ts +3 -0
- package/dist/__tests__/test-repositories.d.ts.map +1 -0
- package/dist/__tests__/test-repositories.js +18 -0
- package/dist/__tests__/test-repositories.js.map +1 -0
- package/dist/core/catalog-revision.d.ts.map +1 -1
- package/dist/core/catalog-revision.js +4 -2
- package/dist/core/catalog-revision.js.map +1 -1
- package/dist/core/compose-embed.d.ts +35 -0
- package/dist/core/compose-embed.d.ts.map +1 -0
- package/dist/core/compose-embed.js +14 -0
- package/dist/core/compose-embed.js.map +1 -0
- package/dist/core/core-ports.d.ts +10 -0
- package/dist/core/core-ports.d.ts.map +1 -1
- package/dist/core/core-ports.js.map +1 -1
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +8 -2
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +4 -1
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +57 -18
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/core/lifecycle.d.ts +19 -6
- package/dist/core/lifecycle.d.ts.map +1 -1
- package/dist/core/lifecycle.js +17 -6
- package/dist/core/lifecycle.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +20 -0
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +68 -15
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +282 -58
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/access.routes.d.ts.map +1 -1
- package/dist/modules/access/access.routes.js +20 -6
- package/dist/modules/access/access.routes.js.map +1 -1
- package/dist/modules/agent-instructions/shared-file-rules.d.ts +1 -1
- package/dist/modules/agent-instructions/shared-file-rules.js +1 -1
- package/dist/modules/auth/index.d.ts +15 -0
- package/dist/modules/auth/index.d.ts.map +1 -0
- package/dist/modules/auth/index.js +20 -0
- package/dist/modules/auth/index.js.map +1 -0
- package/dist/modules/diff/diff.service.d.ts.map +1 -1
- package/dist/modules/diff/diff.service.js +7 -1
- package/dist/modules/diff/diff.service.js.map +1 -1
- package/dist/modules/embed/embed.interface.d.ts +22 -0
- package/dist/modules/embed/embed.interface.d.ts.map +1 -1
- package/dist/modules/embed/embed.service.d.ts +28 -14
- package/dist/modules/embed/embed.service.d.ts.map +1 -1
- package/dist/modules/embed/embed.service.js +38 -17
- package/dist/modules/embed/embed.service.js.map +1 -1
- package/dist/modules/embed/embed.tools.d.ts.map +1 -1
- package/dist/modules/embed/embed.tools.js +10 -2
- package/dist/modules/embed/embed.tools.js.map +1 -1
- package/dist/modules/kb-fs/git-dir.d.ts +21 -0
- package/dist/modules/kb-fs/git-dir.d.ts.map +1 -0
- package/dist/modules/kb-fs/git-dir.js +58 -0
- package/dist/modules/kb-fs/git-dir.js.map +1 -0
- package/dist/modules/kb-fs/index.d.ts +9 -0
- package/dist/modules/kb-fs/index.d.ts.map +1 -0
- package/dist/modules/kb-fs/index.js +3 -0
- package/dist/modules/kb-fs/index.js.map +1 -0
- package/dist/modules/kb-fs/ref-files.d.ts +63 -0
- package/dist/modules/kb-fs/ref-files.d.ts.map +1 -0
- package/dist/modules/kb-fs/ref-files.js +249 -0
- package/dist/modules/kb-fs/ref-files.js.map +1 -0
- package/dist/modules/plugins/join-request-jobs.service.d.ts.map +1 -1
- package/dist/modules/plugins/join-request-jobs.service.js +43 -8
- package/dist/modules/plugins/join-request-jobs.service.js.map +1 -1
- package/dist/modules/plugins/teams.routes.d.ts +4 -0
- package/dist/modules/plugins/teams.routes.d.ts.map +1 -1
- package/dist/modules/plugins/teams.routes.js +9 -1
- package/dist/modules/plugins/teams.routes.js.map +1 -1
- package/dist/modules/skills/skills.contract.d.ts.map +1 -1
- package/dist/modules/skills/skills.tools.d.ts +8 -1
- package/dist/modules/skills/skills.tools.d.ts.map +1 -1
- package/dist/modules/skills/skills.tools.js +115 -11
- package/dist/modules/skills/skills.tools.js.map +1 -1
- package/dist/modules/tool-auth/internal-token.service.d.ts +1 -1
- package/dist/modules/tool-auth/tool-auth.middleware.d.ts +1 -2
- package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
- package/dist/modules/tool-auth/tool-auth.middleware.js +1 -2
- package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
- package/dist/modules/tool-helpers/default-branch-write.d.ts +69 -0
- package/dist/modules/tool-helpers/default-branch-write.d.ts.map +1 -0
- package/dist/modules/tool-helpers/default-branch-write.js +63 -0
- package/dist/modules/tool-helpers/default-branch-write.js.map +1 -0
- package/dist/modules/tool-helpers/index.d.ts +1 -0
- package/dist/modules/tool-helpers/index.d.ts.map +1 -1
- package/dist/modules/tool-helpers/index.js +1 -0
- package/dist/modules/tool-helpers/index.js.map +1 -1
- package/dist/modules/tool-helpers/tool-handler.d.ts +2 -2
- package/dist/modules/tool-helpers/tool.contract.d.ts +2 -2
- package/dist/modules/workflow/file-lock.service.d.ts +27 -5
- package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
- package/dist/modules/workflow/file-lock.service.js +70 -18
- package/dist/modules/workflow/file-lock.service.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +42 -0
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +201 -24
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/index.d.ts +8 -0
- package/dist/modules/workflow/index.d.ts.map +1 -0
- package/dist/modules/workflow/index.js +8 -0
- package/dist/modules/workflow/index.js.map +1 -0
- package/dist/modules/workflow/pending-commits.service.d.ts +2 -17
- package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
- package/dist/modules/workflow/pending-commits.service.js +4 -24
- package/dist/modules/workflow/pending-commits.service.js.map +1 -1
- package/dist/modules/workflow/workflow-hooks.d.ts +1 -1
- package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.routes.js +20 -1
- package/dist/modules/workflow/workflow.routes.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +5 -3
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/dist/modules/workspace/agent-access.gate.d.ts +2 -2
- package/dist/modules/workspace/agent-access.gate.js +2 -2
- package/dist/modules/workspace/bounded-matcher.d.ts +79 -0
- package/dist/modules/workspace/bounded-matcher.d.ts.map +1 -0
- package/dist/modules/workspace/bounded-matcher.js +261 -0
- package/dist/modules/workspace/bounded-matcher.js.map +1 -0
- package/dist/modules/workspace/fetch-instant.d.ts +9 -0
- package/dist/modules/workspace/fetch-instant.d.ts.map +1 -0
- package/dist/modules/workspace/fetch-instant.js +13 -0
- package/dist/modules/workspace/fetch-instant.js.map +1 -0
- package/dist/modules/workspace/index.d.ts +17 -0
- package/dist/modules/workspace/index.d.ts.map +1 -0
- package/dist/modules/workspace/index.js +16 -0
- package/dist/modules/workspace/index.js.map +1 -0
- package/dist/modules/workspace/kb-repository.contract.d.ts +124 -0
- package/dist/modules/workspace/kb-repository.contract.d.ts.map +1 -0
- package/dist/modules/workspace/kb-repository.contract.js +19 -0
- package/dist/modules/workspace/kb-repository.contract.js.map +1 -0
- package/dist/modules/workspace/kb-repository.d.ts +236 -0
- package/dist/modules/workspace/kb-repository.d.ts.map +1 -0
- package/dist/modules/workspace/kb-repository.js +811 -0
- package/dist/modules/workspace/kb-repository.js.map +1 -0
- package/dist/modules/workspace/pattern-matcher.contract.d.ts +50 -0
- package/dist/modules/workspace/pattern-matcher.contract.d.ts.map +1 -0
- package/dist/modules/workspace/pattern-matcher.contract.js +27 -0
- package/dist/modules/workspace/pattern-matcher.contract.js.map +1 -0
- package/dist/modules/workspace/routine-write-policy.d.ts +0 -7
- package/dist/modules/workspace/routine-write-policy.d.ts.map +1 -1
- package/dist/modules/workspace/routine-write-policy.js +0 -5
- package/dist/modules/workspace/routine-write-policy.js.map +1 -1
- package/dist/modules/workspace/set-aside-clone.d.ts +40 -0
- package/dist/modules/workspace/set-aside-clone.d.ts.map +1 -1
- package/dist/modules/workspace/set-aside-clone.js +139 -0
- package/dist/modules/workspace/set-aside-clone.js.map +1 -1
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts +99 -0
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
- package/dist/modules/workspace/startup/kb-startup-runner.js +407 -29
- package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +68 -32
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +240 -161
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/modules/workspace/workspace.tools.d.ts +13 -4
- package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.tools.js +268 -356
- package/dist/modules/workspace/workspace.tools.js.map +1 -1
- package/dist/shared/workspace-id.d.ts +17 -0
- package/dist/shared/workspace-id.d.ts.map +1 -1
- package/dist/shared/workspace-id.js +24 -0
- package/dist/shared/workspace-id.js.map +1 -1
- package/dist/test-setup.js +4 -0
- package/dist/test-setup.js.map +1 -1
- package/mcp-app/page.html +142 -8
- package/package.json +3 -3
- package/src/__tests__/test-repositories.ts +22 -0
- package/src/__tests__/workspace-service.ts +69 -0
- package/src/core/__tests__/compose-embed.test.ts +65 -0
- package/src/core/__tests__/lifecycle.test.ts +45 -26
- package/src/core/catalog-revision.ts +4 -2
- package/src/core/compose-embed.ts +50 -0
- package/src/core/core-ports.ts +10 -0
- package/src/core/create-core-server.ts +8 -2
- package/src/core/create-core-services.ts +63 -24
- package/src/core/lifecycle.ts +34 -12
- package/src/modules/access/__tests__/access-control.atref-cache.test.ts +443 -25
- package/src/modules/access/__tests__/access-control.group-read.test.ts +73 -0
- package/src/modules/access/__tests__/access.routes.group-grant.test.ts +66 -0
- package/src/modules/access/__tests__/access.routes.unknown-email.test.ts +2 -0
- package/src/modules/access/access-control.interface.ts +22 -0
- package/src/modules/access/access-control.service.ts +302 -58
- package/src/modules/access/access.routes.ts +22 -6
- package/src/modules/agent-instructions/shared-file-rules.ts +1 -1
- package/src/modules/auth/index.ts +29 -0
- package/src/modules/diff/__tests__/diff.service.seed-atomicity.test.ts +2 -1
- package/src/modules/diff/__tests__/diff.service.test.ts +2 -1
- package/src/modules/diff/__tests__/diff.service.worktree.test.ts +76 -0
- package/src/modules/diff/diff.service.ts +6 -4
- package/src/modules/embed/__tests__/embed.service.test.ts +76 -3
- package/src/modules/embed/__tests__/embed.tools.test.ts +29 -0
- package/src/modules/embed/embed.interface.ts +24 -0
- package/src/modules/embed/embed.service.ts +44 -19
- package/src/modules/embed/embed.tools.ts +10 -2
- package/src/modules/kb-fs/__tests__/git-dir.test.ts +77 -0
- package/src/modules/kb-fs/__tests__/ref-files.test.ts +338 -0
- package/src/modules/kb-fs/git-dir.ts +53 -0
- package/src/modules/kb-fs/index.ts +8 -0
- package/src/modules/kb-fs/ref-files.ts +279 -0
- package/src/modules/plugins/__tests__/join-request-jobs.service.test.ts +73 -6
- package/src/modules/plugins/__tests__/teams.routes.test.ts +55 -0
- package/src/modules/plugins/join-request-jobs.service.ts +45 -8
- package/src/modules/plugins/teams.routes.ts +14 -1
- package/src/modules/settings/__tests__/setup.routes.layout-phase.test.ts +2 -2
- package/src/modules/settings/__tests__/setup.routes.managed-phase.test.ts +2 -2
- package/src/modules/skills/__tests__/branch-skills.tools.test.ts +1 -1
- package/src/modules/skills/skills.contract.ts +4 -0
- package/src/modules/skills/skills.tools.ts +120 -10
- package/src/modules/tool-auth/internal-token.service.ts +1 -1
- package/src/modules/tool-auth/tool-auth.middleware.ts +1 -2
- package/src/modules/tool-helpers/__tests__/branch-declaration.test.ts +2 -2
- package/src/modules/tool-helpers/__tests__/can-write.tools.test.ts +581 -0
- package/src/modules/tool-helpers/default-branch-write.ts +90 -0
- package/src/modules/tool-helpers/index.ts +5 -0
- package/src/modules/tool-helpers/tool-handler.ts +2 -2
- package/src/modules/tool-helpers/tool.contract.ts +2 -2
- package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +42 -3
- package/src/modules/workflow/__tests__/fake-file-lock-db.ts +13 -2
- package/src/modules/workflow/__tests__/workflow.routes.lock-path.test.ts +168 -1
- package/src/modules/workflow/__tests__/workflow.service.delete-branch.test.ts +102 -6
- package/src/modules/workflow/file-lock.service.ts +79 -18
- package/src/modules/workflow/git/__tests__/git.service.changedPathsMemo.test.ts +175 -0
- package/src/modules/workflow/git/__tests__/git.service.readFileAtRef.test.ts +42 -0
- package/src/modules/workflow/git/__tests__/git.service.worktrees.test.ts +284 -0
- package/src/modules/workflow/git/git.service.ts +207 -22
- package/src/modules/workflow/index.ts +7 -0
- package/src/modules/workflow/pending-commits.service.ts +4 -23
- package/src/modules/workflow/workflow-hooks.ts +1 -1
- package/src/modules/workflow/workflow.routes.ts +24 -1
- package/src/modules/workflow/workflow.service.ts +3 -2
- package/src/modules/workspace/__tests__/agent-downloads.test.ts +2 -2
- package/src/modules/workspace/__tests__/bounded-matcher.test.ts +213 -0
- package/src/modules/workspace/__tests__/git-internals.security.test.ts +22 -7
- package/src/modules/workspace/__tests__/kb-repository.test.ts +481 -0
- package/src/modules/workspace/__tests__/rest-tool-arguments.test.ts +14 -0
- package/src/modules/workspace/__tests__/routine-write-policy.test.ts +0 -11
- package/src/modules/workspace/__tests__/workspace-paths-inside-the-repo.test.ts +2 -2
- package/src/modules/workspace/__tests__/workspace.routes.folder-zip-access.test.ts +2 -2
- package/src/modules/workspace/__tests__/workspace.routes.folders.test.ts +4 -3
- package/src/modules/workspace/__tests__/workspace.service.any-workspace-credentials.test.ts +2 -2
- package/src/modules/workspace/__tests__/workspace.service.ensure-remotes-fetched.test.ts +2 -2
- package/src/modules/workspace/__tests__/workspace.service.forget-clone-races.test.ts +12 -5
- package/src/modules/workspace/__tests__/workspace.service.list-cloned.test.ts +6 -6
- package/src/modules/workspace/__tests__/workspace.service.move-never-overwrites.test.ts +2 -1
- package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +2 -1
- package/src/modules/workspace/__tests__/workspace.service.replaced-repository.test.ts +2 -1
- package/src/modules/workspace/__tests__/workspace.service.test.ts +24 -23
- package/src/modules/workspace/__tests__/workspace.service.unknown-branch.test.ts +2 -1
- package/src/modules/workspace/__tests__/workspace.service.worktrees.test.ts +620 -0
- package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +15 -1
- package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +71 -5
- package/src/modules/workspace/__tests__/workspace.tools.test.ts +174 -309
- package/src/modules/workspace/agent-access.gate.ts +2 -2
- package/src/modules/workspace/bounded-matcher.ts +328 -0
- package/src/modules/workspace/fetch-instant.ts +13 -0
- package/src/modules/workspace/index.ts +26 -0
- package/src/modules/workspace/kb-repository.contract.ts +124 -0
- package/src/modules/workspace/kb-repository.ts +844 -0
- package/src/modules/workspace/pattern-matcher.contract.ts +69 -0
- package/src/modules/workspace/routine-write-policy.ts +0 -15
- package/src/modules/workspace/set-aside-clone.ts +140 -0
- package/src/modules/workspace/startup/__tests__/kb-startup-runner.degraded.test.ts +3 -2
- package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +12 -10
- package/src/modules/workspace/startup/__tests__/kb-startup-runner.worktrees.test.ts +613 -0
- package/src/modules/workspace/startup/kb-startup-runner.ts +422 -29
- package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +4 -3
- package/src/modules/workspace/workspace.service.ts +247 -169
- package/src/modules/workspace/workspace.tools.ts +299 -385
- package/src/shared/workspace-id.ts +24 -0
- package/src/test-setup.ts +5 -0
|
@@ -41,6 +41,7 @@ const log = logger('pending-commits');
|
|
|
41
41
|
import type { Database } from '../database/connection.js';
|
|
42
42
|
import { canonicalEmail } from '../../shared/email-identity.js';
|
|
43
43
|
import { pendingCommits } from '../database/schema.js';
|
|
44
|
+
import { canonicalWorkspaceId } from '../../shared/workspace-id.js';
|
|
44
45
|
|
|
45
46
|
/**
|
|
46
47
|
* Transient-failure budget per recovery cycle. After this many consecutive
|
|
@@ -80,29 +81,9 @@ export const N_RECOVERY = 3;
|
|
|
80
81
|
*/
|
|
81
82
|
export const BACKOFF_MS = [1_000, 5_000, 30_000];
|
|
82
83
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
*
|
|
87
|
-
* The id reaches `enqueue` via a route `:id` path param, which Express
|
|
88
|
-
* URL-decodes — so a slashed feature branch arrives as `alice/feature`. But the
|
|
89
|
-
* worker claims per `WorkspaceService.knownWorkspaces()`, whose ids are
|
|
90
|
-
* `encodeURIComponent(branch)` → `alice%2Ffeature`. Those strings differ, so
|
|
91
|
-
* `WHERE workspace_id = …` never matched and the row was never drained —
|
|
92
|
-
* silently stranding every human save on a feature branch (protected branches
|
|
93
|
-
* have no `/`, so encoded == decoded and they were unaffected).
|
|
94
|
-
*
|
|
95
|
-
* `encodeURIComponent(decodeURIComponent(id))` is idempotent and collapses both
|
|
96
|
-
* forms to the encoded id `knownWorkspaces()` uses. A malformed `%` sequence
|
|
97
|
-
* (which `decodeURIComponent` would throw on) is left untouched.
|
|
98
|
-
*/
|
|
99
|
-
export function canonicalWorkspaceId(id: string): string {
|
|
100
|
-
try {
|
|
101
|
-
return encodeURIComponent(decodeURIComponent(id));
|
|
102
|
-
} catch {
|
|
103
|
-
return id;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
84
|
+
// Lives in `shared/workspace-id.ts`, beside the encoding it canonicalises to;
|
|
85
|
+
// re-exported for this module's existing importers.
|
|
86
|
+
export { canonicalWorkspaceId };
|
|
106
87
|
|
|
107
88
|
export interface PendingCommit {
|
|
108
89
|
id: string;
|
|
@@ -65,7 +65,7 @@ export interface AgentOperationContext {
|
|
|
65
65
|
sessionId?: string;
|
|
66
66
|
/**
|
|
67
67
|
* The workspace-relative path the operation targets. Absent for a
|
|
68
|
-
* write-capable operation with no resolvable target
|
|
68
|
+
* write-capable operation with no resolvable target.
|
|
69
69
|
*/
|
|
70
70
|
wsPath?: string;
|
|
71
71
|
/** The branch (draft) whose workspace the operation acts on. */
|
|
@@ -40,6 +40,7 @@ import type { WorkflowEventBus } from './event-bus.js';
|
|
|
40
40
|
import { WorkflowDomainError } from '../../shared/domain-errors.js';
|
|
41
41
|
import { assertValidRelativePath } from '../kb-fs/branch-name.js';
|
|
42
42
|
import { domainErrorBody } from '../../shared/http-errors.js';
|
|
43
|
+
import { canonicalFileIdentity } from '../../shared/canonical-file-identity.js';
|
|
43
44
|
import '../auth/auth.middleware.js'; // Express Request augmentation
|
|
44
45
|
|
|
45
46
|
function toHttpError(
|
|
@@ -501,6 +502,8 @@ export function createWorkflowRoutes(
|
|
|
501
502
|
// answer for that same input, and `toHttpError` passes it through unchanged.
|
|
502
503
|
// Nothing is canonicalised at this layer on purpose: a route-level fix would
|
|
503
504
|
// leave any other caller of the service coordinating on its own identity.
|
|
505
|
+
// (The status read canonicalises once for its read gate only; the lookup
|
|
506
|
+
// still takes the caller's spelling.)
|
|
504
507
|
|
|
505
508
|
router.post('/workspace/:id/workflow/locks', async (req, res) => {
|
|
506
509
|
const user = await requireUser(req, res);
|
|
@@ -589,7 +592,8 @@ export function createWorkflowRoutes(
|
|
|
589
592
|
});
|
|
590
593
|
|
|
591
594
|
router.get('/workspace/:id/workflow/locks', async (req, res) => {
|
|
592
|
-
|
|
595
|
+
const user = await requireUser(req, res);
|
|
596
|
+
if (!user) return;
|
|
593
597
|
const branch = typeof req.query.branch === 'string' ? req.query.branch : '';
|
|
594
598
|
const targetPath = typeof req.query.path === 'string' ? req.query.path : '';
|
|
595
599
|
if (!branch || !targetPath) {
|
|
@@ -597,6 +601,25 @@ export function createWorkflowRoutes(
|
|
|
597
601
|
return;
|
|
598
602
|
}
|
|
599
603
|
try {
|
|
604
|
+
// A lock names who is editing the file, so a caller who may not read
|
|
605
|
+
// the file is refused before the lookup: the answer is the same whether
|
|
606
|
+
// or not it is held. The gate judges the canonical spelling, which is
|
|
607
|
+
// the one the lock is found under (`./kb//x.md` would otherwise slip
|
|
608
|
+
// past the KB-prefix test), and a refused spelling keeps the file
|
|
609
|
+
// verbs' status. A path without the KB prefix is not exempted as
|
|
610
|
+
// non-KB: the git layer also takes the repo-relative form (`GTM/x.md`
|
|
611
|
+
// for `knowledge-base/GTM/x.md`, see `requireReadPermission`), so it is
|
|
612
|
+
// judged as the repository file it would name.
|
|
613
|
+
const canonical = canonicalFileIdentity(targetPath);
|
|
614
|
+
const readable = await accessControl.canRead(
|
|
615
|
+
req.params.id,
|
|
616
|
+
user.email,
|
|
617
|
+
toKbRelative(canonical, kbDirName) ?? canonical,
|
|
618
|
+
);
|
|
619
|
+
if (!readable) {
|
|
620
|
+
res.status(403).json({ error: `You don't have permission to read "${targetPath}".` });
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
600
623
|
res.json({ lock: await workflow.getLock(req.params.id, branch, targetPath) });
|
|
601
624
|
} catch (err) {
|
|
602
625
|
const { status, body } = toHttpError(err);
|
|
@@ -542,7 +542,7 @@ export class WorkflowService implements IWorkflowService {
|
|
|
542
542
|
// One line per deletion, whoever asked: the commit is what a restore
|
|
543
543
|
// starts from, and for a deletion made in the app or by the cleanup this
|
|
544
544
|
// line is the only trace there is.
|
|
545
|
-
// By id
|
|
545
|
+
// By id: log lines carry no email addresses.
|
|
546
546
|
const actor = opts?.systemCleanup ? 'system' : user.id;
|
|
547
547
|
log.info(`branch deleted by ${actor}: "${name}" at ${lastCommit ?? '(no commit)'}`);
|
|
548
548
|
// Retire the deleted branch's own workspace clone, best-effort. A stale
|
|
@@ -629,6 +629,7 @@ export class WorkflowService implements IWorkflowService {
|
|
|
629
629
|
*/
|
|
630
630
|
private async closeIfEmpty(
|
|
631
631
|
number: number,
|
|
632
|
+
// `actor` is a user id or `system`, never an address: it goes into a log line.
|
|
632
633
|
opts: { reason: string; actor: string; workspaceId?: string; fresh?: boolean },
|
|
633
634
|
): Promise<{ base: string } | null> {
|
|
634
635
|
const summary = await this.prs.getPr(number);
|
|
@@ -3360,7 +3361,7 @@ export class WorkflowService implements IWorkflowService {
|
|
|
3360
3361
|
* Returns true when THIS call closed it.
|
|
3361
3362
|
*/
|
|
3362
3363
|
async closeEmptyChangeRequest(number: number, user: AuthUser): Promise<boolean> {
|
|
3363
|
-
const closed = await this.closeIfEmpty(number, { reason: 'found empty', actor: user.
|
|
3364
|
+
const closed = await this.closeIfEmpty(number, { reason: 'found empty', actor: user.id });
|
|
3364
3365
|
if (!closed) return false;
|
|
3365
3366
|
await this.retireMergedSourceBranch(number, closed.base, user);
|
|
3366
3367
|
return true;
|
|
@@ -22,7 +22,6 @@ import { WorkflowHooks } from '../../workflow/workflow-hooks.js';
|
|
|
22
22
|
import { ToolDescriptionNotes } from '../agent-access.gate.js';
|
|
23
23
|
import { SpillStore } from '../spill-store.js';
|
|
24
24
|
import { DocExtractService } from '../file-readers/doc-extract.service.js';
|
|
25
|
-
import { WorkspaceService } from '../workspace.service.js';
|
|
26
25
|
import { workspaceIdForBranch } from '../../../shared/workspace-id.js';
|
|
27
26
|
import { AgentDownloadStore, DOWNLOAD_TOKEN_REFUSAL } from '../agent-download.store.js';
|
|
28
27
|
import { attachmentDisposition, createAgentDownloadRoutes, createDownloadFetcherIdentifier } from '../agent-download.routes.js';
|
|
@@ -31,6 +30,7 @@ import jwt from 'jsonwebtoken';
|
|
|
31
30
|
import { buildDownload, DOWNLOAD_PERMISSION_REQUIRED, NOT_FOUND } from '../agent-download.builder.js';
|
|
32
31
|
import { READ_ONLY_CODE, type IWriteAccess } from '../../write-access/write-access.js';
|
|
33
32
|
import { sharedFileRules } from '../../agent-instructions/shared-file-rules.js';
|
|
33
|
+
import { newWorkspaceService } from '../../../__tests__/workspace-service.js';
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
36
|
* `request_file_download` end to end, over the REAL access resolver on a
|
|
@@ -136,7 +136,7 @@ async function start(options: StartOptions = {}): Promise<Started> {
|
|
|
136
136
|
await mkdir(dirname(abs), { recursive: true });
|
|
137
137
|
await writeFile(abs, contents);
|
|
138
138
|
}
|
|
139
|
-
const workspaceService =
|
|
139
|
+
const workspaceService = newWorkspaceService(
|
|
140
140
|
root,
|
|
141
141
|
'https://example.invalid/kb.git',
|
|
142
142
|
testKbContext({ kbDirName: KB }),
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { Worker } from 'node:worker_threads';
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { WorkerPatternMatcher, type WorkerPatternMatcherOptions } from '../bounded-matcher.js';
|
|
4
|
+
import { PatternTooExpensiveError } from '../pattern-matcher.contract.js';
|
|
5
|
+
|
|
6
|
+
/** The QA finding's pattern, and a 40-character line it backtracks on for ever. */
|
|
7
|
+
const CATASTROPHIC = '^(a+)+$';
|
|
8
|
+
const LONG_LINE = `${'a'.repeat(39)}!`;
|
|
9
|
+
/** The ticket's per-search budget (Razvan, 2026-10-15). */
|
|
10
|
+
const FIVE_SECONDS = 5_000;
|
|
11
|
+
|
|
12
|
+
let matchers: WorkerPatternMatcher[] = [];
|
|
13
|
+
/** A matcher of the test's own, closed after it — the defaults unless a test says otherwise. */
|
|
14
|
+
function matcher(opts?: WorkerPatternMatcherOptions): WorkerPatternMatcher {
|
|
15
|
+
const m = new WorkerPatternMatcher(opts);
|
|
16
|
+
matchers.push(m);
|
|
17
|
+
return m;
|
|
18
|
+
}
|
|
19
|
+
afterEach(async () => {
|
|
20
|
+
await Promise.all(matchers.map((m) => m.close()));
|
|
21
|
+
matchers = [];
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The longest the event loop went without running a timer while `work` ran.
|
|
26
|
+
* A pattern run on the loop would show the whole match time here.
|
|
27
|
+
*/
|
|
28
|
+
async function longestStall(work: () => Promise<unknown>): Promise<number> {
|
|
29
|
+
let last = Date.now();
|
|
30
|
+
let worst = 0;
|
|
31
|
+
const tick = setInterval(() => {
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
worst = Math.max(worst, now - last);
|
|
34
|
+
last = now;
|
|
35
|
+
}, 10);
|
|
36
|
+
try {
|
|
37
|
+
await work().catch(() => {});
|
|
38
|
+
} finally {
|
|
39
|
+
clearInterval(tick);
|
|
40
|
+
}
|
|
41
|
+
return Math.max(worst, Date.now() - last);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe('WorkerPatternMatcher', () => {
|
|
45
|
+
it('stops a catastrophic pattern over a 40-character line with the worded 400 in its 5 s budget, and never stalls the loop', async () => {
|
|
46
|
+
expect(LONG_LINE).toHaveLength(40);
|
|
47
|
+
const started = Date.now();
|
|
48
|
+
let caught: unknown;
|
|
49
|
+
const stall = await longestStall(async () => {
|
|
50
|
+
try {
|
|
51
|
+
await matcher().search(CATASTROPHIC, '').matchingLines([LONG_LINE], 200);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
caught = err;
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
const took = Date.now() - started;
|
|
57
|
+
expect(caught).toBeInstanceOf(PatternTooExpensiveError);
|
|
58
|
+
expect((caught as PatternTooExpensiveError).status).toBe(400);
|
|
59
|
+
expect((caught as Error).message).toMatch(/^Pattern too expensive: /);
|
|
60
|
+
expect((caught as Error).message).toContain('simpler pattern');
|
|
61
|
+
expect((caught as Error).message).toContain("this search's 5 s budget");
|
|
62
|
+
expect(took).toBeGreaterThanOrEqual(FIVE_SECONDS - 100);
|
|
63
|
+
expect(took).toBeLessThan(FIVE_SECONDS + 2_000);
|
|
64
|
+
expect(stall).toBeLessThan(500);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('answers ordinary patterns with exactly the lines RegExp#test matches', async () => {
|
|
68
|
+
const m = matcher();
|
|
69
|
+
const lines = ['hello world', 'Getting started', 'aaa', 'line 42', '', 'foo|bar', 'bar baz', LONG_LINE];
|
|
70
|
+
for (const [source, flags] of [
|
|
71
|
+
['world', ''],
|
|
72
|
+
['^aaa$', ''],
|
|
73
|
+
['getting started', 'i'],
|
|
74
|
+
['[0-9]+', ''],
|
|
75
|
+
['foo|baz', ''],
|
|
76
|
+
['^$', ''],
|
|
77
|
+
['a+!$', ''],
|
|
78
|
+
] as const) {
|
|
79
|
+
const re = new RegExp(source, flags);
|
|
80
|
+
const expected = lines.flatMap((l, i) => (re.test(l) ? [i] : []));
|
|
81
|
+
expect(await m.search(source, flags).matchingLines(lines, 1000), `/${source}/${flags}`).toEqual(expected);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('returns only the first `limit` matches', async () => {
|
|
86
|
+
expect(await matcher().search('x', '').matchingLines(['x', 'y', 'x', 'x'], 2)).toEqual([0, 2]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('refuses a sizing that could never serve a search when it is built, not when a search hangs on it', () => {
|
|
90
|
+
expect(() => new WorkerPatternMatcher({ poolMax: 0 })).toThrow(/poolMax must be an integer from 1 /);
|
|
91
|
+
// Past the longest delay setTimeout honours, the timer would fire after 1 ms and stop every search.
|
|
92
|
+
expect(() => new WorkerPatternMatcher({ budgetMs: 2_147_483_647 })).toThrow(/budgetMs must be an integer from 1 to 2147483397/);
|
|
93
|
+
expect(() => new WorkerPatternMatcher({ poolMax: 1.5 })).toThrow(RangeError);
|
|
94
|
+
expect(() => new WorkerPatternMatcher({ budgetMs: 0 })).toThrow(/budgetMs/);
|
|
95
|
+
expect(() => new WorkerPatternMatcher({ maxWaiting: -1 })).toThrow(/maxWaiting/);
|
|
96
|
+
expect(() => new WorkerPatternMatcher({ batchChars: Number.NaN })).toThrow(/batchChars/);
|
|
97
|
+
expect(() => new WorkerPatternMatcher({ batchLines: 0 })).toThrow(/batchLines/);
|
|
98
|
+
expect(() => matcher({ maxWaiting: 0 })).not.toThrow();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('serves an ordinary search under the largest budget the timer can hold', async () => {
|
|
102
|
+
expect(await matcher({ budgetMs: 2_147_483_647 - 250 }).search('b', '').matchingLines(['a', 'b'], 10)).toEqual([1]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('refuses invalid syntax with the 400 grep always gave', () => {
|
|
106
|
+
expect(() => matcher().search('(', '')).toThrow(/^Invalid regex: /);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('refuses a pattern too long to compile on the request thread, and accepts one at the limit', () => {
|
|
110
|
+
const m = matcher({ maxPatternLength: 10 });
|
|
111
|
+
expect(() => m.search('a'.repeat(11), '')).toThrow(/^Pattern too long: /);
|
|
112
|
+
expect(() => m.search('a'.repeat(10), '')).not.toThrow();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('hands a large file over in batches and answers the same line numbers, up to `limit`', async () => {
|
|
116
|
+
const m = matcher({ batchChars: 100 });
|
|
117
|
+
const filler = 'x'.repeat(9);
|
|
118
|
+
const lines = Array.from({ length: 250 }, (_, i) => (i % 7 === 0 ? `hit ${i}` : filler));
|
|
119
|
+
const expected = lines.flatMap((l, i) => (l.startsWith('hit') ? [i] : []));
|
|
120
|
+
expect(expected.at(-1)! * filler.length).toBeGreaterThan(200);
|
|
121
|
+
expect(await m.search('^hit', '').matchingLines(lines, 10_000)).toEqual(expected);
|
|
122
|
+
expect(await m.search('^hit', '').matchingLines(lines, 3)).toEqual(expected.slice(0, 3));
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('caps a batch by its line count too, so a file of empty lines never goes over in one message', async () => {
|
|
126
|
+
const m = matcher({ batchLines: 100 });
|
|
127
|
+
const lines = Array.from({ length: 1_050 }, (_, i) => (i % 97 === 0 ? 'hit' : ''));
|
|
128
|
+
const expected = lines.flatMap((l, i) => (l === 'hit' ? [i] : []));
|
|
129
|
+
const posted = vi.spyOn(Worker.prototype, 'postMessage');
|
|
130
|
+
try {
|
|
131
|
+
expect(await m.search('^hit$', '').matchingLines(lines, 10_000)).toEqual(expected);
|
|
132
|
+
expect(await m.search('^$', '').matchingLines(lines, 10_000)).toHaveLength(lines.length - expected.length);
|
|
133
|
+
const postedLines = posted.mock.calls.map(([msg]) => ((msg as { lines?: string[] }).lines ?? []).length);
|
|
134
|
+
expect(postedLines.filter((n) => n > 0)).toHaveLength(2 * 11);
|
|
135
|
+
expect(Math.max(...postedLines)).toBeLessThanOrEqual(100);
|
|
136
|
+
} finally {
|
|
137
|
+
posted.mockRestore();
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('hands a line longer than one batch over in shared memory, never in a message, and matches it exactly', async () => {
|
|
142
|
+
const m = matcher({ batchChars: 100 });
|
|
143
|
+
// A lone surrogate included: the line must reach the worker code unit for code unit.
|
|
144
|
+
const long = `${'x'.repeat(1_000)}\uD800 tail ${'y'.repeat(500)}`;
|
|
145
|
+
const lines = ['short', long, 'z'.repeat(400), 'end tail'];
|
|
146
|
+
const posted = vi.spyOn(Worker.prototype, 'postMessage');
|
|
147
|
+
try {
|
|
148
|
+
for (const pattern of ['tail', '\\uD800 tail', '^x+\\uD800', 'y{500}$', '^z+$', 'nowhere']) {
|
|
149
|
+
const re = new RegExp(pattern);
|
|
150
|
+
const expected = lines.flatMap((l, i) => (re.test(l) ? [i] : []));
|
|
151
|
+
expect(await m.search(pattern, '').matchingLines(lines, 10_000)).toEqual(expected);
|
|
152
|
+
}
|
|
153
|
+
const postedChars = posted.mock.calls.map(([msg]) =>
|
|
154
|
+
((msg as { lines?: string[] }).lines ?? []).reduce((n, l) => n + l.length, 0),
|
|
155
|
+
);
|
|
156
|
+
expect(postedChars.length).toBeGreaterThan(0);
|
|
157
|
+
expect(Math.max(...postedChars)).toBeLessThanOrEqual(100);
|
|
158
|
+
} finally {
|
|
159
|
+
posted.mockRestore();
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('refuses a search with 503 once the queue for a worker is full, rather than holding its lines', async () => {
|
|
164
|
+
const m = matcher({ budgetMs: 500, poolMax: 2, maxWaiting: 2 });
|
|
165
|
+
// Every worker held by a pattern that runs out its budget, the queue filled behind them.
|
|
166
|
+
const holding = Promise.allSettled(Array.from({ length: 2 }, () => m.search(CATASTROPHIC, '').matchingLines([LONG_LINE], 1)));
|
|
167
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
168
|
+
const queued = Array.from({ length: 2 }, () => m.search('b', '').matchingLines(['b'], 1));
|
|
169
|
+
await expect(m.search('b', '').matchingLines(['b'], 1)).rejects.toMatchObject({
|
|
170
|
+
status: 503,
|
|
171
|
+
details: { code: 'search_busy' },
|
|
172
|
+
});
|
|
173
|
+
// The queued searches are served once the workers come free.
|
|
174
|
+
expect(await Promise.all(queued)).toEqual(queued.map(() => [0]));
|
|
175
|
+
expect((await holding).every((r) => r.status === 'rejected')).toBe(true);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('stops two expensive searches at once, and a search started meanwhile still answers', async () => {
|
|
179
|
+
const m = matcher({ budgetMs: 500 });
|
|
180
|
+
const results = await Promise.allSettled([
|
|
181
|
+
m.search(CATASTROPHIC, '').matchingLines([LONG_LINE], 10),
|
|
182
|
+
m.search('(a|aa)+$', '').matchingLines([`${'a'.repeat(60)}!`], 10),
|
|
183
|
+
m.search('aaa', '').matchingLines(['xaaay'], 10),
|
|
184
|
+
]);
|
|
185
|
+
expect(results[0]).toMatchObject({ status: 'rejected', reason: expect.any(PatternTooExpensiveError) });
|
|
186
|
+
expect(results[1]).toMatchObject({ status: 'rejected', reason: expect.any(PatternTooExpensiveError) });
|
|
187
|
+
expect(results[2]).toEqual({ status: 'fulfilled', value: [0] });
|
|
188
|
+
// The terminated workers were replaced: the pool still serves.
|
|
189
|
+
expect(await m.search('b', '').matchingLines(['a', 'b'], 10)).toEqual([1]);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('ends its workers on close: a search running then, and one after, is answered busy', async () => {
|
|
193
|
+
const m = matcher();
|
|
194
|
+
const running = m.search(CATASTROPHIC, '').matchingLines([LONG_LINE], 1);
|
|
195
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
196
|
+
const started = Date.now();
|
|
197
|
+
await m.close();
|
|
198
|
+
await expect(running).rejects.toMatchObject({ status: 503, details: { code: 'search_busy' } });
|
|
199
|
+
expect(Date.now() - started).toBeLessThan(1_000);
|
|
200
|
+
await expect(m.search('b', '').matchingLines(['b'], 1)).rejects.toMatchObject({ status: 503 });
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('answers busy at once when closed while a long line is still being written to shared memory', async () => {
|
|
204
|
+
// A line of a million characters written ten at a time: many turns of the loop to close in.
|
|
205
|
+
const m = matcher({ budgetMs: 3_000, batchChars: 10 });
|
|
206
|
+
const running = m.search('nowhere', '').matchingLines(['a'.repeat(1_000_000)], 1);
|
|
207
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
208
|
+
const started = Date.now();
|
|
209
|
+
await m.close();
|
|
210
|
+
await expect(running).rejects.toMatchObject({ status: 503, details: { code: 'search_busy' } });
|
|
211
|
+
expect(Date.now() - started).toBeLessThan(1_000);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
@@ -5,7 +5,7 @@ import { tmpdir } from 'node:os';
|
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import AdmZip from 'adm-zip';
|
|
7
7
|
import express from 'express';
|
|
8
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
8
|
+
import { afterEach, beforeEach, describe, expect, it, vi, afterAll } from 'vitest';
|
|
9
9
|
import { testKbContext } from '../../../__tests__/kb-context.js';
|
|
10
10
|
import type { IWorkflowService } from '@bevel-software/platform-shared';
|
|
11
11
|
import { GIT_INTERNALS_MESSAGE, GitInternalsError } from '../../../shared/domain-errors.js';
|
|
@@ -35,6 +35,12 @@ import { createDiffRoutes } from '../../diff/diff.routes.js';
|
|
|
35
35
|
import { DiffService } from '../../diff/diff.service.js';
|
|
36
36
|
import { WorkspaceMutex } from '../../kb-fs/mutex.js';
|
|
37
37
|
import { WorkspaceService } from '../workspace.service.js';
|
|
38
|
+
import { WorkerPatternMatcher } from '../bounded-matcher.js';
|
|
39
|
+
import { newWorkspaceService } from '../../../__tests__/workspace-service.js';
|
|
40
|
+
|
|
41
|
+
/** The grep matcher this file's harness wires, closed once the file is done. */
|
|
42
|
+
const patternMatcher = new WorkerPatternMatcher();
|
|
43
|
+
afterAll(() => patternMatcher.close());
|
|
38
44
|
|
|
39
45
|
/**
|
|
40
46
|
* Security regression: the repository's internal git folder is unreachable
|
|
@@ -244,7 +250,7 @@ describe('workspace tools refuse the git folder', () => {
|
|
|
244
250
|
|
|
245
251
|
beforeEach(async () => {
|
|
246
252
|
workflow = makeWorkflow();
|
|
247
|
-
const service =
|
|
253
|
+
const service = newWorkspaceService(root, 'https://example.invalid/kb.git', testKbContext({ kbDirName: KB }), new NodeFs());
|
|
248
254
|
unzipSpy = vi.spyOn(service, 'unzipFile');
|
|
249
255
|
bootstrappedSpy = vi.spyOn(service, 'hasBootstrappedWorkspace');
|
|
250
256
|
const lockingFs = new LockingFilesystem(
|
|
@@ -282,6 +288,15 @@ describe('workspace tools refuse the git folder', () => {
|
|
|
282
288
|
{ recoveryBotEmail: 'recovery-bot@bevel.local', hooks: new WorkflowHooks(), notes: new ToolDescriptionNotes() },
|
|
283
289
|
new RoutineWritePolicyService(),
|
|
284
290
|
{} as never,
|
|
291
|
+
// Skill check, change gate, uploads, guide, downloads, starter packs, disk: not this harness's business.
|
|
292
|
+
undefined,
|
|
293
|
+
undefined,
|
|
294
|
+
undefined,
|
|
295
|
+
undefined,
|
|
296
|
+
undefined,
|
|
297
|
+
undefined,
|
|
298
|
+
undefined,
|
|
299
|
+
patternMatcher,
|
|
285
300
|
);
|
|
286
301
|
app.use('/api', router);
|
|
287
302
|
({ server, baseUrl } = await listen(app));
|
|
@@ -419,7 +434,7 @@ describe('workspace routes refuse the git folder', () => {
|
|
|
419
434
|
|
|
420
435
|
beforeEach(async () => {
|
|
421
436
|
workflow = makeWorkflow();
|
|
422
|
-
service =
|
|
437
|
+
service = newWorkspaceService(root, 'https://example.invalid/kb.git', testKbContext({ kbDirName: KB }), new NodeFs());
|
|
423
438
|
const app = express();
|
|
424
439
|
app.use(express.json());
|
|
425
440
|
app.use('/api', (req, _res, next) => {
|
|
@@ -566,7 +581,7 @@ describe('WorkspaceService refuses the git folder on its own', () => {
|
|
|
566
581
|
let service: WorkspaceService;
|
|
567
582
|
|
|
568
583
|
beforeEach(() => {
|
|
569
|
-
service =
|
|
584
|
+
service = newWorkspaceService(root, 'https://example.invalid/kb.git', testKbContext({ kbDirName: KB }), new NodeFs());
|
|
570
585
|
});
|
|
571
586
|
|
|
572
587
|
afterEach(async () => {
|
|
@@ -623,7 +638,7 @@ describe('WorkspaceService refuses the git folder on its own', () => {
|
|
|
623
638
|
return { stdout: '.git\n', stderr: '', code: 0 };
|
|
624
639
|
}),
|
|
625
640
|
};
|
|
626
|
-
const refService =
|
|
641
|
+
const refService = newWorkspaceService(root, 'https://example.invalid/kb.git', testKbContext({ kbDirName: KB }), new NodeFs(), gitRunner as never);
|
|
627
642
|
|
|
628
643
|
for (const [form, raw] of Object.entries(fileForms('config'))) {
|
|
629
644
|
if (!form.startsWith('symlinked')) continue;
|
|
@@ -679,7 +694,7 @@ describe('the route guard on its own', () => {
|
|
|
679
694
|
beforeEach(async () => {
|
|
680
695
|
reached = 0;
|
|
681
696
|
userId = USER.id;
|
|
682
|
-
const service =
|
|
697
|
+
const service = newWorkspaceService(root, 'https://example.invalid/kb.git', testKbContext({ kbDirName: KB }), new NodeFs());
|
|
683
698
|
const app = express();
|
|
684
699
|
// The server's own order: the guard ahead of the body parser, then again
|
|
685
700
|
// on the parsed body, then again once the caller is known.
|
|
@@ -779,7 +794,7 @@ describe('DiffService refuses the git folder on its own', () => {
|
|
|
779
794
|
let diffService: DiffService;
|
|
780
795
|
|
|
781
796
|
beforeEach(() => {
|
|
782
|
-
const service =
|
|
797
|
+
const service = newWorkspaceService(root, 'https://example.invalid/kb.git', testKbContext({ kbDirName: KB }), new NodeFs());
|
|
783
798
|
diffService = new DiffService(service, new WorkspaceMutex(), root, join(root, 'backups'), KB, new NodeFs());
|
|
784
799
|
});
|
|
785
800
|
|