@voxgig/sdkgen 4.5.3 → 4.7.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 (248) hide show
  1. package/bin/voxgig-sdkgen +1 -1
  2. package/dist/action/check.js +15 -0
  3. package/dist/action/check.js.map +1 -1
  4. package/dist/cmp/AgentGuide.js +1 -1
  5. package/dist/cmp/AgentGuide.js.map +1 -1
  6. package/dist/cmp/AgentGuideContent.d.ts +1 -1
  7. package/dist/cmp/AgentGuideContent.js +8 -2
  8. package/dist/cmp/AgentGuideContent.js.map +1 -1
  9. package/dist/cmp/Feature.js +22 -0
  10. package/dist/cmp/Feature.js.map +1 -1
  11. package/dist/cmp/FeatureDocs.d.ts +1 -1
  12. package/dist/cmp/FeatureDocs.js +6 -1
  13. package/dist/cmp/FeatureDocs.js.map +1 -1
  14. package/dist/cmp/ReadmeExplanation.js +3 -1
  15. package/dist/cmp/ReadmeExplanation.js.map +1 -1
  16. package/dist/cmp/ReadmeFeatures.js +1 -1
  17. package/dist/cmp/ReadmeFeatures.js.map +1 -1
  18. package/dist/cmp/ReadmeRefFeatures.js +1 -1
  19. package/dist/cmp/ReadmeRefFeatures.js.map +1 -1
  20. package/dist/cmp/TestControl.d.ts +4 -0
  21. package/dist/cmp/TestControl.js +51 -0
  22. package/dist/cmp/TestControl.js.map +1 -0
  23. package/dist/helpers/applicability.d.ts +6 -0
  24. package/dist/helpers/applicability.js +103 -0
  25. package/dist/helpers/applicability.js.map +1 -0
  26. package/dist/helpers/collectDeps.js +4 -2
  27. package/dist/helpers/collectDeps.js.map +1 -1
  28. package/dist/helpers/featureSource.d.ts +3 -1
  29. package/dist/helpers/featureSource.js +105 -0
  30. package/dist/helpers/featureSource.js.map +1 -1
  31. package/dist/helpers/serverVars.d.ts +2 -1
  32. package/dist/helpers/serverVars.js +17 -0
  33. package/dist/helpers/serverVars.js.map +1 -1
  34. package/dist/sdkgen.d.ts +5 -3
  35. package/dist/sdkgen.js +16 -4
  36. package/dist/sdkgen.js.map +1 -1
  37. package/dist/tsconfig.tsbuildinfo +1 -1
  38. package/dist/utility.d.ts +2 -1
  39. package/dist/utility.js +59 -1
  40. package/dist/utility.js.map +1 -1
  41. package/model/sdkgen.aon +97 -0
  42. package/package.json +1 -1
  43. package/project/.sdk/model/feature/feature-index.aon +1 -0
  44. package/project/.sdk/model/feature/secrets.aon +200 -0
  45. package/project/.sdk/model/target/ts.aon +8 -1
  46. package/project/.sdk/src/cmp/c/Config_c.ts +5 -1
  47. package/project/.sdk/src/cmp/cpp/Config_cpp.ts +5 -1
  48. package/project/.sdk/src/cmp/cpp/Main_cpp.ts +2 -1
  49. package/project/.sdk/src/cmp/cpp/Test_cpp.ts +5 -1
  50. package/project/.sdk/src/cmp/csharp/Config_csharp.ts +5 -1
  51. package/project/.sdk/src/cmp/csharp/Main_csharp.ts +2 -1
  52. package/project/.sdk/src/cmp/csharp/TestDirect_csharp.ts +30 -6
  53. package/project/.sdk/src/cmp/csharp/TestEntity_csharp.ts +29 -5
  54. package/project/.sdk/src/cmp/csharp/Test_csharp.ts +5 -1
  55. package/project/.sdk/src/cmp/dart/Config_dart.ts +5 -1
  56. package/project/.sdk/src/cmp/dart/Main_dart.ts +2 -1
  57. package/project/.sdk/src/cmp/dart/TestDirect_dart.ts +23 -6
  58. package/project/.sdk/src/cmp/dart/TestEntity_dart.ts +28 -7
  59. package/project/.sdk/src/cmp/dart/Test_dart.ts +5 -1
  60. package/project/.sdk/src/cmp/elixir/Config_elixir.ts +5 -1
  61. package/project/.sdk/src/cmp/elixir/Main_elixir.ts +5 -1
  62. package/project/.sdk/src/cmp/go/Config_go.ts +5 -1
  63. package/project/.sdk/src/cmp/go/Main_go.ts +8 -3
  64. package/project/.sdk/src/cmp/go/ReadmeQuick_go.ts +16 -3
  65. package/project/.sdk/src/cmp/go/ReadmeTopQuick_go.ts +16 -3
  66. package/project/.sdk/src/cmp/go/TestDirect_go.ts +28 -4
  67. package/project/.sdk/src/cmp/go/TestEntity_go.ts +43 -10
  68. package/project/.sdk/src/cmp/go/Test_go.ts +5 -1
  69. package/project/.sdk/src/cmp/go/fragment/Main.fragment.go +5 -5
  70. package/project/.sdk/src/cmp/java/Config_java.ts +22 -1
  71. package/project/.sdk/src/cmp/java/Main_java.ts +7 -2
  72. package/project/.sdk/src/cmp/java/ReadmeQuick_java.ts +17 -3
  73. package/project/.sdk/src/cmp/java/ReadmeTopQuick_java.ts +17 -3
  74. package/project/.sdk/src/cmp/java/TestDirect_java.ts +23 -4
  75. package/project/.sdk/src/cmp/java/TestEntity_java.ts +29 -4
  76. package/project/.sdk/src/cmp/java/Test_java.ts +5 -1
  77. package/project/.sdk/src/cmp/js/Config_js.ts +5 -1
  78. package/project/.sdk/src/cmp/js/Main_js.ts +7 -2
  79. package/project/.sdk/src/cmp/js/Package_js.ts +4 -1
  80. package/project/.sdk/src/cmp/js/TestDirect_js.ts +30 -5
  81. package/project/.sdk/src/cmp/js/TestEntity_js.ts +35 -5
  82. package/project/.sdk/src/cmp/js/Test_js.ts +5 -1
  83. package/project/.sdk/src/cmp/js/fragment/Direct.test.fragment.js +7 -1
  84. package/project/.sdk/src/cmp/js/fragment/Entity.test.fragment.js +7 -1
  85. package/project/.sdk/src/cmp/kotlin/Config_kotlin.ts +5 -1
  86. package/project/.sdk/src/cmp/kotlin/Main_kotlin.ts +2 -1
  87. package/project/.sdk/src/cmp/kotlin/Test_kotlin.ts +5 -1
  88. package/project/.sdk/src/cmp/lean/Config_lean.ts +5 -1
  89. package/project/.sdk/src/cmp/lua/Config_lua.ts +5 -1
  90. package/project/.sdk/src/cmp/lua/Main_lua.ts +7 -2
  91. package/project/.sdk/src/cmp/lua/Package_lua.ts +4 -1
  92. package/project/.sdk/src/cmp/lua/TestDirect_lua.ts +28 -4
  93. package/project/.sdk/src/cmp/lua/TestEntity_lua.ts +24 -4
  94. package/project/.sdk/src/cmp/lua/Test_lua.ts +5 -1
  95. package/project/.sdk/src/cmp/ocaml/Config_ocaml.ts +5 -1
  96. package/project/.sdk/src/cmp/ocaml/Gitignore_ocaml.ts +1 -0
  97. package/project/.sdk/src/cmp/perl/Main_perl.ts +7 -2
  98. package/project/.sdk/src/cmp/perl/TestDirect_perl.ts +25 -4
  99. package/project/.sdk/src/cmp/perl/TestEntity_perl.ts +24 -4
  100. package/project/.sdk/src/cmp/perl/Test_perl.ts +5 -1
  101. package/project/.sdk/src/cmp/perl/utility_perl.ts +10 -0
  102. package/project/.sdk/src/cmp/php/Config_php.ts +5 -1
  103. package/project/.sdk/src/cmp/php/Main_php.ts +7 -2
  104. package/project/.sdk/src/cmp/php/TestDirect_php.ts +24 -5
  105. package/project/.sdk/src/cmp/php/TestEntity_php.ts +28 -4
  106. package/project/.sdk/src/cmp/php/Test_php.ts +5 -1
  107. package/project/.sdk/src/cmp/py/Config_py.ts +5 -1
  108. package/project/.sdk/src/cmp/py/Main_py.ts +7 -2
  109. package/project/.sdk/src/cmp/py/ReadmeQuick_py.ts +16 -3
  110. package/project/.sdk/src/cmp/py/ReadmeTopQuick_py.ts +16 -3
  111. package/project/.sdk/src/cmp/py/TestDirect_py.ts +24 -5
  112. package/project/.sdk/src/cmp/py/TestEntity_py.ts +23 -3
  113. package/project/.sdk/src/cmp/py/Test_py.ts +5 -1
  114. package/project/.sdk/src/cmp/rb/Config_rb.ts +5 -1
  115. package/project/.sdk/src/cmp/rb/Main_rb.ts +7 -2
  116. package/project/.sdk/src/cmp/rb/TestDirect_rb.ts +24 -5
  117. package/project/.sdk/src/cmp/rb/TestEntity_rb.ts +24 -4
  118. package/project/.sdk/src/cmp/rb/Test_rb.ts +5 -1
  119. package/project/.sdk/src/cmp/rb/utility_rb.ts +3 -1
  120. package/project/.sdk/src/cmp/rust/Config_rust.ts +5 -1
  121. package/project/.sdk/src/cmp/rust/Main_rust.ts +7 -2
  122. package/project/.sdk/src/cmp/rust/TestDirect_rust.ts +31 -4
  123. package/project/.sdk/src/cmp/rust/TestEntity_rust.ts +37 -4
  124. package/project/.sdk/src/cmp/rust/Test_rust.ts +5 -1
  125. package/project/.sdk/src/cmp/scala/Config_scala.ts +5 -1
  126. package/project/.sdk/src/cmp/scala/Main_scala.ts +7 -2
  127. package/project/.sdk/src/cmp/scala/Test_scala.ts +5 -1
  128. package/project/.sdk/src/cmp/swift/Config_swift.ts +5 -1
  129. package/project/.sdk/src/cmp/ts/Config_ts.ts +55 -9
  130. package/project/.sdk/src/cmp/ts/Main_ts.ts +67 -3
  131. package/project/.sdk/src/cmp/ts/Package_ts.ts +4 -1
  132. package/project/.sdk/src/cmp/ts/ReadmeModel_ts.ts +15 -3
  133. package/project/.sdk/src/cmp/ts/ReadmeQuick_ts.ts +18 -5
  134. package/project/.sdk/src/cmp/ts/ReadmeTopQuick_ts.ts +19 -5
  135. package/project/.sdk/src/cmp/ts/TestDirect_ts.ts +75 -8
  136. package/project/.sdk/src/cmp/ts/TestEntity_ts.ts +36 -5
  137. package/project/.sdk/src/cmp/ts/Test_ts.ts +5 -1
  138. package/project/.sdk/src/cmp/ts/fragment/Direct.test.fragment.ts +9 -2
  139. package/project/.sdk/src/cmp/ts/fragment/Entity.test.fragment.ts +9 -2
  140. package/project/.sdk/src/cmp/ts/fragment/Main.fragment.ts +7 -0
  141. package/project/.sdk/src/cmp/zig/Config_zig.ts +5 -1
  142. package/project/.sdk/tm/c/utility/make_options.c +22 -0
  143. package/project/.sdk/tm/clojure/src/sdk/core.clj +18 -1
  144. package/project/.sdk/tm/cpp/utility/pipeline.hpp +23 -0
  145. package/project/.sdk/tm/csharp/test/CustomUtilityTest.cs +15 -0
  146. package/project/.sdk/tm/csharp/test/Runner.cs +45 -0
  147. package/project/.sdk/tm/csharp/utility/MakeOptions.cs +23 -0
  148. package/project/.sdk/tm/dart/lib/utility/MakeOptionsUtility.dart +22 -0
  149. package/project/.sdk/tm/dart/test/utility.dart +39 -0
  150. package/project/.sdk/tm/elixir/lib/projectname/utility.ex +19 -0
  151. package/project/.sdk/tm/go/core/context.go +1 -1
  152. package/project/.sdk/tm/go/feature/cost_feature.go +1 -1
  153. package/project/.sdk/tm/go/feature/paging_feature.go +5 -5
  154. package/project/.sdk/tm/go/feature/test_feature.go +12 -5
  155. package/project/.sdk/tm/go/test/custom_utility_test.go +15 -0
  156. package/project/.sdk/tm/go/test/feature_corpus_test.go +7 -0
  157. package/project/.sdk/tm/go/test/runner_test.go +46 -0
  158. package/project/.sdk/tm/go/test/struct_runner_test.go +7 -7
  159. package/project/.sdk/tm/go/test/struct_utility_test.go +63 -27
  160. package/project/.sdk/tm/go/utility/fetcher.go +2 -2
  161. package/project/.sdk/tm/go/utility/make_options.go +24 -5
  162. package/project/.sdk/tm/go/utility/make_point.go +1 -1
  163. package/project/.sdk/tm/go/utility/make_spec.go +17 -1
  164. package/project/.sdk/tm/go/utility/prepare_auth.go +1 -1
  165. package/project/.sdk/tm/go/utility/struct/voxgigstruct.go +370 -307
  166. package/project/.sdk/tm/go/utility/transform_request.go +29 -1
  167. package/project/.sdk/tm/go/utility/transform_response.go +20 -1
  168. package/project/.sdk/tm/java/test/CustomUtilityTest.java +10 -0
  169. package/project/.sdk/tm/java/test/FeatureCorpusTest.java +7 -0
  170. package/project/.sdk/tm/java/test/PrimaryUtilityTest.java +6 -0
  171. package/project/.sdk/tm/java/test/RunnerSupport.java +43 -0
  172. package/project/.sdk/tm/java/utility/MakeOptions.java +84 -1
  173. package/project/.sdk/tm/js/src/utility/MakeOptionsUtility.js +24 -0
  174. package/project/.sdk/tm/js/src/utility/StructUtility.js +2076 -1837
  175. package/project/.sdk/tm/js/test/feature/Corpus.test.js +9 -0
  176. package/project/.sdk/tm/js/test/sdk-test-control.json +19 -0
  177. package/project/.sdk/tm/js/test/utility.js +91 -1
  178. package/project/.sdk/tm/kotlin/utility/MakeOptions.kt +21 -0
  179. package/project/.sdk/tm/lean/src/SdkUtility.lean +12 -1
  180. package/project/.sdk/tm/lua/test/runner.lua +40 -0
  181. package/project/.sdk/tm/ocaml/sdk_runtime.ml +11 -0
  182. package/project/.sdk/tm/ocaml/test/harness.ml +14 -4
  183. package/project/.sdk/tm/perl/t/runner.pm +30 -0
  184. package/project/.sdk/tm/perl/utility/make_options.pm +18 -0
  185. package/project/.sdk/tm/php/test/FeatureCorpusTest.php +10 -1
  186. package/project/.sdk/tm/php/test/Runner.php +36 -0
  187. package/project/.sdk/tm/php/utility/MakeOptions.php +24 -0
  188. package/project/.sdk/tm/py/pkg/utility/make_options.py +20 -0
  189. package/project/.sdk/tm/py/test/runner.py +32 -0
  190. package/project/.sdk/tm/py/test/test_feature_corpus.py +10 -1
  191. package/project/.sdk/tm/rb/test/feature_corpus_test.rb +9 -1
  192. package/project/.sdk/tm/rb/test/runner.rb +25 -0
  193. package/project/.sdk/tm/rb/utility/make_options.rb +17 -0
  194. package/project/.sdk/tm/rust/tests/common/mod.rs +32 -0
  195. package/project/.sdk/tm/rust/utility/make_options.rs +26 -1
  196. package/project/.sdk/tm/scala/utility/Make.scala +17 -0
  197. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/MakeOptions.swift +21 -0
  198. package/project/.sdk/tm/ts/src/feature/secrets/SecretsFeature.ts +419 -0
  199. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/Sekreto.ts +387 -0
  200. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/Sigv4.ts +175 -0
  201. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/index.ts +51 -0
  202. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/Registry.ts +90 -0
  203. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/addr.ts +48 -0
  204. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/aws.ts +219 -0
  205. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/azuresecrets.ts +144 -0
  206. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/boru.ts +113 -0
  207. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/doppler.ts +77 -0
  208. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/dotenv.ts +51 -0
  209. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/env.ts +31 -0
  210. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/file.ts +54 -0
  211. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/gcpsecrets.ts +118 -0
  212. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/hashicorp.ts +159 -0
  213. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/http.ts +66 -0
  214. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/infisical.ts +107 -0
  215. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/memory.ts +33 -0
  216. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/onepassword.ts +120 -0
  217. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/support.ts +176 -0
  218. package/project/.sdk/tm/ts/src/utility/MakeOptionsUtility.ts +18 -0
  219. package/project/.sdk/tm/ts/src/utility/StructUtility.ts +1321 -710
  220. package/project/.sdk/tm/ts/test/feature/Corpus.test.ts +9 -0
  221. package/project/.sdk/tm/ts/test/feature/secrets/Secrets.test.ts +764 -0
  222. package/project/.sdk/tm/ts/test/omni.test.ts +84 -0
  223. package/project/.sdk/tm/ts/test/omni.ts +86 -0
  224. package/project/.sdk/tm/ts/test/utility/PrimaryUtility.test.ts +6 -2
  225. package/project/.sdk/tm/ts/test/utility/StructUtility.test.ts +2 -2
  226. package/project/.sdk/tm/ts/test/utility.ts +122 -0
  227. package/project/.sdk/tm/ts/test/vendor/omni/Runner.ts +694 -0
  228. package/project/.sdk/tm/ts/test/vendor/omni/Util.ts +244 -0
  229. package/project/.sdk/tm/ts/test/vendor/omni/compat.ts +188 -0
  230. package/project/.sdk/tm/ts/test/vendor/omni/index.ts +38 -0
  231. package/project/.sdk/tm/zig/core/utility.zig +24 -1
  232. package/project/sdkgen-package.json +2 -1
  233. package/src/action/check.ts +18 -0
  234. package/src/cmp/AgentGuide.ts +1 -1
  235. package/src/cmp/AgentGuideContent.ts +9 -2
  236. package/src/cmp/Feature.ts +24 -0
  237. package/src/cmp/FeatureDocs.ts +7 -1
  238. package/src/cmp/ReadmeExplanation.ts +4 -1
  239. package/src/cmp/ReadmeFeatures.ts +1 -1
  240. package/src/cmp/ReadmeRefFeatures.ts +1 -1
  241. package/src/cmp/TestControl.ts +64 -0
  242. package/src/helpers/applicability.ts +123 -0
  243. package/src/helpers/collectDeps.ts +5 -1
  244. package/src/helpers/featureSource.ts +130 -0
  245. package/src/helpers/serverVars.ts +19 -0
  246. package/src/sdkgen.ts +16 -1
  247. package/src/utility.ts +71 -1
  248. package/project/.sdk/tm/ts/test/runner.ts +0 -425
@@ -84,7 +84,16 @@ function scriptedFetcher(res) {
84
84
 
85
85
 
86
86
  function makeClient(kase) {
87
+ // `test: { active: true }` is the OPTION, not the `test` FEATURE.
88
+ //
89
+ // It says "this client is not live", which is what makes a REQUIRED
90
+ // OpenAPI server variable resolve to a deterministic `test-<name>`
91
+ // rather than refuse to construct (see makeOptions). It installs no
92
+ // transport, so the scripted fetcher below still stands — the FEATURE
93
+ // is transport: 'base' and would shadow it, which is why this cannot
94
+ // just turn the feature on.
87
95
  return new SDK({
96
+ test: { active: true },
88
97
  feature: kase.feature,
89
98
  utility: { fetcher: scriptedFetcher(kase.res || [{ status: 200, body: {} }]) },
90
99
  })
@@ -0,0 +1,19 @@
1
+ {
2
+ "version": 1,
3
+ "_doc": "Per-SDK test control. Lists tests/operations to skip in unit and live modes; tunes per-test pacing. Edit by hand. Loaded by js/test/utility.js.",
4
+ "test": {
5
+ "skip": {
6
+ "live": {
7
+ "direct": [],
8
+ "entityOp": []
9
+ },
10
+ "unit": {
11
+ "direct": [],
12
+ "entityOp": []
13
+ }
14
+ },
15
+ "live": {
16
+ "delayMs": 500
17
+ }
18
+ }
19
+ }
@@ -5,6 +5,10 @@
5
5
  * for creating test data, transformations, validations, and environment overrides.
6
6
  */
7
7
 
8
+ const Fs = require('node:fs')
9
+ const Path = require('node:path')
10
+
11
+
8
12
  // Creates a new step data structure within the data model
9
13
  function makeStepData(dm, stepname) {
10
14
  dm.s[stepname] = {
@@ -62,11 +66,97 @@ function envOverride(m) {
62
66
  }
63
67
 
64
68
 
69
+ // Loads sdk-test-control.json (cached). Returns an empty-skip object if
70
+ // the file is missing or unparsable so tests never crash on a bad config.
71
+ let _testControlCache = null
72
+
73
+ function loadTestControl() {
74
+ if (_testControlCache) return _testControlCache
75
+ const ctrlPath = Path.resolve(__dirname, '../test/sdk-test-control.json')
76
+ try {
77
+ _testControlCache = JSON.parse(Fs.readFileSync(ctrlPath, 'utf8'))
78
+ }
79
+ catch {
80
+ _testControlCache = {
81
+ version: 1,
82
+ test: { skip: { live: { direct: [], entityOp: [] }, unit: { direct: [], entityOp: [] } } }
83
+ }
84
+ }
85
+ return _testControlCache
86
+ }
87
+
88
+
89
+ // Extra SDK options every LIVE client is constructed with, read from
90
+ // sdk-test-control.json `test.client.options`.
91
+ //
92
+ // The generated live client knows two things: the base URL (from the spec)
93
+ // and the credential (from the environment). Everything else about how a
94
+ // particular API wants to be talked to - which features to switch on, and
95
+ // with what settings - is a property of THAT API, known to the project and
96
+ // to nothing in the toolchain. So it lives in the file the project owns and
97
+ // describes, where it can be read and reviewed. Secrets still come from the
98
+ // environment (the providers this block names read them).
99
+ //
100
+ // Merged UNDER the generated fields, so the suite's own base/apikey/server
101
+ // values win - this adds to the live client, it does not redirect it.
102
+ //
103
+ // That contract is enforced HERE rather than left to each merge site: the
104
+ // generated object only names a field when the model calls for one, so a
105
+ // `base` in this block would face no competing value and would silently
106
+ // redirect the whole suite - credential included - to another host. The
107
+ // reserved fields are stripped once, where the block is read, so every
108
+ // caller gets the same guarantee whether or not it happens to emit them.
109
+ const LIVE_RESERVED = ['base', 'prefix', 'suffix', 'server', 'apikey', 'secret']
110
+
111
+ function liveClientOptions() {
112
+ const ctrl = loadTestControl()
113
+ const opts = ctrl && ctrl.test && ctrl.test.client && ctrl.test.client.options
114
+
115
+ if (null == opts || 'object' !== typeof opts) {
116
+ return {}
117
+ }
118
+
119
+ const out = {}
120
+ for (const key of Object.keys(opts)) {
121
+ if (!LIVE_RESERVED.includes(key)) {
122
+ out[key] = opts[key]
123
+ }
124
+ }
125
+
126
+ return out
127
+ }
128
+
129
+
130
+ // Per-test live pacing delay (ms). Read from sdk-test-control.json
131
+ // `test.live.delayMs`; defaults to 500ms if absent or invalid.
132
+ function liveDelayMs() {
133
+ const ctrl = loadTestControl()
134
+ const v = ctrl && ctrl.test && ctrl.test.live && ctrl.test.live.delayMs
135
+ return ('number' === typeof v && v >= 0) ? v : 500
136
+ }
137
+
138
+
139
+
140
+ // afterEach hook helper for live pacing. Generated tests register this via
141
+ // `afterEach(liveDelay(<envVar>))`; it sleeps `liveDelayMs()` only when the
142
+ // SDK's *_TEST_LIVE env var is set.
143
+ function liveDelay(liveEnvVar) {
144
+ return async () => {
145
+ if ('TRUE' === process.env[liveEnvVar]) {
146
+ await new Promise(r => setTimeout(r, liveDelayMs()))
147
+ }
148
+ }
149
+ }
150
+
65
151
  module.exports = {
66
152
  makeStepData,
67
153
  makeMatch,
68
154
  makeReqdata,
69
155
  makeValid,
70
156
  makeCtrl,
71
- envOverride
157
+ envOverride,
158
+ loadTestControl,
159
+ liveClientOptions,
160
+ liveDelayMs,
161
+ liveDelay
72
162
  }
@@ -28,8 +28,24 @@ fun makeOptions(ctx: Context): MutableMap<String, Any?> {
28
28
  }
29
29
  }
30
30
 
31
+ // `auth: null` is the documented way to disable auth outright, and
32
+ // PrepareAuth honours it before it ever reads the apikey. It cannot survive
33
+ // validate: depending on the struct port a stored null is either REPLACED
34
+ // by the optspec default - transmitting the credential the caller withheld
35
+ // - or REJECTED outright. Withhold the key for validate, then put the null
36
+ // back. Same fix as ts/js/go makeOptions.
37
+ //
38
+ // Suppliedness cannot be recovered after validate, hence here, and it must
39
+ // tell an ABSENT auth from a present null: containsKey rather than a null
40
+ // check on the value, which cannot distinguish them.
41
+ val authSuppressed = options.containsKey("auth") && null == options["auth"]
42
+
31
43
  var opts = Struct.clone(options) as MutableMap<String, Any?>
32
44
 
45
+ if (authSuppressed) {
46
+ opts.remove("auth")
47
+ }
48
+
33
49
  // Feature add-order. options.feature may be given as an ordered LIST of
34
50
  // { name, active, ...opts } entries (the list position IS the order in which
35
51
  // features are added), or as a { name: {opts} } map. Normalize a list to a
@@ -107,6 +123,11 @@ fun makeOptions(ctx: Context): MutableMap<String, Any?> {
107
123
  val validated = Struct.validate(merged, optspec, vopts)
108
124
  opts = validated as MutableMap<String, Any?>
109
125
 
126
+ // Restore the suppression the optspec default would otherwise erase.
127
+ if (authSuppressed) {
128
+ opts["auth"] = null
129
+ }
130
+
110
131
  // Restore system.fetch.
111
132
  if (sysFetch != null) {
112
133
  val sys = Helpers.toMapAny(opts["system"])
@@ -260,8 +260,19 @@ def prepareAuth (ctx : Value) : SIO (Value × Option Value) := do
260
260
  | .map _ => do
261
261
  let headers ← gpMap specV "headers"
262
262
  let options ← gp ctx "options"
263
+ -- `auth: null` is the documented way to suppress auth outright: NO
264
+ -- authorization header, whatever the apikey says. Without this the
265
+ -- withheld credential goes on the wire.
266
+ --
267
+ -- The ports that run options through validate against an optspec carrying
268
+ -- an `auth` default can use absence as the signal (validate guarantees the
269
+ -- key is present otherwise). lean's makeOptions has no optspec, so `auth`
270
+ -- is ABSENT in the ordinary case and absence must stay the ordinary case.
271
+ -- getpropRaw is the only reader that tells a STORED null from an absent
272
+ -- key - gp collapses both to .noval - so the stored null alone suppresses.
273
+ let authRaw ← getpropRaw options "auth"
263
274
  let apikey ← gpS options "apikey"
264
- if apikey == "" then do
275
+ if authRaw == .null || apikey == "" then do
265
276
  dp headers "authorization"
266
277
  pure (specV, none)
267
278
  else do
@@ -146,6 +146,46 @@ function runner.is_control_skipped(kind, name, mode)
146
146
  end
147
147
 
148
148
 
149
+ -- Extra SDK options every LIVE client is constructed with, read from
150
+ -- sdk-test-control.json `test.client.options`.
151
+ --
152
+ -- The generated live client knows two things: the base URL (from the spec)
153
+ -- and the credential (from the environment). Everything else about how a
154
+ -- particular API wants to be talked to - which features to switch on, and
155
+ -- with what settings - is a property of THAT API, known to the project and
156
+ -- to nothing in the toolchain.
157
+ --
158
+ -- Merged UNDER the generated fields, so the suite's own base/apikey/server
159
+ -- values win: this ADDS to the live client, it does not redirect it.
160
+ --
161
+ -- Reserved fields are stripped HERE rather than at each merge site: the
162
+ -- generated table only names a field when the model calls for one, so a
163
+ -- "base" in this block would face no competing value and would silently
164
+ -- redirect the whole suite - credential included - to another host.
165
+ local LIVE_RESERVED = {
166
+ base = true, prefix = true, suffix = true,
167
+ server = true, apikey = true, secret = true,
168
+ }
169
+
170
+ function runner.live_client_options()
171
+ local ctrl = runner.load_test_control()
172
+ local test = ctrl.test
173
+ if type(test) ~= "table" then return {} end
174
+ local client = test.client
175
+ if type(client) ~= "table" then return {} end
176
+ local opts = client.options
177
+ if type(opts) ~= "table" then return {} end
178
+
179
+ local out = {}
180
+ for k, v in pairs(opts) do
181
+ if not LIVE_RESERVED[k] then
182
+ out[k] = v
183
+ end
184
+ end
185
+ return out
186
+ end
187
+
188
+
149
189
  -- Per-test live pacing delay (ms); default 500.
150
190
  function runner.live_delay_ms()
151
191
  local ctrl = runner.load_test_control()
@@ -968,7 +968,16 @@ let make_options_util (ctx : ctx) : value =
968
968
  | Some u -> List.iter (fun k -> setp u.u_custom k (getp custom_utils k)) (keysof custom_utils)
969
969
  | None -> ())
970
970
  | _ -> ());
971
+ (* `auth: null` is the documented way to suppress auth outright. validate()
972
+ treats a STORED null as "no value" (getprop returns the alt), so the
973
+ optspec `auth` default would fire and turn the suppression into "use the
974
+ default auth" - transmitting a credential the caller withheld. getprop_raw
975
+ is the only reader that tells a stored Null from an absent key. Withhold
976
+ the key across merge/validate, then restore the null below. *)
977
+ let auth_suppressed =
978
+ (match getprop_raw options "auth" with Null -> true | _ -> false) in
971
979
  let opts = match clone options with Map _ as m -> m | _ -> empty_map () in
980
+ let () = if auth_suppressed then ignore (delprop opts (Str "auth")) in
972
981
  (* Feature add-order. options.feature may be given as an ordered LIST of
973
982
  {name; active; ...opts} entries (list position = add order) or a
974
983
  {name => {opts}} map. Normalize a list to a map (so merge/validate/init
@@ -999,6 +1008,8 @@ let make_options_util (ctx : ctx) : value =
999
1008
  let merged = merge (ja [empty_map (); cfgopts; opts]) in
1000
1009
  let validated = validate merged optspec in
1001
1010
  let opts = match validated with Map _ as m -> m | _ -> empty_map () in
1011
+ (* Restore the suppression the optspec default would otherwise erase. *)
1012
+ let () = if auth_suppressed then setp opts "auth" Null in
1002
1013
  (* Resolve a templated base URL (e.g. https://{tenant_id}.hanko.io).
1003
1014
  Every placeholder must resolve to a non-empty value: from options.server
1004
1015
  (user), else the Config default. A placeholder that resolves to "" is a
@@ -93,14 +93,24 @@ let make_client ?(server : server option) ?(mode = "test") ?(base = "http://api.
93
93
  rootctx.c_op <- new_operation (jo [("name", Str "root"); ("entity", Str "_")]);
94
94
  client.cl_rootctx <- Some rootctx;
95
95
  List.iter (fun (name, opts) ->
96
- if has_feature name then begin
97
- let ftr = Sdk_config.make_feature name in
96
+ (* `netsim` is a test-only network simulator implemented in
97
+ Sdk_features, not a model feature, so it never appears in the
98
+ generated config and has_feature rejects it. Construct it directly:
99
+ without this every netsim-driven test (retry backoff, timeout,
100
+ and the metrics/telemetry/audit error paths) silently runs against
101
+ an unsimulated transport and sees a plain 200. *)
102
+ let ftr =
103
+ if name = "netsim" then Some (Sdk_features.netsim_feature ())
104
+ else if has_feature name then Some (Sdk_config.make_feature name)
105
+ else None in
106
+ match ftr with
107
+ | None -> ()
108
+ | Some ftr ->
98
109
  let fopts = jo [("active", Bool true)] in
99
110
  (match opts with Map _ -> List.iter (fun k -> setp fopts k (getp opts k)) (keysof opts) | _ -> ());
100
111
  setp (getp client.cl_options "feature") name fopts;
101
112
  ftr.f_init rootctx fopts;
102
- client.cl_features <- client.cl_features @ [ftr]
103
- end) features;
113
+ client.cl_features <- client.cl_features @ [ftr]) features;
104
114
  feature_hook_util rootctx "PostConstruct";
105
115
  { h_base = base; h_headers = hheaders; h_client = client; h_utility = utility; h_rootctx = rootctx }
106
116
 
@@ -129,6 +129,36 @@ sub is_control_skipped {
129
129
  return (0, undef);
130
130
  }
131
131
 
132
+ # Extra SDK options every LIVE client is constructed with, read from
133
+ # sdk-test-control.json `test.client.options`.
134
+ #
135
+ # The generated live client knows two things: the base URL (from the spec)
136
+ # and the credential (from the environment). Everything else about how a
137
+ # particular API wants to be talked to - which features to switch on, and
138
+ # with what settings - is a property of THAT API, known to the project and
139
+ # to nothing in the toolchain.
140
+ #
141
+ # Merged UNDER the generated fields, so the suite's own base/apikey/server
142
+ # values win: this ADDS to the live client, it does not redirect it.
143
+ #
144
+ # Reserved fields are stripped HERE rather than at each merge site: the
145
+ # generated hash only names a field when the model calls for one, so a
146
+ # "base" in this block would face no competing value and would silently
147
+ # redirect the whole suite - credential included - to another host.
148
+ my %LIVE_RESERVED = map { $_ => 1 }
149
+ qw(base prefix suffix server apikey secret);
150
+
151
+ sub live_client_options {
152
+ my $ctrl = load_test_control();
153
+ my $opts = ProjectNameHelpers::gpath($ctrl, 'test.client.options');
154
+ return {} unless Voxgig::Struct::ismap($opts);
155
+ my %out;
156
+ for my $k (keys %$opts) {
157
+ $out{$k} = $opts->{$k} unless $LIVE_RESERVED{$k};
158
+ }
159
+ return \%out;
160
+ }
161
+
132
162
  # Per-test live pacing delay (ms); default 500.
133
163
  sub live_delay_ms {
134
164
  my $ctrl = load_test_control();
@@ -69,7 +69,22 @@ $REGISTRY{make_options} = sub {
69
69
  # reads options.extend, but clone/validate dropped the instances.
70
70
  my $extend_raw = ProjectNameHelpers::gp($options, 'extend');
71
71
 
72
+ # `auth => undef` is the documented way to disable auth outright, and
73
+ # prepare_auth honours it before it ever reads the apikey. It cannot survive
74
+ # validate: depending on the struct port a stored null is either REPLACED by
75
+ # the optspec default - transmitting the credential the caller withheld - or
76
+ # REJECTED outright. Withhold the key for validate, then put the undef back.
77
+ # Same fix as ts/js/go make_options.
78
+ #
79
+ # Suppliedness cannot be recovered after validate, hence here, and it must
80
+ # tell an ABSENT auth from a present undef: exists rather than defined,
81
+ # which is false for both.
82
+ my $authsuppressed =
83
+ (ref($options) eq 'HASH' && exists $options->{auth} && !defined $options->{auth}) ? 1 : 0;
84
+
72
85
  my $opts = Voxgig::Struct::clone($options);
86
+
87
+ delete $opts->{auth} if $authsuppressed && ref($opts) eq 'HASH';
73
88
  $opts = {} unless Voxgig::Struct::ismap($opts);
74
89
  delete $opts->{extend};
75
90
 
@@ -143,6 +158,9 @@ $REGISTRY{make_options} = sub {
143
158
  my $validated = Voxgig::Struct::validate($merged, $optspec);
144
159
  $opts = Voxgig::Struct::ismap($validated) ? $validated : {};
145
160
 
161
+ # Restore the suppression the optspec default would otherwise erase.
162
+ $opts->{auth} = undef if $authsuppressed;
163
+
146
164
  if ($sys_fetch) {
147
165
  $opts->{system} = {} unless Voxgig::Struct::ismap($opts->{system});
148
166
  $opts->{system}{fetch} = $sys_fetch;
@@ -91,7 +91,16 @@ class FeatureCorpusTest extends TestCase
91
91
  */
92
92
  private static function buildClient(array $kase)
93
93
  {
94
- $opts = ['utility' => ['fetcher' => self::scriptedFetcher($kase['res'] ?? null)]];
94
+ // 'test' here is the OPTION, not the `test` FEATURE. It says "this
95
+ // client is not live", which is what makes a REQUIRED OpenAPI server
96
+ // variable resolve to a deterministic test-<name> rather than throw
97
+ // at construction (see makeOptions). It installs no transport, so the
98
+ // scripted fetcher still stands - the FEATURE is transport: 'base'
99
+ // and would shadow it.
100
+ $opts = [
101
+ 'test' => ['active' => true],
102
+ 'utility' => ['fetcher' => self::scriptedFetcher($kase['res'] ?? null)],
103
+ ];
95
104
  if (isset($kase['feature'])) {
96
105
  $opts['feature'] = $kase['feature'];
97
106
  }
@@ -138,6 +138,42 @@ class ProjectNameTestRunner
138
138
  return [false, null];
139
139
  }
140
140
 
141
+ /**
142
+ * Extra SDK options every LIVE client is constructed with, read from
143
+ * sdk-test-control.json `test.client.options`.
144
+ *
145
+ * The generated live client knows two things: the base URL (from the
146
+ * spec) and the credential (from the environment). Everything else
147
+ * about how a particular API wants to be talked to - which features to
148
+ * switch on, and with what settings - is a property of THAT API, known
149
+ * to the project and to nothing in the toolchain.
150
+ *
151
+ * Merged UNDER the generated fields, so the suite's own
152
+ * base/apikey/server values win: this ADDS to the live client, it does
153
+ * not redirect it.
154
+ *
155
+ * Reserved fields are stripped HERE rather than at each merge site: the
156
+ * generated array only names a field when the model calls for one, so a
157
+ * "base" in this block would face no competing value and would silently
158
+ * redirect the whole suite - credential included - to another host.
159
+ */
160
+ public static function live_client_options(): array
161
+ {
162
+ $ctrl = self::load_test_control();
163
+ $opts = $ctrl['test']['client']['options'] ?? null;
164
+ if (!is_array($opts)) {
165
+ return [];
166
+ }
167
+ $reserved = ['base', 'prefix', 'suffix', 'server', 'apikey', 'secret'];
168
+ $out = [];
169
+ foreach ($opts as $k => $v) {
170
+ if (!in_array($k, $reserved, true)) {
171
+ $out[$k] = $v;
172
+ }
173
+ }
174
+ return $out;
175
+ }
176
+
141
177
  /** Per-test live pacing delay (ms); default 500. */
142
178
  public static function live_delay_ms(): int
143
179
  {
@@ -94,12 +94,31 @@ class ProjectNameMakeOptions
94
94
  }
95
95
  }
96
96
 
97
+ // `auth: null` is the documented way to disable auth outright, and
98
+ // prepareAuth honours it before it ever reads the apikey. It cannot
99
+ // survive validate: depending on the struct port a stored null is
100
+ // either REPLACED by the optspec default - transmitting the
101
+ // credential the caller withheld - or REJECTED outright. Withhold the
102
+ // key for validate, then put the null back. Same fix as ts/js/go
103
+ // makeOptions.
104
+ //
105
+ // Suppliedness cannot be recovered after validate, hence here, and it
106
+ // must tell an ABSENT auth from a present null: array_key_exists
107
+ // rather than isset, which is false for both.
108
+ $authsuppressed = is_array($options)
109
+ && array_key_exists('auth', $options)
110
+ && null === $options['auth'];
111
+
97
112
  $opts = \Voxgig\Struct\Struct::clone($options);
98
113
  $opts = self::to_array_deep($opts);
99
114
  if (!is_array($opts)) {
100
115
  $opts = [];
101
116
  }
102
117
 
118
+ if ($authsuppressed) {
119
+ unset($opts['auth']);
120
+ }
121
+
103
122
  // Feature add-order. options['feature'] may be given as an ordered LIST
104
123
  // of ['name'=>..., 'active'=>..., ...] entries (the list position IS the
105
124
  // order in which features are added), or as a ['name'=>[opts]] map.
@@ -164,6 +183,11 @@ class ProjectNameMakeOptions
164
183
  $opts = [];
165
184
  }
166
185
 
186
+ // Restore the suppression the optspec default would otherwise erase.
187
+ if ($authsuppressed) {
188
+ $opts['auth'] = null;
189
+ }
190
+
167
191
  // Reattach the station binding handle held aside above (the feature
168
192
  // optspec is `$OPEN`, so the extra keys are legal; they were only
169
193
  // kept out of the clone/validate round-trip).
@@ -65,10 +65,26 @@ def make_options_util(ctx):
65
65
  if isinstance(options, dict) and options.get("extend") is not None:
66
66
  options = {k: v for k, v in options.items() if k != "extend"}
67
67
 
68
+ # `auth: None` is the documented way to disable auth outright, and
69
+ # prepare_auth honours it before it ever reads the apikey. It cannot
70
+ # survive validate: depending on the struct port a stored null is either
71
+ # REPLACED by the optspec default — transmitting the credential the
72
+ # caller withheld — or REJECTED outright. Withhold the key for validate,
73
+ # then put the null back. Same fix as ts/js/go make_options.
74
+ #
75
+ # Suppliedness cannot be recovered after validate, hence here, and it
76
+ # must tell an ABSENT auth from a present None: only the latter is a
77
+ # suppression.
78
+ authsuppressed = (
79
+ isinstance(options, dict) and "auth" in options and options["auth"] is None)
80
+
68
81
  opts = vs.clone(options)
69
82
  if not isinstance(opts, dict):
70
83
  opts = {}
71
84
 
85
+ if authsuppressed:
86
+ opts.pop("auth", None)
87
+
72
88
  # Feature add-order. options["feature"] may be given as an ordered LIST of
73
89
  # {name, active, ...opts} entries (the list position IS the order in which
74
90
  # features are added), or as a {name: {opts}} map. Normalize a list to a
@@ -162,6 +178,10 @@ def make_options_util(ctx):
162
178
  validated = {}
163
179
  opts = validated
164
180
 
181
+ # Restore the suppression the optspec default would otherwise erase.
182
+ if authsuppressed:
183
+ opts["auth"] = None
184
+
165
185
  # Resolve a templated base URL (e.g. https://{tenant_id}.hanko.io).
166
186
  # Every placeholder must resolve to a non-empty value: from
167
187
  # options["server"] (user), else the config default. A placeholder that
@@ -101,6 +101,34 @@ class ProjectNameTestRunner:
101
101
  return True, item.get("reason")
102
102
  return False, None
103
103
 
104
+ @staticmethod
105
+ def live_client_options():
106
+ """Extra SDK options every LIVE client is constructed with, from
107
+ sdk-test-control.json `test.client.options`.
108
+
109
+ The generated live client knows two things: the base URL (from the
110
+ spec) and the credential (from the environment). Everything else
111
+ about how a particular API wants to be talked to - which features to
112
+ switch on, and with what settings - is a property of THAT API, known
113
+ to the project and to nothing in the toolchain.
114
+
115
+ Merged UNDER the generated fields, so the suite's own
116
+ base/apikey/server values win: this ADDS to the live client, it does
117
+ not redirect it.
118
+
119
+ Reserved fields are stripped HERE rather than at each merge site:
120
+ the generated dict only names a field when the model calls for one,
121
+ so a "base" in this block would face no competing value and would
122
+ silently redirect the whole suite - credential included - to another
123
+ host.
124
+ """
125
+ ctrl = ProjectNameTestRunner.load_test_control()
126
+ opts = ctrl.get("test", {}).get("client", {}).get("options")
127
+ if not isinstance(opts, dict):
128
+ return {}
129
+ reserved = ("base", "prefix", "suffix", "server", "apikey", "secret")
130
+ return {k: v for k, v in opts.items() if k not in reserved}
131
+
104
132
  @staticmethod
105
133
  def live_delay_ms():
106
134
  """Per-test live pacing delay (ms); default 500."""
@@ -161,5 +189,9 @@ def load_test_control():
161
189
  return ProjectNameTestRunner.load_test_control()
162
190
 
163
191
 
192
+ def live_client_options():
193
+ return ProjectNameTestRunner.live_client_options()
194
+
195
+
164
196
  def live_delay_ms():
165
197
  return ProjectNameTestRunner.live_delay_ms()
@@ -95,7 +95,16 @@ def _client(kase):
95
95
  transport: 'base' and REPLACES the transport, so a client in test mode
96
96
  would shadow the script.
97
97
  """
98
- opts = {"utility": {"fetcher": _scripted_fetcher(kase.get("res"))}}
98
+ # "test" here is the OPTION, not the `test` FEATURE. It says "this
99
+ # client is not live", which is what makes a REQUIRED OpenAPI server
100
+ # variable resolve to a deterministic test-<name> rather than raise at
101
+ # construction (see make_options). It installs no transport, so the
102
+ # scripted fetcher still stands - the FEATURE is transport: 'base' and
103
+ # would shadow it, which is why this cannot just turn the feature on.
104
+ opts = {
105
+ "test": {"active": True},
106
+ "utility": {"fetcher": _scripted_fetcher(kase.get("res"))},
107
+ }
99
108
  if kase.get("feature") is not None:
100
109
  opts["feature"] = kase["feature"]
101
110
  return ProjectNameSDK(opts)
@@ -71,7 +71,15 @@ class FeatureCorpusTest < Minitest::Test
71
71
  # transport: 'base' and REPLACES the transport, so a client in test mode
72
72
  # would shadow the script.
73
73
  def build_client(kase)
74
- opts = { "utility" => { "fetcher" => scripted_fetcher(kase["res"]) } }
74
+ # "test" here is the OPTION, not the `test` FEATURE. It says "this
75
+ # client is not live", which is what makes a REQUIRED OpenAPI server
76
+ # variable resolve to a deterministic test-<name> rather than raise at
77
+ # construction (see make_options). It installs no transport, so the
78
+ # scripted fetcher still stands.
79
+ opts = {
80
+ "test" => { "active" => true },
81
+ "utility" => { "fetcher" => scripted_fetcher(kase["res"]) },
82
+ }
75
83
  opts["feature"] = kase["feature"] unless kase["feature"].nil?
76
84
  ProjectNameSDK.new(opts)
77
85
  end
@@ -101,6 +101,31 @@ module ProjectNameTestRunner
101
101
  [false, nil]
102
102
  end
103
103
 
104
+ # Extra SDK options every LIVE client is constructed with, read from
105
+ # sdk-test-control.json `test.client.options`.
106
+ #
107
+ # The generated live client knows two things: the base URL (from the spec)
108
+ # and the credential (from the environment). Everything else about how a
109
+ # particular API wants to be talked to - which features to switch on, and
110
+ # with what settings - is a property of THAT API, known to the project and
111
+ # to nothing in the toolchain.
112
+ #
113
+ # Merged UNDER the generated fields, so the suite's own base/apikey/server
114
+ # values win: this ADDS to the live client, it does not redirect it.
115
+ #
116
+ # Reserved fields are stripped HERE rather than at each merge site: the
117
+ # generated hash only names a field when the model calls for one, so a
118
+ # "base" in this block would face no competing value and would silently
119
+ # redirect the whole suite - credential included - to another host.
120
+ LIVE_RESERVED = %w[base prefix suffix server apikey secret].freeze
121
+
122
+ def self.live_client_options
123
+ ctrl = load_test_control
124
+ opts = ctrl.dig('test', 'client', 'options')
125
+ return {} unless opts.is_a?(Hash)
126
+ opts.reject { |k, _v| LIVE_RESERVED.include?(k) }
127
+ end
128
+
104
129
  # Per-test live pacing delay (ms); default 500.
105
130
  def self.live_delay_ms
106
131
  ctrl = load_test_control