@voxgig/sdkgen 4.5.3 → 4.6.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 (174) 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 +17 -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/helpers/applicability.d.ts +6 -0
  21. package/dist/helpers/applicability.js +103 -0
  22. package/dist/helpers/applicability.js.map +1 -0
  23. package/dist/helpers/collectDeps.js +4 -2
  24. package/dist/helpers/collectDeps.js.map +1 -1
  25. package/dist/helpers/serverVars.d.ts +2 -1
  26. package/dist/helpers/serverVars.js +17 -0
  27. package/dist/helpers/serverVars.js.map +1 -1
  28. package/dist/sdkgen.d.ts +3 -2
  29. package/dist/sdkgen.js +9 -2
  30. package/dist/sdkgen.js.map +1 -1
  31. package/dist/tsconfig.tsbuildinfo +1 -1
  32. package/dist/utility.js +7 -1
  33. package/dist/utility.js.map +1 -1
  34. package/model/sdkgen.aon +39 -0
  35. package/package.json +1 -1
  36. package/project/.sdk/model/feature/feature-index.aon +1 -0
  37. package/project/.sdk/model/feature/secrets.aon +118 -0
  38. package/project/.sdk/model/target/ts.aon +8 -1
  39. package/project/.sdk/src/cmp/c/Config_c.ts +5 -1
  40. package/project/.sdk/src/cmp/cpp/Config_cpp.ts +5 -1
  41. package/project/.sdk/src/cmp/csharp/Config_csharp.ts +5 -1
  42. package/project/.sdk/src/cmp/dart/Config_dart.ts +5 -1
  43. package/project/.sdk/src/cmp/elixir/Config_elixir.ts +5 -1
  44. package/project/.sdk/src/cmp/elixir/Main_elixir.ts +5 -1
  45. package/project/.sdk/src/cmp/go/Config_go.ts +5 -1
  46. package/project/.sdk/src/cmp/go/Main_go.ts +6 -2
  47. package/project/.sdk/src/cmp/go/ReadmeQuick_go.ts +16 -3
  48. package/project/.sdk/src/cmp/go/ReadmeTopQuick_go.ts +16 -3
  49. package/project/.sdk/src/cmp/go/TestDirect_go.ts +28 -4
  50. package/project/.sdk/src/cmp/go/TestEntity_go.ts +43 -10
  51. package/project/.sdk/src/cmp/go/fragment/Main.fragment.go +5 -5
  52. package/project/.sdk/src/cmp/java/Config_java.ts +22 -1
  53. package/project/.sdk/src/cmp/java/Main_java.ts +5 -1
  54. package/project/.sdk/src/cmp/java/ReadmeQuick_java.ts +17 -3
  55. package/project/.sdk/src/cmp/java/ReadmeTopQuick_java.ts +17 -3
  56. package/project/.sdk/src/cmp/java/TestDirect_java.ts +23 -4
  57. package/project/.sdk/src/cmp/java/TestEntity_java.ts +29 -4
  58. package/project/.sdk/src/cmp/js/Config_js.ts +5 -1
  59. package/project/.sdk/src/cmp/js/Main_js.ts +5 -1
  60. package/project/.sdk/src/cmp/js/Package_js.ts +4 -1
  61. package/project/.sdk/src/cmp/kotlin/Config_kotlin.ts +5 -1
  62. package/project/.sdk/src/cmp/lean/Config_lean.ts +5 -1
  63. package/project/.sdk/src/cmp/lua/Config_lua.ts +5 -1
  64. package/project/.sdk/src/cmp/lua/Main_lua.ts +5 -1
  65. package/project/.sdk/src/cmp/lua/Package_lua.ts +4 -1
  66. package/project/.sdk/src/cmp/ocaml/Config_ocaml.ts +5 -1
  67. package/project/.sdk/src/cmp/perl/Main_perl.ts +5 -1
  68. package/project/.sdk/src/cmp/php/Config_php.ts +5 -1
  69. package/project/.sdk/src/cmp/php/Main_php.ts +5 -1
  70. package/project/.sdk/src/cmp/py/Config_py.ts +5 -1
  71. package/project/.sdk/src/cmp/py/Main_py.ts +5 -1
  72. package/project/.sdk/src/cmp/py/ReadmeQuick_py.ts +16 -3
  73. package/project/.sdk/src/cmp/py/ReadmeTopQuick_py.ts +16 -3
  74. package/project/.sdk/src/cmp/py/TestDirect_py.ts +24 -5
  75. package/project/.sdk/src/cmp/py/TestEntity_py.ts +23 -3
  76. package/project/.sdk/src/cmp/rb/Config_rb.ts +5 -1
  77. package/project/.sdk/src/cmp/rb/Main_rb.ts +5 -1
  78. package/project/.sdk/src/cmp/rust/Config_rust.ts +5 -1
  79. package/project/.sdk/src/cmp/rust/Main_rust.ts +5 -1
  80. package/project/.sdk/src/cmp/scala/Config_scala.ts +5 -1
  81. package/project/.sdk/src/cmp/scala/Main_scala.ts +5 -1
  82. package/project/.sdk/src/cmp/swift/Config_swift.ts +5 -1
  83. package/project/.sdk/src/cmp/ts/Config_ts.ts +5 -1
  84. package/project/.sdk/src/cmp/ts/Main_ts.ts +57 -1
  85. package/project/.sdk/src/cmp/ts/Package_ts.ts +4 -1
  86. package/project/.sdk/src/cmp/ts/ReadmeModel_ts.ts +15 -3
  87. package/project/.sdk/src/cmp/ts/ReadmeQuick_ts.ts +18 -5
  88. package/project/.sdk/src/cmp/ts/ReadmeTopQuick_ts.ts +19 -5
  89. package/project/.sdk/src/cmp/ts/TestDirect_ts.ts +67 -7
  90. package/project/.sdk/src/cmp/ts/TestEntity_ts.ts +27 -4
  91. package/project/.sdk/src/cmp/ts/fragment/Direct.test.fragment.ts +9 -2
  92. package/project/.sdk/src/cmp/ts/fragment/Entity.test.fragment.ts +9 -2
  93. package/project/.sdk/src/cmp/ts/fragment/Main.fragment.ts +7 -0
  94. package/project/.sdk/src/cmp/zig/Config_zig.ts +5 -1
  95. package/project/.sdk/tm/c/utility/make_options.c +22 -0
  96. package/project/.sdk/tm/clojure/src/sdk/core.clj +18 -1
  97. package/project/.sdk/tm/cpp/utility/pipeline.hpp +23 -0
  98. package/project/.sdk/tm/csharp/test/CustomUtilityTest.cs +15 -0
  99. package/project/.sdk/tm/csharp/utility/MakeOptions.cs +23 -0
  100. package/project/.sdk/tm/dart/lib/utility/MakeOptionsUtility.dart +22 -0
  101. package/project/.sdk/tm/elixir/lib/projectname/utility.ex +19 -0
  102. package/project/.sdk/tm/go/core/context.go +1 -1
  103. package/project/.sdk/tm/go/feature/cost_feature.go +1 -1
  104. package/project/.sdk/tm/go/feature/paging_feature.go +5 -5
  105. package/project/.sdk/tm/go/feature/test_feature.go +12 -5
  106. package/project/.sdk/tm/go/test/custom_utility_test.go +15 -0
  107. package/project/.sdk/tm/go/test/feature_corpus_test.go +7 -0
  108. package/project/.sdk/tm/go/test/runner_test.go +46 -0
  109. package/project/.sdk/tm/go/test/struct_runner_test.go +7 -7
  110. package/project/.sdk/tm/go/test/struct_utility_test.go +63 -27
  111. package/project/.sdk/tm/go/utility/fetcher.go +2 -2
  112. package/project/.sdk/tm/go/utility/make_options.go +24 -5
  113. package/project/.sdk/tm/go/utility/make_point.go +1 -1
  114. package/project/.sdk/tm/go/utility/make_spec.go +17 -1
  115. package/project/.sdk/tm/go/utility/prepare_auth.go +1 -1
  116. package/project/.sdk/tm/go/utility/struct/voxgigstruct.go +370 -307
  117. package/project/.sdk/tm/go/utility/transform_request.go +29 -1
  118. package/project/.sdk/tm/go/utility/transform_response.go +20 -1
  119. package/project/.sdk/tm/java/test/CustomUtilityTest.java +10 -0
  120. package/project/.sdk/tm/java/test/FeatureCorpusTest.java +7 -0
  121. package/project/.sdk/tm/java/test/PrimaryUtilityTest.java +6 -0
  122. package/project/.sdk/tm/java/test/RunnerSupport.java +43 -0
  123. package/project/.sdk/tm/java/utility/MakeOptions.java +84 -1
  124. package/project/.sdk/tm/js/src/utility/MakeOptionsUtility.js +24 -0
  125. package/project/.sdk/tm/js/src/utility/StructUtility.js +2076 -1837
  126. package/project/.sdk/tm/js/test/feature/Corpus.test.js +9 -0
  127. package/project/.sdk/tm/kotlin/utility/MakeOptions.kt +21 -0
  128. package/project/.sdk/tm/lean/src/SdkUtility.lean +12 -1
  129. package/project/.sdk/tm/ocaml/sdk_runtime.ml +11 -0
  130. package/project/.sdk/tm/perl/utility/make_options.pm +18 -0
  131. package/project/.sdk/tm/php/test/FeatureCorpusTest.php +10 -1
  132. package/project/.sdk/tm/php/utility/MakeOptions.php +24 -0
  133. package/project/.sdk/tm/py/pkg/utility/make_options.py +20 -0
  134. package/project/.sdk/tm/py/test/runner.py +32 -0
  135. package/project/.sdk/tm/py/test/test_feature_corpus.py +10 -1
  136. package/project/.sdk/tm/rb/test/feature_corpus_test.rb +9 -1
  137. package/project/.sdk/tm/rb/utility/make_options.rb +17 -0
  138. package/project/.sdk/tm/rust/utility/make_options.rs +25 -0
  139. package/project/.sdk/tm/scala/utility/Make.scala +17 -0
  140. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/MakeOptions.swift +21 -0
  141. package/project/.sdk/tm/ts/src/feature/secrets/SecretsFeature.ts +419 -0
  142. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/Providers.ts +1221 -0
  143. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/Sekreto.ts +381 -0
  144. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/Sigv4.ts +151 -0
  145. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/index.ts +42 -0
  146. package/project/.sdk/tm/ts/src/utility/MakeOptionsUtility.ts +18 -0
  147. package/project/.sdk/tm/ts/src/utility/StructUtility.ts +1321 -710
  148. package/project/.sdk/tm/ts/test/feature/Corpus.test.ts +9 -0
  149. package/project/.sdk/tm/ts/test/feature/secrets/Secrets.test.ts +764 -0
  150. package/project/.sdk/tm/ts/test/omni.test.ts +84 -0
  151. package/project/.sdk/tm/ts/test/omni.ts +86 -0
  152. package/project/.sdk/tm/ts/test/utility/PrimaryUtility.test.ts +6 -2
  153. package/project/.sdk/tm/ts/test/utility/StructUtility.test.ts +2 -2
  154. package/project/.sdk/tm/ts/test/utility.ts +122 -0
  155. package/project/.sdk/tm/ts/test/vendor/omni/Runner.ts +692 -0
  156. package/project/.sdk/tm/ts/test/vendor/omni/Util.ts +238 -0
  157. package/project/.sdk/tm/ts/test/vendor/omni/compat.ts +188 -0
  158. package/project/.sdk/tm/ts/test/vendor/omni/index.ts +38 -0
  159. package/project/.sdk/tm/zig/core/utility.zig +23 -0
  160. package/project/sdkgen-package.json +2 -1
  161. package/src/action/check.ts +18 -0
  162. package/src/cmp/AgentGuide.ts +1 -1
  163. package/src/cmp/AgentGuideContent.ts +9 -2
  164. package/src/cmp/Feature.ts +19 -0
  165. package/src/cmp/FeatureDocs.ts +7 -1
  166. package/src/cmp/ReadmeExplanation.ts +4 -1
  167. package/src/cmp/ReadmeFeatures.ts +1 -1
  168. package/src/cmp/ReadmeRefFeatures.ts +1 -1
  169. package/src/helpers/applicability.ts +123 -0
  170. package/src/helpers/collectDeps.ts +5 -1
  171. package/src/helpers/serverVars.ts +19 -0
  172. package/src/sdkgen.ts +9 -1
  173. package/src/utility.ts +10 -1
  174. package/project/.sdk/tm/ts/test/runner.ts +0 -425
@@ -0,0 +1,692 @@
1
+ // VENDORED: @voxgig/omni 0.1.2 (typescript/src/Runner.ts)
2
+ // Source: https://github.com/voxgig/omni @ bc9535d655564c0833f6eff003b0b13dad8b350f
3
+ // License: MIT (c) voxgig - see repository LICENSE. Do not edit: resync from upstream.
4
+ // Omni: the shared multi-language test runner.
5
+ //
6
+ // A test spec is plain JSON. The same spec file drives the same tests in
7
+ // every language that ships an omni port, so behaviour is defined once and
8
+ // verified everywhere.
9
+ //
10
+ // This file is CANONICAL. Every other port is a translation of it.
11
+
12
+ import { readFileSync } from 'node:fs'
13
+
14
+ import {
15
+ EXISTSMARK,
16
+ Json,
17
+ NULLMARK,
18
+ UNDEFMARK,
19
+ clone,
20
+ deepequal,
21
+ getpath,
22
+ islist,
23
+ ismap,
24
+ isnode,
25
+ pathify,
26
+ stringify,
27
+ walk,
28
+ } from './Util'
29
+
30
+ // A test subject: the function under test.
31
+ export type Subject = (...args: Json[]) => any
32
+
33
+ // Run-time options for a set of test entries.
34
+ export type Flags = {
35
+ // Convert JSON nulls to the NULLMARK sentinel (default: true).
36
+ null?: boolean
37
+ // Label used in failure messages (default: the runner name).
38
+ name?: string
39
+ [flag: string]: any
40
+ }
41
+
42
+ // The host of the system under test. Every hook is optional: a spec that
43
+ // resolves its subjects explicitly needs no provider at all.
44
+ export type Provider = {
45
+ // Resolve a test subject by name.
46
+ subject?: (name: string) => Subject | undefined | null
47
+ // Build a sub-provider from a spec DEF.client entry's options.
48
+ client?: (options: Json) => Provider | Promise<Provider>
49
+ // Wrap a map argument as a call context before it is passed in.
50
+ contextify?: (val: Json) => Json
51
+ // Resolve references in client options against the runner store.
52
+ inject?: (options: Json, store: Json) => Json
53
+ }
54
+
55
+ export type RunSet = (testspec: Json, testsubject?: Subject) => Promise<void>
56
+
57
+ export type RunSetFlags = (
58
+ testspec: Json,
59
+ flags: Flags,
60
+ testsubject?: Subject,
61
+ ) => Promise<void>
62
+
63
+ export type RunPack = {
64
+ // The resolved spec (the named section, or the whole file).
65
+ spec: Json
66
+ // Run one set of test entries.
67
+ runset: RunSet
68
+ // Run one set of test entries with flags.
69
+ runsetflags: RunSetFlags
70
+ // The default subject, if the provider could resolve one.
71
+ subject?: Subject
72
+ // The root provider.
73
+ client?: Provider
74
+ }
75
+
76
+ export type Runner = (name?: string, store?: Json) => Promise<RunPack>
77
+
78
+ // The newest spec format version this runner understands. A spec with no
79
+ // OMNI block is version 0: the original, lenient format, frozen forever.
80
+ // Version 1 turns on strict entry validation (see checkentry).
81
+ export const SPECVERSION = 1
82
+
83
+ // Capability strings this runner supports beyond the version baseline. A
84
+ // spec's OMNI.requires list is checked against this: an unknown capability
85
+ // refuses the spec loudly at load time, instead of a lagging port silently
86
+ // mis-running it. (Empty today; future format features mint a string here.)
87
+ export const CAPABILITIES: string[] = []
88
+
89
+ // The complete set of fields an entry may carry. Under version 1 anything
90
+ // else is an error: an unrecognised key is almost always a typo'd
91
+ // assertion, and a typo'd assertion is a test that silently stopped
92
+ // testing.
93
+ const ENTRYFIELDS = ['in', 'args', 'ctx', 'out', 'err', 'match', 'client', 'id', 'doc']
94
+
95
+ // A test failure (or a malformed spec). Distinct from errors thrown by the
96
+ // subject under test, which are candidates for an `err` expectation.
97
+ export class OmniError extends Error {
98
+ entry?: Json
99
+ constructor(message: string, entry?: Json) {
100
+ super(message)
101
+ this.name = 'OmniError'
102
+ this.entry = entry
103
+ }
104
+ }
105
+
106
+ // Load a spec: either a path to a JSON file, or an already-parsed object.
107
+ function loadspec(specref: string | Json): Json {
108
+ if ('string' === typeof specref) {
109
+ return JSON.parse(readFileSync(specref, 'utf8'))
110
+ }
111
+ return specref
112
+ }
113
+
114
+ // Read the spec's format version from its optional top-level OMNI block,
115
+ // and refuse a spec this runner cannot faithfully run: a version newer
116
+ // than SPECVERSION, or a required capability not in CAPABILITIES.
117
+ function resolveversion(alltests: Json): number {
118
+ const meta = ismap(alltests) ? alltests.OMNI : undefined
119
+
120
+ if (undefined === meta) {
121
+ return 0
122
+ }
123
+
124
+ if (!ismap(meta) || 'number' !== typeof meta.version || 0 !== meta.version % 1) {
125
+ throw new OmniError('omni: malformed OMNI version block')
126
+ }
127
+
128
+ if (meta.version < 0 || SPECVERSION < meta.version) {
129
+ throw new OmniError('omni: unsupported spec version: ' + meta.version)
130
+ }
131
+
132
+ const requires = meta.requires
133
+ if (undefined !== requires) {
134
+ if (!islist(requires)) {
135
+ throw new OmniError('omni: malformed OMNI requires list')
136
+ }
137
+ for (const cap of requires) {
138
+ if ('string' !== typeof cap || !CAPABILITIES.includes(cap)) {
139
+ throw new OmniError('omni: spec requires unsupported capability: ' + stringify(cap))
140
+ }
141
+ }
142
+ }
143
+
144
+ return meta.version
145
+ }
146
+
147
+ // Strict entry validation, applied when the spec declares version 1 or
148
+ // later. The lenient format converts each of these mistakes into a silent
149
+ // pass or a dead field; here they fail with the entry named.
150
+ function checkentry(flags: Flags, index: number, entry: Json): void {
151
+ if (!ismap(entry)) {
152
+ throw fail(flags, index, entry, 'entry is not a map')
153
+ }
154
+
155
+ for (const key of Object.keys(entry)) {
156
+ if (!ENTRYFIELDS.includes(key)) {
157
+ throw fail(flags, index, entry, 'unknown entry field: ' + key)
158
+ }
159
+ }
160
+
161
+ let argsources = 0
162
+ for (const key of ['in', 'args', 'ctx']) {
163
+ if (undefined !== entry[key]) {
164
+ argsources++
165
+ }
166
+ }
167
+ if (1 < argsources) {
168
+ throw fail(flags, index, entry, 'entry has more than one of in, args, ctx')
169
+ }
170
+
171
+ if (null != entry.err && undefined !== entry.out) {
172
+ throw fail(flags, index, entry, 'entry has both err and out')
173
+ }
174
+
175
+ if (undefined !== entry.id && 'string' !== typeof entry.id) {
176
+ throw fail(flags, index, entry, 'entry id is not a string')
177
+ }
178
+ }
179
+
180
+ // Validate a version-1 group up front, against the AUTHORED entries -
181
+ // null-normalisation would otherwise rewrite an authored null (e.g.
182
+ // id: null) into a sentinel string and hide it from validation. A
183
+ // malformed spec is a spec error, not a test result, so it fails before
184
+ // any subject runs.
185
+ function checkset(flags: Flags, testspec: Json, normalset: Json[]): void {
186
+ const origset =
187
+ ismap(testspec) && islist(testspec.set) ? testspec.set : normalset
188
+
189
+ if (0 === origset.length && true !== (ismap(testspec) ? testspec.empty : undefined)) {
190
+ throw new OmniError('omni: empty test set: ' + flags.name)
191
+ }
192
+
193
+ for (let index = 0; index < origset.length; index++) {
194
+ checkentry(flags, index, origset[index])
195
+ }
196
+ }
197
+
198
+ // Find the named section of a spec: `primary.<name>`, then `<name>`, then
199
+ // the whole spec.
200
+ function resolvespec(name: string | undefined, alltests: Json): Json {
201
+ if (null == name) {
202
+ return alltests
203
+ }
204
+
205
+ const primary = alltests.primary
206
+ if (ismap(primary) && null != primary[name]) {
207
+ return primary[name]
208
+ }
209
+
210
+ if (ismap(alltests) && null != alltests[name]) {
211
+ return alltests[name]
212
+ }
213
+
214
+ return alltests
215
+ }
216
+
217
+ // Build the named clients declared by the spec's DEF.client block.
218
+ async function resolveclients(
219
+ provider: Provider,
220
+ spec: Json,
221
+ store: Json,
222
+ ): Promise<Record<string, Provider>> {
223
+ const clients: Record<string, Provider> = {}
224
+
225
+ const defclient = ismap(spec) && ismap(spec.DEF) ? spec.DEF.client : undefined
226
+ if (!ismap(defclient)) {
227
+ return clients
228
+ }
229
+
230
+ // A spec may define clients that a given test run never references.
231
+ if (null == provider.client) {
232
+ return clients
233
+ }
234
+
235
+ for (const clientname of Object.keys(defclient)) {
236
+ const cdef = defclient[clientname]
237
+ const copts = clone((ismap(cdef) && ismap(cdef.test) ? cdef.test.options : undefined) || {})
238
+
239
+ if (ismap(store) && null != provider.inject) {
240
+ provider.inject(copts, store)
241
+ }
242
+
243
+ clients[clientname] = await provider.client(copts)
244
+ }
245
+
246
+ return clients
247
+ }
248
+
249
+ function resolvesubject(name: string | undefined, provider: Provider): Subject | undefined {
250
+ if (null == name || null == provider.subject) {
251
+ return undefined
252
+ }
253
+ return provider.subject(name) || undefined
254
+ }
255
+
256
+ function resolveflags(flags?: Flags): Flags {
257
+ const out: Flags = null == flags ? {} : { ...flags }
258
+ out.null = null == out.null ? true : !!out.null
259
+ return out
260
+ }
261
+
262
+ // An entry with no `out` expects a null (or absent) result.
263
+ function resolveentry(entry: Json, flags: Flags): Json {
264
+ if (null == entry.out && flags.null) {
265
+ entry.out = NULLMARK
266
+ }
267
+ return entry
268
+ }
269
+
270
+ function resolvetestpack(
271
+ name: string | undefined,
272
+ entry: Json,
273
+ subject: Subject,
274
+ provider: Provider,
275
+ clients: Record<string, Provider>,
276
+ ) {
277
+ const testpack = { client: provider, subject }
278
+
279
+ if (null != entry.client) {
280
+ const client = clients[entry.client]
281
+ if (null == client) {
282
+ throw new OmniError('omni: unknown client: ' + entry.client, entry)
283
+ }
284
+ testpack.client = client
285
+ testpack.subject = resolvesubject(name, client) || subject
286
+ }
287
+
288
+ return testpack
289
+ }
290
+
291
+ // Build the argument list for one entry: `ctx` (single context argument),
292
+ // `args` (explicit list), or `in` (single value).
293
+ function resolveargs(entry: Json, testpack: { client: Provider }, provider: Provider): Json[] {
294
+ let args: Json[]
295
+
296
+ if (undefined !== entry.ctx) {
297
+ args = [entry.ctx]
298
+ } else if (undefined !== entry.args) {
299
+ args = entry.args
300
+ } else {
301
+ args = [clone(entry.in)]
302
+ }
303
+
304
+ if (undefined !== entry.ctx || undefined !== entry.args) {
305
+ let first = args[0]
306
+ if (ismap(first)) {
307
+ first = clone(first)
308
+ if (null != provider.contextify) {
309
+ first = provider.contextify(first)
310
+ }
311
+ args[0] = first
312
+ entry.ctx = first
313
+ if (ismap(first)) {
314
+ first.client = testpack.client
315
+ }
316
+ }
317
+ }
318
+
319
+ return args
320
+ }
321
+
322
+ // Normalise a value for comparison: JSON nulls (and absent values) become
323
+ // the NULLMARK sentinel, errors become {name,message} maps. Always returns
324
+ // a fresh copy, so a spec can be run repeatedly.
325
+ function fixjson(val: Json, flags?: Flags): Json {
326
+ const donull = null == flags || null == flags.null ? true : !!flags.null
327
+ return fixjsonval(val, donull)
328
+ }
329
+
330
+ function fixjsonval(val: Json, donull: boolean): Json {
331
+ if (null == val) {
332
+ return donull ? NULLMARK : val
333
+ }
334
+
335
+ if (val instanceof Error) {
336
+ return errify(val)
337
+ }
338
+
339
+ if (islist(val)) {
340
+ return val.map((entry: Json) => fixjsonval(entry, donull))
341
+ }
342
+
343
+ if (ismap(val)) {
344
+ const out: Record<string, Json> = {}
345
+ for (const key of Object.keys(val)) {
346
+ out[key] = fixjsonval(val[key], donull)
347
+ }
348
+ return out
349
+ }
350
+
351
+ return val
352
+ }
353
+
354
+ // The JSON form of an error: always at least {name,message}.
355
+ // PATCH (solardemo prototype, pending upstream fix), here and in
356
+ // errmessage below: handle error-shaped MAPS. The shared corpus throws
357
+ // plain objects in some ports (this SDK's makeError rethrows the
358
+ // fixture's ctrl.err verbatim), and the struct repos' original runner
359
+ // matched err.message regardless of the thrown value's class. Upstream
360
+ // collapses any non-Error to String(err) — '[object Object]' — which
361
+ // fails both the err check and every match.err.* leaf.
362
+ function errify(err: any): Json {
363
+ if (err instanceof Error) {
364
+ return { ...err, name: err.name, message: err.message }
365
+ }
366
+ if (null != err && 'object' === typeof err) {
367
+ return { name: 'Error', ...err }
368
+ }
369
+ return { name: 'Error', message: String(err) }
370
+ }
371
+
372
+ function errmessage(err: any): string {
373
+ return err instanceof Error ? err.message
374
+ : null != err && 'string' === typeof err.message ? err.message
375
+ : String(err)
376
+ }
377
+
378
+ // The label of one entry, for failure messages.
379
+ function entryref(flags: Flags, index: number, entry: Json): string {
380
+ const label = flags.name || 'set'
381
+ const id = null != entry && null != entry.id ? ' (' + entry.id + ')' : ''
382
+ return label + '[' + index + ']' + id
383
+ }
384
+
385
+ function fail(
386
+ flags: Flags,
387
+ index: number,
388
+ entry: Json,
389
+ reason: string,
390
+ expected?: string,
391
+ actual?: string,
392
+ ): OmniError {
393
+ let msg = 'omni: ' + entryref(flags, index, entry) + ': ' + reason
394
+ if (undefined !== expected) {
395
+ msg += '\n expected: ' + expected
396
+ }
397
+ if (undefined !== actual) {
398
+ msg += '\n actual: ' + actual
399
+ }
400
+ msg += '\n entry: ' + stringify(entrysummary(entry))
401
+ return new OmniError(msg, entry)
402
+ }
403
+
404
+ // The spec-defined part of an entry (drop runner bookkeeping).
405
+ function entrysummary(entry: Json): Json {
406
+ if (!ismap(entry)) {
407
+ return entry
408
+ }
409
+ const out: Record<string, Json> = {}
410
+ for (const key of Object.keys(entry)) {
411
+ if ('res' !== key && 'thrown' !== key && 'ctx' !== key) {
412
+ out[key] = entry[key]
413
+ }
414
+ }
415
+ return out
416
+ }
417
+
418
+ function checkresult(flags: Flags, index: number, entry: Json, args: Json[], res: Json) {
419
+ let matched = false
420
+
421
+ if (null != entry.err) {
422
+ throw fail(
423
+ flags,
424
+ index,
425
+ entry,
426
+ 'expected error did not occur',
427
+ stringify(entry.err),
428
+ stringify(res),
429
+ )
430
+ }
431
+
432
+ if (null != entry.match) {
433
+ match(flags, index, entry, entry.match, {
434
+ in: entry.in,
435
+ args,
436
+ out: entry.res,
437
+ ctx: entry.ctx,
438
+ })
439
+ matched = true
440
+ }
441
+
442
+ const out = entry.out
443
+
444
+ if (out === res) {
445
+ return
446
+ }
447
+
448
+ // NOTE: a match with no explicit out is a complete check on its own.
449
+ if (matched && (NULLMARK === out || null == out)) {
450
+ return
451
+ }
452
+
453
+ if (!deepequal(res, out)) {
454
+ throw fail(flags, index, entry, 'result mismatch', stringify(out), stringify(res))
455
+ }
456
+ }
457
+
458
+ function handleerror(flags: Flags, index: number, entry: Json, err: any) {
459
+ entry.thrown = err
460
+
461
+ const entryerr = entry.err
462
+
463
+ if (null != entryerr) {
464
+ if (true === entryerr || matchval(entryerr, errmessage(err))) {
465
+ if (null != entry.match) {
466
+ match(flags, index, entry, entry.match, {
467
+ in: entry.in,
468
+ out: entry.res,
469
+ ctx: entry.ctx,
470
+ err: errify(err),
471
+ })
472
+ }
473
+ return
474
+ }
475
+
476
+ throw fail(flags, index, entry, 'error mismatch', stringify(entryerr), errmessage(err))
477
+ }
478
+
479
+ throw fail(flags, index, entry, 'unexpected error', undefined, errmessage(err))
480
+ }
481
+
482
+ // Check that every leaf of `check` is present, and matches, in `base`.
483
+ function match(flags: Flags, index: number, entry: Json, check: Json, base: Json) {
484
+ // PATCH (solardemo prototype, pending upstream fix): read the base
485
+ // directly instead of cloning it. The clone gained nothing (the walk
486
+ // below only READS via getpath) and blew the stack on cyclic bases —
487
+ // which every corpus ctx entry produces, because a live Context reaches
488
+ // its client and the client's root context reaches the client again.
489
+ const cbase = base
490
+
491
+ const at = (path: Json[]) => (0 === path.length ? '<root>' : pathify(path))
492
+
493
+ walk(clone(check), (_key, val, _parent, path) => {
494
+ // An empty container in the check is a structural placeholder: it has
495
+ // no leaves to check, so it matches whatever is at that path. This is
496
+ // deliberate - voxgig/struct's corpus relies on it (struct-compat), and
497
+ // an empty sub-pattern matching anything is the usual partial-match
498
+ // convention. The leaf checks below are where the strictness lives.
499
+ if (!isnode(val)) {
500
+ const baseval = getpath(cbase, path)
501
+
502
+ // The sentinels are tested BEFORE the identity check below. Otherwise
503
+ // a subject returning the literal string "__UNDEF__" satisfies an
504
+ // assertion that the key is absent - two mutually exclusive states
505
+ // passing one check. A sentinel that accepts its own literal is not a
506
+ // sentinel. (NULLMARK still accepts NULLMARK: under the default null
507
+ // flag a real null has already been normalised to it, so the two are
508
+ // genuinely indistinguishable here - that one needs a raw-value
509
+ // escape, not an ordering change.)
510
+
511
+ // Explicitly absent: satisfied only by a genuinely missing key, never
512
+ // by a present null (the distinction the sentinels exist to keep).
513
+ if (UNDEFMARK === val) {
514
+ if (undefined === baseval) {
515
+ return val
516
+ }
517
+ throw fail(flags, index, entry, 'expected absent at ' + at(path), 'absent', stringify(baseval))
518
+ }
519
+
520
+ // Explicitly null: satisfied only by a present null.
521
+ if (NULLMARK === val) {
522
+ if (null === baseval || NULLMARK === baseval) {
523
+ return val
524
+ }
525
+ throw fail(flags, index, entry, 'expected null at ' + at(path), 'null', stringify(baseval))
526
+ }
527
+
528
+ // Explicitly present: any present value, including null.
529
+ if (EXISTSMARK === val) {
530
+ if (undefined !== baseval) {
531
+ return val
532
+ }
533
+ throw fail(flags, index, entry, 'expected present at ' + at(path), 'present', 'absent')
534
+ }
535
+
536
+ // Identical values match. This sits below the sentinel branches on
537
+ // purpose - see the note above.
538
+ if (baseval === val) {
539
+ return val
540
+ }
541
+
542
+ // A concrete expectation never matches a missing key - a match leaf
543
+ // against an absent value must fail, not substring-match "undefined".
544
+ if (undefined === baseval) {
545
+ throw fail(flags, index, entry, 'match failed at ' + at(path), stringify(val), 'absent')
546
+ }
547
+
548
+ if (!matchval(val, baseval)) {
549
+ throw fail(flags, index, entry, 'match failed at ' + at(path), stringify(val), stringify(baseval))
550
+ }
551
+ }
552
+
553
+ return val
554
+ })
555
+ }
556
+
557
+
558
+ // Match one leaf. Strings are matched as /regex/ or as a case-insensitive
559
+ // substring, which is what makes error-message expectations portable.
560
+ function matchval(check: Json, base: Json): boolean {
561
+ if (check === base) {
562
+ return true
563
+ }
564
+
565
+ let want = check
566
+ if (UNDEFMARK === want || NULLMARK === want) {
567
+ want = null
568
+ }
569
+
570
+ if (null == want) {
571
+ return null == base || NULLMARK === base
572
+ }
573
+
574
+ if ('string' === typeof want) {
575
+ // An empty want is not a wildcard: the empty string is a substring of
576
+ // everything, so `match:{out:""}` (or `err:""`) would accept any value.
577
+ if ('' === want) {
578
+ return '' === base
579
+ }
580
+
581
+ const basestr = stringify(base)
582
+
583
+ const rem = want.match(/^\/(.+)\/$/)
584
+ if (rem) {
585
+ return new RegExp(rem[1]).test(basestr)
586
+ }
587
+
588
+ return basestr.toLowerCase().includes(want.toLowerCase())
589
+ }
590
+
591
+ if ('function' === typeof want) {
592
+ return true
593
+ }
594
+
595
+ return deepequal(want, base)
596
+ }
597
+
598
+ // Convert NULLMARK sentinels back into real nulls. Use as a walk callback
599
+ // when a spec value must reach the subject as an actual null.
600
+ function nullmodifier(val: Json, key: any, parent: Json) {
601
+ if (NULLMARK === val) {
602
+ parent[key] = null
603
+ } else if ('string' === typeof val) {
604
+ parent[key] = val.split(NULLMARK).join('null')
605
+ }
606
+ }
607
+
608
+ // Make a runner for a spec file (or spec object) and a provider.
609
+ async function makeRunner(specref: string | Json, provider?: Provider): Promise<Runner> {
610
+ const alltests = loadspec(specref)
611
+ const specversion = resolveversion(alltests)
612
+ const useprovider: Provider = provider || {}
613
+
614
+ return async function runner(name?: string, store?: Json): Promise<RunPack> {
615
+ const spec = resolvespec(name, alltests)
616
+ const clients = await resolveclients(useprovider, spec, store || {})
617
+ const defsubject = resolvesubject(name, useprovider)
618
+
619
+ const runsetflags: RunSetFlags = async (
620
+ testspec: Json,
621
+ flags: Flags,
622
+ testsubject?: Subject,
623
+ ) => {
624
+ const useflags = resolveflags(flags)
625
+ useflags.name = useflags.name || name || 'set'
626
+
627
+ const subject = testsubject || defsubject
628
+ if (null == subject) {
629
+ throw new OmniError('omni: no test subject for: ' + useflags.name)
630
+ }
631
+
632
+ const testspecmap = fixjson(testspec, useflags)
633
+
634
+ if (!ismap(testspecmap) || !islist(testspecmap.set)) {
635
+ throw new OmniError('omni: test spec has no set: ' + useflags.name)
636
+ }
637
+
638
+ const testset: Json[] = testspecmap.set
639
+
640
+ if (1 <= specversion) {
641
+ checkset(useflags, testspec, testset)
642
+ }
643
+
644
+ for (let index = 0; index < testset.length; index++) {
645
+ let entry = testset[index]
646
+
647
+ try {
648
+ entry = resolveentry(entry, useflags)
649
+
650
+ const testpack = resolvetestpack(name, entry, subject, useprovider, clients)
651
+ const args = resolveargs(entry, testpack, useprovider)
652
+
653
+ let res = await testpack.subject(...args)
654
+ res = fixjson(res, useflags)
655
+ entry.res = res
656
+
657
+ checkresult(useflags, index, entry, args, res)
658
+ } catch (err: any) {
659
+ if (err instanceof OmniError) {
660
+ throw err
661
+ }
662
+ handleerror(useflags, index, entry, err)
663
+ }
664
+ }
665
+ }
666
+
667
+ const runset: RunSet = async (testspec: Json, testsubject?: Subject) =>
668
+ runsetflags(testspec, {}, testsubject)
669
+
670
+ return {
671
+ spec,
672
+ runset,
673
+ runsetflags,
674
+ subject: defsubject,
675
+ client: useprovider,
676
+ }
677
+ }
678
+ }
679
+
680
+ export {
681
+ EXISTSMARK,
682
+ NULLMARK,
683
+ UNDEFMARK,
684
+ errify,
685
+ fixjson,
686
+ loadspec,
687
+ makeRunner,
688
+ match,
689
+ matchval,
690
+ nullmodifier,
691
+ resolvespec,
692
+ }