@pikku/inspector 0.12.74 → 0.12.81

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 (219) hide show
  1. package/CHANGELOG.md +347 -0
  2. package/dist/add/add-analytics.d.ts +9 -0
  3. package/dist/add/add-analytics.js +178 -0
  4. package/dist/add/add-auth.js +0 -54
  5. package/dist/add/add-feature-flag.d.ts +2 -0
  6. package/dist/add/add-feature-flag.js +162 -0
  7. package/dist/add/add-wire-addon.js +1 -0
  8. package/dist/add/add-wire-remote-addon.js +1 -0
  9. package/dist/index.d.ts +3 -1
  10. package/dist/index.js +3 -1
  11. package/dist/inspector.js +4 -0
  12. package/dist/surface/collect-surface.d.ts +58 -0
  13. package/dist/surface/collect-surface.js +613 -0
  14. package/dist/types.d.ts +20 -11
  15. package/dist/utils/filter-inspector-state.js +23 -13
  16. package/dist/utils/post-process.js +15 -0
  17. package/dist/utils/read-module-specifiers.d.ts +8 -0
  18. package/dist/utils/read-module-specifiers.js +23 -0
  19. package/dist/utils/read-tsconfig-out-dir.d.ts +5 -0
  20. package/dist/utils/read-tsconfig-out-dir.js +12 -0
  21. package/dist/utils/resolve-deploy-target.d.ts +7 -0
  22. package/dist/utils/resolve-deploy-target.js +15 -7
  23. package/dist/utils/serialize-inspector-state.d.ts +5 -0
  24. package/dist/utils/serialize-inspector-state.js +8 -0
  25. package/dist/visit.js +4 -0
  26. package/package.json +16 -6
  27. package/run-tests.sh +21 -6
  28. package/src/add/add-addon-bans.ts +0 -85
  29. package/src/add/add-agent-scorer.test.ts +0 -197
  30. package/src/add/add-agent-scorer.ts +0 -83
  31. package/src/add/add-agent.test.ts +0 -87
  32. package/src/add/add-agent.ts +0 -638
  33. package/src/add/add-approval-description.ts +0 -76
  34. package/src/add/add-auth.test.ts +0 -791
  35. package/src/add/add-auth.ts +0 -424
  36. package/src/add/add-channel.ts +0 -778
  37. package/src/add/add-cli-renderers.test.ts +0 -74
  38. package/src/add/add-cli.ts +0 -1006
  39. package/src/add/add-credential.ts +0 -186
  40. package/src/add/add-feature.test.ts +0 -306
  41. package/src/add/add-feature.ts +0 -253
  42. package/src/add/add-file-extends-core-type.ts +0 -66
  43. package/src/add/add-file-with-config.ts +0 -45
  44. package/src/add/add-file-with-factory.test.ts +0 -134
  45. package/src/add/add-file-with-factory.ts +0 -288
  46. package/src/add/add-functions.test.ts +0 -743
  47. package/src/add/add-functions.ts +0 -1548
  48. package/src/add/add-gateway.ts +0 -143
  49. package/src/add/add-http-route-ref.test.ts +0 -149
  50. package/src/add/add-http-route.ts +0 -530
  51. package/src/add/add-http-routes.ts +0 -471
  52. package/src/add/add-keyed-wiring.ts +0 -180
  53. package/src/add/add-mcp-prompt.ts +0 -157
  54. package/src/add/add-mcp-resource.ts +0 -175
  55. package/src/add/add-middleware.test.ts +0 -196
  56. package/src/add/add-middleware.ts +0 -879
  57. package/src/add/add-permission-auth.test.ts +0 -59
  58. package/src/add/add-permission.ts +0 -311
  59. package/src/add/add-personas.test.ts +0 -406
  60. package/src/add/add-personas.ts +0 -287
  61. package/src/add/add-queue-worker.ts +0 -117
  62. package/src/add/add-rpc-invocations.test.ts +0 -287
  63. package/src/add/add-rpc-invocations.ts +0 -221
  64. package/src/add/add-scenario-step.test.ts +0 -509
  65. package/src/add/add-schedule.ts +0 -120
  66. package/src/add/add-scope.test.ts +0 -419
  67. package/src/add/add-scope.ts +0 -221
  68. package/src/add/add-secret.ts +0 -10
  69. package/src/add/add-subject-scenario-steps.test.ts +0 -161
  70. package/src/add/add-system-role.test.ts +0 -297
  71. package/src/add/add-system-role.ts +0 -162
  72. package/src/add/add-trigger.ts +0 -165
  73. package/src/add/add-variable.ts +0 -10
  74. package/src/add/add-wire-addon.test.ts +0 -185
  75. package/src/add/add-wire-addon.ts +0 -146
  76. package/src/add/add-wire-remote-addon.ts +0 -80
  77. package/src/add/add-workflow-edge-cases.test.ts +0 -201
  78. package/src/add/add-workflow-fanout.test.ts +0 -166
  79. package/src/add/add-workflow-graph-foreach.test.ts +0 -174
  80. package/src/add/add-workflow-graph-input.test.ts +0 -94
  81. package/src/add/add-workflow-graph-notes.test.ts +0 -74
  82. package/src/add/add-workflow-graph.ts +0 -644
  83. package/src/add/add-workflow-on-error.test.ts +0 -118
  84. package/src/add/add-workflow-return-switch.test.ts +0 -121
  85. package/src/add/add-workflow-silent-drops.test.ts +0 -261
  86. package/src/add/add-workflow-sleep-expression.test.ts +0 -96
  87. package/src/add/add-workflow.test.ts +0 -155
  88. package/src/add/add-workflow.ts +0 -482
  89. package/src/add/addon-bans.test.ts +0 -121
  90. package/src/add/addon-contracts.test.ts +0 -221
  91. package/src/add/allow-escape-hatches.test.ts +0 -144
  92. package/src/add/audit-meta.test.ts +0 -75
  93. package/src/add/classified-sink-check.test.ts +0 -347
  94. package/src/add/config-value-metadata.test.ts +0 -263
  95. package/src/add/dynamic-import-check.test.ts +0 -119
  96. package/src/add/expect-eventually-scenario-only.test.ts +0 -108
  97. package/src/add/function-body-span.test.ts +0 -96
  98. package/src/add/inline-wiring-function.test.ts +0 -104
  99. package/src/add/pii-check.test.ts +0 -258
  100. package/src/add/rpc-type-cast.test.ts +0 -123
  101. package/src/add/scenario-instrumentation.test.ts +0 -89
  102. package/src/add/scenario-instrumentation.ts +0 -42
  103. package/src/add/scenario-marker.test.ts +0 -89
  104. package/src/add/scenario-not-extractable.test.ts +0 -159
  105. package/src/add/scenario-services.test.ts +0 -103
  106. package/src/add/scenario-skip.test.ts +0 -83
  107. package/src/add/validate-workflow-graph-addons.test.ts +0 -102
  108. package/src/add/wire-name-literal.test.ts +0 -117
  109. package/src/error-codes-emitted.test.ts +0 -76
  110. package/src/error-codes.ts +0 -145
  111. package/src/index.ts +0 -66
  112. package/src/inspector.ts +0 -575
  113. package/src/schema-generator.ts +0 -1
  114. package/src/source-file-cache.ts +0 -73
  115. package/src/tsconfig-paths.test.ts +0 -90
  116. package/src/types-map.ts +0 -141
  117. package/src/types.ts +0 -775
  118. package/src/utils/annotate-http-route-auth.test.ts +0 -161
  119. package/src/utils/annotate-http-route-auth.ts +0 -53
  120. package/src/utils/check-classified-sinks.ts +0 -215
  121. package/src/utils/check-pii-output.ts +0 -120
  122. package/src/utils/compute-diagnostics.test.ts +0 -106
  123. package/src/utils/compute-required-schemas.ts +0 -48
  124. package/src/utils/contract-hashes.test.ts +0 -553
  125. package/src/utils/contract-hashes.ts +0 -387
  126. package/src/utils/custom-types-generator.test.ts +0 -124
  127. package/src/utils/custom-types-generator.ts +0 -150
  128. package/src/utils/detect-schema-vendor.test.ts +0 -123
  129. package/src/utils/detect-schema-vendor.ts +0 -120
  130. package/src/utils/does-type-extend-core-type.ts +0 -53
  131. package/src/utils/ensure-function-metadata.ts +0 -385
  132. package/src/utils/extract-function-name.ts +0 -612
  133. package/src/utils/extract-function-node.ts +0 -70
  134. package/src/utils/extract-node-value.test.ts +0 -117
  135. package/src/utils/extract-node-value.ts +0 -172
  136. package/src/utils/extract-secret-usage.test.ts +0 -81
  137. package/src/utils/extract-secret-usage.ts +0 -73
  138. package/src/utils/extract-services.ts +0 -70
  139. package/src/utils/extract-surface-usage.test.ts +0 -147
  140. package/src/utils/extract-surface-usage.ts +0 -121
  141. package/src/utils/filter-inspector-state.test.ts +0 -2207
  142. package/src/utils/filter-inspector-state.ts +0 -1302
  143. package/src/utils/filter-utils.test.ts +0 -843
  144. package/src/utils/filter-utils.ts +0 -161
  145. package/src/utils/find-root-dir.ts +0 -68
  146. package/src/utils/find-zod-transform.test.ts +0 -70
  147. package/src/utils/find-zod-transform.ts +0 -103
  148. package/src/utils/get-exported-variable-name.ts +0 -48
  149. package/src/utils/get-files-and-methods.ts +0 -157
  150. package/src/utils/get-property-value.test.ts +0 -141
  151. package/src/utils/get-property-value.ts +0 -331
  152. package/src/utils/hash.ts +0 -26
  153. package/src/utils/literal-properties.ts +0 -97
  154. package/src/utils/load-addon-functions-meta.test.ts +0 -157
  155. package/src/utils/load-addon-functions-meta.ts +0 -508
  156. package/src/utils/load-addon-scopes.test.ts +0 -138
  157. package/src/utils/middleware.test.ts +0 -204
  158. package/src/utils/middleware.ts +0 -303
  159. package/src/utils/nested-project-filter.test.ts +0 -53
  160. package/src/utils/nested-project-filter.ts +0 -28
  161. package/src/utils/permissions-shorthand.test.ts +0 -79
  162. package/src/utils/permissions.test.ts +0 -125
  163. package/src/utils/permissions.ts +0 -129
  164. package/src/utils/post-process.test.ts +0 -1094
  165. package/src/utils/post-process.ts +0 -1604
  166. package/src/utils/resolve-addon-package.ts +0 -106
  167. package/src/utils/resolve-core-type.test.ts +0 -142
  168. package/src/utils/resolve-core-type.ts +0 -35
  169. package/src/utils/resolve-deploy-target.test.ts +0 -135
  170. package/src/utils/resolve-deploy-target.ts +0 -65
  171. package/src/utils/resolve-function-meta.ts +0 -25
  172. package/src/utils/resolve-function-types.ts +0 -42
  173. package/src/utils/resolve-identifier.ts +0 -46
  174. package/src/utils/resolve-ref-contract.ts +0 -71
  175. package/src/utils/resolve-versions.test.ts +0 -499
  176. package/src/utils/resolve-versions.ts +0 -228
  177. package/src/utils/schema-cache.test.ts +0 -124
  178. package/src/utils/schema-generator.test.ts +0 -272
  179. package/src/utils/schema-generator.ts +0 -780
  180. package/src/utils/secret-alias-services.test.ts +0 -146
  181. package/src/utils/secret-alias-services.ts +0 -58
  182. package/src/utils/serialize-inspector-state.ts +0 -742
  183. package/src/utils/serialize-mcp-json.ts +0 -150
  184. package/src/utils/serialize-middleware-groups-meta.ts +0 -36
  185. package/src/utils/serialize-openapi-json.ts +0 -277
  186. package/src/utils/serialize-permissions-groups-meta.ts +0 -7
  187. package/src/utils/single-declaration.ts +0 -57
  188. package/src/utils/type-utils.ts +0 -145
  189. package/src/utils/validate-auth-sessionless.ts +0 -30
  190. package/src/utils/validate-exposed-functions-gated.test.ts +0 -230
  191. package/src/utils/validate-exposed-functions-gated.ts +0 -103
  192. package/src/utils/validate-tags-resolve-to-middleware.test.ts +0 -108
  193. package/src/utils/validate-tags-resolve-to-middleware.ts +0 -48
  194. package/src/utils/workflow/derive-workflow-plan.test.ts +0 -178
  195. package/src/utils/workflow/derive-workflow-plan.ts +0 -117
  196. package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +0 -1465
  197. package/src/utils/workflow/dsl/deserialize-fanout-body.test.ts +0 -71
  198. package/src/utils/workflow/dsl/deserialize-fanout-fidelity.test.ts +0 -99
  199. package/src/utils/workflow/dsl/deserialize-graph-next.test.ts +0 -161
  200. package/src/utils/workflow/dsl/deserialize-roundtrip-bindings.test.ts +0 -121
  201. package/src/utils/workflow/dsl/deserialize-roundtrip-values.test.ts +0 -116
  202. package/src/utils/workflow/dsl/deserialize-roundtrip.test.ts +0 -145
  203. package/src/utils/workflow/dsl/extract-approval-policy.test.ts +0 -71
  204. package/src/utils/workflow/dsl/extract-dsl-workflow.ts +0 -2254
  205. package/src/utils/workflow/dsl/index.ts +0 -11
  206. package/src/utils/workflow/dsl/patterns.ts +0 -440
  207. package/src/utils/workflow/dsl/validation.ts +0 -203
  208. package/src/utils/workflow/graph/convert-dsl-to-graph-skip.test.ts +0 -35
  209. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +0 -543
  210. package/src/utils/workflow/graph/fanout-node-identity.test.ts +0 -105
  211. package/src/utils/workflow/graph/finalize-workflow-wires.ts +0 -310
  212. package/src/utils/workflow/graph/finalize-workflows.test.ts +0 -50
  213. package/src/utils/workflow/graph/finalize-workflows.ts +0 -130
  214. package/src/utils/workflow/graph/index.ts +0 -28
  215. package/src/utils/workflow/graph/serialize-workflow-graph.ts +0 -240
  216. package/src/utils/workflow/graph/workflow-graph.types.ts +0 -281
  217. package/src/visit.ts +0 -181
  218. package/tsconfig.json +0 -19
  219. package/tsconfig.tsbuildinfo +0 -1
@@ -1,509 +0,0 @@
1
- import { strict as assert } from 'assert'
2
- import { describe, test } from 'node:test'
3
- import { mkdtemp, rm, writeFile } from 'node:fs/promises'
4
- import { tmpdir } from 'node:os'
5
- import { join } from 'node:path'
6
- import { inspect } from '../inspector.js'
7
- import type { InspectorLogger } from '../types.js'
8
- import type { WorkflowStepMeta } from '@pikku/core/workflow'
9
- import type { ScenarioStepMeta } from '@pikku/core/scenario'
10
-
11
- function makeLogger(
12
- criticals: Array<{ code: string; message: string }>
13
- ): InspectorLogger {
14
- return {
15
- debug: () => {},
16
- info: () => {},
17
- warn: () => {},
18
- error: () => {},
19
- diagnostic: ({ code, message }: any) => {
20
- criticals.push({ code, message })
21
- },
22
- critical: (code: any, message: string) => {
23
- criticals.push({ code, message })
24
- },
25
- hasCriticalErrors: () => criticals.length > 0,
26
- } as InspectorLogger
27
- }
28
-
29
- const STEPS = [
30
- "import { pikkuScenarioStep } from '@pikku/core'",
31
- 'export const buysAnApple = pikkuScenarioStep({',
32
- " name: 'buysAnApple',",
33
- " description: 'buys an apple',",
34
- ' browser: async ({ logger }, data: { qty: number }) => ({ ok: data.qty > 0 }),',
35
- ' default: async ({ logger }, data: { qty: number }) => ({ ok: data.qty > 0 }),',
36
- '})',
37
- 'export const seesAReceipt = pikkuScenarioStep({',
38
- " name: 'seesAReceipt',",
39
- " description: 'sees a receipt',",
40
- ' default: async ({ logger }) => ({ ok: true }),',
41
- '})',
42
- 'export const asksForTheReceipt = pikkuScenarioStep({',
43
- " name: 'asksForTheReceipt',",
44
- " description: 'asks for the receipt',",
45
- ' actor: true,',
46
- ' default: async ({ logger }) => ({ ok: true }),',
47
- '})',
48
- ].join('\n')
49
-
50
- const scenarioSource = (body: string[], preamble: string[] = []) =>
51
- [
52
- "import { pikkuScenario } from '@pikku/core/workflow'",
53
- 'declare const actors: Record<string, any>',
54
- ...preamble,
55
- 'export const shopFlow = pikkuScenario(async (_services, _input, { scenario }: any) => {',
56
- ...body.map((line) => ` ${line}`),
57
- ' return { ok: true }',
58
- '})',
59
- ].join('\n')
60
-
61
- async function run(body: string[], preamble: string[] = []) {
62
- const rootDir = await mkdtemp(join(tmpdir(), 'pikku-scenario-step-'))
63
- const stepsFile = join(rootDir, 'shop.steps.ts')
64
- const scenarioFile = join(rootDir, 'shop.scenario.ts')
65
- await writeFile(stepsFile, STEPS)
66
- await writeFile(scenarioFile, scenarioSource(body, preamble))
67
- const criticals: Array<{ code: string; message: string }> = []
68
- const state = await inspect(
69
- makeLogger(criticals),
70
- [stepsFile, scenarioFile],
71
- {
72
- rootDir,
73
- }
74
- )
75
- return {
76
- state,
77
- criticals,
78
- cleanup: () => rm(rootDir, { recursive: true, force: true }),
79
- }
80
- }
81
-
82
- const flatten = (steps: WorkflowStepMeta[]): WorkflowStepMeta[] =>
83
- steps.flatMap((step) => [
84
- step,
85
- ...('body' in step && step.body
86
- ? flatten(step.body as WorkflowStepMeta[])
87
- : []),
88
- ])
89
-
90
- const scenarioSteps = (state: any): ScenarioStepMeta[] =>
91
- flatten(state.workflows.meta.shopFlow?.steps ?? []).filter(
92
- (step): step is ScenarioStepMeta => step.type === 'scenarioStep'
93
- )
94
-
95
- describe('pikkuScenarioStep', () => {
96
- test('a step is registered under its declared name, not its export name', async () => {
97
- const { state, cleanup } = await run([
98
- "await scenario.given('buys an apple', 'buysAnApple', { qty: 1 }, { actor: actors.shopper })",
99
- ])
100
- try {
101
- assert.ok(
102
- state.functions.meta['buysAnApple'],
103
- `expected a function registered as 'buysAnApple', got: ${Object.keys(state.functions.meta).join(', ')}`
104
- )
105
- } finally {
106
- await cleanup()
107
- }
108
- })
109
-
110
- test('a step is never registered as an RPC — a browser step must not be network-callable', async () => {
111
- const { state, cleanup } = await run([
112
- "await scenario.given('buys an apple', 'buysAnApple', { qty: 1 }, { actor: actors.shopper })",
113
- ])
114
- try {
115
- assert.equal(
116
- state.rpc.internalMeta['buysAnApple'],
117
- undefined,
118
- 'scenario steps must not be RPC-callable'
119
- )
120
- assert.equal(
121
- state.rpc.exposedMeta['buysAnApple'],
122
- undefined,
123
- 'scenario steps must not be exposed over RPC'
124
- )
125
- } finally {
126
- await cleanup()
127
- }
128
- })
129
-
130
- test('given/when/then record phase, target and actor', async () => {
131
- const { state, cleanup } = await run([
132
- "await scenario.given('buys an apple', 'buysAnApple', { qty: 1 }, { actor: actors.shopper })",
133
- "await scenario.then('sees a receipt', 'seesAReceipt', {}, { actor: actors.shopper })",
134
- ])
135
- try {
136
- const steps = scenarioSteps(state)
137
- assert.equal(
138
- steps.length,
139
- 2,
140
- `expected 2 scenario steps, got ${steps.length}`
141
- )
142
- assert.equal(steps[0]!.phase, 'given')
143
- assert.equal(steps[0]!.stepFunc, 'buysAnApple')
144
- assert.equal(steps[0]!.actor, 'shopper')
145
- assert.equal(steps[1]!.phase, 'then')
146
- assert.equal(steps[1]!.stepFunc, 'seesAReceipt')
147
- } finally {
148
- await cleanup()
149
- }
150
- })
151
-
152
- test('the step target is bundled, so the step function is wired', async () => {
153
- const { state, cleanup } = await run([
154
- "await scenario.given('buys an apple', 'buysAnApple', { qty: 1 }, { actor: actors.shopper })",
155
- ])
156
- try {
157
- assert.ok(
158
- state.rpc.invokedFunctions.has('buysAnApple'),
159
- `expected 'buysAnApple' to be bundled, got: ${[...state.rpc.invokedFunctions].join(', ')}`
160
- )
161
- } finally {
162
- await cleanup()
163
- }
164
- })
165
-
166
- test('a step inside a for..of loop is extracted, not silently dropped', async () => {
167
- const { state, cleanup } = await run([
168
- 'const quantities = [1, 2]',
169
- 'for (const qty of quantities) {',
170
- " await scenario.when('buys an apple', 'buysAnApple', { qty }, { actor: actors.shopper })",
171
- '}',
172
- ])
173
- try {
174
- const steps = scenarioSteps(state)
175
- assert.equal(
176
- steps.length,
177
- 1,
178
- `expected the looped step to appear in the fanout body, got ${steps.length} scenario steps`
179
- )
180
- assert.equal(steps[0]!.stepFunc, 'buysAnApple')
181
- assert.ok(
182
- state.rpc.invokedFunctions.has('buysAnApple'),
183
- 'a looped step must still be bundled'
184
- )
185
- } finally {
186
- await cleanup()
187
- }
188
- })
189
-
190
- test('a non-literal step target is a PKU678 critical', async () => {
191
- const { criticals, cleanup } = await run([
192
- 'const target = Math.random() > 0.5 ? "buysAnApple" : "seesAReceipt"',
193
- "await scenario.when('buys something', target as any, {}, { actor: actors.shopper })",
194
- ])
195
- try {
196
- assert.ok(
197
- criticals.find((c) => c.code === 'PKU678'),
198
- `expected a PKU678 critical for a dynamic step target, got: ${JSON.stringify(criticals)}`
199
- )
200
- } finally {
201
- await cleanup()
202
- }
203
- })
204
-
205
- test('a browser step called without an actor is a PKU677 critical', async () => {
206
- const { criticals, cleanup } = await run([
207
- "await scenario.given('buys an apple', 'buysAnApple', { qty: 1 })",
208
- ])
209
- try {
210
- assert.ok(
211
- criticals.find((c) => c.code === 'PKU677'),
212
- `expected a PKU677 critical for a browser step without an actor, got: ${JSON.stringify(criticals)}`
213
- )
214
- } finally {
215
- await cleanup()
216
- }
217
- })
218
-
219
- test('a step declaring `actor: true` called without one is a PKU677 critical', async () => {
220
- const { criticals, cleanup } = await run([
221
- "await scenario.then('asks for the receipt', 'asksForTheReceipt', {})",
222
- ])
223
- try {
224
- assert.ok(
225
- criticals.find((c) => c.code === 'PKU677'),
226
- `a step that says it runs as somebody must be given somebody, browser binding or not, got: ${JSON.stringify(criticals)}`
227
- )
228
- } finally {
229
- await cleanup()
230
- }
231
- })
232
-
233
- test('a step that declares no actor is not flagged for running without one', async () => {
234
- const { criticals, cleanup } = await run([
235
- "await scenario.then('sees a receipt', 'seesAReceipt', {})",
236
- ])
237
- try {
238
- assert.equal(
239
- criticals.filter((c) => c.code === 'PKU677').length,
240
- 0,
241
- `an assertion over an earlier step's value has nobody to be, got: ${JSON.stringify(criticals)}`
242
- )
243
- } finally {
244
- await cleanup()
245
- }
246
- })
247
-
248
- test('a step whose prose opens with its own actor is a PKU681 critical', async () => {
249
- const { criticals, cleanup } = await run([
250
- "await scenario.given(`'shopper' buys an apple`, 'buysAnApple', { qty: 1 }, { actor: actors.shopper })",
251
- "await scenario.then('sees a receipt', 'seesAReceipt', {}, { actor: actors.shopper })",
252
- ])
253
- try {
254
- assert.ok(
255
- criticals.find((c) => c.code === 'PKU681'),
256
- `the reporter already renders the actor as the subject, so this reads "Given shopper 'shopper' buys an apple", got: ${JSON.stringify(criticals)}`
257
- )
258
- } finally {
259
- await cleanup()
260
- }
261
- })
262
-
263
- test('an unquoted, capitalised actor opening the prose is caught too', async () => {
264
- const { criticals, cleanup } = await run([
265
- "await scenario.given('Shopper buys an apple', 'buysAnApple', { qty: 1 }, { actor: actors.shopper })",
266
- "await scenario.then('sees a receipt', 'seesAReceipt', {}, { actor: actors.shopper })",
267
- ])
268
- try {
269
- assert.ok(
270
- criticals.find((c) => c.code === 'PKU681'),
271
- `quotes and case are decoration — the subject is still doubled, got: ${JSON.stringify(criticals)}`
272
- )
273
- } finally {
274
- await cleanup()
275
- }
276
- })
277
-
278
- test('a call-site description naming the actor is caught as well as the step name', async () => {
279
- const { criticals, cleanup } = await run([
280
- "await scenario.given('buys an apple', 'buysAnApple', { qty: 1 }, { actor: actors.shopper, description: `shopper buys an apple` })",
281
- "await scenario.then('sees a receipt', 'seesAReceipt', {}, { actor: actors.shopper })",
282
- ])
283
- try {
284
- assert.ok(
285
- criticals.find((c) => c.code === 'PKU681'),
286
- `the description is what actually renders when it is given, got: ${JSON.stringify(criticals)}`
287
- )
288
- } finally {
289
- await cleanup()
290
- }
291
- })
292
-
293
- test('an actor named mid-sentence is left alone', async () => {
294
- const { criticals, cleanup } = await run([
295
- "await scenario.given('buys an apple', 'buysAnApple', { qty: 1 }, { actor: actors.shopper })",
296
- "await scenario.then('sends shopper a receipt', 'seesAReceipt', {}, { actor: actors.shopper })",
297
- ])
298
- try {
299
- assert.equal(
300
- criticals.filter((c) => c.code === 'PKU681').length,
301
- 0,
302
- `only the subject position doubles up; naming someone mid-sentence is ordinary prose, got: ${JSON.stringify(criticals)}`
303
- )
304
- } finally {
305
- await cleanup()
306
- }
307
- })
308
-
309
- test('a scenario that never asserts is a PKU680 critical', async () => {
310
- const { criticals, cleanup } = await run([
311
- "await scenario.given('buys an apple', 'buysAnApple', { qty: 1 }, { actor: actors.shopper })",
312
- "await scenario.when('buys an apple', 'buysAnApple', { qty: 2 }, { actor: actors.shopper })",
313
- ])
314
- try {
315
- assert.ok(
316
- criticals.find((c) => c.code === 'PKU680'),
317
- `a flow with no 'then' proves only that nothing threw, and contributes 0/0 to witness coverage, got: ${JSON.stringify(criticals)}`
318
- )
319
- } finally {
320
- await cleanup()
321
- }
322
- })
323
-
324
- test('a scenario whose witness is an expectation helper is not flagged', async () => {
325
- // `expectService` is an inline step and carries no phase, so it reaches
326
- // PKU680 as nothing at all — but a recorded service call is a witness, and
327
- // a flow that has one is not the assertion-free flow the rule is for.
328
- const { criticals, cleanup } = await run([
329
- "await scenario.given('buys an apple', 'buysAnApple', { qty: 1 }, { actor: actors.shopper })",
330
- "await scenario.expectService('the receipt was emailed', 'emailService.send', { actor: actors.shopper })",
331
- ])
332
- try {
333
- assert.equal(
334
- criticals.filter((c) => c.code === 'PKU680').length,
335
- 0,
336
- `got: ${JSON.stringify(criticals)}`
337
- )
338
- } finally {
339
- await cleanup()
340
- }
341
- })
342
-
343
- test('a graded agent run is a witness too', async () => {
344
- // An agent's answer cannot be matched against a fixed string, so a scenario
345
- // that grades one is asserting — even though `expectScore`, like the other
346
- // helpers, is an inline step that reaches PKU680 as nothing at all.
347
- const { criticals, cleanup } = await run([
348
- "const answer = await scenario.when('asks for a summary', 'runAssistant', { prompt: 'hi' }, { actor: actors.shopper })",
349
- "await scenario.expectScore('answered briefly', answer.runId, 'brevity', { atLeast: 0.8 })",
350
- ])
351
- try {
352
- assert.equal(
353
- criticals.filter((c) => c.code === 'PKU680').length,
354
- 0,
355
- `got: ${JSON.stringify(criticals)}`
356
- )
357
- } finally {
358
- await cleanup()
359
- }
360
- })
361
-
362
- test('an expectation parked in an uncalled helper is still no assertion', async () => {
363
- const { criticals, cleanup } = await run([
364
- 'const neverCalled = async () => {',
365
- " await scenario.expectService('the receipt was emailed', 'emailService.send')",
366
- '}',
367
- "await scenario.given('buys an apple', 'buysAnApple', { qty: 1 }, { actor: actors.shopper })",
368
- ])
369
- try {
370
- assert.ok(
371
- criticals.find((c) => c.code === 'PKU680'),
372
- `expected PKU680 — the helper never runs, so nothing is asserted; got: ${JSON.stringify(criticals)}`
373
- )
374
- } finally {
375
- await cleanup()
376
- }
377
- })
378
-
379
- test('a scenario with an assertion is not flagged', async () => {
380
- const { criticals, cleanup } = await run([
381
- "await scenario.given('buys an apple', 'buysAnApple', { qty: 1 }, { actor: actors.shopper })",
382
- "await scenario.then('sees a receipt', 'seesAReceipt', {}, { actor: actors.shopper })",
383
- ])
384
- try {
385
- assert.equal(
386
- criticals.filter((c) => c.code === 'PKU680').length,
387
- 0,
388
- `got: ${JSON.stringify(criticals)}`
389
- )
390
- } finally {
391
- await cleanup()
392
- }
393
- })
394
-
395
- test('a scenario driving only rpc steps is not flagged — it has no step ladder to assert on', async () => {
396
- const { criticals, cleanup } = await run([
397
- "await scenario.do('reads the order', 'orderGet', { orderId: 'o1' })",
398
- ])
399
- try {
400
- assert.equal(
401
- criticals.filter((c) => c.code === 'PKU680').length,
402
- 0,
403
- `the rule is about scenarios written as given/when/then, got: ${JSON.stringify(criticals)}`
404
- )
405
- } finally {
406
- await cleanup()
407
- }
408
- })
409
-
410
- test('a non-browser step without an actor is allowed', async () => {
411
- const { criticals, cleanup } = await run([
412
- "await scenario.given('sees a receipt', 'seesAReceipt', {})",
413
- ])
414
- try {
415
- assert.equal(
416
- criticals.filter((c) => c.code === 'PKU677').length,
417
- 0,
418
- `setup steps need no actor, got: ${JSON.stringify(criticals)}`
419
- )
420
- } finally {
421
- await cleanup()
422
- }
423
- })
424
-
425
- test('a step is marked as a step RPC in its function meta', async () => {
426
- const { state, cleanup } = await run([
427
- "await scenario.given('buys an apple', 'buysAnApple', { qty: 1 }, { actor: actors.shopper })",
428
- ])
429
- try {
430
- // The fourth RPC kind, alongside public/private/remote: a name that is
431
- // dispatched inside a run and refused everywhere else.
432
- assert.equal(state.functions.meta['buysAnApple']?.scenarioStep, true)
433
- assert.equal(state.functions.meta['seesAReceipt']?.scenarioStep, true)
434
- } finally {
435
- await cleanup()
436
- }
437
- })
438
-
439
- test('an ordinary function is not a step RPC', async () => {
440
- const { state, cleanup } = await run([
441
- "await scenario.given('buys an apple', 'buysAnApple', { qty: 1 }, { actor: actors.shopper })",
442
- ])
443
- try {
444
- assert.equal(state.functions.meta['shopFlow']?.scenarioStep, undefined)
445
- } finally {
446
- await cleanup()
447
- }
448
- })
449
-
450
- test('a closure in the scenario body does not have its statements whitelisted', async () => {
451
- const { state, cleanup } = await run([
452
- 'const safely = async (fn: any) => { try { return await fn() } catch { return null } }',
453
- 'void safely',
454
- "await scenario.given('buys an apple', 'buysAnApple', { qty: 1 }, { actor: actors.shopper })",
455
- ])
456
- try {
457
- const steps = scenarioSteps(state)
458
- assert.equal(
459
- steps.length,
460
- 1,
461
- `a try/catch inside a closure must not drop the scenario's steps, got ${steps.length}`
462
- )
463
- assert.equal(steps[0]!.stepFunc, 'buysAnApple')
464
- } finally {
465
- await cleanup()
466
- }
467
- })
468
-
469
- test('destructuring a step result is reported, not silently dropped', async () => {
470
- const { state, criticals, cleanup } = await run([
471
- "const { ok } = await scenario.given('sees a receipt', 'seesAReceipt', {})",
472
- 'void ok',
473
- ])
474
- try {
475
- const steps = scenarioSteps(state)
476
- assert.equal(
477
- steps.length,
478
- 0,
479
- 'the DSL does not model a destructured step result'
480
- )
481
- assert.ok(
482
- criticals.find((c) => c.code === 'PKU679'),
483
- `a step the DSL cannot model must be loud, got: ${JSON.stringify(criticals)}`
484
- )
485
- } finally {
486
- await cleanup()
487
- }
488
- })
489
-
490
- test('a module-level constant used as step input is inlined, not read off the trigger', async () => {
491
- const { state, cleanup } = await run(
492
- [
493
- "await scenario.given('buys an apple', 'buysAnApple', { qty: QTY }, { actor: actors.shopper })",
494
- ],
495
- ['const QTY = 3']
496
- )
497
- try {
498
- const steps = scenarioSteps(state)
499
- assert.equal(steps.length, 1)
500
- assert.deepEqual(
501
- steps[0]!.inputs?.qty,
502
- { from: 'literal', value: 3 },
503
- `a constant declared outside the scenario is not a trigger field, got: ${JSON.stringify(steps[0]!.inputs)}`
504
- )
505
- } finally {
506
- await cleanup()
507
- }
508
- })
509
- })
@@ -1,120 +0,0 @@
1
- import * as ts from 'typescript'
2
- import {
3
- getPropertyValue,
4
- getCommonWireMetaData,
5
- } from '../utils/get-property-value.js'
6
- import type { AddWiring } from '../types.js'
7
- import {
8
- extractFunctionName,
9
- makeContextBasedId,
10
- } from '../utils/extract-function-name.js'
11
- import { getPropertyAssignmentInitializer } from '../utils/type-utils.js'
12
- import { ensureInlineWiringFunction } from '../utils/ensure-function-metadata.js'
13
- import { resolveMiddleware } from '../utils/middleware.js'
14
- import { extractWireNames } from '../utils/post-process.js'
15
- import { resolveAddonName } from '../utils/resolve-addon-package.js'
16
-
17
- import { ErrorCode } from '../error-codes.js'
18
- export const addSchedule: AddWiring = (
19
- logger,
20
- node,
21
- checker,
22
- state,
23
- options
24
- ) => {
25
- if (!ts.isCallExpression(node)) {
26
- return
27
- }
28
-
29
- const args = node.arguments
30
- const firstArg = args[0]
31
- const expression = node.expression
32
-
33
- // Check if the call is to addScheduledTask
34
- if (!ts.isIdentifier(expression) || expression.text !== 'wireScheduler') {
35
- return
36
- }
37
-
38
- if (!firstArg) {
39
- return
40
- }
41
-
42
- if (ts.isObjectLiteralExpression(firstArg)) {
43
- const obj = firstArg
44
-
45
- const nameValue = getPropertyValue(obj, 'name') as string | null
46
- const scheduleValue = getPropertyValue(obj, 'schedule') as string | null
47
- const { disabled, tags, summary, description, errors } =
48
- getCommonWireMetaData(obj, 'Scheduler', nameValue, logger, checker)
49
-
50
- if (disabled) return
51
-
52
- const funcInitializer = getPropertyAssignmentInitializer(
53
- obj,
54
- 'func',
55
- true,
56
- checker
57
- )
58
- if (!funcInitializer) {
59
- logger.critical(
60
- ErrorCode.MISSING_FUNC,
61
- `No valid 'func' property for scheduled task '${nameValue}'.`
62
- )
63
- return
64
- }
65
-
66
- const extracted = extractFunctionName(
67
- funcInitializer,
68
- checker,
69
- state.rootDir
70
- )
71
- let pikkuFuncId = extracted.pikkuFuncId
72
- if (pikkuFuncId.startsWith('__temp_') && nameValue) {
73
- pikkuFuncId = makeContextBasedId('scheduler', nameValue)
74
- }
75
-
76
- const packageName = ts.isIdentifier(funcInitializer)
77
- ? resolveAddonName(
78
- funcInitializer,
79
- checker,
80
- state.rpc.wireAddonDeclarations
81
- )
82
- : null
83
-
84
- if (!nameValue || !scheduleValue) {
85
- return
86
- }
87
-
88
- // Register metadata for a func inlined into the wiring (see helper).
89
- ensureInlineWiringFunction(
90
- state,
91
- pikkuFuncId,
92
- nameValue,
93
- funcInitializer,
94
- checker,
95
- extracted.isHelper
96
- )
97
-
98
- // --- resolve middleware ---
99
- const middleware = resolveMiddleware(state, obj, tags, checker)
100
-
101
- // --- track used functions/middleware for service aggregation ---
102
- state.serviceAggregation.usedFunctions.add(pikkuFuncId)
103
- extractWireNames(middleware).forEach((name) =>
104
- state.serviceAggregation.usedMiddleware.add(name)
105
- )
106
-
107
- state.scheduledTasks.files.add(node.getSourceFile().fileName)
108
- state.scheduledTasks.meta[nameValue] = {
109
- pikkuFuncId,
110
- ...(packageName && { packageName }),
111
- name: nameValue,
112
- schedule: scheduleValue,
113
- summary,
114
- description,
115
- errors,
116
- tags,
117
- middleware,
118
- }
119
- }
120
- }