@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,305 @@
|
|
|
1
|
+
import { get as httpGet, type Server } from 'node:http';
|
|
2
|
+
import type { AddressInfo } from 'node:net';
|
|
3
|
+
import express from 'express';
|
|
4
|
+
import { describe, it, expect, afterEach, vi } from 'vitest';
|
|
5
|
+
import type { IWorkflowService } from '@bevel-software/platform-shared';
|
|
6
|
+
import { testKbContext } from '../../../__tests__/kb-context.js';
|
|
7
|
+
import { NodeFs } from '../../kb-fs/node-fs.js';
|
|
8
|
+
import { VersionNotOnBranchError, VERSION_NOT_ON_BRANCH_MESSAGE } from '../../../shared/domain-errors.js';
|
|
9
|
+
import type { IAccessControl } from '../../access/access-control.interface.js';
|
|
10
|
+
import type { ICreatorAccess } from '../../access-model/creator.js';
|
|
11
|
+
import type { IAdminAccessService } from '../../admin/admin.interface.js';
|
|
12
|
+
import type { WorkflowEventBus } from '../../workflow/event-bus.js';
|
|
13
|
+
import type { AuthService } from '../../auth/auth.service.js';
|
|
14
|
+
import { createWorkspaceRoutes } from '../workspace.routes.js';
|
|
15
|
+
import type { WorkspaceService } from '../workspace.service.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* `GET /workspace/:id/file/raw?ref=<sha>` — the bytes of a PAST save.
|
|
19
|
+
*
|
|
20
|
+
* What the suite is really pinning is that the ref changes WHICH bytes and
|
|
21
|
+
* nothing else about the route: the same read gate, the same `download:` verb,
|
|
22
|
+
* the same content type and svg sandbox, and — the one that would be worst to
|
|
23
|
+
* get wrong — that a request naming a ref can never come back with today's
|
|
24
|
+
* working-tree bytes. A silent fall back there looks exactly like a success.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const USER_ID = 'user-1';
|
|
28
|
+
const USER = { id: USER_ID, email: 'alice@example.com', name: 'Alice' };
|
|
29
|
+
const WS = 'target-company-state';
|
|
30
|
+
const KB = 'knowledge-base';
|
|
31
|
+
const SHA = 'a1b2c3d4e5f60718293a4b5c6d7e8f9012345678';
|
|
32
|
+
|
|
33
|
+
/** Bytes that are not valid UTF-8, so a decode step anywhere would be visible. */
|
|
34
|
+
const PAST_BYTES = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x00, 0xff, 0xfe, 0x01]);
|
|
35
|
+
const TREE_BYTES = Buffer.from('TODAY');
|
|
36
|
+
|
|
37
|
+
const stubCreatorAccess: ICreatorAccess = {
|
|
38
|
+
planForCreate: async () => null,
|
|
39
|
+
grantInExtractedFile: async () => null,
|
|
40
|
+
noteAccessFileWritten: () => {},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
interface Harness {
|
|
44
|
+
server: Server;
|
|
45
|
+
baseUrl: string;
|
|
46
|
+
fileBytesAtChange: ReturnType<typeof vi.fn>;
|
|
47
|
+
readFileBinary: ReturnType<typeof vi.fn>;
|
|
48
|
+
canRead: ReturnType<typeof vi.fn>;
|
|
49
|
+
canDownload: ReturnType<typeof vi.fn>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function makeHarness(
|
|
53
|
+
opts: {
|
|
54
|
+
canRead?: boolean;
|
|
55
|
+
canDownload?: boolean;
|
|
56
|
+
/** What the workflow service answers; default is the past bytes. */
|
|
57
|
+
atRef?: () => Promise<{ bytes: Uint8Array; blobId: string } | null>;
|
|
58
|
+
} = {},
|
|
59
|
+
): Promise<Harness> {
|
|
60
|
+
const canRead = vi.fn(async () => opts.canRead !== false);
|
|
61
|
+
const canDownload = vi.fn(async () => opts.canDownload !== false);
|
|
62
|
+
const accessControl = {
|
|
63
|
+
canRead,
|
|
64
|
+
canDownload,
|
|
65
|
+
canReadBatch: vi.fn(async (_w: string, _e: string, paths: string[]) =>
|
|
66
|
+
new Map(paths.map((p) => [p, opts.canRead !== false])),
|
|
67
|
+
),
|
|
68
|
+
} as unknown as IAccessControl;
|
|
69
|
+
|
|
70
|
+
const readFileBinary = vi.fn(async () => TREE_BYTES);
|
|
71
|
+
const workspaceService = { readFileBinary } as unknown as WorkspaceService;
|
|
72
|
+
|
|
73
|
+
const fileBytesAtChange = vi.fn(
|
|
74
|
+
opts.atRef ?? (async () => ({ bytes: PAST_BYTES, blobId: 'deadbeef'.repeat(5) })),
|
|
75
|
+
);
|
|
76
|
+
const workflowService = { fileBytesAtChange } as unknown as IWorkflowService;
|
|
77
|
+
|
|
78
|
+
const authService = { getUserById: vi.fn(async () => USER) } as unknown as AuthService;
|
|
79
|
+
|
|
80
|
+
const app = express();
|
|
81
|
+
app.use(express.json());
|
|
82
|
+
app.use('/api', (req, _res, next) => {
|
|
83
|
+
(req as unknown as { userId: string }).userId = USER_ID;
|
|
84
|
+
next();
|
|
85
|
+
});
|
|
86
|
+
app.use(
|
|
87
|
+
'/api',
|
|
88
|
+
createWorkspaceRoutes(
|
|
89
|
+
workspaceService,
|
|
90
|
+
authService,
|
|
91
|
+
workflowService,
|
|
92
|
+
{} as unknown as WorkflowEventBus,
|
|
93
|
+
accessControl,
|
|
94
|
+
testKbContext({ kbDirName: KB }),
|
|
95
|
+
stubCreatorAccess,
|
|
96
|
+
{ isAdmin: async () => false } as unknown as IAdminAccessService,
|
|
97
|
+
new NodeFs(),
|
|
98
|
+
),
|
|
99
|
+
);
|
|
100
|
+
const server = await new Promise<Server>((resolve) => {
|
|
101
|
+
const s = app.listen(0, () => resolve(s));
|
|
102
|
+
});
|
|
103
|
+
const addr = server.address() as AddressInfo;
|
|
104
|
+
return {
|
|
105
|
+
server,
|
|
106
|
+
baseUrl: `http://127.0.0.1:${addr.port}`,
|
|
107
|
+
fileBytesAtChange,
|
|
108
|
+
readFileBinary,
|
|
109
|
+
canRead,
|
|
110
|
+
canDownload,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function close(s: Server): Promise<void> {
|
|
115
|
+
return new Promise((resolve, reject) => s.close((e) => (e ? reject(e) : resolve())));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
describe('GET /workspace/:id/file/raw?ref= — one file at a past save', () => {
|
|
119
|
+
let h: Harness | null = null;
|
|
120
|
+
afterEach(async () => {
|
|
121
|
+
if (h) await close(h.server);
|
|
122
|
+
h = null;
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const raw = (query: string) => fetch(`${h!.baseUrl}/api/workspace/${WS}/file/raw?${query}`);
|
|
126
|
+
const png = (extra = '') =>
|
|
127
|
+
`path=${encodeURIComponent(`${KB}/Docs/logo.png`)}&ref=${SHA}${extra}`;
|
|
128
|
+
|
|
129
|
+
it('serves the save\'s exact bytes with the format\'s content type, and never the working tree', async () => {
|
|
130
|
+
h = await makeHarness();
|
|
131
|
+
const res = await raw(png());
|
|
132
|
+
expect(res.status).toBe(200);
|
|
133
|
+
expect(res.headers.get('content-type')).toBe('image/png');
|
|
134
|
+
expect(res.headers.get('x-content-type-options')).toBe('nosniff');
|
|
135
|
+
expect(res.headers.get('cache-control')).toBe('private, no-cache');
|
|
136
|
+
const body = Buffer.from(await res.arrayBuffer());
|
|
137
|
+
expect(body.equals(PAST_BYTES)).toBe(true);
|
|
138
|
+
// THE regression guard: a ref read must not reach the working tree at all.
|
|
139
|
+
expect(h.readFileBinary).not.toHaveBeenCalled();
|
|
140
|
+
expect(h.fileBytesAtChange).toHaveBeenCalledWith(WS, `${KB}/Docs/logo.png`, SHA, 'after');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('reads the side just before the save when asked, for a save that deleted the file', async () => {
|
|
144
|
+
h = await makeHarness();
|
|
145
|
+
const res = await raw(png('&side=before'));
|
|
146
|
+
expect(res.status).toBe(200);
|
|
147
|
+
expect(h.fileBytesAtChange).toHaveBeenCalledWith(WS, `${KB}/Docs/logo.png`, SHA, 'before');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('answers 400 for a malformed ref, and never falls back to today\'s bytes', async () => {
|
|
151
|
+
h = await makeHarness();
|
|
152
|
+
for (const bad of ['HEAD', 'main', 'zzz', `${SHA}${SHA}`, 'abc']) {
|
|
153
|
+
const res = await raw(`path=${encodeURIComponent(`${KB}/Docs/logo.png`)}&ref=${bad}`);
|
|
154
|
+
expect(res.status).toBe(400);
|
|
155
|
+
}
|
|
156
|
+
expect(h.readFileBinary).not.toHaveBeenCalled();
|
|
157
|
+
expect(h.fileBytesAtChange).not.toHaveBeenCalled();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('answers 400 for a path carrying a line break, before any gate is asked', async () => {
|
|
161
|
+
// `cat-file --batch` reads one `<ref>:<path>` object name per LINE, so
|
|
162
|
+
// `Docs/secret.md%0Azzz` would be TWO specs: git answers about
|
|
163
|
+
// `Docs/secret.md` and serves its bytes, while the read gate, the
|
|
164
|
+
// download gate and the not-a-directory check were all asked about the
|
|
165
|
+
// whole string — a name that exists nowhere, whose own frontmatter rules
|
|
166
|
+
// therefore never ran. Refused at the normaliser, which is before the
|
|
167
|
+
// gates, so neither gate is even consulted.
|
|
168
|
+
h = await makeHarness();
|
|
169
|
+
for (const injected of [
|
|
170
|
+
`${KB}/Docs/secret.md\nzzz`,
|
|
171
|
+
`${KB}/Docs/secret.md\rzzz`,
|
|
172
|
+
`${KB}/Docs/secret.md\n${KB}/Docs/logo.png`,
|
|
173
|
+
`${KB}/Docs/secret.md\u0000zzz`,
|
|
174
|
+
]) {
|
|
175
|
+
const withRef = await raw(`path=${encodeURIComponent(injected)}&ref=${SHA}`);
|
|
176
|
+
expect(withRef.status).toBe(400);
|
|
177
|
+
expect(((await withRef.json()) as { error: string }).error).toMatch(/control character/);
|
|
178
|
+
// The same refusal without a ref: the rule is the path's, not the
|
|
179
|
+
// history feature's, so today's working-tree read is closed too.
|
|
180
|
+
expect((await raw(`path=${encodeURIComponent(injected)}`)).status).toBe(400);
|
|
181
|
+
}
|
|
182
|
+
expect(h.canRead).not.toHaveBeenCalled();
|
|
183
|
+
expect(h.canDownload).not.toHaveBeenCalled();
|
|
184
|
+
expect(h.fileBytesAtChange).not.toHaveBeenCalled();
|
|
185
|
+
expect(h.readFileBinary).not.toHaveBeenCalled();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('answers 400 for an unknown side, and for a side with no ref', async () => {
|
|
189
|
+
h = await makeHarness();
|
|
190
|
+
expect((await raw(png('&side=middle'))).status).toBe(400);
|
|
191
|
+
const orphan = await raw(`path=${encodeURIComponent(`${KB}/Docs/logo.png`)}&side=before`);
|
|
192
|
+
expect(orphan.status).toBe(400);
|
|
193
|
+
expect(h.readFileBinary).not.toHaveBeenCalled();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('answers 404 with the history message for a save off this branch', async () => {
|
|
197
|
+
h = await makeHarness({
|
|
198
|
+
atRef: async () => {
|
|
199
|
+
throw new VersionNotOnBranchError();
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
const res = await raw(png());
|
|
203
|
+
expect(res.status).toBe(404);
|
|
204
|
+
expect(((await res.json()) as { error: string }).error).toBe(VERSION_NOT_ON_BRANCH_MESSAGE);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('answers 404 when the file was not there on that side', async () => {
|
|
208
|
+
h = await makeHarness({ atRef: async () => null });
|
|
209
|
+
const res = await raw(png());
|
|
210
|
+
expect(res.status).toBe(404);
|
|
211
|
+
expect(((await res.json()) as { error: string }).error).toBe('File not found');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('answers a failure of OUR making with a 500, not "File not found"', async () => {
|
|
215
|
+
// The ref read goes through the git runner: a blob over its output ceiling
|
|
216
|
+
// arrives here as a RangeError, a hung `cat-file` as a run error. Either
|
|
217
|
+
// one answered 404 would tell a reader the save they can SEE LISTED had
|
|
218
|
+
// vanished — and would retire the pane's "Try again" as pointless.
|
|
219
|
+
h = await makeHarness({
|
|
220
|
+
atRef: async () => {
|
|
221
|
+
throw new RangeError('git output exceeded 67108864 bytes');
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
const res = await raw(png());
|
|
225
|
+
expect(res.status).toBe(500);
|
|
226
|
+
expect(((await res.json()) as { error: string }).error).toBe(
|
|
227
|
+
'Could not read this version of the file',
|
|
228
|
+
);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('refuses a reader who may not read the file today, before any read happens', async () => {
|
|
232
|
+
h = await makeHarness({ canRead: false });
|
|
233
|
+
const res = await raw(png());
|
|
234
|
+
expect(res.status).toBe(403);
|
|
235
|
+
expect(h.fileBytesAtChange).not.toHaveBeenCalled();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('refuses a download without the download verb, and allows the inline read', async () => {
|
|
239
|
+
h = await makeHarness({ canDownload: false });
|
|
240
|
+
const dl = await raw(png('&download=1'));
|
|
241
|
+
expect(dl.status).toBe(403);
|
|
242
|
+
expect(((await dl.json()) as { error: string }).error).toBe('Download permission required');
|
|
243
|
+
expect(h.fileBytesAtChange).not.toHaveBeenCalled();
|
|
244
|
+
|
|
245
|
+
// The same reader still SEES the version — read and download are separate
|
|
246
|
+
// verbs at a ref exactly as they are in the working tree.
|
|
247
|
+
const inline = await raw(png());
|
|
248
|
+
expect(inline.status).toBe(200);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('downloads the VERSION bytes, named in the attachment disposition', async () => {
|
|
252
|
+
h = await makeHarness();
|
|
253
|
+
const res = await raw(png('&download=1'));
|
|
254
|
+
expect(res.status).toBe(200);
|
|
255
|
+
// The bytes, not just the headers: a download that quietly fell back to
|
|
256
|
+
// the working tree would hand over today's file under a past save's name,
|
|
257
|
+
// and every header assertion would still pass.
|
|
258
|
+
expect(Buffer.from(await res.arrayBuffer()).equals(PAST_BYTES)).toBe(true);
|
|
259
|
+
expect(h.fileBytesAtChange).toHaveBeenCalledWith(WS, `${KB}/Docs/logo.png`, SHA, 'after');
|
|
260
|
+
expect(h.readFileBinary).not.toHaveBeenCalled();
|
|
261
|
+
expect(res.headers.get('content-disposition')).toContain(
|
|
262
|
+
`filename*=UTF-8''${encodeURIComponent('logo.png')}`,
|
|
263
|
+
);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('keeps the svg rules at a ref: sandboxed inline, octet-stream on download', async () => {
|
|
267
|
+
h = await makeHarness();
|
|
268
|
+
const path = `path=${encodeURIComponent(`${KB}/Docs/chart.svg`)}&ref=${SHA}`;
|
|
269
|
+
const inline = await raw(path);
|
|
270
|
+
expect(inline.headers.get('content-type')).toBe('image/svg+xml');
|
|
271
|
+
expect(inline.headers.get('content-security-policy')).toBe('sandbox');
|
|
272
|
+
const dl = await raw(`${path}&download=1`);
|
|
273
|
+
expect(dl.headers.get('content-type')).toBe('application/octet-stream');
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('ETags the version by its blob id and answers a revalidation with 304', async () => {
|
|
277
|
+
h = await makeHarness();
|
|
278
|
+
const first = await raw(png());
|
|
279
|
+
const etag = first.headers.get('etag');
|
|
280
|
+
expect(etag).toBe(`"${'deadbeef'.repeat(5)}"`);
|
|
281
|
+
|
|
282
|
+
// `node:http` rather than fetch: undici adds `Cache-Control: no-cache` to
|
|
283
|
+
// a conditional request, which Express reads as "do not answer 304".
|
|
284
|
+
const status = await new Promise<number>((resolve, reject) => {
|
|
285
|
+
const req = httpGet(
|
|
286
|
+
`${h!.baseUrl}/api/workspace/${WS}/file/raw?${png()}`,
|
|
287
|
+
{ headers: { 'If-None-Match': etag! } },
|
|
288
|
+
(res) => {
|
|
289
|
+
res.resume();
|
|
290
|
+
resolve(res.statusCode ?? 0);
|
|
291
|
+
},
|
|
292
|
+
);
|
|
293
|
+
req.on('error', reject);
|
|
294
|
+
});
|
|
295
|
+
expect(status).toBe(304);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('is unchanged for a request with no ref: the working tree, as before', async () => {
|
|
299
|
+
h = await makeHarness();
|
|
300
|
+
const res = await raw(`path=${encodeURIComponent(`${KB}/Docs/logo.png`)}`);
|
|
301
|
+
expect(res.status).toBe(200);
|
|
302
|
+
expect(Buffer.from(await res.arrayBuffer()).equals(TREE_BYTES)).toBe(true);
|
|
303
|
+
expect(h.fileBytesAtChange).not.toHaveBeenCalled();
|
|
304
|
+
});
|
|
305
|
+
});
|
|
@@ -109,6 +109,8 @@ async function makeHarness(opts: {
|
|
|
109
109
|
return { userId: USER_ID, email: USER.email };
|
|
110
110
|
}) as unknown as AuthService['verifyToken'],
|
|
111
111
|
};
|
|
112
|
+
// A session is accepted only for an account that is on; this one is.
|
|
113
|
+
authServiceMock.resolveSession = vi.fn(async (token: string) => authServiceMock.verifyToken!(token));
|
|
112
114
|
const authService = authServiceMock as AuthService;
|
|
113
115
|
|
|
114
116
|
const workflowService = {} as unknown as IWorkflowService;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import { testKbContext } from '../../../__tests__/kb-context.js';
|
|
6
|
+
import { NodeFs } from '../../kb-fs/node-fs.js';
|
|
7
|
+
import { WorkspaceService } from '../workspace.service.js';
|
|
8
|
+
import type { IGitRunner } from '../../../shared/git.contract.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* What happens to work ALREADY IN FLIGHT when the knowledge-base repository is
|
|
12
|
+
* replaced under a running server.
|
|
13
|
+
*
|
|
14
|
+
* `forgetClone` clearing the caches is not enough on its own: a bootstrap or a
|
|
15
|
+
* fetch started before the replacement finishes afterwards and writes its
|
|
16
|
+
* result back into those very caches — about a working copy that no longer
|
|
17
|
+
* exists. Both readers record how many times the copy has been taken away and
|
|
18
|
+
* check that before publishing anything.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const BRANCH = 'main';
|
|
22
|
+
const WORKSPACE_ID = encodeURIComponent(BRANCH);
|
|
23
|
+
|
|
24
|
+
let root: string;
|
|
25
|
+
let workspacesRoot: string;
|
|
26
|
+
|
|
27
|
+
beforeEach(async () => {
|
|
28
|
+
root = await fs.mkdtemp(path.join(os.tmpdir(), 'ws-forget-races-'));
|
|
29
|
+
workspacesRoot = path.join(root, 'workspaces');
|
|
30
|
+
await fs.mkdir(workspacesRoot, { recursive: true });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterEach(async () => {
|
|
34
|
+
vi.restoreAllMocks();
|
|
35
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
/** A git runner whose every call is a promise this test resolves by hand. */
|
|
39
|
+
function heldRunner() {
|
|
40
|
+
const calls: Array<{ args: string[]; settle: () => void; fail: (err: Error) => void }> = [];
|
|
41
|
+
const runner = {
|
|
42
|
+
defaultTimeoutMs: 1000,
|
|
43
|
+
credentials: { token: () => null, username: () => null, fingerprint: () => 'none' },
|
|
44
|
+
run: vi.fn(
|
|
45
|
+
(_cwd: string, args: string[]) =>
|
|
46
|
+
new Promise((resolve, reject) => {
|
|
47
|
+
calls.push({
|
|
48
|
+
args,
|
|
49
|
+
settle: () => resolve({ stdout: '', stderr: '', exitCode: 0 }),
|
|
50
|
+
fail: (err: Error) => reject(err),
|
|
51
|
+
});
|
|
52
|
+
}),
|
|
53
|
+
),
|
|
54
|
+
} as unknown as IGitRunner;
|
|
55
|
+
return { runner, calls };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function service(runner: IGitRunner) {
|
|
59
|
+
return new WorkspaceService(
|
|
60
|
+
workspacesRoot,
|
|
61
|
+
() => `${root}/configured.git`,
|
|
62
|
+
testKbContext({ branchModel: { defaultBranch: BRANCH, protectedBranches: [BRANCH] } }),
|
|
63
|
+
new NodeFs(),
|
|
64
|
+
runner,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** A working copy on disk, as far as anything that only looks for `.git` cares. */
|
|
69
|
+
async function cloneOnDisk(): Promise<string> {
|
|
70
|
+
const repoDir = path.join(workspacesRoot, WORKSPACE_ID, 'knowledge-base');
|
|
71
|
+
await fs.mkdir(path.join(repoDir, '.git'), { recursive: true });
|
|
72
|
+
return repoDir;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
describe('a fetch in flight when the working copy is taken away', () => {
|
|
76
|
+
it('does not stamp its result onto the clone that replaced it', async () => {
|
|
77
|
+
const { runner, calls } = heldRunner();
|
|
78
|
+
const svc = service(runner);
|
|
79
|
+
const repoDir = await cloneOnDisk();
|
|
80
|
+
|
|
81
|
+
// A fetch of the working copy as it is now, held open.
|
|
82
|
+
const fetching = svc.ensureRemotesFetched(WORKSPACE_ID);
|
|
83
|
+
await vi.waitFor(() => expect(calls.length).toBe(1));
|
|
84
|
+
|
|
85
|
+
// The replacement lands mid-fetch: the copy is set aside, a fresh one is
|
|
86
|
+
// cloned to the same path, and this process is told to forget the old one.
|
|
87
|
+
svc.forgetClone(WORKSPACE_ID);
|
|
88
|
+
calls[0]!.settle();
|
|
89
|
+
await fetching;
|
|
90
|
+
|
|
91
|
+
// The next reader must actually refresh the NEW clone. Were the stale
|
|
92
|
+
// fetch's success stamped, it would be inside the TTL and skipped — the
|
|
93
|
+
// app would serve the refs of a working copy that is gone for 30 seconds.
|
|
94
|
+
const second = svc.ensureRemotesFetched(WORKSPACE_ID);
|
|
95
|
+
await vi.waitFor(() => expect(calls.length).toBe(2));
|
|
96
|
+
expect(calls[1]!.args).toEqual(calls[0]!.args);
|
|
97
|
+
expect(calls[1]!.args).toContain('fetch');
|
|
98
|
+
calls[1]!.settle();
|
|
99
|
+
await second;
|
|
100
|
+
expect(repoDir).toContain(WORKSPACE_ID);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('does not leave a strict caller reading the old copy’s failure', async () => {
|
|
104
|
+
const { runner, calls } = heldRunner();
|
|
105
|
+
const svc = service(runner);
|
|
106
|
+
await cloneOnDisk();
|
|
107
|
+
|
|
108
|
+
const fetching = svc.ensureRemotesFetched(WORKSPACE_ID);
|
|
109
|
+
await vi.waitFor(() => expect(calls.length).toBe(1));
|
|
110
|
+
svc.forgetClone(WORKSPACE_ID);
|
|
111
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
112
|
+
calls[0]!.fail(new Error('repository not found'));
|
|
113
|
+
await fetching;
|
|
114
|
+
|
|
115
|
+
// The failure belonged to the working copy that was replaced. The fresh
|
|
116
|
+
// one gets its own answer, and a successful fetch of it is a success.
|
|
117
|
+
const strict = svc.ensureRemotesFetched(WORKSPACE_ID, { strict: true });
|
|
118
|
+
await vi.waitFor(() => expect(calls.length).toBe(2));
|
|
119
|
+
calls[1]!.settle();
|
|
120
|
+
await expect(strict).resolves.toBeUndefined();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('a bootstrap in flight when the working copy is taken away', () => {
|
|
125
|
+
it('is refused rather than cached as the path of a directory that is gone', async () => {
|
|
126
|
+
const { runner, calls } = heldRunner();
|
|
127
|
+
const svc = service(runner);
|
|
128
|
+
// Nothing on disk: this is a first clone, which is what takes seconds.
|
|
129
|
+
const opening = svc.getOrCreateForBranch(BRANCH);
|
|
130
|
+
await vi.waitFor(() => expect(calls.length).toBeGreaterThan(0));
|
|
131
|
+
|
|
132
|
+
// The replacement lands while the clone runs.
|
|
133
|
+
svc.forgetClone(WORKSPACE_ID);
|
|
134
|
+
// Let the clone — and every config call after it — finish.
|
|
135
|
+
for (let i = 0; i < 40 && calls.length > 0; i++) {
|
|
136
|
+
const pending = calls.splice(0, calls.length);
|
|
137
|
+
for (const call of pending) call.settle();
|
|
138
|
+
await Promise.resolve();
|
|
139
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Told, rather than handed a cached path to a working copy of the
|
|
143
|
+
// repository that was replaced — which every later read would ENOENT on,
|
|
144
|
+
// with nothing left to re-clone it.
|
|
145
|
+
await expect(opening).rejects.toThrow(/replaced while the "main" working copy was being created/);
|
|
146
|
+
});
|
|
147
|
+
});
|