@bevel-software/platform-core-backend 0.21.0 → 0.23.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/dist/core/core-ports.d.ts +11 -1
- 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 +3 -3
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +62 -13
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +22 -3
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +158 -23
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/core/lifecycle.d.ts +34 -1
- package/dist/core/lifecycle.d.ts.map +1 -1
- package/dist/core/lifecycle.js +89 -13
- package/dist/core/lifecycle.js.map +1 -1
- package/dist/core-config.d.ts +0 -12
- package/dist/core-config.d.ts.map +1 -1
- package/dist/core-config.js +11 -13
- package/dist/core-config.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +42 -12
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +67 -10
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +214 -35
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/access-requests.contract.d.ts +75 -0
- package/dist/modules/access/access-requests.contract.d.ts.map +1 -0
- package/dist/modules/access/access-requests.contract.js +20 -0
- package/dist/modules/access/access-requests.contract.js.map +1 -0
- package/dist/modules/access/access-requests.routes.d.ts +35 -0
- package/dist/modules/access/access-requests.routes.d.ts.map +1 -0
- package/dist/modules/access/access-requests.routes.js +237 -0
- package/dist/modules/access/access-requests.routes.js.map +1 -0
- package/dist/modules/access/access-requests.service.d.ts +123 -0
- package/dist/modules/access/access-requests.service.d.ts.map +1 -0
- package/dist/modules/access/access-requests.service.js +337 -0
- package/dist/modules/access/access-requests.service.js.map +1 -0
- package/dist/modules/access/access.routes.d.ts.map +1 -1
- package/dist/modules/access/access.routes.js +13 -15
- package/dist/modules/access/access.routes.js.map +1 -1
- package/dist/modules/access-model/access-grammar.d.ts +12 -0
- package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
- package/dist/modules/access-model/access-grammar.js +24 -0
- package/dist/modules/access-model/access-grammar.js.map +1 -1
- package/dist/modules/auth/account-admission.d.ts +58 -7
- package/dist/modules/auth/account-admission.d.ts.map +1 -1
- package/dist/modules/auth/account-admission.js +44 -1
- package/dist/modules/auth/account-admission.js.map +1 -1
- package/dist/modules/auth/account.routes.d.ts +1 -1
- package/dist/modules/auth/account.routes.d.ts.map +1 -1
- package/dist/modules/auth/account.routes.js +61 -5
- package/dist/modules/auth/account.routes.js.map +1 -1
- package/dist/modules/auth/auth.middleware.d.ts +1 -1
- package/dist/modules/auth/auth.middleware.d.ts.map +1 -1
- package/dist/modules/auth/auth.middleware.js +18 -7
- package/dist/modules/auth/auth.middleware.js.map +1 -1
- package/dist/modules/auth/auth.routes.d.ts.map +1 -1
- package/dist/modules/auth/auth.routes.js +8 -0
- package/dist/modules/auth/auth.routes.js.map +1 -1
- package/dist/modules/auth/auth.service.d.ts +96 -3
- package/dist/modules/auth/auth.service.d.ts.map +1 -1
- package/dist/modules/auth/auth.service.js +187 -9
- package/dist/modules/auth/auth.service.js.map +1 -1
- package/dist/modules/auth/oidc-auth-provider.d.ts.map +1 -1
- package/dist/modules/auth/oidc-auth-provider.js +7 -2
- package/dist/modules/auth/oidc-auth-provider.js.map +1 -1
- package/dist/modules/database/core-schema.d.ts +41 -24
- package/dist/modules/database/core-schema.d.ts.map +1 -1
- package/dist/modules/database/core-schema.js +35 -27
- package/dist/modules/database/core-schema.js.map +1 -1
- package/dist/modules/github-app/github-app.client.d.ts +101 -0
- package/dist/modules/github-app/github-app.client.d.ts.map +1 -0
- package/dist/modules/github-app/github-app.client.js +222 -0
- package/dist/modules/github-app/github-app.client.js.map +1 -0
- package/dist/modules/github-app/github-app.connection.d.ts +102 -0
- package/dist/modules/github-app/github-app.connection.d.ts.map +1 -0
- package/dist/modules/github-app/github-app.connection.js +185 -0
- package/dist/modules/github-app/github-app.connection.js.map +1 -0
- package/dist/modules/github-app/github-app.routes.d.ts +59 -0
- package/dist/modules/github-app/github-app.routes.d.ts.map +1 -0
- package/dist/modules/github-app/github-app.routes.js +323 -0
- package/dist/modules/github-app/github-app.routes.js.map +1 -0
- package/dist/modules/github-app/index.d.ts +4 -0
- package/dist/modules/github-app/index.d.ts.map +1 -0
- package/dist/modules/github-app/index.js +4 -0
- package/dist/modules/github-app/index.js.map +1 -0
- package/dist/modules/kb-fs/branch-name.d.ts.map +1 -1
- package/dist/modules/kb-fs/branch-name.js +12 -2
- package/dist/modules/kb-fs/branch-name.js.map +1 -1
- package/dist/modules/kb-fs/remote-url.d.ts +22 -0
- package/dist/modules/kb-fs/remote-url.d.ts.map +1 -0
- package/dist/modules/kb-fs/remote-url.js +35 -0
- package/dist/modules/kb-fs/remote-url.js.map +1 -0
- package/dist/modules/kb-fs/repo-path.d.ts +11 -16
- package/dist/modules/kb-fs/repo-path.d.ts.map +1 -1
- package/dist/modules/kb-fs/repo-path.js +79 -0
- package/dist/modules/kb-fs/repo-path.js.map +1 -1
- package/dist/modules/kb-sync/kb-sync.routes.d.ts +2 -2
- package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -1
- package/dist/modules/kb-sync/kb-sync.routes.js +14 -3
- package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -1
- package/dist/modules/kb-sync/sync-auth.d.ts +3 -1
- package/dist/modules/kb-sync/sync-auth.d.ts.map +1 -1
- package/dist/modules/kb-sync/sync-auth.js +1 -1
- package/dist/modules/kb-sync/sync-auth.js.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.js +21 -6
- package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js +3 -2
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
- package/dist/modules/plugins/join-proposals.d.ts +17 -5
- package/dist/modules/plugins/join-proposals.d.ts.map +1 -1
- package/dist/modules/plugins/join-proposals.js +76 -22
- package/dist/modules/plugins/join-proposals.js.map +1 -1
- package/dist/modules/plugins/join-requests.service.d.ts +111 -18
- package/dist/modules/plugins/join-requests.service.d.ts.map +1 -1
- package/dist/modules/plugins/join-requests.service.js +173 -29
- package/dist/modules/plugins/join-requests.service.js.map +1 -1
- package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.routes.js +3 -2
- package/dist/modules/plugins/plugins.routes.js.map +1 -1
- package/dist/modules/settings/deployment-settings.service.d.ts +22 -1
- package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
- package/dist/modules/settings/deployment-settings.service.js +101 -7
- package/dist/modules/settings/deployment-settings.service.js.map +1 -1
- package/dist/modules/settings/managed-repository.d.ts +43 -0
- package/dist/modules/settings/managed-repository.d.ts.map +1 -0
- package/dist/modules/settings/managed-repository.js +60 -0
- package/dist/modules/settings/managed-repository.js.map +1 -0
- package/dist/modules/settings/repository-source.d.ts +128 -0
- package/dist/modules/settings/repository-source.d.ts.map +1 -0
- package/dist/modules/settings/repository-source.js +150 -0
- package/dist/modules/settings/repository-source.js.map +1 -0
- package/dist/modules/settings/setup.routes.d.ts +78 -4
- package/dist/modules/settings/setup.routes.d.ts.map +1 -1
- package/dist/modules/settings/setup.routes.js +379 -17
- package/dist/modules/settings/setup.routes.js.map +1 -1
- package/dist/modules/skills/skill-access-requests.routes.d.ts +6 -8
- package/dist/modules/skills/skill-access-requests.routes.d.ts.map +1 -1
- package/dist/modules/skills/skill-access-requests.routes.js +24 -63
- package/dist/modules/skills/skill-access-requests.routes.js.map +1 -1
- package/dist/modules/skills/skills.contract.d.ts +90 -18
- package/dist/modules/skills/skills.contract.d.ts.map +1 -1
- package/dist/modules/skills/skills.contract.js +4 -1
- package/dist/modules/skills/skills.contract.js.map +1 -1
- package/dist/modules/skills/skills.service.d.ts +98 -9
- package/dist/modules/skills/skills.service.d.ts.map +1 -1
- package/dist/modules/skills/skills.service.js +239 -37
- package/dist/modules/skills/skills.service.js.map +1 -1
- package/dist/modules/skills/skills.tools.d.ts +7 -0
- package/dist/modules/skills/skills.tools.d.ts.map +1 -1
- package/dist/modules/skills/skills.tools.js +71 -7
- package/dist/modules/skills/skills.tools.js.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.js +4 -2
- package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
- package/dist/modules/tool-auth/internal-token.service.d.ts +3 -3
- package/dist/modules/tool-auth/tool-auth.middleware.d.ts +16 -7
- package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
- package/dist/modules/tool-auth/tool-auth.middleware.js +34 -12
- 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 +15 -0
- package/dist/modules/tool-helpers/tool-context.js.map +1 -1
- package/dist/modules/tool-helpers/tool-handler.d.ts +2 -1
- package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool-handler.js +25 -4
- package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
- package/dist/modules/tool-helpers/tool.contract.d.ts +3 -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/tool-helpers/validate-token.js +1 -1
- package/dist/modules/tool-helpers/validate-token.js.map +1 -1
- package/dist/modules/workflow/agent-tools/change-request-summary.d.ts +98 -0
- package/dist/modules/workflow/agent-tools/change-request-summary.d.ts.map +1 -0
- package/dist/modules/workflow/agent-tools/change-request-summary.js +81 -0
- package/dist/modules/workflow/agent-tools/change-request-summary.js.map +1 -0
- package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.js +113 -37
- package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
- package/dist/modules/workflow/file-lock.service.d.ts +24 -0
- package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
- package/dist/modules/workflow/file-lock.service.js +30 -0
- package/dist/modules/workflow/file-lock.service.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +72 -1
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +170 -6
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/git/node-git-runner.d.ts.map +1 -1
- package/dist/modules/workflow/git/node-git-runner.js +5 -0
- package/dist/modules/workflow/git/node-git-runner.js.map +1 -1
- package/dist/modules/workflow/git/pull-request.service.d.ts +45 -0
- package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/pull-request.service.js +99 -0
- package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
- package/dist/modules/workflow/pending-commits.service.d.ts +38 -0
- package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
- package/dist/modules/workflow/pending-commits.service.js +55 -0
- package/dist/modules/workflow/pending-commits.service.js.map +1 -1
- package/dist/modules/workflow/workflow-hooks.d.ts +54 -32
- package/dist/modules/workflow/workflow-hooks.d.ts.map +1 -1
- package/dist/modules/workflow/workflow-hooks.js +16 -1
- package/dist/modules/workflow/workflow-hooks.js.map +1 -1
- package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.routes.js +3 -1
- package/dist/modules/workflow/workflow.routes.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +94 -13
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +258 -51
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/dist/modules/workspace/agent-access.gate.d.ts +94 -0
- package/dist/modules/workspace/agent-access.gate.d.ts.map +1 -0
- package/dist/modules/workspace/agent-access.gate.js +123 -0
- package/dist/modules/workspace/agent-access.gate.js.map +1 -0
- package/dist/modules/workspace/routine-write-policy.d.ts +5 -6
- package/dist/modules/workspace/routine-write-policy.d.ts.map +1 -1
- package/dist/modules/workspace/routine-write-policy.js +5 -6
- package/dist/modules/workspace/routine-write-policy.js.map +1 -1
- package/dist/modules/workspace/session-sink.d.ts +5 -5
- package/dist/modules/workspace/set-aside-clone.d.ts +46 -0
- package/dist/modules/workspace/set-aside-clone.d.ts.map +1 -0
- package/dist/modules/workspace/set-aside-clone.js +92 -0
- package/dist/modules/workspace/set-aside-clone.js.map +1 -0
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts +117 -1
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
- package/dist/modules/workspace/startup/kb-startup-runner.js +190 -1
- package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
- package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.routes.js +92 -3
- package/dist/modules/workspace/workspace.routes.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +115 -5
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +272 -26
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/modules/workspace/workspace.tools.d.ts +2 -2
- package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.tools.js +337 -117
- package/dist/modules/workspace/workspace.tools.js.map +1 -1
- package/dist/modules/write-access/write-access.d.ts +60 -0
- package/dist/modules/write-access/write-access.d.ts.map +1 -0
- package/dist/modules/write-access/write-access.js +129 -0
- package/dist/modules/write-access/write-access.js.map +1 -0
- package/dist/shared/domain-errors.d.ts +64 -0
- package/dist/shared/domain-errors.d.ts.map +1 -1
- package/dist/shared/domain-errors.js +83 -0
- package/dist/shared/domain-errors.js.map +1 -1
- package/dist/shared/git.contract.d.ts +30 -0
- package/dist/shared/git.contract.d.ts.map +1 -1
- package/dist/shared/git.contract.js +26 -0
- package/dist/shared/git.contract.js.map +1 -1
- package/dist/tenancy/static-tenant-source.d.ts +0 -1
- package/dist/tenancy/static-tenant-source.d.ts.map +1 -1
- package/dist/tenancy/static-tenant-source.js +0 -2
- package/dist/tenancy/static-tenant-source.js.map +1 -1
- package/migrations/0014_change_request_closed_reason.sql +1 -0
- package/migrations/0015_account_deactivation.sql +3 -0
- package/migrations/meta/0014_snapshot.json +2265 -0
- package/migrations/meta/0015_snapshot.json +2271 -0
- package/migrations/meta/_journal.json +14 -0
- package/package.json +3 -3
- package/src/__tests__/kb-layout-config.test.ts +0 -2
- package/src/__tests__/retired-settings.test.ts +96 -0
- package/src/core/__tests__/gated-boot.test.ts +132 -0
- package/src/core/__tests__/lifecycle.test.ts +170 -1
- package/src/core/__tests__/set-aside-root-is-one-place.test.ts +63 -0
- package/src/core/core-ports.ts +11 -1
- package/src/core/create-core-server.ts +69 -15
- package/src/core/create-core-services.ts +190 -27
- package/src/core/lifecycle.ts +98 -13
- package/src/core-config.ts +13 -14
- package/src/index.ts +12 -1
- package/src/modules/access/__tests__/access-control.preview-relocation.test.ts +385 -0
- package/src/modules/access/__tests__/access-control.prospective.test.ts +94 -18
- package/src/modules/access/__tests__/access-requests.recut.test.ts +134 -0
- package/src/modules/access/__tests__/access-requests.routes.test.ts +610 -0
- package/src/modules/access/__tests__/access.routes.prospective.test.ts +6 -7
- package/src/modules/access/access-control.interface.ts +43 -12
- package/src/modules/access/access-control.service.ts +234 -36
- package/src/modules/access/access-requests.contract.ts +93 -0
- package/src/modules/access/access-requests.routes.ts +286 -0
- package/src/modules/access/access-requests.service.ts +420 -0
- package/src/modules/access/access.routes.ts +13 -15
- package/src/modules/access-model/access-grammar.ts +22 -0
- package/src/modules/agent-instructions/__tests__/agent-instructions.route.test.ts +3 -1
- package/src/modules/auth/__tests__/account-deactivation.test.ts +252 -0
- package/src/modules/auth/__tests__/account.routes.test.ts +81 -3
- package/src/modules/auth/__tests__/auth.middleware.test.ts +45 -22
- package/src/modules/auth/__tests__/auth.service.test.ts +39 -1
- package/src/modules/auth/account-admission.ts +78 -9
- package/src/modules/auth/account.routes.ts +63 -7
- package/src/modules/auth/auth.middleware.ts +19 -8
- package/src/modules/auth/auth.routes.ts +8 -0
- package/src/modules/auth/auth.service.ts +207 -7
- package/src/modules/auth/oidc-auth-provider.ts +7 -2
- package/src/modules/database/core-schema.ts +35 -27
- package/src/modules/github-app/__tests__/github-app.test.ts +848 -0
- package/src/modules/github-app/github-app.client.ts +268 -0
- package/src/modules/github-app/github-app.connection.ts +204 -0
- package/src/modules/github-app/github-app.routes.ts +359 -0
- package/src/modules/github-app/index.ts +19 -0
- package/src/modules/kb-fs/__tests__/branch-name.test.ts +10 -0
- package/src/modules/kb-fs/__tests__/remote-url.test.ts +38 -0
- package/src/modules/kb-fs/__tests__/repo-path.test.ts +123 -0
- package/src/modules/kb-fs/branch-name.ts +14 -1
- package/src/modules/kb-fs/remote-url.ts +35 -0
- package/src/modules/kb-fs/repo-path.ts +85 -0
- package/src/modules/kb-sync/__tests__/kb-sync.routes.test.ts +26 -1
- package/src/modules/kb-sync/kb-sync.routes.ts +14 -4
- package/src/modules/kb-sync/sync-auth.ts +2 -2
- package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +60 -3
- package/src/modules/mcp/__tests__/mcp.service.test.ts +68 -6
- package/src/modules/mcp/mcp-auth.middleware.ts +20 -6
- package/src/modules/mcp/oauth/bevel-oauth-provider.ts +3 -1
- package/src/modules/plugins/__tests__/join-proposals.test.ts +100 -19
- package/src/modules/plugins/__tests__/join-requests.service.test.ts +27 -11
- package/src/modules/plugins/__tests__/join-requests.settlement.test.ts +371 -0
- package/src/modules/plugins/__tests__/plugins.routes.test.ts +1 -1
- package/src/modules/plugins/__tests__/plugins.tools.test.ts +2 -1
- package/src/modules/plugins/join-proposals.ts +87 -19
- package/src/modules/plugins/join-requests.service.ts +199 -39
- package/src/modules/plugins/plugins.routes.ts +3 -2
- package/src/modules/settings/__tests__/deployment-settings.service.test.ts +16 -0
- package/src/modules/settings/__tests__/repository-source.test.ts +191 -0
- package/src/modules/settings/__tests__/setup.routes.git-mode.test.ts +386 -0
- package/src/modules/settings/__tests__/setup.routes.github-app.test.ts +299 -0
- package/src/modules/settings/__tests__/setup.routes.managed-phase.test.ts +236 -0
- package/src/modules/settings/__tests__/setup.routes.repository-change.test.ts +390 -0
- package/src/modules/settings/__tests__/setup.routes.test.ts +3 -0
- package/src/modules/settings/deployment-settings.service.ts +117 -6
- package/src/modules/settings/managed-repository.ts +68 -0
- package/src/modules/settings/repository-source.ts +197 -0
- package/src/modules/settings/setup.routes.ts +463 -15
- package/src/modules/skills/__tests__/allowed-tools-warn.tools.test.ts +2 -1
- package/src/modules/skills/__tests__/branch-skills.tools.test.ts +218 -0
- package/src/modules/skills/__tests__/skill-access-requests.routes.test.ts +5 -1
- package/src/modules/skills/__tests__/skills.service.test.ts +229 -5
- package/src/modules/skills/skill-access-requests.routes.ts +25 -75
- package/src/modules/skills/skills.contract.ts +91 -18
- package/src/modules/skills/skills.service.ts +278 -41
- package/src/modules/skills/skills.tools.ts +80 -8
- package/src/modules/tool-auth/__tests__/manual-auth.middleware.test.ts +14 -1
- package/src/modules/tool-auth/external-api-key.service.ts +4 -2
- package/src/modules/tool-auth/internal-token.service.ts +3 -3
- package/src/modules/tool-auth/tool-auth.middleware.ts +34 -11
- package/src/modules/tool-helpers/__tests__/agent-roles-write.test.ts +5 -3
- package/src/modules/tool-helpers/__tests__/phase4-tools.test.ts +3 -3
- package/src/modules/tool-helpers/__tests__/validate-token.test.ts +11 -1
- package/src/modules/tool-helpers/tool-context.ts +15 -0
- package/src/modules/tool-helpers/tool-handler.ts +24 -4
- package/src/modules/tool-helpers/tool.contract.ts +3 -2
- package/src/modules/tool-helpers/validate-token.ts +1 -1
- package/src/modules/workflow/__tests__/apply-failure.test.ts +9 -1
- package/src/modules/workflow/__tests__/pending-commits.repository-replaced.test.ts +111 -0
- package/src/modules/workflow/__tests__/workflow.routes.history-read-gate.test.ts +25 -0
- package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +159 -8
- package/src/modules/workflow/__tests__/workflow.service.repository-replaced.test.ts +249 -0
- package/src/modules/workflow/__tests__/workflow.service.update-from-target.test.ts +146 -40
- package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +405 -7
- package/src/modules/workflow/agent-tools/change-request-summary.ts +182 -0
- package/src/modules/workflow/agent-tools/workflow.tools.ts +135 -38
- package/src/modules/workflow/file-lock.service.ts +31 -0
- package/src/modules/workflow/git/__tests__/git.service.fileBytesAtCommit.test.ts +260 -0
- package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +121 -0
- package/src/modules/workflow/git/__tests__/pull-request.service.getPrDetail.test.ts +145 -2
- package/src/modules/workflow/git/__tests__/pull-request.service.viewer-can-delete.test.ts +166 -0
- package/src/modules/workflow/git/git.service.ts +193 -4
- package/src/modules/workflow/git/node-git-runner.ts +6 -0
- package/src/modules/workflow/git/pull-request.service.ts +119 -0
- package/src/modules/workflow/pending-commits.service.ts +59 -1
- package/src/modules/workflow/workflow-hooks.ts +64 -26
- package/src/modules/workflow/workflow.routes.ts +3 -1
- package/src/modules/workflow/workflow.service.ts +290 -55
- package/src/modules/workspace/__tests__/agent-access.gate.test.ts +208 -0
- package/src/modules/workspace/__tests__/escape-sequences.routes.test.ts +384 -0
- package/src/modules/workspace/__tests__/file-stat-access.test.ts +2 -1
- package/src/modules/workspace/__tests__/git-internals.security.test.ts +2 -1
- package/src/modules/workspace/__tests__/set-aside-clone.test.ts +52 -0
- package/src/modules/workspace/__tests__/workspace.routes.at-ref.test.ts +305 -0
- package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +2 -0
- package/src/modules/workspace/__tests__/workspace.service.forget-clone-races.test.ts +147 -0
- package/src/modules/workspace/__tests__/workspace.service.replaced-repository.test.ts +326 -0
- package/src/modules/workspace/__tests__/workspace.service.test.ts +20 -9
- package/src/modules/workspace/__tests__/workspace.service.unknown-branch.test.ts +43 -0
- package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +2 -3
- package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +235 -10
- package/src/modules/workspace/__tests__/workspace.tools.test.ts +720 -17
- package/src/modules/workspace/agent-access.gate.ts +164 -0
- package/src/modules/workspace/routine-write-policy.ts +5 -6
- package/src/modules/workspace/session-sink.ts +5 -5
- package/src/modules/workspace/set-aside-clone.ts +96 -0
- package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +297 -0
- package/src/modules/workspace/startup/kb-startup-runner.ts +242 -2
- package/src/modules/workspace/workspace.routes.ts +95 -3
- package/src/modules/workspace/workspace.service.ts +273 -25
- package/src/modules/workspace/workspace.tools.ts +373 -119
- package/src/modules/write-access/__tests__/write-access.test.ts +248 -0
- package/src/modules/write-access/write-access.ts +153 -0
- package/src/shared/domain-errors.ts +89 -0
- package/src/shared/git.contract.ts +37 -0
- package/src/tenancy/__tests__/static-tenant-source.test.ts +0 -1
- package/src/tenancy/static-tenant-source.ts +0 -3
- package/dist/modules/workflow/session-ontology.policy.d.ts +0 -52
- package/dist/modules/workflow/session-ontology.policy.d.ts.map +0 -1
- package/dist/modules/workflow/session-ontology.policy.js +0 -62
- package/dist/modules/workflow/session-ontology.policy.js.map +0 -1
- package/dist/modules/workflow/session-ontology.service.d.ts +0 -105
- package/dist/modules/workflow/session-ontology.service.d.ts.map +0 -1
- package/dist/modules/workflow/session-ontology.service.js +0 -147
- package/dist/modules/workflow/session-ontology.service.js.map +0 -1
- package/dist/modules/workspace/session-ontology.gate.d.ts +0 -114
- package/dist/modules/workspace/session-ontology.gate.d.ts.map +0 -1
- package/dist/modules/workspace/session-ontology.gate.js +0 -161
- package/dist/modules/workspace/session-ontology.gate.js.map +0 -1
- package/dist/shared/kb-layout.d.ts +0 -39
- package/dist/shared/kb-layout.d.ts.map +0 -1
- package/dist/shared/kb-layout.js +0 -103
- package/dist/shared/kb-layout.js.map +0 -1
- package/dist/shared/kb-layout.test.d.ts +0 -2
- package/dist/shared/kb-layout.test.d.ts.map +0 -1
- package/dist/shared/kb-layout.test.js +0 -75
- package/dist/shared/kb-layout.test.js.map +0 -1
- package/src/modules/workflow/__tests__/session-ontology.policy.test.ts +0 -62
- package/src/modules/workflow/__tests__/session-ontology.service.test.ts +0 -201
- package/src/modules/workflow/session-ontology.policy.ts +0 -70
- package/src/modules/workflow/session-ontology.service.ts +0 -183
- package/src/modules/workspace/__tests__/session-ontology.gate.test.ts +0 -239
- package/src/modules/workspace/session-ontology.gate.ts +0 -191
- package/src/shared/kb-layout.test.ts +0 -98
- package/src/shared/kb-layout.ts +0 -102
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import type { SQL } from 'drizzle-orm';
|
|
3
|
+
import { PgDialect } from 'drizzle-orm/pg-core';
|
|
4
|
+
import { PendingCommitsService } from '../pending-commits.service.js';
|
|
5
|
+
import type { Database } from '../../database/connection.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Commits still queued for a branch when the knowledge-base repository is
|
|
9
|
+
* REPLACED.
|
|
10
|
+
*
|
|
11
|
+
* A release enqueues the bytes and only then drops its lock, so the moment the
|
|
12
|
+
* locks on a branch are dropped there may be rows already waiting — written
|
|
13
|
+
* against a working copy of the repository that is gone. Left `pending`, the
|
|
14
|
+
* worker picks them up after the re-clone and commits them onto a same-named
|
|
15
|
+
* branch of a DIFFERENT repository: somebody else's file, overwritten by a
|
|
16
|
+
* change nobody made to it.
|
|
17
|
+
*
|
|
18
|
+
* So they are escalated, never dropped: the bytes stay in the row, the reason
|
|
19
|
+
* is on it, and a person decides.
|
|
20
|
+
*/
|
|
21
|
+
function makeFakeDb(moved: Array<{ id: string }>) {
|
|
22
|
+
const sets: Record<string, unknown>[] = [];
|
|
23
|
+
const db = {
|
|
24
|
+
update: vi.fn(() => ({
|
|
25
|
+
set: (values: Record<string, unknown>) => {
|
|
26
|
+
sets.push(values);
|
|
27
|
+
return { where: () => ({ returning: async () => moved }) };
|
|
28
|
+
},
|
|
29
|
+
})),
|
|
30
|
+
// Queued bytes are never thrown away over this.
|
|
31
|
+
delete: () => {
|
|
32
|
+
throw new Error('a queued commit must never be deleted');
|
|
33
|
+
},
|
|
34
|
+
} as unknown as Database;
|
|
35
|
+
return { db, sets };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('PendingCommitsService.markNeedsAttentionOnBranch', () => {
|
|
39
|
+
it('escalates every queued commit on the branch, with the reason, and answers how many', async () => {
|
|
40
|
+
const { db, sets } = makeFakeDb([{ id: 'a' }, { id: 'b' }]);
|
|
41
|
+
const svc = new PendingCommitsService(db);
|
|
42
|
+
|
|
43
|
+
await expect(
|
|
44
|
+
svc.markNeedsAttentionOnBranch('alice/draft', 'The repository was replaced.'),
|
|
45
|
+
).resolves.toBe(2);
|
|
46
|
+
|
|
47
|
+
expect(sets).toHaveLength(1);
|
|
48
|
+
// `needs_attention` is the status the admin surface reads — the row is out
|
|
49
|
+
// of the worker's reach and in front of a person.
|
|
50
|
+
expect(sets[0]?.status).toBe('needs_attention');
|
|
51
|
+
expect(sets[0]?.lastError).toBe('The repository was replaced.');
|
|
52
|
+
expect(sets[0]?.lastAttemptedAt).toBeInstanceOf(Date);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('answers zero when nothing was queued for that branch', async () => {
|
|
56
|
+
const { db } = makeFakeDb([]);
|
|
57
|
+
const svc = new PendingCommitsService(db);
|
|
58
|
+
await expect(svc.markNeedsAttentionOnBranch('quiet/branch', 'whatever')).resolves.toBe(0);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The same, keyed on the WORKING COPY that was set aside. Closing change
|
|
64
|
+
* requests covers their branches and nothing else; a commit queued on the
|
|
65
|
+
* default branch was written against the repository that was left all the
|
|
66
|
+
* same, and the path it names is about to hold a clone of another one.
|
|
67
|
+
*/
|
|
68
|
+
describe('PendingCommitsService.markNeedsAttentionInWorkspace', () => {
|
|
69
|
+
it('escalates every queued commit for the working copy, with the reason, and answers how many', async () => {
|
|
70
|
+
const { db, sets } = makeFakeDb([{ id: 'a' }, { id: 'b' }, { id: 'c' }]);
|
|
71
|
+
const svc = new PendingCommitsService(db);
|
|
72
|
+
|
|
73
|
+
await expect(svc.markNeedsAttentionInWorkspace('main', 'The repository was replaced.')).resolves.toBe(3);
|
|
74
|
+
|
|
75
|
+
expect(sets).toHaveLength(1);
|
|
76
|
+
expect(sets[0]?.status).toBe('needs_attention');
|
|
77
|
+
expect(sets[0]?.lastError).toBe('The repository was replaced.');
|
|
78
|
+
expect(sets[0]?.lastAttemptedAt).toBeInstanceOf(Date);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Rows are stored under the encoded id, whichever spelling the caller
|
|
83
|
+
* enqueued with. The predicate is rendered and read, since the stub does
|
|
84
|
+
* not evaluate it: the id it compares must be the stored spelling, and it
|
|
85
|
+
* must match only the two statuses a worker could still write, `pending`
|
|
86
|
+
* and `running`.
|
|
87
|
+
*/
|
|
88
|
+
it('looks for the working copy under the id the queue stores, and only for commits still queued', async () => {
|
|
89
|
+
const wheres: SQL[] = [];
|
|
90
|
+
const db = {
|
|
91
|
+
update: () => ({
|
|
92
|
+
set: () => ({
|
|
93
|
+
where: (predicate: SQL) => {
|
|
94
|
+
wheres.push(predicate);
|
|
95
|
+
return { returning: async () => [] };
|
|
96
|
+
},
|
|
97
|
+
}),
|
|
98
|
+
}),
|
|
99
|
+
} as unknown as Database;
|
|
100
|
+
await new PendingCommitsService(db).markNeedsAttentionInWorkspace('alice/feature', 'whatever');
|
|
101
|
+
|
|
102
|
+
const { sql: text, params } = new PgDialect().sqlToQuery(wheres[0]!);
|
|
103
|
+
expect(text).toContain('"workspace_id" =');
|
|
104
|
+
expect(params).toEqual(['alice%2Ffeature', 'pending', 'running']);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('answers zero when nothing was queued for it', async () => {
|
|
108
|
+
const { db } = makeFakeDb([]);
|
|
109
|
+
await expect(new PendingCommitsService(db).markNeedsAttentionInWorkspace('main', 'whatever')).resolves.toBe(0);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -8,6 +8,10 @@ import type { AuthService } from '../../auth/auth.service.js';
|
|
|
8
8
|
import type { WorkspaceService } from '../../workspace/workspace.service.js';
|
|
9
9
|
import type { WorkflowEventBus } from '../event-bus.js';
|
|
10
10
|
import { createWorkflowRoutes } from '../workflow.routes.js';
|
|
11
|
+
import {
|
|
12
|
+
VersionNotOnBranchError,
|
|
13
|
+
VERSION_NOT_ON_BRANCH_MESSAGE,
|
|
14
|
+
} from '../../../shared/domain-errors.js';
|
|
11
15
|
|
|
12
16
|
// A file's history is its content with a time axis: the same default-deny
|
|
13
17
|
// read model that hides a file must hide its commit list, its diffs, and its
|
|
@@ -129,6 +133,27 @@ describe('history routes enforce the read model', () => {
|
|
|
129
133
|
expect((await get(`/compare-file?from=a&to=b&path=${OPEN}`)).status).toBe(403);
|
|
130
134
|
});
|
|
131
135
|
|
|
136
|
+
it.each([
|
|
137
|
+
['/show-file?sha=abc1234&path=%s', 'showFileAtChange'],
|
|
138
|
+
['/file-at-change?sha=abc1234&path=%s', 'fileAtChange'],
|
|
139
|
+
] as const)(
|
|
140
|
+
'%s: a save off this branch is refused with the history message',
|
|
141
|
+
async (route, method) => {
|
|
142
|
+
// The service raises it (the git layer knows the branch); the route has
|
|
143
|
+
// to pass the typed refusal through with its own status and sentence,
|
|
144
|
+
// because a reader who is told "not found" about a file that is plainly
|
|
145
|
+
// there learns the wrong thing. The bytes route carries the same rule,
|
|
146
|
+
// so neither can be read around through the other.
|
|
147
|
+
h = await makeHarness();
|
|
148
|
+
h.workflow[method].mockRejectedValueOnce(new VersionNotOnBranchError());
|
|
149
|
+
const res = await get(route.replace('%s', OPEN));
|
|
150
|
+
expect(res.status).toBe(404);
|
|
151
|
+
expect(((await res.json()) as { error: string }).error).toBe(
|
|
152
|
+
VERSION_NOT_ON_BRANCH_MESSAGE,
|
|
153
|
+
);
|
|
154
|
+
},
|
|
155
|
+
);
|
|
156
|
+
|
|
132
157
|
it('an access-model error fails closed, not open', async () => {
|
|
133
158
|
h = await makeHarness();
|
|
134
159
|
h.canRead.mockRejectedValueOnce(new Error('access tree unreadable'));
|
|
@@ -17,6 +17,7 @@ import { PullRebaseConflictError, WorkflowDomainError } from '../../../shared/do
|
|
|
17
17
|
import { DEFAULT_BRANCH } from '@bevel-software/platform-shared';
|
|
18
18
|
import type { Database } from '../../database/connection.js';
|
|
19
19
|
import { openChangeGate } from '../../../__tests__/open-change-gate.js';
|
|
20
|
+
import { hashEmail } from '../../../shared/email-identity.js';
|
|
20
21
|
|
|
21
22
|
// `deleteBranch`'s open-request guard is the only DB touch these tests
|
|
22
23
|
// exercise. The stub ANSWERS FROM THE WHERE-CLAUSE rather than returning the
|
|
@@ -375,7 +376,7 @@ describe('WorkflowService — change request delegation + cache invalidation', (
|
|
|
375
376
|
expect(outcome).toEqual({ kind: 'merged', result: mergeResult });
|
|
376
377
|
// The target branch's workspace is pulled so it doesn't fall behind origin.
|
|
377
378
|
expect(workspaceService.getOrCreateForBranch).toHaveBeenCalledWith('main');
|
|
378
|
-
expect(git.pull).toHaveBeenCalledWith('main');
|
|
379
|
+
expect(git.pull).toHaveBeenCalledWith('main', {});
|
|
379
380
|
expect(reviewWorkflow.mergePr).toHaveBeenCalledWith(
|
|
380
381
|
4,
|
|
381
382
|
expect.objectContaining({ email: 'alice@example.com' }),
|
|
@@ -784,8 +785,13 @@ describe('WorkflowService — revertChangeRequestFile / closeEmptyChangeRequest'
|
|
|
784
785
|
});
|
|
785
786
|
});
|
|
786
787
|
|
|
787
|
-
describe('WorkflowService — deleteChangeRequest (admin
|
|
788
|
-
|
|
788
|
+
describe('WorkflowService — deleteChangeRequest (the author’s and the admin’s verb)', () => {
|
|
789
|
+
const AUTHOR_EMAIL = 'alice@example.com'; // makeUser()'s default caller
|
|
790
|
+
const STRANGER_AUTHOR = 'mallory@example.com';
|
|
791
|
+
|
|
792
|
+
function makeDeleteHarness(
|
|
793
|
+
opts: { isAdmin?: boolean; prState?: string; authorEmail?: string | null } = {},
|
|
794
|
+
) {
|
|
789
795
|
const git = Object.assign(makeGit(), {
|
|
790
796
|
changedPathsForPr: vi.fn().mockResolvedValue(['Docs/a.md']),
|
|
791
797
|
}) as unknown as GitService;
|
|
@@ -795,26 +801,83 @@ describe('WorkflowService — deleteChangeRequest (admin moderation verb)', () =
|
|
|
795
801
|
base: 'main',
|
|
796
802
|
branch: 'mallory/spam',
|
|
797
803
|
state: opts.prState ?? 'open',
|
|
804
|
+
// Authorship as the server reads it: the stored hash, never the caller's
|
|
805
|
+
// claim. Defaults to someone OTHER than makeUser(), so a test that says
|
|
806
|
+
// nothing about authorship is testing the admin grant alone.
|
|
807
|
+
// `null` means a request with no stored author (opened outside this
|
|
808
|
+
// backend); undefined means "someone, just not the caller".
|
|
809
|
+
authorId:
|
|
810
|
+
opts.authorEmail === null ? undefined : hashEmail(opts.authorEmail ?? STRANGER_AUTHOR),
|
|
798
811
|
});
|
|
799
812
|
const access = makeAccessControl();
|
|
800
813
|
(access.canWriteAtRef as ReturnType<typeof vi.fn>).mockResolvedValue(opts.isAdmin ?? false);
|
|
801
814
|
const chain: Record<string, unknown> = {};
|
|
815
|
+
// Two different selects run under one chain mock: the branch-retirement
|
|
816
|
+
// lookup asks for `{ sourceBranch }`, and `openChangeRequestOn` asks for
|
|
817
|
+
// `{ number, sourceBranch }`. Answer by projection rather than call order,
|
|
818
|
+
// so the retirement path is exercised (a branch to retire, and no other
|
|
819
|
+
// open request holding it) without the test depending on call sequence.
|
|
820
|
+
let projection: Record<string, unknown> = {};
|
|
802
821
|
Object.assign(chain, {
|
|
803
|
-
select: vi.fn(() =>
|
|
822
|
+
select: vi.fn((proj?: Record<string, unknown>) => {
|
|
823
|
+
projection = proj ?? {};
|
|
824
|
+
return chain;
|
|
825
|
+
}),
|
|
804
826
|
from: vi.fn(() => chain),
|
|
805
827
|
where: vi.fn(() => chain),
|
|
806
|
-
limit: vi.fn(async () =>
|
|
828
|
+
limit: vi.fn(async () =>
|
|
829
|
+
'number' in projection ? [] : [{ sourceBranch: 'mallory/spam' }],
|
|
830
|
+
),
|
|
807
831
|
update: vi.fn(() => chain),
|
|
808
832
|
set: vi.fn(() => chain),
|
|
809
833
|
returning: vi.fn(async () => [{ id: 1 }]),
|
|
810
834
|
});
|
|
811
835
|
const workspaces = makeWorkspaceService();
|
|
812
836
|
const svc = new WorkflowService(chain as unknown as Database, git, prs, makeReviewWorkflow(), workspaces, access, makeFileLockService(), makePendingCommits(), testKbContext(), openChangeGate());
|
|
813
|
-
return { svc, prs, db: chain, access, workspaces };
|
|
837
|
+
return { svc, prs, db: chain, access, workspaces, git };
|
|
814
838
|
}
|
|
815
839
|
|
|
816
|
-
it('refuses a non-admin with 403 and
|
|
817
|
-
const { svc, db } = makeDeleteHarness({ isAdmin: false });
|
|
840
|
+
it('refuses a non-admin who is not the author, with 403 and the author-or-admin message', async () => {
|
|
841
|
+
const { svc, db, git } = makeDeleteHarness({ isAdmin: false });
|
|
842
|
+
await expect(svc.deleteChangeRequest(9, makeUser())).rejects.toMatchObject({
|
|
843
|
+
status: 403,
|
|
844
|
+
message: "Only the request's author or an admin can delete it.",
|
|
845
|
+
});
|
|
846
|
+
// "Changes nothing" is the whole promise of the refusal: no row flipped,
|
|
847
|
+
// no branch removed.
|
|
848
|
+
expect(db.update).not.toHaveBeenCalled();
|
|
849
|
+
expect(git.deleteBranch).not.toHaveBeenCalled();
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
it('lets the AUTHOR delete their own request without admin rights: closes it and retires the branch', async () => {
|
|
853
|
+
const { svc, prs, db, git } = makeDeleteHarness({
|
|
854
|
+
isAdmin: false,
|
|
855
|
+
authorEmail: AUTHOR_EMAIL,
|
|
856
|
+
});
|
|
857
|
+
await svc.deleteChangeRequest(9, makeUser());
|
|
858
|
+
expect(db.update).toHaveBeenCalled();
|
|
859
|
+
expect(prs.invalidateDetailCache).toHaveBeenCalledWith(9);
|
|
860
|
+
expect(git.deleteBranch).toHaveBeenCalledWith(
|
|
861
|
+
expect.any(String),
|
|
862
|
+
'mallory/spam',
|
|
863
|
+
expect.objectContaining({ email: AUTHOR_EMAIL }),
|
|
864
|
+
expect.objectContaining({ systemCleanup: true }),
|
|
865
|
+
);
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
it('matches the author case-insensitively, as hashEmail does', async () => {
|
|
869
|
+
// The stored hash is of the normalized address; a caller signed in as
|
|
870
|
+
// `Alice@Example.com` is the same person and must not be refused.
|
|
871
|
+
const { svc, db } = makeDeleteHarness({ isAdmin: false, authorEmail: AUTHOR_EMAIL });
|
|
872
|
+
await svc.deleteChangeRequest(9, makeUser({ email: ' Alice@Example.com ' }));
|
|
873
|
+
expect(db.update).toHaveBeenCalled();
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
it('refuses a request with no stored author to a non-admin', async () => {
|
|
877
|
+
// Opened outside this backend: nobody can claim authorship of it, so the
|
|
878
|
+
// admin grant is the only one left. An absent hash must never read as a
|
|
879
|
+
// match.
|
|
880
|
+
const { svc, db } = makeDeleteHarness({ isAdmin: false, authorEmail: null });
|
|
818
881
|
await expect(svc.deleteChangeRequest(9, makeUser())).rejects.toMatchObject({ status: 403 });
|
|
819
882
|
expect(db.update).not.toHaveBeenCalled();
|
|
820
883
|
});
|
|
@@ -832,6 +895,94 @@ describe('WorkflowService — deleteChangeRequest (admin moderation verb)', () =
|
|
|
832
895
|
expect(db.update).not.toHaveBeenCalled();
|
|
833
896
|
});
|
|
834
897
|
|
|
898
|
+
it('refuses the AUTHOR on an applied request with 422 and the applied message', async () => {
|
|
899
|
+
// Applied between opening the dialog and confirming. The author's new
|
|
900
|
+
// grant does not reach applied history either.
|
|
901
|
+
const { svc, db, git } = makeDeleteHarness({
|
|
902
|
+
isAdmin: false,
|
|
903
|
+
prState: 'merged',
|
|
904
|
+
authorEmail: AUTHOR_EMAIL,
|
|
905
|
+
});
|
|
906
|
+
await expect(svc.deleteChangeRequest(9, makeUser())).rejects.toMatchObject({
|
|
907
|
+
status: 422,
|
|
908
|
+
message: 'This change request has already been applied.',
|
|
909
|
+
});
|
|
910
|
+
expect(db.update).not.toHaveBeenCalled();
|
|
911
|
+
expect(git.deleteBranch).not.toHaveBeenCalled();
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
it('refuses the AUTHOR with 409 when the base cannot be verified, and changes nothing', async () => {
|
|
915
|
+
// The strict base fetch is NOT skipped for an author, even though their
|
|
916
|
+
// grant needs no roles.yaml read: an unreachable origin means the branch
|
|
917
|
+
// removal would fail silently, and the delete must refuse whole rather
|
|
918
|
+
// than close the request and leave the branch.
|
|
919
|
+
const { svc, db, git, workspaces } = makeDeleteHarness({
|
|
920
|
+
isAdmin: false,
|
|
921
|
+
authorEmail: AUTHOR_EMAIL,
|
|
922
|
+
});
|
|
923
|
+
(workspaces.ensureRemotesFetched as ReturnType<typeof vi.fn>).mockRejectedValue(
|
|
924
|
+
new Error('origin unreachable'),
|
|
925
|
+
);
|
|
926
|
+
await expect(svc.deleteChangeRequest(9, makeUser())).rejects.toMatchObject({
|
|
927
|
+
status: 409,
|
|
928
|
+
message: expect.stringContaining('Retry in a moment.'),
|
|
929
|
+
});
|
|
930
|
+
expect(db.update).not.toHaveBeenCalled();
|
|
931
|
+
expect(git.deleteBranch).not.toHaveBeenCalled();
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
it('refuses the AUTHOR with 409 when the base ref does not resolve', async () => {
|
|
935
|
+
const { svc, db, access } = makeDeleteHarness({
|
|
936
|
+
isAdmin: false,
|
|
937
|
+
authorEmail: AUTHOR_EMAIL,
|
|
938
|
+
});
|
|
939
|
+
(access.canWriteAtRef as ReturnType<typeof vi.fn>).mockResolvedValue(null);
|
|
940
|
+
await expect(svc.deleteChangeRequest(9, makeUser())).rejects.toMatchObject({ status: 409 });
|
|
941
|
+
expect(db.update).not.toHaveBeenCalled();
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
it('a request already closed elsewhere still has its branch retired', async () => {
|
|
945
|
+
// Withdrawn in another tab: the guarded update flips no row, the request
|
|
946
|
+
// is not merged, so the delete finishes the job the withdraw left undone.
|
|
947
|
+
const { svc, db, git } = makeDeleteHarness({
|
|
948
|
+
isAdmin: false,
|
|
949
|
+
prState: 'closed',
|
|
950
|
+
authorEmail: AUTHOR_EMAIL,
|
|
951
|
+
});
|
|
952
|
+
// The update is guarded on `state = 'open'`, so it flips nothing here.
|
|
953
|
+
(db.returning as ReturnType<typeof vi.fn>).mockResolvedValue([]);
|
|
954
|
+
await expect(svc.deleteChangeRequest(9, makeUser())).resolves.toBeUndefined();
|
|
955
|
+
expect(git.deleteBranch).toHaveBeenCalledWith(
|
|
956
|
+
expect.any(String),
|
|
957
|
+
'mallory/spam',
|
|
958
|
+
expect.anything(),
|
|
959
|
+
expect.objectContaining({ systemCleanup: true }),
|
|
960
|
+
);
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
it('keeps a branch another open request still uses, and closes the request silently', async () => {
|
|
964
|
+
const { svc, db, git } = makeDeleteHarness({
|
|
965
|
+
isAdmin: false,
|
|
966
|
+
authorEmail: AUTHOR_EMAIL,
|
|
967
|
+
});
|
|
968
|
+
// `openChangeRequestOn` finds a live request on the branch (the
|
|
969
|
+
// `{ number, sourceBranch }` projection), so retirement backs off.
|
|
970
|
+
let projection: Record<string, unknown> = {};
|
|
971
|
+
(db.select as ReturnType<typeof vi.fn>).mockImplementation((proj?: Record<string, unknown>) => {
|
|
972
|
+
projection = proj ?? {};
|
|
973
|
+
return db;
|
|
974
|
+
});
|
|
975
|
+
(db.limit as ReturnType<typeof vi.fn>).mockImplementation(async () =>
|
|
976
|
+
'number' in projection
|
|
977
|
+
? [{ number: 11, sourceBranch: 'mallory/spam' }]
|
|
978
|
+
: [{ sourceBranch: 'mallory/spam' }],
|
|
979
|
+
);
|
|
980
|
+
// No throw, no message: the request closes and the branch survives.
|
|
981
|
+
await expect(svc.deleteChangeRequest(9, makeUser())).resolves.toBeUndefined();
|
|
982
|
+
expect(db.update).toHaveBeenCalled();
|
|
983
|
+
expect(git.deleteBranch).not.toHaveBeenCalled();
|
|
984
|
+
});
|
|
985
|
+
|
|
835
986
|
it('kicks the deleted-branch sweep when the base workspace cannot be resolved', async () => {
|
|
836
987
|
const { svc, db, workspaces } = makeDeleteHarness({ isAdmin: true });
|
|
837
988
|
(workspaces.getOrCreateForBranch as ReturnType<typeof vi.fn>).mockRejectedValue(
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { testKbContext } from '../../../__tests__/kb-context.js';
|
|
3
|
+
import type { GitService } from '../git/git.service.js';
|
|
4
|
+
import type { PullRequestService } from '../git/pull-request.service.js';
|
|
5
|
+
import type { IReviewWorkflowService } from '../review-workflow/review-workflow.interface.js';
|
|
6
|
+
import type { WorkspaceService } from '../../workspace/workspace.service.js';
|
|
7
|
+
import type { IAccessControl } from '../../access/access-control.interface.js';
|
|
8
|
+
import type { FileLockService } from '../file-lock.service.js';
|
|
9
|
+
import type { PendingCommitsService } from '../pending-commits.service.js';
|
|
10
|
+
import type { Database } from '../../database/connection.js';
|
|
11
|
+
import { WorkflowService } from '../workflow.service.js';
|
|
12
|
+
import { openChangeGate } from '../../../__tests__/open-change-gate.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The CLOSE half of the admin's choice when the knowledge-base repository is
|
|
16
|
+
* replaced: the requests point at branches that are not in the new
|
|
17
|
+
* repository, so they are closed — with a reason, releasing the locks held on
|
|
18
|
+
* those branches and taking any commit still queued for them off the worker,
|
|
19
|
+
* and without deleting a single row.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
interface ClosedRow {
|
|
23
|
+
number: number;
|
|
24
|
+
sourceBranch: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* DB stub shaped like the real close: ONE `update … where state = 'open'
|
|
29
|
+
* returning` per pass, answering with the rows that pass actually closed.
|
|
30
|
+
*
|
|
31
|
+
* `passes` is therefore what the statement RETURNS, pass by pass — which is
|
|
32
|
+
* how a request already closed by a merge or a withdrawal is expressed (it is
|
|
33
|
+
* simply not in the returned set) and how a request opened BETWEEN passes is
|
|
34
|
+
* expressed (it turns up in a later one). A pass past the end answers nothing,
|
|
35
|
+
* which is what stops the loop.
|
|
36
|
+
*/
|
|
37
|
+
function makeDb(passes: ClosedRow[][], sets: Record<string, unknown>[], openCount: number) {
|
|
38
|
+
let pass = 0;
|
|
39
|
+
const db = {
|
|
40
|
+
// The count is a SQL aggregate: one row, one integer.
|
|
41
|
+
select: () => ({ from: () => ({ where: async () => [{ open: openCount }] }) }),
|
|
42
|
+
update: () => ({
|
|
43
|
+
set: (values: Record<string, unknown>) => {
|
|
44
|
+
sets.push(values);
|
|
45
|
+
return { where: () => ({ returning: async () => passes[pass++] ?? [] }) };
|
|
46
|
+
},
|
|
47
|
+
}),
|
|
48
|
+
// Nothing here may ever delete a change request.
|
|
49
|
+
delete: () => {
|
|
50
|
+
throw new Error('a change request must never be deleted');
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
return db as unknown as Database;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function makeService(passes: ClosedRow[][], openCount = passes.flat().length) {
|
|
57
|
+
const sets: Record<string, unknown>[] = [];
|
|
58
|
+
const unlocked: string[] = [];
|
|
59
|
+
const dequeued: string[] = [];
|
|
60
|
+
const invalidated: number[] = [];
|
|
61
|
+
const emitted: unknown[] = [];
|
|
62
|
+
const fileLocks = {
|
|
63
|
+
releaseAllOnBranch: vi.fn(async (branch: string) => {
|
|
64
|
+
unlocked.push(branch);
|
|
65
|
+
return 2;
|
|
66
|
+
}),
|
|
67
|
+
} as unknown as FileLockService;
|
|
68
|
+
const pendingCommits = {
|
|
69
|
+
markNeedsAttentionOnBranch: vi.fn(async (branch: string) => {
|
|
70
|
+
dequeued.push(branch);
|
|
71
|
+
return 1;
|
|
72
|
+
}),
|
|
73
|
+
} as unknown as PendingCommitsService;
|
|
74
|
+
const prs = {
|
|
75
|
+
invalidateDetailCache: vi.fn((n: number) => invalidated.push(n)),
|
|
76
|
+
} as unknown as PullRequestService;
|
|
77
|
+
const svc = new WorkflowService(
|
|
78
|
+
makeDb(passes, sets, openCount),
|
|
79
|
+
{} as GitService,
|
|
80
|
+
prs,
|
|
81
|
+
{} as IReviewWorkflowService,
|
|
82
|
+
{} as WorkspaceService,
|
|
83
|
+
{} as IAccessControl,
|
|
84
|
+
fileLocks,
|
|
85
|
+
pendingCommits,
|
|
86
|
+
testKbContext(),
|
|
87
|
+
openChangeGate(),
|
|
88
|
+
);
|
|
89
|
+
(svc as unknown as { events?: { emit(e: unknown): void } }).events = {
|
|
90
|
+
emit: (e: unknown) => emitted.push(e),
|
|
91
|
+
};
|
|
92
|
+
return { svc, sets, unlocked, dequeued, invalidated, emitted, fileLocks, pendingCommits };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
describe('closeOpenChangeRequestsAsRepositoryReplaced', () => {
|
|
96
|
+
it('closes every open request with the reason, and deletes nothing', async () => {
|
|
97
|
+
const { svc, sets, invalidated, emitted } = makeService([
|
|
98
|
+
[
|
|
99
|
+
{ number: 7, sourceBranch: 'alice/draft' },
|
|
100
|
+
{ number: 9, sourceBranch: 'bob/draft' },
|
|
101
|
+
],
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
await expect(svc.closeOpenChangeRequestsAsRepositoryReplaced()).resolves.toBe(2);
|
|
105
|
+
|
|
106
|
+
// ONE statement closed both: a read-then-write loop would have missed any
|
|
107
|
+
// request opened while it ran.
|
|
108
|
+
expect(sets).toHaveLength(2); // the closing pass, then the pass that found nothing
|
|
109
|
+
expect(sets[0]?.state).toBe('closed');
|
|
110
|
+
// The reason outlives the admin who chose it — that is the whole point
|
|
111
|
+
// of a column rather than a log line.
|
|
112
|
+
expect(sets[0]?.closedReason).toBe('repository-replaced');
|
|
113
|
+
expect(sets[0]?.closedAt).toBeInstanceOf(Date);
|
|
114
|
+
expect(invalidated).toEqual([7, 9]);
|
|
115
|
+
expect(emitted).toEqual([
|
|
116
|
+
{ kind: 'change-request-rejected', number: 7 },
|
|
117
|
+
{ kind: 'change-request-rejected', number: 9 },
|
|
118
|
+
]);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('closes a request opened while it was closing the others', async () => {
|
|
122
|
+
// The deployment is still live — the address has not been saved yet — so
|
|
123
|
+
// `POST /change-requests` is still being served. The first pass closed two;
|
|
124
|
+
// #11 arrived after it and is closed by the next one.
|
|
125
|
+
const { svc, invalidated } = makeService([
|
|
126
|
+
[
|
|
127
|
+
{ number: 7, sourceBranch: 'alice/draft' },
|
|
128
|
+
{ number: 9, sourceBranch: 'bob/draft' },
|
|
129
|
+
],
|
|
130
|
+
[{ number: 11, sourceBranch: 'carol/draft' }],
|
|
131
|
+
]);
|
|
132
|
+
|
|
133
|
+
await expect(svc.closeOpenChangeRequestsAsRepositoryReplaced()).resolves.toBe(3);
|
|
134
|
+
expect(invalidated).toEqual([7, 9, 11]);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('gives up after a bounded number of passes rather than holding the save open', async () => {
|
|
138
|
+
// A client opening requests in a loop must not be able to keep the save
|
|
139
|
+
// from ever returning. What survives the last pass was opened after the
|
|
140
|
+
// repository was replaced, and the deleted-branch sweep owns it.
|
|
141
|
+
const endless = Array.from({ length: 50 }, (_, i) => [
|
|
142
|
+
{ number: i + 1, sourceBranch: `writer-${i}/draft` },
|
|
143
|
+
]);
|
|
144
|
+
const { svc } = makeService(endless, 50);
|
|
145
|
+
const closed = await svc.closeOpenChangeRequestsAsRepositoryReplaced();
|
|
146
|
+
expect(closed).toBeGreaterThan(0);
|
|
147
|
+
expect(closed).toBeLessThan(50);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('skips a request a merge or a withdrawal closed first', async () => {
|
|
151
|
+
// The guard is the statement's own `where state = 'open'`: #9 was closed by
|
|
152
|
+
// somebody else between the count and this call, so it is not in the
|
|
153
|
+
// returned set — and nothing is done on its behalf.
|
|
154
|
+
const { svc, unlocked, dequeued, invalidated, emitted } = makeService(
|
|
155
|
+
[[{ number: 7, sourceBranch: 'alice/draft' }]],
|
|
156
|
+
2,
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
await expect(svc.closeOpenChangeRequestsAsRepositoryReplaced()).resolves.toBe(1);
|
|
160
|
+
|
|
161
|
+
expect(unlocked).toEqual(['alice/draft']);
|
|
162
|
+
expect(dequeued).toEqual(['alice/draft']);
|
|
163
|
+
expect(invalidated).toEqual([7]);
|
|
164
|
+
expect(emitted).toEqual([{ kind: 'change-request-rejected', number: 7 }]);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('releases the file locks held on the branches it closes', async () => {
|
|
168
|
+
const { svc, unlocked } = makeService([
|
|
169
|
+
[
|
|
170
|
+
{ number: 7, sourceBranch: 'alice/draft' },
|
|
171
|
+
{ number: 9, sourceBranch: 'bob/draft' },
|
|
172
|
+
],
|
|
173
|
+
]);
|
|
174
|
+
|
|
175
|
+
await svc.closeOpenChangeRequestsAsRepositoryReplaced();
|
|
176
|
+
|
|
177
|
+
// Nobody can publish those bytes or release the lock by finishing: the
|
|
178
|
+
// branch belongs to a repository this deployment no longer has.
|
|
179
|
+
expect(unlocked).toEqual(['alice/draft', 'bob/draft']);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('takes the commits already queued for those branches off the worker', async () => {
|
|
183
|
+
const { svc, dequeued, pendingCommits } = makeService([
|
|
184
|
+
[{ number: 7, sourceBranch: 'alice/draft' }],
|
|
185
|
+
]);
|
|
186
|
+
|
|
187
|
+
await svc.closeOpenChangeRequestsAsRepositoryReplaced();
|
|
188
|
+
|
|
189
|
+
// A release ENQUEUES the bytes and only then drops its lock, so dropping
|
|
190
|
+
// the locks can leave rows waiting. Left pending, the worker would commit
|
|
191
|
+
// them onto a same-named branch of a different repository after the
|
|
192
|
+
// re-clone.
|
|
193
|
+
expect(dequeued).toEqual(['alice/draft']);
|
|
194
|
+
expect(vi.mocked(pendingCommits.markNeedsAttentionOnBranch).mock.calls[0]?.[1]).toMatch(
|
|
195
|
+
/repository was replaced/,
|
|
196
|
+
);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('still closes the requests when a lock release fails', async () => {
|
|
200
|
+
const { svc, fileLocks } = makeService([[{ number: 7, sourceBranch: 'alice/draft' }]]);
|
|
201
|
+
vi.mocked(fileLocks.releaseAllOnBranch).mockRejectedValueOnce(new Error('lock table is down'));
|
|
202
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
203
|
+
try {
|
|
204
|
+
// The close is the decision the admin made; a lock expires on its own
|
|
205
|
+
// within the minute, so it is not worth half-closing the set over.
|
|
206
|
+
await expect(svc.closeOpenChangeRequestsAsRepositoryReplaced()).resolves.toBe(1);
|
|
207
|
+
} finally {
|
|
208
|
+
vi.restoreAllMocks();
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('still closes the requests when the queued commits cannot be set aside', async () => {
|
|
213
|
+
const { svc, pendingCommits, unlocked } = makeService([
|
|
214
|
+
[{ number: 7, sourceBranch: 'alice/draft' }],
|
|
215
|
+
]);
|
|
216
|
+
vi.mocked(pendingCommits.markNeedsAttentionOnBranch).mockRejectedValueOnce(
|
|
217
|
+
new Error('pending_commits is down'),
|
|
218
|
+
);
|
|
219
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
220
|
+
try {
|
|
221
|
+
await expect(svc.closeOpenChangeRequestsAsRepositoryReplaced()).resolves.toBe(1);
|
|
222
|
+
expect(unlocked).toEqual(['alice/draft']);
|
|
223
|
+
} finally {
|
|
224
|
+
vi.restoreAllMocks();
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('closes nothing, and asks nothing of the lock table, when no request is open', async () => {
|
|
229
|
+
const { svc, unlocked, dequeued, emitted } = makeService([]);
|
|
230
|
+
await expect(svc.closeOpenChangeRequestsAsRepositoryReplaced()).resolves.toBe(0);
|
|
231
|
+
expect(unlocked).toEqual([]);
|
|
232
|
+
expect(dequeued).toEqual([]);
|
|
233
|
+
expect(emitted).toEqual([]);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe('countOpenChangeRequests', () => {
|
|
238
|
+
it('answers how many are open, so the confirmation can say what it is about', async () => {
|
|
239
|
+
const { svc } = makeService([], 2);
|
|
240
|
+
// A SQL aggregate: the question is an integer, and materializing one row
|
|
241
|
+
// per open request to answer it would scale with the wrong thing.
|
|
242
|
+
await expect(svc.countOpenChangeRequests()).resolves.toBe(2);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('answers zero when the aggregate comes back empty', async () => {
|
|
246
|
+
const { svc } = makeService([], 0);
|
|
247
|
+
await expect(svc.countOpenChangeRequests()).resolves.toBe(0);
|
|
248
|
+
});
|
|
249
|
+
});
|