@voxgig/sdkgen 4.16.0 → 4.17.1

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 (107) hide show
  1. package/README.md +8 -0
  2. package/bin/voxgig-sdkgen +2 -2
  3. package/dist/action/action.js +2 -2
  4. package/dist/action/action.js.map +1 -1
  5. package/dist/action/check.js +9 -8
  6. package/dist/action/check.js.map +1 -1
  7. package/dist/action/dispatch.js +4 -4
  8. package/dist/action/dispatch.js.map +1 -1
  9. package/dist/action/doctor.js +16 -15
  10. package/dist/action/doctor.js.map +1 -1
  11. package/dist/action/edition.d.ts +4 -0
  12. package/dist/action/{docs.js → edition.js} +57 -54
  13. package/dist/action/edition.js.map +1 -0
  14. package/dist/action/kind.d.ts +1 -2
  15. package/dist/action/kind.js +22 -38
  16. package/dist/action/kind.js.map +1 -1
  17. package/dist/action/package.js +17 -16
  18. package/dist/action/package.js.map +1 -1
  19. package/dist/action/resolve.d.ts +3 -1
  20. package/dist/action/resolve.js +29 -11
  21. package/dist/action/resolve.js.map +1 -1
  22. package/dist/admin/status.d.ts +43 -0
  23. package/dist/admin/status.js +161 -0
  24. package/dist/admin/status.js.map +1 -0
  25. package/dist/cmp/Test.js +11 -0
  26. package/dist/cmp/Test.js.map +1 -1
  27. package/dist/helpers/kindCollection.d.ts +1 -0
  28. package/dist/helpers/kindCollection.js +14 -0
  29. package/dist/helpers/kindCollection.js.map +1 -0
  30. package/dist/helpers/modelcheck.d.ts +1 -1
  31. package/dist/helpers/modelcheck.js +13 -3
  32. package/dist/helpers/modelcheck.js.map +1 -1
  33. package/dist/helpers/shipped.js +2 -3
  34. package/dist/helpers/shipped.js.map +1 -1
  35. package/dist/helpers/testPolicy.d.ts +1 -1
  36. package/dist/helpers/testPolicy.js +6 -37
  37. package/dist/helpers/testPolicy.js.map +1 -1
  38. package/dist/sdkgen.d.ts +1 -1
  39. package/dist/sdkgen.js +22 -89
  40. package/dist/sdkgen.js.map +1 -1
  41. package/dist/tsconfig.tsbuildinfo +1 -1
  42. package/dist/utility.js +9 -1
  43. package/dist/utility.js.map +1 -1
  44. package/model/sdkgen.aon +5 -52
  45. package/package.json +1 -1
  46. package/project/.sdk/src/cmp/go/Config_go.ts +4 -4
  47. package/project/.sdk/src/cmp/go/utility_go.ts +10 -2
  48. package/project/.sdk/src/cmp/js/Package_js.ts +5 -1
  49. package/project/.sdk/src/cmp/js/TestDirect_js.ts +11 -7
  50. package/project/.sdk/src/cmp/js/TestEntity_js.ts +16 -2
  51. package/project/.sdk/src/cmp/js/TestLive_js.ts +32 -0
  52. package/project/.sdk/src/cmp/js/Test_js.ts +2 -0
  53. package/project/.sdk/src/cmp/js/fragment/Direct.test.fragment.js +1 -1
  54. package/project/.sdk/src/cmp/js/fragment/Entity.test.fragment.js +4 -2
  55. package/project/.sdk/src/cmp/php/ReadmeExamplesTest_php.ts +30 -9
  56. package/project/.sdk/src/cmp/php/ReadmeHowto_php.ts +8 -3
  57. package/project/.sdk/src/cmp/php/ReadmeQuick_php.ts +5 -4
  58. package/project/.sdk/src/cmp/php/ReadmeTopQuick_php.ts +2 -2
  59. package/project/.sdk/src/cmp/ts/Package_ts.ts +5 -1
  60. package/project/.sdk/src/cmp/ts/ReadmeHowto_ts.ts +5 -0
  61. package/project/.sdk/src/cmp/ts/TestDirect_ts.ts +15 -11
  62. package/project/.sdk/src/cmp/ts/TestEntity_ts.ts +13 -16
  63. package/project/.sdk/src/cmp/ts/TestLive_ts.ts +32 -0
  64. package/project/.sdk/src/cmp/ts/Test_ts.ts +2 -0
  65. package/project/.sdk/src/cmp/ts/fragment/Direct.test.fragment.ts +1 -0
  66. package/project/.sdk/src/cmp/ts/fragment/Entity.test.fragment.ts +2 -0
  67. package/project/.sdk/tm/c/Makefile +16 -11
  68. package/project/.sdk/tm/c/feature/secrets/sekreto/json.c +3 -3
  69. package/project/.sdk/tm/csharp/test/feature/secrets/SecretsFeatureTest.cs +9 -2
  70. package/project/.sdk/tm/js/test/live-contract.js +183 -0
  71. package/project/.sdk/tm/js/test/live-entity.js +169 -0
  72. package/project/.sdk/tm/js/test/live-runner.js +129 -0
  73. package/project/.sdk/tm/js/test/live-scenarios.js +142 -0
  74. package/project/.sdk/tm/js/test/utility.js +84 -0
  75. package/project/.sdk/tm/php/test/feature/secrets/SecretsTest.php +22 -20
  76. package/project/.sdk/tm/ts/test/live-contract.ts +104 -0
  77. package/project/.sdk/tm/ts/test/live-entity.ts +142 -0
  78. package/project/.sdk/tm/ts/test/live-runner.ts +166 -0
  79. package/project/.sdk/tm/ts/test/live-scenarios.ts +112 -0
  80. package/project/.sdk/tm/ts/test/utility.ts +1 -2
  81. package/project/sdkgen-package.json +1 -1
  82. package/src/action/action.ts +2 -2
  83. package/src/action/check.ts +3 -2
  84. package/src/action/dispatch.ts +4 -4
  85. package/src/action/doctor.ts +10 -9
  86. package/src/action/{docs.ts → edition.ts} +56 -53
  87. package/src/action/kind.ts +12 -33
  88. package/src/action/package.ts +4 -3
  89. package/src/action/resolve.ts +26 -2
  90. package/src/admin/status.ts +124 -0
  91. package/src/cmp/Test.ts +13 -1
  92. package/src/helpers/kindCollection.ts +11 -0
  93. package/src/helpers/modelcheck.ts +10 -3
  94. package/src/helpers/shipped.ts +2 -3
  95. package/src/helpers/testPolicy.ts +7 -45
  96. package/src/sdkgen.ts +15 -94
  97. package/src/utility.ts +8 -2
  98. package/dist/action/docs.d.ts +0 -4
  99. package/dist/action/docs.js.map +0 -1
  100. package/dist/cmp/Docs.d.ts +0 -4
  101. package/dist/cmp/Docs.js +0 -98
  102. package/dist/cmp/Docs.js.map +0 -1
  103. package/dist/cmp/ExternalDocs.d.ts +0 -2
  104. package/dist/cmp/ExternalDocs.js +0 -43
  105. package/dist/cmp/ExternalDocs.js.map +0 -1
  106. package/src/cmp/Docs.ts +0 -125
  107. package/src/cmp/ExternalDocs.ts +0 -54
@@ -0,0 +1,142 @@
1
+ import assert from 'node:assert/strict'
2
+ import { requestContract, synthesizeInput } from './live-contract'
3
+ import { runLiveSteps, assertLiveReport, LiveBlocked } from './live-runner'
4
+ import { isControlSkipped, liveDelayMs } from './utility'
5
+
6
+ // The offline flow keeps its deterministic fixture assertions. Live flows
7
+ // resolve real prerequisites per operation and collect failures until done.
8
+ async function runLiveEntity(setup: any, entity: any, flow: any, accessor: string) {
9
+ const { client, transport } = setup
10
+ const steps: any[] = flow.step || []
11
+ const created = new Map<string, any>()
12
+ const listed: any[] = []
13
+ const marks = new Map<string, { name: string, value: any }>()
14
+ const idField = entity.id?.field || 'id'
15
+ const copy = (value: any) => JSON.parse(JSON.stringify(value ?? {}))
16
+ const hasCreate = steps.some(step => step.op === 'create')
17
+ const report = await runLiveSteps(steps.map((step, index) => {
18
+ const ref = step.input?.ref || entity.name + '_ref01'
19
+ const op = step.op
20
+ const excluded = isControlSkipped('entityOp', entity.name + '.' + op, 'live')
21
+ return {
22
+ id: entity.name + '.' + op + '.' + index,
23
+ cleanup: op === 'remove' || (step.valid || []).some((v: any) => v.apply === 'ItemNotExists'),
24
+ excluded: excluded.skip ? excluded.reason || 'Excluded by test control' : undefined,
25
+ run: async (context: any) => {
26
+ transport.enter(context)
27
+ const points = entity.op?.[op]?.points || []
28
+ if (!points.length) throw new LiveBlocked('No modelled operation point')
29
+ const record = created.get(ref)
30
+ // A failed create must not turn a later update/remove into a write
31
+ // against an arbitrary record found by a list operation.
32
+ if ((op === 'update' || op === 'remove') && hasCreate && !record) {
33
+ throw new LiveBlocked('Create did not provide a usable resource')
34
+ }
35
+ if (op === 'remove' && !record) throw new LiveBlocked('No resource created by this run')
36
+ if (op === 'remove' && !entity.id) throw new LiveBlocked('No modelled resource identity for cleanup')
37
+ if (record && ['update', 'remove'].includes(op) && entity.id &&
38
+ null == (record[idField] ?? record.id)) {
39
+ throw new LiveBlocked('Created resource has no usable identity')
40
+ }
41
+
42
+ let input: any = op === 'create'
43
+ ? copy(setup.data.new?.[entity.name]?.[ref]) : {}
44
+ for (const [name, binding] of Object.entries({ ...step.match, ...step.data })) {
45
+ const value = setup.idmap[binding as string] ?? setup.idmap[name]
46
+ if (undefined !== value) input[name] = value
47
+ }
48
+ const loaded = record || (op === 'load' ? listed[0] : undefined)
49
+ if (loaded && ['load', 'update', 'remove'].includes(op)) {
50
+ const id = loaded[idField] ?? loaded.id
51
+ if (undefined !== id) input.id = id
52
+ }
53
+ // A nested route needing a parent id must not hide an available
54
+ // parameter-free route for the same operation. Use the first viable
55
+ // candidate; the SDK still performs its normal route selection.
56
+ let resolved: any
57
+ let selected: any
58
+ const missing = new Set<string>()
59
+ for (const point of points) {
60
+ if (point.select?.$action !== input.$action) continue
61
+ const candidate = { ...input }
62
+ let viable = true
63
+ const params = point.args?.params || []
64
+ const query = (point.args?.query || []).filter((arg: any) => arg.reqd)
65
+ for (const arg of [...params, ...query]) {
66
+ if (undefined !== candidate[arg.name] && null !== candidate[arg.name]) continue
67
+ const key = arg.name === 'id' ? entity.name + '01' : arg.name.replace(/_id$/, '') + '01'
68
+ const value = setup.idmap[key] ?? setup.idmap[arg.name] ?? loaded?.[arg.name] ?? arg.example
69
+ if (undefined !== value && null !== value) candidate[arg.name] = value
70
+ else if (arg.reqd !== false) { viable = false; missing.add(arg.name) }
71
+ }
72
+ if (viable) { resolved = candidate; selected = point; break }
73
+ }
74
+ if (!resolved) throw new LiveBlocked('No usable route; missing arguments: ' + [...missing].join(', '))
75
+ input = resolved
76
+ if (op === 'create' && selected.contract) {
77
+ const facts = JSON.parse(selected.contract.json)
78
+ const request = requestContract(facts)
79
+ if (request.schema) input = { ...synthesizeInput(request.schema, facts.live?.input ?? request.example) }
80
+ else if (facts.protocol === 'http') input = {}
81
+ for (const arg of selected.args?.params || []) if (resolved[arg.name] !== undefined) input[arg.name] = resolved[arg.name]
82
+ }
83
+ let intendedMark: { name: string, value: any } | undefined
84
+ if (op === 'update') {
85
+ for (const spec of step.spec || []) {
86
+ if (spec.apply === 'TextFieldMark' && step.input?.textfield) {
87
+ const mark = { name: step.input.textfield, value: spec.def.mark + '_' + setup.now }
88
+ input[mark.name] = mark.value
89
+ intendedMark = mark
90
+ }
91
+ }
92
+ }
93
+
94
+ // A fresh entity prevents state left by a failed operation from
95
+ // changing the request for the next independent operation.
96
+ const result = await client[accessor]()[op](input)
97
+ if (intendedMark) marks.set(ref, intendedMark)
98
+ if (op === 'list') {
99
+ assert(Array.isArray(result), 'Expected a list of entity instances')
100
+ const data = result.map((item: any) => {
101
+ assert.equal(typeof item?.data, 'function')
102
+ return item.data()
103
+ })
104
+ listed.splice(0, listed.length, ...data)
105
+ context.publish(data)
106
+ for (const validation of step.valid || []) {
107
+ const previous = created.get(validation.def?.ref)
108
+ const id = previous?.[idField] ?? previous?.id
109
+ if (undefined === id) continue
110
+ const found = data.some((item: any) => (item?.[idField] ?? item?.id) === id)
111
+ if (validation.apply === 'ItemExists') assert(found, 'Created resource missing from list')
112
+ if (validation.apply === 'ItemNotExists') assert(!found, 'Removed resource still in list')
113
+ }
114
+ }
115
+ else {
116
+ assert.equal(typeof result?.data, 'function', 'Expected an entity instance')
117
+ const data = result.data()
118
+ assert(null != data, 'Expected operation data')
119
+ // Publish before assertion checks: cleanup still needs the id
120
+ // when the server created a resource with incorrect field values.
121
+ if (op === 'create') created.set(ref, data)
122
+ context.publish(data)
123
+ if (op === 'create' && entity.id) assert(null != (data[idField] ?? data.id), 'Missing created identity')
124
+ if (['load', 'update'].includes(op) && input.id != null && entity.id) {
125
+ assert.equal(data[idField] ?? data.id, input.id, 'Response identity mismatch')
126
+ }
127
+ const mark = marks.get(ref)
128
+ if (mark && ['update', 'load'].includes(op)) {
129
+ assert.equal(data[mark.name], mark.value, 'Updated field mismatch')
130
+ }
131
+ }
132
+ },
133
+ }
134
+ }), {
135
+ delayMs: liveDelayMs(),
136
+ report: result => console.log('LIVE STEP ' + JSON.stringify(result)),
137
+ })
138
+ console.log('LIVE SUMMARY ' + JSON.stringify({ entity: entity.name, ...report }))
139
+ assertLiveReport(report)
140
+ }
141
+
142
+ export { runLiveEntity }
@@ -0,0 +1,166 @@
1
+ // Execute every independent live step before deciding the suite's result.
2
+ // Values never appear in reports: they can contain credentials or API data.
3
+
4
+ type LiveState = 'passed' | 'failed' | 'blocked' | 'excluded'
5
+
6
+ type LiveResult = {
7
+ id: string
8
+ state: LiveState
9
+ attempted: boolean
10
+ role?: string
11
+ retention?: string
12
+ reason?: string
13
+ requests?: { method: string, path: string, status?: number }[]
14
+ }
15
+
16
+ type LiveContext = {
17
+ values: Map<string, any>
18
+ publish(value: any): void
19
+ attempted(): void
20
+ requests: { method: string, path: string, status?: number }[]
21
+ }
22
+
23
+ type LiveStep = {
24
+ id: string
25
+ needs?: string[]
26
+ cleanup?: boolean
27
+ role?: string
28
+ retention?: string
29
+ excluded?: string
30
+ run(ctx: LiveContext): Promise<void>
31
+ }
32
+
33
+ class LiveBlocked extends Error {}
34
+
35
+ type LiveReport = {
36
+ planned: number
37
+ attempted: number
38
+ passed: number
39
+ failed: number
40
+ blocked: number
41
+ excluded: number
42
+ results: LiveResult[]
43
+ }
44
+
45
+ async function runLiveSteps(
46
+ steps: LiveStep[],
47
+ options: { delayMs?: number, report?: (result: LiveResult) => void } = {},
48
+ ): Promise<LiveReport> {
49
+ const values = new Map<string, any>()
50
+ const results: LiveResult[] = []
51
+ const ids = new Set<string>()
52
+ for (const step of steps) {
53
+ if (!step.id || ids.has(step.id)) throw new Error('Duplicate or empty live step id')
54
+ ids.add(step.id)
55
+ }
56
+
57
+ const record = (result: LiveResult) => {
58
+ const step = steps.find(step => step.id === result.id)
59
+ if (step?.role) result.role = step.role
60
+ if (step?.retention && result.attempted) result.retention = step.retention
61
+ results.push(result)
62
+ options.report?.(result)
63
+ }
64
+
65
+ // Cleanup runs after ordinary work, even if an assertion failed after a
66
+ // resource was created. A step publishes usable data before assertions.
67
+ for (const cleanup of [false, true]) {
68
+ const pending = steps.filter(step => !!step.cleanup === cleanup)
69
+ while (pending.length) {
70
+ let progressed = false
71
+ for (let i = 0; i < pending.length;) {
72
+ const step = pending[i]
73
+ const missing = (step.needs || []).filter(id => !values.has(id) ||
74
+ (!cleanup && results.some(result => result.id === id && result.state !== 'passed')))
75
+ if (!step.excluded && missing.some(id => pending.some(other => other.id === id))) {
76
+ i++
77
+ continue
78
+ }
79
+ pending.splice(i, 1)
80
+ progressed = true
81
+ if (step.excluded) {
82
+ record({ id: step.id, state: 'excluded', attempted: false, reason: step.excluded })
83
+ continue
84
+ }
85
+ if (missing.length) {
86
+ record({ id: step.id, state: 'blocked', attempted: false,
87
+ reason: 'Missing output: ' + missing.join(', ') })
88
+ continue
89
+ }
90
+ let attempted = false
91
+ const requests: LiveContext['requests'] = []
92
+ try {
93
+ await step.run({
94
+ values,
95
+ publish: value => { if (undefined !== value) values.set(step.id, value) },
96
+ attempted: () => { attempted = true },
97
+ requests,
98
+ })
99
+ if (!attempted) throw new LiveBlocked('No request attempted')
100
+ record({ id: step.id, state: 'passed', attempted, requests })
101
+ }
102
+ catch (error) {
103
+ // Ordinary SDK errors can contain request headers, bodies, or an
104
+ // issued token. Report the step's failure without serializing it.
105
+ record({ id: step.id, state: error instanceof LiveBlocked ? 'blocked' : 'failed',
106
+ attempted, requests,
107
+ reason: error instanceof LiveBlocked ? error.message : 'Request or assertion failed' })
108
+ }
109
+ if (attempted && (options.delayMs || 0) > 0) {
110
+ await new Promise(resolve => setTimeout(resolve, options.delayMs))
111
+ }
112
+ }
113
+ if (!progressed) {
114
+ for (const step of pending.splice(0)) {
115
+ record({ id: step.id, state: 'blocked', attempted: false,
116
+ reason: 'Cyclic or unavailable dependency' })
117
+ }
118
+ }
119
+ }
120
+ }
121
+
122
+ return {
123
+ planned: steps.length,
124
+ attempted: results.filter(result => result.attempted).length,
125
+ passed: results.filter(result => result.state === 'passed').length,
126
+ failed: results.filter(result => result.state === 'failed').length,
127
+ blocked: results.filter(result => result.state === 'blocked').length,
128
+ excluded: results.filter(result => result.state === 'excluded').length,
129
+ results,
130
+ }
131
+ }
132
+
133
+ function assertLiveReport(report: LiveReport): void {
134
+ if (report.failed || report.blocked || !report.attempted) {
135
+ throw new Error('Live coverage incomplete: ' + JSON.stringify(report))
136
+ }
137
+ }
138
+
139
+ function createLiveTransport(timeoutMs = 30000) {
140
+ let context: LiveContext | undefined
141
+ const requests: LiveContext['requests'] = []
142
+ const nativeFetch = globalThis.fetch
143
+ return {
144
+ requests,
145
+ enter(next: LiveContext) { context = next },
146
+ fetch: async (input: any, init?: any) => {
147
+ const url = new URL(String(input))
148
+ const request: LiveContext['requests'][number] = {
149
+ method: init?.method || 'GET', path: url.origin + url.pathname,
150
+ }
151
+ requests.push(request)
152
+ context?.requests.push(request)
153
+ context?.attempted()
154
+ if (!context) console.log('LIVE REQUEST ' + JSON.stringify(request))
155
+ const timeout = AbortSignal.timeout(timeoutMs)
156
+ const signal = init?.signal ? AbortSignal.any([init.signal, timeout]) : timeout
157
+ const response = await nativeFetch(input, { ...init, signal })
158
+ request.status = response.status
159
+ if (!context) console.log('LIVE RESPONSE ' + JSON.stringify(request))
160
+ return response
161
+ },
162
+ }
163
+ }
164
+
165
+ export { runLiveSteps, assertLiveReport, LiveBlocked, createLiveTransport }
166
+ export type { LiveStep, LiveContext, LiveReport, LiveResult }
@@ -0,0 +1,112 @@
1
+ import assert from 'node:assert/strict'
2
+ import { LiveBlocked, runLiveSteps, assertLiveReport, createLiveTransport } from './live-runner'
3
+ import { requestContract, synthesizeInput, validateContract } from './live-contract'
4
+ import { liveClientOptions, liveDelayMs, isControlSkipped } from './utility'
5
+
6
+ const at = (value: any, path?: string): any => (path || '').split('.').filter(Boolean).reduce((v, k) => v?.[k], value)
7
+ export function resolveRecipe(recipe: any, values: Map<string, any>): any {
8
+ if (Array.isArray(recipe)) return recipe.map(v => resolveRecipe(v, values))
9
+ if (recipe && typeof recipe === 'object') {
10
+ if (recipe.from) {
11
+ if (!values.has(recipe.from)) throw new LiveBlocked('Missing recipe output: ' + recipe.from)
12
+ let value = values.get(recipe.from)
13
+ if (recipe.where) {
14
+ if (!Array.isArray(value)) throw new LiveBlocked('Discovery output is not a list')
15
+ value = value.find((item: any) => Object.entries(recipe.where).every(([k, v]) => at(item, k) === v) &&
16
+ (!recipe.related || (values.get(recipe.related.from) || []).some((other: any) =>
17
+ at(item, recipe.related.local) === at(other, recipe.related.foreign) &&
18
+ Object.entries(recipe.related.where || {}).every(([k, v]) => at(other, k) === v))))
19
+ }
20
+ value = at(value, recipe.path)
21
+ if (value === undefined || value === null) throw new LiveBlocked('Discovery found no compatible value')
22
+ return value
23
+ }
24
+ return Object.fromEntries(Object.entries(recipe).map(([k, v]) => [k, resolveRecipe(v, values)]))
25
+ }
26
+ return recipe
27
+ }
28
+
29
+ export function recipeNeeds(value: any): string[] {
30
+ if (!value || typeof value !== 'object') return []
31
+ return [...new Set([...(typeof value.from === 'string' ? [value.from] : []), ...Object.values(value).flatMap(recipeNeeds)])]
32
+ }
33
+
34
+ export async function runLiveScenarios(SDK: any, plan: any[], envPrefix: string, liveDefaults: any = {}) {
35
+ const transport = createLiveTransport()
36
+ const steps = plan.map(point => {
37
+ const hint = point.facts.live || {}
38
+ const control = isControlSkipped('entityOp', point.entity + '.' + point.op, 'live')
39
+ return { role: hint.auth || (point.facts.security?.length === 0 || point.facts.securitySource === 'unspecified' ? 'public' : 'account'), retention: hint.retention, id: hint.id || point.id, needs: [...new Set([...(hint.needs || []), ...recipeNeeds(hint.input), ...recipeNeeds(hint.credential), ...recipeNeeds(hint.assert)])] as string[],
40
+ cleanup: !!hint.cleanup,
41
+ excluded: control.skip ? control.reason || 'Excluded by test control' : hint.excluded,
42
+ run: async (ctx: any) => {
43
+ transport.enter(ctx)
44
+ if (point.contractVersion && point.contractVersion !== 1) throw new LiveBlocked('Unsupported operation contract version')
45
+ if (point.op === 'remove' || hint.cleanup) {
46
+ const owned = recipeNeeds(hint.input).some(id => plan.some(source =>
47
+ (source.facts.live?.id || source.id) === id && source.entity === point.entity && source.op === 'create'))
48
+ if (!owned) throw new LiveBlocked('Cleanup input is not bound to a resource created by this run')
49
+ }
50
+ if (!point.reachable) throw new LiveBlocked('Point selector is indistinguishable; add a guide action')
51
+ const request = requestContract(point.facts)
52
+ const explicit = hint.input === undefined ? request.example : resolveRecipe(hint.input, ctx.values)
53
+ let input = request.schema ? synthesizeInput(request.schema, explicit) : explicit ?? {}
54
+ for (const kind of ['params', 'query', 'header', 'cookie']) for (const arg of point.args?.[kind] || []) {
55
+ if (arg.reqd && input[arg.name] === undefined) {
56
+ if (arg.example === undefined) throw new LiveBlocked('Missing required argument: ' + arg.name)
57
+ input[arg.name] = arg.example
58
+ }
59
+ }
60
+ const role = hint.auth || (point.facts.security?.length === 0 || point.facts.securitySource === 'unspecified' ? 'public' : 'account')
61
+ let apikey = role === 'public' ? null : role === 'issued' ? resolveRecipe(hint.credential, ctx.values) : process.env[envPrefix + '_APIKEY']
62
+ if (role === 'issued' && (typeof apikey !== 'string' || !apikey)) throw new LiveBlocked('Issued credential unavailable')
63
+ let wire: any
64
+ let wireSchema: any
65
+ const options = liveClientOptions()
66
+ const client = new SDK({ ...options, ...liveDefaults, apikey,
67
+ feature: { ...options.feature, test: { active: false }, retry: { active: false },
68
+ secrets: { ...options.feature?.secrets, active: role === 'account' && (!apikey || options.feature?.secrets?.active === true) } },
69
+ system: { ...options.system, fetch: async (url: any, init: any) => {
70
+ const headers = new Headers(init?.headers)
71
+ if (role === 'public') assert(!headers.has('authorization'), 'Public request carries authentication')
72
+ else if (!headers.get('authorization')) throw new LiveBlocked('Credential unavailable for ' + role + ' request')
73
+ const response = await transport.fetch(url, { ...init, redirect: 'error' })
74
+ const expectedPath = point.path.replace(/\{([^}]+)\}/g, (_: string, name: string) => encodeURIComponent(input[point.rename?.param?.[name] || name] ?? input[name]))
75
+ if (point.kind === 'graphql') {
76
+ const payload = JSON.parse(init.body)
77
+ assert.equal(payload.query, point.graphql.doc, 'Entity selected wrong GraphQL operation')
78
+ } else assert.equal(new URL(String(url)).pathname, expectedPath, 'Entity selected wrong route')
79
+ assert.equal(init?.method || 'GET', point.method)
80
+ assert(response.status >= 200 && response.status < 300, 'Unsuccessful HTTP response')
81
+ if (point.kind === 'graphql') wire = await response.clone().json()
82
+ const def = point.facts.responses?.[response.status] || point.facts.responses?.[String(response.status)[0] + 'XX'] || point.facts.responses?.default
83
+ if (point.facts.responses && !def) throw new Error('Undeclared response status')
84
+ if (def) {
85
+ const schema = def.content?.['application/json']?.schema ?? def.schema
86
+ if (schema && response.status !== 204) { wireSchema = schema; wire = await response.clone().json() }
87
+ }
88
+ return response
89
+ } } })
90
+ if (point.action) input = { ...input, $action: point.action }
91
+ assert(JSON.stringify(input).length <= 1024 * 1024, 'Request exceeds test payload limit')
92
+ const result = await client[point.accessor]()[point.op](input)
93
+ const data = Array.isArray(result) ? result.map(item => { assert.equal(typeof item?.data, 'function'); return item.data() }) : (assert.equal(typeof result?.data, 'function'), result.data())
94
+ ctx.publish(data)
95
+ if (wireSchema) validateContract(wireSchema, wire, 'response')
96
+ if (Array.isArray(wire?.errors) && wire.errors.length) throw new Error('GraphQL operation reported errors')
97
+ if (wire?.success === false) throw new Error('API reported unsuccessful operation')
98
+ const checks = resolveRecipe(hint.assert || {}, ctx.values)
99
+ for (const [path, expected] of Object.entries(checks.equal || {})) assert.deepEqual(at(data, path), expected)
100
+ for (const path of checks.nonempty || []) assert(at(data, path)?.length > 0, 'Empty required output')
101
+ if (checks.vectors) {
102
+ const vectors = at(data, checks.vectors.path)
103
+ assert(Array.isArray(vectors) && vectors.length === checks.vectors.count)
104
+ for (const vector of vectors) assert(Array.isArray(vector) && vector.length === checks.vectors.dimension && vector.every((n: any) => typeof n === 'number' && Number.isFinite(n)), 'Invalid vector shape')
105
+ }
106
+ } }
107
+ })
108
+ const report = await runLiveSteps(steps, { delayMs: liveDelayMs(), report: result => console.log('LIVE STEP ' + JSON.stringify(result)) })
109
+ console.log('LIVE SUMMARY ' + JSON.stringify(report))
110
+ assertLiveReport(report)
111
+ return report
112
+ }
@@ -161,8 +161,7 @@ function skipIfMissingIds(t: any, setup: any, requiredKeys: string[]): boolean {
161
161
  if (!setup.live) return false
162
162
  const missing = requiredKeys.filter(k => null == setup.idmap?.[k])
163
163
  if (missing.length > 0) {
164
- t.skip(`live test needs ${missing.join(', ')} via *_ENTID env var (synthetic IDs only)`)
165
- return true
164
+ throw new Error(`Live test blocked: needs ${missing.join(', ')} via *_ENTID env var`)
166
165
  }
167
166
  return false
168
167
  }
@@ -3,7 +3,7 @@
3
3
  "package": 1
4
4
  },
5
5
  "name": "@voxgig/sdkgen",
6
- "version": "4.16.0",
6
+ "version": "4.17.1",
7
7
  "provides": {
8
8
  "target": [
9
9
  "c",
@@ -102,7 +102,7 @@ function parseAddNames(args: any[]): string[] {
102
102
  // `seed` IS THE UPGRADE PATH. A project scaffolded before a kind existed has
103
103
  // no `model/<kind>/<kind>-index.aon` — every project alive today is in
104
104
  // exactly that position for `docs` — and reading it unguarded made the FIRST
105
- // `docs add` in any existing project fail on ENOENT before it wrote anything.
105
+ // `edition add` in any existing project fail on ENOENT before it wrote anything.
106
106
  //
107
107
  // Seeded per call rather than defaulted for every kind: a missing
108
108
  // `target-index.aon` in a scaffolded project is a broken project, and
@@ -136,7 +136,7 @@ function loadContent(
136
136
  // indexes of the kinds that existed then. So a project scaffolded before a
137
137
  // kind existed — which is every project alive today, for `docs` — never
138
138
  // includes its index, and the item's model file is an orphan: it is on disk,
139
- // `<kind>-index.aon` includes it, and NOTHING includes that. `main.kit.docs`
139
+ // `<kind>-index.aon` includes it, and NOTHING includes that. `main.kit.doc.edition`
140
140
  // is then absent from the compiled model, so `package list`, `package update`
141
141
  // and `doctor` cannot see the item at all.
142
142
  //
@@ -1,3 +1,4 @@
1
+ import { kindCollection } from '../helpers/kindCollection'
1
2
  // `package check` — the AUTHOR-side battery. Design §14.
2
3
  //
3
4
  // WHAT IT IS FOR
@@ -308,11 +309,11 @@ function checkDefinition(
308
309
  // `main: kit: target: go:` installs an item the consumer's model never
309
310
  // sees — the exact mistake a package author makes copying a bundled
310
311
  // target as a starting point.
311
- const declared = strict.model?.main?.[KIT]?.[kind]?.[name]
312
+ const declared = kindCollection(strict.model, kind)?.[name]
312
313
 
313
314
  if (null == declared || 'object' !== typeof declared) {
314
315
  found.push(at('error', 'model-key-missing',
315
- 'declares no `main: kit: ' + kind + ': ' + name + ':` block — the file ' +
316
+ 'declares no `main: kit: ' + (kind === 'edition' ? 'doc: edition' : kind) + ': ' + name + ':` block — the file ' +
316
317
  'is installed and included under its own name, so nothing it declares ' +
317
318
  'under another name is reachable'))
318
319
  }
@@ -1,7 +1,7 @@
1
1
  // WHAT `voxgig-sdkgen <action> …` CAN BE.
2
2
  //
3
3
  // Built FROM THE KIND REGISTRY rather than hand-listed, so registering a kind
4
- // is the only edit a new kind needs — `docs add …` costs no dispatch code
4
+ // is the only edit a new kind needs — `edition add …` costs no dispatch code
5
5
  // (docs/design/sdkgen-packages.md §9). The `package` and `doctor` verbs are
6
6
  // not kinds and are added beside it.
7
7
  //
@@ -25,7 +25,7 @@ import { KINDS } from './kind'
25
25
 
26
26
  import { action_target, target_add } from './target'
27
27
  import { action_feature, feature_add } from './feature'
28
- import { action_docs, docs_add } from './docs'
28
+ import { action_edition, edition_add } from './edition'
29
29
  import { action_doctor } from './doctor'
30
30
  import { action_package, registerAdder } from './package'
31
31
 
@@ -41,7 +41,7 @@ const KIND_ACTIONS: Record<string, ActionFunc> = Object.assign(
41
41
  Object.create(null), {
42
42
  target: action_target,
43
43
  feature: action_feature,
44
- docs: action_docs,
44
+ edition: action_edition,
45
45
  })
46
46
 
47
47
 
@@ -49,7 +49,7 @@ const KIND_ACTIONS: Record<string, ActionFunc> = Object.assign(
49
49
  // over. Registered rather than imported by `package.ts`, for the cycle above.
50
50
  registerAdder('target', target_add)
51
51
  registerAdder('feature', feature_add)
52
- registerAdder('docs', docs_add)
52
+ registerAdder('edition', edition_add)
53
53
 
54
54
 
55
55
  function actionMap(): Record<string, ActionFunc> {
@@ -1,3 +1,4 @@
1
+ import { kindCollection } from '../helpers/kindCollection'
1
2
  // `voxgig-sdkgen doctor` — does this project's `.sdk/` still match the
2
3
  // scaffold?
3
4
  //
@@ -276,7 +277,7 @@ async function doctor(
276
277
 
277
278
  const counts: Record<string, number> = {}
278
279
  for (const kind of kinds) {
279
- counts[kind] = Object.keys((model as any)?.main?.[KIT]?.[kind] ?? {}).length
280
+ counts[kind] = Object.keys(kindCollection(model, kind) ?? {}).length
280
281
  }
281
282
 
282
283
  log.info({ point: 'doctor-start', targets: counts.target ?? 0, ...counts })
@@ -303,7 +304,7 @@ async function doctor(
303
304
  }
304
305
 
305
306
  for (const kind of kinds) {
306
- const items = Object.keys((model as any)?.main?.[KIT]?.[kind] ?? {}).sort()
307
+ const items = Object.keys(kindCollection(model, kind) ?? {}).sort()
307
308
 
308
309
  for (const name of items) {
309
310
  if (null != scope && !scope(kind, name)) {
@@ -321,8 +322,8 @@ async function doctor(
321
322
  checkTarget(actx, source, report)
322
323
  }
323
324
 
324
- if ('docs' === kind) {
325
- checkDocs(actx, source, report)
325
+ if ('edition' === kind) {
326
+ checkEdition(actx, source, report)
326
327
  }
327
328
 
328
329
  // Only an ACTIVE feature has source copied out; what an inactive one
@@ -433,7 +434,7 @@ function checkWiring(actx: ActionContext, report: DoctorReport) {
433
434
  function resolveDeclared(
434
435
  kind: string, name: string, actx: ActionContext,
435
436
  ): Source | undefined {
436
- const declared: any = (actx.model as any)?.main?.[KIT]?.[kind]?.[name]
437
+ const declared: any = kindCollection(actx.model, kind)?.[name]
437
438
  const ref = recordedRef(declared, name) || name
438
439
 
439
440
  try {
@@ -659,8 +660,8 @@ function compareTrees(
659
660
  // rather than being spelled a second time.
660
661
  //
661
662
  // The optional template tree is skipped when the SOURCE does not ship one:
662
- // `docs add` did not copy it, so the project is right not to have it.
663
- function checkDocs(
663
+ // `edition add` did not copy it, so the project is right not to have it.
664
+ function checkEdition(
664
665
  actx: ActionContext, resolved: Source, report: DoctorReport,
665
666
  ) {
666
667
  const fs = actx.fs()
@@ -669,8 +670,8 @@ function checkDocs(
669
670
  const origname = resolved.origname
670
671
  const aliased = name !== origname
671
672
 
672
- const dest = kindTrees('docs', name)
673
- const from = kindTrees('docs', origname)
673
+ const dest = kindTrees('edition', name)
674
+ const from = kindTrees('edition', origname)
674
675
 
675
676
  const trees: TreeCompare[] = dest.flatMap((tree: TreeDef, i: number) => {
676
677
  const scaffold = Path.join(resolved.folder, ...from[i].path.split('/'))