@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,170 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { testKbContext } from '../../../../__tests__/kb-context.js';
|
|
3
|
+
import { execFile } from 'node:child_process';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import fs from 'node:fs/promises';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import type { WorkspaceService } from '../../../workspace/workspace.service.js';
|
|
9
|
+
import { WorkflowHooks } from '../../workflow-hooks.js';
|
|
10
|
+
import { GitService } from '../git.service.js';
|
|
11
|
+
|
|
12
|
+
const execFileAsync = promisify(execFile);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* What the log says when a change request's branches cannot be fetched.
|
|
16
|
+
*
|
|
17
|
+
* The fetch is best-effort: when it fails, the refs are resolved from what the
|
|
18
|
+
* working copy already has. A working copy that could not reach the remote at
|
|
19
|
+
* all then reported `unknown branch` for every branch it had never seen, and
|
|
20
|
+
* nothing said the fetch had been refused — the log named a missing branch
|
|
21
|
+
* that was on the remote the whole time.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const TRUNK = 'current-company-state';
|
|
25
|
+
const WORKSPACE = 'ws-pr-fetch';
|
|
26
|
+
|
|
27
|
+
async function runGit(cwd: string, args: string[]): Promise<void> {
|
|
28
|
+
await execFileAsync('git', args, {
|
|
29
|
+
cwd,
|
|
30
|
+
env: {
|
|
31
|
+
...process.env,
|
|
32
|
+
GIT_AUTHOR_NAME: 'Test', GIT_AUTHOR_EMAIL: 't@x.com',
|
|
33
|
+
GIT_COMMITTER_NAME: 'Test', GIT_COMMITTER_EMAIL: 't@x.com',
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let root: string;
|
|
39
|
+
let repo: string;
|
|
40
|
+
let upstream: string;
|
|
41
|
+
|
|
42
|
+
beforeEach(async () => {
|
|
43
|
+
root = await fs.mkdtemp(path.join(os.tmpdir(), 'git-pr-fetch-'));
|
|
44
|
+
upstream = path.join(root, 'upstream.git');
|
|
45
|
+
await runGit(root, ['init', '--bare', '-b', TRUNK, upstream]);
|
|
46
|
+
const seed = path.join(root, '.seed');
|
|
47
|
+
await fs.mkdir(seed);
|
|
48
|
+
await runGit(seed, ['init', '-b', TRUNK]);
|
|
49
|
+
await runGit(seed, ['remote', 'add', 'origin', upstream]);
|
|
50
|
+
await fs.writeFile(path.join(seed, 'base.md'), 'base\n');
|
|
51
|
+
await runGit(seed, ['add', '-A']);
|
|
52
|
+
await runGit(seed, ['commit', '-m', 'init']);
|
|
53
|
+
await runGit(seed, ['push', 'origin', TRUNK]);
|
|
54
|
+
// A draft the working copy below has never seen: pushed after it was cloned.
|
|
55
|
+
repo = path.join(root, WORKSPACE, 'knowledge-base');
|
|
56
|
+
await fs.mkdir(path.dirname(repo), { recursive: true });
|
|
57
|
+
await runGit(root, ['clone', upstream, repo]);
|
|
58
|
+
await runGit(seed, ['checkout', '-b', 'alice/feature']);
|
|
59
|
+
await fs.writeFile(path.join(seed, 'new.md'), 'new\n');
|
|
60
|
+
await runGit(seed, ['add', '-A']);
|
|
61
|
+
await runGit(seed, ['commit', '-m', 'feature']);
|
|
62
|
+
await runGit(seed, ['push', 'origin', 'alice/feature']);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
afterEach(async () => {
|
|
66
|
+
vi.restoreAllMocks();
|
|
67
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
function service(): GitService {
|
|
71
|
+
const workspaces = {
|
|
72
|
+
getWorkspacePath: async () => path.dirname(repo),
|
|
73
|
+
} as unknown as WorkspaceService;
|
|
74
|
+
return new GitService(workspaces, new WorkflowHooks(), testKbContext());
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Everything the service logged at warn level, as one string per line. */
|
|
78
|
+
function warnings(spy: ReturnType<typeof vi.spyOn>): string[] {
|
|
79
|
+
return spy.mock.calls.map((call) => call.map((part) => (typeof part === 'string' ? part : JSON.stringify(part))).join(' '));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
describe('a change request whose branches cannot be fetched', () => {
|
|
83
|
+
it('says the fetch failed, and where, when the working copy cannot reach the remote', async () => {
|
|
84
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
85
|
+
// The remote this working copy fetches from is gone from under it.
|
|
86
|
+
await runGit(repo, ['remote', 'set-url', 'origin', path.join(root, 'no-such-remote.git')]);
|
|
87
|
+
|
|
88
|
+
await expect(service().changedPathsForPr(WORKSPACE, TRUNK, 'alice/feature')).rejects.toThrow(
|
|
89
|
+
'unknown branch: alice/feature',
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const said = warnings(warn).filter((line) => line.includes('could not fetch change-request branches'));
|
|
93
|
+
expect(said).toHaveLength(1);
|
|
94
|
+
expect(said[0]).toContain(`"${WORKSPACE}"`);
|
|
95
|
+
expect(said[0]).toContain('though it may be on the remote');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('says it once for a list that asks about many requests in one working copy', async () => {
|
|
99
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
100
|
+
await runGit(repo, ['remote', 'set-url', 'origin', path.join(root, 'no-such-remote.git')]);
|
|
101
|
+
const git = service();
|
|
102
|
+
|
|
103
|
+
for (const branch of ['alice/feature', 'bob/other', 'carol/third']) {
|
|
104
|
+
await git.changedPathsForPr(WORKSPACE, TRUNK, branch).catch(() => undefined);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
expect(warnings(warn).filter((line) => line.includes('could not fetch change-request branches'))).toHaveLength(1);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('says it again once a minute has passed, and not before', async () => {
|
|
111
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
112
|
+
await runGit(repo, ['remote', 'set-url', 'origin', path.join(root, 'no-such-remote.git')]);
|
|
113
|
+
const git = service();
|
|
114
|
+
const said = () => warnings(warn).filter((line) => line.includes('could not fetch change-request branches'));
|
|
115
|
+
// Only the clock the service reads is moved: git itself runs for real.
|
|
116
|
+
const realNow = Date.now.bind(Date);
|
|
117
|
+
let ahead = 0;
|
|
118
|
+
vi.spyOn(Date, 'now').mockImplementation(() => realNow() + ahead);
|
|
119
|
+
|
|
120
|
+
await git.changedPathsForPr(WORKSPACE, TRUNK, 'alice/feature').catch(() => undefined);
|
|
121
|
+
expect(said()).toHaveLength(1);
|
|
122
|
+
|
|
123
|
+
ahead = 30_000;
|
|
124
|
+
await git.changedPathsForPr(WORKSPACE, TRUNK, 'alice/feature').catch(() => undefined);
|
|
125
|
+
expect(said()).toHaveLength(1);
|
|
126
|
+
|
|
127
|
+
ahead = 61_000;
|
|
128
|
+
await git.changedPathsForPr(WORKSPACE, TRUNK, 'alice/feature').catch(() => undefined);
|
|
129
|
+
expect(said()).toHaveLength(2);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('says it again at once for a working copy that reached the remote in between', async () => {
|
|
133
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
134
|
+
const git = service();
|
|
135
|
+
const said = () => warnings(warn).filter((line) => line.includes('could not fetch change-request branches'));
|
|
136
|
+
const broken = path.join(root, 'no-such-remote.git');
|
|
137
|
+
|
|
138
|
+
await runGit(repo, ['remote', 'set-url', 'origin', broken]);
|
|
139
|
+
await git.changedPathsForPr(WORKSPACE, TRUNK, 'alice/feature').catch(() => undefined);
|
|
140
|
+
expect(said()).toHaveLength(1);
|
|
141
|
+
|
|
142
|
+
// It reaches the remote again: what was said about it is over, and nothing
|
|
143
|
+
// of it is kept.
|
|
144
|
+
await runGit(repo, ['remote', 'set-url', 'origin', upstream]);
|
|
145
|
+
expect(await git.changedPathsForPr(WORKSPACE, TRUNK, 'alice/feature')).toEqual(['new.md']);
|
|
146
|
+
|
|
147
|
+
// So a failure right after is a new one, said without waiting out a minute.
|
|
148
|
+
await runGit(repo, ['remote', 'set-url', 'origin', broken]);
|
|
149
|
+
await git.changedPathsForPr(WORKSPACE, TRUNK, 'bob/other').catch(() => undefined);
|
|
150
|
+
expect(said()).toHaveLength(2);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('says nothing when the branch is simply gone from the remote: that is what a retired branch looks like', async () => {
|
|
154
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
155
|
+
|
|
156
|
+
await expect(service().changedPathsForPr(WORKSPACE, TRUNK, 'dave/retired')).rejects.toThrow(
|
|
157
|
+
'unknown branch: dave/retired',
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
expect(warnings(warn).filter((line) => line.includes('could not fetch change-request branches'))).toEqual([]);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('says nothing when the fetch works, and finds the branch the working copy had never seen', async () => {
|
|
164
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
165
|
+
|
|
166
|
+
expect(await service().changedPathsForPr(WORKSPACE, TRUNK, 'alice/feature')).toEqual(['new.md']);
|
|
167
|
+
|
|
168
|
+
expect(warnings(warn).filter((line) => line.includes('could not fetch change-request branches'))).toEqual([]);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -35,10 +35,12 @@ import {
|
|
|
35
35
|
ProtectedBranchError,
|
|
36
36
|
PullRebaseConflictError,
|
|
37
37
|
RemoteBranchGoneError,
|
|
38
|
+
VersionNotOnBranchError,
|
|
38
39
|
isMissingRemoteBranchFailure,
|
|
39
40
|
} from '../../../shared/domain-errors.js';
|
|
40
41
|
import {
|
|
41
42
|
GitRunError,
|
|
43
|
+
assertOneSpecPerLine,
|
|
42
44
|
isGitTimeout,
|
|
43
45
|
redactGitToken,
|
|
44
46
|
type GitCredentials,
|
|
@@ -2331,6 +2333,10 @@ export class GitService implements IGitService {
|
|
|
2331
2333
|
const repoRelativePath = this.stripRepoPrefix(relativePath);
|
|
2332
2334
|
return this.mutex.run(workspaceId, async () => {
|
|
2333
2335
|
const cwd = await this.repoDir(workspaceId);
|
|
2336
|
+
// Branch-scoped, like the bytes route: a patch is a past version too,
|
|
2337
|
+
// and leaving this route open would let a sha refused there be read
|
|
2338
|
+
// here instead.
|
|
2339
|
+
await this.assertOnBranchHistory(cwd, sha);
|
|
2334
2340
|
// BOTH sides: at a commit that DELETES a directory the path is absent
|
|
2335
2341
|
// at `sha` but a tree at `sha^` — and the diff of that deletion is
|
|
2336
2342
|
// every child's content, exactly what the per-file gate never checked.
|
|
@@ -2382,6 +2388,10 @@ export class GitService implements IGitService {
|
|
|
2382
2388
|
const repoRelativePath = this.stripRepoPrefix(relativePath);
|
|
2383
2389
|
return this.mutex.run(workspaceId, async () => {
|
|
2384
2390
|
const cwd = await this.repoDir(workspaceId);
|
|
2391
|
+
// Same branch rule as the patch and the bytes: this route serves the
|
|
2392
|
+
// FULL text of both sides, so it is the most generous of the three and
|
|
2393
|
+
// the least safe one to leave ungated.
|
|
2394
|
+
await this.assertOnBranchHistory(cwd, sha);
|
|
2385
2395
|
await this.assertNotTreeAtRef(cwd, sha, repoRelativePath, relativePath);
|
|
2386
2396
|
await this.assertNotTreeAtRef(cwd, `${sha}^`, repoRelativePath, relativePath);
|
|
2387
2397
|
const current = await this.readFileAtRef(workspaceId, sha, repoRelativePath);
|
|
@@ -2949,7 +2959,53 @@ export class GitService implements IGitService {
|
|
|
2949
2959
|
'fetch', '--no-write-fetch-head', 'origin',
|
|
2950
2960
|
`+refs/heads/${baseBranch}:refs/remotes/origin/${baseBranch}`,
|
|
2951
2961
|
`+refs/heads/${headBranch}:refs/remotes/origin/${headBranch}`,
|
|
2952
|
-
]).
|
|
2962
|
+
]).then(
|
|
2963
|
+
() => void this.prFetchFailureLoggedAt.delete(cwd),
|
|
2964
|
+
(err: unknown) => this.notePrFetchFailure(cwd, err),
|
|
2965
|
+
);
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
/**
|
|
2969
|
+
* When each working copy's change-request fetch failure was last logged.
|
|
2970
|
+
* Holds only the working copies that are failing NOW: an entry goes the
|
|
2971
|
+
* moment that copy reaches the remote again, so the table never outgrows the
|
|
2972
|
+
* clones that cannot fetch, however many come and go over a process's life.
|
|
2973
|
+
*/
|
|
2974
|
+
private readonly prFetchFailureLoggedAt = new Map<string, number>();
|
|
2975
|
+
|
|
2976
|
+
/**
|
|
2977
|
+
* Say WHY a change request's branches could not be fetched, when the reason
|
|
2978
|
+
* is not the one that is expected.
|
|
2979
|
+
*
|
|
2980
|
+
* The fetch stays best-effort, so the caller goes on to resolve the refs
|
|
2981
|
+
* from whatever the clone already has. One failure is ordinary and says
|
|
2982
|
+
* nothing: a branch that is gone from the remote, which is what a retired
|
|
2983
|
+
* source branch looks like. Every OTHER failure is the working copy being
|
|
2984
|
+
* unable to reach the remote at all — a credential it does not have, a host
|
|
2985
|
+
* that does not answer — and swallowed, it resurfaced one step later as
|
|
2986
|
+
* `unknown branch` for a branch that was sitting on the remote the whole
|
|
2987
|
+
* time. A deployment logged that for every open change request on every
|
|
2988
|
+
* list, and nothing in the log said the fetch had been refused.
|
|
2989
|
+
*
|
|
2990
|
+
* Logged once a minute per working copy at most: the list asks once per open
|
|
2991
|
+
* request, and the reason is the same for all of them.
|
|
2992
|
+
*/
|
|
2993
|
+
private notePrFetchFailure(cwd: string, err: unknown): void {
|
|
2994
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2995
|
+
if (isMissingRemoteBranchFailure(message)) {
|
|
2996
|
+
// The remote answered, and said the branch is not there: this working
|
|
2997
|
+
// copy reaches it, so whatever was logged about it before is over.
|
|
2998
|
+
this.prFetchFailureLoggedAt.delete(cwd);
|
|
2999
|
+
return;
|
|
3000
|
+
}
|
|
3001
|
+
const now = Date.now();
|
|
3002
|
+
if (now - (this.prFetchFailureLoggedAt.get(cwd) ?? 0) < 60_000) return;
|
|
3003
|
+
this.prFetchFailureLoggedAt.set(cwd, now);
|
|
3004
|
+
log.warn(
|
|
3005
|
+
`could not fetch change-request branches in the working copy "${path.basename(path.dirname(cwd))}": ` +
|
|
3006
|
+
'a branch it has not seen will be reported as unknown, though it may be on the remote.',
|
|
3007
|
+
{ detail: redactGitToken(message, this.credentials.token()) },
|
|
3008
|
+
);
|
|
2953
3009
|
}
|
|
2954
3010
|
|
|
2955
3011
|
/** Unified-diff patch for one file across the CR range (rename-aware). */
|
|
@@ -3269,6 +3325,143 @@ export class GitService implements IGitService {
|
|
|
3269
3325
|
}
|
|
3270
3326
|
}
|
|
3271
3327
|
|
|
3328
|
+
/**
|
|
3329
|
+
* A repo-relative file's BYTES at an arbitrary git ref — the binary sibling
|
|
3330
|
+
* of {@link readFileAtRef}, with the blob's object id alongside them.
|
|
3331
|
+
*
|
|
3332
|
+
* `readFileAtRef` decodes stdout as UTF-8, which for a PNG or a PDF is not a
|
|
3333
|
+
* lossless round trip: every byte sequence that is not valid UTF-8 comes
|
|
3334
|
+
* back as U+FFFD and the file is ruined. A past version has to be delivered
|
|
3335
|
+
* byte for byte, so this reads through `cat-file --batch`, whose framing is
|
|
3336
|
+
* in BYTES (`<oid> <type> <size>\n<size bytes>\n`) rather than characters.
|
|
3337
|
+
*
|
|
3338
|
+
* One invocation, and absence is a PROTOCOL answer (`<spec> missing`) rather
|
|
3339
|
+
* than a parsed error message — which also covers `<sha>^` of a root commit,
|
|
3340
|
+
* where there is no "before" side at all. `null` therefore means exactly
|
|
3341
|
+
* "nothing is at that path on that side".
|
|
3342
|
+
*
|
|
3343
|
+
* The `oid` is the content's own hash, so it is what the HTTP route uses as
|
|
3344
|
+
* an ETag: cheaper than hashing the body again, and stable for a version
|
|
3345
|
+
* that by definition can never change.
|
|
3346
|
+
*/
|
|
3347
|
+
private async blobAtRef(
|
|
3348
|
+
cwd: string,
|
|
3349
|
+
ref: string,
|
|
3350
|
+
repoRelativePath: string,
|
|
3351
|
+
): Promise<{ bytes: Buffer; blobId: string } | null> {
|
|
3352
|
+
// The spec has to be ONE line: the path is refused a control character
|
|
3353
|
+
// twice over before it gets here (at the route's normaliser and again at
|
|
3354
|
+
// `assertValidRelativePath`), and this is the fence at the protocol —
|
|
3355
|
+
// `--batch` would otherwise read `<sha>:Docs/x.md\nzzz` as two object
|
|
3356
|
+
// names and answer about the first, under the gates of the second.
|
|
3357
|
+
const spec = `${ref}:${repoRelativePath}`;
|
|
3358
|
+
assertOneSpecPerLine([spec]);
|
|
3359
|
+
const { stdout } = await this.gitBytes(cwd, ['cat-file', '--batch'], {
|
|
3360
|
+
input: `${spec}\n`,
|
|
3361
|
+
});
|
|
3362
|
+
const nl = stdout.indexOf(0x0a);
|
|
3363
|
+
if (nl < 0) throw new Error('unexpected end of git cat-file output');
|
|
3364
|
+
// The spec may itself contain spaces (a KB file routinely does), hence the
|
|
3365
|
+
// endsWith checks rather than a split — same protocol reading as the
|
|
3366
|
+
// access resolver's `catFileBatch`.
|
|
3367
|
+
const header = stdout.subarray(0, nl).toString('utf-8');
|
|
3368
|
+
if (header.endsWith(' missing') || header.endsWith(' ambiguous')) return null;
|
|
3369
|
+
const [blobId, type, rawSize] = header.split(' ');
|
|
3370
|
+
// A tree here would be the per-file gate bypassed, and `assertNotTreeAtRef`
|
|
3371
|
+
// is what refuses it before this runs; anything else non-blob (a tag, a
|
|
3372
|
+
// commit) is not a file either. Nothing to serve in either case.
|
|
3373
|
+
if (type !== 'blob' || blobId === undefined) return null;
|
|
3374
|
+
const size = Number(rawSize);
|
|
3375
|
+
if (!Number.isSafeInteger(size) || size < 0) {
|
|
3376
|
+
throw new Error(`unexpected git cat-file header: ${header}`);
|
|
3377
|
+
}
|
|
3378
|
+
return { bytes: stdout.subarray(nl + 1, nl + 1 + size), blobId };
|
|
3379
|
+
}
|
|
3380
|
+
|
|
3381
|
+
/**
|
|
3382
|
+
* Refuse a sha that is not in the history of the branch this workspace has
|
|
3383
|
+
* checked out.
|
|
3384
|
+
*
|
|
3385
|
+
* A file's past is served per BRANCH: the history panel lists what `git log`
|
|
3386
|
+
* on this workspace's own branch touched, and every read of a past save has
|
|
3387
|
+
* to stay inside that list. Without this, a sha lifted from another branch —
|
|
3388
|
+
* or from a change request nobody approved — would be readable through the
|
|
3389
|
+
* workspace of a branch whose history never contained it, which is the
|
|
3390
|
+
* access rules of the viewed branch bypassed by a query parameter.
|
|
3391
|
+
*
|
|
3392
|
+
* `merge-base --is-ancestor` is the exact question, and a commit is its own
|
|
3393
|
+
* ancestor, so the branch head passes. Exit 1 is git's answer for "resolved,
|
|
3394
|
+
* and not an ancestor". An unresolvable object name — a made-up id — gets
|
|
3395
|
+
* the SAME refusal on purpose (see {@link VersionNotOnBranchError}), but only
|
|
3396
|
+
* when git's complaint NAMES that id; every other failure propagates, a
|
|
3397
|
+
* broken repository included. A guard that fails open on its own errors is
|
|
3398
|
+
* not a guard, and one that reports them as a refusal is worse than no
|
|
3399
|
+
* message at all.
|
|
3400
|
+
*/
|
|
3401
|
+
private async assertOnBranchHistory(cwd: string, sha: string): Promise<void> {
|
|
3402
|
+
try {
|
|
3403
|
+
await this.git(cwd, ['merge-base', '--is-ancestor', sha, 'HEAD']);
|
|
3404
|
+
} catch (err) {
|
|
3405
|
+
if (err instanceof GitRunError && !err.timedOut) {
|
|
3406
|
+
if (err.exitCode === 1) throw new VersionNotOnBranchError();
|
|
3407
|
+
const stderr = err.stderr ?? err.message;
|
|
3408
|
+
if (
|
|
3409
|
+
// The complaint has to be about THE SHA WE ASKED FOR. `HEAD` is the
|
|
3410
|
+
// other argument, and git spells its own failures the same way: a
|
|
3411
|
+
// repository with no commits answers "ambiguous argument 'HEAD'",
|
|
3412
|
+
// which is a broken workspace, not a version off the branch.
|
|
3413
|
+
// Refusing it as "not in this file's history" would hide an
|
|
3414
|
+
// infrastructure failure behind a sentence about access.
|
|
3415
|
+
stderr.toLowerCase().includes(sha.toLowerCase()) &&
|
|
3416
|
+
// git's wording for "I cannot resolve that" varies by subcommand and
|
|
3417
|
+
// by version — `merge-base` says "Not a valid commit name", others
|
|
3418
|
+
// "not a valid object name" / "unknown revision" — so every spelling
|
|
3419
|
+
// that means the same thing is listed rather than one guessed at.
|
|
3420
|
+
/not a valid (object|commit) name|no such commit|bad object|unknown revision|malformed object name|ambiguous argument/i.test(
|
|
3421
|
+
stderr,
|
|
3422
|
+
)
|
|
3423
|
+
) {
|
|
3424
|
+
throw new VersionNotOnBranchError();
|
|
3425
|
+
}
|
|
3426
|
+
}
|
|
3427
|
+
throw err;
|
|
3428
|
+
}
|
|
3429
|
+
}
|
|
3430
|
+
|
|
3431
|
+
/**
|
|
3432
|
+
* One file's BYTES at a save, exactly as they were — the `after` side of
|
|
3433
|
+
* `sha`, or the `before` side (`<sha>^`) for the save that deleted it.
|
|
3434
|
+
*
|
|
3435
|
+
* Every guard the text history surfaces apply, in the same order: the sha is
|
|
3436
|
+
* a sha, the save is on this branch, and the pathspec is not a directory at
|
|
3437
|
+
* that ref (a per-file gate upstream authorized one path, and a tree read
|
|
3438
|
+
* would hand over every child). `null` means the file was not there on the
|
|
3439
|
+
* side asked for.
|
|
3440
|
+
*
|
|
3441
|
+
* Runs inside the workspace's git turn — one turn for the whole read, so the
|
|
3442
|
+
* ancestry check and the bytes it authorizes cannot be separated by another
|
|
3443
|
+
* caller's checkout.
|
|
3444
|
+
*/
|
|
3445
|
+
async fileBytesAtCommit(
|
|
3446
|
+
workspaceId: string,
|
|
3447
|
+
relativePath: string,
|
|
3448
|
+
sha: string,
|
|
3449
|
+
side: 'after' | 'before',
|
|
3450
|
+
): Promise<{ bytes: Buffer; blobId: string } | null> {
|
|
3451
|
+
assertValidRelativePath(relativePath);
|
|
3452
|
+
if (!/^[a-f0-9]{7,40}$/i.test(sha)) {
|
|
3453
|
+
throw new WorkflowValidationError('invalid commit sha');
|
|
3454
|
+
}
|
|
3455
|
+
const repoRelativePath = this.stripRepoPrefix(relativePath);
|
|
3456
|
+
return this.mutex.run(workspaceId, async () => {
|
|
3457
|
+
const cwd = await this.repoDir(workspaceId);
|
|
3458
|
+
await this.assertOnBranchHistory(cwd, sha);
|
|
3459
|
+
const ref = side === 'before' ? `${sha}^` : sha;
|
|
3460
|
+
await this.assertNotTreeAtRef(cwd, ref, repoRelativePath, relativePath);
|
|
3461
|
+
return this.blobAtRef(cwd, ref, repoRelativePath);
|
|
3462
|
+
});
|
|
3463
|
+
}
|
|
3464
|
+
|
|
3272
3465
|
/**
|
|
3273
3466
|
* The commits on `ref` that touched `repoRelativePath`, newest first, at
|
|
3274
3467
|
* most `limit` of them. What a skill's `version` is looked up through: each
|
|
@@ -3377,6 +3570,27 @@ export class GitService implements IGitService {
|
|
|
3377
3570
|
env: { GIT_LITERAL_PATHSPECS: '1', ...opts?.env },
|
|
3378
3571
|
});
|
|
3379
3572
|
}
|
|
3573
|
+
|
|
3574
|
+
/**
|
|
3575
|
+
* {@link git}, with stdout as raw BYTES.
|
|
3576
|
+
*
|
|
3577
|
+
* A sibling rather than an `encoding` option on `git()` itself: every other
|
|
3578
|
+
* caller in this service reads text, and the two return types (`string` vs
|
|
3579
|
+
* `Buffer`) are not interchangeable at any call site. Same
|
|
3580
|
+
* `GIT_LITERAL_PATHSPECS` and same runner deadline — the only difference is
|
|
3581
|
+
* that the bytes are not decoded.
|
|
3582
|
+
*/
|
|
3583
|
+
private gitBytes(
|
|
3584
|
+
cwd: string,
|
|
3585
|
+
args: string[],
|
|
3586
|
+
opts?: Omit<GitRunOptions, 'encoding'>,
|
|
3587
|
+
): Promise<GitRunResult<Buffer>> {
|
|
3588
|
+
return this.gitRunner.run(cwd, args, {
|
|
3589
|
+
...opts,
|
|
3590
|
+
encoding: 'buffer',
|
|
3591
|
+
env: { GIT_LITERAL_PATHSPECS: '1', ...opts?.env },
|
|
3592
|
+
});
|
|
3593
|
+
}
|
|
3380
3594
|
}
|
|
3381
3595
|
|
|
3382
3596
|
/** Map a git status letter (`git diff --name-status`) to the DTO's `PrFileStatus`. */
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
* (status='needs_attention')
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
|
-
import { and, eq, inArray, isNull, min, or, sql } from 'drizzle-orm';
|
|
37
|
+
import { and, eq, inArray, isNull, min, or, sql, type SQL } from 'drizzle-orm';
|
|
38
38
|
import { logger } from '../../shared/logging.js';
|
|
39
39
|
|
|
40
40
|
const log = logger('pending-commits');
|
|
@@ -195,6 +195,7 @@ export class PendingCommitsService {
|
|
|
195
195
|
.set({
|
|
196
196
|
queuedAt: new Date(),
|
|
197
197
|
authorEmail: email,
|
|
198
|
+
authorEmailBidx: email,
|
|
198
199
|
authorName: input.authorName,
|
|
199
200
|
// Reset transient counters — this is effectively a fresh enqueue.
|
|
200
201
|
attempts: 0,
|
|
@@ -216,6 +217,7 @@ export class PendingCommitsService {
|
|
|
216
217
|
branch: input.branch,
|
|
217
218
|
path: input.path,
|
|
218
219
|
authorEmail: email,
|
|
220
|
+
authorEmailBidx: email,
|
|
219
221
|
authorName: input.authorName,
|
|
220
222
|
});
|
|
221
223
|
}
|
|
@@ -249,11 +251,13 @@ export class PendingCommitsService {
|
|
|
249
251
|
),
|
|
250
252
|
);
|
|
251
253
|
if ((rows[0]?.count ?? 0) > 0) return false;
|
|
254
|
+
const email = canonicalEmail(input.authorEmail);
|
|
252
255
|
await this.db.insert(pendingCommits).values({
|
|
253
256
|
workspaceId,
|
|
254
257
|
branch: input.branch,
|
|
255
258
|
path: input.path,
|
|
256
|
-
authorEmail:
|
|
259
|
+
authorEmail: email,
|
|
260
|
+
authorEmailBidx: email,
|
|
257
261
|
authorName: input.authorName,
|
|
258
262
|
});
|
|
259
263
|
return true;
|
|
@@ -384,6 +388,64 @@ export class PendingCommitsService {
|
|
|
384
388
|
.where(eq(pendingCommits.id, id));
|
|
385
389
|
}
|
|
386
390
|
|
|
391
|
+
/**
|
|
392
|
+
* Take every commit still queued for `branch` out of the worker's reach and
|
|
393
|
+
* onto the admin surface, and answer how many moved.
|
|
394
|
+
*
|
|
395
|
+
* The one caller is the knowledge-base repository being REPLACED. A release
|
|
396
|
+
* ENQUEUES the bytes and only then drops its lock, so the moment the locks
|
|
397
|
+
* on a branch are dropped there may be rows already waiting — bytes that
|
|
398
|
+
* were written against a working copy of the repository that is gone. Left
|
|
399
|
+
* `pending`, the worker would pick them up after the re-clone and commit
|
|
400
|
+
* them onto a same-named branch of a DIFFERENT repository: someone else's
|
|
401
|
+
* file, overwritten by a change nobody made to it.
|
|
402
|
+
*
|
|
403
|
+
* `needs_attention`, not deleted — the same rule as everything else about a
|
|
404
|
+
* replacement. The bytes are still in the row, the reason is on it, and they
|
|
405
|
+
* show up on the pending-commit admin surface for a person to decide about.
|
|
406
|
+
* `running` rows go too: the worker checks the row back in when it finishes,
|
|
407
|
+
* and a row it is mid-flight with is exactly one whose destination just
|
|
408
|
+
* changed under it.
|
|
409
|
+
*/
|
|
410
|
+
async markNeedsAttentionOnBranch(branch: string, error: string): Promise<number> {
|
|
411
|
+
return this.escalateQueued(eq(pendingCommits.branch, branch), error);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** Every commit still queued that `which` selects goes to a person, with the reason. Answers how many. */
|
|
415
|
+
private async escalateQueued(which: SQL, error: string): Promise<number> {
|
|
416
|
+
const moved = await this.db
|
|
417
|
+
.update(pendingCommits)
|
|
418
|
+
.set({
|
|
419
|
+
status: 'needs_attention',
|
|
420
|
+
lastError: error,
|
|
421
|
+
lastAttemptedAt: new Date(),
|
|
422
|
+
})
|
|
423
|
+
.where(and(which, inArray(pendingCommits.status, ['pending', 'running'])))
|
|
424
|
+
.returning({ id: pendingCommits.id });
|
|
425
|
+
return moved.length;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* The same, for every commit still queued against ONE WORKING COPY, and
|
|
430
|
+
* answer how many moved.
|
|
431
|
+
*
|
|
432
|
+
* The caller is the startup phase setting a working copy aside: a clone of
|
|
433
|
+
* a repository the configured one shares no history with. Whatever was
|
|
434
|
+
* queued for it was written against that clone, on any branch, the default
|
|
435
|
+
* one included, and the directory the worker would write to is now a fresh
|
|
436
|
+
* clone of another repository. Closing change requests covers the branches
|
|
437
|
+
* of the requests that were closed and nothing else, which is why this is
|
|
438
|
+
* keyed on the working copy: it is the thing that was replaced.
|
|
439
|
+
*
|
|
440
|
+
* A working copy that was only pointed at a new address (the same
|
|
441
|
+
* repository, moved) keeps its queue, and is never passed here.
|
|
442
|
+
*/
|
|
443
|
+
async markNeedsAttentionInWorkspace(workspaceId: string, error: string): Promise<number> {
|
|
444
|
+
// The canonical form `enqueue` stores (see `canonicalWorkspaceId`), so the
|
|
445
|
+
// caller's spelling of the id cannot make this match nothing.
|
|
446
|
+
return this.escalateQueued(eq(pendingCommits.workspaceId, canonicalWorkspaceId(workspaceId)), error);
|
|
447
|
+
}
|
|
448
|
+
|
|
387
449
|
/**
|
|
388
450
|
* When the oldest commit still waiting to land was queued, or null when
|
|
389
451
|
* nothing waits — the readiness answer's one number. `pending` and
|
|
@@ -41,8 +41,12 @@ export const RECOVERY_BOT_NAME = 'Bevel Recovery Bot';
|
|
|
41
41
|
export async function ensureRecoveryBotUser(db: Database): Promise<AuthUser> {
|
|
42
42
|
const inserted = await db
|
|
43
43
|
.insert(users)
|
|
44
|
-
.values({
|
|
45
|
-
|
|
44
|
+
.values({
|
|
45
|
+
email: RECOVERY_BOT_EMAIL,
|
|
46
|
+
emailBidx: RECOVERY_BOT_EMAIL,
|
|
47
|
+
name: RECOVERY_BOT_NAME,
|
|
48
|
+
})
|
|
49
|
+
.onConflictDoNothing({ target: users.emailBidx })
|
|
46
50
|
.returning();
|
|
47
51
|
if (inserted.length > 0) {
|
|
48
52
|
const row = inserted[0];
|
|
@@ -57,7 +61,7 @@ export async function ensureRecoveryBotUser(db: Database): Promise<AuthUser> {
|
|
|
57
61
|
const [row] = await db
|
|
58
62
|
.select()
|
|
59
63
|
.from(users)
|
|
60
|
-
.where(eq(users.
|
|
64
|
+
.where(eq(users.emailBidx, RECOVERY_BOT_EMAIL))
|
|
61
65
|
.limit(1);
|
|
62
66
|
if (!row) {
|
|
63
67
|
// Vanishingly unlikely: the row exists for the conflict to fire but is
|
|
@@ -247,6 +247,9 @@ describe('ReviewWorkflowService.carryApprovalsForward', () => {
|
|
|
247
247
|
prNumber: PR,
|
|
248
248
|
path: 'Sales/Terms.md',
|
|
249
249
|
approverEmail: 'carol@bevel.software',
|
|
250
|
+
// The index column of a copied row is written with the address, not
|
|
251
|
+
// with the stored index read back beside it.
|
|
252
|
+
approverEmailBidx: 'carol@bevel.software',
|
|
250
253
|
approverName: 'Carol',
|
|
251
254
|
headSha: NEW_HEAD,
|
|
252
255
|
// The reviewer approved THEN, not now — the merge is not an approval.
|
|
@@ -256,6 +259,7 @@ describe('ReviewWorkflowService.carryApprovalsForward', () => {
|
|
|
256
259
|
prNumber: PR,
|
|
257
260
|
path: 'Sales/Pricing.md',
|
|
258
261
|
approverEmail: 'bob@bevel.software',
|
|
262
|
+
approverEmailBidx: 'bob@bevel.software',
|
|
259
263
|
approverName: 'Bob',
|
|
260
264
|
headSha: NEW_HEAD,
|
|
261
265
|
approvedAt: APPROVED_AT,
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import type { SQL } from 'drizzle-orm';
|
|
3
|
+
import { PgDialect } from 'drizzle-orm/pg-core';
|
|
2
4
|
import { ReviewWorkflowService } from '../review-workflow.service.js';
|
|
3
5
|
import type { ApprovalTx } from '../review-workflow.interface.js';
|
|
4
6
|
|
|
@@ -34,6 +36,8 @@ function harness() {
|
|
|
34
36
|
const locked: unknown[][] = [];
|
|
35
37
|
const lockSql: string[] = [];
|
|
36
38
|
const sets: unknown[] = [];
|
|
39
|
+
/** The condition each rewrite was limited to, as the SQL and values it sends. */
|
|
40
|
+
const wheres: { sql: string; params: unknown[] }[] = [];
|
|
37
41
|
const tx = {
|
|
38
42
|
execute: async (statement: { queryChunks?: unknown[] }) => {
|
|
39
43
|
const text = statementText(statement);
|
|
@@ -48,8 +52,9 @@ function harness() {
|
|
|
48
52
|
},
|
|
49
53
|
update: (table: unknown) => ({
|
|
50
54
|
set: (values: unknown) => ({
|
|
51
|
-
where: async () => {
|
|
55
|
+
where: async (condition: SQL) => {
|
|
52
56
|
sets.push(values);
|
|
57
|
+
wheres.push(new PgDialect().sqlToQuery(condition));
|
|
53
58
|
order.push(`update:${tableOf(table)}`);
|
|
54
59
|
},
|
|
55
60
|
}),
|
|
@@ -58,7 +63,7 @@ function harness() {
|
|
|
58
63
|
// `eraseApprover` touches nothing but the transaction it is given, so the
|
|
59
64
|
// service's own collaborators are never reached.
|
|
60
65
|
const svc = new ReviewWorkflowService({} as never, {} as never, {} as never, {} as never);
|
|
61
|
-
return { svc, tx: tx as unknown as ApprovalTx, order, locked, lockSql, sets };
|
|
66
|
+
return { svc, tx: tx as unknown as ApprovalTx, order, locked, lockSql, sets, wheres };
|
|
62
67
|
}
|
|
63
68
|
|
|
64
69
|
describe('ReviewWorkflowService.eraseApprover', () => {
|
|
@@ -79,6 +84,23 @@ describe('ReviewWorkflowService.eraseApprover', () => {
|
|
|
79
84
|
expect(h.lockSql[0]).toContain('pg_advisory_xact_lock');
|
|
80
85
|
expect(h.lockSql[0]).not.toMatch(/pg_advisory_lock\b/);
|
|
81
86
|
// And the rewrite is to the placeholder identity it was given.
|
|
82
|
-
|
|
87
|
+
// The blind index is rewritten with the email: nothing keyed to the
|
|
88
|
+
// erased address may survive, and the email itself is ciphertext. The
|
|
89
|
+
// index column is written with the address; the handle indexes it.
|
|
90
|
+
expect(h.sets).toEqual([
|
|
91
|
+
{
|
|
92
|
+
approverEmail: 'deleted-1@erased.invalid',
|
|
93
|
+
approverEmailBidx: 'deleted-1@erased.invalid',
|
|
94
|
+
approverName: 'Deleted user',
|
|
95
|
+
},
|
|
96
|
+
]);
|
|
97
|
+
// WHICH rows: the ones whose blind index is the erased address's. The
|
|
98
|
+
// address column holds randomized ciphertext, so a match on it finds
|
|
99
|
+
// nothing — the rewrite would report success having rewritten no row, and
|
|
100
|
+
// every approval of the erased person would keep their name.
|
|
101
|
+
expect(h.wheres).toHaveLength(1);
|
|
102
|
+
expect(h.wheres[0]!.sql).toMatch(/"approver_email_bidx" = \$1$/);
|
|
103
|
+
expect(h.wheres[0]!.sql).not.toMatch(/"approver_email" =/);
|
|
104
|
+
expect(JSON.stringify(h.wheres[0]!.params)).toContain('bob@bevel.software');
|
|
83
105
|
});
|
|
84
106
|
});
|