@bevel-software/platform-core-backend 0.29.0 → 0.31.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 +49 -4
- package/agent-guide/html-views.md +76 -2
- 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.js +3 -3
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +3 -2
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +3 -0
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +35 -5
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/core/lifecycle.d.ts +8 -0
- package/dist/core/lifecycle.d.ts.map +1 -1
- package/dist/core/lifecycle.js +4 -0
- 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 +32 -1
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +135 -17
- 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/audit/agent-audit.service.d.ts.map +1 -1
- package/dist/modules/audit/agent-audit.service.js +66 -6
- package/dist/modules/audit/agent-audit.service.js.map +1 -1
- package/dist/modules/audit/audit.contract.d.ts +52 -8
- package/dist/modules/audit/audit.contract.d.ts.map +1 -1
- package/dist/modules/audit/audit.contract.js +37 -1
- package/dist/modules/audit/audit.contract.js.map +1 -1
- package/dist/modules/audit/audit.routes.d.ts +1 -1
- package/dist/modules/audit/audit.routes.d.ts.map +1 -1
- package/dist/modules/audit/audit.routes.js +15 -13
- package/dist/modules/audit/audit.routes.js.map +1 -1
- package/dist/modules/audit/request-audit.d.ts +21 -10
- package/dist/modules/audit/request-audit.d.ts.map +1 -1
- package/dist/modules/audit/request-audit.js +26 -10
- package/dist/modules/audit/request-audit.js.map +1 -1
- package/dist/modules/database/core-schema.d.ts +22 -3
- package/dist/modules/database/core-schema.d.ts.map +1 -1
- package/dist/modules/database/core-schema.js +15 -6
- package/dist/modules/database/core-schema.js.map +1 -1
- 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/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 +3 -0
- package/dist/modules/kb-fs/index.d.ts.map +1 -1
- package/dist/modules/kb-fs/index.js +3 -1
- package/dist/modules/kb-fs/index.js.map +1 -1
- package/dist/modules/kb-fs/locking-filesystem.d.ts.map +1 -1
- package/dist/modules/kb-fs/locking-filesystem.js +3 -12
- package/dist/modules/kb-fs/locking-filesystem.js.map +1 -1
- package/dist/modules/kb-fs/mutex.d.ts +11 -0
- package/dist/modules/kb-fs/mutex.d.ts.map +1 -1
- package/dist/modules/kb-fs/mutex.js +14 -0
- package/dist/modules/kb-fs/mutex.js.map +1 -1
- 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/mcp/mcp-auth.middleware.d.ts +14 -0
- package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.js +4 -0
- package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
- package/dist/modules/mcp/mcp.routes.d.ts +5 -0
- package/dist/modules/mcp/mcp.routes.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.routes.js +67 -0
- package/dist/modules/mcp/mcp.routes.js.map +1 -1
- package/dist/modules/mcp/mcp.service.d.ts +107 -27
- package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.service.js +257 -50
- package/dist/modules/mcp/mcp.service.js.map +1 -1
- package/dist/modules/mcp/request-context.d.ts +25 -0
- package/dist/modules/mcp/request-context.d.ts.map +1 -0
- package/dist/modules/mcp/request-context.js +11 -0
- package/dist/modules/mcp/request-context.js.map +1 -0
- package/dist/modules/mcp/shared-definitions.d.ts +56 -0
- package/dist/modules/mcp/shared-definitions.d.ts.map +1 -0
- package/dist/modules/mcp/shared-definitions.js +117 -0
- package/dist/modules/mcp/shared-definitions.js.map +1 -0
- package/dist/modules/mcp/surface-cache.d.ts +70 -0
- package/dist/modules/mcp/surface-cache.d.ts.map +1 -0
- package/dist/modules/mcp/surface-cache.js +104 -0
- package/dist/modules/mcp/surface-cache.js.map +1 -0
- 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/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/tool-context.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool-context.js +33 -12
- package/dist/modules/tool-helpers/tool-context.js.map +1 -1
- package/dist/modules/tool-helpers/tool-handler.d.ts +2 -2
- package/dist/modules/tool-helpers/tool.contract.d.ts +10 -2
- package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
- package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts.map +1 -1
- package/dist/modules/workflow/agent-tools/change-request-read.tools.js +3 -14
- package/dist/modules/workflow/agent-tools/change-request-read.tools.js.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.d.ts +18 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.js +453 -2
- package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +142 -4
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +645 -128
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/read-on-branch.d.ts +35 -0
- package/dist/modules/workflow/read-on-branch.d.ts.map +1 -0
- package/dist/modules/workflow/read-on-branch.js +58 -0
- package/dist/modules/workflow/read-on-branch.js.map +1 -0
- 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 +22 -41
- package/dist/modules/workflow/workflow.routes.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +48 -8
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +163 -16
- 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 +9 -0
- package/dist/modules/workspace/index.d.ts.map +1 -1
- package/dist/modules/workspace/index.js +10 -0
- package/dist/modules/workspace/index.js.map +1 -1
- 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 +162 -336
- package/dist/modules/workspace/workspace.tools.js.map +1 -1
- package/dist/shared/git.contract.d.ts +7 -0
- package/dist/shared/git.contract.d.ts.map +1 -1
- package/dist/shared/git.contract.js +9 -0
- package/dist/shared/git.contract.js.map +1 -1
- package/dist/shared/no-link-below.d.ts +10 -0
- package/dist/shared/no-link-below.d.ts.map +1 -0
- package/dist/shared/no-link-below.js +30 -0
- package/dist/shared/no-link-below.js.map +1 -0
- package/dist/test-setup.js +4 -0
- package/dist/test-setup.js.map +1 -1
- package/migrations/0018_agent_events_page.sql +11 -0
- package/migrations/meta/0018_snapshot.json +2429 -0
- package/migrations/meta/_journal.json +8 -1
- 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__/lifecycle.test.ts +16 -0
- package/src/core/catalog-revision.ts +3 -3
- package/src/core/create-core-server.ts +3 -2
- package/src/core/create-core-services.ts +39 -4
- package/src/core/lifecycle.ts +10 -0
- package/src/modules/access/__tests__/access-control.atref-cache.test.ts +144 -4
- 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 +138 -15
- package/src/modules/access/access.routes.ts +22 -6
- package/src/modules/agent-guide/__tests__/agent-guide.test.ts +22 -1
- package/src/modules/agent-instructions/shared-file-rules.ts +1 -1
- package/src/modules/audit/__tests__/agent-audit.service.test.ts +78 -1
- package/src/modules/audit/__tests__/audit.routes.test.ts +43 -5
- package/src/modules/audit/agent-audit.service.ts +66 -6
- package/src/modules/audit/audit.contract.ts +62 -10
- package/src/modules/audit/audit.routes.ts +14 -12
- package/src/modules/audit/request-audit.ts +28 -13
- package/src/modules/database/core-schema.ts +15 -6
- 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/kb-fs/__tests__/git-dir.test.ts +77 -0
- package/src/modules/kb-fs/__tests__/mutex.canonical.test.ts +57 -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 +3 -0
- package/src/modules/kb-fs/locking-filesystem.ts +3 -12
- package/src/modules/kb-fs/mutex.ts +16 -0
- package/src/modules/kb-fs/ref-files.ts +279 -0
- package/src/modules/mcp/__tests__/mcp-routes-harness.ts +7 -1
- package/src/modules/mcp/__tests__/mcp.routes.page.test.ts +115 -0
- package/src/modules/mcp/__tests__/mcp.surface-cache.test.ts +521 -0
- package/src/modules/mcp/__tests__/shared-definitions.test.ts +28 -0
- package/src/modules/mcp/__tests__/surface-cache.test.ts +127 -0
- package/src/modules/mcp/mcp-auth.middleware.ts +21 -0
- package/src/modules/mcp/mcp.routes.ts +68 -0
- package/src/modules/mcp/mcp.service.ts +315 -51
- package/src/modules/mcp/request-context.ts +34 -0
- package/src/modules/mcp/shared-definitions.ts +121 -0
- package/src/modules/mcp/surface-cache.ts +154 -0
- package/src/modules/plugins/__tests__/teams.routes.test.ts +55 -0
- 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/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/tool-context.ts +37 -18
- package/src/modules/tool-helpers/tool-handler.ts +2 -2
- package/src/modules/tool-helpers/tool.contract.ts +10 -2
- package/src/modules/tool-registry/__tests__/own-tool-schemas.test.ts +1 -1
- package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +7 -2
- package/src/modules/workflow/__tests__/workflow.routes.history-read-gate.test.ts +23 -12
- package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +250 -4
- package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +464 -4
- package/src/modules/workflow/agent-tools/change-request-read.tools.ts +4 -22
- package/src/modules/workflow/agent-tools/workflow.tools.ts +462 -1
- package/src/modules/workflow/git/__tests__/git.service.changedPathsMemo.test.ts +175 -0
- package/src/modules/workflow/git/__tests__/git.service.diffFileBetweenBranches.test.ts +76 -11
- package/src/modules/workflow/git/__tests__/git.service.mergeIntoBranch.test.ts +721 -0
- package/src/modules/workflow/git/__tests__/git.service.worktrees.test.ts +284 -0
- package/src/modules/workflow/git/git.service.ts +719 -126
- package/src/modules/workflow/read-on-branch.ts +79 -0
- package/src/modules/workflow/workflow-hooks.ts +1 -1
- package/src/modules/workflow/workflow.routes.ts +21 -39
- package/src/modules/workflow/workflow.service.ts +203 -16
- 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 +153 -298
- 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 +23 -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 +179 -365
- package/src/shared/__tests__/no-link-below.test.ts +45 -0
- package/src/shared/git.contract.ts +10 -0
- package/src/shared/no-link-below.ts +29 -0
- package/src/test-setup.ts +5 -0
|
@@ -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
|
|