@stigmer/runner 3.9.0 → 3.11.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.
Files changed (270) hide show
  1. package/README.md +12 -1
  2. package/dist/.build-fingerprint +1 -1
  3. package/dist/activities/call-llm.js +9 -10
  4. package/dist/activities/call-llm.js.map +1 -1
  5. package/dist/activities/classify-tool-approvals.d.ts +2 -1
  6. package/dist/activities/classify-tool-approvals.js +28 -2
  7. package/dist/activities/classify-tool-approvals.js.map +1 -1
  8. package/dist/activities/discover-mcp-server.d.ts +32 -0
  9. package/dist/activities/discover-mcp-server.js +162 -27
  10. package/dist/activities/discover-mcp-server.js.map +1 -1
  11. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.d.ts +8 -0
  12. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js +1 -1
  13. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js.map +1 -1
  14. package/dist/activities/execute-cursor/approval-state.d.ts +28 -2
  15. package/dist/activities/execute-cursor/approval-state.js +7 -1
  16. package/dist/activities/execute-cursor/approval-state.js.map +1 -1
  17. package/dist/activities/execute-cursor/attachment-resolver.d.ts +14 -0
  18. package/dist/activities/execute-cursor/attachment-resolver.js +25 -4
  19. package/dist/activities/execute-cursor/attachment-resolver.js.map +1 -1
  20. package/dist/activities/execute-cursor/blueprint-resolver.d.ts +1 -9
  21. package/dist/activities/execute-cursor/blueprint-resolver.js +6 -22
  22. package/dist/activities/execute-cursor/blueprint-resolver.js.map +1 -1
  23. package/dist/activities/execute-cursor/env-resolver.js +3 -1
  24. package/dist/activities/execute-cursor/env-resolver.js.map +1 -1
  25. package/dist/activities/execute-cursor/error-classifier.d.ts +40 -3
  26. package/dist/activities/execute-cursor/error-classifier.js +81 -3
  27. package/dist/activities/execute-cursor/error-classifier.js.map +1 -1
  28. package/dist/activities/execute-cursor/extract-structured-output.d.ts +29 -0
  29. package/dist/activities/execute-cursor/extract-structured-output.js +58 -0
  30. package/dist/activities/execute-cursor/extract-structured-output.js.map +1 -0
  31. package/dist/activities/execute-cursor/hook-script.d.ts +14 -3
  32. package/dist/activities/execute-cursor/hook-script.js +72 -10
  33. package/dist/activities/execute-cursor/hook-script.js.map +1 -1
  34. package/dist/activities/execute-cursor/index.d.ts +5 -1
  35. package/dist/activities/execute-cursor/index.js +59 -58
  36. package/dist/activities/execute-cursor/index.js.map +1 -1
  37. package/dist/activities/execute-cursor/mcp-resolver.d.ts +24 -1
  38. package/dist/activities/execute-cursor/mcp-resolver.js +5 -2
  39. package/dist/activities/execute-cursor/mcp-resolver.js.map +1 -1
  40. package/dist/activities/execute-cursor/prompt-builder.d.ts +18 -4
  41. package/dist/activities/execute-cursor/prompt-builder.js +12 -7
  42. package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
  43. package/dist/activities/execute-cursor/turn-stream.js +4 -1
  44. package/dist/activities/execute-cursor/turn-stream.js.map +1 -1
  45. package/dist/activities/execute-deep-agent/attachment-injector.d.ts +18 -1
  46. package/dist/activities/execute-deep-agent/attachment-injector.js +68 -23
  47. package/dist/activities/execute-deep-agent/attachment-injector.js.map +1 -1
  48. package/dist/activities/execute-deep-agent/environment.js +3 -1
  49. package/dist/activities/execute-deep-agent/environment.js.map +1 -1
  50. package/dist/activities/execute-deep-agent/index.js +15 -0
  51. package/dist/activities/execute-deep-agent/index.js.map +1 -1
  52. package/dist/activities/execute-deep-agent/prompt-builder.d.ts +7 -7
  53. package/dist/activities/execute-deep-agent/prompt-builder.js +8 -2
  54. package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
  55. package/dist/activities/execute-deep-agent/setup.d.ts +10 -0
  56. package/dist/activities/execute-deep-agent/setup.js +65 -26
  57. package/dist/activities/execute-deep-agent/setup.js.map +1 -1
  58. package/dist/activities/execute-deep-agent/subagent-transformer.d.ts +18 -1
  59. package/dist/activities/execute-deep-agent/subagent-transformer.js +8 -1
  60. package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
  61. package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +11 -4
  62. package/dist/activities/execute-deep-agent/subagent-wiring.js +13 -4
  63. package/dist/activities/execute-deep-agent/subagent-wiring.js.map +1 -1
  64. package/dist/activities/hydrate-workflow-execution.js +3 -1
  65. package/dist/activities/hydrate-workflow-execution.js.map +1 -1
  66. package/dist/activities/workflow-event-activities.d.ts +28 -10
  67. package/dist/activities/workflow-event-activities.js +87 -58
  68. package/dist/activities/workflow-event-activities.js.map +1 -1
  69. package/dist/claimcheck/payload-codec.js +21 -1
  70. package/dist/claimcheck/payload-codec.js.map +1 -1
  71. package/dist/client/stigmer-client.d.ts +9 -4
  72. package/dist/client/stigmer-client.js +28 -15
  73. package/dist/client/stigmer-client.js.map +1 -1
  74. package/dist/encryption/config.d.ts +32 -0
  75. package/dist/encryption/config.js +68 -0
  76. package/dist/encryption/config.js.map +1 -0
  77. package/dist/encryption/index.d.ts +3 -0
  78. package/dist/encryption/index.js +3 -0
  79. package/dist/encryption/index.js.map +1 -0
  80. package/dist/encryption/payload-codec.d.ts +41 -0
  81. package/dist/encryption/payload-codec.js +130 -0
  82. package/dist/encryption/payload-codec.js.map +1 -0
  83. package/dist/payload-codecs.d.ts +16 -0
  84. package/dist/payload-codecs.js +38 -0
  85. package/dist/payload-codecs.js.map +1 -0
  86. package/dist/preflight.d.ts +31 -0
  87. package/dist/preflight.js +43 -0
  88. package/dist/preflight.js.map +1 -1
  89. package/dist/runner-manager.js +5 -15
  90. package/dist/runner-manager.js.map +1 -1
  91. package/dist/runner.js +5 -16
  92. package/dist/runner.js.map +1 -1
  93. package/dist/shared/approval-policy.d.ts +9 -3
  94. package/dist/shared/approval-policy.js +15 -6
  95. package/dist/shared/approval-policy.js.map +1 -1
  96. package/dist/shared/attachment-naming.d.ts +53 -0
  97. package/dist/shared/attachment-naming.js +59 -0
  98. package/dist/shared/attachment-naming.js.map +1 -0
  99. package/dist/shared/attachment-vision.d.ts +43 -2
  100. package/dist/shared/attachment-vision.js +72 -6
  101. package/dist/shared/attachment-vision.js.map +1 -1
  102. package/dist/shared/caller-identity.d.ts +23 -2
  103. package/dist/shared/caller-identity.js +36 -5
  104. package/dist/shared/caller-identity.js.map +1 -1
  105. package/dist/shared/channel-attachment.js +1 -0
  106. package/dist/shared/channel-attachment.js.map +1 -1
  107. package/dist/shared/checkpointer/http-saver.d.ts +42 -1
  108. package/dist/shared/checkpointer/http-saver.js +96 -8
  109. package/dist/shared/checkpointer/http-saver.js.map +1 -1
  110. package/dist/shared/conversation-attachment.js +1 -0
  111. package/dist/shared/conversation-attachment.js.map +1 -1
  112. package/dist/shared/datastore-attachment.d.ts +50 -7
  113. package/dist/shared/datastore-attachment.js +93 -11
  114. package/dist/shared/datastore-attachment.js.map +1 -1
  115. package/dist/shared/http-retry.d.ts +43 -0
  116. package/dist/shared/http-retry.js +50 -0
  117. package/dist/shared/http-retry.js.map +1 -0
  118. package/dist/shared/llm-backend.d.ts +275 -0
  119. package/dist/shared/llm-backend.js +425 -0
  120. package/dist/shared/llm-backend.js.map +1 -0
  121. package/dist/shared/llm-proxy.d.ts +8 -0
  122. package/dist/shared/llm-proxy.js +15 -0
  123. package/dist/shared/llm-proxy.js.map +1 -1
  124. package/dist/shared/mcp-enabled-tools.d.ts +57 -0
  125. package/dist/shared/mcp-enabled-tools.js +86 -0
  126. package/dist/shared/mcp-enabled-tools.js.map +1 -0
  127. package/dist/shared/mcp-manager.d.ts +17 -2
  128. package/dist/shared/mcp-manager.js +37 -25
  129. package/dist/shared/mcp-manager.js.map +1 -1
  130. package/dist/shared/mcp-resolver.d.ts +39 -2
  131. package/dist/shared/mcp-resolver.js +38 -2
  132. package/dist/shared/mcp-resolver.js.map +1 -1
  133. package/dist/shared/model-client.d.ts +12 -5
  134. package/dist/shared/model-client.js +138 -18
  135. package/dist/shared/model-client.js.map +1 -1
  136. package/dist/shared/model-error.js +198 -5
  137. package/dist/shared/model-error.js.map +1 -1
  138. package/dist/shared/model-registry.d.ts +20 -2
  139. package/dist/shared/model-registry.js +37 -2
  140. package/dist/shared/model-registry.js.map +1 -1
  141. package/dist/shared/plan-mode-permissions.d.ts +26 -0
  142. package/dist/shared/plan-mode-permissions.js +28 -0
  143. package/dist/shared/plan-mode-permissions.js.map +1 -0
  144. package/dist/worker.d.ts +2 -1
  145. package/dist/worker.js +2 -4
  146. package/dist/worker.js.map +1 -1
  147. package/dist/workflow-engine/types.d.ts +18 -0
  148. package/dist/workflow-engine/types.js.map +1 -1
  149. package/dist/workflows/call-agent-orchestrator.d.ts +9 -0
  150. package/dist/workflows/call-agent-orchestrator.js +1 -0
  151. package/dist/workflows/call-agent-orchestrator.js.map +1 -1
  152. package/dist/workflows/connect-mcp-server.js +7 -0
  153. package/dist/workflows/connect-mcp-server.js.map +1 -1
  154. package/dist/workflows/engine-core.js +23 -2
  155. package/dist/workflows/engine-core.js.map +1 -1
  156. package/dist/workflows/execute-from-execution.d.ts +1 -1
  157. package/dist/workflows/execute-from-execution.js +11 -1
  158. package/dist/workflows/execute-from-execution.js.map +1 -1
  159. package/package.json +8 -2
  160. package/src/__tests__/claimcheck-codec.test.ts +36 -0
  161. package/src/__tests__/encryption-codec.test.ts +234 -0
  162. package/src/__tests__/fixtures/encrypted-payload-fixture.json +15 -0
  163. package/src/__tests__/history-encryption-e2e.test.ts +243 -0
  164. package/src/__tests__/preflight.test.ts +50 -2
  165. package/src/activities/__tests__/call-llm.test.ts +75 -0
  166. package/src/activities/__tests__/classify-tool-approvals.test.ts +117 -1
  167. package/src/activities/__tests__/discover-mcp-server.hang.test.ts +103 -0
  168. package/src/activities/__tests__/discover-mcp-server.test.ts +203 -0
  169. package/src/activities/__tests__/workflow-event-activities.test.ts +107 -8
  170. package/src/activities/call-llm.ts +9 -16
  171. package/src/activities/classify-tool-approvals.ts +34 -4
  172. package/src/activities/discover-mcp-server.ts +190 -32
  173. package/src/activities/execute-cursor/__test-utils__/cursor-hook-harness.ts +9 -0
  174. package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +14 -0
  175. package/src/activities/execute-cursor/__tests__/attachment-resolver.test.ts +92 -0
  176. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +40 -14
  177. package/src/activities/execute-cursor/__tests__/error-classifier-extraction.test.ts +208 -0
  178. package/src/activities/execute-cursor/__tests__/extract-structured-output.test.ts +120 -0
  179. package/src/activities/execute-cursor/__tests__/hook-script.test.ts +93 -0
  180. package/src/activities/execute-cursor/__tests__/mcp-resolver.test.ts +125 -0
  181. package/src/activities/execute-cursor/__tests__/prompt-builder-delegation.test.ts +1 -1
  182. package/src/activities/execute-cursor/__tests__/turn-stream.test.ts +13 -0
  183. package/src/activities/execute-cursor/approval-state.ts +30 -1
  184. package/src/activities/execute-cursor/attachment-resolver.ts +38 -3
  185. package/src/activities/execute-cursor/blueprint-resolver.ts +7 -27
  186. package/src/activities/execute-cursor/env-resolver.ts +3 -1
  187. package/src/activities/execute-cursor/error-classifier.ts +91 -4
  188. package/src/activities/execute-cursor/extract-structured-output.ts +72 -0
  189. package/src/activities/execute-cursor/hook-script.ts +74 -10
  190. package/src/activities/execute-cursor/index.ts +63 -72
  191. package/src/activities/execute-cursor/mcp-resolver.ts +36 -2
  192. package/src/activities/execute-cursor/prompt-builder.ts +34 -9
  193. package/src/activities/execute-cursor/turn-stream.ts +5 -2
  194. package/src/activities/execute-deep-agent/__tests__/attachment-injector.test.ts +132 -8
  195. package/src/activities/execute-deep-agent/__tests__/datastore-degradation.test.ts +104 -0
  196. package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +2 -0
  197. package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +1 -0
  198. package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +1 -0
  199. package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +34 -5
  200. package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +1 -0
  201. package/src/activities/execute-deep-agent/__tests__/subagent-plan-mode-permissions.test.ts +173 -0
  202. package/src/activities/execute-deep-agent/__tests__/subagent-wiring.test.ts +12 -7
  203. package/src/activities/execute-deep-agent/attachment-injector.ts +94 -30
  204. package/src/activities/execute-deep-agent/environment.ts +3 -1
  205. package/src/activities/execute-deep-agent/index.ts +20 -0
  206. package/src/activities/execute-deep-agent/prompt-builder.ts +20 -10
  207. package/src/activities/execute-deep-agent/setup.ts +86 -31
  208. package/src/activities/execute-deep-agent/subagent-transformer.ts +23 -1
  209. package/src/activities/execute-deep-agent/subagent-wiring.ts +14 -4
  210. package/src/activities/hydrate-workflow-execution.ts +3 -1
  211. package/src/activities/workflow-event-activities.ts +96 -69
  212. package/src/claimcheck/payload-codec.ts +33 -1
  213. package/src/client/__tests__/stigmer-client.test.ts +8 -8
  214. package/src/client/stigmer-client.ts +32 -18
  215. package/src/encryption/config.ts +91 -0
  216. package/src/encryption/index.ts +3 -0
  217. package/src/encryption/payload-codec.ts +152 -0
  218. package/src/payload-codecs.ts +56 -0
  219. package/src/preflight.ts +45 -0
  220. package/src/runner-manager.ts +6 -24
  221. package/src/runner.ts +6 -25
  222. package/src/shared/__tests__/approval-policy.test.ts +82 -39
  223. package/src/shared/__tests__/attachment-naming.test.ts +159 -0
  224. package/src/shared/__tests__/attachment-vision.test.ts +97 -0
  225. package/src/shared/__tests__/bedrock-adapter.test.ts +213 -0
  226. package/src/shared/__tests__/bedrock-seam.test.ts +390 -0
  227. package/src/shared/__tests__/caller-identity.test.ts +25 -0
  228. package/src/shared/__tests__/channel-attachment.test.ts +1 -1
  229. package/src/shared/__tests__/connect-backfill.test.ts +1 -0
  230. package/src/shared/__tests__/conversation-attachment.test.ts +1 -1
  231. package/src/shared/__tests__/datastore-attachment.test.ts +129 -1
  232. package/src/shared/__tests__/foundry-adapter.test.ts +276 -0
  233. package/src/shared/__tests__/foundry-seam.test.ts +482 -0
  234. package/src/shared/__tests__/http-retry.test.ts +67 -0
  235. package/src/shared/__tests__/llm-backend.test.ts +616 -0
  236. package/src/shared/__tests__/mcp-enabled-tools.test.ts +86 -0
  237. package/src/shared/__tests__/mcp-manager.test.ts +170 -2
  238. package/src/shared/__tests__/mcp-resolver.test.ts +146 -3
  239. package/src/shared/__tests__/model-client.test.ts +154 -0
  240. package/src/shared/__tests__/model-error.test.ts +289 -1
  241. package/src/shared/__tests__/model-registry.test.ts +71 -0
  242. package/src/shared/__tests__/synthesized-attachment.test.ts +1 -0
  243. package/src/shared/__tests__/vertex-adapter.test.ts +169 -0
  244. package/src/shared/__tests__/vertex-seam.test.ts +295 -0
  245. package/src/shared/approval-policy.ts +14 -7
  246. package/src/shared/attachment-naming.ts +78 -0
  247. package/src/shared/attachment-vision.ts +92 -9
  248. package/src/shared/caller-identity.ts +40 -5
  249. package/src/shared/channel-attachment.ts +1 -0
  250. package/src/shared/checkpointer/__tests__/http-saver.test.ts +196 -1
  251. package/src/shared/checkpointer/http-saver.ts +117 -9
  252. package/src/shared/conversation-attachment.ts +1 -0
  253. package/src/shared/datastore-attachment.ts +106 -11
  254. package/src/shared/http-retry.ts +50 -0
  255. package/src/shared/llm-backend.ts +544 -0
  256. package/src/shared/llm-proxy.ts +15 -0
  257. package/src/shared/mcp-enabled-tools.ts +105 -0
  258. package/src/shared/mcp-manager.ts +43 -26
  259. package/src/shared/mcp-resolver.ts +73 -2
  260. package/src/shared/model-client.ts +161 -19
  261. package/src/shared/model-error.ts +222 -4
  262. package/src/shared/model-registry.ts +50 -2
  263. package/src/shared/plan-mode-permissions.ts +30 -0
  264. package/src/worker.ts +4 -5
  265. package/src/workflow-engine/types.ts +18 -0
  266. package/src/workflows/__tests__/execute-serverless-workflow.test.ts +68 -2
  267. package/src/workflows/call-agent-orchestrator.ts +10 -0
  268. package/src/workflows/connect-mcp-server.ts +7 -0
  269. package/src/workflows/engine-core.ts +23 -2
  270. package/src/workflows/execute-from-execution.ts +12 -2
@@ -0,0 +1,616 @@
1
+ import { describe, it, expect } from "vitest";
2
+
3
+ import {
4
+ toVertexModelId,
5
+ toBedrockModelId,
6
+ toFoundryDeploymentName,
7
+ parseAnthropicBackend,
8
+ parseOpenAiBackend,
9
+ parseBedrockModelMap,
10
+ parseFoundryDeploymentMap,
11
+ resolveAnthropicBackend,
12
+ checkVertexPrerequisites,
13
+ checkBedrockPrerequisites,
14
+ checkFoundryPrerequisites,
15
+ checkDirectCredentials,
16
+ preflightLlmBackends,
17
+ } from "../llm-backend.js";
18
+
19
+ describe("toVertexModelId", () => {
20
+ // Both id shapes below are what the registry actually serves (see
21
+ // stigmer-server registry/data/model-registry.json): pre-4.6 models are
22
+ // dated, 4.6-generation and later are dateless canonical ids.
23
+ it.each([
24
+ // Pre-4.6 snapshot ids: the trailing date separator becomes `@`.
25
+ ["claude-sonnet-4-5-20250929", "claude-sonnet-4-5@20250929"],
26
+ ["claude-haiku-4-5-20251001", "claude-haiku-4-5@20251001"],
27
+ ["claude-opus-4-5-20251101", "claude-opus-4-5@20251101"],
28
+
29
+ // 4.6-generation and later: dateless ids are canonical on Vertex and
30
+ // MUST pass through untouched. Appending a date 404s (the bug Dify
31
+ // langgenius/dify-official-plugins#2905 and Roo-Code #11625 shipped).
32
+ ["claude-sonnet-4-6", "claude-sonnet-4-6"],
33
+ ["claude-opus-4-6", "claude-opus-4-6"],
34
+ ["claude-opus-4-7", "claude-opus-4-7"],
35
+ ["claude-opus-4-8", "claude-opus-4-8"],
36
+ ["claude-sonnet-5", "claude-sonnet-5"],
37
+ ["claude-fable-5", "claude-fable-5"],
38
+
39
+ // Already in Vertex form: translating twice is a no-op.
40
+ ["claude-sonnet-4-5@20250929", "claude-sonnet-4-5@20250929"],
41
+
42
+ // Bedrock-shaped ids never match the trailing-date pattern.
43
+ ["anthropic.claude-sonnet-4-5-20250929-v1:0", "anthropic.claude-sonnet-4-5-20250929-v1:0"],
44
+
45
+ // Version segments are not dates; nothing to rewrite.
46
+ ["claude-3-5-sonnet", "claude-3-5-sonnet"],
47
+ ["", ""],
48
+ ])("%s -> %s", (input, expected) => {
49
+ expect(toVertexModelId(input)).toBe(expected);
50
+ });
51
+ });
52
+
53
+ // Every function takes env as a parameter (defaulting to process.env), so
54
+ // these tables pass explicit records instead of stubbing globals.
55
+
56
+ describe("parseAnthropicBackend", () => {
57
+ it.each([
58
+ // Unset / blank → the public API, the zero-config default.
59
+ [undefined, "public"],
60
+ ["", "public"],
61
+ [" ", "public"],
62
+ // Explicit values, normalized for casing and whitespace.
63
+ ["public", "public"],
64
+ ["vertex", "vertex"],
65
+ ["Vertex", "vertex"],
66
+ [" VERTEX ", "vertex"],
67
+ ["bedrock", "bedrock"],
68
+ [" Bedrock ", "bedrock"],
69
+ ["foundry", "foundry"],
70
+ [" Foundry ", "foundry"],
71
+ ])("value %j parses to %s", (value, backend) => {
72
+ const result = parseAnthropicBackend({ STIGMER_ANTHROPIC_BACKEND: value });
73
+ expect(result).toEqual({ ok: true, backend });
74
+ });
75
+
76
+ it("rejects an unknown value naming the var, the value, and the supported set", () => {
77
+ const result = parseAnthropicBackend({ STIGMER_ANTHROPIC_BACKEND: "verteks" });
78
+ expect(result.ok).toBe(false);
79
+ if (!result.ok) {
80
+ expect(result.message).toContain('STIGMER_ANTHROPIC_BACKEND="verteks"');
81
+ expect(result.message).toContain("Supported: public, vertex, bedrock, foundry");
82
+ }
83
+ });
84
+
85
+ it("rejects the cloud name where the service name is the value (azure vs foundry)", () => {
86
+ // The service that hosts Claude on Azure is Microsoft Foundry; "azure"
87
+ // as an Anthropic backend value would collide with the unrelated Azure
88
+ // OpenAI service reserved on the OpenAI axis. The unsupported-value
89
+ // message lists foundry, steering the operator to the right value.
90
+ const result = parseAnthropicBackend({ STIGMER_ANTHROPIC_BACKEND: "azure" });
91
+ expect(result.ok).toBe(false);
92
+ if (!result.ok) {
93
+ expect(result.message).toContain("foundry");
94
+ }
95
+ });
96
+ });
97
+
98
+ describe("parseOpenAiBackend", () => {
99
+ it.each([[undefined], [""], ["public"], ["PUBLIC"]])(
100
+ "value %j parses to public",
101
+ (value) => {
102
+ const result = parseOpenAiBackend({ STIGMER_OPENAI_BACKEND: value });
103
+ expect(result).toEqual({ ok: true, backend: "public" });
104
+ },
105
+ );
106
+
107
+ it("rejects azure as recognized but not implemented", () => {
108
+ const result = parseOpenAiBackend({ STIGMER_OPENAI_BACKEND: "azure" });
109
+ expect(result.ok).toBe(false);
110
+ if (!result.ok) {
111
+ expect(result.message).toContain("STIGMER_OPENAI_BACKEND");
112
+ expect(result.message).toContain("not implemented in this build");
113
+ }
114
+ });
115
+
116
+ it("rejects a wrong-provider value (vertex is an Anthropic backend)", () => {
117
+ const result = parseOpenAiBackend({ STIGMER_OPENAI_BACKEND: "vertex" });
118
+ expect(result.ok).toBe(false);
119
+ if (!result.ok) {
120
+ expect(result.message).toContain('STIGMER_OPENAI_BACKEND="vertex"');
121
+ expect(result.message).toContain("Supported: public");
122
+ }
123
+ });
124
+ });
125
+
126
+ describe("resolveAnthropicBackend", () => {
127
+ it("returns the parsed backend for valid values", () => {
128
+ expect(resolveAnthropicBackend({ STIGMER_ANTHROPIC_BACKEND: "vertex" })).toBe("vertex");
129
+ expect(resolveAnthropicBackend({})).toBe("public");
130
+ });
131
+
132
+ it("throws the parser's exact message on an invalid value", () => {
133
+ expect(() => resolveAnthropicBackend({ STIGMER_ANTHROPIC_BACKEND: "verteks" }))
134
+ .toThrow(/STIGMER_ANTHROPIC_BACKEND="verteks" is not a supported backend/);
135
+ });
136
+ });
137
+
138
+ describe("checkVertexPrerequisites", () => {
139
+ it("passes when CLOUD_ML_REGION is set", () => {
140
+ expect(checkVertexPrerequisites({ CLOUD_ML_REGION: "asia-south1" })).toBeNull();
141
+ expect(checkVertexPrerequisites({ CLOUD_ML_REGION: "global" })).toBeNull();
142
+ });
143
+
144
+ it.each([[undefined], [""], [" "]])(
145
+ "fails with an actionable message when region is %j",
146
+ (region) => {
147
+ const message = checkVertexPrerequisites({ CLOUD_ML_REGION: region });
148
+ expect(message).toContain("CLOUD_ML_REGION");
149
+ expect(message).toContain("asia-south1");
150
+ },
151
+ );
152
+ });
153
+
154
+ describe("checkBedrockPrerequisites", () => {
155
+ it("passes with an explicit AWS_REGION", () => {
156
+ expect(checkBedrockPrerequisites({ AWS_REGION: "ap-south-1" })).toBeNull();
157
+ });
158
+
159
+ it.each([[undefined], [""], [" "]])(
160
+ "fails when region is %j — the SDK's us-east-1 default must never apply silently",
161
+ (region) => {
162
+ const message = checkBedrockPrerequisites({ AWS_REGION: region });
163
+ expect(message).toContain("AWS_REGION");
164
+ expect(message).toContain("us-east-1");
165
+ expect(message).toContain("docs.stigmer.ai");
166
+ },
167
+ );
168
+
169
+ it("fails on a malformed model map at boot, not at the first model call", () => {
170
+ const message = checkBedrockPrerequisites({
171
+ AWS_REGION: "ap-south-1",
172
+ STIGMER_BEDROCK_MODEL_MAP: "claude-sonnet-4-6",
173
+ });
174
+ expect(message).toContain("STIGMER_BEDROCK_MODEL_MAP");
175
+ expect(message).toContain("canonical=bedrockId");
176
+ });
177
+
178
+ it("accepts a well-formed model map", () => {
179
+ expect(
180
+ checkBedrockPrerequisites({
181
+ AWS_REGION: "eu-central-1",
182
+ STIGMER_BEDROCK_MODEL_MAP:
183
+ "claude-sonnet-4-6=eu.anthropic.claude-sonnet-4-6-v1:0, claude-fable-5=eu.anthropic.claude-fable-5-v1:0",
184
+ }),
185
+ ).toBeNull();
186
+ });
187
+ });
188
+
189
+ describe("checkFoundryPrerequisites", () => {
190
+ it("passes with a resource name alone", () => {
191
+ expect(
192
+ checkFoundryPrerequisites({ ANTHROPIC_FOUNDRY_RESOURCE: "my-foundry-resource" }),
193
+ ).toBeNull();
194
+ });
195
+
196
+ it("passes with a base URL alone", () => {
197
+ expect(
198
+ checkFoundryPrerequisites({
199
+ ANTHROPIC_FOUNDRY_BASE_URL: "https://my-foundry-resource.services.ai.azure.com/anthropic/",
200
+ }),
201
+ ).toBeNull();
202
+ });
203
+
204
+ it.each([[{}], [{ ANTHROPIC_FOUNDRY_RESOURCE: " " }], [{ ANTHROPIC_FOUNDRY_BASE_URL: "" }]])(
205
+ "fails with an actionable message when no endpoint is configured (%j)",
206
+ (env) => {
207
+ const message = checkFoundryPrerequisites(env);
208
+ expect(message).toContain("ANTHROPIC_FOUNDRY_RESOURCE");
209
+ expect(message).toContain("ANTHROPIC_FOUNDRY_BASE_URL");
210
+ expect(message).toContain("docs.stigmer.ai");
211
+ },
212
+ );
213
+
214
+ it("fails when both resource and base URL are set (the SDK's own contract, at boot)", () => {
215
+ // The Foundry SDK constructor throws on this combination; catching it
216
+ // here turns a first-model-call crash into a boot refusal.
217
+ const message = checkFoundryPrerequisites({
218
+ ANTHROPIC_FOUNDRY_RESOURCE: "my-foundry-resource",
219
+ ANTHROPIC_FOUNDRY_BASE_URL: "https://other.services.ai.azure.com/anthropic/",
220
+ });
221
+ expect(message).toContain("mutually exclusive");
222
+ });
223
+
224
+ it("does not require a credential — keyless Entra deployments are valid", () => {
225
+ // ANTHROPIC_FOUNDRY_API_KEY absence is not an error: the adapter falls
226
+ // back to the Azure credential chain, which resolves at request time
227
+ // like Vertex ADC and the AWS chain.
228
+ expect(
229
+ checkFoundryPrerequisites({ ANTHROPIC_FOUNDRY_RESOURCE: "my-foundry-resource" }),
230
+ ).toBeNull();
231
+ });
232
+
233
+ it("fails on a malformed deployment map at boot, not at the first model call", () => {
234
+ const message = checkFoundryPrerequisites({
235
+ ANTHROPIC_FOUNDRY_RESOURCE: "my-foundry-resource",
236
+ STIGMER_FOUNDRY_DEPLOYMENT_MAP: "claude-sonnet-4-6",
237
+ });
238
+ expect(message).toContain("STIGMER_FOUNDRY_DEPLOYMENT_MAP");
239
+ expect(message).toContain("canonical=deploymentName");
240
+ });
241
+
242
+ it("accepts a well-formed deployment map", () => {
243
+ expect(
244
+ checkFoundryPrerequisites({
245
+ ANTHROPIC_FOUNDRY_RESOURCE: "my-foundry-resource",
246
+ STIGMER_FOUNDRY_DEPLOYMENT_MAP:
247
+ "claude-sonnet-4-6=my-sonnet-deployment, claude-fable-5=my-fable-deployment",
248
+ }),
249
+ ).toBeNull();
250
+ });
251
+ });
252
+
253
+ describe("parseFoundryDeploymentMap", () => {
254
+ // The shared pair-parsing behavior (trimming, empty entries, blank ->
255
+ // empty map) is exercised exhaustively by the parseBedrockModelMap table;
256
+ // these pin what is foundry-specific: the var name and message wording.
257
+ it("parses pairs into canonical -> deployment name", () => {
258
+ const result = parseFoundryDeploymentMap({
259
+ STIGMER_FOUNDRY_DEPLOYMENT_MAP: "claude-sonnet-4-6=my-sonnet-deployment",
260
+ });
261
+ expect(result.ok).toBe(true);
262
+ if (result.ok) {
263
+ expect(result.map.get("claude-sonnet-4-6")).toBe("my-sonnet-deployment");
264
+ }
265
+ });
266
+
267
+ it("rejects a malformed entry naming its own var and form", () => {
268
+ const result = parseFoundryDeploymentMap({
269
+ STIGMER_FOUNDRY_DEPLOYMENT_MAP: "no-equals-sign",
270
+ });
271
+ expect(result.ok).toBe(false);
272
+ if (!result.ok) {
273
+ expect(result.message).toContain('STIGMER_FOUNDRY_DEPLOYMENT_MAP entry "no-equals-sign"');
274
+ expect(result.message).toContain("canonical=deploymentName");
275
+ }
276
+ });
277
+ });
278
+
279
+ describe("parseBedrockModelMap", () => {
280
+ it("parses unset / blank to an empty map", () => {
281
+ for (const value of [undefined, "", " "]) {
282
+ const result = parseBedrockModelMap({ STIGMER_BEDROCK_MODEL_MAP: value });
283
+ expect(result.ok).toBe(true);
284
+ if (result.ok) expect(result.map.size).toBe(0);
285
+ }
286
+ });
287
+
288
+ it("parses comma-separated pairs, trimming whitespace and skipping empty entries", () => {
289
+ const result = parseBedrockModelMap({
290
+ STIGMER_BEDROCK_MODEL_MAP:
291
+ " claude-sonnet-4-6 = us.anthropic.claude-sonnet-4-6-v1:0 ,, claude-fable-5=global.anthropic.claude-fable-5-v1:0 ",
292
+ });
293
+ expect(result.ok).toBe(true);
294
+ if (result.ok) {
295
+ expect(result.map.get("claude-sonnet-4-6")).toBe("us.anthropic.claude-sonnet-4-6-v1:0");
296
+ expect(result.map.get("claude-fable-5")).toBe("global.anthropic.claude-fable-5-v1:0");
297
+ }
298
+ });
299
+
300
+ it.each([["no-equals-sign"], ["=missing-canonical"], ["missing-id="]])(
301
+ "rejects malformed entry %j naming the entry and the expected form",
302
+ (entry) => {
303
+ const result = parseBedrockModelMap({ STIGMER_BEDROCK_MODEL_MAP: entry });
304
+ expect(result.ok).toBe(false);
305
+ if (!result.ok) {
306
+ expect(result.message).toContain(entry);
307
+ expect(result.message).toContain("canonical=bedrockId");
308
+ }
309
+ },
310
+ );
311
+ });
312
+
313
+ describe("toBedrockModelId", () => {
314
+ // Layer 3 alone: the deterministic rule, verified against AWS's model
315
+ // catalog for the registry's dated ids (the model card lists
316
+ // anthropic.claude-sonnet-4-5-20250929-v1:0 for claude-sonnet-4-5).
317
+ it.each([
318
+ ["claude-sonnet-4-5-20250929", "anthropic.claude-sonnet-4-5-20250929-v1:0"],
319
+ ["claude-haiku-4-5-20251001", "anthropic.claude-haiku-4-5-20251001-v1:0"],
320
+ ["claude-opus-4-5-20251101", "anthropic.claude-opus-4-5-20251101-v1:0"],
321
+ ["claude-sonnet-4-6", "anthropic.claude-sonnet-4-6-v1:0"],
322
+ ["claude-fable-5", "anthropic.claude-fable-5-v1:0"],
323
+ ])("derives %s -> %s with no prefix or map", (canonical, bedrockId) => {
324
+ expect(toBedrockModelId(canonical, {})).toBe(bedrockId);
325
+ });
326
+
327
+ // Layer 2: the geography prefix is a deployment decision — never
328
+ // defaulted (a missing prefix yields the bare id; AWS's inference-profile
329
+ // rejection is classified with the remedy in model-error.ts).
330
+ it.each([
331
+ ["us", "us.anthropic.claude-sonnet-4-6-v1:0"],
332
+ ["eu", "eu.anthropic.claude-sonnet-4-6-v1:0"],
333
+ ["global", "global.anthropic.claude-sonnet-4-6-v1:0"],
334
+ // Lenient on a trailing dot — "us." and "us" both read as intent.
335
+ ["us.", "us.anthropic.claude-sonnet-4-6-v1:0"],
336
+ [" jp ", "jp.anthropic.claude-sonnet-4-6-v1:0"],
337
+ ])("applies inference prefix %j", (prefix, expected) => {
338
+ expect(
339
+ toBedrockModelId("claude-sonnet-4-6", { STIGMER_BEDROCK_INFERENCE_PREFIX: prefix }),
340
+ ).toBe(expected);
341
+ });
342
+
343
+ it("consults the operator map FIRST, bypassing both prefix and rule", () => {
344
+ expect(
345
+ toBedrockModelId("claude-sonnet-4-6", {
346
+ STIGMER_BEDROCK_MODEL_MAP: "claude-sonnet-4-6=arn:aws:bedrock:eu-central-1:123:inference-profile/custom",
347
+ STIGMER_BEDROCK_INFERENCE_PREFIX: "us",
348
+ }),
349
+ ).toBe("arn:aws:bedrock:eu-central-1:123:inference-profile/custom");
350
+ });
351
+
352
+ it("applies prefix and rule to models the map does not cover", () => {
353
+ expect(
354
+ toBedrockModelId("claude-fable-5", {
355
+ STIGMER_BEDROCK_MODEL_MAP: "claude-sonnet-4-6=us.anthropic.claude-sonnet-4-6-v1:0",
356
+ STIGMER_BEDROCK_INFERENCE_PREFIX: "us",
357
+ }),
358
+ ).toBe("us.anthropic.claude-fable-5-v1:0");
359
+ });
360
+
361
+ it("throws the catalog message on a malformed map (defense in depth)", () => {
362
+ expect(() => toBedrockModelId("claude-sonnet-4-6", { STIGMER_BEDROCK_MODEL_MAP: "broken" }))
363
+ .toThrow(/STIGMER_BEDROCK_MODEL_MAP entry "broken"/);
364
+ });
365
+ });
366
+
367
+ describe("toFoundryDeploymentName", () => {
368
+ // Layer 2 alone: the deterministic strip-date rule, verified against
369
+ // Anthropic's published Foundry model table — every registry id (both
370
+ // shapes the registry serves) maps onto a real DEFAULT deployment name.
371
+ // Note the inversion vs toVertexModelId: Vertex keeps the snapshot date
372
+ // (reseparated with `@`), Foundry drops it.
373
+ it.each([
374
+ // Pre-4.6 snapshot ids: the trailing date is stripped.
375
+ ["claude-sonnet-4-5-20250929", "claude-sonnet-4-5"],
376
+ ["claude-haiku-4-5-20251001", "claude-haiku-4-5"],
377
+ ["claude-opus-4-5-20251101", "claude-opus-4-5"],
378
+
379
+ // 4.6-generation and later: dateless ids ARE the default deployment
380
+ // names and pass through untouched.
381
+ ["claude-sonnet-4-6", "claude-sonnet-4-6"],
382
+ ["claude-opus-4-6", "claude-opus-4-6"],
383
+ ["claude-opus-4-7", "claude-opus-4-7"],
384
+ ["claude-opus-4-8", "claude-opus-4-8"],
385
+ ["claude-sonnet-5", "claude-sonnet-5"],
386
+ ["claude-fable-5", "claude-fable-5"],
387
+
388
+ // Translating twice is a no-op: a stripped id has no date to strip.
389
+ ["claude-sonnet-4-5", "claude-sonnet-4-5"],
390
+
391
+ // Version segments are not dates; nothing to strip.
392
+ ["claude-3-5-sonnet", "claude-3-5-sonnet"],
393
+ ["", ""],
394
+ ])("%s -> %s with no map", (canonical, deployment) => {
395
+ expect(toFoundryDeploymentName(canonical, {})).toBe(deployment);
396
+ });
397
+
398
+ it("consults the operator map FIRST, bypassing the strip-date rule", () => {
399
+ expect(
400
+ toFoundryDeploymentName("claude-sonnet-4-5-20250929", {
401
+ STIGMER_FOUNDRY_DEPLOYMENT_MAP: "claude-sonnet-4-5-20250929=my-sonnet-deployment",
402
+ }),
403
+ ).toBe("my-sonnet-deployment");
404
+ });
405
+
406
+ it("applies the rule to models the map does not cover", () => {
407
+ expect(
408
+ toFoundryDeploymentName("claude-haiku-4-5-20251001", {
409
+ STIGMER_FOUNDRY_DEPLOYMENT_MAP: "claude-sonnet-4-6=my-sonnet-deployment",
410
+ }),
411
+ ).toBe("claude-haiku-4-5");
412
+ });
413
+
414
+ it("throws the catalog message on a malformed map (defense in depth)", () => {
415
+ expect(() =>
416
+ toFoundryDeploymentName("claude-sonnet-4-6", { STIGMER_FOUNDRY_DEPLOYMENT_MAP: "broken" }),
417
+ ).toThrow(/STIGMER_FOUNDRY_DEPLOYMENT_MAP entry "broken"/);
418
+ });
419
+ });
420
+
421
+ describe("checkDirectCredentials", () => {
422
+ describe("anthropic", () => {
423
+ it("is satisfied by a non-blank ANTHROPIC_API_KEY", () => {
424
+ expect(checkDirectCredentials("anthropic", { ANTHROPIC_API_KEY: "sk-ant-x" })).toBeNull();
425
+ });
426
+
427
+ it.each([[undefined], [""], [" "]])(
428
+ "treats key %j as missing on the public backend",
429
+ (key) => {
430
+ const message = checkDirectCredentials("anthropic", { ANTHROPIC_API_KEY: key });
431
+ expect(message).toContain("ANTHROPIC_API_KEY");
432
+ expect(message).toContain("STIGMER_ANTHROPIC_BACKEND=vertex");
433
+ expect(message).toContain("docs.stigmer.ai");
434
+ },
435
+ );
436
+
437
+ it("is satisfied by the vertex backend with no key (ADC authenticates)", () => {
438
+ // The finding-4 pin: a correctly-configured Vertex deployment holds no
439
+ // Anthropic API key, and must not be reported as credential-less.
440
+ expect(
441
+ checkDirectCredentials("anthropic", { STIGMER_ANTHROPIC_BACKEND: "vertex" }),
442
+ ).toBeNull();
443
+ });
444
+
445
+ it("is satisfied by the bedrock backend with no key (AWS chain authenticates)", () => {
446
+ // Covered by the same non-public test that covered vertex — pinned
447
+ // separately so the bedrock adapter's credential story stays locked.
448
+ expect(
449
+ checkDirectCredentials("anthropic", { STIGMER_ANTHROPIC_BACKEND: "bedrock" }),
450
+ ).toBeNull();
451
+ });
452
+
453
+ it("is satisfied by the foundry backend with no key (Foundry key or Entra authenticates)", () => {
454
+ // Same non-public waiver: a Foundry deployment authenticates with
455
+ // ANTHROPIC_FOUNDRY_API_KEY or the Azure credential chain, never
456
+ // with an Anthropic API key.
457
+ expect(
458
+ checkDirectCredentials("anthropic", { STIGMER_ANTHROPIC_BACKEND: "foundry" }),
459
+ ).toBeNull();
460
+ });
461
+
462
+ it("names all shipped backends in the missing-key remedy", () => {
463
+ const message = checkDirectCredentials("anthropic", {});
464
+ expect(message).toContain("vertex, bedrock, or foundry");
465
+ });
466
+
467
+ it("defers an invalid backend value to construction (one message per condition)", () => {
468
+ // resolveAnthropicBackend owns the catalog message for invalid values;
469
+ // reporting "missing credentials" here would mask it.
470
+ expect(
471
+ checkDirectCredentials("anthropic", { STIGMER_ANTHROPIC_BACKEND: "verteks" }),
472
+ ).toBeNull();
473
+ });
474
+ });
475
+
476
+ describe("openai", () => {
477
+ it("is satisfied by a non-blank OPENAI_API_KEY", () => {
478
+ expect(checkDirectCredentials("openai", { OPENAI_API_KEY: "sk-x" })).toBeNull();
479
+ });
480
+
481
+ it.each([[undefined], [""], [" "]])(
482
+ "treats key %j as missing",
483
+ (key) => {
484
+ const message = checkDirectCredentials("openai", { OPENAI_API_KEY: key });
485
+ expect(message).toContain("OPENAI_API_KEY");
486
+ expect(message).toContain("docs.stigmer.ai");
487
+ },
488
+ );
489
+
490
+ it("offers no backend remedy while azure is unshipped", () => {
491
+ // STIGMER_OPENAI_BACKEND=azure would fail with "not implemented in
492
+ // this build" — an error message must not suggest a broken remedy.
493
+ const message = checkDirectCredentials("openai", {});
494
+ expect(message).not.toContain("STIGMER_OPENAI_BACKEND");
495
+ });
496
+
497
+ it("ignores the anthropic key and backend when checking openai", () => {
498
+ const message = checkDirectCredentials("openai", {
499
+ ANTHROPIC_API_KEY: "sk-ant-x",
500
+ STIGMER_ANTHROPIC_BACKEND: "vertex",
501
+ });
502
+ expect(message).toContain("OPENAI_API_KEY");
503
+ });
504
+ });
505
+ });
506
+
507
+ describe("preflightLlmBackends", () => {
508
+ it("is clean for an unconfigured deployment (today's default)", () => {
509
+ expect(preflightLlmBackends({})).toEqual({ error: null, warnings: [] });
510
+ });
511
+
512
+ it("is clean for a fully-configured vertex deployment", () => {
513
+ const result = preflightLlmBackends({
514
+ STIGMER_ANTHROPIC_BACKEND: "vertex",
515
+ CLOUD_ML_REGION: "asia-south1",
516
+ });
517
+ expect(result).toEqual({ error: null, warnings: [] });
518
+ });
519
+
520
+ it("fails vertex without a region, naming CLOUD_ML_REGION", () => {
521
+ const result = preflightLlmBackends({ STIGMER_ANTHROPIC_BACKEND: "vertex" });
522
+ expect(result.error).toContain("CLOUD_ML_REGION");
523
+ });
524
+
525
+ it("is clean for a fully-configured bedrock deployment", () => {
526
+ const result = preflightLlmBackends({
527
+ STIGMER_ANTHROPIC_BACKEND: "bedrock",
528
+ AWS_REGION: "ap-south-1",
529
+ });
530
+ expect(result).toEqual({ error: null, warnings: [] });
531
+ });
532
+
533
+ it("fails bedrock without a region, naming AWS_REGION", () => {
534
+ const result = preflightLlmBackends({ STIGMER_ANTHROPIC_BACKEND: "bedrock" });
535
+ expect(result.error).toContain("AWS_REGION");
536
+ });
537
+
538
+ it("fails bedrock with a malformed model map at boot", () => {
539
+ const result = preflightLlmBackends({
540
+ STIGMER_ANTHROPIC_BACKEND: "bedrock",
541
+ AWS_REGION: "ap-south-1",
542
+ STIGMER_BEDROCK_MODEL_MAP: "not-a-pair",
543
+ });
544
+ expect(result.error).toContain("STIGMER_BEDROCK_MODEL_MAP");
545
+ });
546
+
547
+ it("is clean for a fully-configured foundry deployment", () => {
548
+ const result = preflightLlmBackends({
549
+ STIGMER_ANTHROPIC_BACKEND: "foundry",
550
+ ANTHROPIC_FOUNDRY_RESOURCE: "my-foundry-resource",
551
+ });
552
+ expect(result).toEqual({ error: null, warnings: [] });
553
+ });
554
+
555
+ it("fails foundry without an endpoint, naming both endpoint vars", () => {
556
+ const result = preflightLlmBackends({ STIGMER_ANTHROPIC_BACKEND: "foundry" });
557
+ expect(result.error).toContain("ANTHROPIC_FOUNDRY_RESOURCE");
558
+ expect(result.error).toContain("ANTHROPIC_FOUNDRY_BASE_URL");
559
+ });
560
+
561
+ it("fails foundry with both endpoint vars set (mutually exclusive)", () => {
562
+ const result = preflightLlmBackends({
563
+ STIGMER_ANTHROPIC_BACKEND: "foundry",
564
+ ANTHROPIC_FOUNDRY_RESOURCE: "my-foundry-resource",
565
+ ANTHROPIC_FOUNDRY_BASE_URL: "https://other.services.ai.azure.com/anthropic/",
566
+ });
567
+ expect(result.error).toContain("mutually exclusive");
568
+ });
569
+
570
+ it("fails foundry with a malformed deployment map at boot", () => {
571
+ const result = preflightLlmBackends({
572
+ STIGMER_ANTHROPIC_BACKEND: "foundry",
573
+ ANTHROPIC_FOUNDRY_RESOURCE: "my-foundry-resource",
574
+ STIGMER_FOUNDRY_DEPLOYMENT_MAP: "not-a-pair",
575
+ });
576
+ expect(result.error).toContain("STIGMER_FOUNDRY_DEPLOYMENT_MAP");
577
+ });
578
+
579
+ it("fails on invalid values for either var, reporting all problems at once", () => {
580
+ const result = preflightLlmBackends({
581
+ STIGMER_ANTHROPIC_BACKEND: "verteks",
582
+ STIGMER_OPENAI_BACKEND: "azure",
583
+ });
584
+ expect(result.error).toContain("STIGMER_ANTHROPIC_BACKEND");
585
+ expect(result.error).toContain("STIGMER_OPENAI_BACKEND");
586
+ });
587
+
588
+ it("downgrades backend vars to a warning when the proxy is set (proxy owns routing)", () => {
589
+ const result = preflightLlmBackends({
590
+ STIGMER_PROXY_ENDPOINT: "https://api.stigmer.ai",
591
+ STIGMER_ANTHROPIC_BACKEND: "vertex",
592
+ });
593
+ expect(result.error).toBeNull();
594
+ expect(result.warnings).toHaveLength(1);
595
+ expect(result.warnings[0]).toContain("STIGMER_ANTHROPIC_BACKEND=vertex is ignored");
596
+ });
597
+
598
+ it("does not warn under the proxy for unset or explicit-public backend vars", () => {
599
+ const result = preflightLlmBackends({
600
+ STIGMER_PROXY_ENDPOINT: "https://api.stigmer.ai",
601
+ STIGMER_OPENAI_BACKEND: "public",
602
+ });
603
+ expect(result).toEqual({ error: null, warnings: [] });
604
+ });
605
+
606
+ it("does not fail an invalid-but-inert value under the proxy, but still surfaces it", () => {
607
+ // A proxied fleet must not crash-loop over a var the proxy makes inert;
608
+ // non-silence is preserved through the warning.
609
+ const result = preflightLlmBackends({
610
+ STIGMER_PROXY_ENDPOINT: "https://api.stigmer.ai",
611
+ STIGMER_ANTHROPIC_BACKEND: "verteks",
612
+ });
613
+ expect(result.error).toBeNull();
614
+ expect(result.warnings[0]).toContain("STIGMER_ANTHROPIC_BACKEND=verteks is ignored");
615
+ });
616
+ });