@pikku/inspector 0.12.75 → 0.12.82

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 (223) hide show
  1. package/CHANGELOG.md +357 -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-middleware.js +66 -42
  8. package/dist/add/add-wire-addon.js +7 -4
  9. package/dist/add/add-wire-remote-addon.js +1 -0
  10. package/dist/error-codes.d.ts +1 -0
  11. package/dist/error-codes.js +1 -0
  12. package/dist/index.d.ts +3 -1
  13. package/dist/index.js +3 -1
  14. package/dist/inspector.js +5 -0
  15. package/dist/surface/collect-surface.d.ts +58 -0
  16. package/dist/surface/collect-surface.js +613 -0
  17. package/dist/types.d.ts +36 -12
  18. package/dist/utils/filter-inspector-state.js +14 -0
  19. package/dist/utils/load-addon-functions-meta.js +24 -11
  20. package/dist/utils/post-process.js +15 -0
  21. package/dist/utils/read-module-specifiers.d.ts +8 -0
  22. package/dist/utils/read-module-specifiers.js +23 -0
  23. package/dist/utils/read-tsconfig-out-dir.d.ts +5 -0
  24. package/dist/utils/read-tsconfig-out-dir.js +12 -0
  25. package/dist/utils/resolve-deploy-target.d.ts +7 -0
  26. package/dist/utils/resolve-deploy-target.js +15 -7
  27. package/dist/utils/serialize-inspector-state.d.ts +6 -0
  28. package/dist/utils/serialize-inspector-state.js +10 -0
  29. package/dist/visit.js +4 -0
  30. package/package.json +16 -6
  31. package/run-tests.sh +21 -6
  32. package/src/add/add-addon-bans.ts +0 -85
  33. package/src/add/add-agent-scorer.test.ts +0 -197
  34. package/src/add/add-agent-scorer.ts +0 -83
  35. package/src/add/add-agent.test.ts +0 -87
  36. package/src/add/add-agent.ts +0 -638
  37. package/src/add/add-approval-description.ts +0 -76
  38. package/src/add/add-auth.test.ts +0 -791
  39. package/src/add/add-auth.ts +0 -424
  40. package/src/add/add-channel.ts +0 -778
  41. package/src/add/add-cli-renderers.test.ts +0 -74
  42. package/src/add/add-cli.ts +0 -1006
  43. package/src/add/add-credential.ts +0 -186
  44. package/src/add/add-feature.test.ts +0 -306
  45. package/src/add/add-feature.ts +0 -253
  46. package/src/add/add-file-extends-core-type.ts +0 -66
  47. package/src/add/add-file-with-config.ts +0 -45
  48. package/src/add/add-file-with-factory.test.ts +0 -134
  49. package/src/add/add-file-with-factory.ts +0 -288
  50. package/src/add/add-functions.test.ts +0 -743
  51. package/src/add/add-functions.ts +0 -1548
  52. package/src/add/add-gateway.ts +0 -143
  53. package/src/add/add-http-route-ref.test.ts +0 -149
  54. package/src/add/add-http-route.ts +0 -530
  55. package/src/add/add-http-routes.ts +0 -471
  56. package/src/add/add-keyed-wiring.ts +0 -180
  57. package/src/add/add-mcp-prompt.ts +0 -157
  58. package/src/add/add-mcp-resource.ts +0 -175
  59. package/src/add/add-middleware.test.ts +0 -196
  60. package/src/add/add-middleware.ts +0 -879
  61. package/src/add/add-permission-auth.test.ts +0 -59
  62. package/src/add/add-permission.ts +0 -311
  63. package/src/add/add-personas.test.ts +0 -406
  64. package/src/add/add-personas.ts +0 -287
  65. package/src/add/add-queue-worker.ts +0 -117
  66. package/src/add/add-rpc-invocations.test.ts +0 -287
  67. package/src/add/add-rpc-invocations.ts +0 -221
  68. package/src/add/add-scenario-step.test.ts +0 -509
  69. package/src/add/add-schedule.ts +0 -120
  70. package/src/add/add-scope.test.ts +0 -419
  71. package/src/add/add-scope.ts +0 -221
  72. package/src/add/add-secret.ts +0 -10
  73. package/src/add/add-subject-scenario-steps.test.ts +0 -161
  74. package/src/add/add-system-role.test.ts +0 -297
  75. package/src/add/add-system-role.ts +0 -162
  76. package/src/add/add-trigger.ts +0 -165
  77. package/src/add/add-variable.ts +0 -10
  78. package/src/add/add-wire-addon.test.ts +0 -185
  79. package/src/add/add-wire-addon.ts +0 -146
  80. package/src/add/add-wire-remote-addon.ts +0 -80
  81. package/src/add/add-workflow-edge-cases.test.ts +0 -201
  82. package/src/add/add-workflow-fanout.test.ts +0 -166
  83. package/src/add/add-workflow-graph-foreach.test.ts +0 -174
  84. package/src/add/add-workflow-graph-input.test.ts +0 -94
  85. package/src/add/add-workflow-graph-notes.test.ts +0 -74
  86. package/src/add/add-workflow-graph.ts +0 -644
  87. package/src/add/add-workflow-on-error.test.ts +0 -118
  88. package/src/add/add-workflow-return-switch.test.ts +0 -121
  89. package/src/add/add-workflow-silent-drops.test.ts +0 -261
  90. package/src/add/add-workflow-sleep-expression.test.ts +0 -96
  91. package/src/add/add-workflow.test.ts +0 -155
  92. package/src/add/add-workflow.ts +0 -482
  93. package/src/add/addon-bans.test.ts +0 -121
  94. package/src/add/addon-contracts.test.ts +0 -221
  95. package/src/add/allow-escape-hatches.test.ts +0 -144
  96. package/src/add/audit-meta.test.ts +0 -75
  97. package/src/add/classified-sink-check.test.ts +0 -347
  98. package/src/add/config-value-metadata.test.ts +0 -263
  99. package/src/add/dynamic-import-check.test.ts +0 -119
  100. package/src/add/expect-eventually-scenario-only.test.ts +0 -108
  101. package/src/add/function-body-span.test.ts +0 -96
  102. package/src/add/inline-wiring-function.test.ts +0 -104
  103. package/src/add/pii-check.test.ts +0 -258
  104. package/src/add/rpc-type-cast.test.ts +0 -123
  105. package/src/add/scenario-instrumentation.test.ts +0 -89
  106. package/src/add/scenario-instrumentation.ts +0 -42
  107. package/src/add/scenario-marker.test.ts +0 -89
  108. package/src/add/scenario-not-extractable.test.ts +0 -159
  109. package/src/add/scenario-services.test.ts +0 -103
  110. package/src/add/scenario-skip.test.ts +0 -83
  111. package/src/add/validate-workflow-graph-addons.test.ts +0 -102
  112. package/src/add/wire-name-literal.test.ts +0 -117
  113. package/src/error-codes-emitted.test.ts +0 -76
  114. package/src/error-codes.ts +0 -145
  115. package/src/index.ts +0 -66
  116. package/src/inspector.ts +0 -575
  117. package/src/schema-generator.ts +0 -1
  118. package/src/source-file-cache.ts +0 -73
  119. package/src/tsconfig-paths.test.ts +0 -90
  120. package/src/types-map.ts +0 -141
  121. package/src/types.ts +0 -775
  122. package/src/utils/annotate-http-route-auth.test.ts +0 -161
  123. package/src/utils/annotate-http-route-auth.ts +0 -53
  124. package/src/utils/check-classified-sinks.ts +0 -215
  125. package/src/utils/check-pii-output.ts +0 -120
  126. package/src/utils/compute-diagnostics.test.ts +0 -106
  127. package/src/utils/compute-required-schemas.ts +0 -48
  128. package/src/utils/contract-hashes.test.ts +0 -553
  129. package/src/utils/contract-hashes.ts +0 -387
  130. package/src/utils/custom-types-generator.test.ts +0 -124
  131. package/src/utils/custom-types-generator.ts +0 -150
  132. package/src/utils/detect-schema-vendor.test.ts +0 -123
  133. package/src/utils/detect-schema-vendor.ts +0 -120
  134. package/src/utils/does-type-extend-core-type.ts +0 -53
  135. package/src/utils/ensure-function-metadata.ts +0 -385
  136. package/src/utils/extract-function-name.ts +0 -612
  137. package/src/utils/extract-function-node.ts +0 -70
  138. package/src/utils/extract-node-value.test.ts +0 -117
  139. package/src/utils/extract-node-value.ts +0 -172
  140. package/src/utils/extract-secret-usage.test.ts +0 -81
  141. package/src/utils/extract-secret-usage.ts +0 -73
  142. package/src/utils/extract-services.ts +0 -70
  143. package/src/utils/extract-surface-usage.test.ts +0 -147
  144. package/src/utils/extract-surface-usage.ts +0 -121
  145. package/src/utils/filter-inspector-state.test.ts +0 -2232
  146. package/src/utils/filter-inspector-state.ts +0 -1298
  147. package/src/utils/filter-utils.test.ts +0 -843
  148. package/src/utils/filter-utils.ts +0 -161
  149. package/src/utils/find-root-dir.ts +0 -68
  150. package/src/utils/find-zod-transform.test.ts +0 -70
  151. package/src/utils/find-zod-transform.ts +0 -103
  152. package/src/utils/get-exported-variable-name.ts +0 -48
  153. package/src/utils/get-files-and-methods.ts +0 -157
  154. package/src/utils/get-property-value.test.ts +0 -141
  155. package/src/utils/get-property-value.ts +0 -331
  156. package/src/utils/hash.ts +0 -26
  157. package/src/utils/literal-properties.ts +0 -97
  158. package/src/utils/load-addon-functions-meta.test.ts +0 -157
  159. package/src/utils/load-addon-functions-meta.ts +0 -508
  160. package/src/utils/load-addon-scopes.test.ts +0 -138
  161. package/src/utils/middleware.test.ts +0 -204
  162. package/src/utils/middleware.ts +0 -303
  163. package/src/utils/nested-project-filter.test.ts +0 -53
  164. package/src/utils/nested-project-filter.ts +0 -28
  165. package/src/utils/permissions-shorthand.test.ts +0 -79
  166. package/src/utils/permissions.test.ts +0 -125
  167. package/src/utils/permissions.ts +0 -129
  168. package/src/utils/post-process.test.ts +0 -1094
  169. package/src/utils/post-process.ts +0 -1604
  170. package/src/utils/resolve-addon-package.ts +0 -106
  171. package/src/utils/resolve-core-type.test.ts +0 -142
  172. package/src/utils/resolve-core-type.ts +0 -35
  173. package/src/utils/resolve-deploy-target.test.ts +0 -135
  174. package/src/utils/resolve-deploy-target.ts +0 -65
  175. package/src/utils/resolve-function-meta.ts +0 -25
  176. package/src/utils/resolve-function-types.ts +0 -42
  177. package/src/utils/resolve-identifier.ts +0 -46
  178. package/src/utils/resolve-ref-contract.ts +0 -71
  179. package/src/utils/resolve-versions.test.ts +0 -499
  180. package/src/utils/resolve-versions.ts +0 -228
  181. package/src/utils/schema-cache.test.ts +0 -124
  182. package/src/utils/schema-generator.test.ts +0 -272
  183. package/src/utils/schema-generator.ts +0 -780
  184. package/src/utils/secret-alias-services.test.ts +0 -146
  185. package/src/utils/secret-alias-services.ts +0 -58
  186. package/src/utils/serialize-inspector-state.ts +0 -742
  187. package/src/utils/serialize-mcp-json.ts +0 -150
  188. package/src/utils/serialize-middleware-groups-meta.ts +0 -36
  189. package/src/utils/serialize-openapi-json.ts +0 -277
  190. package/src/utils/serialize-permissions-groups-meta.ts +0 -7
  191. package/src/utils/single-declaration.ts +0 -57
  192. package/src/utils/type-utils.ts +0 -145
  193. package/src/utils/validate-auth-sessionless.ts +0 -30
  194. package/src/utils/validate-exposed-functions-gated.test.ts +0 -230
  195. package/src/utils/validate-exposed-functions-gated.ts +0 -103
  196. package/src/utils/validate-tags-resolve-to-middleware.test.ts +0 -108
  197. package/src/utils/validate-tags-resolve-to-middleware.ts +0 -48
  198. package/src/utils/workflow/derive-workflow-plan.test.ts +0 -178
  199. package/src/utils/workflow/derive-workflow-plan.ts +0 -117
  200. package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +0 -1465
  201. package/src/utils/workflow/dsl/deserialize-fanout-body.test.ts +0 -71
  202. package/src/utils/workflow/dsl/deserialize-fanout-fidelity.test.ts +0 -99
  203. package/src/utils/workflow/dsl/deserialize-graph-next.test.ts +0 -161
  204. package/src/utils/workflow/dsl/deserialize-roundtrip-bindings.test.ts +0 -121
  205. package/src/utils/workflow/dsl/deserialize-roundtrip-values.test.ts +0 -116
  206. package/src/utils/workflow/dsl/deserialize-roundtrip.test.ts +0 -145
  207. package/src/utils/workflow/dsl/extract-approval-policy.test.ts +0 -71
  208. package/src/utils/workflow/dsl/extract-dsl-workflow.ts +0 -2254
  209. package/src/utils/workflow/dsl/index.ts +0 -11
  210. package/src/utils/workflow/dsl/patterns.ts +0 -440
  211. package/src/utils/workflow/dsl/validation.ts +0 -203
  212. package/src/utils/workflow/graph/convert-dsl-to-graph-skip.test.ts +0 -35
  213. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +0 -543
  214. package/src/utils/workflow/graph/fanout-node-identity.test.ts +0 -105
  215. package/src/utils/workflow/graph/finalize-workflow-wires.ts +0 -310
  216. package/src/utils/workflow/graph/finalize-workflows.test.ts +0 -50
  217. package/src/utils/workflow/graph/finalize-workflows.ts +0 -130
  218. package/src/utils/workflow/graph/index.ts +0 -28
  219. package/src/utils/workflow/graph/serialize-workflow-graph.ts +0 -240
  220. package/src/utils/workflow/graph/workflow-graph.types.ts +0 -281
  221. package/src/visit.ts +0 -181
  222. package/tsconfig.json +0 -19
  223. 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
- }