@bevel-software/platform-core-backend 0.22.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/core-ports.d.ts +11 -1
- package/dist/core/core-ports.d.ts.map +1 -1
- package/dist/core/core-ports.js.map +1 -1
- package/dist/core/create-core-server.d.ts +3 -3
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +46 -13
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +12 -2
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +137 -24
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/core/lifecycle.d.ts +46 -1
- package/dist/core/lifecycle.d.ts.map +1 -1
- package/dist/core/lifecycle.js +99 -13
- package/dist/core/lifecycle.js.map +1 -1
- package/dist/core-config.d.ts +16 -12
- package/dist/core-config.d.ts.map +1 -1
- package/dist/core-config.js +28 -13
- package/dist/core-config.js.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -3
- package/dist/index.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +42 -12
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +67 -10
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +214 -35
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/access.routes.d.ts.map +1 -1
- package/dist/modules/access/access.routes.js +17 -16
- package/dist/modules/access/access.routes.js.map +1 -1
- package/dist/modules/access/directory-sync-bot.d.ts.map +1 -1
- package/dist/modules/access/directory-sync-bot.js +7 -3
- package/dist/modules/access/directory-sync-bot.js.map +1 -1
- package/dist/modules/agent-instructions/agent-instructions.routes.d.ts +8 -1
- package/dist/modules/agent-instructions/agent-instructions.routes.d.ts.map +1 -1
- package/dist/modules/agent-instructions/agent-instructions.routes.js +8 -2
- package/dist/modules/agent-instructions/agent-instructions.routes.js.map +1 -1
- package/dist/modules/agent-instructions/compose.d.ts +38 -6
- package/dist/modules/agent-instructions/compose.d.ts.map +1 -1
- package/dist/modules/agent-instructions/compose.js +39 -6
- package/dist/modules/agent-instructions/compose.js.map +1 -1
- package/dist/modules/agent-instructions/index.d.ts +2 -1
- package/dist/modules/agent-instructions/index.d.ts.map +1 -1
- package/dist/modules/agent-instructions/index.js +2 -1
- package/dist/modules/agent-instructions/index.js.map +1 -1
- package/dist/modules/agent-instructions/shared-file-rules.d.ts +115 -0
- package/dist/modules/agent-instructions/shared-file-rules.d.ts.map +1 -0
- package/dist/modules/agent-instructions/shared-file-rules.js +272 -0
- package/dist/modules/agent-instructions/shared-file-rules.js.map +1 -0
- package/dist/modules/audit/agent-audit.service.d.ts.map +1 -1
- package/dist/modules/audit/agent-audit.service.js +4 -2
- package/dist/modules/audit/agent-audit.service.js.map +1 -1
- package/dist/modules/auth/account-admission.d.ts +58 -7
- package/dist/modules/auth/account-admission.d.ts.map +1 -1
- package/dist/modules/auth/account-admission.js +44 -1
- package/dist/modules/auth/account-admission.js.map +1 -1
- package/dist/modules/auth/account-erasure.service.d.ts.map +1 -1
- package/dist/modules/auth/account-erasure.service.js +57 -16
- package/dist/modules/auth/account-erasure.service.js.map +1 -1
- package/dist/modules/auth/account.routes.d.ts +1 -1
- package/dist/modules/auth/account.routes.d.ts.map +1 -1
- package/dist/modules/auth/account.routes.js +61 -5
- package/dist/modules/auth/account.routes.js.map +1 -1
- package/dist/modules/auth/auth.middleware.d.ts +1 -1
- package/dist/modules/auth/auth.middleware.d.ts.map +1 -1
- package/dist/modules/auth/auth.middleware.js +18 -7
- package/dist/modules/auth/auth.middleware.js.map +1 -1
- package/dist/modules/auth/auth.routes.d.ts.map +1 -1
- package/dist/modules/auth/auth.routes.js +8 -0
- package/dist/modules/auth/auth.routes.js.map +1 -1
- package/dist/modules/auth/auth.service.d.ts +70 -2
- package/dist/modules/auth/auth.service.d.ts.map +1 -1
- package/dist/modules/auth/auth.service.js +197 -18
- package/dist/modules/auth/auth.service.js.map +1 -1
- package/dist/modules/auth/oidc-auth-provider.d.ts.map +1 -1
- package/dist/modules/auth/oidc-auth-provider.js +7 -2
- package/dist/modules/auth/oidc-auth-provider.js.map +1 -1
- package/dist/modules/code-mode/code-mode.tool.d.ts +20 -2
- package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
- package/dist/modules/code-mode/code-mode.tool.js +66 -35
- package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
- package/dist/modules/database/connection.d.ts +16 -0
- package/dist/modules/database/connection.d.ts.map +1 -1
- package/dist/modules/database/connection.js +117 -0
- package/dist/modules/database/connection.js.map +1 -1
- package/dist/modules/database/core-schema.d.ts +337 -120
- package/dist/modules/database/core-schema.d.ts.map +1 -1
- package/dist/modules/database/core-schema.js +116 -58
- package/dist/modules/database/core-schema.js.map +1 -1
- package/dist/modules/database/migrate.d.ts +8 -0
- package/dist/modules/database/migrate.d.ts.map +1 -1
- package/dist/modules/database/migrate.js +271 -1
- package/dist/modules/database/migrate.js.map +1 -1
- package/dist/modules/kb-fs/branch-name.d.ts.map +1 -1
- package/dist/modules/kb-fs/branch-name.js +12 -2
- package/dist/modules/kb-fs/branch-name.js.map +1 -1
- package/dist/modules/kb-fs/repo-path.d.ts +11 -16
- package/dist/modules/kb-fs/repo-path.d.ts.map +1 -1
- package/dist/modules/kb-fs/repo-path.js +79 -0
- package/dist/modules/kb-fs/repo-path.js.map +1 -1
- package/dist/modules/kb-sync/kb-sync.routes.d.ts +2 -2
- package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -1
- package/dist/modules/kb-sync/kb-sync.routes.js +14 -3
- package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -1
- package/dist/modules/kb-sync/sync-auth.d.ts +3 -1
- package/dist/modules/kb-sync/sync-auth.d.ts.map +1 -1
- package/dist/modules/kb-sync/sync-auth.js +1 -1
- package/dist/modules/kb-sync/sync-auth.js.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.js +21 -6
- package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
- package/dist/modules/mcp/mcp.service.d.ts +8 -0
- package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.service.js +38 -8
- package/dist/modules/mcp/mcp.service.js.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js +3 -2
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
- package/dist/modules/plugins/join-request-records.store.d.ts.map +1 -1
- package/dist/modules/plugins/join-request-records.store.js +7 -4
- package/dist/modules/plugins/join-request-records.store.js.map +1 -1
- package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
- package/dist/modules/settings/deployment-settings.service.js +8 -3
- package/dist/modules/settings/deployment-settings.service.js.map +1 -1
- package/dist/modules/settings/setup.routes.d.ts +52 -1
- package/dist/modules/settings/setup.routes.d.ts.map +1 -1
- package/dist/modules/settings/setup.routes.js +207 -19
- package/dist/modules/settings/setup.routes.js.map +1 -1
- package/dist/modules/skills/skills.contract.d.ts +90 -18
- package/dist/modules/skills/skills.contract.d.ts.map +1 -1
- package/dist/modules/skills/skills.contract.js +4 -1
- package/dist/modules/skills/skills.contract.js.map +1 -1
- package/dist/modules/skills/skills.service.d.ts +98 -9
- package/dist/modules/skills/skills.service.d.ts.map +1 -1
- package/dist/modules/skills/skills.service.js +239 -37
- package/dist/modules/skills/skills.service.js.map +1 -1
- package/dist/modules/skills/skills.tools.d.ts +7 -0
- package/dist/modules/skills/skills.tools.d.ts.map +1 -1
- package/dist/modules/skills/skills.tools.js +71 -7
- package/dist/modules/skills/skills.tools.js.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.js +12 -4
- package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
- package/dist/modules/tool-auth/internal-token.service.d.ts +3 -3
- package/dist/modules/tool-auth/tool-auth.middleware.d.ts +16 -7
- package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
- package/dist/modules/tool-auth/tool-auth.middleware.js +34 -12
- package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
- package/dist/modules/tool-helpers/tool-handler.d.ts +2 -1
- package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool-handler.js +25 -4
- package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
- package/dist/modules/tool-helpers/tool.contract.d.ts +3 -2
- package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
- package/dist/modules/tool-helpers/validate-token.js +1 -1
- package/dist/modules/tool-helpers/validate-token.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.js +42 -31
- package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
- package/dist/modules/tool-registry/description-length.d.ts +80 -0
- package/dist/modules/tool-registry/description-length.d.ts.map +1 -0
- package/dist/modules/tool-registry/description-length.js +108 -0
- package/dist/modules/tool-registry/description-length.js.map +1 -0
- package/dist/modules/workflow/agent-tools/change-request-summary.d.ts +98 -0
- package/dist/modules/workflow/agent-tools/change-request-summary.d.ts.map +1 -0
- package/dist/modules/workflow/agent-tools/change-request-summary.js +81 -0
- package/dist/modules/workflow/agent-tools/change-request-summary.js.map +1 -0
- package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.js +75 -35
- package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
- package/dist/modules/workflow/file-lock.service.d.ts +24 -0
- package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
- package/dist/modules/workflow/file-lock.service.js +30 -0
- package/dist/modules/workflow/file-lock.service.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +94 -0
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +193 -3
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/pending-commits.service.d.ts +38 -0
- package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
- package/dist/modules/workflow/pending-commits.service.js +60 -1
- package/dist/modules/workflow/pending-commits.service.js.map +1 -1
- package/dist/modules/workflow/recovery-bot.d.ts.map +1 -1
- package/dist/modules/workflow/recovery-bot.js +7 -3
- package/dist/modules/workflow/recovery-bot.js.map +1 -1
- package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/review-workflow/review-workflow.service.js +12 -3
- package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
- package/dist/modules/workflow/workflow-hooks.d.ts +54 -32
- package/dist/modules/workflow/workflow-hooks.d.ts.map +1 -1
- package/dist/modules/workflow/workflow-hooks.js +16 -1
- package/dist/modules/workflow/workflow-hooks.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +60 -6
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +115 -5
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/dist/modules/workspace/agent-access.gate.d.ts +94 -0
- package/dist/modules/workspace/agent-access.gate.d.ts.map +1 -0
- package/dist/modules/workspace/agent-access.gate.js +123 -0
- package/dist/modules/workspace/agent-access.gate.js.map +1 -0
- package/dist/modules/workspace/agent-upload.routes.d.ts +77 -0
- package/dist/modules/workspace/agent-upload.routes.d.ts.map +1 -0
- package/dist/modules/workspace/agent-upload.routes.js +210 -0
- package/dist/modules/workspace/agent-upload.routes.js.map +1 -0
- package/dist/modules/workspace/agent-upload.store.d.ts +284 -0
- package/dist/modules/workspace/agent-upload.store.d.ts.map +1 -0
- package/dist/modules/workspace/agent-upload.store.js +553 -0
- package/dist/modules/workspace/agent-upload.store.js.map +1 -0
- package/dist/modules/workspace/routine-write-policy.d.ts +5 -6
- package/dist/modules/workspace/routine-write-policy.d.ts.map +1 -1
- package/dist/modules/workspace/routine-write-policy.js +5 -6
- package/dist/modules/workspace/routine-write-policy.js.map +1 -1
- package/dist/modules/workspace/session-sink.d.ts +5 -5
- package/dist/modules/workspace/set-aside-clone.d.ts +46 -0
- package/dist/modules/workspace/set-aside-clone.d.ts.map +1 -0
- package/dist/modules/workspace/set-aside-clone.js +92 -0
- package/dist/modules/workspace/set-aside-clone.js.map +1 -0
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts +59 -9
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
- package/dist/modules/workspace/startup/kb-startup-runner.js +65 -24
- package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
- package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/seed-tree.js +3 -3
- package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
- package/dist/modules/workspace/startup/steps/template-source.d.ts +40 -0
- package/dist/modules/workspace/startup/steps/template-source.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/template-source.js +46 -4
- package/dist/modules/workspace/startup/steps/template-source.js.map +1 -1
- package/dist/modules/workspace/upload-limits.d.ts +13 -0
- package/dist/modules/workspace/upload-limits.d.ts.map +1 -0
- package/dist/modules/workspace/upload-limits.js +13 -0
- package/dist/modules/workspace/upload-limits.js.map +1 -0
- package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.routes.js +93 -4
- package/dist/modules/workspace/workspace.routes.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +128 -5
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +316 -58
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/modules/workspace/workspace.tools.d.ts +13 -11
- package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.tools.js +791 -198
- package/dist/modules/workspace/workspace.tools.js.map +1 -1
- package/dist/modules/workspace/write-denial.d.ts +0 -6
- package/dist/modules/workspace/write-denial.d.ts.map +1 -1
- package/dist/modules/workspace/write-denial.js +0 -6
- package/dist/modules/workspace/write-denial.js.map +1 -1
- package/dist/modules/workspace/zip-entry-rules.d.ts +114 -0
- package/dist/modules/workspace/zip-entry-rules.d.ts.map +1 -0
- package/dist/modules/workspace/zip-entry-rules.js +154 -0
- package/dist/modules/workspace/zip-entry-rules.js.map +1 -0
- package/dist/modules/write-access/write-access.d.ts +60 -0
- package/dist/modules/write-access/write-access.d.ts.map +1 -0
- package/dist/modules/write-access/write-access.js +129 -0
- package/dist/modules/write-access/write-access.js.map +1 -0
- package/dist/shared/column-crypto.d.ts +194 -0
- package/dist/shared/column-crypto.d.ts.map +1 -0
- package/dist/shared/column-crypto.js +144 -0
- package/dist/shared/column-crypto.js.map +1 -0
- package/dist/shared/domain-errors.d.ts +25 -0
- package/dist/shared/domain-errors.d.ts.map +1 -1
- package/dist/shared/domain-errors.js +28 -0
- package/dist/shared/domain-errors.js.map +1 -1
- package/dist/shared/git.contract.d.ts +20 -0
- package/dist/shared/git.contract.d.ts.map +1 -1
- package/dist/shared/git.contract.js +26 -0
- package/dist/shared/git.contract.js.map +1 -1
- package/dist/shared/token-crypto.d.ts.map +1 -1
- package/dist/shared/token-crypto.js +25 -1
- package/dist/shared/token-crypto.js.map +1 -1
- package/dist/tenancy/static-tenant-source.d.ts +0 -1
- package/dist/tenancy/static-tenant-source.d.ts.map +1 -1
- package/dist/tenancy/static-tenant-source.js +1 -2
- package/dist/tenancy/static-tenant-source.js.map +1 -1
- package/dist/tenancy/tenant-secrets.d.ts +5 -1
- package/dist/tenancy/tenant-secrets.d.ts.map +1 -1
- package/dist/tenancy/tenant-secrets.js +4 -0
- package/dist/tenancy/tenant-secrets.js.map +1 -1
- package/kb-template/AGENTS.md +2 -0
- package/migrations/0014_change_request_closed_reason.sql +1 -0
- package/migrations/0015_account_deactivation.sql +3 -0
- package/migrations/0016_pii_encryption.sql +20 -0
- package/migrations/meta/0014_snapshot.json +2265 -0
- package/migrations/meta/0015_snapshot.json +2271 -0
- package/migrations/meta/0016_snapshot.json +2327 -0
- package/migrations/meta/_journal.json +21 -0
- package/package.json +3 -3
- package/src/__tests__/kb-layout-config.test.ts +0 -2
- package/src/__tests__/retired-settings.test.ts +96 -0
- package/src/core/__tests__/gated-boot.test.ts +132 -0
- package/src/core/__tests__/lifecycle.test.ts +241 -5
- package/src/core/__tests__/set-aside-root-is-one-place.test.ts +63 -0
- package/src/core/core-ports.ts +11 -1
- package/src/core/create-core-server.ts +52 -16
- package/src/core/create-core-services.ts +156 -24
- package/src/core/lifecycle.ts +117 -13
- package/src/core-config.ts +31 -14
- package/src/index.ts +30 -1
- package/src/modules/access/__tests__/access-control.preview-relocation.test.ts +385 -0
- package/src/modules/access/__tests__/access-control.prospective.test.ts +94 -18
- package/src/modules/access/__tests__/access.routes.prospective.test.ts +6 -7
- package/src/modules/access/__tests__/users-db-double.ts +21 -12
- package/src/modules/access/access-control.interface.ts +43 -12
- package/src/modules/access/access-control.service.ts +234 -36
- package/src/modules/access/access.routes.ts +17 -16
- package/src/modules/access/directory-sync-bot.ts +7 -3
- package/src/modules/agent-instructions/__tests__/agent-instructions.route.test.ts +11 -5
- package/src/modules/agent-instructions/__tests__/compose.test.ts +19 -10
- package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +238 -0
- package/src/modules/agent-instructions/agent-instructions.routes.ts +12 -2
- package/src/modules/agent-instructions/compose.ts +50 -7
- package/src/modules/agent-instructions/index.ts +12 -0
- package/src/modules/agent-instructions/shared-file-rules.ts +314 -0
- package/src/modules/audit/agent-audit.service.ts +4 -2
- package/src/modules/auth/__tests__/account-deactivation.test.ts +253 -0
- package/src/modules/auth/__tests__/account-erasure.approval-gate.test.ts +6 -2
- package/src/modules/auth/__tests__/account.routes.test.ts +87 -6
- package/src/modules/auth/__tests__/auth.middleware.test.ts +45 -22
- package/src/modules/auth/__tests__/auth.service.test.ts +4 -2
- package/src/modules/auth/account-admission.ts +78 -9
- package/src/modules/auth/account-erasure.service.ts +69 -18
- package/src/modules/auth/account.routes.ts +63 -7
- package/src/modules/auth/auth.middleware.ts +19 -8
- package/src/modules/auth/auth.routes.ts +8 -0
- package/src/modules/auth/auth.service.ts +206 -23
- package/src/modules/auth/oidc-auth-provider.ts +7 -2
- package/src/modules/code-mode/__tests__/chain-runtime.e2e.test.ts +335 -0
- package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +46 -2
- package/src/modules/code-mode/code-mode.tool.ts +80 -34
- package/src/modules/database/__tests__/connection.test.ts +12 -0
- package/src/modules/database/__tests__/pii-backfill.pg.test.ts +561 -0
- package/src/modules/database/connection.ts +117 -0
- package/src/modules/database/core-schema.ts +116 -58
- package/src/modules/database/migrate.ts +353 -1
- package/src/modules/kb-fs/__tests__/branch-name.test.ts +10 -0
- package/src/modules/kb-fs/__tests__/repo-path.test.ts +123 -0
- package/src/modules/kb-fs/branch-name.ts +14 -1
- package/src/modules/kb-fs/repo-path.ts +85 -0
- package/src/modules/kb-sync/__tests__/kb-sync.routes.test.ts +26 -1
- package/src/modules/kb-sync/kb-sync.routes.ts +14 -4
- package/src/modules/kb-sync/sync-auth.ts +2 -2
- package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +60 -3
- package/src/modules/mcp/__tests__/mcp.e2e.test.ts +4 -3
- package/src/modules/mcp/__tests__/mcp.service.test.ts +115 -13
- package/src/modules/mcp/mcp-auth.middleware.ts +20 -6
- package/src/modules/mcp/mcp.service.ts +46 -7
- package/src/modules/mcp/oauth/bevel-oauth-provider.ts +3 -1
- package/src/modules/plugins/__tests__/plugins.tools.test.ts +2 -1
- package/src/modules/plugins/join-request-records.store.ts +7 -4
- package/src/modules/settings/__tests__/deployment-settings.service.test.ts +16 -0
- package/src/modules/settings/__tests__/setup.routes.git-mode.test.ts +91 -58
- package/src/modules/settings/__tests__/setup.routes.github-app.test.ts +25 -8
- package/src/modules/settings/__tests__/setup.routes.managed-phase.test.ts +14 -11
- package/src/modules/settings/__tests__/setup.routes.repository-change.test.ts +390 -0
- package/src/modules/settings/__tests__/setup.routes.test.ts +3 -0
- package/src/modules/settings/deployment-settings.service.ts +8 -3
- package/src/modules/settings/setup.routes.ts +263 -19
- package/src/modules/skills/__tests__/allowed-tools-warn.tools.test.ts +2 -1
- package/src/modules/skills/__tests__/branch-skills.tools.test.ts +218 -0
- package/src/modules/skills/__tests__/skills.service.test.ts +229 -5
- package/src/modules/skills/skills.contract.ts +91 -18
- package/src/modules/skills/skills.service.ts +278 -41
- package/src/modules/skills/skills.tools.ts +80 -8
- package/src/modules/tool-auth/__tests__/manual-auth.middleware.test.ts +14 -1
- package/src/modules/tool-auth/external-api-key.service.ts +12 -4
- package/src/modules/tool-auth/internal-token.service.ts +3 -3
- package/src/modules/tool-auth/tool-auth.middleware.ts +34 -11
- package/src/modules/tool-helpers/__tests__/agent-roles-write.test.ts +5 -3
- package/src/modules/tool-helpers/__tests__/phase4-tools.test.ts +3 -3
- package/src/modules/tool-helpers/__tests__/validate-token.test.ts +11 -1
- package/src/modules/tool-helpers/tool-handler.ts +24 -4
- package/src/modules/tool-helpers/tool.contract.ts +3 -2
- package/src/modules/tool-helpers/validate-token.ts +1 -1
- package/src/modules/tool-manuals/tool-manuals.tools.ts +44 -31
- package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +378 -0
- package/src/modules/tool-registry/description-length.ts +111 -0
- package/src/modules/workflow/__tests__/pending-commits.repository-replaced.test.ts +111 -0
- package/src/modules/workflow/__tests__/workflow.routes.history-read-gate.test.ts +25 -0
- package/src/modules/workflow/__tests__/workflow.service.repository-replaced.test.ts +249 -0
- package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +264 -6
- package/src/modules/workflow/agent-tools/change-request-summary.ts +182 -0
- package/src/modules/workflow/agent-tools/workflow.tools.ts +86 -35
- package/src/modules/workflow/file-lock.service.ts +31 -0
- package/src/modules/workflow/git/__tests__/git.service.fileBytesAtCommit.test.ts +260 -0
- package/src/modules/workflow/git/__tests__/git.service.prFetchFailure.test.ts +170 -0
- package/src/modules/workflow/git/git.service.ts +215 -1
- package/src/modules/workflow/pending-commits.service.ts +64 -2
- package/src/modules/workflow/recovery-bot.ts +7 -3
- package/src/modules/workflow/review-workflow/__tests__/carry-approvals-forward.test.ts +4 -0
- package/src/modules/workflow/review-workflow/__tests__/erase-approver.test.ts +25 -3
- package/src/modules/workflow/review-workflow/review-workflow.service.ts +12 -3
- package/src/modules/workflow/workflow-hooks.ts +64 -26
- package/src/modules/workflow/workflow.service.ts +125 -5
- package/src/modules/workspace/__tests__/agent-access.gate.test.ts +208 -0
- package/src/modules/workspace/__tests__/agent-uploads.test.ts +1604 -0
- package/src/modules/workspace/__tests__/escape-sequences.routes.test.ts +394 -0
- package/src/modules/workspace/__tests__/file-stat-access.test.ts +2 -1
- package/src/modules/workspace/__tests__/git-internals.security.test.ts +2 -1
- package/src/modules/workspace/__tests__/set-aside-clone.test.ts +52 -0
- package/src/modules/workspace/__tests__/workspace.routes.at-ref.test.ts +305 -0
- package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +2 -0
- package/src/modules/workspace/__tests__/workspace.service.any-workspace-credentials.test.ts +156 -0
- package/src/modules/workspace/__tests__/workspace.service.forget-clone-races.test.ts +147 -0
- package/src/modules/workspace/__tests__/workspace.service.replaced-repository.test.ts +402 -0
- package/src/modules/workspace/__tests__/workspace.service.test.ts +77 -9
- package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +41 -39
- package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +2 -3
- package/src/modules/workspace/__tests__/workspace.tools.test.ts +766 -56
- package/src/modules/workspace/agent-access.gate.ts +164 -0
- package/src/modules/workspace/agent-upload.routes.ts +214 -0
- package/src/modules/workspace/agent-upload.store.ts +668 -0
- package/src/modules/workspace/routine-write-policy.ts +5 -6
- package/src/modules/workspace/session-sink.ts +5 -5
- package/src/modules/workspace/set-aside-clone.ts +96 -0
- package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +86 -0
- package/src/modules/workspace/startup/kb-startup-runner.ts +115 -34
- package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +9 -9
- package/src/modules/workspace/startup/steps/seed-tree.ts +3 -6
- package/src/modules/workspace/startup/steps/template-source.ts +53 -5
- package/src/modules/workspace/upload-limits.ts +12 -0
- package/src/modules/workspace/workspace.routes.ts +96 -5
- package/src/modules/workspace/workspace.service.ts +319 -61
- package/src/modules/workspace/workspace.tools.ts +894 -214
- package/src/modules/workspace/write-denial.ts +0 -8
- package/src/modules/workspace/zip-entry-rules.ts +173 -0
- package/src/modules/write-access/__tests__/write-access.test.ts +248 -0
- package/src/modules/write-access/write-access.ts +153 -0
- package/src/shared/__tests__/column-crypto.test.ts +217 -0
- package/src/shared/column-crypto.ts +218 -0
- package/src/shared/domain-errors.ts +31 -0
- package/src/shared/git.contract.ts +27 -0
- package/src/shared/token-crypto.ts +28 -1
- package/src/tenancy/__tests__/static-tenant-source.test.ts +4 -1
- package/src/tenancy/static-tenant-source.ts +1 -3
- package/src/tenancy/tenant-secrets.ts +5 -1
- package/dist/modules/workflow/session-ontology.policy.d.ts +0 -52
- package/dist/modules/workflow/session-ontology.policy.d.ts.map +0 -1
- package/dist/modules/workflow/session-ontology.policy.js +0 -62
- package/dist/modules/workflow/session-ontology.policy.js.map +0 -1
- package/dist/modules/workflow/session-ontology.service.d.ts +0 -105
- package/dist/modules/workflow/session-ontology.service.d.ts.map +0 -1
- package/dist/modules/workflow/session-ontology.service.js +0 -147
- package/dist/modules/workflow/session-ontology.service.js.map +0 -1
- package/dist/modules/workspace/session-ontology.gate.d.ts +0 -114
- package/dist/modules/workspace/session-ontology.gate.d.ts.map +0 -1
- package/dist/modules/workspace/session-ontology.gate.js +0 -161
- package/dist/modules/workspace/session-ontology.gate.js.map +0 -1
- package/dist/shared/kb-layout.d.ts +0 -39
- package/dist/shared/kb-layout.d.ts.map +0 -1
- package/dist/shared/kb-layout.js +0 -103
- package/dist/shared/kb-layout.js.map +0 -1
- package/dist/shared/kb-layout.test.d.ts +0 -2
- package/dist/shared/kb-layout.test.d.ts.map +0 -1
- package/dist/shared/kb-layout.test.js +0 -75
- package/dist/shared/kb-layout.test.js.map +0 -1
- package/src/modules/workflow/__tests__/session-ontology.policy.test.ts +0 -62
- package/src/modules/workflow/__tests__/session-ontology.service.test.ts +0 -201
- package/src/modules/workflow/session-ontology.policy.ts +0 -70
- package/src/modules/workflow/session-ontology.service.ts +0 -183
- package/src/modules/workspace/__tests__/session-ontology.gate.test.ts +0 -239
- package/src/modules/workspace/session-ontology.gate.ts +0 -191
- package/src/shared/kb-layout.test.ts +0 -98
- package/src/shared/kb-layout.ts +0 -102
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import {
|
|
4
|
+
CALL_TOOL_CHAIN_NAME,
|
|
5
|
+
CHAIN_FAILURES_RULE,
|
|
6
|
+
CHAIN_LARGE_RESULTS_RULE,
|
|
7
|
+
codeModeMetaTools,
|
|
8
|
+
} from '@bevel-software/platform-mcp-core';
|
|
9
|
+
import { testKbContext } from '../../../__tests__/kb-context.js';
|
|
10
|
+
import { EXTERNAL_KB_MANUAL_NAME } from '../../tool-manuals/tool-manuals.contract.js';
|
|
11
|
+
import { ToolRegistry } from '../tool-registry.js';
|
|
12
|
+
import type { UtcpTool } from '../tool.contract.js';
|
|
13
|
+
import { createToolHandlerFactory } from '../../tool-helpers/tool-handler.js';
|
|
14
|
+
import type { ToolAuth } from '../../tool-auth/tool-auth.middleware.js';
|
|
15
|
+
import { registerWorkspaceTools } from '../../workspace/workspace.tools.js';
|
|
16
|
+
import { registerWorkflowTools } from '../../workflow/agent-tools/workflow.tools.js';
|
|
17
|
+
import { registerPluginsTools } from '../../plugins/plugins.tools.js';
|
|
18
|
+
import { registerSkillsTools } from '../../skills/skills.tools.js';
|
|
19
|
+
import { registerToolManualsTools } from '../../tool-manuals/tool-manuals.tools.js';
|
|
20
|
+
import { ToolDescriptionNotes } from '../../workspace/agent-access.gate.js';
|
|
21
|
+
import { WorkflowHooks } from '../../workflow/workflow-hooks.js';
|
|
22
|
+
import { UuidSessionSink } from '../../workspace/session-sink.js';
|
|
23
|
+
import { RoutineWritePolicyService } from '../../workspace/routine-write-policy.js';
|
|
24
|
+
import { CLIENT_SHORT_CUT, TOOL_DESCRIPTION_CAP, clientVisibleLength, firstSentenceEnd } from '../description-length.js';
|
|
25
|
+
import {
|
|
26
|
+
POINTER_GUIDE_NAME_BUDGET,
|
|
27
|
+
SHARED_RULES_POINTER_MAX,
|
|
28
|
+
TOOL_PREFIX_CAP,
|
|
29
|
+
sharedFileRules,
|
|
30
|
+
sharedRulesPointer,
|
|
31
|
+
} from '../../agent-instructions/index.js';
|
|
32
|
+
import { isPlatformFile, platformFilesByDepth } from '@bevel-software/platform-shared';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The cap exists because clients cut a long tool description, and they cut it
|
|
36
|
+
* from the END — where the text specific to the tool sits. Agents reported
|
|
37
|
+
* `file_stat`, `read_file`, `write_file` and `write_files` arriving as
|
|
38
|
+
* "[truncated]". So this measures what a client is actually handed, and fails
|
|
39
|
+
* NAMING the tool: the next paragraph someone appends to a description has to
|
|
40
|
+
* answer to this test rather than to an agent's truncated catalog.
|
|
41
|
+
*
|
|
42
|
+
* Every registrar Hexis owns is mounted here, on stand-ins, because the
|
|
43
|
+
* descriptions are the only thing under test: a stand-in that is never called
|
|
44
|
+
* is honest about that. Tools PROXIED from connected MCP servers are not
|
|
45
|
+
* measured — their text is the other server's.
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/** Dependencies the registrars take but never touch while they are only building defs. */
|
|
49
|
+
const unused = <T,>(): T => ({}) as T;
|
|
50
|
+
|
|
51
|
+
/** A `.tool` catalog with nothing in it: the shortest honest answer for a listing. */
|
|
52
|
+
const emptyManuals = {
|
|
53
|
+
listLocalOnly: async () => [],
|
|
54
|
+
listAll: async () => [],
|
|
55
|
+
list: async () => [],
|
|
56
|
+
listInvalid: async () => [],
|
|
57
|
+
} as unknown as Parameters<typeof registerToolManualsTools>[4];
|
|
58
|
+
|
|
59
|
+
/** A skill catalog with nothing in it — the per-user skill line is then one fixed sentence. */
|
|
60
|
+
const emptySkills = { listSkills: async () => [] } as unknown as Parameters<typeof registerSkillsTools>[4];
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The meta-tools as the hosted endpoint builds them (`mcp.service.ts`):
|
|
64
|
+
* examples written against the namespace it registers the knowledge-base tools
|
|
65
|
+
* under and against the catalog it serves, and the chain ending in the pointer.
|
|
66
|
+
* So the chain description measured here carries the worked call a client is
|
|
67
|
+
* really sent, which is its longest form.
|
|
68
|
+
*/
|
|
69
|
+
function servedMetaTools(external: readonly UtcpTool[], pointer = sharedRulesPointer(testKbContext().layout)) {
|
|
70
|
+
return codeModeMetaTools(
|
|
71
|
+
EXTERNAL_KB_MANUAL_NAME,
|
|
72
|
+
external.map((t) => ({ utcpName: `${EXTERNAL_KB_MANUAL_NAME}.${t.name}`, inputSchema: t.inputs })),
|
|
73
|
+
{ sharedRulesPointer: pointer },
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Every tool Hexis itself registers, on both surfaces, deduplicated by name. */
|
|
78
|
+
async function hexisTools(): Promise<UtcpTool[]> {
|
|
79
|
+
const registry = new ToolRegistry();
|
|
80
|
+
const router = express.Router();
|
|
81
|
+
const toolAuth = ((_req, _res, next) => next()) as unknown as ToolAuth;
|
|
82
|
+
const toolHandler = createToolHandlerFactory(unused());
|
|
83
|
+
const kb = testKbContext();
|
|
84
|
+
const gate = { recoveryBotEmail: 'bot@x', hooks: new WorkflowHooks(), notes: new ToolDescriptionNotes() };
|
|
85
|
+
|
|
86
|
+
registerWorkspaceTools(
|
|
87
|
+
registry,
|
|
88
|
+
router,
|
|
89
|
+
toolAuth,
|
|
90
|
+
toolHandler,
|
|
91
|
+
unused(),
|
|
92
|
+
unused(),
|
|
93
|
+
unused(),
|
|
94
|
+
kb,
|
|
95
|
+
gate,
|
|
96
|
+
new RoutineWritePolicyService(),
|
|
97
|
+
new UuidSessionSink(),
|
|
98
|
+
undefined,
|
|
99
|
+
undefined,
|
|
100
|
+
// The two upload tools are mounted only when a store is supplied, and
|
|
101
|
+
// every real composition supplies one: without it they would be the two
|
|
102
|
+
// descriptions this suite never measured.
|
|
103
|
+
unused(),
|
|
104
|
+
);
|
|
105
|
+
registerWorkflowTools(registry, router, toolAuth, toolHandler, kb);
|
|
106
|
+
registerPluginsTools(registry);
|
|
107
|
+
registerSkillsTools(registry, router, toolAuth, toolHandler, emptySkills);
|
|
108
|
+
registerToolManualsTools(registry, router, toolAuth, toolHandler, emptyManuals, {
|
|
109
|
+
accessControl: unused(),
|
|
110
|
+
variableStatus: unused(),
|
|
111
|
+
kb,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const byName = new Map<string, UtcpTool>();
|
|
115
|
+
// The meta-tools as a client is served them: the chain carries its pointer
|
|
116
|
+
// (`mcp.service.ts` appends it at the mount), so the catalog measured here is
|
|
117
|
+
// the catalog that goes out rather than the unpointed constant.
|
|
118
|
+
const external = (await registry.listExternal()) as UtcpTool[];
|
|
119
|
+
const metaTools = servedMetaTools(external);
|
|
120
|
+
for (const tool of [...(await registry.listInternal()), ...external, ...metaTools]) {
|
|
121
|
+
byName.set(tool.name, tool as UtcpTool);
|
|
122
|
+
}
|
|
123
|
+
return [...byName.values()];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
describe('no Hexis tool description is long enough to be cut', () => {
|
|
127
|
+
it(`keeps every description a client is handed within ${TOOL_DESCRIPTION_CAP} characters`, async () => {
|
|
128
|
+
const over = (await hexisTools())
|
|
129
|
+
.map((t) => ({ tool: t.name, chars: clientVisibleLength(t) }))
|
|
130
|
+
.filter((m) => m.chars > TOOL_DESCRIPTION_CAP)
|
|
131
|
+
.sort((a, b) => b.chars - a.chars);
|
|
132
|
+
// The message names the tool and its length, because "a description is too
|
|
133
|
+
// long" sends the next reader back to measuring them by hand.
|
|
134
|
+
expect(
|
|
135
|
+
over,
|
|
136
|
+
over.map((m) => `${m.tool}: ${m.chars} characters (cap ${TOOL_DESCRIPTION_CAP})`).join('\n'),
|
|
137
|
+
).toEqual([]);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it(`leaves the tool's own first sentence inside the shorter ${CLIENT_SHORT_CUT}-character cut`, async () => {
|
|
141
|
+
// The cap does not answer the ~500-character cut; the ORDER of the text
|
|
142
|
+
// does, and this is where that claim is checked rather than asserted in a
|
|
143
|
+
// comment. A client that stops at 500 must still have the sentence saying
|
|
144
|
+
// what the tool does — what it loses is the pointer tail, and the file the
|
|
145
|
+
// pointer names is stated in the handshake instructions and in the guide
|
|
146
|
+
// anyway. Lowering the cap to 500 would not buy this; only order does.
|
|
147
|
+
const late = (await hexisTools())
|
|
148
|
+
.map((t) => ({ tool: t.name, endsAt: firstSentenceEnd(t) }))
|
|
149
|
+
.filter((m) => m.endsAt > CLIENT_SHORT_CUT)
|
|
150
|
+
.sort((a, b) => b.endsAt - a.endsAt);
|
|
151
|
+
expect(
|
|
152
|
+
late,
|
|
153
|
+
late
|
|
154
|
+
.map((m) => `${m.tool}: first sentence ends at ${m.endsAt} (cut ${CLIENT_SHORT_CUT}) — lead with what it does`)
|
|
155
|
+
.join('\n'),
|
|
156
|
+
).toEqual([]);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('holds the cap on a deployment that renamed its guide, not only under the default', async () => {
|
|
160
|
+
// The pointer ends every file tool's description and its length moves with
|
|
161
|
+
// a deployment setting, so a cap checked only against the nine characters
|
|
162
|
+
// of `AGENTS.md` guarantees nothing about the catalog a renamed deployment
|
|
163
|
+
// serves. Two things make it hold: the pointer is bounded by construction
|
|
164
|
+
// (past `POINTER_GUIDE_NAME_BUDGET` the guide is named by its role), and
|
|
165
|
+
// `clientVisibleLength` charges the worst case rather than this layout's.
|
|
166
|
+
const tools = await hexisTools();
|
|
167
|
+
const atWorst = tools
|
|
168
|
+
.map((t) => ({ tool: t.name, chars: clientVisibleLength(t) }))
|
|
169
|
+
.filter((m) => m.chars > TOOL_DESCRIPTION_CAP);
|
|
170
|
+
expect(atWorst, atWorst.map((m) => `${m.tool}: ${m.chars}`).join('\n')).toEqual([]);
|
|
171
|
+
|
|
172
|
+
// And measured literally, under the longest guide name a deployment can
|
|
173
|
+
// actually configure: every description still fits.
|
|
174
|
+
const longest = `${'x'.repeat(252)}.md`;
|
|
175
|
+
const pointerHere = sharedRulesPointer(testKbContext().layout);
|
|
176
|
+
const pointerThere = sharedRulesPointer({ ...testKbContext().layout, agentsFile: longest });
|
|
177
|
+
expect(pointerThere.length).toBeLessThanOrEqual(SHARED_RULES_POINTER_MAX);
|
|
178
|
+
for (const tool of tools) {
|
|
179
|
+
if (!tool.description?.endsWith(pointerHere)) continue;
|
|
180
|
+
const asRenamed = tool.description.slice(0, -pointerHere.length) + pointerThere;
|
|
181
|
+
const chars = clientVisibleLength({ name: tool.name, description: asRenamed });
|
|
182
|
+
expect(chars, `${tool.name} on a renamed deployment`).toBeLessThanOrEqual(TOOL_DESCRIPTION_CAP);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('measures a prefixed tool with the prefix a client sees, not without it', async () => {
|
|
187
|
+
// The four knowledge-base tools carry the deployment's tool prefix ahead of
|
|
188
|
+
// their description on the MCP surface — up to `TOOL_PREFIX_CAP` characters
|
|
189
|
+
// the admin writes. A cap applied to the bare description would pass while
|
|
190
|
+
// the catalog the agent reads was over it by 300.
|
|
191
|
+
const read = (await hexisTools()).find((t) => t.name === 'read_file');
|
|
192
|
+
expect(read).toBeDefined();
|
|
193
|
+
// Its own text, with the pointer charged at its worst case rather than at
|
|
194
|
+
// this layout's, plus the prefix at ITS cap and the blank line between.
|
|
195
|
+
const pointer = sharedRulesPointer();
|
|
196
|
+
const ownAtWorstPointer = read!.description!.length - pointer.length + SHARED_RULES_POINTER_MAX;
|
|
197
|
+
expect(clientVisibleLength(read!)).toBe(ownAtWorstPointer + TOOL_PREFIX_CAP + 2);
|
|
198
|
+
expect(ownAtWorstPointer + TOOL_PREFIX_CAP + 2).toBeLessThanOrEqual(TOOL_DESCRIPTION_CAP);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('fails, naming the tool, when a paragraph takes a description over the cap', () => {
|
|
202
|
+
const padded = { name: 'write_file', description: 'x'.repeat(TOOL_DESCRIPTION_CAP + 1) } as UtcpTool;
|
|
203
|
+
expect(clientVisibleLength(padded)).toBeGreaterThan(TOOL_DESCRIPTION_CAP);
|
|
204
|
+
// A tool with no description at all is not over the cap.
|
|
205
|
+
expect(clientVisibleLength({ name: 'nothing' } as UtcpTool)).toBe(0);
|
|
206
|
+
// Unless it is a PREFIXED one: the prefix is sent on its own then (no
|
|
207
|
+
// description, so no blank line either), and that text is what the client
|
|
208
|
+
// was handed. Measuring it as nothing would hide the only thing it got.
|
|
209
|
+
expect(clientVisibleLength({ name: 'read_file' } as UtcpTool)).toBe(TOOL_PREFIX_CAP);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('measures the catalog, which is what a client lists — and says what is outside it', async () => {
|
|
213
|
+
// The cap is about `tools/list`: a client cuts what it was sent. The ONE
|
|
214
|
+
// long description this repository still writes is the in-process Mastra
|
|
215
|
+
// `call_tool_chain`, which no client lists — the in-process agent is handed
|
|
216
|
+
// it directly — and which OPENS with `@utcp/code-mode`'s own 2,500-character
|
|
217
|
+
// usage guide, text this repository does not own. It is left as it is, on
|
|
218
|
+
// purpose, and this assertion is what keeps that a stated fact rather than
|
|
219
|
+
// an oversight: if it ever reaches the catalog, the cap test above measures
|
|
220
|
+
// it like everything else.
|
|
221
|
+
const { createCallToolChainTool } = await import('../../code-mode/code-mode.tool.js');
|
|
222
|
+
const mastraTool = createCallToolChainTool(unused(), unused(), EXTERNAL_KB_MANUAL_NAME) as unknown as {
|
|
223
|
+
description: string;
|
|
224
|
+
};
|
|
225
|
+
expect(mastraTool.description).toContain('UTCP CodeMode Tool Usage Guide');
|
|
226
|
+
expect(mastraTool.description.length).toBeGreaterThan(TOOL_DESCRIPTION_CAP);
|
|
227
|
+
expect((await hexisTools()).some((t) => t.description === mastraTool.description)).toBe(false);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe('every file tool ends with the pointer and carries no shared paragraph', () => {
|
|
232
|
+
/** The tools that used to carry the shared paragraphs — every `mount`ed one. */
|
|
233
|
+
const FILE_TOOLS = [
|
|
234
|
+
'read_file',
|
|
235
|
+
'list_files',
|
|
236
|
+
'file_stat',
|
|
237
|
+
'grep',
|
|
238
|
+
'write_file',
|
|
239
|
+
'write_files',
|
|
240
|
+
'edit_file',
|
|
241
|
+
'delete_file',
|
|
242
|
+
'delete_folder',
|
|
243
|
+
'mkdir',
|
|
244
|
+
'move_file',
|
|
245
|
+
'copy_file',
|
|
246
|
+
'unzip',
|
|
247
|
+
'execute_command',
|
|
248
|
+
'apply_file_upload',
|
|
249
|
+
];
|
|
250
|
+
|
|
251
|
+
it('ends each description with the one sentence naming the shared rules', async () => {
|
|
252
|
+
const tools = await hexisTools();
|
|
253
|
+
const pointer = sharedRulesPointer(testKbContext().layout);
|
|
254
|
+
for (const name of FILE_TOOLS) {
|
|
255
|
+
const def = tools.find((t) => t.name === name);
|
|
256
|
+
expect(def, name).toBeDefined();
|
|
257
|
+
expect(def!.description!.endsWith(pointer), `${name} must end with: ${pointer}`).toBe(true);
|
|
258
|
+
// Ends with a full sentence, so nothing reads as cut off mid-thought.
|
|
259
|
+
expect(def!.description!.trimEnd().endsWith('.'), name).toBe(true);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('no longer repeats a shared paragraph inside a description', async () => {
|
|
264
|
+
const tools = await hexisTools();
|
|
265
|
+
// One recognisable fragment per rule that moved out. Searched across EVERY
|
|
266
|
+
// Hexis description, not only the file tools: a rule that came back by
|
|
267
|
+
// being pasted into a neighbouring tool is the same regression.
|
|
268
|
+
const moved = [
|
|
269
|
+
'Content rule (the same on every file tool)',
|
|
270
|
+
'they refuse documents, images, archives and other binary files',
|
|
271
|
+
'`mode` decides what may happen at a path and DEFAULTS TO',
|
|
272
|
+
'Images: keep them in an `assets/` folder',
|
|
273
|
+
'Escape sequences: some clients decode them in arguments',
|
|
274
|
+
'If this is refused for permissions',
|
|
275
|
+
'Before your first read or change in a workspace',
|
|
276
|
+
'Do NOT set `confirm: true` on your first call',
|
|
277
|
+
];
|
|
278
|
+
for (const fragment of moved) {
|
|
279
|
+
const carriers = tools.filter((t) => (t.description ?? '').includes(fragment)).map((t) => t.name);
|
|
280
|
+
expect(carriers, `"${fragment}" is a shared rule and belongs in the shared places only`).toEqual([]);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
describe('the shared rules describe the tools they name', () => {
|
|
286
|
+
it('ends the chain description with the pointer too, under the cap', async () => {
|
|
287
|
+
// What a chain does with a failure, a large result or an image is true of
|
|
288
|
+
// every call, so it is stated in the shared rules — and the clients that
|
|
289
|
+
// drop the handshake `instructions` see only descriptions, so the chain
|
|
290
|
+
// gets the same pointer every file tool ends with. Composed at the mount,
|
|
291
|
+
// because `mcp-core` may not spell a guide name that is a deployment
|
|
292
|
+
// setting.
|
|
293
|
+
const pointer = sharedRulesPointer(testKbContext().layout);
|
|
294
|
+
const served = (await hexisTools()).find((t) => t.name === CALL_TOOL_CHAIN_NAME)!;
|
|
295
|
+
expect(served.description!.endsWith(pointer)).toBe(true);
|
|
296
|
+
expect(clientVisibleLength(served)).toBeLessThanOrEqual(TOOL_DESCRIPTION_CAP);
|
|
297
|
+
// The other two describe the registry, not what a call does: no pointer.
|
|
298
|
+
for (const tool of servedMetaTools([]).filter((t) => t.name !== CALL_TOOL_CHAIN_NAME)) {
|
|
299
|
+
expect(tool.description).not.toContain(pointer);
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('states what a chain does in the shared rules, and not a second time on the chain', async () => {
|
|
304
|
+
// The pointer is only honest if the rules it points at are THERE. These
|
|
305
|
+
// two were paragraphs of the chain's description; they moved, whole, and a
|
|
306
|
+
// description that kept them as well would be the long one a client cuts.
|
|
307
|
+
const rule = sharedFileRules(testKbContext().layout).find((r) => r.id === 'tool-chain')!;
|
|
308
|
+
expect(rule.body).toContain(CHAIN_FAILURES_RULE);
|
|
309
|
+
expect(rule.body).toContain(CHAIN_LARGE_RESULTS_RULE);
|
|
310
|
+
const served = (await hexisTools()).find((t) => t.name === CALL_TOOL_CHAIN_NAME)!;
|
|
311
|
+
expect(served.description).not.toContain(CHAIN_FAILURES_RULE);
|
|
312
|
+
expect(served.description).not.toContain(CHAIN_LARGE_RESULTS_RULE);
|
|
313
|
+
// What a chained read does to an image was already one of the shared rules.
|
|
314
|
+
const content = sharedFileRules(testKbContext().layout).find((r) => r.id === 'content-kinds')!;
|
|
315
|
+
expect(content.body).toContain('image_omitted');
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('keeps the rules on the chain itself where no shared rules are served', () => {
|
|
319
|
+
// The standalone bridge proxies a deployment whose guide it cannot name, so
|
|
320
|
+
// it passes no pointer — and an agent there must still be told what a chain
|
|
321
|
+
// that timed out, or answered too much, or read an image, does.
|
|
322
|
+
const [chain] = codeModeMetaTools('hexis', []).filter((t) => t.name === CALL_TOOL_CHAIN_NAME);
|
|
323
|
+
expect(chain.description).toContain(CHAIN_FAILURES_RULE);
|
|
324
|
+
expect(chain.description).toContain(CHAIN_LARGE_RESULTS_RULE);
|
|
325
|
+
expect(chain.description).toContain('image_omitted');
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('names a dry run only on the tools that take one', async () => {
|
|
329
|
+
// A rule is worse than no rule when it promises an argument the tool
|
|
330
|
+
// rejects: `delete_file` has no `dryRun`, so an agent told to preflight a
|
|
331
|
+
// single-file delete gets a validation error on the safe call and learns to
|
|
332
|
+
// skip it. Read off the schemas rather than asserted by hand.
|
|
333
|
+
const rule = sharedFileRules(testKbContext().layout).find((r) => r.id === 'dry-run-confirm')!;
|
|
334
|
+
const tools = await hexisTools();
|
|
335
|
+
// `toolDef` wraps a tool's own inputs under `body`, which is the schema a
|
|
336
|
+
// client validates against — so that is where the argument either is or is not.
|
|
337
|
+
const takesDryRun = (name: string): boolean => {
|
|
338
|
+
const inputs = tools.find((t) => t.name === name)?.inputs as
|
|
339
|
+
| { properties?: { body?: { properties?: Record<string, unknown> } } }
|
|
340
|
+
| undefined;
|
|
341
|
+
return inputs?.properties?.body?.properties?.dryRun !== undefined;
|
|
342
|
+
};
|
|
343
|
+
expect(takesDryRun('move_file')).toBe(true);
|
|
344
|
+
expect(takesDryRun('delete_folder')).toBe(true);
|
|
345
|
+
expect(takesDryRun('delete_file')).toBe(false);
|
|
346
|
+
// Every tool that takes one, read off the catalog rather than listed here —
|
|
347
|
+
// a hand-written list is what let `copy_file` gain a `dryRun` on dev while
|
|
348
|
+
// the rule still named two tools, so an agent reading the guide was told
|
|
349
|
+
// the copy had no preflight it could run.
|
|
350
|
+
const withDryRun = tools.filter((t) => takesDryRun(t.name)).map((t) => t.name);
|
|
351
|
+
expect(withDryRun.length, 'no tool takes a dryRun — the rule would be vacuous').toBeGreaterThan(1);
|
|
352
|
+
for (const name of withDryRun) {
|
|
353
|
+
expect(rule.body, name).toContain(name);
|
|
354
|
+
}
|
|
355
|
+
// Named, but as the tool that has none — never as one that takes one.
|
|
356
|
+
expect(rule.body).toContain('delete_file takes neither');
|
|
357
|
+
expect(rule.body).not.toContain('move_file, delete_file and delete_folder take');
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('gives each platform file the depth it actually counts at', () => {
|
|
361
|
+
const rule = sharedFileRules(testKbContext().layout).find((r) => r.id === 'managed-items')!;
|
|
362
|
+
const { anyDepth, rootOnly } = platformFilesByDepth(testKbContext().layout);
|
|
363
|
+
// The split is the half of the rule a list of names leaves out, and
|
|
364
|
+
// `isPlatformFile` is the predicate the prose has to match.
|
|
365
|
+
expect(rule.body).toContain(`${anyDepth.map((n) => `\`${n}\``).join(' or ')} in any folder`);
|
|
366
|
+
expect(rule.body).toContain(`${rootOnly.map((n) => `\`${n}\``).join(' or ')} at the repository root`);
|
|
367
|
+
for (const name of anyDepth) expect(isPlatformFile(`Deep/Folder/${name}`, testKbContext().layout), name).toBe(true);
|
|
368
|
+
for (const name of rootOnly) expect(isPlatformFile(`Deep/Folder/${name}`, testKbContext().layout), name).toBe(false);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('says what unzip extracts, rather than that it takes any bytes', () => {
|
|
372
|
+
const rule = sharedFileRules(testKbContext().layout).find((r) => r.id === 'content-kinds')!;
|
|
373
|
+
// `unzip` refuses anything but a `.zip` (`workspace.service.ts`: "Only .zip
|
|
374
|
+
// files can be extracted"), so the byte-tool clause must not sweep it in.
|
|
375
|
+
expect(rule.body).toContain('unzip extracts the entries of a `.zip`');
|
|
376
|
+
expect(rule.body).not.toContain('and unzip act on bytes of any kind');
|
|
377
|
+
});
|
|
378
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How long a tool description may be, and how to measure one.
|
|
3
|
+
*
|
|
4
|
+
* Clients cut a long description, and they cut it from the END — which is
|
|
5
|
+
* where the text specific to the tool sits, after whatever shared preamble it
|
|
6
|
+
* carried. Agents reported `file_stat`, `read_file`, `write_file` and
|
|
7
|
+
* `write_files` arriving ending in "[truncated]". The rules those descriptions
|
|
8
|
+
* shared now live in one place (see `agent-instructions/shared-file-rules.ts`)
|
|
9
|
+
* and each description ends with one sentence pointing there, which is what
|
|
10
|
+
* makes the cap below reachable rather than aspirational.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { TOOL_PREFIX_CAP } from '@bevel-software/platform-shared';
|
|
14
|
+
import { PREFIXED_TOOLS } from '../agent-instructions/compose.js';
|
|
15
|
+
import { SHARED_RULES_POINTER_MAX, sharedRulesPointer } from '../agent-instructions/shared-file-rules.js';
|
|
16
|
+
import type { UtcpTool } from './tool.contract.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The ceiling on what a client is handed for one tool, enforced by
|
|
20
|
+
* `__tests__/tool-description-length.test.ts`.
|
|
21
|
+
*
|
|
22
|
+
* DERIVED, not published: the clients that cut descriptions do not say where.
|
|
23
|
+
* Two cuts were observed, and they are different problems. claude.ai cuts
|
|
24
|
+
* around 500 characters — NOT what this cap answers, and not something a cap
|
|
25
|
+
* could answer: no useful description of `move_file` fits in 500. What answers
|
|
26
|
+
* that one is the ORDER of the text, which is why the deployment's purpose line
|
|
27
|
+
* is prepended rather than appended (see `prefixToolDescription`) and why every
|
|
28
|
+
* description now leads with what the tool does and ends with the pointer: a
|
|
29
|
+
* cut at 500 then takes the pointer and leaves the tool. The other cut is the
|
|
30
|
+
* four-figure one agents reported on `file_stat`, `read_file`, `write_file` and
|
|
31
|
+
* `write_files`, and 1,200 sits below it with room to spare.
|
|
32
|
+
*
|
|
33
|
+
* So the cap is a ceiling on growth rather than a guarantee of survival, and it
|
|
34
|
+
* is a number the reviewer may move: the point of pinning it is that moving it
|
|
35
|
+
* is a decision someone takes, rather than a paragraph someone appends.
|
|
36
|
+
*/
|
|
37
|
+
export const TOOL_DESCRIPTION_CAP = 1_200;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The OTHER cut — the ~500 characters claude.ai allows — as a number the tests
|
|
41
|
+
* can hold something to.
|
|
42
|
+
*
|
|
43
|
+
* It is deliberately not the value of {@link TOOL_DESCRIPTION_CAP}, and the
|
|
44
|
+
* difference is the whole point: lowering the cap to 500 would not make these
|
|
45
|
+
* descriptions survive that client, it would only move the loss from the client
|
|
46
|
+
* to the source, because no useful description of `move_file` or `file_stat`
|
|
47
|
+
* fits in 500 characters and shortening them to fit means dropping facts an
|
|
48
|
+
* agent needs.
|
|
49
|
+
*
|
|
50
|
+
* What survives a cut here is decided by ORDER instead, and order is testable:
|
|
51
|
+
* the purpose prefix goes first, the tool's own opening sentence next, the
|
|
52
|
+
* pointer to the shared rules last. So a cut at 500 takes the pointer — which
|
|
53
|
+
* costs the agent the name of a file the handshake instructions and the guide
|
|
54
|
+
* both state anyway — and leaves the sentence saying what the tool does.
|
|
55
|
+
* {@link firstSentenceEnd} measures where that sentence ends, and the suite
|
|
56
|
+
* pins it under this number for every tool, so the claim above is a check
|
|
57
|
+
* rather than a comment.
|
|
58
|
+
*/
|
|
59
|
+
export const CLIENT_SHORT_CUT = 500;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Where the tool's OWN opening sentence ends in the text a client is handed:
|
|
63
|
+
* the purpose prefix counted at its cap, as in {@link clientVisibleLength}, and
|
|
64
|
+
* the pointer not counted at all, since it is the part a short cut is meant to
|
|
65
|
+
* take.
|
|
66
|
+
*
|
|
67
|
+
* A description with no sentence-ending punctuation counts whole — the honest
|
|
68
|
+
* answer for text that never finishes a sentence.
|
|
69
|
+
*/
|
|
70
|
+
export function firstSentenceEnd(tool: Pick<UtcpTool, 'name' | 'description'>): number {
|
|
71
|
+
const prefix = PREFIXED_TOOLS.has(tool.name) ? TOOL_PREFIX_CAP + 2 : 0;
|
|
72
|
+
const description = tool.description ?? '';
|
|
73
|
+
if (description === '') return prefix;
|
|
74
|
+
const pointer = sharedRulesPointer();
|
|
75
|
+
const own = description.endsWith(pointer) ? description.slice(0, -pointer.length) : description;
|
|
76
|
+
const firstSentence = own.match(/^[\s\S]*?[.!?](?=\s|$)/)?.[0] ?? own;
|
|
77
|
+
return prefix + firstSentence.length;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The length of the description as a CLIENT receives it — which for the four
|
|
82
|
+
* knowledge-base tools includes the deployment's purpose prefix, since the MCP
|
|
83
|
+
* surface prepends it (`prefixToolDescription`) and the client cuts the result.
|
|
84
|
+
* Measured at the prefix's CAP rather than at whatever the current admin wrote:
|
|
85
|
+
* the cap is what an admin may grow their text to without being told, so a
|
|
86
|
+
* description that only fits beside a short prefix does not really fit.
|
|
87
|
+
*
|
|
88
|
+
* The pointer sentence is measured the same way, for the same reason. It ends
|
|
89
|
+
* every file tool's description and its length moves with a DEPLOYMENT SETTING
|
|
90
|
+
* — the guide's file name — so a description measured beside the nine
|
|
91
|
+
* characters of `AGENTS.md` would pass here and arrive cut on a deployment
|
|
92
|
+
* that renamed its guide. Whatever pointer a description actually carries is
|
|
93
|
+
* discounted and charged at {@link SHARED_RULES_POINTER_MAX} instead.
|
|
94
|
+
*/
|
|
95
|
+
export function clientVisibleLength(tool: Pick<UtcpTool, 'name' | 'description'>): number {
|
|
96
|
+
const own = tool.description?.length ?? 0;
|
|
97
|
+
// A prefixed tool with no description of its own is still handed the prefix,
|
|
98
|
+
// and nothing else — `prefixToolDescription` sends the prefix alone, with no
|
|
99
|
+
// blank line after it. Measuring that as zero would under-report the only
|
|
100
|
+
// text the client got.
|
|
101
|
+
if (own === 0) return PREFIXED_TOOLS.has(tool.name) ? TOOL_PREFIX_CAP : 0;
|
|
102
|
+
// The pointer at its worst case rather than at this layout's: swap the one
|
|
103
|
+
// it carries for the longest it could be. A description that does not end
|
|
104
|
+
// with it (`start_session`, the proxied tools) is charged nothing.
|
|
105
|
+
const pointer = sharedRulesPointer();
|
|
106
|
+
const atWorstPointer = tool.description!.endsWith(pointer)
|
|
107
|
+
? own - pointer.length + SHARED_RULES_POINTER_MAX
|
|
108
|
+
: own;
|
|
109
|
+
// `+ 2` for the blank line `prefixToolDescription` puts between the two.
|
|
110
|
+
return PREFIXED_TOOLS.has(tool.name) ? atWorstPointer + TOOL_PREFIX_CAP + 2 : atWorstPointer;
|
|
111
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import type { SQL } from 'drizzle-orm';
|
|
3
|
+
import { PgDialect } from 'drizzle-orm/pg-core';
|
|
4
|
+
import { PendingCommitsService } from '../pending-commits.service.js';
|
|
5
|
+
import type { Database } from '../../database/connection.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Commits still queued for a branch when the knowledge-base repository is
|
|
9
|
+
* REPLACED.
|
|
10
|
+
*
|
|
11
|
+
* A release enqueues the bytes and only then drops its lock, so the moment the
|
|
12
|
+
* locks on a branch are dropped there may be rows already waiting — written
|
|
13
|
+
* against a working copy of the repository that is gone. Left `pending`, the
|
|
14
|
+
* worker picks them up after the re-clone and commits them onto a same-named
|
|
15
|
+
* branch of a DIFFERENT repository: somebody else's file, overwritten by a
|
|
16
|
+
* change nobody made to it.
|
|
17
|
+
*
|
|
18
|
+
* So they are escalated, never dropped: the bytes stay in the row, the reason
|
|
19
|
+
* is on it, and a person decides.
|
|
20
|
+
*/
|
|
21
|
+
function makeFakeDb(moved: Array<{ id: string }>) {
|
|
22
|
+
const sets: Record<string, unknown>[] = [];
|
|
23
|
+
const db = {
|
|
24
|
+
update: vi.fn(() => ({
|
|
25
|
+
set: (values: Record<string, unknown>) => {
|
|
26
|
+
sets.push(values);
|
|
27
|
+
return { where: () => ({ returning: async () => moved }) };
|
|
28
|
+
},
|
|
29
|
+
})),
|
|
30
|
+
// Queued bytes are never thrown away over this.
|
|
31
|
+
delete: () => {
|
|
32
|
+
throw new Error('a queued commit must never be deleted');
|
|
33
|
+
},
|
|
34
|
+
} as unknown as Database;
|
|
35
|
+
return { db, sets };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('PendingCommitsService.markNeedsAttentionOnBranch', () => {
|
|
39
|
+
it('escalates every queued commit on the branch, with the reason, and answers how many', async () => {
|
|
40
|
+
const { db, sets } = makeFakeDb([{ id: 'a' }, { id: 'b' }]);
|
|
41
|
+
const svc = new PendingCommitsService(db);
|
|
42
|
+
|
|
43
|
+
await expect(
|
|
44
|
+
svc.markNeedsAttentionOnBranch('alice/draft', 'The repository was replaced.'),
|
|
45
|
+
).resolves.toBe(2);
|
|
46
|
+
|
|
47
|
+
expect(sets).toHaveLength(1);
|
|
48
|
+
// `needs_attention` is the status the admin surface reads — the row is out
|
|
49
|
+
// of the worker's reach and in front of a person.
|
|
50
|
+
expect(sets[0]?.status).toBe('needs_attention');
|
|
51
|
+
expect(sets[0]?.lastError).toBe('The repository was replaced.');
|
|
52
|
+
expect(sets[0]?.lastAttemptedAt).toBeInstanceOf(Date);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('answers zero when nothing was queued for that branch', async () => {
|
|
56
|
+
const { db } = makeFakeDb([]);
|
|
57
|
+
const svc = new PendingCommitsService(db);
|
|
58
|
+
await expect(svc.markNeedsAttentionOnBranch('quiet/branch', 'whatever')).resolves.toBe(0);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The same, keyed on the WORKING COPY that was set aside. Closing change
|
|
64
|
+
* requests covers their branches and nothing else; a commit queued on the
|
|
65
|
+
* default branch was written against the repository that was left all the
|
|
66
|
+
* same, and the path it names is about to hold a clone of another one.
|
|
67
|
+
*/
|
|
68
|
+
describe('PendingCommitsService.markNeedsAttentionInWorkspace', () => {
|
|
69
|
+
it('escalates every queued commit for the working copy, with the reason, and answers how many', async () => {
|
|
70
|
+
const { db, sets } = makeFakeDb([{ id: 'a' }, { id: 'b' }, { id: 'c' }]);
|
|
71
|
+
const svc = new PendingCommitsService(db);
|
|
72
|
+
|
|
73
|
+
await expect(svc.markNeedsAttentionInWorkspace('main', 'The repository was replaced.')).resolves.toBe(3);
|
|
74
|
+
|
|
75
|
+
expect(sets).toHaveLength(1);
|
|
76
|
+
expect(sets[0]?.status).toBe('needs_attention');
|
|
77
|
+
expect(sets[0]?.lastError).toBe('The repository was replaced.');
|
|
78
|
+
expect(sets[0]?.lastAttemptedAt).toBeInstanceOf(Date);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Rows are stored under the encoded id, whichever spelling the caller
|
|
83
|
+
* enqueued with. The predicate is rendered and read, since the stub does
|
|
84
|
+
* not evaluate it: the id it compares must be the stored spelling, and it
|
|
85
|
+
* must match only the two statuses a worker could still write, `pending`
|
|
86
|
+
* and `running`.
|
|
87
|
+
*/
|
|
88
|
+
it('looks for the working copy under the id the queue stores, and only for commits still queued', async () => {
|
|
89
|
+
const wheres: SQL[] = [];
|
|
90
|
+
const db = {
|
|
91
|
+
update: () => ({
|
|
92
|
+
set: () => ({
|
|
93
|
+
where: (predicate: SQL) => {
|
|
94
|
+
wheres.push(predicate);
|
|
95
|
+
return { returning: async () => [] };
|
|
96
|
+
},
|
|
97
|
+
}),
|
|
98
|
+
}),
|
|
99
|
+
} as unknown as Database;
|
|
100
|
+
await new PendingCommitsService(db).markNeedsAttentionInWorkspace('alice/feature', 'whatever');
|
|
101
|
+
|
|
102
|
+
const { sql: text, params } = new PgDialect().sqlToQuery(wheres[0]!);
|
|
103
|
+
expect(text).toContain('"workspace_id" =');
|
|
104
|
+
expect(params).toEqual(['alice%2Ffeature', 'pending', 'running']);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('answers zero when nothing was queued for it', async () => {
|
|
108
|
+
const { db } = makeFakeDb([]);
|
|
109
|
+
await expect(new PendingCommitsService(db).markNeedsAttentionInWorkspace('main', 'whatever')).resolves.toBe(0);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -8,6 +8,10 @@ import type { AuthService } from '../../auth/auth.service.js';
|
|
|
8
8
|
import type { WorkspaceService } from '../../workspace/workspace.service.js';
|
|
9
9
|
import type { WorkflowEventBus } from '../event-bus.js';
|
|
10
10
|
import { createWorkflowRoutes } from '../workflow.routes.js';
|
|
11
|
+
import {
|
|
12
|
+
VersionNotOnBranchError,
|
|
13
|
+
VERSION_NOT_ON_BRANCH_MESSAGE,
|
|
14
|
+
} from '../../../shared/domain-errors.js';
|
|
11
15
|
|
|
12
16
|
// A file's history is its content with a time axis: the same default-deny
|
|
13
17
|
// read model that hides a file must hide its commit list, its diffs, and its
|
|
@@ -129,6 +133,27 @@ describe('history routes enforce the read model', () => {
|
|
|
129
133
|
expect((await get(`/compare-file?from=a&to=b&path=${OPEN}`)).status).toBe(403);
|
|
130
134
|
});
|
|
131
135
|
|
|
136
|
+
it.each([
|
|
137
|
+
['/show-file?sha=abc1234&path=%s', 'showFileAtChange'],
|
|
138
|
+
['/file-at-change?sha=abc1234&path=%s', 'fileAtChange'],
|
|
139
|
+
] as const)(
|
|
140
|
+
'%s: a save off this branch is refused with the history message',
|
|
141
|
+
async (route, method) => {
|
|
142
|
+
// The service raises it (the git layer knows the branch); the route has
|
|
143
|
+
// to pass the typed refusal through with its own status and sentence,
|
|
144
|
+
// because a reader who is told "not found" about a file that is plainly
|
|
145
|
+
// there learns the wrong thing. The bytes route carries the same rule,
|
|
146
|
+
// so neither can be read around through the other.
|
|
147
|
+
h = await makeHarness();
|
|
148
|
+
h.workflow[method].mockRejectedValueOnce(new VersionNotOnBranchError());
|
|
149
|
+
const res = await get(route.replace('%s', OPEN));
|
|
150
|
+
expect(res.status).toBe(404);
|
|
151
|
+
expect(((await res.json()) as { error: string }).error).toBe(
|
|
152
|
+
VERSION_NOT_ON_BRANCH_MESSAGE,
|
|
153
|
+
);
|
|
154
|
+
},
|
|
155
|
+
);
|
|
156
|
+
|
|
132
157
|
it('an access-model error fails closed, not open', async () => {
|
|
133
158
|
h = await makeHarness();
|
|
134
159
|
h.canRead.mockRejectedValueOnce(new Error('access tree unreadable'));
|