@vellumai/assistant 0.11.1 → 0.11.2-staging.2
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/Dockerfile +1 -3
- package/README.md +1 -1
- package/eslint.config.mjs +28 -8
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
- package/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
- package/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
- package/node_modules/@vellumai/slack-text/src/index.test.ts +48 -0
- package/node_modules/@vellumai/slack-text/src/index.ts +25 -6
- package/openapi.yaml +292 -5
- package/package.json +1 -1
- package/src/__tests__/agent-loop-resume-interrupted.test.ts +223 -0
- package/src/__tests__/byok-default-profile-ensure.test.ts +43 -0
- package/src/__tests__/cli-logger-boundary-guard.test.ts +87 -0
- package/src/__tests__/compaction-events.test.ts +139 -2
- package/src/__tests__/config-loader-backfill.test.ts +35 -3
- package/src/__tests__/config-schema.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop-fatal-cleanup.test.ts +30 -0
- package/src/__tests__/conversation-agent-loop.test.ts +180 -7
- package/src/__tests__/conversation-error.test.ts +211 -4
- package/src/__tests__/conversation-options-turn-scoped-transport.test.ts +64 -0
- package/src/__tests__/conversation-queue.test.ts +126 -0
- package/src/__tests__/conversation-retry-route.test.ts +46 -7
- package/src/__tests__/conversation-slash.test.ts +4 -2
- package/src/__tests__/conversation-summarize-route.test.ts +7 -1
- package/src/__tests__/conversation-summarize-up-to.test.ts +71 -5
- package/src/__tests__/document-create-dedupe.test.ts +2 -1
- package/src/__tests__/document-find-replace.test.ts +2 -1
- package/src/__tests__/document-tool-security.test.ts +2 -1
- package/src/__tests__/document-update-default-surface.test.ts +2 -1
- package/src/__tests__/document-workspace-file.test.ts +467 -0
- package/src/__tests__/emit-signal-routing-intent.test.ts +290 -49
- package/src/__tests__/guardian-card-withdrawal.test.ts +91 -3
- package/src/__tests__/http-user-message-parity.test.ts +66 -0
- package/src/__tests__/list-messages-attachments.test.ts +87 -0
- package/src/__tests__/list-messages-provider-error.test.ts +143 -0
- package/src/__tests__/list-messages-system-card.test.ts +100 -0
- package/src/__tests__/llm-resolver.test.ts +19 -9
- package/src/__tests__/managed-profile-guard.test.ts +23 -0
- package/src/__tests__/notification-platform-adapter.test.ts +130 -2
- package/src/__tests__/notification-telegram-adapter.test.ts +6 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +45 -0
- package/src/__tests__/plugin-api-model-profiles.test.ts +10 -1
- package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
- package/src/__tests__/provider-error-scenarios.test.ts +140 -0
- package/src/__tests__/provider-send-message-override-profile.test.ts +95 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +66 -3
- package/src/__tests__/scripted-turn-metadata-persistence.test.ts +209 -0
- package/src/__tests__/skills.test.ts +27 -0
- package/src/__tests__/slack-channels-routes.test.ts +0 -2
- package/src/__tests__/slack-share-routes.test.ts +0 -3
- package/src/__tests__/slack-users-routes.test.ts +0 -2
- package/src/__tests__/subagent-tools.test.ts +11 -0
- package/src/__tests__/tool-preview-lifecycle.test.ts +58 -0
- package/src/__tests__/tool-result-spool.test.ts +5 -4
- package/src/__tests__/turn-boundary-resolution.test.ts +53 -0
- package/src/__tests__/turn-events-store.test.ts +26 -0
- package/src/__tests__/ui-visual-surface.test.ts +695 -0
- package/src/__tests__/unified-turn-context-visible-app.test.ts +99 -0
- package/src/__tests__/visible-app-context.test.ts +189 -0
- package/src/__tests__/workspace-git-service.test.ts +173 -33
- package/src/__tests__/workspace-migration-137-repair-retired-fireworks-minimax-model-id.test.ts +157 -0
- package/src/__tests__/workspace-migration-138-backfill-home-feed-titles.test.ts +373 -0
- package/src/__tests__/workspace-migration-139-clear-renamed-cost-profile-label.test.ts +137 -0
- package/src/agent/loop.ts +36 -7
- package/src/api/events/context-window-usage.ts +31 -0
- package/src/api/events/notification-intent.ts +8 -0
- package/src/api/events/ui-surface-pending.ts +35 -0
- package/src/api/index.ts +14 -0
- package/src/api/responses/conversation-message.ts +25 -4
- package/src/api/surfaces.ts +90 -1
- package/src/approvals/guardian-card-withdrawal.ts +66 -31
- package/src/approvals/guardian-decision-primitive.ts +4 -0
- package/src/calls/__tests__/call-setup-router.test.ts +156 -26
- package/src/calls/__tests__/voice-session-bridge.test.ts +201 -12
- package/src/calls/call-setup-router.ts +80 -36
- package/src/calls/voice-session-bridge.ts +173 -26
- package/src/cli/commands/inference.help.ts +3 -3
- package/src/cli/commands/notifications.help.ts +3 -2
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +42 -128
- package/src/cli/commands/platform/__tests__/credits.test.ts +9 -78
- package/src/cli/commands/platform/__tests__/helpers.ts +90 -0
- package/src/cli/commands/platform/__tests__/invoices.test.ts +238 -0
- package/src/cli/commands/platform/__tests__/plans.test.ts +9 -88
- package/src/cli/commands/platform/__tests__/status.test.ts +12 -87
- package/src/cli/commands/platform/__tests__/subscription.test.ts +9 -86
- package/src/cli/commands/platform/index.help.ts +92 -0
- package/src/cli/commands/platform/index.ts +7 -0
- package/src/cli/commands/platform/invoices.ts +132 -0
- package/src/cli/commands/usage.help.ts +1 -1
- package/src/cli/lib/list-installed-plugins.ts +2 -1
- package/src/config/__tests__/default-profile-catalog.test.ts +7 -10
- package/src/config/__tests__/deployment-context-defaults.test.ts +27 -5
- package/src/config/assistant-feature-flags.ts +7 -2
- package/src/config/bundled-skills/app-builder/SKILL.md +3 -2
- package/src/config/bundled-skills/subagent/SKILL.md +3 -1
- package/src/config/bundled-skills/subagent/TOOLS.json +3 -3
- package/src/config/bundled-skills/visualize/SKILL.md +163 -0
- package/src/config/call-site-defaults.ts +3 -2
- package/src/config/default-profile-catalog.ts +75 -70
- package/src/config/default-profile-names.ts +13 -28
- package/src/config/env-registry.ts +1 -0
- package/src/config/feature-flag-registry.json +16 -0
- package/src/config/llm-resolver.ts +21 -5
- package/src/config/loader.ts +18 -11
- package/src/config/schemas/llm.ts +1 -9
- package/src/config/schemas/memory-retrospective.ts +9 -0
- package/src/config/schemas/monitoring.ts +28 -2
- package/src/config/schemas/workspace-git.ts +15 -0
- package/src/config/seed-inference-profiles.ts +19 -5
- package/src/context/post-turn-tool-result-truncation.ts +2 -2
- package/src/conversations/__tests__/message-consolidation.test.ts +54 -0
- package/src/conversations/message-consolidation.ts +17 -15
- package/src/daemon/__tests__/turn-tail-assistant-reply-notify.test.ts +182 -0
- package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +181 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +90 -7
- package/src/daemon/conversation-agent-loop.ts +107 -22
- package/src/daemon/conversation-error.ts +169 -48
- package/src/daemon/conversation-messaging.ts +87 -4
- package/src/daemon/conversation-process.ts +57 -45
- package/src/daemon/conversation-runtime-assembly.ts +57 -0
- package/src/daemon/conversation-store.ts +36 -1
- package/src/daemon/conversation-surfaces.ts +68 -6
- package/src/daemon/conversation-turn-finalize.ts +76 -18
- package/src/daemon/conversation.ts +122 -27
- package/src/daemon/lifecycle.ts +9 -0
- package/src/daemon/message-types/conversations.ts +8 -0
- package/src/daemon/message-types/surfaces.ts +3 -0
- package/src/documents/document-store.ts +247 -10
- package/src/home/__tests__/feed-types.test.ts +43 -8
- package/src/home/__tests__/feed-writer.test.ts +59 -0
- package/src/home/feed-types.ts +34 -7
- package/src/home/feed-writer.ts +15 -6
- package/src/live-voice/__tests__/activity-label.test.ts +95 -0
- package/src/live-voice/__tests__/live-activity-reporter.test.ts +86 -5
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +310 -25
- package/src/live-voice/__tests__/live-voice-events.test.ts +4 -4
- package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +8 -4
- package/src/live-voice/__tests__/live-voice-vad.test.ts +9 -1
- package/src/live-voice/activity-label.ts +169 -0
- package/src/live-voice/live-activity-reporter.ts +35 -6
- package/src/live-voice/live-voice-session.ts +282 -41
- package/src/live-voice/protocol.ts +35 -0
- package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +84 -2
- package/src/messaging/providers/slack/adapter.ts +86 -19
- package/src/messaging/providers/slack/api.test.ts +85 -1
- package/src/messaging/providers/slack/api.ts +121 -288
- package/src/messaging/providers/slack/client.ts +20 -251
- package/src/messaging/providers/slack/send.test.ts +4 -9
- package/src/messaging/providers/slack/send.ts +1 -1
- package/src/messaging/providers/slack/types.ts +5 -0
- package/src/messaging/providers/slack/web-api-transport.test.ts +181 -0
- package/src/messaging/providers/slack/web-api-transport.ts +367 -0
- package/src/messaging/providers/slack/withdraw.ts +6 -14
- package/src/messaging/providers/telegram-bot/send.test.ts +31 -1
- package/src/messaging/providers/telegram-bot/send.ts +23 -2
- package/src/messaging/providers/telegram-bot/withdraw.test.ts +152 -0
- package/src/messaging/providers/telegram-bot/withdraw.ts +166 -0
- package/src/monitoring/__tests__/db-integrity-sample.test.ts +18 -4
- package/src/monitoring/__tests__/file-descriptors.test.ts +144 -0
- package/src/monitoring/file-descriptors.ts +262 -0
- package/src/monitoring/process-memory.ts +4 -11
- package/src/monitoring/resource-sampler.ts +5 -0
- package/src/monitoring/worker.ts +12 -0
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +740 -0
- package/src/notifications/__tests__/broadcaster.test.ts +347 -7
- package/src/notifications/__tests__/copy-composer.test.ts +53 -3
- package/src/notifications/__tests__/decision-engine.test.ts +349 -29
- package/src/notifications/__tests__/deterministic-checks.test.ts +21 -0
- package/src/notifications/__tests__/edit-notification.test.ts +330 -0
- package/src/notifications/__tests__/guardian-delivery-recorder.test.ts +71 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +170 -12
- package/src/notifications/adapters/macos.ts +3 -0
- package/src/notifications/adapters/platform.ts +90 -14
- package/src/notifications/adapters/telegram.ts +6 -4
- package/src/notifications/assistant-reply-producer.ts +219 -0
- package/src/notifications/broadcaster.ts +523 -317
- package/src/notifications/copy-composer.ts +27 -6
- package/src/notifications/decision-engine.ts +154 -77
- package/src/notifications/deterministic-checks.ts +6 -7
- package/src/notifications/edit-notification.ts +8 -4
- package/src/notifications/emit-signal.ts +49 -16
- package/src/notifications/guardian-delivery-recorder.ts +8 -2
- package/src/notifications/home-feed-side-effect.ts +43 -6
- package/src/notifications/notification-utils.ts +39 -4
- package/src/notifications/signal.ts +10 -0
- package/src/notifications/types.ts +17 -0
- package/src/persistence/bookmark-crud.ts +18 -9
- package/src/persistence/conversation-attention-store.ts +26 -0
- package/src/persistence/conversation-crud.ts +79 -32
- package/src/persistence/conversation-queries.ts +30 -15
- package/src/persistence/conversation-title-service.ts +5 -170
- package/src/persistence/conversation-types.ts +192 -1
- package/src/persistence/db-init.ts +14 -1
- package/src/persistence/db-maintenance.ts +5 -4
- package/src/persistence/embeddings/__tests__/plugin-index-qdrant-init.test.ts +219 -0
- package/src/persistence/embeddings/__tests__/plugin-index.test.ts +22 -0
- package/src/persistence/embeddings/__tests__/worker-script-version.test.ts +76 -0
- package/src/persistence/embeddings/embedding-local.ts +2 -1
- package/src/persistence/embeddings/embedding-runtime-manager.ts +29 -5
- package/src/persistence/embeddings/plugin-index.ts +84 -12
- package/src/persistence/migrations/360-add-document-workspace-path.test.ts +110 -0
- package/src/persistence/migrations/360-add-document-workspace-path.ts +39 -0
- package/src/persistence/planner-statistics.ts +14 -0
- package/src/persistence/schema/documents.ts +26 -11
- package/src/persistence/steps.ts +2 -0
- package/src/platform/client.test.ts +172 -2
- package/src/platform/client.ts +178 -55
- package/src/plugin-api/conversation-turn.ts +13 -0
- package/src/plugin-api/model-profiles.test.ts +6 -2
- package/src/plugins/defaults/memory/__tests__/bookmark-crud.test.ts +34 -0
- package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +22 -0
- package/src/plugins/defaults/memory/__tests__/jobs-store-enqueue-gate.test.ts +11 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +199 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +98 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +105 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +24 -2
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +524 -0
- package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +1 -1
- package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +9 -4
- package/src/plugins/defaults/memory/host-utils.ts +5 -0
- package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +105 -1
- package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +64 -5
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +36 -1
- package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +12 -3
- package/src/plugins/defaults/memory/src/__tests__/memory-v2-simulate-route.test.ts +1 -0
- package/src/plugins/defaults/memory/substrate/__tests__/skill-store.test.ts +195 -1
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +1 -1
- package/src/plugins/defaults/memory/substrate/skill-store.ts +167 -29
- package/src/plugins/defaults/memory/v2/__tests__/injection.test.ts +131 -0
- package/src/plugins/defaults/memory/v2/__tests__/router.test.ts +1 -0
- package/src/plugins/defaults/memory/v2/activation-log-store.ts +4 -0
- package/src/plugins/defaults/memory/v2/injection.ts +48 -3
- package/src/plugins/defaults/memory/v2/rerank-local.ts +6 -2
- package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +1 -1
- package/src/plugins/defaults/platform-hosted/routes/reengage.ts +1 -1
- package/src/plugins/defaults/turn-context/injectors.ts +1 -0
- package/src/plugins/defaults/turn-context/unified-turn-context.ts +26 -0
- package/src/plugins/types.ts +17 -0
- package/src/prompts/templates/system-sections.ts +2 -2
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +43 -2
- package/src/providers/__tests__/vellum-mismatch-routing.test.ts +8 -0
- package/src/providers/call-site-routing.ts +77 -14
- package/src/providers/connection-resolution.ts +42 -2
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +127 -1
- package/src/providers/inference/adapter-factory.ts +70 -12
- package/src/providers/model-catalog.ts +0 -11
- package/src/providers/model-intents.ts +11 -0
- package/src/providers/registry.ts +8 -0
- package/src/providers/retry.ts +85 -17
- package/src/providers/types.ts +8 -1
- package/src/runtime/__tests__/agent-wake.test.ts +181 -0
- package/src/runtime/agent-wake.ts +132 -9
- package/src/runtime/channel-approval-types.ts +25 -0
- package/src/runtime/routes/__tests__/connection-routes-vs-cli-parity.test.ts +2 -2
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +11 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +121 -9
- package/src/runtime/routes/__tests__/platform-invoice-routes.test.ts +392 -0
- package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +7 -0
- package/src/runtime/routes/__tests__/workspace-commit-routes.test.ts +63 -0
- package/src/runtime/routes/consolidation-routes.ts +1 -1
- package/src/runtime/routes/conversation-management-routes.ts +18 -6
- package/src/runtime/routes/conversation-query-routes.ts +14 -23
- package/src/runtime/routes/conversation-routes.ts +113 -8
- package/src/runtime/routes/credential-routes.ts +1 -5
- package/src/runtime/routes/documents-routes.ts +207 -12
- package/src/runtime/routes/identity-routes.ts +3 -93
- package/src/runtime/routes/inbound-stages/admission-policy.test.ts +31 -1
- package/src/runtime/routes/inbound-stages/admission-policy.ts +18 -0
- package/src/runtime/routes/inference-provider-connection-routes.ts +52 -3
- package/src/runtime/routes/notification-routes.ts +4 -1
- package/src/runtime/routes/platform-routes.ts +238 -3
- package/src/runtime/routes/playground/guard.ts +1 -2
- package/src/runtime/routes/slack-channel-routes.ts +2 -4
- package/src/runtime/routes/workspace-commit-routes.ts +49 -2
- package/src/runtime/routes/workspace-routes.ts +11 -12
- package/src/runtime/routes/workspace-utils.ts +49 -2
- package/src/runtime/services/conversation-serializer.ts +2 -4
- package/src/subagent/__tests__/consult-context-gating.test.ts +98 -0
- package/src/subagent/__tests__/consult-context-skills.test.ts +82 -0
- package/src/subagent/__tests__/consult-context.test.ts +84 -0
- package/src/subagent/__tests__/consult-prompt.test.ts +36 -0
- package/src/subagent/consult-context.ts +410 -0
- package/src/subagent/consult-prompt.ts +38 -5
- package/src/subagent/manager.ts +9 -4
- package/src/subagent/types.ts +8 -0
- package/src/telemetry/telemetry-event-sources.ts +7 -0
- package/src/telemetry/telemetry-wire-source.json +1 -1
- package/src/telemetry/telemetry-wire.generated.ts +1 -0
- package/src/telemetry/turn-events-store.ts +30 -1
- package/src/telemetry/types.ts +24 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +48 -0
- package/src/tools/browser/pinned-tabs.ts +3 -1
- package/src/tools/subagent/spawn.ts +23 -1
- package/src/tools/terminal/safe-env.ts +1 -0
- package/src/tools/ui-surface/definitions.ts +39 -1
- package/src/tools/ui-surface/surface-shape-docs.ts +38 -4
- package/src/tools/ui-surface/visual-validation.ts +787 -0
- package/src/tools/workflows/run-workflow.test.ts +1 -0
- package/src/util/__tests__/short-title.test.ts +229 -0
- package/src/util/__tests__/worker-compute.test.ts +65 -0
- package/src/util/cgroup-cpu.ts +93 -0
- package/src/util/errors.ts +27 -0
- package/src/util/process-tree.ts +19 -0
- package/src/util/short-title.ts +189 -0
- package/src/util/worker-compute.ts +83 -0
- package/src/workspace/byok-default-profile-ensure.ts +25 -15
- package/src/workspace/git-service.ts +79 -30
- package/src/workspace/migrations/137-repair-retired-fireworks-minimax-model-id.ts +131 -0
- package/src/workspace/migrations/138-backfill-home-feed-titles.ts +179 -0
- package/src/workspace/migrations/139-clear-renamed-cost-profile-label.ts +92 -0
- package/src/workspace/migrations/registry.ts +6 -0
- package/src/plugins/defaults/memory/substrate/constants.ts +0 -8
|
@@ -207,7 +207,7 @@ describe("POST inference/provider-connections (create)", () => {
|
|
|
207
207
|
{
|
|
208
208
|
body: {
|
|
209
209
|
name: "managed-openai",
|
|
210
|
-
provider: "
|
|
210
|
+
provider: "vellum",
|
|
211
211
|
auth: { type: "platform" },
|
|
212
212
|
},
|
|
213
213
|
},
|
|
@@ -215,6 +215,39 @@ describe("POST inference/provider-connections (create)", () => {
|
|
|
215
215
|
expect(result.auth).toEqual({ type: "platform" });
|
|
216
216
|
});
|
|
217
217
|
|
|
218
|
+
test("rejects platform auth on a real provider", async () => {
|
|
219
|
+
const err = await call(
|
|
220
|
+
findHandler("inference_provider_connections_create"),
|
|
221
|
+
{
|
|
222
|
+
body: {
|
|
223
|
+
name: "managed-openai",
|
|
224
|
+
provider: "openai",
|
|
225
|
+
auth: { type: "platform" },
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
).catch((e: unknown) => e);
|
|
229
|
+
|
|
230
|
+
expect(err).toBeInstanceOf(BadRequestError);
|
|
231
|
+
expect((err as BadRequestError).message).toContain("platform");
|
|
232
|
+
expect((err as BadRequestError).message).toContain("vellum");
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("rejects key auth on the vellum provider", async () => {
|
|
236
|
+
const err = await call(
|
|
237
|
+
findHandler("inference_provider_connections_create"),
|
|
238
|
+
{
|
|
239
|
+
body: {
|
|
240
|
+
name: "keyed-vellum",
|
|
241
|
+
provider: "vellum",
|
|
242
|
+
auth: { type: "api_key", credential: "vault/vellum/key" },
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
).catch((e: unknown) => e);
|
|
246
|
+
|
|
247
|
+
expect(err).toBeInstanceOf(BadRequestError);
|
|
248
|
+
expect((err as BadRequestError).message).toContain("vellum");
|
|
249
|
+
});
|
|
250
|
+
|
|
218
251
|
test("creates connection with none auth (e.g. ollama)", async () => {
|
|
219
252
|
const result = (await call(
|
|
220
253
|
findHandler("inference_provider_connections_create"),
|
|
@@ -504,7 +537,7 @@ describe("POST inference/provider-connections (create)", () => {
|
|
|
504
537
|
body: {
|
|
505
538
|
name: "dup-name",
|
|
506
539
|
provider: "openai",
|
|
507
|
-
auth: { type: "
|
|
540
|
+
auth: { type: "api_key", credential: "vault/openai/key" },
|
|
508
541
|
},
|
|
509
542
|
}),
|
|
510
543
|
).rejects.toBeInstanceOf(ConflictError);
|
|
@@ -585,6 +618,85 @@ describe("PATCH inference/provider-connections/:name (update)", () => {
|
|
|
585
618
|
).rejects.toBeInstanceOf(NotFoundError);
|
|
586
619
|
});
|
|
587
620
|
|
|
621
|
+
test("rejects switching a real provider's connection to platform auth", async () => {
|
|
622
|
+
seedConnection({
|
|
623
|
+
name: "byok-openai",
|
|
624
|
+
provider: "openai",
|
|
625
|
+
auth: { type: "api_key", credential: "vault/openai/key" },
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
const err = await call(
|
|
629
|
+
findHandler("inference_provider_connections_update"),
|
|
630
|
+
{
|
|
631
|
+
pathParams: { name: "byok-openai" },
|
|
632
|
+
body: { auth: { type: "platform" } },
|
|
633
|
+
},
|
|
634
|
+
).catch((e: unknown) => e);
|
|
635
|
+
|
|
636
|
+
expect(err).toBeInstanceOf(BadRequestError);
|
|
637
|
+
expect((err as BadRequestError).message).toContain("platform");
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
// A persisted row whose provider and auth disagree stays editable: both the
|
|
641
|
+
// web editor and the CLI resend the stored auth on every edit, so the guard
|
|
642
|
+
// keys on an actual auth change rather than on the field being present.
|
|
643
|
+
test("allows editing a legacy mismatched row when the client resends the stored auth", async () => {
|
|
644
|
+
seedConnection({
|
|
645
|
+
name: "legacy-managed-openai",
|
|
646
|
+
provider: "openai",
|
|
647
|
+
auth: { type: "platform" },
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
const result = (await call(
|
|
651
|
+
findHandler("inference_provider_connections_update"),
|
|
652
|
+
{
|
|
653
|
+
pathParams: { name: "legacy-managed-openai" },
|
|
654
|
+
body: { auth: { type: "platform" }, label: "Legacy" },
|
|
655
|
+
},
|
|
656
|
+
)) as { label: string | null; auth: object };
|
|
657
|
+
expect(result.label).toBe("Legacy");
|
|
658
|
+
expect(result.auth).toEqual({ type: "platform" });
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
test("allows editing a legacy mismatched row when auth is omitted", async () => {
|
|
662
|
+
seedConnection({
|
|
663
|
+
name: "legacy-managed-gemini",
|
|
664
|
+
provider: "gemini",
|
|
665
|
+
auth: { type: "platform" },
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
const result = (await call(
|
|
669
|
+
findHandler("inference_provider_connections_update"),
|
|
670
|
+
{
|
|
671
|
+
pathParams: { name: "legacy-managed-gemini" },
|
|
672
|
+
body: { label: "Legacy" },
|
|
673
|
+
},
|
|
674
|
+
)) as { label: string | null };
|
|
675
|
+
expect(result.label).toBe("Legacy");
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
// The guard must not trap a legacy row in its mismatched state: an auth
|
|
679
|
+
// change that repairs the pairing is exactly what should be allowed.
|
|
680
|
+
test("allows repairing a legacy mismatched row with a valid auth change", async () => {
|
|
681
|
+
seedConnection({
|
|
682
|
+
name: "legacy-managed-anthropic",
|
|
683
|
+
provider: "anthropic",
|
|
684
|
+
auth: { type: "platform" },
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
const result = (await call(
|
|
688
|
+
findHandler("inference_provider_connections_update"),
|
|
689
|
+
{
|
|
690
|
+
pathParams: { name: "legacy-managed-anthropic" },
|
|
691
|
+
body: { auth: { type: "api_key", credential: "vault/anthropic/key" } },
|
|
692
|
+
},
|
|
693
|
+
)) as { auth: object };
|
|
694
|
+
expect(result.auth).toEqual({
|
|
695
|
+
type: "api_key",
|
|
696
|
+
credential: "vault/anthropic/key",
|
|
697
|
+
});
|
|
698
|
+
});
|
|
699
|
+
|
|
588
700
|
test("throws 400 when auth schema is invalid", async () => {
|
|
589
701
|
seedConnection({
|
|
590
702
|
name: "bad-auth",
|
|
@@ -971,7 +1083,7 @@ describe("POST with label", () => {
|
|
|
971
1083
|
body: {
|
|
972
1084
|
name: "labeled-conn",
|
|
973
1085
|
provider: "anthropic",
|
|
974
|
-
auth: { type: "
|
|
1086
|
+
auth: { type: "api_key", credential: "vault/anthropic/key" },
|
|
975
1087
|
label: "My Anthropic",
|
|
976
1088
|
},
|
|
977
1089
|
},
|
|
@@ -987,7 +1099,7 @@ describe("POST with label", () => {
|
|
|
987
1099
|
body: {
|
|
988
1100
|
name: "no-label-conn",
|
|
989
1101
|
provider: "openai",
|
|
990
|
-
auth: { type: "
|
|
1102
|
+
auth: { type: "api_key", credential: "vault/openai/key" },
|
|
991
1103
|
},
|
|
992
1104
|
},
|
|
993
1105
|
)) as { label: string | null };
|
|
@@ -999,7 +1111,7 @@ describe("PATCH with label", () => {
|
|
|
999
1111
|
test("updates label to a string", async () => {
|
|
1000
1112
|
seedConnection({
|
|
1001
1113
|
name: "set-label",
|
|
1002
|
-
provider: "
|
|
1114
|
+
provider: "vellum",
|
|
1003
1115
|
auth: { type: "platform" },
|
|
1004
1116
|
});
|
|
1005
1117
|
|
|
@@ -1016,7 +1128,7 @@ describe("PATCH with label", () => {
|
|
|
1016
1128
|
test("clears label by setting it to null", async () => {
|
|
1017
1129
|
seedConnection({
|
|
1018
1130
|
name: "clear-label",
|
|
1019
|
-
provider: "
|
|
1131
|
+
provider: "vellum",
|
|
1020
1132
|
auth: { type: "platform" },
|
|
1021
1133
|
});
|
|
1022
1134
|
// First set a label.
|
|
@@ -1038,7 +1150,7 @@ describe("PATCH with label", () => {
|
|
|
1038
1150
|
test("rejects label: empty string with 400", async () => {
|
|
1039
1151
|
seedConnection({
|
|
1040
1152
|
name: "reject-empty",
|
|
1041
|
-
provider: "
|
|
1153
|
+
provider: "vellum",
|
|
1042
1154
|
auth: { type: "platform" },
|
|
1043
1155
|
});
|
|
1044
1156
|
|
|
@@ -1168,7 +1280,7 @@ describe("Managed connection write protection", () => {
|
|
|
1168
1280
|
test("allows PATCH with auth still set to platform (no-op auth change)", async () => {
|
|
1169
1281
|
seedConnection({
|
|
1170
1282
|
name: "vellum",
|
|
1171
|
-
provider: "
|
|
1283
|
+
provider: "vellum",
|
|
1172
1284
|
auth: { type: "platform" },
|
|
1173
1285
|
});
|
|
1174
1286
|
|
|
@@ -1190,7 +1302,7 @@ describe("Managed connection write protection", () => {
|
|
|
1190
1302
|
test("allows relabeling a managed connection", async () => {
|
|
1191
1303
|
seedConnection({
|
|
1192
1304
|
name: "vellum",
|
|
1193
|
-
provider: "
|
|
1305
|
+
provider: "vellum",
|
|
1194
1306
|
auth: { type: "platform" },
|
|
1195
1307
|
});
|
|
1196
1308
|
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the platform_invoices_list and platform_invoices_by_id_get route
|
|
3
|
+
* handlers: cursor forwarding, page walking, the page cap, and error paths.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
BadRequestError,
|
|
10
|
+
InternalError,
|
|
11
|
+
NotFoundError,
|
|
12
|
+
UnprocessableEntityError,
|
|
13
|
+
} from "../errors.js";
|
|
14
|
+
|
|
15
|
+
let platformBaseUrl = "https://platform.test";
|
|
16
|
+
let authHeader: string | null = "Api-Key test";
|
|
17
|
+
|
|
18
|
+
// Spread the real module: replacing it wholesale breaks unrelated importers
|
|
19
|
+
// pulled in by the module under test.
|
|
20
|
+
const actualRegistration =
|
|
21
|
+
await import("../../../inbound/platform-callback-registration.js");
|
|
22
|
+
mock.module("../../../inbound/platform-callback-registration.js", () => ({
|
|
23
|
+
...actualRegistration,
|
|
24
|
+
resolvePlatformCallbackRegistrationContext: async () => ({
|
|
25
|
+
isPlatform: false,
|
|
26
|
+
platformBaseUrl,
|
|
27
|
+
assistantId: "assistant-123",
|
|
28
|
+
hasAssistantApiKey: !!authHeader,
|
|
29
|
+
authHeader,
|
|
30
|
+
enabled: !!(platformBaseUrl && authHeader),
|
|
31
|
+
}),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
const { INVOICE_WALK_DEADLINE_MS, MAX_INVOICE_PAGES, ROUTES } =
|
|
35
|
+
await import("../platform-routes.js");
|
|
36
|
+
|
|
37
|
+
const listHandler = ROUTES.find(
|
|
38
|
+
(r) => r.operationId === "platform_invoices_list",
|
|
39
|
+
)!.handler;
|
|
40
|
+
const getHandler = ROUTES.find(
|
|
41
|
+
(r) => r.operationId === "platform_invoices_by_id_get",
|
|
42
|
+
)!.handler;
|
|
43
|
+
|
|
44
|
+
const INVOICES_URL = "https://platform.test/v1/organizations/billing/invoices/";
|
|
45
|
+
|
|
46
|
+
function invoice(id: string) {
|
|
47
|
+
return {
|
|
48
|
+
id,
|
|
49
|
+
number: `INV-${id}`,
|
|
50
|
+
status: "paid",
|
|
51
|
+
currency: "usd",
|
|
52
|
+
amount_due: 1234,
|
|
53
|
+
amount_paid: 1234,
|
|
54
|
+
amount_remaining: 0,
|
|
55
|
+
created: 1750000000,
|
|
56
|
+
hosted_invoice_url: `https://stripe.test/${id}`,
|
|
57
|
+
invoice_pdf: null,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface FetchCall {
|
|
62
|
+
url: string;
|
|
63
|
+
init?: RequestInit;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const realFetch = globalThis.fetch;
|
|
67
|
+
let fetchCalls: FetchCall[] = [];
|
|
68
|
+
|
|
69
|
+
/** Stub globalThis.fetch, recording calls and answering via `respond`. */
|
|
70
|
+
function stubFetch(
|
|
71
|
+
respond: (url: string, callIndex: number) => Response | Promise<Response>,
|
|
72
|
+
): void {
|
|
73
|
+
globalThis.fetch = (async (
|
|
74
|
+
input: string | URL | Request,
|
|
75
|
+
init?: RequestInit,
|
|
76
|
+
) => {
|
|
77
|
+
const url =
|
|
78
|
+
typeof input === "string"
|
|
79
|
+
? input
|
|
80
|
+
: input instanceof URL
|
|
81
|
+
? input.toString()
|
|
82
|
+
: input.url;
|
|
83
|
+
fetchCalls.push({ url, init });
|
|
84
|
+
return respond(url, fetchCalls.length - 1);
|
|
85
|
+
}) as typeof fetch;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Await a handler call that must reject, returning the thrown error. */
|
|
89
|
+
async function expectRejection(run: () => unknown): Promise<Error> {
|
|
90
|
+
try {
|
|
91
|
+
await run();
|
|
92
|
+
} catch (err) {
|
|
93
|
+
return err as Error;
|
|
94
|
+
}
|
|
95
|
+
throw new Error("expected rejection");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function jsonResponse(body: unknown, status = 200): Response {
|
|
99
|
+
return new Response(JSON.stringify(body), {
|
|
100
|
+
status,
|
|
101
|
+
headers: { "Content-Type": "application/json" },
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
beforeEach(() => {
|
|
106
|
+
platformBaseUrl = "https://platform.test";
|
|
107
|
+
authHeader = "Api-Key test";
|
|
108
|
+
fetchCalls = [];
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
afterEach(() => {
|
|
112
|
+
globalThis.fetch = realFetch;
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("platform_invoices_list", () => {
|
|
116
|
+
test("returns the platform page and calls the bare invoices URL", async () => {
|
|
117
|
+
const page = { invoices: [invoice("in_1")], has_more: false };
|
|
118
|
+
stubFetch(() => jsonResponse(page));
|
|
119
|
+
|
|
120
|
+
expect(await listHandler({})).toEqual(page);
|
|
121
|
+
expect(fetchCalls).toHaveLength(1);
|
|
122
|
+
expect(fetchCalls[0]!.url).toBe(INVOICES_URL);
|
|
123
|
+
expect(
|
|
124
|
+
(fetchCalls[0]!.init?.headers as Record<string, string>).Authorization,
|
|
125
|
+
).toBe("Api-Key test");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("forwards starting_after as a cursor and passes has_more through", async () => {
|
|
129
|
+
const page = { invoices: [invoice("in_2")], has_more: true };
|
|
130
|
+
stubFetch(() => jsonResponse(page));
|
|
131
|
+
|
|
132
|
+
const result = await listHandler({
|
|
133
|
+
queryParams: { starting_after: "in_a" },
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
expect(fetchCalls[0]!.url).toBe(`${INVOICES_URL}?starting_after=in_a`);
|
|
137
|
+
expect((result as { has_more: boolean }).has_more).toBe(true);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("rejects with UnprocessableEntityError when credentials are missing", async () => {
|
|
141
|
+
platformBaseUrl = "";
|
|
142
|
+
stubFetch(() => jsonResponse({ invoices: [], has_more: false }));
|
|
143
|
+
|
|
144
|
+
await expect(listHandler({})).rejects.toBeInstanceOf(
|
|
145
|
+
UnprocessableEntityError,
|
|
146
|
+
);
|
|
147
|
+
expect(fetchCalls).toHaveLength(0);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("resolves an upstream 404 to an empty page", async () => {
|
|
151
|
+
stubFetch(() => new Response("Not found", { status: 404 }));
|
|
152
|
+
|
|
153
|
+
// The platform 404s the invoice list for an organization without invoice
|
|
154
|
+
// history; that is an empty page, not a server error.
|
|
155
|
+
expect(await listHandler({})).toEqual({ invoices: [], has_more: false });
|
|
156
|
+
expect(fetchCalls).toHaveLength(1);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("rejects with InternalError naming the status on a non-OK response", async () => {
|
|
160
|
+
stubFetch(() => jsonResponse({ detail: "boom" }, 500));
|
|
161
|
+
|
|
162
|
+
const err = await expectRejection(() => listHandler({}));
|
|
163
|
+
expect(err).toBeInstanceOf(InternalError);
|
|
164
|
+
expect(err.message).toMatch(/HTTP 500/);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("rejects with BadRequestError carrying the detail on an upstream 400", async () => {
|
|
168
|
+
stubFetch(
|
|
169
|
+
() => new Response("Invalid starting_after cursor", { status: 400 }),
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
const err = await expectRejection(() =>
|
|
173
|
+
listHandler({ queryParams: { starting_after: "in_bogus" } }),
|
|
174
|
+
);
|
|
175
|
+
expect(err).toBeInstanceOf(BadRequestError);
|
|
176
|
+
expect(err.message).toMatch(/Invalid starting_after cursor/);
|
|
177
|
+
expect(err.message).toMatch(
|
|
178
|
+
/use the id of the last invoice from the previous page/,
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("rejects with InternalError, not BadRequestError, on a 400 without a cursor", async () => {
|
|
183
|
+
stubFetch(() => new Response("malformed request", { status: 400 }));
|
|
184
|
+
|
|
185
|
+
const err = await expectRejection(() => listHandler({}));
|
|
186
|
+
// Without a caller-supplied starting_after there is nothing
|
|
187
|
+
// caller-correctable in a 400, so it stays an internal failure.
|
|
188
|
+
expect(err).not.toBeInstanceOf(BadRequestError);
|
|
189
|
+
expect(err).toBeInstanceOf(InternalError);
|
|
190
|
+
expect(err.message).toMatch(/HTTP 400/);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe("platform_invoices_by_id_get", () => {
|
|
195
|
+
test("resolves an invoice found on the first page with one fetch", async () => {
|
|
196
|
+
const target = invoice("in_target");
|
|
197
|
+
stubFetch(() =>
|
|
198
|
+
jsonResponse({ invoices: [invoice("in_1"), target], has_more: false }),
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
expect(await getHandler({ pathParams: { id: "in_target" } })).toEqual(
|
|
202
|
+
target,
|
|
203
|
+
);
|
|
204
|
+
expect(fetchCalls).toHaveLength(1);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("follows the cursor to a later page using the last invoice's id", async () => {
|
|
208
|
+
const target = invoice("in_target");
|
|
209
|
+
stubFetch((_url, callIndex) =>
|
|
210
|
+
callIndex === 0
|
|
211
|
+
? jsonResponse({
|
|
212
|
+
invoices: [invoice("in_a"), invoice("in_b")],
|
|
213
|
+
has_more: true,
|
|
214
|
+
})
|
|
215
|
+
: jsonResponse({ invoices: [target], has_more: false }),
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
expect(await getHandler({ pathParams: { id: "in_target" } })).toEqual(
|
|
219
|
+
target,
|
|
220
|
+
);
|
|
221
|
+
expect(fetchCalls).toHaveLength(2);
|
|
222
|
+
expect(fetchCalls[1]!.url).toBe(`${INVOICES_URL}?starting_after=in_b`);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("rejects with an actionable NotFoundError when the id is absent", async () => {
|
|
226
|
+
stubFetch(() =>
|
|
227
|
+
jsonResponse({ invoices: [invoice("in_other")], has_more: false }),
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
const err = await expectRejection(() =>
|
|
231
|
+
getHandler({ pathParams: { id: "in_missing" } }),
|
|
232
|
+
);
|
|
233
|
+
expect(err).toBeInstanceOf(NotFoundError);
|
|
234
|
+
expect(err.message).toMatch(
|
|
235
|
+
/Invoice "in_missing" not found.*assistant platform invoices list/,
|
|
236
|
+
);
|
|
237
|
+
// No paging beyond the final (has_more: false) page.
|
|
238
|
+
expect(fetchCalls).toHaveLength(1);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("rejects with NotFoundError, not InternalError, on an upstream 404", async () => {
|
|
242
|
+
stubFetch(() => new Response("Not found", { status: 404 }));
|
|
243
|
+
|
|
244
|
+
// A 404 from the platform means no invoice history: the walk sees an
|
|
245
|
+
// empty first page and reports the invoice as not found.
|
|
246
|
+
const err = await expectRejection(() =>
|
|
247
|
+
getHandler({ pathParams: { id: "in_missing" } }),
|
|
248
|
+
);
|
|
249
|
+
expect(err).not.toBeInstanceOf(InternalError);
|
|
250
|
+
expect(err).toBeInstanceOf(NotFoundError);
|
|
251
|
+
expect(fetchCalls).toHaveLength(1);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("stops after MAX_INVOICE_PAGES pages when every page has more", async () => {
|
|
255
|
+
stubFetch((_url, callIndex) =>
|
|
256
|
+
jsonResponse({
|
|
257
|
+
invoices: [invoice(`in_${callIndex}`)],
|
|
258
|
+
has_more: true,
|
|
259
|
+
}),
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
await expect(
|
|
263
|
+
getHandler({ pathParams: { id: "in_never" } }),
|
|
264
|
+
).rejects.toBeInstanceOf(InternalError);
|
|
265
|
+
expect(fetchCalls).toHaveLength(MAX_INVOICE_PAGES);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("stops the cursor walk when the request abort signal fires", async () => {
|
|
269
|
+
const controller = new AbortController();
|
|
270
|
+
stubFetch(() => {
|
|
271
|
+
// Abort after the first page has been served; the handler must not
|
|
272
|
+
// fetch a second page.
|
|
273
|
+
controller.abort();
|
|
274
|
+
return jsonResponse({ invoices: [invoice("in_a")], has_more: true });
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
const err = await expectRejection(() =>
|
|
278
|
+
getHandler({
|
|
279
|
+
pathParams: { id: "in_never" },
|
|
280
|
+
abortSignal: controller.signal,
|
|
281
|
+
}),
|
|
282
|
+
);
|
|
283
|
+
expect(err).toBeInstanceOf(InternalError);
|
|
284
|
+
expect(err.message).toMatch(/aborted/);
|
|
285
|
+
expect(fetchCalls).toHaveLength(1);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("rejects with InternalError when the walk exceeds the aggregate deadline", async () => {
|
|
289
|
+
const realDateNow = Date.now;
|
|
290
|
+
let now = 0;
|
|
291
|
+
Date.now = () => now;
|
|
292
|
+
stubFetch(() => {
|
|
293
|
+
// Push the clock past the deadline once the first page has been
|
|
294
|
+
// served; the handler must not fetch a second page.
|
|
295
|
+
now = INVOICE_WALK_DEADLINE_MS + 1;
|
|
296
|
+
return jsonResponse({ invoices: [invoice("in_a")], has_more: true });
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
const err = await expectRejection(() =>
|
|
301
|
+
getHandler({ pathParams: { id: "in_never" } }),
|
|
302
|
+
);
|
|
303
|
+
expect(err).toBeInstanceOf(InternalError);
|
|
304
|
+
expect(err.message).toMatch(/timed out after 30 seconds/);
|
|
305
|
+
} finally {
|
|
306
|
+
Date.now = realDateNow;
|
|
307
|
+
}
|
|
308
|
+
expect(fetchCalls).toHaveLength(1);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("caps the per-page fetch timeout to the remaining walk deadline", async () => {
|
|
312
|
+
const realDateNow = Date.now;
|
|
313
|
+
let now = 0;
|
|
314
|
+
Date.now = () => now;
|
|
315
|
+
stubFetch((_url, callIndex) => {
|
|
316
|
+
if (callIndex === 0) {
|
|
317
|
+
// Leave only 50ms of aggregate budget for the next page.
|
|
318
|
+
now = INVOICE_WALK_DEADLINE_MS - 50;
|
|
319
|
+
return jsonResponse({ invoices: [invoice("in_a")], has_more: true });
|
|
320
|
+
}
|
|
321
|
+
// Hang until the fetch's combined signal aborts. With the timeout
|
|
322
|
+
// capped to the remaining 50ms this resolves almost immediately; an
|
|
323
|
+
// uncapped 10s timeout would blow the assertion on real elapsed time.
|
|
324
|
+
const signal = fetchCalls[callIndex]!.init?.signal as AbortSignal;
|
|
325
|
+
return new Promise<Response>((_resolve, reject) => {
|
|
326
|
+
signal.addEventListener("abort", () => {
|
|
327
|
+
now = INVOICE_WALK_DEADLINE_MS + 1;
|
|
328
|
+
reject(signal.reason as Error);
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
const startedRealMs = realDateNow();
|
|
334
|
+
try {
|
|
335
|
+
const err = await expectRejection(() =>
|
|
336
|
+
getHandler({ pathParams: { id: "in_never" } }),
|
|
337
|
+
);
|
|
338
|
+
expect(err).toBeInstanceOf(InternalError);
|
|
339
|
+
expect(err.message).toMatch(/timed out after 30 seconds/);
|
|
340
|
+
} finally {
|
|
341
|
+
Date.now = realDateNow;
|
|
342
|
+
}
|
|
343
|
+
expect(fetchCalls).toHaveLength(2);
|
|
344
|
+
// The second fetch was cancelled by the capped ~50ms timeout, not the
|
|
345
|
+
// flat 10s per-request timeout.
|
|
346
|
+
expect(realDateNow() - startedRealMs).toBeLessThan(5_000);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test("threads the request abort signal into the page fetch", async () => {
|
|
350
|
+
const controller = new AbortController();
|
|
351
|
+
stubFetch(() =>
|
|
352
|
+
jsonResponse({ invoices: [invoice("in_target")], has_more: false }),
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
await getHandler({
|
|
356
|
+
pathParams: { id: "in_target" },
|
|
357
|
+
abortSignal: controller.signal,
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// The fetch signal must be derived from the caller's signal: aborting
|
|
361
|
+
// the request aborts the in-flight platform fetch.
|
|
362
|
+
const fetchSignal = fetchCalls[0]!.init?.signal as AbortSignal;
|
|
363
|
+
expect(fetchSignal.aborted).toBe(false);
|
|
364
|
+
controller.abort();
|
|
365
|
+
expect(fetchSignal.aborted).toBe(true);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
test("rejects with InternalError, not BadRequestError, on a 400 mid-walk", async () => {
|
|
369
|
+
stubFetch((_url, callIndex) =>
|
|
370
|
+
callIndex === 0
|
|
371
|
+
? jsonResponse({ invoices: [invoice("in_a")], has_more: true })
|
|
372
|
+
: new Response("Invalid starting_after cursor", { status: 400 }),
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
const err = await expectRejection(() =>
|
|
376
|
+
getHandler({ pathParams: { id: "in_never" } }),
|
|
377
|
+
);
|
|
378
|
+
// The walk's cursor is internally generated, so an upstream 400 is an
|
|
379
|
+
// internal failure rather than a caller input error.
|
|
380
|
+
expect(err).not.toBeInstanceOf(BadRequestError);
|
|
381
|
+
expect(err).toBeInstanceOf(InternalError);
|
|
382
|
+
expect(err.message).toMatch(/HTTP 400/);
|
|
383
|
+
expect(fetchCalls).toHaveLength(2);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test("rejects with BadRequestError when the id path param is missing", async () => {
|
|
387
|
+
stubFetch(() => jsonResponse({ invoices: [], has_more: false }));
|
|
388
|
+
|
|
389
|
+
await expect(getHandler({})).rejects.toBeInstanceOf(BadRequestError);
|
|
390
|
+
expect(fetchCalls).toHaveLength(0);
|
|
391
|
+
});
|
|
392
|
+
});
|
|
@@ -16,6 +16,13 @@ const originalFetch = globalThis.fetch;
|
|
|
16
16
|
|
|
17
17
|
mock.module("../../../security/secure-keys.js", () => ({
|
|
18
18
|
getSecureKeyAsync: async () => "xoxb-test",
|
|
19
|
+
// The route's import graph (api.ts -> auth.ts -> oauth/*) also reads the
|
|
20
|
+
// result-shaped accessor; provide it so the mock stays import-complete
|
|
21
|
+
// even if these static imports ever become dynamic.
|
|
22
|
+
getSecureKeyResultAsync: async () => ({
|
|
23
|
+
value: "xoxb-test",
|
|
24
|
+
unreachable: false,
|
|
25
|
+
}),
|
|
19
26
|
}));
|
|
20
27
|
|
|
21
28
|
mock.module("../../sync/sync-publisher.js", () => ({
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request-body validation for the workspace git admin routes.
|
|
3
|
+
*
|
|
4
|
+
* Each handler validates its body before touching the workspace git
|
|
5
|
+
* service, so a malformed body is rejected with a `BadRequestError`
|
|
6
|
+
* (→ 400) without running any git operation. The handlers are async,
|
|
7
|
+
* hence the `.rejects` form.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, test } from "bun:test";
|
|
11
|
+
|
|
12
|
+
import { BadRequestError } from "../errors.js";
|
|
13
|
+
import { ROUTES } from "../workspace-commit-routes.js";
|
|
14
|
+
|
|
15
|
+
const routeFor = (operationId: string) => {
|
|
16
|
+
const route = ROUTES.find((r) => r.operationId === operationId);
|
|
17
|
+
if (!route) {
|
|
18
|
+
throw new Error(`no route: ${operationId}`);
|
|
19
|
+
}
|
|
20
|
+
return route;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
describe("workspace git route body validation", () => {
|
|
24
|
+
test("workspace_commit rejects a missing message", async () => {
|
|
25
|
+
await expect(
|
|
26
|
+
routeFor("workspace_commit").handler({
|
|
27
|
+
body: {} as Record<string, unknown>,
|
|
28
|
+
}),
|
|
29
|
+
).rejects.toThrow(BadRequestError);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("workspace_commit rejects an empty message", async () => {
|
|
33
|
+
await expect(
|
|
34
|
+
routeFor("workspace_commit").handler({
|
|
35
|
+
body: { message: "" } as Record<string, unknown>,
|
|
36
|
+
}),
|
|
37
|
+
).rejects.toThrow(BadRequestError);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("workspace_compact_history rejects a non-boolean force", async () => {
|
|
41
|
+
await expect(
|
|
42
|
+
routeFor("workspace_compact_history").handler({
|
|
43
|
+
body: { force: "yes" } as Record<string, unknown>,
|
|
44
|
+
}),
|
|
45
|
+
).rejects.toThrow(BadRequestError);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("workspace_compact_history rejects a non-integer retentionDays", async () => {
|
|
49
|
+
await expect(
|
|
50
|
+
routeFor("workspace_compact_history").handler({
|
|
51
|
+
body: { retentionDays: 1.5 } as Record<string, unknown>,
|
|
52
|
+
}),
|
|
53
|
+
).rejects.toThrow(BadRequestError);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("workspace_compact_history rejects a non-positive retentionDays", async () => {
|
|
57
|
+
await expect(
|
|
58
|
+
routeFor("workspace_compact_history").handler({
|
|
59
|
+
body: { retentionDays: 0 } as Record<string, unknown>,
|
|
60
|
+
}),
|
|
61
|
+
).rejects.toThrow(BadRequestError);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
getMessageRoleStatsByConversation,
|
|
23
23
|
listConversationsBySource,
|
|
24
24
|
} from "../../persistence/conversation-queries.js";
|
|
25
|
+
import { MEMORY_V2_CONSOLIDATION_SOURCE } from "../../persistence/conversation-types.js";
|
|
25
26
|
import {
|
|
26
27
|
enqueueMemoryJob,
|
|
27
28
|
hasActiveJobOfType,
|
|
@@ -29,7 +30,6 @@ import {
|
|
|
29
30
|
} from "../../persistence/jobs-store.js";
|
|
30
31
|
import { getUsageCostForConversationWindow } from "../../persistence/llm-usage-store.js";
|
|
31
32
|
import { GRAPH_MAINTENANCE_CHECKPOINTS } from "../../plugins/defaults/memory/jobs-worker.js";
|
|
32
|
-
import { MEMORY_V2_CONSOLIDATION_SOURCE } from "../../plugins/defaults/memory/substrate/constants.js";
|
|
33
33
|
import { resolveSubstrateTuning } from "../../plugins/defaults/memory/substrate/tuning.js";
|
|
34
34
|
import { ACTOR_PRINCIPALS } from "../auth/route-policy.js";
|
|
35
35
|
import { BadRequestError } from "./errors.js";
|