@voxgig/sdkgen 4.6.0 → 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 (115) hide show
  1. package/bin/voxgig-sdkgen +1 -1
  2. package/dist/cmp/Feature.js +5 -0
  3. package/dist/cmp/Feature.js.map +1 -1
  4. package/dist/cmp/TestControl.d.ts +4 -0
  5. package/dist/cmp/TestControl.js +51 -0
  6. package/dist/cmp/TestControl.js.map +1 -0
  7. package/dist/helpers/featureSource.d.ts +3 -1
  8. package/dist/helpers/featureSource.js +105 -0
  9. package/dist/helpers/featureSource.js.map +1 -1
  10. package/dist/sdkgen.d.ts +3 -2
  11. package/dist/sdkgen.js +9 -4
  12. package/dist/sdkgen.js.map +1 -1
  13. package/dist/tsconfig.tsbuildinfo +1 -1
  14. package/dist/utility.d.ts +2 -1
  15. package/dist/utility.js +52 -0
  16. package/dist/utility.js.map +1 -1
  17. package/model/sdkgen.aon +58 -0
  18. package/package.json +1 -1
  19. package/project/.sdk/model/feature/secrets.aon +82 -0
  20. package/project/.sdk/src/cmp/cpp/Main_cpp.ts +2 -1
  21. package/project/.sdk/src/cmp/cpp/Test_cpp.ts +5 -1
  22. package/project/.sdk/src/cmp/csharp/Main_csharp.ts +2 -1
  23. package/project/.sdk/src/cmp/csharp/TestDirect_csharp.ts +30 -6
  24. package/project/.sdk/src/cmp/csharp/TestEntity_csharp.ts +29 -5
  25. package/project/.sdk/src/cmp/csharp/Test_csharp.ts +5 -1
  26. package/project/.sdk/src/cmp/dart/Main_dart.ts +2 -1
  27. package/project/.sdk/src/cmp/dart/TestDirect_dart.ts +23 -6
  28. package/project/.sdk/src/cmp/dart/TestEntity_dart.ts +28 -7
  29. package/project/.sdk/src/cmp/dart/Test_dart.ts +5 -1
  30. package/project/.sdk/src/cmp/go/Main_go.ts +2 -1
  31. package/project/.sdk/src/cmp/go/Test_go.ts +5 -1
  32. package/project/.sdk/src/cmp/java/Main_java.ts +2 -1
  33. package/project/.sdk/src/cmp/java/Test_java.ts +5 -1
  34. package/project/.sdk/src/cmp/js/Main_js.ts +2 -1
  35. package/project/.sdk/src/cmp/js/TestDirect_js.ts +30 -5
  36. package/project/.sdk/src/cmp/js/TestEntity_js.ts +35 -5
  37. package/project/.sdk/src/cmp/js/Test_js.ts +5 -1
  38. package/project/.sdk/src/cmp/js/fragment/Direct.test.fragment.js +7 -1
  39. package/project/.sdk/src/cmp/js/fragment/Entity.test.fragment.js +7 -1
  40. package/project/.sdk/src/cmp/kotlin/Main_kotlin.ts +2 -1
  41. package/project/.sdk/src/cmp/kotlin/Test_kotlin.ts +5 -1
  42. package/project/.sdk/src/cmp/lua/Main_lua.ts +2 -1
  43. package/project/.sdk/src/cmp/lua/TestDirect_lua.ts +28 -4
  44. package/project/.sdk/src/cmp/lua/TestEntity_lua.ts +24 -4
  45. package/project/.sdk/src/cmp/lua/Test_lua.ts +5 -1
  46. package/project/.sdk/src/cmp/ocaml/Gitignore_ocaml.ts +1 -0
  47. package/project/.sdk/src/cmp/perl/Main_perl.ts +2 -1
  48. package/project/.sdk/src/cmp/perl/TestDirect_perl.ts +25 -4
  49. package/project/.sdk/src/cmp/perl/TestEntity_perl.ts +24 -4
  50. package/project/.sdk/src/cmp/perl/Test_perl.ts +5 -1
  51. package/project/.sdk/src/cmp/perl/utility_perl.ts +10 -0
  52. package/project/.sdk/src/cmp/php/Main_php.ts +2 -1
  53. package/project/.sdk/src/cmp/php/TestDirect_php.ts +24 -5
  54. package/project/.sdk/src/cmp/php/TestEntity_php.ts +28 -4
  55. package/project/.sdk/src/cmp/php/Test_php.ts +5 -1
  56. package/project/.sdk/src/cmp/py/Main_py.ts +2 -1
  57. package/project/.sdk/src/cmp/py/Test_py.ts +5 -1
  58. package/project/.sdk/src/cmp/rb/Main_rb.ts +2 -1
  59. package/project/.sdk/src/cmp/rb/TestDirect_rb.ts +24 -5
  60. package/project/.sdk/src/cmp/rb/TestEntity_rb.ts +24 -4
  61. package/project/.sdk/src/cmp/rb/Test_rb.ts +5 -1
  62. package/project/.sdk/src/cmp/rb/utility_rb.ts +3 -1
  63. package/project/.sdk/src/cmp/rust/Main_rust.ts +2 -1
  64. package/project/.sdk/src/cmp/rust/TestDirect_rust.ts +31 -4
  65. package/project/.sdk/src/cmp/rust/TestEntity_rust.ts +37 -4
  66. package/project/.sdk/src/cmp/rust/Test_rust.ts +5 -1
  67. package/project/.sdk/src/cmp/scala/Main_scala.ts +2 -1
  68. package/project/.sdk/src/cmp/scala/Test_scala.ts +5 -1
  69. package/project/.sdk/src/cmp/ts/Config_ts.ts +50 -8
  70. package/project/.sdk/src/cmp/ts/Main_ts.ts +10 -2
  71. package/project/.sdk/src/cmp/ts/TestDirect_ts.ts +10 -3
  72. package/project/.sdk/src/cmp/ts/TestEntity_ts.ts +11 -3
  73. package/project/.sdk/src/cmp/ts/Test_ts.ts +5 -1
  74. package/project/.sdk/tm/csharp/test/Runner.cs +45 -0
  75. package/project/.sdk/tm/dart/test/utility.dart +39 -0
  76. package/project/.sdk/tm/js/test/sdk-test-control.json +19 -0
  77. package/project/.sdk/tm/js/test/utility.js +91 -1
  78. package/project/.sdk/tm/lua/test/runner.lua +40 -0
  79. package/project/.sdk/tm/ocaml/test/harness.ml +14 -4
  80. package/project/.sdk/tm/perl/t/runner.pm +30 -0
  81. package/project/.sdk/tm/php/test/Runner.php +36 -0
  82. package/project/.sdk/tm/rb/test/runner.rb +25 -0
  83. package/project/.sdk/tm/rust/tests/common/mod.rs +32 -0
  84. package/project/.sdk/tm/rust/utility/make_options.rs +1 -1
  85. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/Sekreto.ts +8 -2
  86. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/Sigv4.ts +29 -5
  87. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/index.ts +30 -21
  88. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/Registry.ts +90 -0
  89. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/addr.ts +48 -0
  90. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/aws.ts +219 -0
  91. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/azuresecrets.ts +144 -0
  92. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/boru.ts +113 -0
  93. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/doppler.ts +77 -0
  94. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/dotenv.ts +51 -0
  95. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/env.ts +31 -0
  96. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/file.ts +54 -0
  97. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/gcpsecrets.ts +118 -0
  98. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/hashicorp.ts +159 -0
  99. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/http.ts +66 -0
  100. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/infisical.ts +107 -0
  101. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/memory.ts +33 -0
  102. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/onepassword.ts +120 -0
  103. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/support.ts +176 -0
  104. package/project/.sdk/tm/ts/test/vendor/omni/Runner.ts +15 -13
  105. package/project/.sdk/tm/ts/test/vendor/omni/Util.ts +25 -19
  106. package/project/.sdk/tm/ts/test/vendor/omni/compat.ts +1 -1
  107. package/project/.sdk/tm/ts/test/vendor/omni/index.ts +1 -1
  108. package/project/.sdk/tm/zig/core/utility.zig +1 -1
  109. package/project/sdkgen-package.json +1 -1
  110. package/src/cmp/Feature.ts +5 -0
  111. package/src/cmp/TestControl.ts +64 -0
  112. package/src/helpers/featureSource.ts +130 -0
  113. package/src/sdkgen.ts +7 -0
  114. package/src/utility.ts +61 -0
  115. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/Providers.ts +0 -1221
@@ -5,6 +5,7 @@ import {
5
5
  cmp, each,
6
6
  File, Content, Copy, Folder, Fragment,
7
7
  targetFeatures,
8
+ TEST_CONTROL_EXCLUDE
8
9
  } from '@voxgig/sdkgen'
9
10
 
10
11
 
@@ -51,7 +52,7 @@ const Main = cmp(async function Main(props: any) {
51
52
  // ProjectName carries the SDK name into vendored template strings.
52
53
  Copy({
53
54
  from: 'tm/' + target.name,
54
- exclude: [/src\//],
55
+ exclude: [/src\//, TEST_CONTROL_EXCLUDE],
55
56
  replace: {
56
57
  ...props.ctx$.stdrep,
57
58
  ProjectName: model.const.Name,
@@ -9,7 +9,8 @@ import type {
9
9
  ModelEntity
10
10
  } from '@voxgig/apidef'
11
11
 
12
- import { cmp, each, Folder, File, Content, entityCollection } from '@voxgig/sdkgen'
12
+ import { cmp, each, Folder, File, Content, entityCollection,
13
+ TestControl } from '@voxgig/sdkgen'
13
14
 
14
15
 
15
16
  import { TestEntity } from './TestEntity_scala'
@@ -31,6 +32,9 @@ const Test = cmp(function Test(props: any) {
31
32
  // template, and are driven by the generated SdkEntityTestMain aggregator.
32
33
  Folder({ name: 'sdktest' }, () => {
33
34
 
35
+ // Write-once: a project's edited control file survives regeneration.
36
+ TestControl({ target, dir: 'sdktest' })
37
+
34
38
  const entities: { Name: string; entity: boolean; direct: boolean }[] = []
35
39
 
36
40
  const entity = each(entityCollection(model))
@@ -99,10 +99,13 @@ const Config = cmp(async function Config(props: any) {
99
99
 
100
100
  replace: {
101
101
 
102
- '// #ImportFeatures': () => each(feature, (f: any) => {
103
- Line(`import { ${nom(f, 'Name')}Feature } from ` +
104
- `'./feature/${f.name}/${nom(f, 'Name')}Feature'`)
105
- }),
102
+ '// #ImportFeatures': () => {
103
+ each(feature, (f: any) => {
104
+ Line(`import { ${nom(f, 'Name')}Feature } from ` +
105
+ `'./feature/${f.name}/${nom(f, 'Name')}Feature'`)
106
+ })
107
+ pluginImports(feature)
108
+ },
106
109
 
107
110
  '// #FeatureClasses': () => each(feature, (f: any) => {
108
111
  Line(` ${f.name}: ${nom(f, 'Name')}Feature,`)
@@ -130,10 +133,13 @@ const Config = cmp(async function Config(props: any) {
130
133
 
131
134
  "'HEADERS'": indent(JSON.stringify(headers, null, 2), 4).trim(),
132
135
 
133
- '// #ImportFeatures': () => each(feature, (f: any) => {
134
- Line(`import { ${nom(f, 'Name')}Feature } from ` +
135
- `'./feature/${f.name}/${nom(f, 'Name')}Feature'`)
136
- }),
136
+ '// #ImportFeatures': () => {
137
+ each(feature, (f: any) => {
138
+ Line(`import { ${nom(f, 'Name')}Feature } from ` +
139
+ `'./feature/${f.name}/${nom(f, 'Name')}Feature'`)
140
+ })
141
+ pluginImports(feature)
142
+ },
137
143
 
138
144
  // Values from configDefinition's def, not re-derived here, so the
139
145
  // literal rep and the data rep cannot disagree on identity.
@@ -177,6 +183,42 @@ const Config = cmp(async function Config(props: any) {
177
183
  })
178
184
 
179
185
 
186
+
187
+ // PLUGIN REGISTRATION IMPORTS.
188
+ //
189
+ // A sekreto provider module registers itself when it is imported, and
190
+ // nothing else imports it: the core surface deliberately does not reach a
191
+ // platform-dependent provider, and the full-set barrel — the file whose
192
+ // job was to reach all of them — is not vendored into an SDK at all.
193
+ //
194
+ // So without these lines an SDK generates `provider/dotenv.ts`, never
195
+ // imports it, and throws `unknown provider kind: dotenv` the first time a
196
+ // chain names it. Emitted here because Config already imports every active
197
+ // feature from the model, and this is the same list one level down.
198
+ //
199
+ // Import for SIDE EFFECT only — `import '...'`, no bindings. The module's
200
+ // top-level `register()` call is the whole point.
201
+ function pluginImports(feature: any) {
202
+ each(feature, (f: any) => {
203
+ each(f.plugin, (plugin: any) => {
204
+ // Filter on `active` HERE rather than trusting the feature object to
205
+ // arrive filtered. Whether a model path was read with `only_active`
206
+ // varies by call site, and getting it wrong in this direction emits
207
+ // an import for a module the trim just deleted — an SDK that does
208
+ // not compile, rather than one that merely carries too much.
209
+ if (false === plugin.active || null == plugin.active) return
210
+
211
+ for (const one of (plugin.path || [])) {
212
+ const path = String(one)
213
+ // Only a provider MODULE registers a kind; a plugin may own other
214
+ // files (Sigv4.ts is signing support, not a provider).
215
+ if (!/\/provider\/[^/]+\.ts$/.test(path)) continue
216
+ Line(`import '${'./' + path.replace(/^src\//, '').replace(/\.ts$/, '')}'`)
217
+ }
218
+ })
219
+ })
220
+ }
221
+
180
222
  export {
181
223
  Config
182
224
  }
@@ -4,8 +4,10 @@ import * as Path from 'node:path'
4
4
  import {
5
5
  cmp, each, names, cmap,
6
6
  List, File, Content, Copy, Folder, Fragment, Line, FeatureHook,
7
- entityClassName, entityCollection, srcFeatureExcludes, stationLibrary,
7
+ entityClassName, entityCollection, srcFeatureExcludes, pluginExcludes,
8
+ stationLibrary,
8
9
  targetFeatures,
10
+ TEST_CONTROL_EXCLUDE
9
11
  } from '@voxgig/sdkgen'
10
12
 
11
13
 
@@ -53,7 +55,13 @@ const Main = cmp(async function Main(props: any) {
53
55
  from: 'tm/' + target.name,
54
56
  // Root copies src/feature/<name>/ per ACTIVE feature; keep this blanket
55
57
  // copy from restoring one that was switched off after `target add`.
56
- exclude: srcFeatureExcludes(model),
58
+ // A feature's inactive plugins go too - same rule, one level
59
+ // deeper. See helpers/featureSource.pluginExcludes.
60
+ exclude: [
61
+ ...srcFeatureExcludes(model),
62
+ ...pluginExcludes(model),
63
+ TEST_CONTROL_EXCLUDE,
64
+ ],
57
65
  replace: {
58
66
  ...props.ctx$.stdrep,
59
67
  }
@@ -21,7 +21,8 @@ import {
21
21
  serverVariables,
22
22
  isHttpBasicAuth,
23
23
  jsProp,
24
- jsOptProp, envName, envToken, liveStrict
24
+ jsOptProp, envName, envToken, liveStrict,
25
+ jsKey
25
26
  } from '@voxgig/sdkgen'
26
27
 
27
28
 
@@ -49,7 +50,7 @@ const TestDirect = cmp(function TestDirect(props: any) {
49
50
  const authActive = isAuthActive(model)
50
51
  const authBasic = authActive && isHttpBasicAuth(model)
51
52
  const apikeyEnvEntry = authActive
52
- ? `\n '${PROJECTNAME}_APIKEY': '',${authBasic ? `\n '${PROJECTNAME}_SECRET': 'NONE',` : ''}`
53
+ ? `\n '${PROJECTNAME}_APIKEY': '',${authBasic ? `\n '${PROJECTNAME}_SECRET': '',` : ''}`
53
54
  : ''
54
55
  const apikeyLiveField = authActive
55
56
  ? `
@@ -61,13 +62,19 @@ const TestDirect = cmp(function TestDirect(props: any) {
61
62
  // impossible to construct without values: makeOptions raises rather than
62
63
  // request a URL with a literal `{account_id}` in it. Taken from the
63
64
  // environment, the same way the apikey is.
65
+ //
66
+ // Keys are quoted and the env read is bracketed via jsKey/jsProp: a server
67
+ // variable name is spec-derived and need not be a JS identifier — the URL
68
+ // grammar admits a leading digit ({2fa}), and a declared-but-unreferenced
69
+ // variable ({edge-zone}) is not constrained at all. Bare `name:` and
70
+ // `env.PROJ_SERVER_EDGE-ZONE` are both syntax errors.
64
71
  const svars = serverVariables(model)
65
72
  const serverEnvEntry = svars
66
73
  .map((v: any) => `\n '${serverVarEnv(PROJECTNAME, v.name)}': ${JSON.stringify(v.dflt)},`).join('')
67
74
  const serverLiveField = 0 === svars.length ? '' : `
68
75
  server: {${svars
69
76
  .map((v: any) => `
70
- ${v.name}: env.${serverVarEnv(PROJECTNAME, v.name)},`).join('')}
77
+ ${jsKey(v.name)}: ${jsProp('env', serverVarEnv(PROJECTNAME, v.name))},`).join('')}
71
78
  },`
72
79
 
73
80
  const opnames = Object.keys(entity.op || {})
@@ -33,7 +33,9 @@ import {
33
33
  serverVarEnv,
34
34
  serverVariables,
35
35
  isHttpBasicAuth,
36
- entityDataIdField, envName, envToken
36
+ entityDataIdField, envName, envToken,
37
+ jsKey,
38
+ jsProp
37
39
  } from '@voxgig/sdkgen'
38
40
 
39
41
 
@@ -69,7 +71,7 @@ const TestEntity = cmp(function TestEntity(props: any) {
69
71
  const authActive = isAuthActive(model)
70
72
  const authBasic = authActive && isHttpBasicAuth(model)
71
73
  const apikeyEnvEntry = authActive
72
- ? `\n '${PROJENVNAME}_APIKEY': '',${authBasic ? `\n '${PROJENVNAME}_SECRET': 'NONE',` : ''}`
74
+ ? `\n '${PROJENVNAME}_APIKEY': '',${authBasic ? `\n '${PROJENVNAME}_SECRET': '',` : ''}`
73
75
  : ''
74
76
  const apikeyLiveField = authActive
75
77
  ? `
@@ -81,13 +83,19 @@ const TestEntity = cmp(function TestEntity(props: any) {
81
83
  // impossible to construct without values: makeOptions raises rather than
82
84
  // request a URL with a literal `{account_id}` in it. So the live suite
83
85
  // takes them from the environment the same way it takes the apikey.
86
+ //
87
+ // Keys are quoted and the env read is bracketed via jsKey/jsProp: a server
88
+ // variable name is spec-derived and need not be a JS identifier — the URL
89
+ // grammar admits a leading digit ({2fa}), and a declared-but-unreferenced
90
+ // variable ({edge-zone}) is not constrained at all. Bare `name:` and
91
+ // `env.PROJ_SERVER_EDGE-ZONE` are both syntax errors.
84
92
  const svars = serverVariables(model)
85
93
  const serverEnvEntry = svars
86
94
  .map((v: any) => `\n '${serverVarEnv(PROJENVNAME, v.name)}': ${JSON.stringify(v.dflt)},`).join('')
87
95
  const serverLiveField = 0 === svars.length ? '' : `
88
96
  server: {${svars
89
97
  .map((v: any) => `
90
- ${v.name}: env.${serverVarEnv(PROJENVNAME, v.name)},`).join('')}
98
+ ${jsKey(v.name)}: ${jsProp('env', serverVarEnv(PROJENVNAME, v.name))},`).join('')}
91
99
  },`
92
100
 
93
101
  // TODO: should be a utility function
@@ -3,7 +3,8 @@ import type {
3
3
  ModelEntity
4
4
  } from '@voxgig/apidef'
5
5
 
6
- import { cmp, each, Folder, entityCollection } from '@voxgig/sdkgen'
6
+ import { cmp, each, Folder, entityCollection,
7
+ TestControl } from '@voxgig/sdkgen'
7
8
 
8
9
 
9
10
  // import { Quick } from './Quick_ts'
@@ -19,6 +20,9 @@ const Test = cmp(function Test(props: any) {
19
20
  const { target } = props
20
21
 
21
22
  Folder({ name: 'test' }, () => {
23
+
24
+ // Write-once: a project's edited control file survives regeneration.
25
+ TestControl({ target, dir: 'test' })
22
26
  // Quick({ target })
23
27
  // TestMain({ target })
24
28
 
@@ -153,6 +153,51 @@ public static class TestRunner
153
153
  return _testControl;
154
154
  }
155
155
 
156
+ // LiveClientOptions returns the extra SDK options every LIVE client is
157
+ // constructed with, read from sdk-test-control.json `test.client.options`.
158
+ //
159
+ // The generated live client knows two things: the base URL (from the
160
+ // spec) and the credential (from the environment). Everything else about
161
+ // how a particular API wants to be talked to - which features to switch
162
+ // on, and with what settings - is a property of THAT API, known to the
163
+ // project and to nothing in the toolchain.
164
+ //
165
+ // Merged UNDER the generated fields, so the suite's own
166
+ // base/apikey/server values win: this ADDS to the live client, it does
167
+ // not redirect it.
168
+ //
169
+ // Reserved fields are stripped HERE rather than at each merge site: the
170
+ // generated dictionary only names a field when the model calls for one,
171
+ // so a "base" in this block would face no competing value and would
172
+ // silently redirect the whole suite - credential included - to another
173
+ // host.
174
+ private static readonly string[] LiveReserved =
175
+ { "base", "prefix", "suffix", "server", "apikey", "secret" };
176
+
177
+ public static Dictionary<string, object?> LiveClientOptions()
178
+ {
179
+ var ctrl = LoadTestControl();
180
+ if (!ctrl.TryGetValue("test", out var testRaw) ||
181
+ testRaw is not Dictionary<string, object?> test ||
182
+ !test.TryGetValue("client", out var clientRaw) ||
183
+ clientRaw is not Dictionary<string, object?> client ||
184
+ !client.TryGetValue("options", out var optsRaw) ||
185
+ optsRaw is not Dictionary<string, object?> opts)
186
+ {
187
+ return new Dictionary<string, object?>();
188
+ }
189
+
190
+ var out_ = new Dictionary<string, object?>();
191
+ foreach (var kv in opts)
192
+ {
193
+ if (Array.IndexOf(LiveReserved, kv.Key) < 0)
194
+ {
195
+ out_[kv.Key] = kv.Value;
196
+ }
197
+ }
198
+ return out_;
199
+ }
200
+
156
201
  // IsControlSkipped checks sdk-test-control.json for a skip entry.
157
202
  // Returns (skip, reason).
158
203
  public static (bool, string) IsControlSkipped(string kind, string name, string mode)
@@ -143,6 +143,45 @@ bool skipIfMissingIds(T t, Map setup, List<String> requiredKeys) {
143
143
 
144
144
  // Per-test live pacing delay (ms). Read from sdk-test-control.json
145
145
  // `test.live.delayMs`; defaults to 500ms if absent or invalid.
146
+ // Extra SDK options every LIVE client is constructed with, read from
147
+ // sdk-test-control.json `test.client.options`.
148
+ //
149
+ // The generated live client knows two things: the base URL (from the spec)
150
+ // and the credential (from the environment). Everything else about how a
151
+ // particular API wants to be talked to - which features to switch on, and
152
+ // with what settings - is a property of THAT API, known to the project and
153
+ // to nothing in the toolchain.
154
+ //
155
+ // Merged UNDER the generated fields, so the suite's own base/apikey/server
156
+ // values win: this ADDS to the live client, it does not redirect it.
157
+ //
158
+ // Reserved fields are stripped HERE rather than at each merge site: the
159
+ // generated map only names a field when the model calls for one, so a
160
+ // 'base' in this block would face no competing value and would silently
161
+ // redirect the whole suite - credential included - to another host.
162
+ const List<String> liveReserved = [
163
+ 'base', 'prefix', 'suffix', 'server', 'apikey', 'secret'
164
+ ];
165
+
166
+ Map<String, dynamic> liveClientOptions() {
167
+ final ctrl = loadTestControl();
168
+ final t = ctrl['test'];
169
+ if (t is! Map) return <String, dynamic>{};
170
+ final c = t['client'];
171
+ if (c is! Map) return <String, dynamic>{};
172
+ final opts = c['options'];
173
+ if (opts is! Map) return <String, dynamic>{};
174
+
175
+ final out = <String, dynamic>{};
176
+ opts.forEach((k, v) {
177
+ if (!liveReserved.contains(k)) {
178
+ out['$k'] = v;
179
+ }
180
+ });
181
+ return out;
182
+ }
183
+
184
+
146
185
  int liveDelayMs() {
147
186
  final ctrl = loadTestControl();
148
187
  final t = ctrl['test'];
@@ -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
  }
@@ -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()
@@ -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();
@@ -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
  {
@@ -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