@bevel-software/platform-core-backend 0.14.0 → 0.15.1
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/THIRD-PARTY-NOTICES.md +6 -6
- package/dist/core/catalog-cache-invalidation.d.ts +29 -0
- package/dist/core/catalog-cache-invalidation.d.ts.map +1 -0
- package/dist/core/catalog-cache-invalidation.js +71 -0
- package/dist/core/catalog-cache-invalidation.js.map +1 -0
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +99 -8
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +18 -1
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +96 -43
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +18 -6
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +26 -1
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +213 -71
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/access.routes.d.ts.map +1 -1
- package/dist/modules/access/access.routes.js +59 -7
- package/dist/modules/access/access.routes.js.map +1 -1
- package/dist/modules/access-model/access-grammar.d.ts +51 -1
- package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
- package/dist/modules/access-model/access-grammar.js +95 -1
- package/dist/modules/access-model/access-grammar.js.map +1 -1
- package/dist/modules/access-model/group-files.d.ts.map +1 -1
- package/dist/modules/access-model/group-files.js +4 -1
- package/dist/modules/access-model/group-files.js.map +1 -1
- package/dist/modules/access-model/plugin-principals.d.ts +49 -0
- package/dist/modules/access-model/plugin-principals.d.ts.map +1 -0
- package/dist/modules/access-model/plugin-principals.js +125 -0
- package/dist/modules/access-model/plugin-principals.js.map +1 -0
- package/dist/modules/database/core-schema.d.ts +326 -0
- package/dist/modules/database/core-schema.d.ts.map +1 -1
- package/dist/modules/database/core-schema.js +52 -0
- package/dist/modules/database/core-schema.js.map +1 -1
- package/dist/modules/kb-sync/index.d.ts +6 -0
- package/dist/modules/kb-sync/index.d.ts.map +1 -0
- package/dist/modules/kb-sync/index.js +5 -0
- package/dist/modules/kb-sync/index.js.map +1 -0
- package/dist/modules/kb-sync/kb-sync.interface.d.ts +77 -0
- package/dist/modules/kb-sync/kb-sync.interface.d.ts.map +1 -0
- package/dist/modules/kb-sync/kb-sync.interface.js +2 -0
- package/dist/modules/kb-sync/kb-sync.interface.js.map +1 -0
- package/dist/modules/kb-sync/kb-sync.routes.d.ts +70 -0
- package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -0
- package/dist/modules/kb-sync/kb-sync.routes.js +135 -0
- package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -0
- package/dist/modules/kb-sync/kb-sync.service.d.ts +45 -0
- package/dist/modules/kb-sync/kb-sync.service.d.ts.map +1 -0
- package/dist/modules/kb-sync/kb-sync.service.js +183 -0
- package/dist/modules/kb-sync/kb-sync.service.js.map +1 -0
- package/dist/modules/kb-sync/sync-auth.d.ts +59 -0
- package/dist/modules/kb-sync/sync-auth.d.ts.map +1 -0
- package/dist/modules/kb-sync/sync-auth.js +62 -0
- package/dist/modules/kb-sync/sync-auth.js.map +1 -0
- package/dist/modules/kb-sync/sync-payload.d.ts +32 -0
- package/dist/modules/kb-sync/sync-payload.d.ts.map +1 -0
- package/dist/modules/kb-sync/sync-payload.js +105 -0
- package/dist/modules/kb-sync/sync-payload.js.map +1 -0
- package/dist/modules/marketplace/git-http.routes.d.ts +42 -0
- package/dist/modules/marketplace/git-http.routes.d.ts.map +1 -0
- package/dist/modules/marketplace/git-http.routes.js +190 -0
- package/dist/modules/marketplace/git-http.routes.js.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts +23 -0
- package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js +59 -0
- package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts +59 -0
- package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-codes.store.js +96 -0
- package/dist/modules/marketplace/github-facade/github-facade-codes.store.js.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts +99 -0
- package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js +147 -0
- package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts +47 -0
- package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade.routes.js +228 -0
- package/dist/modules/marketplace/github-facade/github-facade.routes.js.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade.service.d.ts +109 -0
- package/dist/modules/marketplace/github-facade/github-facade.service.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade.service.js +172 -0
- package/dist/modules/marketplace/github-facade/github-facade.service.js.map +1 -0
- package/dist/modules/marketplace/github-facade/index.d.ts +6 -0
- package/dist/modules/marketplace/github-facade/index.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/index.js +6 -0
- package/dist/modules/marketplace/github-facade/index.js.map +1 -0
- package/dist/modules/marketplace/index.d.ts +4 -0
- package/dist/modules/marketplace/index.d.ts.map +1 -0
- package/dist/modules/marketplace/index.js +4 -0
- package/dist/modules/marketplace/index.js.map +1 -0
- package/dist/modules/marketplace/marketplace-repo.service.d.ts +122 -0
- package/dist/modules/marketplace/marketplace-repo.service.d.ts.map +1 -0
- package/dist/modules/marketplace/marketplace-repo.service.js +300 -0
- package/dist/modules/marketplace/marketplace-repo.service.js.map +1 -0
- package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js +4 -0
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
- package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts +14 -8
- package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/oauth-consent.routes.js +13 -1
- package/dist/modules/mcp/oauth/oauth-consent.routes.js.map +1 -1
- package/dist/modules/mcp/oauth/oauth-state.d.ts +13 -2
- package/dist/modules/mcp/oauth/oauth-state.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/oauth-state.js +3 -1
- package/dist/modules/mcp/oauth/oauth-state.js.map +1 -1
- package/dist/modules/plugins/compile/compile-marketplace.d.ts +97 -0
- package/dist/modules/plugins/compile/compile-marketplace.d.ts.map +1 -0
- package/dist/modules/plugins/compile/compile-marketplace.js +240 -0
- package/dist/modules/plugins/compile/compile-marketplace.js.map +1 -0
- package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts +66 -0
- package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts.map +1 -0
- package/dist/modules/plugins/compile/marketplace-compiler.service.js +132 -0
- package/dist/modules/plugins/compile/marketplace-compiler.service.js.map +1 -0
- package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts +37 -0
- package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts.map +1 -0
- package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js +135 -0
- package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js.map +1 -0
- package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts +45 -0
- package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts.map +1 -0
- package/dist/modules/plugins/discovery/bundle-dialect/registry.js +147 -0
- package/dist/modules/plugins/discovery/bundle-dialect/registry.js.map +1 -0
- package/dist/modules/plugins/discovery/kb-plugin-source.d.ts +37 -0
- package/dist/modules/plugins/discovery/kb-plugin-source.d.ts.map +1 -0
- package/dist/modules/plugins/discovery/kb-plugin-source.js +107 -0
- package/dist/modules/plugins/discovery/kb-plugin-source.js.map +1 -0
- package/dist/modules/plugins/discovery/native.source.d.ts +15 -0
- package/dist/modules/plugins/discovery/native.source.d.ts.map +1 -0
- package/dist/modules/plugins/discovery/native.source.js +102 -0
- package/dist/modules/plugins/discovery/native.source.js.map +1 -0
- package/dist/modules/plugins/discovery/plugin-source.d.ts +96 -0
- package/dist/modules/plugins/discovery/plugin-source.d.ts.map +1 -0
- package/dist/modules/plugins/discovery/plugin-source.js +2 -0
- package/dist/modules/plugins/discovery/plugin-source.js.map +1 -0
- package/dist/modules/plugins/index.d.ts +10 -1
- package/dist/modules/plugins/index.d.ts.map +1 -1
- package/dist/modules/plugins/index.js +8 -0
- package/dist/modules/plugins/index.js.map +1 -1
- package/dist/modules/plugins/plugin-links.d.ts +66 -0
- package/dist/modules/plugins/plugin-links.d.ts.map +1 -0
- package/dist/modules/plugins/plugin-links.js +160 -0
- package/dist/modules/plugins/plugin-links.js.map +1 -0
- package/dist/modules/plugins/plugin-links.service.d.ts +114 -0
- package/dist/modules/plugins/plugin-links.service.d.ts.map +1 -0
- package/dist/modules/plugins/plugin-links.service.js +304 -0
- package/dist/modules/plugins/plugin-links.service.js.map +1 -0
- package/dist/modules/plugins/plugin-provision.service.d.ts +39 -3
- package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -1
- package/dist/modules/plugins/plugin-provision.service.js +152 -52
- package/dist/modules/plugins/plugin-provision.service.js.map +1 -1
- package/dist/modules/plugins/plugin-rename.service.d.ts +69 -0
- package/dist/modules/plugins/plugin-rename.service.d.ts.map +1 -0
- package/dist/modules/plugins/plugin-rename.service.js +245 -0
- package/dist/modules/plugins/plugin-rename.service.js.map +1 -0
- package/dist/modules/plugins/plugins.contract.d.ts +27 -1
- package/dist/modules/plugins/plugins.contract.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.routes.d.ts +7 -1
- package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.routes.js +120 -12
- package/dist/modules/plugins/plugins.routes.js.map +1 -1
- package/dist/modules/plugins/plugins.service.d.ts +50 -3
- package/dist/modules/plugins/plugins.service.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.service.js +108 -47
- package/dist/modules/plugins/plugins.service.js.map +1 -1
- package/dist/modules/settings/deployment-settings.service.d.ts +9 -0
- package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
- package/dist/modules/settings/deployment-settings.service.js +82 -2
- package/dist/modules/settings/deployment-settings.service.js.map +1 -1
- package/dist/modules/settings/setup.routes.d.ts +26 -0
- package/dist/modules/settings/setup.routes.d.ts.map +1 -1
- package/dist/modules/settings/setup.routes.js +9 -1
- package/dist/modules/settings/setup.routes.js.map +1 -1
- package/dist/modules/skills/index.d.ts +1 -0
- package/dist/modules/skills/index.d.ts.map +1 -1
- package/dist/modules/skills/index.js +1 -0
- package/dist/modules/skills/index.js.map +1 -1
- package/dist/modules/skills/pending-skills.service.d.ts.map +1 -1
- package/dist/modules/skills/pending-skills.service.js +9 -4
- package/dist/modules/skills/pending-skills.service.js.map +1 -1
- package/dist/modules/skills/skill-access-requests.routes.d.ts +38 -0
- package/dist/modules/skills/skill-access-requests.routes.d.ts.map +1 -0
- package/dist/modules/skills/skill-access-requests.routes.js +168 -0
- package/dist/modules/skills/skill-access-requests.routes.js.map +1 -0
- package/dist/modules/skills/skills.contract.d.ts +31 -0
- package/dist/modules/skills/skills.contract.d.ts.map +1 -1
- package/dist/modules/skills/skills.routes.d.ts +28 -3
- package/dist/modules/skills/skills.routes.d.ts.map +1 -1
- package/dist/modules/skills/skills.routes.js +42 -3
- package/dist/modules/skills/skills.routes.js.map +1 -1
- package/dist/modules/skills/skills.service.d.ts +4 -0
- package/dist/modules/skills/skills.service.d.ts.map +1 -1
- package/dist/modules/skills/skills.service.js +83 -22
- package/dist/modules/skills/skills.service.js.map +1 -1
- package/dist/modules/tool-auth/external-api-key.interface.d.ts +18 -1
- package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
- package/dist/modules/tool-auth/external-api-key.interface.js +2 -1
- package/dist/modules/tool-auth/external-api-key.interface.js.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.d.ts +13 -4
- package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.js +26 -9
- package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
- package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +36 -0
- package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -1
- package/dist/modules/tool-manuals/mcp-json-discovery.js +130 -92
- package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts +6 -1
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.js +18 -25
- package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +40 -2
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +227 -54
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/git/pull-request.service.d.ts +6 -0
- package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/pull-request.service.js +8 -0
- package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +52 -1
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +242 -9
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts +35 -0
- package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts.map +1 -0
- package/dist/modules/workspace/startup/steps/plugin-manifests.step.js +121 -0
- package/dist/modules/workspace/startup/steps/plugin-manifests.step.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 +46 -2
- package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
- package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/template-files.step.js +112 -25
- package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -1
- package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.routes.js +9 -9
- package/dist/modules/workspace/workspace.routes.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +33 -0
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +110 -16
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/shared/domain-errors.d.ts +20 -0
- package/dist/shared/domain-errors.d.ts.map +1 -1
- package/dist/shared/domain-errors.js +29 -0
- package/dist/shared/domain-errors.js.map +1 -1
- package/dist/shared/fs-errors.d.ts +8 -0
- package/dist/shared/fs-errors.d.ts.map +1 -0
- package/dist/shared/fs-errors.js +11 -0
- package/dist/shared/fs-errors.js.map +1 -0
- package/dist/shared/fs-walk.d.ts +20 -6
- package/dist/shared/fs-walk.d.ts.map +1 -1
- package/dist/shared/fs-walk.js +35 -27
- package/dist/shared/fs-walk.js.map +1 -1
- package/dist/shared/kb-walk.d.ts +52 -0
- package/dist/shared/kb-walk.d.ts.map +1 -0
- package/dist/shared/kb-walk.js +76 -0
- package/dist/shared/kb-walk.js.map +1 -0
- package/dist/shared/path-order.d.ts +18 -0
- package/dist/shared/path-order.d.ts.map +1 -0
- package/dist/shared/path-order.js +35 -0
- package/dist/shared/path-order.js.map +1 -0
- package/dist/shared/ttl-cache.d.ts +28 -1
- package/dist/shared/ttl-cache.d.ts.map +1 -1
- package/dist/shared/ttl-cache.js +33 -1
- package/dist/shared/ttl-cache.js.map +1 -1
- package/kb-template/.bevelignore +1 -1
- package/kb-template/AGENTS.md +95 -30
- package/kb-template/Skills/.gitkeep +0 -0
- package/migrations/0005_github_facade.sql +25 -0
- package/migrations/meta/0005_snapshot.json +1717 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +6 -6
- package/src/__tests__/kb-layout-config.test.ts +110 -0
- package/src/core/__tests__/catalog-cache-invalidation.test.ts +146 -0
- package/src/core/catalog-cache-invalidation.ts +92 -0
- package/src/core/create-core-server.ts +123 -7
- package/src/core/create-core-services.ts +162 -41
- package/src/modules/access/__tests__/access-control.invalidate-race.test.ts +86 -0
- package/src/modules/access/__tests__/access-control.service.test.ts +19 -3
- package/src/modules/access/__tests__/access-plugin-principals.test.ts +368 -0
- package/src/modules/access/__tests__/groups-admin.service.test.ts +8 -0
- package/src/modules/access/access-control.interface.ts +20 -9
- package/src/modules/access/access-control.service.ts +240 -78
- package/src/modules/access/access.routes.ts +96 -14
- package/src/modules/access-model/__tests__/access-grammar.test.ts +18 -0
- package/src/modules/access-model/__tests__/plugin-token-canonical.test.ts +40 -0
- package/src/modules/access-model/access-grammar.ts +114 -2
- package/src/modules/access-model/group-files.ts +4 -0
- package/src/modules/access-model/plugin-principals.ts +137 -0
- package/src/modules/database/core-schema.ts +54 -0
- package/src/modules/kb-sync/__tests__/kb-sync.routes.test.ts +297 -0
- package/src/modules/kb-sync/__tests__/kb-sync.service.test.ts +295 -0
- package/src/modules/kb-sync/__tests__/sync-auth.test.ts +118 -0
- package/src/modules/kb-sync/__tests__/sync-payload.test.ts +148 -0
- package/src/modules/kb-sync/index.ts +19 -0
- package/src/modules/kb-sync/kb-sync.interface.ts +79 -0
- package/src/modules/kb-sync/kb-sync.routes.ts +197 -0
- package/src/modules/kb-sync/kb-sync.service.ts +201 -0
- package/src/modules/kb-sync/sync-auth.ts +118 -0
- package/src/modules/kb-sync/sync-payload.ts +143 -0
- package/src/modules/marketplace/__tests__/github-facade.test.ts +443 -0
- package/src/modules/marketplace/__tests__/marketplace-git.test.ts +179 -0
- package/src/modules/marketplace/git-http.routes.ts +193 -0
- package/src/modules/marketplace/github-facade/github-facade-admin.routes.ts +78 -0
- package/src/modules/marketplace/github-facade/github-facade-codes.store.ts +145 -0
- package/src/modules/marketplace/github-facade/github-facade-credentials.service.ts +208 -0
- package/src/modules/marketplace/github-facade/github-facade.routes.ts +268 -0
- package/src/modules/marketplace/github-facade/github-facade.service.ts +221 -0
- package/src/modules/marketplace/github-facade/index.ts +25 -0
- package/src/modules/marketplace/index.ts +3 -0
- package/src/modules/marketplace/marketplace-repo.service.ts +337 -0
- package/src/modules/mcp/oauth/bevel-oauth-provider.ts +3 -0
- package/src/modules/mcp/oauth/oauth-consent.routes.ts +24 -2
- package/src/modules/mcp/oauth/oauth-state.ts +15 -3
- package/src/modules/plugins/__tests__/bundle-dialect.e2e.test.ts +163 -0
- package/src/modules/plugins/__tests__/compile-marketplace.test.ts +294 -0
- package/src/modules/plugins/__tests__/plugin-index.service.test.ts +56 -12
- package/src/modules/plugins/__tests__/plugin-links.service.test.ts +287 -0
- package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +205 -5
- package/src/modules/plugins/__tests__/plugin-rename.service.test.ts +394 -0
- package/src/modules/plugins/__tests__/plugins.routes.test.ts +80 -33
- package/src/modules/plugins/compile/compile-marketplace.ts +403 -0
- package/src/modules/plugins/compile/marketplace-compiler.service.ts +150 -0
- package/src/modules/plugins/discovery/__tests__/kb-plugin-source.test.ts +329 -0
- package/src/modules/plugins/discovery/bundle-dialect/bundle.source.ts +140 -0
- package/src/modules/plugins/discovery/bundle-dialect/registry.ts +175 -0
- package/src/modules/plugins/discovery/kb-plugin-source.ts +114 -0
- package/src/modules/plugins/discovery/native.source.ts +122 -0
- package/src/modules/plugins/discovery/plugin-source.ts +99 -0
- package/src/modules/plugins/index.ts +10 -0
- package/src/modules/plugins/plugin-links.service.ts +363 -0
- package/src/modules/plugins/plugin-links.ts +184 -0
- package/src/modules/plugins/plugin-provision.service.ts +160 -54
- package/src/modules/plugins/plugin-rename.service.ts +298 -0
- package/src/modules/plugins/plugins.contract.ts +28 -1
- package/src/modules/plugins/plugins.routes.ts +128 -11
- package/src/modules/plugins/plugins.service.ts +106 -53
- package/src/modules/settings/__tests__/deployment-settings.service.test.ts +57 -0
- package/src/modules/settings/__tests__/setup.routes.sync-status.test.ts +77 -0
- package/src/modules/settings/deployment-settings.service.ts +95 -2
- package/src/modules/settings/setup.routes.ts +24 -0
- package/src/modules/skills/__tests__/skill-access-requests.routes.test.ts +128 -0
- package/src/modules/skills/__tests__/skills.service.test.ts +85 -0
- package/src/modules/skills/index.ts +1 -0
- package/src/modules/skills/pending-skills.service.ts +7 -5
- package/src/modules/skills/skill-access-requests.routes.ts +199 -0
- package/src/modules/skills/skills.contract.ts +32 -0
- package/src/modules/skills/skills.routes.ts +59 -3
- package/src/modules/skills/skills.service.ts +84 -22
- package/src/modules/tool-auth/external-api-key.interface.ts +20 -1
- package/src/modules/tool-auth/external-api-key.service.ts +31 -13
- package/src/modules/tool-manuals/__tests__/mcp-json-discovery.test.ts +24 -1
- package/src/modules/tool-manuals/__tests__/tool-manuals.cli.test.ts +2 -0
- package/src/modules/tool-manuals/mcp-json-discovery.ts +154 -88
- package/src/modules/tool-manuals/tool-manuals.service.ts +14 -23
- package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +1 -1
- package/src/modules/workflow/__tests__/workflow.service.commitFileWhileLocked.test.ts +1 -0
- package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +2 -2
- package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +119 -2
- package/src/modules/workflow/__tests__/workflow.service.sync.test.ts +437 -0
- package/src/modules/workflow/git/__tests__/git.service.postMergePull.test.ts +3 -3
- package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +47 -4
- package/src/modules/workflow/git/__tests__/git.service.syncFromRemote.test.ts +253 -0
- package/src/modules/workflow/git/git.service.ts +239 -56
- package/src/modules/workflow/git/pull-request.service.ts +9 -0
- package/src/modules/workflow/workflow.service.ts +260 -9
- package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +6 -7
- package/src/modules/workspace/__tests__/workspace.service.list-cloned.test.ts +93 -0
- package/src/modules/workspace/__tests__/workspace.service.missing-branch.test.ts +28 -0
- package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +42 -12
- package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +219 -4
- package/src/modules/workspace/startup/steps/plugin-manifests.step.ts +129 -0
- package/src/modules/workspace/startup/steps/seed-tree.ts +44 -2
- package/src/modules/workspace/startup/steps/template-files.step.ts +131 -27
- package/src/modules/workspace/workspace.routes.ts +9 -17
- package/src/modules/workspace/workspace.service.ts +107 -16
- package/src/shared/__tests__/filename.test.ts +0 -0
- package/src/shared/__tests__/fs-walk.test.ts +22 -1
- package/src/shared/__tests__/kb-walk.test.ts +107 -0
- package/src/shared/__tests__/path-order.test.ts +24 -0
- package/src/shared/__tests__/ttl-cache.test.ts +55 -0
- package/src/shared/domain-errors.ts +29 -0
- package/src/shared/fs-errors.ts +10 -0
- package/src/shared/fs-walk.ts +38 -23
- package/src/shared/kb-walk.ts +95 -0
- package/src/shared/path-order.ts +33 -0
- package/src/shared/ttl-cache.ts +33 -1
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import type { RemoteSyncPullResult } from '@bevel-software/platform-shared';
|
|
3
|
+
import type { GitService } from '../git/git.service.js';
|
|
4
|
+
import type { PullRequestService } from '../git/pull-request.service.js';
|
|
5
|
+
import type { IReviewWorkflowService } from '../review-workflow/review-workflow.interface.js';
|
|
6
|
+
import type { WorkspaceService } from '../../workspace/workspace.service.js';
|
|
7
|
+
import type { IAccessControl } from '../../access/access-control.interface.js';
|
|
8
|
+
import type { FileLockService } from '../file-lock.service.js';
|
|
9
|
+
import type { PendingCommitsService } from '../pending-commits.service.js';
|
|
10
|
+
import type { WorkflowEventBus } from '../event-bus.js';
|
|
11
|
+
import type { Database } from '../../database/connection.js';
|
|
12
|
+
import { WorkflowService, syncConflictMessage } from '../workflow.service.js';
|
|
13
|
+
import { PullRebaseConflictError, RemoteBranchGoneError } from '../../../shared/domain-errors.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* `syncWorkspaceFromRemote` — the per-branch step a remote sync drives.
|
|
17
|
+
* Built on a fake git whose one call, `syncFromRemote`, answers what the real
|
|
18
|
+
* one observes under a single hold of the clone. The assertions are about
|
|
19
|
+
* the CONTRACT: what gets announced when content changed, what does not
|
|
20
|
+
* when it did not, that a conflict is both queued for the usual recovery and
|
|
21
|
+
* reported as an outcome, and that nothing here ever throws.
|
|
22
|
+
*/
|
|
23
|
+
const PULLED: RemoteSyncPullResult = { before: 'aaa', after: 'bbb', treeChanged: true, changedPaths: [] };
|
|
24
|
+
|
|
25
|
+
function build(opts: { sync: () => Promise<RemoteSyncPullResult> }) {
|
|
26
|
+
const git = { syncFromRemote: vi.fn(opts.sync) } as unknown as GitService;
|
|
27
|
+
const prs = { invalidateListCache: vi.fn() } as unknown as PullRequestService;
|
|
28
|
+
const pendingCommits = { enqueueIfAbsent: vi.fn(async () => true) } as unknown as PendingCommitsService;
|
|
29
|
+
const events = { emit: vi.fn() } as unknown as WorkflowEventBus;
|
|
30
|
+
const svc = new WorkflowService(
|
|
31
|
+
{} as Database,
|
|
32
|
+
git,
|
|
33
|
+
prs,
|
|
34
|
+
{} as IReviewWorkflowService,
|
|
35
|
+
{} as WorkspaceService,
|
|
36
|
+
{} as IAccessControl,
|
|
37
|
+
{} as FileLockService,
|
|
38
|
+
pendingCommits,
|
|
39
|
+
'knowledge-base',
|
|
40
|
+
events,
|
|
41
|
+
);
|
|
42
|
+
return { svc, git, prs, pendingCommits, emit: events.emit as ReturnType<typeof vi.fn> };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const kinds = (emit: ReturnType<typeof vi.fn>) =>
|
|
46
|
+
emit.mock.calls.map((c) => (c[0] as { kind: string }).kind);
|
|
47
|
+
|
|
48
|
+
describe('WorkflowService.syncWorkspaceFromRemote', () => {
|
|
49
|
+
it('content changed: reports updated, announces the tree and each changed file, drops the CR list cache', async () => {
|
|
50
|
+
const { svc, prs, emit } = build({
|
|
51
|
+
sync: async () => ({ ...PULLED, changedPaths: ['Plugins/x/SKILL.md', 'Docs/a.md'] }),
|
|
52
|
+
});
|
|
53
|
+
const out = await svc.syncWorkspaceFromRemote('ali%2Fx');
|
|
54
|
+
expect(out).toEqual({ branch: 'ali/x', outcome: 'updated', from: 'aaa', to: 'bbb' });
|
|
55
|
+
expect(prs.invalidateListCache).toHaveBeenCalledTimes(1);
|
|
56
|
+
expect(kinds(emit)).toEqual(['fs-tree-changed', 'file-changed', 'file-changed']);
|
|
57
|
+
expect(emit.mock.calls[0][0]).toEqual({
|
|
58
|
+
kind: 'fs-tree-changed',
|
|
59
|
+
workspaceId: 'ali%2Fx',
|
|
60
|
+
branch: 'ali/x',
|
|
61
|
+
});
|
|
62
|
+
// Paths are announced workspace-relative — the shape open tabs hold.
|
|
63
|
+
expect(emit.mock.calls[1][0]).toMatchObject({
|
|
64
|
+
kind: 'file-changed',
|
|
65
|
+
workspaceId: 'ali%2Fx',
|
|
66
|
+
branch: 'ali/x',
|
|
67
|
+
path: 'knowledge-base/Plugins/x/SKILL.md',
|
|
68
|
+
newSha: 'bbb',
|
|
69
|
+
byUserId: 'system',
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('hands git the DECODED id — the spelling the HTTP routes use, so the mutex is shared with a concurrent save', async () => {
|
|
74
|
+
const { svc, git } = build({ sync: async () => PULLED });
|
|
75
|
+
await svc.syncWorkspaceFromRemote('ali%2Fnew-skill');
|
|
76
|
+
expect(git.syncFromRemote).toHaveBeenCalledWith('ali/new-skill');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('HEAD unchanged: up-to-date, nothing announced, nothing invalidated', async () => {
|
|
80
|
+
const { svc, prs, emit } = build({
|
|
81
|
+
sync: async () => ({ before: 'aaa', after: 'aaa', treeChanged: false, changedPaths: [] }),
|
|
82
|
+
});
|
|
83
|
+
expect(await svc.syncWorkspaceFromRemote('main')).toEqual({
|
|
84
|
+
branch: 'main',
|
|
85
|
+
outcome: 'up-to-date',
|
|
86
|
+
to: 'aaa',
|
|
87
|
+
});
|
|
88
|
+
expect(emit).not.toHaveBeenCalled();
|
|
89
|
+
expect(prs.invalidateListCache).not.toHaveBeenCalled();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('HEAD moved across content-identical commits: updated, but nothing announced', async () => {
|
|
93
|
+
const { svc, prs, emit } = build({
|
|
94
|
+
sync: async () => ({ before: 'aaa', after: 'bbb', treeChanged: false, changedPaths: [] }),
|
|
95
|
+
});
|
|
96
|
+
expect(await svc.syncWorkspaceFromRemote('main')).toEqual({
|
|
97
|
+
branch: 'main',
|
|
98
|
+
outcome: 'updated',
|
|
99
|
+
from: 'aaa',
|
|
100
|
+
to: 'bbb',
|
|
101
|
+
});
|
|
102
|
+
expect(emit).not.toHaveBeenCalled();
|
|
103
|
+
expect(prs.invalidateListCache).not.toHaveBeenCalled();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('an unborn clone of an unborn upstream is up to date; one that just received content is updated from null', async () => {
|
|
107
|
+
const empty = build({
|
|
108
|
+
sync: async () => ({ before: null, after: null, treeChanged: false, changedPaths: [] }),
|
|
109
|
+
});
|
|
110
|
+
expect(await empty.svc.syncWorkspaceFromRemote('main')).toEqual({
|
|
111
|
+
branch: 'main',
|
|
112
|
+
outcome: 'up-to-date',
|
|
113
|
+
to: '',
|
|
114
|
+
});
|
|
115
|
+
expect(empty.emit).not.toHaveBeenCalled();
|
|
116
|
+
|
|
117
|
+
const born = build({
|
|
118
|
+
sync: async () => ({ before: null, after: 'bbb', treeChanged: true, changedPaths: ['README.md'] }),
|
|
119
|
+
});
|
|
120
|
+
expect(await born.svc.syncWorkspaceFromRemote('main')).toEqual({
|
|
121
|
+
branch: 'main',
|
|
122
|
+
outcome: 'updated',
|
|
123
|
+
from: null,
|
|
124
|
+
to: 'bbb',
|
|
125
|
+
});
|
|
126
|
+
expect(kinds(born.emit)).toEqual(['fs-tree-changed', 'file-changed']);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('a large change set announces the tree only', async () => {
|
|
130
|
+
const { svc, emit } = build({
|
|
131
|
+
sync: async () => ({ ...PULLED, changedPaths: Array.from({ length: 201 }, (_, i) => `f${i}.md`) }),
|
|
132
|
+
});
|
|
133
|
+
await svc.syncWorkspaceFromRemote('main');
|
|
134
|
+
expect(kinds(emit)).toEqual(['fs-tree-changed']);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('a conflict queues the same recovery as a normal update, and is reported with the files', async () => {
|
|
138
|
+
const { svc, pendingCommits, emit } = build({
|
|
139
|
+
sync: async () => {
|
|
140
|
+
throw new PullRebaseConflictError('main', ['Plugins/x/SKILL.md'], 'CONFLICT (content)');
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
const out = await svc.syncWorkspaceFromRemote('main');
|
|
144
|
+
const message = syncConflictMessage('main', ['Plugins/x/SKILL.md']);
|
|
145
|
+
expect(out).toEqual({
|
|
146
|
+
branch: 'main',
|
|
147
|
+
outcome: 'conflict',
|
|
148
|
+
conflictedPaths: ['Plugins/x/SKILL.md'],
|
|
149
|
+
error: message,
|
|
150
|
+
});
|
|
151
|
+
expect(message).toBe(
|
|
152
|
+
'main is not in sync yet: Plugins/x/SKILL.md changed both in Hexis and on the git host. ' +
|
|
153
|
+
'Recovery is queued; if this stays, open the files on main in Hexis, keep what you want, and save.',
|
|
154
|
+
);
|
|
155
|
+
// The retry → recovery-agent → escalate ladder — one row, never reset by
|
|
156
|
+
// a repeat (the sync can fire on every hook).
|
|
157
|
+
expect(pendingCommits.enqueueIfAbsent).toHaveBeenCalledTimes(1);
|
|
158
|
+
// The branch's banner gets the same sentence, plus the files to open.
|
|
159
|
+
expect(emit).toHaveBeenCalledTimes(1);
|
|
160
|
+
expect(emit.mock.calls[0][0]).toMatchObject({
|
|
161
|
+
kind: 'git-sync-failed',
|
|
162
|
+
workspaceId: 'main',
|
|
163
|
+
branch: 'main',
|
|
164
|
+
reason: message,
|
|
165
|
+
conflictedPaths: ['Plugins/x/SKILL.md'],
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('a conflict in many files reaches the banner with the SAME sentence as the response', async () => {
|
|
170
|
+
const paths = Array.from({ length: 12 }, (_, i) => `Plugins/some-long-plugin-name-${i}/SKILL.md`);
|
|
171
|
+
const { svc, emit } = build({
|
|
172
|
+
sync: async () => {
|
|
173
|
+
throw new PullRebaseConflictError('main', paths, 'CONFLICT');
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
const out = await svc.syncWorkspaceFromRemote('main');
|
|
177
|
+
const message = (out as { error: string }).error;
|
|
178
|
+
expect(message).toContain('and 9 more files');
|
|
179
|
+
expect(emit.mock.calls[0][0]).toMatchObject({ reason: message, conflictedPaths: paths });
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('a second sync of a still-conflicted branch answers the same, and asks for recovery idempotently', async () => {
|
|
183
|
+
let pulls = 0;
|
|
184
|
+
const { svc, pendingCommits } = build({
|
|
185
|
+
sync: async () => {
|
|
186
|
+
pulls++;
|
|
187
|
+
throw new PullRebaseConflictError('main', ['a.md'], 'CONFLICT');
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
const first = await svc.syncWorkspaceFromRemote('main');
|
|
191
|
+
const second = await svc.syncWorkspaceFromRemote('main');
|
|
192
|
+
expect(second).toEqual(first);
|
|
193
|
+
expect(pulls).toBe(2);
|
|
194
|
+
expect(pendingCommits.enqueueIfAbsent).toHaveBeenCalledTimes(2);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('a branch deleted on the host is remote-gone: no banner, no recovery', async () => {
|
|
198
|
+
const { svc, emit, pendingCommits } = build({
|
|
199
|
+
sync: async () => {
|
|
200
|
+
throw new RemoteBranchGoneError('ali/x');
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
expect(await svc.syncWorkspaceFromRemote('ali%2Fx')).toEqual({ branch: 'ali/x', outcome: 'remote-gone' });
|
|
204
|
+
expect(emit).not.toHaveBeenCalled();
|
|
205
|
+
expect(pendingCommits.enqueueIfAbsent).not.toHaveBeenCalled();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('any other pull failure is an error outcome with a sanitised message, and raises the banner', async () => {
|
|
209
|
+
const { svc, emit } = build({
|
|
210
|
+
sync: async () => {
|
|
211
|
+
throw new Error("fatal: unable to access 'https://x-access-token:ghp_secret123@host/repo': timed out");
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
const out = await svc.syncWorkspaceFromRemote('main');
|
|
215
|
+
expect(out).toMatchObject({ branch: 'main', outcome: 'error' });
|
|
216
|
+
const error = (out as { error: string }).error;
|
|
217
|
+
expect(error).not.toContain('ghp_secret123');
|
|
218
|
+
expect(error).toContain('timed out');
|
|
219
|
+
expect(kinds(emit)).toEqual(['git-sync-failed']);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('a clean pull clears a banner an earlier failure raised', async () => {
|
|
223
|
+
let fail = true;
|
|
224
|
+
const { svc, emit } = build({
|
|
225
|
+
sync: async () => {
|
|
226
|
+
if (fail) throw new Error('could not read from remote');
|
|
227
|
+
return { before: 'aaa', after: 'aaa', treeChanged: false, changedPaths: [] };
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
await svc.syncWorkspaceFromRemote('main');
|
|
231
|
+
fail = false;
|
|
232
|
+
await svc.syncWorkspaceFromRemote('main');
|
|
233
|
+
expect(kinds(emit)).toEqual(['git-sync-failed', 'git-sync-recovered']);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('a failing announcement is an error outcome, never a throw, and raises no banner', async () => {
|
|
237
|
+
const { svc, prs, emit } = build({ sync: async () => PULLED });
|
|
238
|
+
(prs.invalidateListCache as ReturnType<typeof vi.fn>).mockImplementation(() => {
|
|
239
|
+
throw new Error('cache exploded');
|
|
240
|
+
});
|
|
241
|
+
await expect(svc.syncWorkspaceFromRemote('main')).resolves.toMatchObject({ outcome: 'error' });
|
|
242
|
+
expect(kinds(emit)).toEqual([]);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe('WorkflowService.retireRemoteGoneClone', () => {
|
|
247
|
+
function buildRetire(opts: { cloned: boolean; stillOnOrigin: boolean; bootstrapping?: boolean }) {
|
|
248
|
+
const git = {
|
|
249
|
+
remoteBranchExists: vi.fn(async () => opts.stillOnOrigin),
|
|
250
|
+
createBranch: vi.fn(async (_ws: string, name: string) => ({ name })),
|
|
251
|
+
} as unknown as GitService;
|
|
252
|
+
const workspaceService = {
|
|
253
|
+
hasBootstrappedWorkspace: vi.fn(async () => opts.cloned),
|
|
254
|
+
isBootstrapInFlight: vi.fn(() => opts.bootstrapping === true),
|
|
255
|
+
deleteWorkspace: vi.fn(async () => {}),
|
|
256
|
+
} as unknown as WorkspaceService;
|
|
257
|
+
const svc = new WorkflowService(
|
|
258
|
+
{} as Database, git, {} as PullRequestService, {} as IReviewWorkflowService,
|
|
259
|
+
workspaceService, {} as IAccessControl, {} as FileLockService, {} as PendingCommitsService,
|
|
260
|
+
'knowledge-base',
|
|
261
|
+
);
|
|
262
|
+
return { svc, git, workspaceService };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
it('removes the clone when origin still lacks the branch, asking origin again rather than trusting the pull', async () => {
|
|
266
|
+
const { svc, git, workspaceService } = buildRetire({ cloned: true, stillOnOrigin: false });
|
|
267
|
+
expect(await svc.retireRemoteGoneClone('ali%2Fx')).toBe(true);
|
|
268
|
+
expect(git.remoteBranchExists).toHaveBeenCalledWith('ali/x', 'ali/x');
|
|
269
|
+
expect(workspaceService.deleteWorkspace).toHaveBeenCalledWith('ali%2Fx');
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('keeps the clone when the branch is back on origin', async () => {
|
|
273
|
+
const { svc, workspaceService } = buildRetire({ cloned: true, stillOnOrigin: true });
|
|
274
|
+
expect(await svc.retireRemoteGoneClone('ali%2Fx')).toBe(false);
|
|
275
|
+
expect(workspaceService.deleteWorkspace).not.toHaveBeenCalled();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('does nothing when there is no clone any more', async () => {
|
|
279
|
+
const { svc, git, workspaceService } = buildRetire({ cloned: false, stillOnOrigin: false });
|
|
280
|
+
expect(await svc.retireRemoteGoneClone('ali%2Fx')).toBe(false);
|
|
281
|
+
expect(git.remoteBranchExists).not.toHaveBeenCalled();
|
|
282
|
+
expect(workspaceService.deleteWorkspace).not.toHaveBeenCalled();
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('waits for a createBranch of the same name that holds the lifecycle lock', async () => {
|
|
286
|
+
// Observed through the public API alone: createBranch holds the lock for
|
|
287
|
+
// as long as its git call is pending, and the retirement's first read
|
|
288
|
+
// must not happen until that call has released it.
|
|
289
|
+
const order: string[] = [];
|
|
290
|
+
const { svc, git, workspaceService } = buildRetire({ cloned: true, stillOnOrigin: false });
|
|
291
|
+
let release!: () => void;
|
|
292
|
+
(git.createBranch as ReturnType<typeof vi.fn>).mockImplementation(
|
|
293
|
+
() =>
|
|
294
|
+
new Promise((res) => {
|
|
295
|
+
release = () => {
|
|
296
|
+
order.push('create-released');
|
|
297
|
+
res({ name: 'ali/x' });
|
|
298
|
+
};
|
|
299
|
+
}),
|
|
300
|
+
);
|
|
301
|
+
(workspaceService.hasBootstrappedWorkspace as ReturnType<typeof vi.fn>).mockImplementation(async () => {
|
|
302
|
+
order.push('retire-read');
|
|
303
|
+
return true;
|
|
304
|
+
});
|
|
305
|
+
const creating = svc.createBranch('main', 'ali/x');
|
|
306
|
+
const retiring = svc.retireRemoteGoneClone('ali%2Fx');
|
|
307
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
308
|
+
expect(order).toEqual([]);
|
|
309
|
+
release();
|
|
310
|
+
await creating;
|
|
311
|
+
await retiring;
|
|
312
|
+
expect(order).toEqual(['create-released', 'retire-read']);
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
describe('WorkflowService.retireRemoteGoneClone — what can bring the branch back', () => {
|
|
317
|
+
it('backs off while a clone of the branch is being bootstrapped', async () => {
|
|
318
|
+
const git = { remoteBranchExists: vi.fn(async () => false) } as unknown as GitService;
|
|
319
|
+
const workspaceService = {
|
|
320
|
+
hasBootstrappedWorkspace: vi.fn(async () => true),
|
|
321
|
+
isBootstrapInFlight: vi.fn(() => true),
|
|
322
|
+
deleteWorkspace: vi.fn(async () => {}),
|
|
323
|
+
} as unknown as WorkspaceService;
|
|
324
|
+
const svc = new WorkflowService(
|
|
325
|
+
{} as Database, git, {} as PullRequestService, {} as IReviewWorkflowService,
|
|
326
|
+
workspaceService, {} as IAccessControl, {} as FileLockService, {} as PendingCommitsService,
|
|
327
|
+
'knowledge-base',
|
|
328
|
+
);
|
|
329
|
+
expect(await svc.retireRemoteGoneClone('ali%2Fx')).toBe(false);
|
|
330
|
+
expect(workspaceService.deleteWorkspace).not.toHaveBeenCalled();
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('createBranch holds the same lifecycle lock, so a recreate cannot slip between the origin check and the delete', async () => {
|
|
334
|
+
const order: string[] = [];
|
|
335
|
+
const git = {
|
|
336
|
+
remoteBranchExists: vi.fn(async () => {
|
|
337
|
+
order.push('retire-checks-origin');
|
|
338
|
+
return false;
|
|
339
|
+
}),
|
|
340
|
+
createBranch: vi.fn(async (_ws: string, name: string) => {
|
|
341
|
+
order.push('create');
|
|
342
|
+
return { name, isProtected: false, ahead: 0, behind: 0, hasRemote: true };
|
|
343
|
+
}),
|
|
344
|
+
} as unknown as GitService;
|
|
345
|
+
const workspaceService = {
|
|
346
|
+
hasBootstrappedWorkspace: vi.fn(async () => true),
|
|
347
|
+
isBootstrapInFlight: vi.fn(() => false),
|
|
348
|
+
deleteWorkspace: vi.fn(async () => {
|
|
349
|
+
order.push('retire-deletes');
|
|
350
|
+
}),
|
|
351
|
+
} as unknown as WorkspaceService;
|
|
352
|
+
const svc = new WorkflowService(
|
|
353
|
+
{} as Database, git, {} as PullRequestService, {} as IReviewWorkflowService,
|
|
354
|
+
workspaceService, {} as IAccessControl, {} as FileLockService, {} as PendingCommitsService,
|
|
355
|
+
'knowledge-base',
|
|
356
|
+
);
|
|
357
|
+
// Retirement enters first; a createBranch for the same name arrives while
|
|
358
|
+
// it holds the lock and must wait until the delete has happened — never
|
|
359
|
+
// land between the check and the delete.
|
|
360
|
+
const retiring = svc.retireRemoteGoneClone('ali%2Fx');
|
|
361
|
+
const creating = svc.createBranch('main', 'ali/x');
|
|
362
|
+
await Promise.all([retiring, creating]);
|
|
363
|
+
expect(order).toEqual(['retire-checks-origin', 'retire-deletes', 'create']);
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
describe('WorkflowService.syncWorkspaceFromRemote — an announcement is owed until delivered', () => {
|
|
368
|
+
it('a retry after a failed announcement delivers it even though HEAD did not move again', async () => {
|
|
369
|
+
let head = 'bbb';
|
|
370
|
+
let pulls = 0;
|
|
371
|
+
const { svc, prs, emit } = build({
|
|
372
|
+
sync: async () => {
|
|
373
|
+
pulls++;
|
|
374
|
+
// First pull moves HEAD and changes content; later pulls find nothing new.
|
|
375
|
+
if (pulls === 1) return { before: 'aaa', after: 'bbb', treeChanged: true, changedPaths: ['Docs/a.md'] };
|
|
376
|
+
return { before: head, after: head, treeChanged: false, changedPaths: [] };
|
|
377
|
+
},
|
|
378
|
+
});
|
|
379
|
+
(prs.invalidateListCache as ReturnType<typeof vi.fn>).mockImplementationOnce(() => {
|
|
380
|
+
throw new Error('cache exploded');
|
|
381
|
+
});
|
|
382
|
+
// 1. The pull lands, the announcement does not: error, and nothing sent.
|
|
383
|
+
expect(await svc.syncWorkspaceFromRemote('main')).toMatchObject({ outcome: 'error' });
|
|
384
|
+
expect(emit).not.toHaveBeenCalled();
|
|
385
|
+
|
|
386
|
+
// 2. The retry finds HEAD where it left it — and still announces what is owed.
|
|
387
|
+
head = 'bbb';
|
|
388
|
+
expect(await svc.syncWorkspaceFromRemote('main')).toEqual({ branch: 'main', outcome: 'up-to-date', to: 'bbb' });
|
|
389
|
+
expect(kinds(emit)).toEqual(['fs-tree-changed', 'file-changed']);
|
|
390
|
+
expect(emit.mock.calls[1][0]).toMatchObject({ path: 'knowledge-base/Docs/a.md', newSha: 'bbb' });
|
|
391
|
+
|
|
392
|
+
// 3. Delivered once: a further sync announces nothing.
|
|
393
|
+
await svc.syncWorkspaceFromRemote('main');
|
|
394
|
+
expect(emit).toHaveBeenCalledTimes(2);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('an owed announcement merges with the next pull\'s own changes', async () => {
|
|
398
|
+
let pulls = 0;
|
|
399
|
+
const { svc, prs, emit } = build({
|
|
400
|
+
sync: async () => {
|
|
401
|
+
pulls++;
|
|
402
|
+
if (pulls === 1) return { before: 'aaa', after: 'bbb', treeChanged: true, changedPaths: ['Docs/a.md'] };
|
|
403
|
+
return { before: 'bbb', after: 'ccc', treeChanged: true, changedPaths: ['Docs/b.md'] };
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
(prs.invalidateListCache as ReturnType<typeof vi.fn>).mockImplementationOnce(() => {
|
|
407
|
+
throw new Error('cache exploded');
|
|
408
|
+
});
|
|
409
|
+
await svc.syncWorkspaceFromRemote('main');
|
|
410
|
+
expect(await svc.syncWorkspaceFromRemote('main')).toEqual({ branch: 'main', outcome: 'updated', from: 'bbb', to: 'ccc' });
|
|
411
|
+
const paths = emit.mock.calls.filter((c) => (c[0] as { kind: string }).kind === 'file-changed').map((c) => (c[0] as { path: string }).path);
|
|
412
|
+
expect(paths.sort()).toEqual(['knowledge-base/Docs/a.md', 'knowledge-base/Docs/b.md']);
|
|
413
|
+
expect(emit.mock.calls[1][0]).toMatchObject({ newSha: 'ccc' });
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
describe('WorkflowService.syncWorkspaceFromRemote — a debt dies with its clone', () => {
|
|
418
|
+
it('an announcement owed to a branch the host deleted is not replayed against a branch recreated under that name', async () => {
|
|
419
|
+
let pulls = 0;
|
|
420
|
+
const { svc, prs, emit } = build({
|
|
421
|
+
sync: async () => {
|
|
422
|
+
pulls++;
|
|
423
|
+
if (pulls === 1) return { before: 'aaa', after: 'bbb', treeChanged: true, changedPaths: ['Old/a.md'] };
|
|
424
|
+
if (pulls === 2) throw new RemoteBranchGoneError('ali/x');
|
|
425
|
+
// The recreated branch: a fresh clone with nothing new to announce.
|
|
426
|
+
return { before: 'ccc', after: 'ccc', treeChanged: false, changedPaths: [] };
|
|
427
|
+
},
|
|
428
|
+
});
|
|
429
|
+
(prs.invalidateListCache as ReturnType<typeof vi.fn>).mockImplementationOnce(() => {
|
|
430
|
+
throw new Error('cache exploded');
|
|
431
|
+
});
|
|
432
|
+
expect(await svc.syncWorkspaceFromRemote('ali%2Fx')).toMatchObject({ outcome: 'error' });
|
|
433
|
+
expect(await svc.syncWorkspaceFromRemote('ali%2Fx')).toEqual({ branch: 'ali/x', outcome: 'remote-gone' });
|
|
434
|
+
expect(await svc.syncWorkspaceFromRemote('ali%2Fx')).toEqual({ branch: 'ali/x', outcome: 'up-to-date', to: 'ccc' });
|
|
435
|
+
expect(emit).not.toHaveBeenCalled();
|
|
436
|
+
});
|
|
437
|
+
});
|
|
@@ -119,7 +119,7 @@ describe('post-merge refresh of the target branch workspace', () => {
|
|
|
119
119
|
expect(merged.kind).toBe('merged');
|
|
120
120
|
|
|
121
121
|
// 2. The post-merge refresh of the TARGET branch's own workspace succeeds.
|
|
122
|
-
await expect(git.pull(baseWsId)).resolves.
|
|
122
|
+
await expect(git.pull(baseWsId)).resolves.toEqual({ treeChanged: true });
|
|
123
123
|
|
|
124
124
|
// The workspace is at origin's head, and serves the merged file.
|
|
125
125
|
const originHead = await gitOut(baseRepo, ['rev-parse', `origin/${BASE}`]);
|
|
@@ -203,7 +203,7 @@ describe('post-merge refresh of the target branch workspace', () => {
|
|
|
203
203
|
// ~40% of rounds hit the fatal pre-fix (measured), so 20 rounds makes a
|
|
204
204
|
// pre-fix regression a practical certainty while staying quick.
|
|
205
205
|
for (let round = 0; round < 20; round += 1) {
|
|
206
|
-
await expect(git.pull(baseWsId)).resolves.
|
|
206
|
+
await expect(git.pull(baseWsId)).resolves.toEqual({ treeChanged: round === 0 });
|
|
207
207
|
}
|
|
208
208
|
} finally {
|
|
209
209
|
stop = true;
|
|
@@ -268,7 +268,7 @@ describe('post-merge refresh of the target branch workspace', () => {
|
|
|
268
268
|
expect(upstreamLog.split('\n')).toContain(latestSource);
|
|
269
269
|
|
|
270
270
|
// ...and the post-merge refresh of the base workspace serves that revision.
|
|
271
|
-
await expect(git.pull(baseWsId)).resolves.
|
|
271
|
+
await expect(git.pull(baseWsId)).resolves.toEqual({ treeChanged: true });
|
|
272
272
|
const revision = await fs.readFile(path.join(baseRepo, 'revision.md'), 'utf8');
|
|
273
273
|
expect(revision.replace(/\r\n/g, '\n')).toBe('second revision\n');
|
|
274
274
|
});
|
|
@@ -88,7 +88,7 @@ describe('GitService.pull', () => {
|
|
|
88
88
|
'knowledge-base',
|
|
89
89
|
);
|
|
90
90
|
|
|
91
|
-
await expect(svc.pull(workspaceId)).resolves.
|
|
91
|
+
await expect(svc.pull(workspaceId)).resolves.toEqual({ treeChanged: true });
|
|
92
92
|
|
|
93
93
|
// The pull landed origin's commit...
|
|
94
94
|
const head = await gitOut(repo, ['log', '-1', '--pretty=%s']);
|
|
@@ -131,7 +131,7 @@ describe('GitService.pull', () => {
|
|
|
131
131
|
'knowledge-base',
|
|
132
132
|
);
|
|
133
133
|
|
|
134
|
-
await expect(svc.pull(workspaceId)).resolves.
|
|
134
|
+
await expect(svc.pull(workspaceId)).resolves.toEqual({ treeChanged: true });
|
|
135
135
|
|
|
136
136
|
// Origin's commit landed...
|
|
137
137
|
expect(await gitOut(repo, ['log', '-1', '--pretty=%s'])).toBe('remote change');
|
|
@@ -177,7 +177,7 @@ describe('GitService.pull', () => {
|
|
|
177
177
|
'knowledge-base',
|
|
178
178
|
);
|
|
179
179
|
|
|
180
|
-
await expect(svc.pull(workspaceId)).resolves.
|
|
180
|
+
await expect(svc.pull(workspaceId)).resolves.toEqual({ treeChanged: true });
|
|
181
181
|
|
|
182
182
|
// Landed origin's commit — not the stray head the hostile file names.
|
|
183
183
|
expect(await gitOut(repo, ['log', '-1', '--pretty=%s'])).toBe('remote change');
|
|
@@ -222,7 +222,7 @@ describe('GitService.pull', () => {
|
|
|
222
222
|
'knowledge-base',
|
|
223
223
|
);
|
|
224
224
|
|
|
225
|
-
await expect(svc.pull(workspaceId)).resolves.
|
|
225
|
+
await expect(svc.pull(workspaceId)).resolves.toEqual({ treeChanged: true });
|
|
226
226
|
|
|
227
227
|
// Both commits are present — the local commit was rebased onto origin's.
|
|
228
228
|
const log = await gitOut(repo, ['log', '--pretty=%s']);
|
|
@@ -236,6 +236,49 @@ describe('GitService.pull', () => {
|
|
|
236
236
|
expect(dash).toBe('v1\n');
|
|
237
237
|
});
|
|
238
238
|
|
|
239
|
+
// `treeChanged` is what keeps an "already up to date" sync from broadcasting
|
|
240
|
+
// a tree change that did not happen (every catalog cache dropped, every
|
|
241
|
+
// attached browser refetching its file tree). The fixture pulls above all
|
|
242
|
+
// land a commit and assert `treeChanged: true`; this is the other half.
|
|
243
|
+
it('reports treeChanged: false when origin has nothing new', async () => {
|
|
244
|
+
const { repo } = await seedWorkspace(root, workspaceId);
|
|
245
|
+
const headBefore = await gitOut(repo, ['rev-parse', 'HEAD']);
|
|
246
|
+
|
|
247
|
+
const svc = new GitService(
|
|
248
|
+
stubWorkspaceService({ [workspaceId]: path.join(root, workspaceId) }),
|
|
249
|
+
stubWorkflowHooks(),
|
|
250
|
+
'knowledge-base',
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
await expect(svc.pull(workspaceId)).resolves.toEqual({ treeChanged: false });
|
|
254
|
+
expect(await gitOut(repo, ['rev-parse', 'HEAD'])).toBe(headBefore);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// TREE ids are compared, not commit ids: a pull that lands only an empty
|
|
258
|
+
// commit moves HEAD but changes no content, and must not read as a tree
|
|
259
|
+
// change — the consumers of this signal (catalog drops, browser file-tree
|
|
260
|
+
// reloads) care about bytes, not refs.
|
|
261
|
+
it('reports treeChanged: false for a pull that lands only an empty commit', async () => {
|
|
262
|
+
const { upstream, repo } = await seedWorkspace(root, workspaceId);
|
|
263
|
+
const pusher = path.join(root, '.empty-pusher');
|
|
264
|
+
await runGit(root, ['clone', upstream, pusher]);
|
|
265
|
+
await runGit(pusher, ['commit', '--allow-empty', '-m', 'empty remote commit']);
|
|
266
|
+
await runGit(pusher, ['push', 'origin', 'target-company-state']);
|
|
267
|
+
const headBefore = await gitOut(repo, ['rev-parse', 'HEAD']);
|
|
268
|
+
|
|
269
|
+
const svc = new GitService(
|
|
270
|
+
stubWorkspaceService({ [workspaceId]: path.join(root, workspaceId) }),
|
|
271
|
+
stubWorkflowHooks(),
|
|
272
|
+
'knowledge-base',
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
await expect(svc.pull(workspaceId)).resolves.toEqual({ treeChanged: false });
|
|
276
|
+
// HEAD DID move — the empty commit landed — which is exactly why commit
|
|
277
|
+
// ids would have been the wrong thing to compare.
|
|
278
|
+
expect(await gitOut(repo, ['rev-parse', 'HEAD'])).not.toBe(headBefore);
|
|
279
|
+
expect(await gitOut(repo, ['log', '-1', '--pretty=%s'])).toBe('empty remote commit');
|
|
280
|
+
});
|
|
281
|
+
|
|
239
282
|
// The production stuck-workspace state: a local commit and an origin commit
|
|
240
283
|
// both rewrote the same file, so replaying the local commit conflicts. The
|
|
241
284
|
// pull must (a) abort the rebase so the clone isn't left in a detached
|