@bevel-software/platform-core-backend 0.25.2 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent-guide/access-control.md +251 -0
- package/agent-guide/conventions.md +35 -0
- package/agent-guide/directory-structure.md +145 -0
- package/agent-guide/finding-things.md +7 -0
- package/agent-guide/html-views.md +83 -0
- package/agent-guide/introduction.md +27 -0
- package/agent-guide/new-knowledge-base.md +24 -0
- package/agent-guide/skills.md +47 -0
- package/agent-guide/tool-manuals.md +227 -0
- package/agent-guide/where-a-new-file-goes.md +36 -0
- package/dist/assets.d.ts +26 -0
- package/dist/assets.d.ts.map +1 -1
- package/dist/assets.js +35 -2
- package/dist/assets.js.map +1 -1
- package/dist/core/catalog-revision.d.ts +19 -1
- package/dist/core/catalog-revision.d.ts.map +1 -1
- package/dist/core/catalog-revision.js +14 -3
- package/dist/core/catalog-revision.js.map +1 -1
- package/dist/core/core-ports.d.ts +11 -0
- package/dist/core/core-ports.d.ts.map +1 -1
- package/dist/core/core-ports.js.map +1 -1
- package/dist/core/create-core-server.d.ts +5 -0
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +65 -3
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +35 -0
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +114 -12
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/core/lifecycle.d.ts +16 -1
- package/dist/core/lifecycle.d.ts.map +1 -1
- package/dist/core/lifecycle.js +18 -1
- package/dist/core/lifecycle.js.map +1 -1
- package/dist/core-config.d.ts +21 -0
- package/dist/core-config.d.ts.map +1 -1
- package/dist/core-config.js +22 -1
- package/dist/core-config.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +17 -0
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +2 -0
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +27 -0
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/directory-sync-bot.js +1 -1
- package/dist/modules/access/directory-sync-bot.js.map +1 -1
- package/dist/modules/agent-guide/agent-guide.d.ts +145 -0
- package/dist/modules/agent-guide/agent-guide.d.ts.map +1 -0
- package/dist/modules/agent-guide/agent-guide.js +199 -0
- package/dist/modules/agent-guide/agent-guide.js.map +1 -0
- package/dist/modules/agent-guide/agent-guide.tools.d.ts +24 -0
- package/dist/modules/agent-guide/agent-guide.tools.d.ts.map +1 -0
- package/dist/modules/agent-guide/agent-guide.tools.js +100 -0
- package/dist/modules/agent-guide/agent-guide.tools.js.map +1 -0
- package/dist/modules/agent-guide/index.d.ts +4 -0
- package/dist/modules/agent-guide/index.d.ts.map +1 -0
- package/dist/modules/agent-guide/index.js +4 -0
- package/dist/modules/agent-guide/index.js.map +1 -0
- package/dist/modules/agent-instructions/agent-instructions.routes.d.ts +3 -2
- package/dist/modules/agent-instructions/agent-instructions.routes.d.ts.map +1 -1
- package/dist/modules/agent-instructions/agent-instructions.routes.js +3 -2
- package/dist/modules/agent-instructions/agent-instructions.routes.js.map +1 -1
- package/dist/modules/agent-instructions/compose.d.ts +14 -6
- package/dist/modules/agent-instructions/compose.d.ts.map +1 -1
- package/dist/modules/agent-instructions/compose.js +28 -8
- package/dist/modules/agent-instructions/compose.js.map +1 -1
- package/dist/modules/agent-instructions/index.d.ts +1 -1
- package/dist/modules/agent-instructions/index.d.ts.map +1 -1
- package/dist/modules/agent-instructions/index.js +1 -1
- package/dist/modules/agent-instructions/index.js.map +1 -1
- package/dist/modules/agent-instructions/shared-file-rules.d.ts +10 -50
- package/dist/modules/agent-instructions/shared-file-rules.d.ts.map +1 -1
- package/dist/modules/agent-instructions/shared-file-rules.js +51 -104
- package/dist/modules/agent-instructions/shared-file-rules.js.map +1 -1
- package/dist/modules/audit/agent-audit.service.d.ts +21 -2
- package/dist/modules/audit/agent-audit.service.d.ts.map +1 -1
- package/dist/modules/audit/agent-audit.service.js +43 -2
- package/dist/modules/audit/agent-audit.service.js.map +1 -1
- package/dist/modules/audit/agent-connection.routes.d.ts +34 -0
- package/dist/modules/audit/agent-connection.routes.d.ts.map +1 -0
- package/dist/modules/audit/agent-connection.routes.js +38 -0
- package/dist/modules/audit/agent-connection.routes.js.map +1 -0
- package/dist/modules/audit/audit.contract.d.ts +24 -0
- package/dist/modules/audit/audit.contract.d.ts.map +1 -1
- package/dist/modules/audit/audit.contract.js.map +1 -1
- package/dist/modules/audit/index.d.ts +1 -0
- package/dist/modules/audit/index.d.ts.map +1 -1
- package/dist/modules/audit/index.js +1 -0
- package/dist/modules/audit/index.js.map +1 -1
- package/dist/modules/auth/account.routes.d.ts.map +1 -1
- package/dist/modules/auth/account.routes.js +10 -3
- package/dist/modules/auth/account.routes.js.map +1 -1
- package/dist/modules/auth/auth.middleware.d.ts +7 -0
- package/dist/modules/auth/auth.middleware.d.ts.map +1 -1
- package/dist/modules/auth/auth.middleware.js +1 -1
- package/dist/modules/auth/auth.middleware.js.map +1 -1
- package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
- package/dist/modules/code-mode/code-mode.tool.js +11 -3
- package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
- package/dist/modules/database/core-schema.d.ts +96 -0
- package/dist/modules/database/core-schema.d.ts.map +1 -1
- package/dist/modules/database/core-schema.js +32 -3
- package/dist/modules/database/core-schema.js.map +1 -1
- package/dist/modules/embed/account-link.service.d.ts +42 -0
- package/dist/modules/embed/account-link.service.d.ts.map +1 -0
- package/dist/modules/embed/account-link.service.js +50 -0
- package/dist/modules/embed/account-link.service.js.map +1 -0
- package/dist/modules/embed/embed-link.d.ts +58 -0
- package/dist/modules/embed/embed-link.d.ts.map +1 -0
- package/dist/modules/embed/embed-link.js +160 -0
- package/dist/modules/embed/embed-link.js.map +1 -0
- package/dist/modules/embed/embed.errors.d.ts +30 -0
- package/dist/modules/embed/embed.errors.d.ts.map +1 -0
- package/dist/modules/embed/embed.errors.js +42 -0
- package/dist/modules/embed/embed.errors.js.map +1 -0
- package/dist/modules/embed/embed.interface.d.ts +204 -0
- package/dist/modules/embed/embed.interface.d.ts.map +1 -0
- package/dist/modules/embed/embed.interface.js +2 -0
- package/dist/modules/embed/embed.interface.js.map +1 -0
- package/dist/modules/embed/embed.routes.d.ts +59 -0
- package/dist/modules/embed/embed.routes.d.ts.map +1 -0
- package/dist/modules/embed/embed.routes.js +343 -0
- package/dist/modules/embed/embed.routes.js.map +1 -0
- package/dist/modules/embed/embed.service.d.ts +169 -0
- package/dist/modules/embed/embed.service.d.ts.map +1 -0
- package/dist/modules/embed/embed.service.js +627 -0
- package/dist/modules/embed/embed.service.js.map +1 -0
- package/dist/modules/embed/embed.tools.d.ts +51 -0
- package/dist/modules/embed/embed.tools.d.ts.map +1 -0
- package/dist/modules/embed/embed.tools.js +170 -0
- package/dist/modules/embed/embed.tools.js.map +1 -0
- package/dist/modules/embed/mcp-app.d.ts +64 -0
- package/dist/modules/embed/mcp-app.d.ts.map +1 -0
- package/dist/modules/embed/mcp-app.js +147 -0
- package/dist/modules/embed/mcp-app.js.map +1 -0
- package/dist/modules/embed/mcp-app.routes.d.ts +21 -0
- package/dist/modules/embed/mcp-app.routes.d.ts.map +1 -0
- package/dist/modules/embed/mcp-app.routes.js +36 -0
- package/dist/modules/embed/mcp-app.routes.js.map +1 -0
- package/dist/modules/github-app/github-app.connection.d.ts +15 -0
- package/dist/modules/github-app/github-app.connection.d.ts.map +1 -1
- package/dist/modules/github-app/github-app.connection.js +34 -3
- package/dist/modules/github-app/github-app.connection.js.map +1 -1
- package/dist/modules/kb-fs/locking-filesystem.d.ts +65 -0
- package/dist/modules/kb-fs/locking-filesystem.d.ts.map +1 -1
- package/dist/modules/kb-fs/locking-filesystem.js +300 -0
- package/dist/modules/kb-fs/locking-filesystem.js.map +1 -1
- package/dist/modules/kb-sync/kb-sync.interface.d.ts +5 -0
- package/dist/modules/kb-sync/kb-sync.interface.d.ts.map +1 -1
- package/dist/modules/kb-sync/kb-sync.service.d.ts.map +1 -1
- package/dist/modules/kb-sync/kb-sync.service.js +6 -0
- package/dist/modules/kb-sync/kb-sync.service.js.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.js +2 -1
- package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
- package/dist/modules/mcp/mcp.service.d.ts +57 -3
- package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.service.js +177 -24
- package/dist/modules/mcp/mcp.service.js.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts +24 -0
- package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js +32 -4
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
- package/dist/modules/mcp/tool-schema-guard.d.ts +105 -0
- package/dist/modules/mcp/tool-schema-guard.d.ts.map +1 -0
- package/dist/modules/mcp/tool-schema-guard.js +171 -0
- package/dist/modules/mcp/tool-schema-guard.js.map +1 -0
- package/dist/modules/onboarding/onboarding.contract.d.ts +80 -0
- package/dist/modules/onboarding/onboarding.contract.d.ts.map +1 -0
- package/dist/modules/onboarding/onboarding.contract.js +7 -0
- package/dist/modules/onboarding/onboarding.contract.js.map +1 -0
- package/dist/modules/onboarding/onboarding.routes.d.ts +18 -0
- package/dist/modules/onboarding/onboarding.routes.d.ts.map +1 -0
- package/dist/modules/onboarding/onboarding.routes.js +81 -0
- package/dist/modules/onboarding/onboarding.routes.js.map +1 -0
- package/dist/modules/onboarding/starter-pack.service.d.ts +213 -0
- package/dist/modules/onboarding/starter-pack.service.d.ts.map +1 -0
- package/dist/modules/onboarding/starter-pack.service.js +492 -0
- package/dist/modules/onboarding/starter-pack.service.js.map +1 -0
- package/dist/modules/onboarding/starter-packs.d.ts +78 -0
- package/dist/modules/onboarding/starter-packs.d.ts.map +1 -0
- package/dist/modules/onboarding/starter-packs.js +156 -0
- package/dist/modules/onboarding/starter-packs.js.map +1 -0
- package/dist/modules/plugins/index.d.ts +1 -1
- package/dist/modules/plugins/index.d.ts.map +1 -1
- package/dist/modules/plugins/index.js +1 -1
- package/dist/modules/plugins/index.js.map +1 -1
- package/dist/modules/plugins/plugin-provision.service.d.ts +19 -0
- package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -1
- package/dist/modules/plugins/plugin-provision.service.js +18 -1
- package/dist/modules/plugins/plugin-provision.service.js.map +1 -1
- package/dist/modules/plugins/plugins.routes.d.ts +0 -36
- package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.routes.js +31 -0
- package/dist/modules/plugins/plugins.routes.js.map +1 -1
- package/dist/modules/plugins/plugins.tools.d.ts +36 -2
- package/dist/modules/plugins/plugins.tools.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.tools.js +71 -14
- package/dist/modules/plugins/plugins.tools.js.map +1 -1
- package/dist/modules/settings/deployment-settings.service.d.ts +34 -7
- package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
- package/dist/modules/settings/deployment-settings.service.js +148 -54
- package/dist/modules/settings/deployment-settings.service.js.map +1 -1
- package/dist/modules/settings/repository-source.d.ts +1 -0
- package/dist/modules/settings/repository-source.d.ts.map +1 -1
- package/dist/modules/settings/repository-source.js +3 -3
- package/dist/modules/settings/repository-source.js.map +1 -1
- package/dist/modules/settings/setup.routes.d.ts.map +1 -1
- package/dist/modules/settings/setup.routes.js +3 -6
- package/dist/modules/settings/setup.routes.js.map +1 -1
- package/dist/modules/skills/skills.tools.d.ts.map +1 -1
- package/dist/modules/skills/skills.tools.js +103 -43
- package/dist/modules/skills/skills.tools.js.map +1 -1
- package/dist/modules/tool-auth/connection-key-rejection.d.ts +10 -1
- package/dist/modules/tool-auth/connection-key-rejection.d.ts.map +1 -1
- package/dist/modules/tool-auth/connection-key-rejection.js +11 -3
- package/dist/modules/tool-auth/connection-key-rejection.js.map +1 -1
- package/dist/modules/tool-auth/external-api-key.interface.d.ts +8 -0
- package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.d.ts +24 -2
- package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.js +59 -2
- package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
- package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
- package/dist/modules/tool-auth/tool-auth.middleware.js +9 -4
- package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
- package/dist/modules/tool-helpers/index.d.ts +28 -1
- package/dist/modules/tool-helpers/index.d.ts.map +1 -1
- package/dist/modules/tool-helpers/index.js +28 -1
- package/dist/modules/tool-helpers/index.js.map +1 -1
- package/dist/modules/tool-helpers/route-argument-check.d.ts +10 -0
- package/dist/modules/tool-helpers/route-argument-check.d.ts.map +1 -0
- package/dist/modules/tool-helpers/route-argument-check.js +132 -0
- package/dist/modules/tool-helpers/route-argument-check.js.map +1 -0
- package/dist/modules/tool-helpers/route-tool-schemas.d.ts +89 -0
- package/dist/modules/tool-helpers/route-tool-schemas.d.ts.map +1 -0
- package/dist/modules/tool-helpers/route-tool-schemas.js +113 -0
- package/dist/modules/tool-helpers/route-tool-schemas.js.map +1 -0
- package/dist/modules/tool-helpers/tool-def.d.ts +71 -0
- package/dist/modules/tool-helpers/tool-def.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool-def.js +70 -6
- package/dist/modules/tool-helpers/tool-def.js.map +1 -1
- package/dist/modules/tool-helpers/tool-handler.d.ts +46 -6
- package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool-handler.js +157 -8
- package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
- package/dist/modules/tool-helpers/tool.contract.d.ts +9 -0
- 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-manuals/tool-manuals.contract.d.ts +23 -4
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.contract.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts +4 -0
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.js +14 -0
- package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.d.ts +7 -0
- package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.js +77 -38
- package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
- package/dist/modules/tool-registry/description-length.d.ts +20 -16
- package/dist/modules/tool-registry/description-length.d.ts.map +1 -1
- package/dist/modules/tool-registry/description-length.js +50 -30
- package/dist/modules/tool-registry/description-length.js.map +1 -1
- package/dist/modules/tool-registry/guide-first.d.ts +27 -0
- package/dist/modules/tool-registry/guide-first.d.ts.map +1 -0
- package/dist/modules/tool-registry/guide-first.js +40 -0
- package/dist/modules/tool-registry/guide-first.js.map +1 -0
- package/dist/modules/tool-registry/tool-registry.d.ts +6 -0
- package/dist/modules/tool-registry/tool-registry.d.ts.map +1 -1
- package/dist/modules/tool-registry/tool-registry.js +9 -2
- package/dist/modules/tool-registry/tool-registry.js.map +1 -1
- package/dist/modules/workflow/agent-tools/change-request-read-shape.d.ts +449 -0
- package/dist/modules/workflow/agent-tools/change-request-read-shape.d.ts.map +1 -0
- package/dist/modules/workflow/agent-tools/change-request-read-shape.js +481 -0
- package/dist/modules/workflow/agent-tools/change-request-read-shape.js.map +1 -0
- package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts +126 -0
- package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts.map +1 -0
- package/dist/modules/workflow/agent-tools/change-request-read.tools.js +611 -0
- package/dist/modules/workflow/agent-tools/change-request-read.tools.js.map +1 -0
- package/dist/modules/workflow/agent-tools/change-request-summary.d.ts +12 -1
- package/dist/modules/workflow/agent-tools/change-request-summary.d.ts.map +1 -1
- package/dist/modules/workflow/agent-tools/change-request-summary.js +5 -1
- package/dist/modules/workflow/agent-tools/change-request-summary.js.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.d.ts +8 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.js +133 -37
- package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
- package/dist/modules/workflow/file-lock.service.d.ts +29 -5
- package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
- package/dist/modules/workflow/file-lock.service.js +62 -5
- package/dist/modules/workflow/file-lock.service.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +282 -14
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +627 -126
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/git/merge-commit.d.ts +73 -0
- package/dist/modules/workflow/git/merge-commit.d.ts.map +1 -0
- package/dist/modules/workflow/git/merge-commit.js +89 -0
- package/dist/modules/workflow/git/merge-commit.js.map +1 -0
- package/dist/modules/workflow/git/node-git-runner.d.ts +15 -0
- package/dist/modules/workflow/git/node-git-runner.d.ts.map +1 -1
- package/dist/modules/workflow/git/node-git-runner.js +78 -6
- package/dist/modules/workflow/git/node-git-runner.js.map +1 -1
- package/dist/modules/workflow/git/pull-request.service.d.ts +94 -1
- package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/pull-request.service.js +332 -37
- package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
- package/dist/modules/workflow/pending-commits.service.d.ts +15 -0
- package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
- package/dist/modules/workflow/pending-commits.service.js +25 -1
- package/dist/modules/workflow/pending-commits.service.js.map +1 -1
- package/dist/modules/workflow/pending-commits.worker.d.ts +6 -1
- package/dist/modules/workflow/pending-commits.worker.d.ts.map +1 -1
- package/dist/modules/workflow/pending-commits.worker.js +23 -6
- package/dist/modules/workflow/pending-commits.worker.js.map +1 -1
- package/dist/modules/workflow/recovery-bot.js +1 -1
- package/dist/modules/workflow/recovery-bot.js.map +1 -1
- package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts +35 -0
- package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/review-workflow/review-workflow.service.js +180 -13
- package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
- package/dist/modules/workflow/sanitize-error.d.ts +16 -0
- package/dist/modules/workflow/sanitize-error.d.ts.map +1 -1
- package/dist/modules/workflow/sanitize-error.js +45 -0
- package/dist/modules/workflow/sanitize-error.js.map +1 -1
- package/dist/modules/workflow/workflow.routes.d.ts +6 -2
- package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.routes.js +7 -2
- package/dist/modules/workflow/workflow.routes.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +187 -15
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +808 -106
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/dist/modules/workspace/agent-download.builder.d.ts +105 -0
- package/dist/modules/workspace/agent-download.builder.d.ts.map +1 -0
- package/dist/modules/workspace/agent-download.builder.js +238 -0
- package/dist/modules/workspace/agent-download.builder.js.map +1 -0
- package/dist/modules/workspace/agent-download.routes.d.ts +77 -0
- package/dist/modules/workspace/agent-download.routes.d.ts.map +1 -0
- package/dist/modules/workspace/agent-download.routes.js +156 -0
- package/dist/modules/workspace/agent-download.routes.js.map +1 -0
- package/dist/modules/workspace/agent-download.store.d.ts +225 -0
- package/dist/modules/workspace/agent-download.store.d.ts.map +1 -0
- package/dist/modules/workspace/agent-download.store.js +423 -0
- package/dist/modules/workspace/agent-download.store.js.map +1 -0
- package/dist/modules/workspace/agent-upload.store.d.ts +10 -1
- package/dist/modules/workspace/agent-upload.store.d.ts.map +1 -1
- package/dist/modules/workspace/agent-upload.store.js +16 -6
- package/dist/modules/workspace/agent-upload.store.js.map +1 -1
- package/dist/modules/workspace/file-readers/raw-mime.d.ts +6 -0
- package/dist/modules/workspace/file-readers/raw-mime.d.ts.map +1 -0
- package/dist/modules/workspace/file-readers/raw-mime.js +38 -0
- package/dist/modules/workspace/file-readers/raw-mime.js.map +1 -0
- package/dist/modules/workspace/file-readers/text-reader.d.ts +5 -5
- package/dist/modules/workspace/file-readers/text-reader.d.ts.map +1 -1
- package/dist/modules/workspace/file-readers/text-reader.js +5 -8
- package/dist/modules/workspace/file-readers/text-reader.js.map +1 -1
- package/dist/modules/workspace/first-run.d.ts +111 -0
- package/dist/modules/workspace/first-run.d.ts.map +1 -0
- package/dist/modules/workspace/first-run.js +181 -0
- package/dist/modules/workspace/first-run.js.map +1 -0
- package/dist/modules/workspace/move-links.d.ts +92 -0
- package/dist/modules/workspace/move-links.d.ts.map +1 -0
- package/dist/modules/workspace/move-links.js +173 -0
- package/dist/modules/workspace/move-links.js.map +1 -0
- package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/seed-tree.js +24 -39
- package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
- package/dist/modules/workspace/startup/steps/template-files.step.d.ts +58 -52
- package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/template-files.step.js +209 -223
- package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -1
- package/dist/modules/workspace/startup/steps/template-source.d.ts +5 -3
- package/dist/modules/workspace/startup/steps/template-source.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/template-source.js +5 -3
- package/dist/modules/workspace/startup/steps/template-source.js.map +1 -1
- package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.routes.js +63 -22
- package/dist/modules/workspace/workspace.routes.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +75 -2
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +197 -24
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/modules/workspace/workspace.tools.d.ts +68 -1
- package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.tools.js +642 -71
- package/dist/modules/workspace/workspace.tools.js.map +1 -1
- package/dist/modules/write-access/write-access.d.ts.map +1 -1
- package/dist/modules/write-access/write-access.js +9 -0
- package/dist/modules/write-access/write-access.js.map +1 -1
- package/dist/shared/domain-errors.d.ts +84 -1
- package/dist/shared/domain-errors.d.ts.map +1 -1
- package/dist/shared/domain-errors.js +100 -12
- package/dist/shared/domain-errors.js.map +1 -1
- package/dist/shared/git.contract.d.ts +10 -1
- package/dist/shared/git.contract.d.ts.map +1 -1
- package/dist/shared/git.contract.js.map +1 -1
- package/dist/shared/hidden-tools.d.ts +44 -0
- package/dist/shared/hidden-tools.d.ts.map +1 -0
- package/dist/shared/hidden-tools.js +13 -0
- package/dist/shared/hidden-tools.js.map +1 -0
- package/dist/shared/utf8-text.d.ts +15 -0
- package/dist/shared/utf8-text.d.ts.map +1 -0
- package/dist/shared/utf8-text.js +20 -0
- package/dist/shared/utf8-text.js.map +1 -0
- package/dist/tenancy/static-tenant-source.d.ts +2 -1
- package/dist/tenancy/static-tenant-source.d.ts.map +1 -1
- package/dist/tenancy/static-tenant-source.js +9 -2
- package/dist/tenancy/static-tenant-source.js.map +1 -1
- package/kb-template/.bevelignore +0 -5
- package/mcp-app/page.html +349 -0
- package/migrations/0017_atlassian_account_links.sql +37 -0
- package/migrations/meta/0017_snapshot.json +2396 -0
- package/migrations/meta/_journal.json +8 -1
- package/package.json +6 -3
- package/src/__tests__/kb-layout-config.test.ts +10 -100
- package/src/__tests__/packaged-assets-ship.test.ts +66 -0
- package/src/assets.ts +38 -2
- package/src/core/__tests__/catalog-revision.test.ts +40 -0
- package/src/core/__tests__/gated-boot.test.ts +1 -0
- package/src/core/__tests__/lifecycle.test.ts +14 -0
- package/src/core/catalog-revision.ts +15 -2
- package/src/core/core-ports.ts +11 -0
- package/src/core/create-core-server.ts +78 -3
- package/src/core/create-core-services.ts +168 -10
- package/src/core/lifecycle.ts +28 -2
- package/src/core-config.ts +24 -1
- package/src/index.ts +2 -2
- package/src/modules/access/__tests__/access-control.atref-batch.test.ts +58 -0
- package/src/modules/access/__tests__/access-control.platform-restore.test.ts +8 -7
- package/src/modules/access/__tests__/access-control.service.test.ts +19 -0
- package/src/modules/access/__tests__/access-personal-plugin.test.ts +1 -18
- package/src/modules/access/access-control.interface.ts +28 -0
- package/src/modules/access/access-control.service.ts +37 -0
- package/src/modules/access/directory-sync-bot.ts +1 -1
- package/src/modules/agent-guide/__tests__/agent-guide.test.ts +386 -0
- package/src/modules/agent-guide/__tests__/agent-guide.tools.test.ts +207 -0
- package/src/modules/agent-guide/agent-guide.tools.ts +122 -0
- package/src/modules/agent-guide/agent-guide.ts +300 -0
- package/src/modules/agent-guide/index.ts +22 -0
- package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +39 -118
- package/src/modules/agent-instructions/agent-instructions.routes.ts +3 -2
- package/src/modules/agent-instructions/compose.ts +26 -8
- package/src/modules/agent-instructions/index.ts +0 -3
- package/src/modules/agent-instructions/shared-file-rules.ts +50 -112
- package/src/modules/audit/__tests__/agent-audit.service.test.ts +64 -4
- package/src/modules/audit/__tests__/agent-connection.routes.test.ts +102 -0
- package/src/modules/audit/agent-audit.service.ts +58 -3
- package/src/modules/audit/agent-connection.routes.ts +56 -0
- package/src/modules/audit/audit.contract.ts +26 -0
- package/src/modules/audit/index.ts +1 -0
- package/src/modules/auth/__tests__/account.routes.test.ts +5 -3
- package/src/modules/auth/account.routes.ts +10 -3
- package/src/modules/auth/auth.middleware.ts +1 -1
- package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +18 -2
- package/src/modules/code-mode/code-mode.tool.ts +13 -2
- package/src/modules/database/core-schema.ts +33 -3
- package/src/modules/embed/__tests__/account-links.migration.test.ts +98 -0
- package/src/modules/embed/__tests__/embed-link.test.ts +122 -0
- package/src/modules/embed/__tests__/embed.routes.test.ts +362 -0
- package/src/modules/embed/__tests__/embed.service.lock.test.ts +193 -0
- package/src/modules/embed/__tests__/embed.service.test.ts +805 -0
- package/src/modules/embed/__tests__/embed.tools.test.ts +304 -0
- package/src/modules/embed/__tests__/mcp-app.test.ts +207 -0
- package/src/modules/embed/account-link.service.ts +76 -0
- package/src/modules/embed/embed-link.ts +180 -0
- package/src/modules/embed/embed.errors.ts +43 -0
- package/src/modules/embed/embed.interface.ts +203 -0
- package/src/modules/embed/embed.routes.ts +363 -0
- package/src/modules/embed/embed.service.ts +705 -0
- package/src/modules/embed/embed.tools.ts +206 -0
- package/src/modules/embed/mcp-app.routes.ts +41 -0
- package/src/modules/embed/mcp-app.ts +173 -0
- package/src/modules/github-app/__tests__/github-app.test.ts +76 -0
- package/src/modules/github-app/github-app.connection.ts +38 -4
- package/src/modules/kb-fs/__tests__/move-with-edits.test.ts +304 -0
- package/src/modules/kb-fs/locking-filesystem.ts +287 -0
- package/src/modules/kb-sync/__tests__/kb-sync.service.test.ts +35 -0
- package/src/modules/kb-sync/kb-sync.interface.ts +2 -0
- package/src/modules/kb-sync/kb-sync.service.ts +6 -0
- package/src/modules/mcp/__tests__/bevel-oauth-provider.test.ts +68 -11
- package/src/modules/mcp/__tests__/fake-downstream-mcp-server.ts +14 -3
- package/src/modules/mcp/__tests__/mcp-proxy-helpers.test.ts +37 -2
- package/src/modules/mcp/__tests__/mcp.app.e2e.test.ts +200 -0
- package/src/modules/mcp/__tests__/mcp.e2e.test.ts +252 -0
- package/src/modules/mcp/__tests__/mcp.service.test.ts +42 -24
- package/src/modules/mcp/__tests__/tool-call-interface.e2e.test.ts +499 -0
- package/src/modules/mcp/__tests__/tool-schema-guard.test.ts +266 -0
- package/src/modules/mcp/mcp-auth.middleware.ts +2 -1
- package/src/modules/mcp/mcp.service.ts +218 -31
- package/src/modules/mcp/oauth/bevel-oauth-provider.ts +42 -4
- package/src/modules/mcp/tool-schema-guard.ts +196 -0
- package/src/modules/onboarding/__tests__/onboarding.routes.test.ts +163 -0
- package/src/modules/onboarding/__tests__/starter-pack.service.test.ts +635 -0
- package/src/modules/onboarding/__tests__/starter-packs.test.ts +169 -0
- package/src/modules/onboarding/onboarding.contract.ts +85 -0
- package/src/modules/onboarding/onboarding.routes.ts +85 -0
- package/src/modules/onboarding/starter-pack.service.ts +580 -0
- package/src/modules/onboarding/starter-packs.ts +182 -0
- package/src/modules/plugins/__tests__/plugins.tools.test.ts +177 -4
- package/src/modules/plugins/index.ts +1 -0
- package/src/modules/plugins/plugin-provision.service.ts +22 -1
- package/src/modules/plugins/plugins.routes.ts +29 -0
- package/src/modules/plugins/plugins.tools.ts +75 -15
- package/src/modules/settings/__tests__/deployment-settings.service.test.ts +176 -56
- package/src/modules/settings/__tests__/repository-source.test.ts +9 -0
- package/src/modules/settings/deployment-settings.service.ts +144 -55
- package/src/modules/settings/repository-source.ts +4 -4
- package/src/modules/settings/setup.routes.ts +3 -6
- package/src/modules/skills/__tests__/skills.tools.description.test.ts +95 -0
- package/src/modules/skills/skills.tools.ts +112 -46
- package/src/modules/tool-auth/__tests__/connection-key-rejection.test.ts +32 -0
- package/src/modules/tool-auth/__tests__/external-api-key.service.test.ts +65 -0
- package/src/modules/tool-auth/connection-key-rejection.ts +13 -3
- package/src/modules/tool-auth/external-api-key.interface.ts +9 -0
- package/src/modules/tool-auth/external-api-key.service.ts +61 -4
- package/src/modules/tool-auth/tool-auth.middleware.ts +13 -4
- package/src/modules/tool-helpers/__tests__/branch-declaration.test.ts +502 -0
- package/src/modules/tool-helpers/__tests__/route-argument-check.test.ts +337 -0
- package/src/modules/tool-helpers/index.ts +34 -1
- package/src/modules/tool-helpers/route-argument-check.ts +149 -0
- package/src/modules/tool-helpers/route-tool-schemas.ts +171 -0
- package/src/modules/tool-helpers/tool-def.ts +132 -7
- package/src/modules/tool-helpers/tool-handler.ts +196 -8
- package/src/modules/tool-helpers/tool.contract.ts +9 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +57 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +73 -4
- package/src/modules/tool-manuals/tool-manuals.contract.ts +24 -4
- package/src/modules/tool-manuals/tool-manuals.service.ts +17 -0
- package/src/modules/tool-manuals/tool-manuals.tools.ts +86 -38
- package/src/modules/tool-registry/__tests__/own-tool-schemas.test.ts +173 -0
- package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +81 -66
- package/src/modules/tool-registry/description-length.ts +57 -32
- package/src/modules/tool-registry/guide-first.ts +41 -0
- package/src/modules/tool-registry/tool-registry.ts +9 -2
- package/src/modules/workflow/__tests__/apply-failure.test.ts +6 -1
- package/src/modules/workflow/__tests__/file-lock.service.deletion-gate.test.ts +104 -0
- package/src/modules/workflow/__tests__/pending-commits.repository-replaced.test.ts +66 -0
- package/src/modules/workflow/__tests__/pending-commits.worker.test.ts +31 -2
- package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +110 -6
- package/src/modules/workflow/__tests__/sanitize-error.test.ts +89 -1
- package/src/modules/workflow/__tests__/workflow.routes.push-refused.test.ts +202 -0
- package/src/modules/workflow/__tests__/workflow.service.branch-in-use.test.ts +6 -6
- package/src/modules/workflow/__tests__/workflow.service.delete-branch.test.ts +1026 -0
- package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +468 -10
- package/src/modules/workflow/__tests__/workflow.service.folder-proposals.test.ts +4 -0
- package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +5 -4
- package/src/modules/workflow/__tests__/workflow.service.update-from-target.test.ts +54 -2
- package/src/modules/workflow/agent-tools/__tests__/change-request-read-shape.test.ts +705 -0
- package/src/modules/workflow/agent-tools/__tests__/change-request-read.tools.test.ts +1554 -0
- package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +150 -4
- package/src/modules/workflow/agent-tools/change-request-read-shape.ts +712 -0
- package/src/modules/workflow/agent-tools/change-request-read.tools.ts +759 -0
- package/src/modules/workflow/agent-tools/change-request-summary.ts +5 -1
- package/src/modules/workflow/agent-tools/workflow.tools.ts +137 -35
- package/src/modules/workflow/file-lock.service.ts +67 -5
- package/src/modules/workflow/git/__tests__/git.service.appliedChange.test.ts +285 -0
- package/src/modules/workflow/git/__tests__/git.service.changedFilesForPr.test.ts +124 -0
- package/src/modules/workflow/git/__tests__/git.service.deleteBranch.test.ts +164 -0
- package/src/modules/workflow/git/__tests__/git.service.mergeChangeRequest.test.ts +334 -0
- package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +48 -1
- package/src/modules/workflow/git/__tests__/node-git-runner.test.ts +231 -2
- package/src/modules/workflow/git/__tests__/pull-request.service.list-fetch.test.ts +72 -2
- package/src/modules/workflow/git/__tests__/pull-request.service.placeholder.test.ts +24 -2
- package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +620 -1
- package/src/modules/workflow/git/git.service.ts +714 -133
- package/src/modules/workflow/git/merge-commit.ts +88 -0
- package/src/modules/workflow/git/node-git-runner.ts +82 -7
- package/src/modules/workflow/git/pull-request.service.ts +380 -54
- package/src/modules/workflow/pending-commits.service.ts +29 -1
- package/src/modules/workflow/pending-commits.worker.ts +24 -6
- package/src/modules/workflow/recovery-bot.ts +1 -1
- package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +7 -1
- package/src/modules/workflow/review-workflow/__tests__/merge-records-own-commit.test.ts +407 -0
- package/src/modules/workflow/review-workflow/review-workflow.service.ts +191 -12
- package/src/modules/workflow/sanitize-error.ts +50 -0
- package/src/modules/workflow/workflow.routes.ts +7 -2
- package/src/modules/workflow/workflow.service.ts +886 -112
- package/src/modules/workspace/__tests__/agent-downloads.test.ts +823 -0
- package/src/modules/workspace/__tests__/escape-sequences.routes.test.ts +4 -3
- package/src/modules/workspace/__tests__/first-run.test.ts +234 -0
- package/src/modules/workspace/__tests__/git-internals.security.test.ts +5 -2
- package/src/modules/workspace/__tests__/md-links.test.ts +353 -0
- package/src/modules/workspace/__tests__/move-links.test.ts +261 -0
- package/src/modules/workspace/__tests__/rest-tool-arguments.test.ts +219 -0
- package/src/modules/workspace/__tests__/workspace-paths-inside-the-repo.test.ts +3 -0
- package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +102 -0
- package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +52 -5
- package/src/modules/workspace/__tests__/workspace.routes.folder-zip-access.test.ts +190 -0
- package/src/modules/workspace/__tests__/workspace.routes.move-platform-files.test.ts +21 -10
- package/src/modules/workspace/__tests__/workspace.service.forget-clone-races.test.ts +8 -4
- package/src/modules/workspace/__tests__/workspace.service.test.ts +50 -8
- package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +33 -55
- package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +44 -1
- package/src/modules/workspace/__tests__/workspace.tools.test.ts +664 -34
- package/src/modules/workspace/agent-download.builder.ts +308 -0
- package/src/modules/workspace/agent-download.routes.ts +183 -0
- package/src/modules/workspace/agent-download.store.ts +533 -0
- package/src/modules/workspace/agent-upload.store.ts +23 -7
- package/src/modules/workspace/file-readers/raw-mime.ts +40 -0
- package/src/modules/workspace/file-readers/text-reader.ts +5 -8
- package/src/modules/workspace/first-run.ts +222 -0
- package/src/modules/workspace/move-links.ts +237 -0
- package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +191 -489
- package/src/modules/workspace/startup/steps/seed-tree.ts +23 -38
- package/src/modules/workspace/startup/steps/template-files.step.ts +217 -249
- package/src/modules/workspace/startup/steps/template-source.ts +5 -3
- package/src/modules/workspace/workspace.routes.ts +64 -22
- package/src/modules/workspace/workspace.service.ts +202 -25
- package/src/modules/workspace/workspace.tools.ts +707 -68
- package/src/modules/write-access/__tests__/write-access.test.ts +13 -0
- package/src/modules/write-access/write-access.ts +9 -0
- package/src/shared/domain-errors.ts +123 -11
- package/src/shared/git.contract.ts +8 -1
- package/src/shared/hidden-tools.ts +45 -0
- package/src/shared/utf8-text.ts +21 -0
- package/src/tenancy/__tests__/static-tenant-source.test.ts +1 -0
- package/src/tenancy/static-tenant-source.ts +10 -2
- package/starter-packs/README.md +103 -0
- package/starter-packs/engineering/KnowledgeBase/About us.md +9 -0
- package/starter-packs/engineering/KnowledgeBase/Architecture.md +9 -0
- package/starter-packs/engineering/KnowledgeBase/Glossary.md +9 -0
- package/starter-packs/engineering/KnowledgeBase/How we ship.md +9 -0
- package/starter-packs/engineering/Plugins/engineering-starter/LICENSE +21 -0
- package/starter-packs/engineering/Plugins/engineering-starter/SOURCE.md +20 -0
- package/starter-packs/engineering/Plugins/engineering-starter/access.md +19 -0
- package/starter-packs/engineering/Plugins/engineering-starter/plugin.json +10 -0
- package/starter-packs/engineering/Plugins/engineering-starter/skills/office-hours/SKILL.md +171 -0
- package/starter-packs/engineering/Plugins/engineering-starter/skills/office-hours/sections/phase-2a-startup-diagnostic.md +157 -0
- package/starter-packs/engineering/Plugins/engineering-starter/skills/office-hours/sections/phase-2b-builder-brainstorm.md +39 -0
- package/starter-packs/engineering/Plugins/engineering-starter/skills/plan-ceo-review/SKILL.md +144 -0
- package/starter-packs/engineering/Plugins/engineering-starter/skills/plan-eng-review/SKILL.md +144 -0
- package/starter-packs/engineering/Plugins/engineering-starter/skills/review/SKILL.md +80 -0
- package/starter-packs/engineering/Plugins/engineering-starter/skills/review/checklist.md +184 -0
- package/starter-packs/engineering/pack.yaml +11 -0
- package/starter-packs/general/KnowledgeBase/About us.md +9 -0
- package/starter-packs/general/KnowledgeBase/Glossary.md +9 -0
- package/starter-packs/general/KnowledgeBase/How we work.md +9 -0
- package/starter-packs/general/Plugins/general-starter/LICENSE +202 -0
- package/starter-packs/general/Plugins/general-starter/SOURCE.md +10 -0
- package/starter-packs/general/Plugins/general-starter/access.md +19 -0
- package/starter-packs/general/Plugins/general-starter/plugin.json +10 -0
- package/starter-packs/general/Plugins/general-starter/skills/memory-management/SKILL.md +332 -0
- package/starter-packs/general/Plugins/general-starter/skills/productivity-start/SKILL.md +163 -0
- package/starter-packs/general/Plugins/general-starter/skills/productivity-update/SKILL.md +167 -0
- package/starter-packs/general/Plugins/general-starter/skills/task-management/SKILL.md +83 -0
- package/starter-packs/general/pack.yaml +10 -0
- package/starter-packs/operations/KnowledgeBase/Glossary.md +9 -0
- package/starter-packs/operations/KnowledgeBase/How we work.md +9 -0
- package/starter-packs/operations/KnowledgeBase/Processes.md +9 -0
- package/starter-packs/operations/KnowledgeBase/Tools we use.md +9 -0
- package/starter-packs/operations/KnowledgeBase/Vendors.md +9 -0
- package/starter-packs/operations/Plugins/operations-starter/LICENSE +202 -0
- package/starter-packs/operations/Plugins/operations-starter/SOURCE.md +10 -0
- package/starter-packs/operations/Plugins/operations-starter/access.md +19 -0
- package/starter-packs/operations/Plugins/operations-starter/plugin.json +10 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/capacity-plan/SKILL.md +111 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/change-request/SKILL.md +119 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/compliance-tracking/SKILL.md +44 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/process-doc/SKILL.md +80 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/process-optimization/SKILL.md +39 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/risk-assessment/SKILL.md +40 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/runbook/SKILL.md +85 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/status-report/SKILL.md +75 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/vendor-review/SKILL.md +102 -0
- package/starter-packs/operations/pack.yaml +12 -0
- package/starter-packs/sales/KnowledgeBase/About us.md +9 -0
- package/starter-packs/sales/KnowledgeBase/Customers.md +9 -0
- package/starter-packs/sales/KnowledgeBase/Glossary.md +9 -0
- package/starter-packs/sales/KnowledgeBase/How we sell.md +9 -0
- package/starter-packs/sales/KnowledgeBase/Objections.md +9 -0
- package/starter-packs/sales/KnowledgeBase/Pricing.md +9 -0
- package/starter-packs/sales/Plugins/sales-starter/LICENSE +202 -0
- package/starter-packs/sales/Plugins/sales-starter/SOURCE.md +10 -0
- package/starter-packs/sales/Plugins/sales-starter/access.md +19 -0
- package/starter-packs/sales/Plugins/sales-starter/plugin.json +10 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/account-context/SKILL.md +94 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/account-plan/SKILL.md +95 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/account-research/SKILL.md +99 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/account-tiering/SKILL.md +101 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/call-prep/SKILL.md +111 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/call-summary/SKILL.md +111 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/close-plan/SKILL.md +106 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/competitive-intelligence/SKILL.md +102 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/create-an-asset/SKILL.md +105 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/crm-hygiene-check/SKILL.md +103 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/customer-health/SKILL.md +108 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/customer-voice/SKILL.md +94 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/daily-briefing/SKILL.md +61 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/deal-advance-gap/SKILL.md +102 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/deal-review/SKILL.md +105 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/deal-signals/SKILL.md +96 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/deal-slip-scenario/SKILL.md +104 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/draft-outreach/SKILL.md +116 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/end-of-day/SKILL.md +114 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/expansion-whitespace/SKILL.md +97 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/forecast/SKILL.md +100 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/handle-objection/SKILL.md +93 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/inbox-sweep/SKILL.md +114 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/lead-triage/SKILL.md +106 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/log-activity/SKILL.md +111 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/pipeline-review/SKILL.md +97 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/renewal-radar/SKILL.md +95 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/rep-context/SKILL.md +90 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/route-lead/SKILL.md +111 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/sales-setup/SKILL.md +200 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/schedule-meeting/SKILL.md +99 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/stakeholder-map/SKILL.md +107 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/team-pipeline/SKILL.md +105 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/update-opportunity/SKILL.md +112 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/weekly-wrap/SKILL.md +90 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/win-loss-review/SKILL.md +88 -0
- package/starter-packs/sales/pack.yaml +13 -0
- package/starter-packs/support/KnowledgeBase/Escalations.md +9 -0
- package/starter-packs/support/KnowledgeBase/Glossary.md +9 -0
- package/starter-packs/support/KnowledgeBase/Known issues.md +9 -0
- package/starter-packs/support/KnowledgeBase/Products.md +9 -0
- package/starter-packs/support/KnowledgeBase/Tone of voice.md +9 -0
- package/starter-packs/support/Plugins/support-starter/LICENSE +202 -0
- package/starter-packs/support/Plugins/support-starter/SOURCE.md +10 -0
- package/starter-packs/support/Plugins/support-starter/access.md +19 -0
- package/starter-packs/support/Plugins/support-starter/plugin.json +10 -0
- package/starter-packs/support/Plugins/support-starter/skills/customer-escalation/SKILL.md +246 -0
- package/starter-packs/support/Plugins/support-starter/skills/customer-research/SKILL.md +250 -0
- package/starter-packs/support/Plugins/support-starter/skills/draft-response/SKILL.md +416 -0
- package/starter-packs/support/Plugins/support-starter/skills/kb-article/SKILL.md +350 -0
- package/starter-packs/support/Plugins/support-starter/skills/ticket-triage/SKILL.md +273 -0
- package/starter-packs/support/pack.yaml +12 -0
- package/kb-template/AGENTS.md +0 -730
|
@@ -0,0 +1,1026 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach, type MockInstance } from 'vitest';
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
import fs from 'node:fs/promises';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { drizzle } from 'drizzle-orm/pg-proxy';
|
|
8
|
+
import type { AuthUser, DeleteBranchPreview, DeleteBranchResult } from '@bevel-software/platform-shared';
|
|
9
|
+
import { testKbContext } from '../../../__tests__/kb-context.js';
|
|
10
|
+
import { openChangeGate } from '../../../__tests__/open-change-gate.js';
|
|
11
|
+
import { GitService } from '../git/git.service.js';
|
|
12
|
+
import type { PullRequestService } from '../git/pull-request.service.js';
|
|
13
|
+
import type { IReviewWorkflowService } from '../review-workflow/review-workflow.interface.js';
|
|
14
|
+
import type { WorkspaceService } from '../../workspace/workspace.service.js';
|
|
15
|
+
import type { IAccessControl } from '../../access/access-control.interface.js';
|
|
16
|
+
import { FileLockService } from '../file-lock.service.js';
|
|
17
|
+
import type { PendingCommitsService } from '../pending-commits.service.js';
|
|
18
|
+
import type { Database } from '../../database/connection.js';
|
|
19
|
+
import { WorkflowHooks } from '../workflow-hooks.js';
|
|
20
|
+
import { WorkflowService } from '../workflow.service.js';
|
|
21
|
+
import type { WorkflowEventBus } from '../event-bus.js';
|
|
22
|
+
import { workspaceIdForBranch } from '../../../shared/workspace-id.js';
|
|
23
|
+
import {
|
|
24
|
+
BranchAuthorshipError,
|
|
25
|
+
BranchDeleteBlockedError,
|
|
26
|
+
BranchNotFoundError,
|
|
27
|
+
ProtectedBranchError,
|
|
28
|
+
} from '../../../shared/domain-errors.js';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* An agent's `delete_branch` (`deleteBranchChecked`) and the server's cleanup
|
|
32
|
+
* of branches merged change requests left behind
|
|
33
|
+
* (`retireLeftoverMergedBranches`).
|
|
34
|
+
*
|
|
35
|
+
* The change-request rows are answered by the REAL drizzle query, read back off
|
|
36
|
+
* the SQL the pg-proxy driver hands over (the harness of
|
|
37
|
+
* `workflow.service.branch-in-use.test.ts`): a stub answering by call order
|
|
38
|
+
* would pass just as happily against a guard that asked about the wrong end of
|
|
39
|
+
* a request, or the wrong state.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
interface CrRow {
|
|
43
|
+
number: number;
|
|
44
|
+
source_branch: string;
|
|
45
|
+
target_branch: string;
|
|
46
|
+
state: string;
|
|
47
|
+
merged_sha: string | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The guarded closes the fake table applied, as `{ number, params }`. */
|
|
51
|
+
const closes: { number: number; params: unknown[] }[] = [];
|
|
52
|
+
/** The requests a failed deletion reopened. */
|
|
53
|
+
const reopens: number[] = [];
|
|
54
|
+
|
|
55
|
+
function fakeDb(rows: CrRow[]): Database {
|
|
56
|
+
return drizzle(async (sql: string, params: unknown[]) => {
|
|
57
|
+
if (/^\s*update/i.test(sql) && /"number"\s+in\s*\(/i.test(sql)) {
|
|
58
|
+
// The reopen: `update … set state = 'open', closed_at = null … where number in (…) and state = 'closed'`.
|
|
59
|
+
const hit = rows.filter((r) => r.state === 'closed' && params.includes(r.number) && params.includes('closed'));
|
|
60
|
+
for (const row of hit) {
|
|
61
|
+
row.state = 'open';
|
|
62
|
+
reopens.push(row.number);
|
|
63
|
+
}
|
|
64
|
+
return { rows: hit.map((r) => [r.number]) };
|
|
65
|
+
}
|
|
66
|
+
if (/^\s*update/i.test(sql)) {
|
|
67
|
+
// The guarded close: `update … set state, closed_at … where number = $n and state = 'open'`.
|
|
68
|
+
const at = /"number"\s*=\s*\$(\d+)/.exec(sql);
|
|
69
|
+
const number = at ? params[Number(at[1]) - 1] : undefined;
|
|
70
|
+
const row = rows.find((r) => r.number === number && r.state === 'open');
|
|
71
|
+
if (!row || !/"closed_at"/.test(sql)) return { rows: [] };
|
|
72
|
+
row.state = 'closed';
|
|
73
|
+
closes.push({ number: row.number, params });
|
|
74
|
+
return { rows: [['row-id']] };
|
|
75
|
+
}
|
|
76
|
+
if (!/^\s*select/i.test(sql)) return { rows: [['row-id']] };
|
|
77
|
+
const projection = (/^\s*select\s+(.+?)\s+from\s/is.exec(sql)?.[1] ?? '')
|
|
78
|
+
.split(',')
|
|
79
|
+
.map((item) => item.trim().replace(/.*"(\w+)"$/, '$1'));
|
|
80
|
+
const byValue = new Map<unknown, string[]>();
|
|
81
|
+
for (const m of sql.matchAll(/"(\w+)"\s*=\s*\$(\d+)/g)) {
|
|
82
|
+
const value = params[Number(m[2]) - 1];
|
|
83
|
+
byValue.set(value, [...(byValue.get(value) ?? []), m[1]!]);
|
|
84
|
+
}
|
|
85
|
+
if (!projection.length || projection.some((c) => !/^\w+$/.test(c))) {
|
|
86
|
+
throw new Error(`fakeDb could not read the projection of: ${sql}`);
|
|
87
|
+
}
|
|
88
|
+
if (/\bwhere\b/i.test(sql) && byValue.size === 0) {
|
|
89
|
+
throw new Error(`fakeDb could not read the predicate of: ${sql}`);
|
|
90
|
+
}
|
|
91
|
+
const matched = rows.filter((row) =>
|
|
92
|
+
[...byValue].every(([value, columns]) => columns.some((c) => row[c as keyof CrRow] === value)),
|
|
93
|
+
);
|
|
94
|
+
return { rows: matched.map((row) => projection.map((c) => row[c as keyof CrRow])) };
|
|
95
|
+
}) as unknown as Database;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const ADMIN: AuthUser = { id: 'u-ana', email: 'ana@example.com', name: 'Ana' };
|
|
99
|
+
const BOB: AuthUser = { id: 'u-bob', email: 'bob@example.com', name: 'Bob' };
|
|
100
|
+
const DEFAULT = 'target-company-state';
|
|
101
|
+
|
|
102
|
+
type State = { exists: boolean; lastCommit: string | null; unmergedCommits: number };
|
|
103
|
+
|
|
104
|
+
interface Harness {
|
|
105
|
+
svc: WorkflowService;
|
|
106
|
+
git: {
|
|
107
|
+
branchState: ReturnType<typeof vi.fn>;
|
|
108
|
+
mayDeleteBranch: ReturnType<typeof vi.fn>;
|
|
109
|
+
isAncestor: ReturnType<typeof vi.fn>;
|
|
110
|
+
changedPathsForPr: ReturnType<typeof vi.fn>;
|
|
111
|
+
deleteBranch: ReturnType<typeof vi.fn>;
|
|
112
|
+
};
|
|
113
|
+
fetch: ReturnType<typeof vi.fn>;
|
|
114
|
+
deleted: string[];
|
|
115
|
+
fileLocks: FileLockService;
|
|
116
|
+
/** What the service announced to connected tabs. */
|
|
117
|
+
emitted: { kind: string; number?: number }[];
|
|
118
|
+
hasAnyActive: MockInstance<FileLockService['hasAnyActive']>;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* A service over a mocked git layer. `branches` is the shared repository as
|
|
123
|
+
* the fetch left it; `ancestry` lists the `[tip, mergeCommit]` pairs that are
|
|
124
|
+
* contained; `admins` may delete anything; `savesLanding` names the branches
|
|
125
|
+
* whose checkout still has a save or a lock.
|
|
126
|
+
*/
|
|
127
|
+
function harness(opts: {
|
|
128
|
+
rows?: CrRow[];
|
|
129
|
+
branches?: Record<string, State>;
|
|
130
|
+
ancestry?: [string, string][];
|
|
131
|
+
admins?: string[];
|
|
132
|
+
savesLanding?: string[];
|
|
133
|
+
/** Branches whose checkout has a commit queued (no lock held). */
|
|
134
|
+
commitsQueued?: string[];
|
|
135
|
+
fetchFails?: boolean;
|
|
136
|
+
/** The changes each open request proposes, by its source branch; absent, one file. `error`: undeterminable. */
|
|
137
|
+
changes?: Record<string, string[] | 'error'>;
|
|
138
|
+
/** Branches whose deletion fails at the host (the leased push is refused). */
|
|
139
|
+
deleteFails?: string[];
|
|
140
|
+
}): Harness {
|
|
141
|
+
const deleted: string[] = [];
|
|
142
|
+
const emitted: { kind: string; number?: number }[] = [];
|
|
143
|
+
const rows = opts.rows ?? [];
|
|
144
|
+
const branches = { ...(opts.branches ?? {}) };
|
|
145
|
+
const git = {
|
|
146
|
+
branchState: vi.fn(async (_ws: string, name: string): Promise<State> =>
|
|
147
|
+
branches[name] ?? { exists: false, lastCommit: null, unmergedCommits: 0 },
|
|
148
|
+
),
|
|
149
|
+
mayDeleteBranch: vi.fn(async (_ws: string, name: string, user: AuthUser) =>
|
|
150
|
+
(opts.admins ?? [ADMIN.email]).includes(user.email) || name.startsWith(`${user.email.split('@')[0]}/`),
|
|
151
|
+
),
|
|
152
|
+
isAncestor: vi.fn(async (_ws: string, a: string, d: string) =>
|
|
153
|
+
(opts.ancestry ?? []).some(([x, y]) => x === a && y === d),
|
|
154
|
+
),
|
|
155
|
+
changedPathsForPr: vi.fn(async (_ws: string, _base: string, head: string) => {
|
|
156
|
+
const c = opts.changes?.[head] ?? ['KnowledgeBase/f.md'];
|
|
157
|
+
if (c === 'error') throw new Error('unknown branch');
|
|
158
|
+
return c;
|
|
159
|
+
}),
|
|
160
|
+
deleteBranch: vi.fn(async (_ws: string, name: string) => {
|
|
161
|
+
if (opts.deleteFails?.includes(name)) throw new Error(`stale info: "${name}" moved since it was checked`);
|
|
162
|
+
deleted.push(name);
|
|
163
|
+
const tip = branches[name]?.lastCommit ?? null;
|
|
164
|
+
delete branches[name];
|
|
165
|
+
return { lastCommit: tip };
|
|
166
|
+
}),
|
|
167
|
+
// What a merge touches besides the branch: the roles.yaml check and the target's pull.
|
|
168
|
+
fetch: vi.fn(async () => {}),
|
|
169
|
+
readFileAtRef: vi.fn(async () => 'roles: {}'),
|
|
170
|
+
pull: vi.fn(async () => ({ treeChanged: false })),
|
|
171
|
+
};
|
|
172
|
+
const fetch = vi.fn(async () => {
|
|
173
|
+
if (opts.fetchFails) throw new Error('git fetch origin failed — remote refs could not be refreshed');
|
|
174
|
+
});
|
|
175
|
+
const landing = new Set((opts.savesLanding ?? []).map(workspaceIdForBranch));
|
|
176
|
+
const queued = new Set((opts.commitsQueued ?? []).map(workspaceIdForBranch));
|
|
177
|
+
// The real service, for its gate on acquiring; what is held is mocked.
|
|
178
|
+
const fileLocks = new FileLockService({} as Database);
|
|
179
|
+
const hasAnyActive = vi.spyOn(fileLocks, 'hasAnyActive').mockImplementation(async (ws: string) => landing.has(ws));
|
|
180
|
+
const svc = new WorkflowService(
|
|
181
|
+
fakeDb(rows),
|
|
182
|
+
git as unknown as GitService,
|
|
183
|
+
{
|
|
184
|
+
invalidateDetailCache: vi.fn(),
|
|
185
|
+
getPr: vi.fn(async (n: number) => {
|
|
186
|
+
const row = rows.find((r) => r.number === n);
|
|
187
|
+
return row
|
|
188
|
+
? { number: n, branch: row.source_branch, base: row.target_branch, state: row.state, url: `https://hexis.test/change-requests/${n}` }
|
|
189
|
+
: null;
|
|
190
|
+
}),
|
|
191
|
+
} as unknown as PullRequestService,
|
|
192
|
+
{
|
|
193
|
+
// The merge itself: the request's row is marked merged, as the review workflow records it.
|
|
194
|
+
mergePr: vi.fn(async (n: number) => {
|
|
195
|
+
const row = rows.find((r) => r.number === n)!;
|
|
196
|
+
row.state = 'merged';
|
|
197
|
+
return { sha: `m${n}` };
|
|
198
|
+
}),
|
|
199
|
+
} as unknown as IReviewWorkflowService,
|
|
200
|
+
{
|
|
201
|
+
getOrCreateForBranch: vi.fn(async (b: string) => ({ id: workspaceIdForBranch(b) })),
|
|
202
|
+
ensureRemotesFetched: fetch,
|
|
203
|
+
hasBootstrappedWorkspace: vi.fn(async () => false),
|
|
204
|
+
} as unknown as WorkspaceService,
|
|
205
|
+
{} as IAccessControl,
|
|
206
|
+
fileLocks,
|
|
207
|
+
{ hasAnyForWorkspace: vi.fn(async (ws: string) => queued.has(ws)) } as unknown as PendingCommitsService,
|
|
208
|
+
testKbContext(),
|
|
209
|
+
openChangeGate(),
|
|
210
|
+
{ emit: (e: { kind: string; number?: number }) => void emitted.push(e) } as unknown as WorkflowEventBus,
|
|
211
|
+
);
|
|
212
|
+
return { svc, git, fetch, deleted, fileLocks, emitted, hasAnyActive };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const merged = (number: number, source: string, sha: string | null): CrRow => ({
|
|
216
|
+
number,
|
|
217
|
+
source_branch: source,
|
|
218
|
+
target_branch: DEFAULT,
|
|
219
|
+
state: 'merged',
|
|
220
|
+
merged_sha: sha,
|
|
221
|
+
});
|
|
222
|
+
const open = (number: number, source: string, target: string): CrRow => ({
|
|
223
|
+
number,
|
|
224
|
+
source_branch: source,
|
|
225
|
+
target_branch: target,
|
|
226
|
+
state: 'open',
|
|
227
|
+
merged_sha: null,
|
|
228
|
+
});
|
|
229
|
+
const clean = (sha: string): State => ({ exists: true, lastCommit: sha, unmergedCommits: 0 });
|
|
230
|
+
|
|
231
|
+
async function refusal(p: Promise<unknown>): Promise<Error> {
|
|
232
|
+
const err = await p.then(
|
|
233
|
+
() => null,
|
|
234
|
+
(e: unknown) => e,
|
|
235
|
+
);
|
|
236
|
+
if (!(err instanceof Error)) throw new Error('expected a refusal, got a success');
|
|
237
|
+
return err;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
describe('deleteBranchChecked — an agent deletes a branch', () => {
|
|
241
|
+
it("deletes a colleague's merged draft for an Admin, from the default branch's workspace, and answers its last commit", async () => {
|
|
242
|
+
const h = harness({ branches: { 'ali/old-draft': clean('a1') } });
|
|
243
|
+
const result = (await h.svc.deleteBranchChecked(ADMIN, 'ali/old-draft')) as DeleteBranchResult;
|
|
244
|
+
expect(result).toEqual({ kind: 'deleted', branch: 'ali/old-draft', lastCommit: 'a1', discardedCommits: 0 });
|
|
245
|
+
// Fetched strictly and freshly first, and acted from the default branch's
|
|
246
|
+
// workspace — never the caller's, never the branch's own.
|
|
247
|
+
expect(h.fetch).toHaveBeenCalledWith(workspaceIdForBranch(DEFAULT), { strict: true, force: true });
|
|
248
|
+
expect(h.git.deleteBranch).toHaveBeenCalledWith(workspaceIdForBranch(DEFAULT), 'ali/old-draft', ADMIN, {
|
|
249
|
+
expectTip: 'a1',
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('refuses anyone who is neither the author nor an Admin, with the existing message, and deletes nothing', async () => {
|
|
254
|
+
const h = harness({ branches: { 'ali/old-draft': clean('a1') } });
|
|
255
|
+
const err = await refusal(h.svc.deleteBranchChecked(BOB, 'ali/old-draft'));
|
|
256
|
+
expect(err).toBeInstanceOf(BranchAuthorshipError);
|
|
257
|
+
expect(err.message).toBe('Only the author of "ali/old-draft" can delete it.');
|
|
258
|
+
expect(h.deleted).toEqual([]);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("lets the author delete their own draft", async () => {
|
|
262
|
+
const h = harness({ branches: { 'bob/draft': clean('b1') } });
|
|
263
|
+
await expect(h.svc.deleteBranchChecked(BOB, 'bob/draft')).resolves.toMatchObject({ kind: 'deleted' });
|
|
264
|
+
expect(h.deleted).toEqual(['bob/draft']);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('refuses a protected branch', async () => {
|
|
268
|
+
const h = harness({ branches: { [DEFAULT]: clean('m1'), 'current-company-state': clean('c1') } });
|
|
269
|
+
await expect(h.svc.deleteBranchChecked(ADMIN, DEFAULT)).rejects.toBeInstanceOf(ProtectedBranchError);
|
|
270
|
+
await expect(h.svc.deleteBranchChecked(ADMIN, 'current-company-state')).rejects.toBeInstanceOf(
|
|
271
|
+
ProtectedBranchError,
|
|
272
|
+
);
|
|
273
|
+
expect(h.deleted).toEqual([]);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('refuses while a change request is open FROM the branch, naming it and sending the user to the app', async () => {
|
|
277
|
+
const h = harness({ rows: [open(12, 'ali/draft', DEFAULT)], branches: { 'ali/draft': clean('a1') } });
|
|
278
|
+
const err = await refusal(h.svc.deleteBranchChecked(ADMIN, 'ali/draft'));
|
|
279
|
+
expect(err).toBeInstanceOf(BranchDeleteBlockedError);
|
|
280
|
+
expect((err as BranchDeleteBlockedError).status).toBe(409);
|
|
281
|
+
expect((err as BranchDeleteBlockedError).payload).toMatchObject({ reason: 'open-change-request', number: 12 });
|
|
282
|
+
expect(err.message).toContain('#12');
|
|
283
|
+
// It links to the request and says who can act on it.
|
|
284
|
+
expect(err.message).toContain('https://hexis.test/change-requests/12');
|
|
285
|
+
expect(err.message).toContain('Its author can withdraw it, or an Admin can decline it, in the app');
|
|
286
|
+
expect((err as BranchDeleteBlockedError).payload).toMatchObject({ url: 'https://hexis.test/change-requests/12' });
|
|
287
|
+
expect(h.deleted).toEqual([]);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('refuses while a change request is open INTO the branch', async () => {
|
|
291
|
+
const h = harness({ rows: [open(13, 'ali/other', 'ali/draft')], branches: { 'ali/draft': clean('a1') } });
|
|
292
|
+
const err = await refusal(h.svc.deleteBranchChecked(ADMIN, 'ali/draft'));
|
|
293
|
+
expect(err.message).toContain('#13');
|
|
294
|
+
expect(err.message).toContain('open into "ali/draft"');
|
|
295
|
+
expect(h.deleted).toEqual([]);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('refuses a branch holding commits not on the default branch, saying how many', async () => {
|
|
299
|
+
const h = harness({ branches: { 'ali/draft': { exists: true, lastCommit: 'a3', unmergedCommits: 3 } } });
|
|
300
|
+
const err = await refusal(h.svc.deleteBranchChecked(ADMIN, 'ali/draft'));
|
|
301
|
+
expect(err).toBeInstanceOf(BranchDeleteBlockedError);
|
|
302
|
+
expect((err as BranchDeleteBlockedError).payload).toMatchObject({ reason: 'unmerged-commits', unmergedCommits: 3 });
|
|
303
|
+
expect(err.message).toContain('3 commits');
|
|
304
|
+
expect(err.message).toContain('discardUnmerged');
|
|
305
|
+
expect(h.deleted).toEqual([]);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('with discardUnmerged, deletes it anyway and reports the discarded commits', async () => {
|
|
309
|
+
const h = harness({ branches: { 'ali/draft': { exists: true, lastCommit: 'a3', unmergedCommits: 3 } } });
|
|
310
|
+
await expect(h.svc.deleteBranchChecked(ADMIN, 'ali/draft', { discardUnmerged: true })).resolves.toEqual({
|
|
311
|
+
kind: 'deleted',
|
|
312
|
+
branch: 'ali/draft',
|
|
313
|
+
lastCommit: 'a3',
|
|
314
|
+
discardedCommits: 3,
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('refuses a branch with saves still landing, with or without discardUnmerged', async () => {
|
|
319
|
+
const h = harness({
|
|
320
|
+
branches: { 'ali/draft': { exists: true, lastCommit: 'a3', unmergedCommits: 3 } },
|
|
321
|
+
savesLanding: ['ali/draft'],
|
|
322
|
+
});
|
|
323
|
+
for (const discardUnmerged of [false, true]) {
|
|
324
|
+
const err = await refusal(h.svc.deleteBranchChecked(ADMIN, 'ali/draft', { discardUnmerged }));
|
|
325
|
+
expect((err as BranchDeleteBlockedError).payload).toMatchObject({ reason: 'saves-landing' });
|
|
326
|
+
expect(err.message).toContain('Try again once they have landed');
|
|
327
|
+
}
|
|
328
|
+
expect(h.deleted).toEqual([]);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('refuses a branch with a commit still queued and no file held, with or without discardUnmerged', async () => {
|
|
332
|
+
const h = harness({
|
|
333
|
+
branches: { 'ali/draft': { exists: true, lastCommit: 'a3', unmergedCommits: 3 } },
|
|
334
|
+
commitsQueued: ['ali/draft'],
|
|
335
|
+
});
|
|
336
|
+
for (const discardUnmerged of [false, true]) {
|
|
337
|
+
const err = await refusal(h.svc.deleteBranchChecked(ADMIN, 'ali/draft', { discardUnmerged }));
|
|
338
|
+
expect((err as BranchDeleteBlockedError).payload).toMatchObject({ reason: 'saves-landing' });
|
|
339
|
+
}
|
|
340
|
+
expect(h.deleted).toEqual([]);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('refuses a save whose lock was taken after the first check, and grants no lock while it deletes', async () => {
|
|
344
|
+
const h = harness({ branches: { 'ali/draft': clean('a1') } });
|
|
345
|
+
// Nothing held at the first check; a save takes its lock before the delete.
|
|
346
|
+
h.hasAnyActive.mockResolvedValueOnce(false).mockResolvedValue(true);
|
|
347
|
+
const err = await refusal(h.svc.deleteBranchChecked(ADMIN, 'ali/draft'));
|
|
348
|
+
expect((err as BranchDeleteBlockedError).payload).toMatchObject({ reason: 'saves-landing' });
|
|
349
|
+
expect(h.deleted).toEqual([]);
|
|
350
|
+
|
|
351
|
+
// Once the checks pass, no save can start until the branch is gone.
|
|
352
|
+
h.hasAnyActive.mockResolvedValue(false);
|
|
353
|
+
let during: unknown = null;
|
|
354
|
+
h.git.deleteBranch.mockImplementationOnce(async () => {
|
|
355
|
+
during = await h.fileLocks.acquire(workspaceIdForBranch('ali/draft'), 'ali/draft', 'KnowledgeBase/f.md', BOB).catch((e: unknown) => e);
|
|
356
|
+
return { lastCommit: 'a1' };
|
|
357
|
+
});
|
|
358
|
+
await h.svc.deleteBranchChecked(ADMIN, 'ali/draft');
|
|
359
|
+
expect(during).toBeInstanceOf(Error);
|
|
360
|
+
expect((during as { payload?: unknown }).payload).toMatchObject({ kind: 'branch-being-deleted' });
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('answers "no branch named …" for a name that is not a branch, never a success', async () => {
|
|
364
|
+
const h = harness({});
|
|
365
|
+
const err = await refusal(h.svc.deleteBranchChecked(ADMIN, 'ali/no-such-branch'));
|
|
366
|
+
expect(err).toBeInstanceOf(BranchNotFoundError);
|
|
367
|
+
expect((err as BranchNotFoundError).status).toBe(404);
|
|
368
|
+
expect(err.message).toContain('no branch named ali/no-such-branch');
|
|
369
|
+
expect(h.git.deleteBranch).not.toHaveBeenCalled();
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('refuses, and deletes nothing, when the shared repository cannot be reached', async () => {
|
|
373
|
+
const h = harness({ branches: { 'ali/draft': clean('a1') }, fetchFails: true });
|
|
374
|
+
const err = await refusal(h.svc.deleteBranchChecked(ADMIN, 'ali/draft'));
|
|
375
|
+
expect(err).toBeInstanceOf(BranchDeleteBlockedError);
|
|
376
|
+
expect((err as BranchDeleteBlockedError).status).toBe(503);
|
|
377
|
+
expect(err.message).toContain('nothing was deleted');
|
|
378
|
+
expect(h.git.branchState).not.toHaveBeenCalled();
|
|
379
|
+
expect(h.deleted).toEqual([]);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('dryRun changes nothing and reports existence, permission and why not, unmerged commits, open requests and the last commit', async () => {
|
|
383
|
+
const h = harness({
|
|
384
|
+
rows: [open(12, 'ali/draft', DEFAULT), open(14, 'ali/other', 'ali/draft')],
|
|
385
|
+
branches: { 'ali/draft': { exists: true, lastCommit: 'a2', unmergedCommits: 2 } },
|
|
386
|
+
});
|
|
387
|
+
const preview = (await h.svc.deleteBranchChecked(BOB, 'ali/draft', { dryRun: true })) as DeleteBranchPreview;
|
|
388
|
+
expect(preview).toMatchObject({
|
|
389
|
+
kind: 'preview',
|
|
390
|
+
branch: 'ali/draft',
|
|
391
|
+
exists: true,
|
|
392
|
+
canDelete: false,
|
|
393
|
+
unmergedCommits: 2,
|
|
394
|
+
openChangeRequests: [
|
|
395
|
+
{ number: 12, end: 'source', url: 'https://hexis.test/change-requests/12', proposesNothing: false },
|
|
396
|
+
{ number: 14, end: 'target', url: 'https://hexis.test/change-requests/14', proposesNothing: false },
|
|
397
|
+
],
|
|
398
|
+
lastCommit: 'a2',
|
|
399
|
+
});
|
|
400
|
+
expect(preview.refusals).toHaveLength(3);
|
|
401
|
+
expect(preview.refusals[0]).toBe('Only the author of "ali/draft" can delete it.');
|
|
402
|
+
expect(preview.refusals.join(' ')).toContain('#12');
|
|
403
|
+
expect(preview.refusals.join(' ')).toContain('2 commits');
|
|
404
|
+
expect(h.git.deleteBranch).not.toHaveBeenCalled();
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it('dryRun of a deletable branch says it can be deleted; of a missing one, that it does not exist', async () => {
|
|
408
|
+
const h = harness({ branches: { 'ali/draft': clean('a1') } });
|
|
409
|
+
await expect(h.svc.deleteBranchChecked(ADMIN, 'ali/draft', { dryRun: true })).resolves.toEqual({
|
|
410
|
+
kind: 'preview',
|
|
411
|
+
branch: 'ali/draft',
|
|
412
|
+
exists: true,
|
|
413
|
+
canDelete: true,
|
|
414
|
+
refusals: [],
|
|
415
|
+
unmergedCommits: 0,
|
|
416
|
+
openChangeRequests: [],
|
|
417
|
+
lastCommit: 'a1',
|
|
418
|
+
});
|
|
419
|
+
const missing = (await h.svc.deleteBranchChecked(ADMIN, 'ali/gone', { dryRun: true })) as DeleteBranchPreview;
|
|
420
|
+
expect(missing).toMatchObject({ exists: false, canDelete: false, lastCommit: null });
|
|
421
|
+
expect(missing.refusals[0]).toContain('no branch named ali/gone');
|
|
422
|
+
expect(h.deleted).toEqual([]);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it('a preview grants nothing: a commit pushed after it is refused at deletion', async () => {
|
|
426
|
+
const branches: Record<string, State> = { 'ali/draft': clean('a1') };
|
|
427
|
+
const h = harness({ branches });
|
|
428
|
+
const preview = (await h.svc.deleteBranchChecked(ADMIN, 'ali/draft', { dryRun: true })) as DeleteBranchPreview;
|
|
429
|
+
expect(preview.canDelete).toBe(true);
|
|
430
|
+
// A colleague pushes before the delete.
|
|
431
|
+
branches['ali/draft'] = { exists: true, lastCommit: 'a2', unmergedCommits: 1 };
|
|
432
|
+
h.git.branchState.mockImplementation(async (_ws: string, name: string) => branches[name]);
|
|
433
|
+
const err = await refusal(h.svc.deleteBranchChecked(ADMIN, 'ali/draft'));
|
|
434
|
+
expect((err as BranchDeleteBlockedError).payload).toMatchObject({ reason: 'unmerged-commits', unmergedCommits: 1 });
|
|
435
|
+
expect(h.fetch).toHaveBeenCalledTimes(2);
|
|
436
|
+
expect(h.deleted).toEqual([]);
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
describe('a change request that proposes nothing never blocks a deletion', () => {
|
|
441
|
+
beforeEach(() => {
|
|
442
|
+
closes.length = 0;
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it('closes an empty request FROM the branch and one INTO it, then deletes — without waiting on its own lock', async () => {
|
|
446
|
+
const rows = [open(21, 'ali/sync', DEFAULT), open(22, 'ali/feature', 'ali/sync')];
|
|
447
|
+
const h = harness({ rows, branches: { 'ali/sync': clean('s1') }, changes: { 'ali/sync': [], 'ali/feature': [] } });
|
|
448
|
+
const lines: string[] = [];
|
|
449
|
+
const spy = vi.spyOn(console, 'log').mockImplementation((line: unknown) => void lines.push(String(line)));
|
|
450
|
+
try {
|
|
451
|
+
// The request's source IS the branch being deleted: a self-wait would hang here.
|
|
452
|
+
await expect(h.svc.deleteBranchChecked(ADMIN, 'ali/sync')).resolves.toMatchObject({ kind: 'deleted', lastCommit: 's1' });
|
|
453
|
+
} finally {
|
|
454
|
+
spy.mockRestore();
|
|
455
|
+
}
|
|
456
|
+
expect(rows.map((r) => r.state)).toEqual(['closed', 'closed']);
|
|
457
|
+
expect(h.deleted).toEqual(['ali/sync']);
|
|
458
|
+
// Recorded as today's empty-close: closed, with a closing time.
|
|
459
|
+
expect(closes.map((c) => c.number)).toEqual([21, 22]);
|
|
460
|
+
const isTime = (v: unknown) => v instanceof Date || (typeof v === 'string' && /^\d{4}-\d\d-\d\dT/.test(v));
|
|
461
|
+
for (const c of closes) expect(c.params.some(isTime), String(c.params)).toBe(true);
|
|
462
|
+
// One log line per close, naming the request, its branches and why.
|
|
463
|
+
const closeLines = lines.filter((l) => l.includes('closed change request #'));
|
|
464
|
+
expect(closeLines).toEqual([
|
|
465
|
+
'[cr] closed change request #21 ("ali/sync" into "target-company-state") by u-ana: it proposes nothing (empty when "ali/sync" was deleted)',
|
|
466
|
+
'[cr] closed change request #22 ("ali/feature" into "ali/sync") by u-ana: it proposes nothing (empty when "ali/sync" was deleted)',
|
|
467
|
+
]);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it('a request that still proposes something refuses, with the link, and closes nothing — not even the empty one beside it', async () => {
|
|
471
|
+
const rows = [open(21, 'ali/sync', DEFAULT), open(23, 'ali/other', 'ali/sync')];
|
|
472
|
+
const h = harness({ rows, branches: { 'ali/sync': clean('s1') }, changes: { 'ali/sync': [] } });
|
|
473
|
+
const err = await refusal(h.svc.deleteBranchChecked(ADMIN, 'ali/sync'));
|
|
474
|
+
expect((err as BranchDeleteBlockedError).payload).toMatchObject({ reason: 'open-change-request', number: 23 });
|
|
475
|
+
expect(err.message).toContain('https://hexis.test/change-requests/23');
|
|
476
|
+
expect(err.message).toContain('Its author can withdraw it, or an Admin can decline it');
|
|
477
|
+
expect(rows.map((r) => r.state)).toEqual(['open', 'open']);
|
|
478
|
+
expect(h.deleted).toEqual([]);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it('a request whose changes cannot be determined counts as proposing something', async () => {
|
|
482
|
+
const rows = [open(21, 'ali/sync', DEFAULT)];
|
|
483
|
+
const h = harness({ rows, branches: { 'ali/sync': clean('s1') }, changes: { 'ali/sync': 'error' } });
|
|
484
|
+
const err = await refusal(h.svc.deleteBranchChecked(ADMIN, 'ali/sync'));
|
|
485
|
+
expect((err as BranchDeleteBlockedError).payload).toMatchObject({ reason: 'open-change-request', number: 21 });
|
|
486
|
+
expect(rows[0]!.state).toBe('open');
|
|
487
|
+
expect(h.deleted).toEqual([]);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it('a request with a save still landing on its branch counts as proposing something', async () => {
|
|
491
|
+
// The request comes INTO the branch being deleted; the save lands on its source.
|
|
492
|
+
const rows = [open(22, 'ali/feature', 'ali/sync')];
|
|
493
|
+
const h = harness({
|
|
494
|
+
rows,
|
|
495
|
+
branches: { 'ali/sync': clean('s1') },
|
|
496
|
+
changes: { 'ali/feature': [] },
|
|
497
|
+
savesLanding: ['ali/feature'],
|
|
498
|
+
});
|
|
499
|
+
const err = await refusal(h.svc.deleteBranchChecked(ADMIN, 'ali/sync'));
|
|
500
|
+
expect((err as BranchDeleteBlockedError).payload).toMatchObject({ reason: 'open-change-request', number: 22 });
|
|
501
|
+
expect(rows[0]!.state).toBe('open');
|
|
502
|
+
expect(h.deleted).toEqual([]);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it('a deletion refused for another reason closes nothing', async () => {
|
|
506
|
+
const rows = [open(21, 'ali/sync', DEFAULT)];
|
|
507
|
+
const h = harness({ rows, branches: { 'ali/sync': clean('s1') }, changes: { 'ali/sync': [] } });
|
|
508
|
+
await expect(h.svc.deleteBranchChecked(BOB, 'ali/sync')).rejects.toBeInstanceOf(BranchAuthorshipError);
|
|
509
|
+
expect(rows[0]!.state).toBe('open');
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
it('the preview reports an empty request as one the delete would close, and closes nothing', async () => {
|
|
513
|
+
const rows = [open(21, 'ali/sync', DEFAULT)];
|
|
514
|
+
const h = harness({ rows, branches: { 'ali/sync': clean('s1') }, changes: { 'ali/sync': [] } });
|
|
515
|
+
await expect(h.svc.deleteBranchChecked(ADMIN, 'ali/sync', { dryRun: true })).resolves.toMatchObject({
|
|
516
|
+
canDelete: true,
|
|
517
|
+
refusals: [],
|
|
518
|
+
openChangeRequests: [{ number: 21, end: 'source', url: 'https://hexis.test/change-requests/21', proposesNothing: true }],
|
|
519
|
+
});
|
|
520
|
+
expect(rows[0]!.state).toBe('open');
|
|
521
|
+
expect(h.deleted).toEqual([]);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
it('closes empty requests only after the last saves check: a save landing by then closes nothing', async () => {
|
|
525
|
+
const rows = [open(21, 'ali/sync', DEFAULT)];
|
|
526
|
+
const h = harness({ rows, branches: { 'ali/sync': clean('s1') }, changes: { 'ali/sync': [] } });
|
|
527
|
+
// Clear at the first check; landing by the one made inside the lock gate.
|
|
528
|
+
h.hasAnyActive.mockResolvedValueOnce(false).mockResolvedValueOnce(false).mockResolvedValue(true);
|
|
529
|
+
const err = await refusal(h.svc.deleteBranchChecked(ADMIN, 'ali/sync'));
|
|
530
|
+
expect((err as BranchDeleteBlockedError).payload).toMatchObject({ reason: 'saves-landing' });
|
|
531
|
+
expect(rows[0]!.state).toBe('open');
|
|
532
|
+
expect(closes).toEqual([]);
|
|
533
|
+
expect(h.deleted).toEqual([]);
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it('reopens the empty requests it closed when the deletion itself then fails', async () => {
|
|
537
|
+
reopens.length = 0;
|
|
538
|
+
const rows = [open(21, 'ali/sync', DEFAULT), open(22, 'ali/feature', 'ali/sync')];
|
|
539
|
+
const h = harness({
|
|
540
|
+
rows,
|
|
541
|
+
branches: { 'ali/sync': clean('s1') },
|
|
542
|
+
changes: { 'ali/sync': [], 'ali/feature': [] },
|
|
543
|
+
deleteFails: ['ali/sync'],
|
|
544
|
+
});
|
|
545
|
+
await expect(h.svc.deleteBranchChecked(ADMIN, 'ali/sync')).rejects.toThrow(/stale info/);
|
|
546
|
+
expect(closes.map((c) => c.number)).toEqual([21, 22]);
|
|
547
|
+
expect(reopens.sort()).toEqual([21, 22]);
|
|
548
|
+
expect(rows.map((r) => r.state)).toEqual(['open', 'open']);
|
|
549
|
+
// Every tab was told each request went, and is told it is back.
|
|
550
|
+
const told = (kind: string) => h.emitted.filter((e) => e.kind === kind).map((e) => e.number).sort();
|
|
551
|
+
expect(told('change-request-rejected')).toEqual([21, 22]);
|
|
552
|
+
expect(h.emitted.filter((e) => e.kind === 'change-request-opened')).toEqual([
|
|
553
|
+
expect.objectContaining({ number: 21, source: 'ali/sync', target: DEFAULT }),
|
|
554
|
+
expect.objectContaining({ number: 22, source: 'ali/feature', target: 'ali/sync' }),
|
|
555
|
+
]);
|
|
556
|
+
// The app's path, the same.
|
|
557
|
+
reopens.length = 0;
|
|
558
|
+
await expect(h.svc.deleteBranch(workspaceIdForBranch(DEFAULT), 'ali/sync', ADMIN)).rejects.toThrow(/stale info/);
|
|
559
|
+
expect(reopens.sort()).toEqual([21, 22]);
|
|
560
|
+
expect(rows.map((r) => r.state)).toEqual(['open', 'open']);
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
it('a request the caller may not see is not in the preview at all, and is refused on without its number or link', async () => {
|
|
564
|
+
// #21 is empty and #25 live; the caller can see neither.
|
|
565
|
+
const rows = [open(21, 'bob/sync', DEFAULT), open(25, 'ali/secret', 'bob/sync')];
|
|
566
|
+
const h = harness({ rows, branches: { 'bob/sync': clean('s1') }, changes: { 'bob/sync': [] } });
|
|
567
|
+
const maySee = vi.fn(async () => false);
|
|
568
|
+
const preview = (await h.svc.deleteBranchChecked(BOB, 'bob/sync', { dryRun: true, maySee })) as DeleteBranchPreview;
|
|
569
|
+
// The same answer as for a branch with no request on it, live or empty:
|
|
570
|
+
// the preview tells nothing about a request the caller cannot see.
|
|
571
|
+
const none = harness({ branches: { 'bob/sync': clean('s1') } });
|
|
572
|
+
const onlyEmpty = harness({ rows: [open(21, 'bob/sync', DEFAULT)], branches: { 'bob/sync': clean('s1') }, changes: { 'bob/sync': [] } });
|
|
573
|
+
for (const other of [none, onlyEmpty]) {
|
|
574
|
+
await expect(other.svc.deleteBranchChecked(BOB, 'bob/sync', { dryRun: true, maySee })).resolves.toEqual(preview);
|
|
575
|
+
}
|
|
576
|
+
expect(preview).toMatchObject({ canDelete: true, refusals: [], openChangeRequests: [] });
|
|
577
|
+
// Never judged, either.
|
|
578
|
+
expect(h.git.changedPathsForPr).not.toHaveBeenCalled();
|
|
579
|
+
const err = await refusal(h.svc.deleteBranchChecked(BOB, 'bob/sync', { maySee }));
|
|
580
|
+
const payload = (err as BranchDeleteBlockedError).payload as Record<string, unknown>;
|
|
581
|
+
expect(payload).toMatchObject({ reason: 'open-change-request' });
|
|
582
|
+
expect(payload.number).toBeUndefined();
|
|
583
|
+
expect(payload.url).toBeUndefined();
|
|
584
|
+
expect(err.message).not.toMatch(/#25|change-requests\//);
|
|
585
|
+
expect(rows.map((r) => r.state)).toEqual(['open', 'open']);
|
|
586
|
+
// One the caller may see is still listed and named.
|
|
587
|
+
maySee.mockImplementation(async (n: number) => n === 25);
|
|
588
|
+
const seen = (await h.svc.deleteBranchChecked(BOB, 'bob/sync', { dryRun: true, maySee })) as DeleteBranchPreview;
|
|
589
|
+
expect(seen.openChangeRequests.map((cr) => cr.number)).toEqual([25]);
|
|
590
|
+
expect(seen.refusals.join(' ')).toContain('https://hexis.test/change-requests/25');
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
it("the app's delete closes an empty request too, and goes ahead", async () => {
|
|
594
|
+
const rows = [open(21, 'ana/sync', DEFAULT)];
|
|
595
|
+
const h = harness({ rows, branches: { 'ana/sync': clean('s1') }, changes: { 'ana/sync': [] } });
|
|
596
|
+
await h.svc.deleteBranch(workspaceIdForBranch(DEFAULT), 'ana/sync', ADMIN);
|
|
597
|
+
expect(rows[0]!.state).toBe('closed');
|
|
598
|
+
expect(h.deleted).toEqual(['ana/sync']);
|
|
599
|
+
// No fetch-first in the app's path.
|
|
600
|
+
expect(h.fetch).not.toHaveBeenCalled();
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
it("the app's delete still refuses on a request that proposes something, and closes nothing for a non-author", async () => {
|
|
604
|
+
const rows = [open(21, 'ali/sync', DEFAULT), open(24, 'ana/live', DEFAULT)];
|
|
605
|
+
const h = harness({ rows, branches: { 'ali/sync': clean('s1'), 'ana/live': clean('l1') }, changes: { 'ali/sync': [] } });
|
|
606
|
+
await expect(h.svc.deleteBranch(workspaceIdForBranch(DEFAULT), 'ana/live', ADMIN)).rejects.toThrow(/open change request \(#24\)/);
|
|
607
|
+
await expect(h.svc.deleteBranch(workspaceIdForBranch(DEFAULT), 'ali/sync', BOB)).rejects.toThrow(/open change request \(#21\)/);
|
|
608
|
+
expect(rows.map((r) => r.state)).toEqual(['open', 'open']);
|
|
609
|
+
expect(h.deleted).toEqual([]);
|
|
610
|
+
});
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
describe('closeEmptyOpenChangeRequests — the background close', () => {
|
|
614
|
+
it('closes the open requests that propose nothing and removes their source branch; leaves the rest open', async () => {
|
|
615
|
+
const rows = [
|
|
616
|
+
open(31, 'ali/empty', DEFAULT),
|
|
617
|
+
open(32, 'ali/live', DEFAULT),
|
|
618
|
+
open(33, 'ali/unknown', DEFAULT),
|
|
619
|
+
open(34, 'ali/saving', DEFAULT),
|
|
620
|
+
];
|
|
621
|
+
const h = harness({
|
|
622
|
+
rows,
|
|
623
|
+
admins: [],
|
|
624
|
+
branches: { 'ali/empty': clean('e1'), 'ali/live': clean('l1') },
|
|
625
|
+
changes: { 'ali/empty': [], 'ali/unknown': 'error', 'ali/saving': [] },
|
|
626
|
+
savesLanding: ['ali/saving'],
|
|
627
|
+
});
|
|
628
|
+
await expect(h.svc.closeEmptyOpenChangeRequests()).resolves.toBe(1);
|
|
629
|
+
expect(rows.map((r) => r.state)).toEqual(['closed', 'open', 'open', 'open']);
|
|
630
|
+
// Judged from the default branch's workspace after one fetch, not by cloning each request's branch.
|
|
631
|
+
expect(h.fetch).toHaveBeenCalledTimes(1);
|
|
632
|
+
expect(h.git.changedPathsForPr).toHaveBeenCalledWith(workspaceIdForBranch(DEFAULT), DEFAULT, 'ali/empty', { fetch: false });
|
|
633
|
+
// The source branch is removed as opening the page would, by `system`,
|
|
634
|
+
// leased on the tip the request was judged empty at.
|
|
635
|
+
expect(h.deleted).toEqual(['ali/empty']);
|
|
636
|
+
expect(h.git.deleteBranch).toHaveBeenCalledWith(
|
|
637
|
+
workspaceIdForBranch(DEFAULT),
|
|
638
|
+
'ali/empty',
|
|
639
|
+
expect.objectContaining({ email: 'system' }),
|
|
640
|
+
{ systemCleanup: true, expectTip: 'e1' },
|
|
641
|
+
);
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
it('keeps the source branch when a save starts landing on it after the request was judged empty', async () => {
|
|
645
|
+
const rows = [open(31, 'ali/empty', DEFAULT)];
|
|
646
|
+
const h = harness({ rows, branches: { 'ali/empty': clean('e1') }, changes: { 'ali/empty': [] } });
|
|
647
|
+
// Clear when the request is judged; a lock is held by the removal's check.
|
|
648
|
+
h.hasAnyActive.mockResolvedValueOnce(false).mockResolvedValue(true);
|
|
649
|
+
await expect(h.svc.closeEmptyOpenChangeRequests()).resolves.toBe(1);
|
|
650
|
+
expect(rows[0]!.state).toBe('closed');
|
|
651
|
+
expect(h.deleted).toEqual([]);
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
it('grants no lock on the source branch while removing it', async () => {
|
|
655
|
+
const rows = [open(31, 'ali/empty', DEFAULT)];
|
|
656
|
+
const h = harness({ rows, branches: { 'ali/empty': clean('e1') }, changes: { 'ali/empty': [] } });
|
|
657
|
+
let during: unknown = null;
|
|
658
|
+
h.git.deleteBranch.mockImplementationOnce(async () => {
|
|
659
|
+
during = await h.fileLocks.acquire(workspaceIdForBranch('ali/empty'), 'ali/empty', 'KnowledgeBase/f.md', BOB).catch((e: unknown) => e);
|
|
660
|
+
return { lastCommit: 'e1' };
|
|
661
|
+
});
|
|
662
|
+
await h.svc.closeEmptyOpenChangeRequests();
|
|
663
|
+
expect((during as { payload?: unknown }).payload).toMatchObject({ kind: 'branch-being-deleted' });
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
it('keeps the source branch, and still counts the close, when a commit pushed since is refused by the lease', async () => {
|
|
667
|
+
const rows = [open(31, 'ali/empty', DEFAULT)];
|
|
668
|
+
const h = harness({ rows, branches: { 'ali/empty': clean('e1') }, changes: { 'ali/empty': [] }, deleteFails: ['ali/empty'] });
|
|
669
|
+
await expect(h.svc.closeEmptyOpenChangeRequests()).resolves.toBe(1);
|
|
670
|
+
expect(h.git.deleteBranch).toHaveBeenCalledWith(
|
|
671
|
+
workspaceIdForBranch(DEFAULT),
|
|
672
|
+
'ali/empty',
|
|
673
|
+
expect.objectContaining({ email: 'system' }),
|
|
674
|
+
{ systemCleanup: true, expectTip: 'e1' },
|
|
675
|
+
);
|
|
676
|
+
expect(h.deleted).toEqual([]);
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
it('keeps the source branch while another request is still open from or into it', async () => {
|
|
680
|
+
const rows = [open(31, 'ali/empty', DEFAULT), open(35, 'ali/next', 'ali/empty')];
|
|
681
|
+
const h = harness({ rows, branches: { 'ali/empty': clean('e1'), 'ali/next': clean('n1') }, changes: { 'ali/empty': [] } });
|
|
682
|
+
await expect(h.svc.closeEmptyOpenChangeRequests()).resolves.toBe(1);
|
|
683
|
+
expect(rows.map((r) => r.state)).toEqual(['closed', 'open']);
|
|
684
|
+
expect(h.deleted).toEqual([]);
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
it('closes nothing in a round whose fetch fails', async () => {
|
|
688
|
+
const rows = [open(31, 'ali/empty', DEFAULT)];
|
|
689
|
+
const h = harness({ rows, branches: { 'ali/empty': clean('e1') }, changes: { 'ali/empty': [] }, fetchFails: true });
|
|
690
|
+
await expect(h.svc.closeEmptyOpenChangeRequests()).resolves.toBe(0);
|
|
691
|
+
expect(rows[0]!.state).toBe('open');
|
|
692
|
+
expect(h.deleted).toEqual([]);
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
it('starts no round once the graph has stopped it', async () => {
|
|
696
|
+
const rows = [open(31, 'ali/empty', DEFAULT), merged(32, 'ali/done', 'm1')];
|
|
697
|
+
const h = harness({
|
|
698
|
+
rows,
|
|
699
|
+
branches: { 'ali/empty': clean('e1'), 'ali/done': clean('d1') },
|
|
700
|
+
ancestry: [['d1', 'm1']],
|
|
701
|
+
changes: { 'ali/empty': [] },
|
|
702
|
+
});
|
|
703
|
+
h.svc.stopTidying();
|
|
704
|
+
await expect(h.svc.tidyAfterSweep()).resolves.toEqual({ closedEmpty: 0, removedLeftovers: 0 });
|
|
705
|
+
await h.svc.drainTidy();
|
|
706
|
+
expect(h.fetch).not.toHaveBeenCalled();
|
|
707
|
+
expect(rows[0]!.state).toBe('open');
|
|
708
|
+
expect(h.deleted).toEqual([]);
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
it('drains the round under way: waits for it to settle, and it stops at its next request or branch', async () => {
|
|
712
|
+
const rows = [open(31, 'ali/empty', DEFAULT), merged(32, 'ali/done', 'm1')];
|
|
713
|
+
const h = harness({
|
|
714
|
+
rows,
|
|
715
|
+
branches: { 'ali/empty': clean('e1'), 'ali/done': clean('d1') },
|
|
716
|
+
ancestry: [['d1', 'm1']],
|
|
717
|
+
changes: { 'ali/empty': [] },
|
|
718
|
+
});
|
|
719
|
+
// The round is held at its fetch until released.
|
|
720
|
+
let release = (): void => undefined;
|
|
721
|
+
h.fetch.mockImplementationOnce(() => new Promise<void>((resolve) => (release = resolve)));
|
|
722
|
+
const round = h.svc.tidyAfterSweep();
|
|
723
|
+
await vi.waitFor(() => expect(h.fetch).toHaveBeenCalledTimes(1));
|
|
724
|
+
|
|
725
|
+
h.svc.stopTidying();
|
|
726
|
+
let drained = false;
|
|
727
|
+
const drain = h.svc.drainTidy().then(() => {
|
|
728
|
+
drained = true;
|
|
729
|
+
});
|
|
730
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
731
|
+
expect(drained).toBe(false);
|
|
732
|
+
|
|
733
|
+
release();
|
|
734
|
+
await drain;
|
|
735
|
+
await expect(round).resolves.toEqual({ closedEmpty: 0, removedLeftovers: 0 });
|
|
736
|
+
expect(rows[0]!.state).toBe('open');
|
|
737
|
+
expect(h.deleted).toEqual([]);
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
it('runs before the leftover cleanup, and even with that cleanup switched off', async () => {
|
|
741
|
+
const h = harness({});
|
|
742
|
+
const order: string[] = [];
|
|
743
|
+
vi.spyOn(h.svc, 'closeEmptyOpenChangeRequests').mockImplementation(async () => (order.push('empty'), 2));
|
|
744
|
+
h.svc.leftoverCleanupEnabled = () => false;
|
|
745
|
+
const leftover = vi.spyOn(h.svc, 'retireLeftoverMergedBranches');
|
|
746
|
+
leftover.mockImplementation(async () => (order.push('leftover'), 0));
|
|
747
|
+
await expect(h.svc.tidyAfterSweep()).resolves.toEqual({ closedEmpty: 2, removedLeftovers: 0 });
|
|
748
|
+
expect(order).toEqual(['empty', 'leftover']);
|
|
749
|
+
});
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
describe('retireLeftoverMergedBranches — the server removes what merged change requests left behind', () => {
|
|
753
|
+
it('removes only the qualifying branches, as `system`', async () => {
|
|
754
|
+
const h = harness({
|
|
755
|
+
rows: [
|
|
756
|
+
merged(3, 'ali/sync-0709', 'm3'), // qualifies
|
|
757
|
+
merged(4, 'ali/sync-0901', 'm4'), // new commits since the merge
|
|
758
|
+
merged(5, 'ali/recreated', 'm5'), // recreated after the merge: tip not in m5
|
|
759
|
+
merged(6, 'ali/no-merge-sha', null), // no recorded merge commit
|
|
760
|
+
merged(7, 'ali/target-of-open', 'm7'), // an open request goes into it
|
|
761
|
+
open(8, 'ali/feeds', 'ali/target-of-open'),
|
|
762
|
+
merged(9, 'ali/saving', 'm9'), // a save is still landing
|
|
763
|
+
merged(10, 'current-company-state', 'm10'), // protected
|
|
764
|
+
merged(11, 'ali/already-gone', 'm11'), // not there any more
|
|
765
|
+
],
|
|
766
|
+
branches: {
|
|
767
|
+
'ali/sync-0709': clean('t3'),
|
|
768
|
+
'ali/sync-0901': { exists: true, lastCommit: 't4b', unmergedCommits: 2 },
|
|
769
|
+
'ali/recreated': clean('fresh'),
|
|
770
|
+
'ali/no-merge-sha': clean('t6'),
|
|
771
|
+
'ali/target-of-open': clean('t7'),
|
|
772
|
+
'ali/saving': clean('t9'),
|
|
773
|
+
'current-company-state': clean('t10'),
|
|
774
|
+
'ali/sync-0806': { exists: true, lastCommit: 'x', unmergedCommits: 4 }, // never had a request
|
|
775
|
+
'ali/brand-new': clean('base'), // a minute old, no request
|
|
776
|
+
},
|
|
777
|
+
ancestry: [
|
|
778
|
+
['t3', 'm3'],
|
|
779
|
+
['t4', 'm4'],
|
|
780
|
+
['t6', 'm6'],
|
|
781
|
+
['t7', 'm7'],
|
|
782
|
+
['t9', 'm9'],
|
|
783
|
+
['t10', 'm10'],
|
|
784
|
+
],
|
|
785
|
+
savesLanding: ['ali/saving'],
|
|
786
|
+
});
|
|
787
|
+
await expect(h.svc.retireLeftoverMergedBranches()).resolves.toBe(1);
|
|
788
|
+
expect(h.deleted).toEqual(['ali/sync-0709']);
|
|
789
|
+
expect(h.git.deleteBranch).toHaveBeenCalledWith(
|
|
790
|
+
workspaceIdForBranch(DEFAULT),
|
|
791
|
+
'ali/sync-0709',
|
|
792
|
+
expect.objectContaining({ email: 'system' }),
|
|
793
|
+
{ systemCleanup: true, expectTip: 't3' },
|
|
794
|
+
);
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
it('writes one log line per deletion, naming who did it (or system), the branch and its last commit', async () => {
|
|
798
|
+
const h = harness({
|
|
799
|
+
rows: [merged(3, 'ali/sync-0709', 'm3')],
|
|
800
|
+
branches: { 'ali/sync-0709': clean('t3'), 'ali/old-draft': clean('a1'), 'ana/mine': clean('n1') },
|
|
801
|
+
ancestry: [['t3', 'm3']],
|
|
802
|
+
});
|
|
803
|
+
const lines: string[] = [];
|
|
804
|
+
const spy = vi.spyOn(console, 'log').mockImplementation((line: unknown) => void lines.push(String(line)));
|
|
805
|
+
try {
|
|
806
|
+
await h.svc.retireLeftoverMergedBranches();
|
|
807
|
+
await h.svc.deleteBranchChecked(ADMIN, 'ali/old-draft');
|
|
808
|
+
// The app's branch switcher: today's path, gaining only the line.
|
|
809
|
+
await h.svc.deleteBranch(workspaceIdForBranch(DEFAULT), 'ana/mine', ADMIN);
|
|
810
|
+
} finally {
|
|
811
|
+
spy.mockRestore();
|
|
812
|
+
}
|
|
813
|
+
const deletions = lines.filter((l) => l.includes('branch deleted by'));
|
|
814
|
+
expect(deletions).toEqual([
|
|
815
|
+
'[workflow] branch deleted by system: "ali/sync-0709" at t3',
|
|
816
|
+
'[workflow] branch deleted by u-ana: "ali/old-draft" at a1',
|
|
817
|
+
'[workflow] branch deleted by u-ana: "ana/mine" at n1',
|
|
818
|
+
]);
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
it('takes any of several merged requests from the same branch name', async () => {
|
|
822
|
+
const h = harness({
|
|
823
|
+
rows: [merged(3, 'ali/twice', 'm3'), merged(20, 'ali/twice', 'm20')],
|
|
824
|
+
branches: { 'ali/twice': clean('t20') },
|
|
825
|
+
ancestry: [['t20', 'm20']],
|
|
826
|
+
});
|
|
827
|
+
await expect(h.svc.retireLeftoverMergedBranches()).resolves.toBe(1);
|
|
828
|
+
expect(h.deleted).toEqual(['ali/twice']);
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
it('removes nothing in a round whose fetch fails', async () => {
|
|
832
|
+
const h = harness({
|
|
833
|
+
rows: [merged(3, 'ali/sync-0709', 'm3')],
|
|
834
|
+
branches: { 'ali/sync-0709': clean('t3') },
|
|
835
|
+
ancestry: [['t3', 'm3']],
|
|
836
|
+
fetchFails: true,
|
|
837
|
+
});
|
|
838
|
+
await expect(h.svc.retireLeftoverMergedBranches()).resolves.toBe(0);
|
|
839
|
+
expect(h.git.branchState).not.toHaveBeenCalled();
|
|
840
|
+
expect(h.deleted).toEqual([]);
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
it('removes nothing when switched off, without even fetching', async () => {
|
|
844
|
+
const h = harness({
|
|
845
|
+
rows: [merged(3, 'ali/sync-0709', 'm3')],
|
|
846
|
+
branches: { 'ali/sync-0709': clean('t3') },
|
|
847
|
+
ancestry: [['t3', 'm3']],
|
|
848
|
+
});
|
|
849
|
+
h.svc.leftoverCleanupEnabled = () => false;
|
|
850
|
+
await expect(h.svc.retireLeftoverMergedBranches()).resolves.toBe(0);
|
|
851
|
+
expect(h.fetch).not.toHaveBeenCalled();
|
|
852
|
+
expect(h.deleted).toEqual([]);
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
it('switching it off leaves the removal of a branch when its change request is merged alone', async () => {
|
|
856
|
+
const h = harness({ rows: [open(3, 'ali/just-merged', DEFAULT)], branches: { 'ali/just-merged': clean('t3') } });
|
|
857
|
+
h.svc.leftoverCleanupEnabled = () => false;
|
|
858
|
+
await expect(
|
|
859
|
+
h.svc.mergeChangeRequest(3, ADMIN, 't3', [], {} as never, 'Just merged', DEFAULT, workspaceIdForBranch(DEFAULT)),
|
|
860
|
+
).resolves.toMatchObject({ kind: 'merged' });
|
|
861
|
+
await h.svc.drainTidy();
|
|
862
|
+
expect(h.deleted).toEqual(['ali/just-merged']);
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
it('a branch it cannot read is left, and the round goes on', async () => {
|
|
866
|
+
const h = harness({
|
|
867
|
+
rows: [merged(3, 'ali/broken', 'm3'), merged(4, 'ali/fine', 'm4')],
|
|
868
|
+
branches: { 'ali/fine': clean('t4') },
|
|
869
|
+
ancestry: [['t4', 'm4']],
|
|
870
|
+
});
|
|
871
|
+
const real = h.git.branchState.getMockImplementation()!;
|
|
872
|
+
h.git.branchState.mockImplementation(async (ws: string, name: string, d: string) => {
|
|
873
|
+
if (name === 'ali/broken') throw new Error('bad object');
|
|
874
|
+
return real(ws, name, d);
|
|
875
|
+
});
|
|
876
|
+
await expect(h.svc.retireLeftoverMergedBranches()).resolves.toBe(1);
|
|
877
|
+
expect(h.deleted).toEqual(['ali/fine']);
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
it('runs after the sweep for deleted branches that every merge starts', async () => {
|
|
881
|
+
const h = harness({ rows: [open(3, 'ali/just-merged', DEFAULT)], branches: { 'ali/just-merged': clean('t3') } });
|
|
882
|
+
const cleanup = vi.spyOn(h.svc, 'tidyAfterSweep').mockResolvedValue({ closedEmpty: 0, removedLeftovers: 0 });
|
|
883
|
+
const sweep = vi.spyOn(h.svc, 'closeChangeRequestsWithDeletedBranches').mockResolvedValue(0);
|
|
884
|
+
await h.svc.mergeChangeRequest(3, ADMIN, 't3', [], {} as never, 'Just merged', DEFAULT, workspaceIdForBranch(DEFAULT));
|
|
885
|
+
await h.svc.drainTidy();
|
|
886
|
+
expect(sweep).toHaveBeenCalledTimes(1);
|
|
887
|
+
expect(cleanup).toHaveBeenCalledTimes(1);
|
|
888
|
+
expect(cleanup.mock.invocationCallOrder[0]).toBeGreaterThan(sweep.mock.invocationCallOrder[0]!);
|
|
889
|
+
});
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
// ── End to end, over real git ────────────────────────────────────────────────
|
|
893
|
+
|
|
894
|
+
const execFileAsync = promisify(execFile);
|
|
895
|
+
const GIT_ENV = {
|
|
896
|
+
...process.env,
|
|
897
|
+
GIT_AUTHOR_NAME: 'Test',
|
|
898
|
+
GIT_AUTHOR_EMAIL: 't@x.com',
|
|
899
|
+
GIT_COMMITTER_NAME: 'Test',
|
|
900
|
+
GIT_COMMITTER_EMAIL: 't@x.com',
|
|
901
|
+
};
|
|
902
|
+
const git = async (cwd: string, args: string[]) =>
|
|
903
|
+
(await execFileAsync('git', args, { cwd, env: GIT_ENV })).stdout.trim();
|
|
904
|
+
|
|
905
|
+
describe('deleteBranchChecked over real git — a delete made from another workspace', () => {
|
|
906
|
+
let root: string;
|
|
907
|
+
beforeEach(async () => {
|
|
908
|
+
root = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-delete-branch-checked-'));
|
|
909
|
+
});
|
|
910
|
+
afterEach(async () => {
|
|
911
|
+
await fs.rm(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
it("removes the branch from the shared repository and the server, though the call came from the branch's own workspace", async () => {
|
|
915
|
+
const upstream = path.join(root, 'upstream.git');
|
|
916
|
+
await git(root, ['init', '--bare', '-b', DEFAULT, upstream]);
|
|
917
|
+
const seed = path.join(root, '.seed');
|
|
918
|
+
await git(root, ['clone', upstream, seed]);
|
|
919
|
+
await git(seed, ['checkout', '-b', DEFAULT]);
|
|
920
|
+
await git(seed, ['commit', '--allow-empty', '-m', 'init']);
|
|
921
|
+
await git(seed, ['push', 'origin', DEFAULT]);
|
|
922
|
+
await git(seed, ['checkout', '-b', 'bob/draft']);
|
|
923
|
+
await git(seed, ['push', 'origin', 'bob/draft']);
|
|
924
|
+
|
|
925
|
+
// Two clones on the server: the default branch's, and the draft's own,
|
|
926
|
+
// which has the draft checked out (git refuses to delete from there).
|
|
927
|
+
const wsDir = (id: string) => path.join(root, 'workspaces', id);
|
|
928
|
+
const defaultId = workspaceIdForBranch(DEFAULT);
|
|
929
|
+
const draftId = workspaceIdForBranch('bob/draft');
|
|
930
|
+
await git(root, ['clone', '-b', DEFAULT, upstream, path.join(wsDir(defaultId), 'knowledge-base')]);
|
|
931
|
+
await git(root, ['clone', '-b', 'bob/draft', upstream, path.join(wsDir(draftId), 'knowledge-base')]);
|
|
932
|
+
const tip = await git(seed, ['rev-parse', 'HEAD']);
|
|
933
|
+
|
|
934
|
+
const workspaces = {
|
|
935
|
+
getWorkspacePath: async (id: string) => wsDir(id),
|
|
936
|
+
getOrCreateForBranch: async (b: string) => ({ id: workspaceIdForBranch(b) }),
|
|
937
|
+
ensureRemotesFetched: async (id: string) => {
|
|
938
|
+
await git(path.join(wsDir(id), 'knowledge-base'), ['fetch', '--prune', 'origin']);
|
|
939
|
+
},
|
|
940
|
+
hasBootstrappedWorkspace: async (id: string) =>
|
|
941
|
+
fs.stat(path.join(wsDir(id), 'knowledge-base', '.git')).then(
|
|
942
|
+
() => true,
|
|
943
|
+
() => false,
|
|
944
|
+
),
|
|
945
|
+
deleteWorkspace: async (id: string) => fs.rm(wsDir(id), { recursive: true, force: true }),
|
|
946
|
+
} as unknown as WorkspaceService;
|
|
947
|
+
const gitService = new GitService(workspaces, new WorkflowHooks(), testKbContext());
|
|
948
|
+
const svc = new WorkflowService(
|
|
949
|
+
fakeDb([]),
|
|
950
|
+
gitService,
|
|
951
|
+
{ invalidateDetailCache: vi.fn() } as unknown as PullRequestService,
|
|
952
|
+
{} as IReviewWorkflowService,
|
|
953
|
+
workspaces,
|
|
954
|
+
{} as IAccessControl,
|
|
955
|
+
{ hasAnyActive: async () => false, whileNoneAcquired: (_b: string, fn: () => Promise<unknown>) => fn() } as unknown as FileLockService,
|
|
956
|
+
{ hasAnyForWorkspace: async () => false } as unknown as PendingCommitsService,
|
|
957
|
+
testKbContext(),
|
|
958
|
+
openChangeGate(),
|
|
959
|
+
);
|
|
960
|
+
|
|
961
|
+
await expect(svc.deleteBranchChecked(BOB, 'bob/draft')).resolves.toEqual({
|
|
962
|
+
kind: 'deleted',
|
|
963
|
+
branch: 'bob/draft',
|
|
964
|
+
lastCommit: tip,
|
|
965
|
+
discardedCommits: 0,
|
|
966
|
+
});
|
|
967
|
+
expect(await git(upstream, ['for-each-ref', 'refs/heads/bob/draft'])).toBe('');
|
|
968
|
+
// The draft's own clone on the server went with it.
|
|
969
|
+
await expect(fs.stat(wsDir(draftId))).rejects.toThrow();
|
|
970
|
+
// Asked again, it is not a branch.
|
|
971
|
+
await expect(svc.deleteBranchChecked(BOB, 'bob/draft')).rejects.toBeInstanceOf(BranchNotFoundError);
|
|
972
|
+
});
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
describe('emptiness over real git — judged from the default workspace as from the branch’s own', () => {
|
|
976
|
+
let root: string;
|
|
977
|
+
beforeEach(async () => {
|
|
978
|
+
root = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-empty-request-'));
|
|
979
|
+
});
|
|
980
|
+
afterEach(async () => {
|
|
981
|
+
await fs.rm(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
it('answers the same changed paths from either clone, before and after the change lands on the default branch', async () => {
|
|
985
|
+
const upstream = path.join(root, 'upstream.git');
|
|
986
|
+
await git(root, ['init', '--bare', '-b', DEFAULT, upstream]);
|
|
987
|
+
const seed = path.join(root, '.seed');
|
|
988
|
+
await git(root, ['clone', upstream, seed]);
|
|
989
|
+
await git(seed, ['checkout', '-b', DEFAULT]);
|
|
990
|
+
await fs.mkdir(path.join(seed, 'KnowledgeBase'));
|
|
991
|
+
await fs.writeFile(path.join(seed, 'KnowledgeBase', 'f.md'), 'one\n');
|
|
992
|
+
await git(seed, ['add', '.']);
|
|
993
|
+
await git(seed, ['commit', '-m', 'init']);
|
|
994
|
+
await git(seed, ['push', 'origin', DEFAULT]);
|
|
995
|
+
await git(seed, ['checkout', '-b', 'ali/sync']);
|
|
996
|
+
await fs.writeFile(path.join(seed, 'KnowledgeBase', 'f.md'), 'two\n');
|
|
997
|
+
await git(seed, ['commit', '-am', 'change']);
|
|
998
|
+
await git(seed, ['push', 'origin', 'ali/sync']);
|
|
999
|
+
|
|
1000
|
+
const wsDir = (id: string) => path.join(root, 'workspaces', id);
|
|
1001
|
+
const defaultId = workspaceIdForBranch(DEFAULT);
|
|
1002
|
+
const ownId = workspaceIdForBranch('ali/sync');
|
|
1003
|
+
await git(root, ['clone', '-b', DEFAULT, upstream, path.join(wsDir(defaultId), 'knowledge-base')]);
|
|
1004
|
+
await git(root, ['clone', '-b', 'ali/sync', upstream, path.join(wsDir(ownId), 'knowledge-base')]);
|
|
1005
|
+
const gitService = new GitService(
|
|
1006
|
+
{ getWorkspacePath: async (id: string) => wsDir(id) } as unknown as WorkspaceService,
|
|
1007
|
+
new WorkflowHooks(),
|
|
1008
|
+
testKbContext(),
|
|
1009
|
+
);
|
|
1010
|
+
const fetchDefault = () => git(path.join(wsDir(defaultId), 'knowledge-base'), ['fetch', '--prune', 'origin']);
|
|
1011
|
+
const fromDefault = () => gitService.changedPathsForPr(defaultId, DEFAULT, 'ali/sync', { fetch: false });
|
|
1012
|
+
const fromOwn = () => gitService.changedPathsForPr(ownId, DEFAULT, 'ali/sync');
|
|
1013
|
+
|
|
1014
|
+
await fetchDefault();
|
|
1015
|
+
expect(await fromDefault()).toEqual(await fromOwn());
|
|
1016
|
+
expect(await fromDefault()).not.toEqual([]);
|
|
1017
|
+
|
|
1018
|
+
// The change lands on the default branch another way: the request now proposes nothing.
|
|
1019
|
+
await git(seed, ['checkout', DEFAULT]);
|
|
1020
|
+
await git(seed, ['merge', '--no-ff', '-m', 'landed elsewhere', 'ali/sync']);
|
|
1021
|
+
await git(seed, ['push', 'origin', DEFAULT]);
|
|
1022
|
+
await fetchDefault();
|
|
1023
|
+
expect(await fromDefault()).toEqual([]);
|
|
1024
|
+
expect(await fromOwn()).toEqual([]);
|
|
1025
|
+
});
|
|
1026
|
+
});
|