@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,287 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import { DEFAULT_BRANCH, type AuthUser } from '@bevel-software/platform-shared';
|
|
6
|
+
|
|
7
|
+
import type { WorkspaceService } from '../../workspace/workspace.service.js';
|
|
8
|
+
import { AccessControlService } from '../../access/access-control.service.js';
|
|
9
|
+
import { SkillService } from '../../skills/skills.service.js';
|
|
10
|
+
import { workspaceIdForBranch } from '../../../shared/workspace-id.js';
|
|
11
|
+
import { PluginLinkIndex } from '../plugin-links.js';
|
|
12
|
+
import { PluginLinksService, PluginLinkError } from '../plugin-links.service.js';
|
|
13
|
+
import type { ProvisionCommitDriver } from '../plugin-provision.service.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Linking end to end over a real tree: the real resolver decides who may
|
|
17
|
+
* link and who may read afterwards, the real catalog resolves roots, and the
|
|
18
|
+
* commit driver is the only thing stubbed (it records what would land).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const KB_DIR = 'knowledge-base';
|
|
22
|
+
const wsId = workspaceIdForBranch(DEFAULT_BRANCH);
|
|
23
|
+
|
|
24
|
+
const manager: AuthUser = { id: 'u-mia', email: 'mia@x.io', name: 'Mia' } as AuthUser;
|
|
25
|
+
const editor: AuthUser = { id: 'u-eve', email: 'eve@x.io', name: 'Eve' } as AuthUser;
|
|
26
|
+
const member: AuthUser = { id: 'u-sam', email: 'sam@x.io', name: 'Sam' } as AuthUser;
|
|
27
|
+
|
|
28
|
+
const ROLES_YAML = `roles:
|
|
29
|
+
Admin:
|
|
30
|
+
- admin@x.io
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
describe('PluginLinksService', () => {
|
|
34
|
+
let root: string;
|
|
35
|
+
let repo: string;
|
|
36
|
+
let commits: string[];
|
|
37
|
+
let access: AccessControlService;
|
|
38
|
+
let skills: SkillService;
|
|
39
|
+
let index: PluginLinkIndex;
|
|
40
|
+
let svc: PluginLinksService;
|
|
41
|
+
|
|
42
|
+
const write = async (rel: string, text: string) => {
|
|
43
|
+
const abs = path.join(repo, rel);
|
|
44
|
+
await fs.mkdir(path.dirname(abs), { recursive: true });
|
|
45
|
+
await fs.writeFile(abs, text);
|
|
46
|
+
};
|
|
47
|
+
const read = (rel: string) => fs.readFile(path.join(repo, rel), 'utf-8');
|
|
48
|
+
|
|
49
|
+
beforeEach(async () => {
|
|
50
|
+
root = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-links-'));
|
|
51
|
+
repo = path.join(root, wsId, KB_DIR);
|
|
52
|
+
commits = [];
|
|
53
|
+
const workspaceService = {
|
|
54
|
+
getOrCreateForBranch: async () => ({ id: wsId }),
|
|
55
|
+
getWorkspacePath: async (id: string) => path.join(root, id),
|
|
56
|
+
readFile: async (id: string, rel: string) => fs.readFile(path.join(root, id, rel), 'utf-8'),
|
|
57
|
+
writeFile: async (id: string, rel: string, text: string) => {
|
|
58
|
+
const abs = path.join(root, id, rel);
|
|
59
|
+
await fs.mkdir(path.dirname(abs), { recursive: true });
|
|
60
|
+
await fs.writeFile(abs, text);
|
|
61
|
+
},
|
|
62
|
+
ensureRemotesFetched: async () => undefined,
|
|
63
|
+
} as unknown as WorkspaceService;
|
|
64
|
+
const driver: ProvisionCommitDriver = {
|
|
65
|
+
runPendingCommit: async (_ws, _branch, target) => {
|
|
66
|
+
commits.push(target);
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
await write('roles.yaml', ROLES_YAML);
|
|
71
|
+
await write('access.md', '---\nwrite:\n - Admin\n---\n');
|
|
72
|
+
// GTM: Mia manages, Sam is a member.
|
|
73
|
+
await write(
|
|
74
|
+
'Plugins/GTM/access.md',
|
|
75
|
+
'---\nread:\n - everyone\n---\nread:\n - Sam <sam@x.io>\nwrite:\n - Mia <mia@x.io>\nowner:\n - Mia <mia@x.io>\n',
|
|
76
|
+
);
|
|
77
|
+
await write('Plugins/GTM/plugin.json', '{\n "name": "gtm",\n "version": "1.0.0"\n}\n');
|
|
78
|
+
// A shared scope Eve edits, holding two skills.
|
|
79
|
+
await write('Skills/Eng/access.md', '---\n---\nwrite:\n - Eve <eve@x.io>\n');
|
|
80
|
+
await write('Skills/Eng/deploy/SKILL.md', '---\ndescription: Ship it.\n---\n');
|
|
81
|
+
await write('Skills/Eng/rollback/SKILL.md', '---\ndescription: Undo it.\n---\n');
|
|
82
|
+
|
|
83
|
+
access = new AccessControlService(workspaceService, KB_DIR);
|
|
84
|
+
skills = new SkillService(workspaceService, access, KB_DIR);
|
|
85
|
+
index = new PluginLinkIndex(workspaceService, skills, access, KB_DIR);
|
|
86
|
+
svc = new PluginLinksService(workspaceService, driver, access, skills, index, KB_DIR);
|
|
87
|
+
});
|
|
88
|
+
afterEach(() => fs.rm(root, { recursive: true, force: true }));
|
|
89
|
+
|
|
90
|
+
it('links a skill: manifest entry + read/write grants for the plugin principals, both committed', async () => {
|
|
91
|
+
// Mia manages GTM but cannot edit Skills/Eng — Eve grants her write first.
|
|
92
|
+
await write('Skills/Eng/access.md', '---\n---\nwrite:\n - Eve <eve@x.io>\n - Mia <mia@x.io>\n');
|
|
93
|
+
access.invalidate(wsId);
|
|
94
|
+
|
|
95
|
+
const result = await svc.link(manager, 'gtm', 'Skills/Eng/deploy');
|
|
96
|
+
expect(result).toEqual({ root: 'Skills/Eng/deploy', skills: ['Skills/Eng/deploy'] });
|
|
97
|
+
|
|
98
|
+
const manifest = JSON.parse(await read('Plugins/GTM/plugin.json'));
|
|
99
|
+
expect(manifest.version).toBe('1.0.0'); // untouched
|
|
100
|
+
expect(manifest.extensions['software.bevel.hexis'].skills).toEqual(['Skills/Eng/deploy']);
|
|
101
|
+
const rules = await read('Skills/Eng/deploy/access.md');
|
|
102
|
+
expect(rules).toContain('plugin/gtm/read');
|
|
103
|
+
expect(rules).toContain('plugin/gtm/write');
|
|
104
|
+
expect(commits).toEqual([
|
|
105
|
+
`${KB_DIR}/Plugins/GTM/plugin.json`,
|
|
106
|
+
`${KB_DIR}/Skills/Eng/deploy/access.md`,
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
// The point of it all: Sam, a GTM member, can now read the skill; Mia can edit it.
|
|
110
|
+
expect(await access.canRead(wsId, member.email, 'Skills/Eng/deploy/SKILL.md')).toBe(true);
|
|
111
|
+
expect(await access.canWrite(wsId, manager.email, 'Skills/Eng/deploy/SKILL.md')).toBe(true);
|
|
112
|
+
// And the index reports the membership as linked and granted.
|
|
113
|
+
expect(await index.pluginsOf('Skills/Eng/deploy')).toEqual([{ name: 'gtm', linked: true, granted: true }]);
|
|
114
|
+
expect(await index.pluginsOf('Skills/Eng/rollback')).toEqual([]);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('a plugin whose folder is not its own slug links, re-links and unlinks through ONE spelling', async () => {
|
|
118
|
+
// The folder "Sales Team" is only where the plugin lives; its manifest
|
|
119
|
+
// name "sales-team" is the plugin. Every grant, comparison and revocation
|
|
120
|
+
// goes through that one key, so a second link adds nothing and an unlink
|
|
121
|
+
// finds what it wrote.
|
|
122
|
+
await write(
|
|
123
|
+
'Plugins/Sales Team/access.md',
|
|
124
|
+
'---\nread:\n - everyone\n---\nread:\n - Sam <sam@x.io>\nwrite:\n - Mia <mia@x.io>\nowner:\n - Mia <mia@x.io>\n',
|
|
125
|
+
);
|
|
126
|
+
await write('Plugins/Sales Team/plugin.json', '{"name":"sales-team"}');
|
|
127
|
+
await write('Skills/Eng/access.md', '---\n---\nwrite:\n - Eve <eve@x.io>\n - Mia <mia@x.io>\n');
|
|
128
|
+
access.invalidate(wsId);
|
|
129
|
+
|
|
130
|
+
await svc.link(manager, 'sales-team', 'Skills/Eng/deploy');
|
|
131
|
+
await svc.link(manager, 'sales-team', 'Skills/Eng/deploy');
|
|
132
|
+
const rules = await read('Skills/Eng/deploy/access.md');
|
|
133
|
+
expect(rules.match(/plugin\/sales-team\/read/g)).toHaveLength(1);
|
|
134
|
+
expect(rules.match(/plugin\/sales-team\/write/g)).toHaveLength(1);
|
|
135
|
+
expect(await access.canRead(wsId, member.email, 'Skills/Eng/deploy/SKILL.md')).toBe(true);
|
|
136
|
+
|
|
137
|
+
await svc.unlink(manager, 'sales-team', 'Skills/Eng/deploy');
|
|
138
|
+
expect(await read('Skills/Eng/deploy/access.md')).not.toContain('plugin/sales-team');
|
|
139
|
+
access.invalidate(wsId);
|
|
140
|
+
expect(await access.canRead(wsId, member.email, 'Skills/Eng/deploy/SKILL.md')).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('two plugins linking one root at once both land their grants — the root is locked, not only the plugin', async () => {
|
|
144
|
+
await write(
|
|
145
|
+
'Plugins/Ops/access.md',
|
|
146
|
+
'---\nread:\n - everyone\n---\nread:\n - Sam <sam@x.io>\nwrite:\n - Mia <mia@x.io>\nowner:\n - Mia <mia@x.io>\n',
|
|
147
|
+
);
|
|
148
|
+
await write('Plugins/Ops/plugin.json', '{"name":"ops"}');
|
|
149
|
+
await write('Skills/Eng/access.md', '---\n---\nwrite:\n - Eve <eve@x.io>\n - Mia <mia@x.io>\n');
|
|
150
|
+
access.invalidate(wsId);
|
|
151
|
+
|
|
152
|
+
await Promise.all([svc.link(manager, 'gtm', 'Skills/Eng'), svc.link(manager, 'ops', 'Skills/Eng')]);
|
|
153
|
+
const rules = await read('Skills/Eng/access.md');
|
|
154
|
+
expect(rules).toContain('plugin/gtm/read');
|
|
155
|
+
expect(rules).toContain('plugin/ops/read');
|
|
156
|
+
expect(rules).toContain('plugin/gtm/write');
|
|
157
|
+
expect(rules).toContain('plugin/ops/write');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('refuses with needs-skill-write when the manager cannot edit the skill\'s rules', async () => {
|
|
161
|
+
await expect(svc.link(manager, 'gtm', 'Skills/Eng/deploy')).rejects.toMatchObject({
|
|
162
|
+
status: 409,
|
|
163
|
+
payload: { kind: 'needs-skill-write' },
|
|
164
|
+
});
|
|
165
|
+
expect(commits).toEqual([]);
|
|
166
|
+
expect(await fs.readFile(path.join(repo, 'Plugins/GTM/plugin.json'), 'utf-8')).not.toContain('skills');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('a folder of skills links every skill beneath it with one grant on the folder', async () => {
|
|
170
|
+
await write('Skills/Eng/access.md', '---\n---\nwrite:\n - Eve <eve@x.io>\n - Mia <mia@x.io>\n');
|
|
171
|
+
access.invalidate(wsId);
|
|
172
|
+
const result = await svc.link(manager, 'gtm', 'Skills/Eng');
|
|
173
|
+
expect(result.skills.sort()).toEqual(['Skills/Eng/deploy', 'Skills/Eng/rollback']);
|
|
174
|
+
expect(await read('Skills/Eng/access.md')).toContain('plugin/gtm/read');
|
|
175
|
+
expect(await access.canRead(wsId, member.email, 'Skills/Eng/rollback/SKILL.md')).toBe(true);
|
|
176
|
+
const m = await index.membership();
|
|
177
|
+
expect(m.byPlugin.get('gtm')?.linkedSkills.sort()).toEqual(['Skills/Eng/deploy', 'Skills/Eng/rollback']);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('is fail-closed on the plugin side: a non-manager, or an unknown plugin, gets the same 404', async () => {
|
|
181
|
+
await expect(svc.link(member, 'gtm', 'Skills/Eng/deploy')).rejects.toMatchObject({ status: 404 });
|
|
182
|
+
await expect(svc.link(manager, 'Ghost', 'Skills/Eng/deploy')).rejects.toMatchObject({ status: 404 });
|
|
183
|
+
await expect(svc.link(manager, 'personal-abc', 'Skills/Eng/deploy')).rejects.toMatchObject({ status: 404 });
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('rejects a root that holds no released skill, and a path that could escape the repo', async () => {
|
|
187
|
+
await write('Skills/Eng/access.md', '---\n---\nwrite:\n - Mia <mia@x.io>\n');
|
|
188
|
+
access.invalidate(wsId);
|
|
189
|
+
await expect(svc.link(manager, 'gtm', 'Skills/Nowhere')).rejects.toMatchObject({
|
|
190
|
+
status: 422,
|
|
191
|
+
payload: { kind: 'no-skills' },
|
|
192
|
+
});
|
|
193
|
+
await expect(svc.link(manager, 'gtm', '../etc')).rejects.toMatchObject({ status: 422, payload: { kind: 'bad-root' } });
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('refuses a root that holds a retired skill anywhere beneath it — the grant would reach it', async () => {
|
|
197
|
+
await write('Skills/Eng/access.md', '---\n---\nwrite:\n - Mia <mia@x.io>\n');
|
|
198
|
+
await write('Skills/Eng/old-deploy/SKILL.md', '---\ndescription: Gone.\nmetadata:\n lifecycle: retired\n---\n');
|
|
199
|
+
access.invalidate(wsId);
|
|
200
|
+
skills.invalidate();
|
|
201
|
+
|
|
202
|
+
await expect(svc.link(manager, 'gtm', 'Skills/Eng')).rejects.toMatchObject({
|
|
203
|
+
status: 422,
|
|
204
|
+
payload: { kind: 'retired-skills', retired: ['Skills/Eng/old-deploy'] },
|
|
205
|
+
});
|
|
206
|
+
await expect(svc.link(manager, 'gtm', 'Skills/Eng/old-deploy')).rejects.toMatchObject({ status: 422 });
|
|
207
|
+
// The active skill on its own is fine.
|
|
208
|
+
await svc.link(manager, 'gtm', 'Skills/Eng/deploy');
|
|
209
|
+
expect(await read('Skills/Eng/deploy/access.md')).toContain('plugin/gtm/read');
|
|
210
|
+
expect(await access.canRead(wsId, member.email, 'Skills/Eng/old-deploy/SKILL.md')).toBe(false);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('unlink removes the entry and revokes the tokens when the actor may edit the skill', async () => {
|
|
214
|
+
await write('Skills/Eng/access.md', '---\n---\nwrite:\n - Mia <mia@x.io>\n');
|
|
215
|
+
access.invalidate(wsId);
|
|
216
|
+
await svc.link(manager, 'gtm', 'Skills/Eng/deploy');
|
|
217
|
+
commits.length = 0;
|
|
218
|
+
|
|
219
|
+
expect(await svc.unlink(manager, 'gtm', 'Skills/Eng/deploy')).toEqual({ root: 'Skills/Eng/deploy', revoked: true });
|
|
220
|
+
expect(JSON.parse(await read('Plugins/GTM/plugin.json')).extensions).toBeUndefined();
|
|
221
|
+
expect(await read('Skills/Eng/deploy/access.md')).not.toContain('plugin/gtm');
|
|
222
|
+
expect(await access.canRead(wsId, member.email, 'Skills/Eng/deploy/SKILL.md')).toBe(false);
|
|
223
|
+
// Revoke lands first, manifest second: a failure between the two leaves
|
|
224
|
+
// the visible half-state (still listed, no grant), never the silent one.
|
|
225
|
+
expect(commits).toEqual([`${KB_DIR}/Skills/Eng/deploy/access.md`, `${KB_DIR}/Plugins/GTM/plugin.json`]);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('unlink leaves the grant in place when the actor may not edit the skill — and says so', async () => {
|
|
229
|
+
await write('Skills/Eng/access.md', '---\n---\nwrite:\n - Mia <mia@x.io>\n');
|
|
230
|
+
access.invalidate(wsId);
|
|
231
|
+
await svc.link(manager, 'gtm', 'Skills/Eng/deploy');
|
|
232
|
+
// Eve takes Mia's write on the scope away, and hand-edits the skill's own
|
|
233
|
+
// rules down to the read token — the link's write token, which would still
|
|
234
|
+
// let GTM's managers edit, is gone.
|
|
235
|
+
await write('Skills/Eng/access.md', '---\n---\nwrite:\n - Eve <eve@x.io>\n');
|
|
236
|
+
await write('Skills/Eng/deploy/access.md', '---\n---\nread:\n - plugin/gtm/read\n');
|
|
237
|
+
access.invalidate(wsId);
|
|
238
|
+
|
|
239
|
+
expect(await svc.unlink(manager, 'gtm', 'Skills/Eng/deploy')).toEqual({ root: 'Skills/Eng/deploy', revoked: false });
|
|
240
|
+
expect(await read('Skills/Eng/deploy/access.md')).toContain('plugin/gtm/read');
|
|
241
|
+
await expect(svc.unlink(manager, 'gtm', 'Skills/Eng/deploy')).rejects.toMatchObject({ status: 404, payload: { kind: 'not-linked' } });
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('a hand-removed grant shows as not granted, and repair puts it back', async () => {
|
|
245
|
+
await write('Skills/Eng/access.md', '---\n---\nwrite:\n - Mia <mia@x.io>\n');
|
|
246
|
+
access.invalidate(wsId);
|
|
247
|
+
await svc.link(manager, 'gtm', 'Skills/Eng/deploy');
|
|
248
|
+
|
|
249
|
+
await write('Skills/Eng/deploy/access.md', '---\n---\nread:\n - Sam <sam@x.io>\n');
|
|
250
|
+
access.invalidate(wsId);
|
|
251
|
+
index.invalidate();
|
|
252
|
+
expect(await index.pluginsOf('Skills/Eng/deploy')).toEqual([{ name: 'gtm', linked: true, granted: false }]);
|
|
253
|
+
|
|
254
|
+
await svc.repair(manager, 'gtm', 'Skills/Eng/deploy');
|
|
255
|
+
expect(await index.pluginsOf('Skills/Eng/deploy')).toEqual([{ name: 'gtm', linked: true, granted: true }]);
|
|
256
|
+
// Repair is a skill-editor's action, not a plugin-manager's: with both the
|
|
257
|
+
// scope grant and the link's write token gone, Mia may not touch the rules.
|
|
258
|
+
await write('Skills/Eng/access.md', '---\n---\nwrite:\n - Eve <eve@x.io>\n');
|
|
259
|
+
await write('Skills/Eng/deploy/access.md', '---\n---\nread:\n - Sam <sam@x.io>\n');
|
|
260
|
+
access.invalidate(wsId);
|
|
261
|
+
await expect(svc.repair(manager, 'gtm', 'Skills/Eng/deploy')).rejects.toBeInstanceOf(PluginLinkError);
|
|
262
|
+
// Eve, who edits the scope, can.
|
|
263
|
+
await svc.repair(editor, 'gtm', 'Skills/Eng/deploy');
|
|
264
|
+
expect(await read('Skills/Eng/deploy/access.md')).toContain('plugin/gtm/read');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('a plugin nested below the root links like any other — position means nothing', async () => {
|
|
268
|
+
await write('Plugins/teams/Deep/plugin.json', '{\n "name": "deep"\n}\n');
|
|
269
|
+
await write('Plugins/teams/Deep/access.md', '---\n---\nread:\n - Sam <sam@x.io>\nwrite:\n - Mia <mia@x.io>\n');
|
|
270
|
+
await write('Skills/Eng/access.md', '---\n---\nwrite:\n - Mia <mia@x.io>\n');
|
|
271
|
+
access.invalidate(wsId);
|
|
272
|
+
index.invalidate();
|
|
273
|
+
|
|
274
|
+
await svc.link(manager, 'deep', 'Skills/Eng/rollback');
|
|
275
|
+
expect(JSON.parse(await read('Plugins/teams/Deep/plugin.json')).extensions['software.bevel.hexis'].skills).toEqual(['Skills/Eng/rollback']);
|
|
276
|
+
expect(await read('Skills/Eng/rollback/access.md')).toContain('plugin/deep/read');
|
|
277
|
+
expect(await access.canRead(wsId, member.email, 'Skills/Eng/rollback/SKILL.md')).toBe(true);
|
|
278
|
+
expect(await index.pluginsOf('Skills/Eng/rollback')).toEqual([{ name: 'deep', linked: true, granted: true }]);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('a skill inside a plugin folder is inline membership, never a link', async () => {
|
|
282
|
+
await write('Plugins/GTM/skills/outreach/SKILL.md', '---\ndescription: Reach out.\n---\n');
|
|
283
|
+
skills.invalidate();
|
|
284
|
+
index.invalidate();
|
|
285
|
+
expect(await index.pluginsOf('Plugins/GTM/skills/outreach')).toEqual([{ name: 'gtm', linked: false, granted: true }]);
|
|
286
|
+
});
|
|
287
|
+
});
|
|
@@ -51,6 +51,20 @@ async function makeHarness() {
|
|
|
51
51
|
return { svc, dir, commits, accessControl, events, writeFile };
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/** Wait until a delete has parked `dir` (it is gone from its place) — the moment a racing creation may start. */
|
|
55
|
+
async function untilParked(dir: string): Promise<void> {
|
|
56
|
+
for (let i = 0; i < 200; i++) {
|
|
57
|
+
if (await fs.stat(dir).then(() => false, () => true)) return;
|
|
58
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
59
|
+
}
|
|
60
|
+
throw new Error(`${dir} was never parked`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Let every pending microtask and short timer run — enough for a queued lock waiter to be queued. */
|
|
64
|
+
async function settle(): Promise<void> {
|
|
65
|
+
for (let i = 0; i < 5; i++) await new Promise((r) => setTimeout(r, 5));
|
|
66
|
+
}
|
|
67
|
+
|
|
54
68
|
describe('PluginProvisionService.createPlugin', () => {
|
|
55
69
|
let h: Awaited<ReturnType<typeof makeHarness>>;
|
|
56
70
|
beforeEach(async () => {
|
|
@@ -59,7 +73,9 @@ describe('PluginProvisionService.createPlugin', () => {
|
|
|
59
73
|
|
|
60
74
|
it('writes the discoverable template, commits it inline, and drops the access cache', async () => {
|
|
61
75
|
const result = await h.svc.createPlugin(USER, 'GTM');
|
|
62
|
-
|
|
76
|
+
// The folder is where it lives; the name is what it IS (the identity the
|
|
77
|
+
// page navigates to and the grants spell).
|
|
78
|
+
expect(result).toEqual({ folder: 'GTM', name: 'gtm', created: true });
|
|
63
79
|
|
|
64
80
|
const accessMd = await fs.readFile(path.join(h.dir, KB, 'Plugins/GTM/access.md'), 'utf-8');
|
|
65
81
|
// Discoverable FILE (frontmatter read: everyone), creator-run FOLDER
|
|
@@ -87,7 +103,7 @@ describe('PluginProvisionService.createPlugin', () => {
|
|
|
87
103
|
expect(h.accessControl.invalidate).toHaveBeenCalledWith('ws-main');
|
|
88
104
|
});
|
|
89
105
|
|
|
90
|
-
it('writes a conformant plugin.json
|
|
106
|
+
it('writes a conformant plugin.json: the identifier in slug form, the typed name as the display name', async () => {
|
|
91
107
|
await h.svc.createPlugin(USER, 'GTM');
|
|
92
108
|
const manifest = JSON.parse(
|
|
93
109
|
await fs.readFile(path.join(h.dir, KB, 'Plugins/GTM/plugin.json'), 'utf-8'),
|
|
@@ -95,6 +111,7 @@ describe('PluginProvisionService.createPlugin', () => {
|
|
|
95
111
|
expect(manifest).toEqual({
|
|
96
112
|
$schema: 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json',
|
|
97
113
|
name: 'gtm',
|
|
114
|
+
displayName: 'GTM',
|
|
98
115
|
});
|
|
99
116
|
// The schema's `name` pattern is the thing a conformant client refuses on.
|
|
100
117
|
expect(manifest.name).toMatch(/^(?!.*(?:--|\.\.))[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/);
|
|
@@ -118,6 +135,7 @@ describe('PluginProvisionService.createPlugin', () => {
|
|
|
118
135
|
// A genuinely distinct slug still goes through.
|
|
119
136
|
await expect(h.svc.createPlugin(USER, 'Sales Ops')).resolves.toEqual({
|
|
120
137
|
folder: 'Sales Ops',
|
|
138
|
+
name: 'sales-ops',
|
|
121
139
|
created: true,
|
|
122
140
|
});
|
|
123
141
|
});
|
|
@@ -145,6 +163,7 @@ describe('PluginProvisionService.createPlugin', () => {
|
|
|
145
163
|
await fs.writeFile(path.join(h.dir, KB, 'Plugins', 'slack.tool'), 'id: slack\n', 'utf-8');
|
|
146
164
|
await expect(h.svc.createPlugin(USER, 'Slack Tool')).resolves.toEqual({
|
|
147
165
|
folder: 'Slack Tool',
|
|
166
|
+
name: 'slack-tool',
|
|
148
167
|
created: true,
|
|
149
168
|
});
|
|
150
169
|
});
|
|
@@ -180,7 +199,7 @@ describe('PluginProvisionService.createPlugin', () => {
|
|
|
180
199
|
await expect(h.svc.createPlugin(USER, 'GTM')).rejects.toThrow('push refused');
|
|
181
200
|
// The folder is gone again — the next attempt starts clean.
|
|
182
201
|
await expect(fs.stat(path.join(h.dir, KB, 'Plugins/GTM'))).rejects.toThrow();
|
|
183
|
-
await expect(h.svc.createPlugin(USER, 'GTM')).resolves.toEqual({ folder: 'GTM', created: true });
|
|
202
|
+
await expect(h.svc.createPlugin(USER, 'GTM')).resolves.toEqual({ folder: 'GTM', name: 'gtm', created: true });
|
|
184
203
|
});
|
|
185
204
|
});
|
|
186
205
|
|
|
@@ -220,6 +239,116 @@ describe('PluginProvisionService.deletePlugin', () => {
|
|
|
220
239
|
);
|
|
221
240
|
});
|
|
222
241
|
|
|
242
|
+
it('refuses a folder path the filesystem cannot carry with 422 — control characters, a reserved name, a dot-prefix', async () => {
|
|
243
|
+
for (const bad of ['teams/Deep', 'a\tb', 'teams/NUL', '.deleting-x', 'teams/.hidden']) {
|
|
244
|
+
await expect(h.svc.deletePlugin(USER, bad)).rejects.toMatchObject({ status: 422 });
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('a bundle-shaped plugin locks on the SLUG of the name its bundle declares — the one a creation would take', async () => {
|
|
249
|
+
await fs.mkdir(path.join(h.dir, KB, 'Plugins/Ext'), { recursive: true });
|
|
250
|
+
// Declared as people write it, not as the marketplace publishes it.
|
|
251
|
+
await fs.writeFile(path.join(h.dir, KB, 'Plugins/Ext/plugin.bundle.json'), '{"name":"Ext Id"}');
|
|
252
|
+
await fs.writeFile(path.join(h.dir, KB, 'Plugins/Ext/access.md'), '---\n---\n');
|
|
253
|
+
let refuse: (err: Error) => void = () => {};
|
|
254
|
+
h.commits.runPendingCommit.mockImplementationOnce(
|
|
255
|
+
() => new Promise<undefined>((_resolve, reject) => { refuse = reject; }),
|
|
256
|
+
);
|
|
257
|
+
const deleting = h.svc.deletePlugin(USER, 'Ext');
|
|
258
|
+
await untilParked(path.join(h.dir, KB, 'Plugins/Ext'));
|
|
259
|
+
// Spelled differently, same identity: must wait for the delete, then see the twin.
|
|
260
|
+
const creating = h.svc.createPlugin(USER, 'Ext Id');
|
|
261
|
+
await settle();
|
|
262
|
+
refuse(new Error('push refused'));
|
|
263
|
+
await expect(deleting).rejects.toThrow('push refused');
|
|
264
|
+
await expect(creating).rejects.toMatchObject({ status: 409 });
|
|
265
|
+
await expect(fs.stat(path.join(h.dir, KB, 'Plugins/Ext/plugin.bundle.json'))).resolves.toBeDefined();
|
|
266
|
+
await expect(fs.stat(path.join(h.dir, KB, 'Plugins/Ext Id'))).rejects.toThrow();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('validates the folder path AS GIVEN — a padded spelling is refused, never trimmed into a real folder', async () => {
|
|
270
|
+
await h.svc.createPlugin(USER, 'GTM');
|
|
271
|
+
for (const padded of [' GTM', 'GTM ', 'teams/ Deep']) {
|
|
272
|
+
await expect(h.svc.deletePlugin(USER, padded)).rejects.toMatchObject({ status: 422 });
|
|
273
|
+
}
|
|
274
|
+
await expect(fs.stat(path.join(h.dir, KB, 'Plugins/GTM/plugin.json'))).resolves.toBeDefined();
|
|
275
|
+
expect(h.commits.runPendingCommit).toHaveBeenCalledTimes(1); // the create above only
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('a hole ANYWHERE in discovery stops a delete — even of a plugin whose own folder was read fine', async () => {
|
|
279
|
+
await h.svc.createPlugin(USER, 'GTM');
|
|
280
|
+
await fs.mkdir(path.join(h.dir, KB, 'Plugins/Other'), { recursive: true });
|
|
281
|
+
await fs.writeFile(path.join(h.dir, KB, 'Plugins/Other/plugin.json'), '{"name":"other"}');
|
|
282
|
+
const real = fs.readFile;
|
|
283
|
+
const spy = vi.spyOn(fs, 'readFile').mockImplementation(((file: string, opts: unknown) =>
|
|
284
|
+
String(file).endsWith(path.join('Other', 'plugin.json'))
|
|
285
|
+
? Promise.reject(Object.assign(new Error('EACCES: permission denied'), { code: 'EACCES' }))
|
|
286
|
+
: (real as (f: string, o: unknown) => Promise<unknown>).call(fs, file, opts)) as never);
|
|
287
|
+
try {
|
|
288
|
+
await expect(h.svc.deletePlugin(USER, 'GTM')).rejects.toMatchObject({ status: 503 });
|
|
289
|
+
} finally {
|
|
290
|
+
spy.mockRestore();
|
|
291
|
+
}
|
|
292
|
+
await expect(fs.stat(path.join(h.dir, KB, 'Plugins/GTM/plugin.json'))).resolves.toBeDefined();
|
|
293
|
+
expect(h.commits.runPendingCommit).toHaveBeenCalledTimes(1); // the create above only
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('a manifest that is there but cannot be read stops the delete — never a guessed identity lock', async () => {
|
|
297
|
+
await h.svc.createPlugin(USER, 'GTM');
|
|
298
|
+
const real = fs.readFile;
|
|
299
|
+
const spy = vi.spyOn(fs, 'readFile').mockImplementation(((file: string, opts: unknown) =>
|
|
300
|
+
String(file).endsWith(path.join('GTM', 'plugin.json'))
|
|
301
|
+
? Promise.reject(Object.assign(new Error('EACCES: permission denied'), { code: 'EACCES' }))
|
|
302
|
+
: (real as (f: string, o: unknown) => Promise<unknown>).call(fs, file, opts)) as never);
|
|
303
|
+
try {
|
|
304
|
+
// Discovery reports the hole; the delete refuses the way a creation would.
|
|
305
|
+
await expect(h.svc.deletePlugin(USER, 'GTM')).rejects.toMatchObject({ status: 503 });
|
|
306
|
+
} finally {
|
|
307
|
+
spy.mockRestore();
|
|
308
|
+
}
|
|
309
|
+
await expect(fs.stat(path.join(h.dir, KB, 'Plugins/GTM/plugin.json'))).resolves.toBeDefined();
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('a nested plugin holds its identity against a creation at the root — taken is discovery\'s answer', async () => {
|
|
313
|
+
await fs.mkdir(path.join(h.dir, KB, 'Plugins/teams/Deep'), { recursive: true });
|
|
314
|
+
await fs.writeFile(path.join(h.dir, KB, 'Plugins/teams/Deep/plugin.json'), '{"name":"deep"}');
|
|
315
|
+
await fs.writeFile(path.join(h.dir, KB, 'Plugins/teams/Deep/access.md'), '---\n---\n');
|
|
316
|
+
await expect(h.svc.createPlugin(USER, 'Deep')).rejects.toMatchObject({ status: 409 });
|
|
317
|
+
await expect(h.svc.createPlugin(USER, 'Deep')).rejects.toThrow('Plugins/teams/Deep');
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('a delete and a creation of one IDENTITY never overlap — even when the identity is not the folder path', async () => {
|
|
321
|
+
await fs.mkdir(path.join(h.dir, KB, 'Plugins/teams/Deep'), { recursive: true });
|
|
322
|
+
await fs.writeFile(path.join(h.dir, KB, 'Plugins/teams/Deep/plugin.json'), '{"name":"deep"}');
|
|
323
|
+
await fs.writeFile(path.join(h.dir, KB, 'Plugins/teams/Deep/access.md'), '---\n---\n');
|
|
324
|
+
// The delete's commit hangs, then is REFUSED — the folder comes back.
|
|
325
|
+
let refuse: (err: Error) => void = () => {};
|
|
326
|
+
h.commits.runPendingCommit.mockImplementationOnce(
|
|
327
|
+
() => new Promise<undefined>((_resolve, reject) => { refuse = reject; }),
|
|
328
|
+
);
|
|
329
|
+
const deleting = h.svc.deletePlugin(USER, 'teams/Deep');
|
|
330
|
+
await untilParked(path.join(h.dir, KB, 'Plugins/teams/Deep'));
|
|
331
|
+
// A creation of the same identity, started while the folder is parked
|
|
332
|
+
// and invisible: it must wait for the delete, not slip in beside it.
|
|
333
|
+
const creating = h.svc.createPlugin(USER, 'Deep');
|
|
334
|
+
await settle();
|
|
335
|
+
refuse(new Error('push refused'));
|
|
336
|
+
await expect(deleting).rejects.toThrow('push refused');
|
|
337
|
+
// The delete rolled back, so `deep` is still taken — and the creation sees it.
|
|
338
|
+
await expect(creating).rejects.toMatchObject({ status: 409 });
|
|
339
|
+
await expect(fs.stat(path.join(h.dir, KB, 'Plugins/teams/Deep/plugin.json'))).resolves.toBeDefined();
|
|
340
|
+
await expect(fs.stat(path.join(h.dir, KB, 'Plugins/Deep'))).rejects.toThrow();
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('deletes a plugin whose name sits at the filesystem component limit — the park adds nothing to the name', async () => {
|
|
344
|
+
// 240 characters: valid to create (≤ 255 bytes), and long enough that a
|
|
345
|
+
// park spelled `.<name>.deleting-<uuid>` would not be a legal component.
|
|
346
|
+
const long = 'a'.repeat(240);
|
|
347
|
+
await h.svc.createPlugin(USER, long);
|
|
348
|
+
await h.svc.deletePlugin(USER, long);
|
|
349
|
+
expect(await fs.readdir(path.join(h.dir, KB, 'Plugins'))).toEqual([]);
|
|
350
|
+
});
|
|
351
|
+
|
|
223
352
|
it('refuses an unknown name — and a casing mismatch, which is the same thing — with 404', async () => {
|
|
224
353
|
await h.svc.createPlugin(USER, 'GTM');
|
|
225
354
|
for (const name of ['Nope', 'gtm']) {
|
|
@@ -228,6 +357,77 @@ describe('PluginProvisionService.deletePlugin', () => {
|
|
|
228
357
|
expect(await fs.readdir(path.join(h.dir, KB, 'Plugins'))).toEqual(['GTM']);
|
|
229
358
|
});
|
|
230
359
|
|
|
360
|
+
it('deletes a plugin nested below the root by its folder PATH, parking it beside itself', async () => {
|
|
361
|
+
await fs.mkdir(path.join(h.dir, KB, 'Plugins/teams/Deep'), { recursive: true });
|
|
362
|
+
await fs.writeFile(path.join(h.dir, KB, 'Plugins/teams/Deep/plugin.json'), '{"name":"deep"}');
|
|
363
|
+
await fs.writeFile(path.join(h.dir, KB, 'Plugins/teams/Deep/access.md'), '---\n---\n');
|
|
364
|
+
|
|
365
|
+
await h.svc.deletePlugin(USER, 'teams/Deep');
|
|
366
|
+
|
|
367
|
+
await expect(fs.stat(path.join(h.dir, KB, 'Plugins/teams/Deep'))).rejects.toThrow();
|
|
368
|
+
expect(await fs.readdir(path.join(h.dir, KB, 'Plugins/teams'))).toEqual([]);
|
|
369
|
+
expect(h.commits.runPendingCommit).toHaveBeenCalledWith(
|
|
370
|
+
'ws-main',
|
|
371
|
+
DEFAULT_BRANCH,
|
|
372
|
+
`${KB}/Plugins/teams/Deep`,
|
|
373
|
+
USER,
|
|
374
|
+
{ systemAuthorized: true },
|
|
375
|
+
);
|
|
376
|
+
// Segments only — nothing climbs out of the root, and no backslash (a
|
|
377
|
+
// second separator on Windows); a missing nested folder is unknown.
|
|
378
|
+
await expect(h.svc.deletePlugin(USER, '../etc')).rejects.toMatchObject({ status: 422 });
|
|
379
|
+
await expect(h.svc.deletePlugin(USER, 'teams\\Deep')).rejects.toMatchObject({ status: 422 });
|
|
380
|
+
await expect(h.svc.deletePlugin(USER, 'teams/Nope')).rejects.toMatchObject({ status: 404 });
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('removes nothing but the plugin it names — a sibling that happens to look like a park survives', async () => {
|
|
384
|
+
await fs.mkdir(path.join(h.dir, KB, 'Plugins/teams/Deep'), { recursive: true });
|
|
385
|
+
await fs.writeFile(path.join(h.dir, KB, 'Plugins/teams/Deep/plugin.json'), '{"name":"deep"}');
|
|
386
|
+
await fs.writeFile(path.join(h.dir, KB, 'Plugins/teams/Deep/access.md'), '---\n---\n');
|
|
387
|
+
// Whatever this is — a person's folder, the residue of a crashed run — it is not ours to delete.
|
|
388
|
+
await fs.mkdir(path.join(h.dir, KB, 'Plugins/teams/.Deep.deleting'), { recursive: true });
|
|
389
|
+
await fs.writeFile(path.join(h.dir, KB, 'Plugins/teams/.Deep.deleting/keep.md'), 'mine');
|
|
390
|
+
|
|
391
|
+
await h.svc.deletePlugin(USER, 'teams/Deep');
|
|
392
|
+
|
|
393
|
+
await expect(fs.stat(path.join(h.dir, KB, 'Plugins/teams/Deep'))).rejects.toThrow();
|
|
394
|
+
expect(await fs.readFile(path.join(h.dir, KB, 'Plugins/teams/.Deep.deleting/keep.md'), 'utf-8')).toBe('mine');
|
|
395
|
+
expect(await fs.readdir(path.join(h.dir, KB, 'Plugins/teams'))).toEqual(['.Deep.deleting']);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it('deletes only the exact spelling, at every depth — a stale casing must not park a replacement at the same place', async () => {
|
|
399
|
+
await fs.mkdir(path.join(h.dir, KB, 'Plugins/teams/Deep'), { recursive: true });
|
|
400
|
+
await fs.writeFile(path.join(h.dir, KB, 'Plugins/teams/Deep/plugin.json'), '{"name":"deep"}');
|
|
401
|
+
await fs.writeFile(path.join(h.dir, KB, 'Plugins/teams/Deep/access.md'), '---\n---\n');
|
|
402
|
+
|
|
403
|
+
// On a case-insensitive filesystem both of these `stat` fine — and must still be refused.
|
|
404
|
+
await expect(h.svc.deletePlugin(USER, 'teams/deep')).rejects.toMatchObject({ status: 404 });
|
|
405
|
+
await expect(h.svc.deletePlugin(USER, 'Teams/Deep')).rejects.toMatchObject({ status: 404 });
|
|
406
|
+
expect(await fs.readdir(path.join(h.dir, KB, 'Plugins/teams'))).toEqual(['Deep']);
|
|
407
|
+
expect(h.commits.runPendingCommit).not.toHaveBeenCalled();
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it('a folder that cannot be LISTED is an error, never "unknown plugin" — absence and failure are different answers', async () => {
|
|
411
|
+
await h.svc.createPlugin(USER, 'GTM');
|
|
412
|
+
const real = fs.readdir;
|
|
413
|
+
const spy = vi.spyOn(fs, 'readdir').mockImplementation(((dir: string, opts: unknown) => {
|
|
414
|
+
if (String(dir).endsWith('Plugins')) {
|
|
415
|
+
return Promise.reject(Object.assign(new Error('EACCES: permission denied'), { code: 'EACCES' }));
|
|
416
|
+
}
|
|
417
|
+
return (real as (d: string, o: unknown) => Promise<unknown>).call(fs, dir, opts);
|
|
418
|
+
}) as never);
|
|
419
|
+
try {
|
|
420
|
+
// Discovery cannot list the root: a hole, refused as such — never "unknown plugin" (404).
|
|
421
|
+
await expect(h.svc.deletePlugin(USER, 'GTM')).rejects.toMatchObject({ status: 503 });
|
|
422
|
+
await expect(h.svc.createPlugin(USER, 'Ops')).rejects.toMatchObject({ status: 503 });
|
|
423
|
+
} finally {
|
|
424
|
+
spy.mockRestore();
|
|
425
|
+
}
|
|
426
|
+
// Nothing moved, nothing committed: the plugin is exactly where it was.
|
|
427
|
+
await expect(fs.stat(path.join(h.dir, KB, 'Plugins/GTM/plugin.json'))).resolves.toBeDefined();
|
|
428
|
+
expect(h.commits.runPendingCommit).toHaveBeenCalledTimes(1); // the create above only
|
|
429
|
+
});
|
|
430
|
+
|
|
231
431
|
it('never deletes a personal folder through the plugin door', async () => {
|
|
232
432
|
await h.svc.ensurePersonalPlugin(USER);
|
|
233
433
|
const folder = personalPluginFolderName(USER.id);
|
|
@@ -260,7 +460,7 @@ describe('PluginProvisionService.ensurePersonalPlugin', () => {
|
|
|
260
460
|
const folder = personalPluginFolderName(USER.id);
|
|
261
461
|
|
|
262
462
|
const first = await h.svc.ensurePersonalPlugin(USER);
|
|
263
|
-
expect(first).toEqual({ folder, created: true });
|
|
463
|
+
expect(first).toEqual({ folder, name: folder, created: true });
|
|
264
464
|
const accessMd = await fs.readFile(
|
|
265
465
|
path.join(h.dir, KB, 'Plugins', folder, 'access.md'),
|
|
266
466
|
'utf-8',
|
|
@@ -273,7 +473,7 @@ describe('PluginProvisionService.ensurePersonalPlugin', () => {
|
|
|
273
473
|
}
|
|
274
474
|
|
|
275
475
|
const second = await h.svc.ensurePersonalPlugin(USER);
|
|
276
|
-
expect(second).toEqual({ folder, created: false });
|
|
476
|
+
expect(second).toEqual({ folder, name: folder, created: false });
|
|
277
477
|
// Idempotent for real: one provision (access.md + plugin.json), one commit.
|
|
278
478
|
expect(h.writeFile).toHaveBeenCalledTimes(2);
|
|
279
479
|
expect(h.commits.runPendingCommit).toHaveBeenCalledTimes(1);
|