@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,406 +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 { ErrorCode } from '../error-codes.js'
8
- import type { InspectorLogger } from '../types.js'
9
-
10
- const makeLogger = (criticals: Array<{ code: ErrorCode; message: string }>) =>
11
- ({
12
- debug: () => {},
13
- info: () => {},
14
- warn: () => {},
15
- error: () => {},
16
- diagnostic: ({ code, message }) => {
17
- criticals.push({ code, message })
18
- },
19
- critical: (code: ErrorCode, message: string) => {
20
- criticals.push({ code, message })
21
- },
22
- hasCriticalErrors: () => criticals.length > 0,
23
- }) satisfies InspectorLogger
24
-
25
- const inspectSource = async (source: string) => {
26
- const rootDir = await mkdtemp(join(tmpdir(), 'pikku-add-personas-'))
27
- const file = join(rootDir, 'personas.ts')
28
- await writeFile(file, source)
29
- const criticals: Array<{ code: ErrorCode; message: string }> = []
30
- try {
31
- const state = await inspect(makeLogger(criticals), [file], { rootDir })
32
- return { state, criticals }
33
- } finally {
34
- await rm(rootDir, { recursive: true, force: true })
35
- }
36
- }
37
-
38
- /** Inspects several files at once, keyed by basename. */
39
- const inspectSources = async (sources: Record<string, string>) => {
40
- const rootDir = await mkdtemp(join(tmpdir(), 'pikku-add-personas-multi-'))
41
- const files: Record<string, string> = {}
42
- for (const [name, source] of Object.entries(sources)) {
43
- files[name] = join(rootDir, name)
44
- await writeFile(files[name]!, source)
45
- }
46
- const criticals: Array<{ code: ErrorCode; message: string }> = []
47
- try {
48
- const state = await inspect(makeLogger(criticals), Object.values(files), {
49
- rootDir,
50
- })
51
- return { state, criticals, files }
52
- } finally {
53
- await rm(rootDir, { recursive: true, force: true })
54
- }
55
- }
56
-
57
- /** The declared world every persona below is checked against. */
58
- const withRoles = (...lines: string[]) =>
59
- [
60
- "import { defineScope } from '@pikku/core/scope'",
61
- "import { defineSystemRole } from '@pikku/core/role'",
62
- "import { definePersonas } from '@pikku/core/persona'",
63
- 'defineScope({ catalogue: { scopes: { read: {} } }, admin: {} })',
64
- 'defineSystemRole({',
65
- " buyer: { scopes: ['catalogue:read'] },",
66
- " admin: { scopes: ['admin'] },",
67
- '})',
68
- ...lines,
69
- ].join('\n')
70
-
71
- describe('addPersonas inspector', () => {
72
- test('extracts a whole person', async () => {
73
- const { state, criticals } = await inspectSource(
74
- withRoles(
75
- 'definePersonas({',
76
- ' susan: {',
77
- " name: 'Susan',",
78
- " jobTitle: 'Buys for a small café',",
79
- " roles: ['buyer'],",
80
- " personality: 'Hunts cheap deals.',",
81
- " goals: ['Get the weekly order in under five minutes'],",
82
- " tags: ['commerce'],",
83
- " disposition: 'careless',",
84
- ' tuning: { repeatRate: 0.3 },',
85
- " fixtures: ['seedCatalogue'],",
86
- ' account: {},',
87
- ' },',
88
- '})'
89
- )
90
- )
91
-
92
- assert.deepEqual(criticals, [])
93
- assert.equal(state.personas.definitions.length, 1)
94
- const susan = state.personas.definitions[0]!
95
- assert.equal(susan.id, 'susan')
96
- assert.equal(susan.name, 'Susan')
97
- assert.equal(susan.jobTitle, 'Buys for a small café')
98
- assert.deepEqual(susan.roles, ['buyer'])
99
- assert.equal(susan.personality, 'Hunts cheap deals.')
100
- assert.deepEqual(susan.goals, [
101
- 'Get the weekly order in under five minutes',
102
- ])
103
- assert.deepEqual(susan.tags, ['commerce'])
104
- assert.equal(susan.disposition, 'careless')
105
- assert.deepEqual(susan.tuning, { repeatRate: 0.3 })
106
- assert.deepEqual(susan.fixtures, ['seedCatalogue'])
107
- assert.deepEqual(susan.account, {})
108
- assert.equal(susan.runnable, true)
109
- })
110
-
111
- test('the app a person signs into survives into the meta', async () => {
112
- const { state, criticals } = await inspectSource(
113
- withRoles(
114
- 'definePersonas({',
115
- ' susan: {',
116
- " name: 'Susan',",
117
- " app: 'storefront',",
118
- ' },',
119
- '})'
120
- )
121
- )
122
-
123
- assert.deepEqual(criticals, [])
124
- assert.equal(state.personas.definitions[0]!.app, 'storefront')
125
- })
126
-
127
- test('a person who names no app has none, rather than a guessed one', async () => {
128
- const { state } = await inspectSource(
129
- withRoles('definePersonas({', " susan: { name: 'Susan' },", '})')
130
- )
131
-
132
- assert.equal(state.personas.definitions[0]!.app, undefined)
133
- })
134
-
135
- test('extracts a declared avatar', async () => {
136
- const { state } = await inspectSource(
137
- withRoles(
138
- 'definePersonas({',
139
- ' susan: {',
140
- " name: 'Susan',",
141
- " avatarUrl: '/avatars/susan.png',",
142
- ' },',
143
- '})'
144
- )
145
- )
146
- assert.equal(state.personas.definitions[0]!.avatarUrl, '/avatars/susan.png')
147
- })
148
-
149
- // The console draws a colour-and-icon avatar from the persona's id when none
150
- // is declared, so an absent field has to stay absent rather than become an
151
- // empty string the renderer would treat as a broken image.
152
- test('leaves the avatar absent when none is declared', async () => {
153
- const { state } = await inspectSource(
154
- withRoles('definePersonas({', " susan: { name: 'Susan' },", '})')
155
- )
156
- assert.equal(state.personas.definitions[0]!.avatarUrl, undefined)
157
- })
158
-
159
- test('extracts the environments a persona declares', async () => {
160
- const { state } = await inspectSource(
161
- withRoles(
162
- 'definePersonas({',
163
- ' robin: {',
164
- " name: 'Robin',",
165
- " disposition: 'accountable',",
166
- " environments: ['staging', 'prod'],",
167
- ' },',
168
- '})'
169
- )
170
- )
171
- assert.deepEqual(state.personas.definitions[0]!.environments, [
172
- 'staging',
173
- 'prod',
174
- ])
175
- })
176
-
177
- // "Everywhere but production" is a fact about the config, and the meta file
178
- // outlives whichever environments were configured the day it was written —
179
- // so the absence is preserved rather than resolved into a list here.
180
- test('leaves environments absent when none are declared', async () => {
181
- const { state } = await inspectSource(
182
- withRoles('definePersonas({', " susan: { name: 'Susan' },", '})')
183
- )
184
- assert.equal(state.personas.definitions[0]!.environments, undefined)
185
- })
186
-
187
- test('the declaration key is the id', async () => {
188
- const { state } = await inspectSource(
189
- withRoles(
190
- 'definePersonas({',
191
- " susan: { name: 'Susan' },",
192
- " mallory: { name: 'Mallory' },",
193
- '})'
194
- )
195
- )
196
- assert.deepEqual(
197
- state.personas.definitions.map((p) => p.id),
198
- ['susan', 'mallory']
199
- )
200
- })
201
-
202
- test('a persona without a name is refused', async () => {
203
- const { state, criticals } = await inspectSource(
204
- withRoles('definePersonas({', " susan: { roles: ['buyer'] },", '})')
205
- )
206
- assert.equal(state.personas.definitions.length, 0)
207
- assert.ok(criticals.some((c) => /needs a literal 'name'/.test(c.message)))
208
- })
209
-
210
- test('an unknown disposition is refused rather than defaulted', async () => {
211
- const { state, criticals } = await inspectSource(
212
- withRoles(
213
- 'definePersonas({',
214
- " susan: { name: 'Susan', disposition: 'grumpy' },",
215
- '})'
216
- )
217
- )
218
- assert.equal(state.personas.definitions.length, 0)
219
- assert.ok(
220
- criticals.some((c) => /unknown disposition 'grumpy'/.test(c.message))
221
- )
222
- })
223
-
224
- // `repeatRate: 18` meaning 18% would silently double every call, and the run
225
- // would read as a product bug.
226
- test('a tuning dial outside its range is refused', async () => {
227
- const { state, criticals } = await inspectSource(
228
- withRoles(
229
- 'definePersonas({',
230
- " susan: { name: 'Susan', tuning: { repeatRate: 18 } },",
231
- '})'
232
- )
233
- )
234
- assert.equal(state.personas.definitions.length, 0)
235
- assert.ok(
236
- criticals.some((c) =>
237
- /repeatRate must be between 0 and 1/.test(c.message)
238
- )
239
- )
240
- })
241
-
242
- test('a linked account is extracted alongside the primary one', async () => {
243
- const { state, criticals } = await inspectSource(
244
- withRoles(
245
- 'definePersonas({',
246
- ' yasser: {',
247
- " name: 'Yasser',",
248
- ' account: {},',
249
- " linkedAccounts: { google: { provider: 'google' } },",
250
- ' },',
251
- '})'
252
- )
253
- )
254
- assert.deepEqual(criticals, [])
255
- const yasser = state.personas.definitions[0]!
256
- assert.deepEqual(yasser.account, {})
257
- assert.deepEqual(yasser.linkedAccounts, { google: { provider: 'google' } })
258
- // Their primary login is email+password, so they can still be driven.
259
- assert.equal(yasser.runnable, true)
260
- })
261
-
262
- // Driving a consent screen needs a human — refused at the CLI rather than
263
- // failing somewhere inside a browser.
264
- test('a persona whose own login is a provider is not runnable', async () => {
265
- const { state } = await inspectSource(
266
- withRoles(
267
- 'definePersonas({',
268
- " yasser: { name: 'Yasser', account: { provider: 'google' } },",
269
- '})'
270
- )
271
- )
272
- assert.equal(state.personas.definitions[0]!.runnable, false)
273
- })
274
-
275
- test('a person who is only ever acted upon says so', async () => {
276
- const { state } = await inspectSource(
277
- withRoles(
278
- 'definePersonas({',
279
- " target: { name: 'Target', account: {}, runnable: false },",
280
- '})'
281
- )
282
- )
283
- assert.equal(state.personas.definitions[0]!.runnable, false)
284
- })
285
-
286
- // One call site per codebase, so there is one place to read the cast from
287
- // and one place to add to. The keyed form above is how you declare more.
288
- test('a second definePersonas in the same file is refused', async () => {
289
- const { state, criticals } = await inspectSource(
290
- withRoles(
291
- "definePersonas({ susan: { name: 'Susan' } })",
292
- "definePersonas({ dave: { name: 'Dave' } })"
293
- )
294
- )
295
-
296
- const hit = criticals.find(
297
- (c) => c.code === ErrorCode.DUPLICATE_PERSONAS_DEFINITION
298
- )
299
- assert.ok(
300
- hit,
301
- `expected DUPLICATE_PERSONAS_DEFINITION, got ${JSON.stringify(criticals)}`
302
- )
303
- assert.deepEqual(
304
- state.personas.definitions.map((p) => p.id),
305
- ['susan']
306
- )
307
- })
308
-
309
- test('a second definePersonas in another file is refused', async () => {
310
- const { criticals, files } = await inspectSources({
311
- 'personas.ts': withRoles("definePersonas({ susan: { name: 'Susan' } })"),
312
- 'more-personas.ts': [
313
- "import { definePersonas } from '@pikku/core/persona'",
314
- "definePersonas({ dave: { name: 'Dave' } })",
315
- ].join('\n'),
316
- })
317
-
318
- const hit = criticals.find(
319
- (c) => c.code === ErrorCode.DUPLICATE_PERSONAS_DEFINITION
320
- )
321
- assert.ok(
322
- hit,
323
- `expected DUPLICATE_PERSONAS_DEFINITION, got ${JSON.stringify(criticals)}`
324
- )
325
- assert.ok(
326
- hit!.message.includes(files['personas.ts']!) &&
327
- hit!.message.includes(files['more-personas.ts']!),
328
- `expected both files named, got ${hit!.message}`
329
- )
330
- })
331
-
332
- // The CLI scaffolds personas of its own, and nobody adds a persona to a file
333
- // the next codegen run overwrites — so a generated declaration neither takes
334
- // the single slot nor collides with the app's own.
335
- test('a generated declaration neither claims the slot nor conflicts', async () => {
336
- const { state, criticals } = await inspectSources({
337
- 'pikku-personas.gen.ts': [
338
- "import { definePersonas } from '@pikku/core/persona'",
339
- "definePersonas({ dave: { name: 'Dave' } })",
340
- ].join('\n'),
341
- 'personas.ts': withRoles("definePersonas({ susan: { name: 'Susan' } })"),
342
- })
343
-
344
- assert.deepEqual(
345
- criticals.filter(
346
- (c) => c.code === ErrorCode.DUPLICATE_PERSONAS_DEFINITION
347
- ),
348
- [],
349
- `expected no duplicate critical, got ${JSON.stringify(criticals)}`
350
- )
351
- assert.deepEqual(state.personas.definitions.map((d) => d.id).sort(), [
352
- 'dave',
353
- 'susan',
354
- ])
355
- })
356
-
357
- // A half-read roles array typechecks, runs, and grants less than the source
358
- // says — so an unreadable one reads as absent.
359
- test('a computed roles array is not half-extracted', async () => {
360
- const { state } = await inspectSource(
361
- withRoles(
362
- 'const r = "buyer"',
363
- 'definePersonas({',
364
- " susan: { name: 'Susan', roles: ['buyer', r] },",
365
- '})'
366
- )
367
- )
368
- assert.deepEqual(state.personas.definitions[0]!.roles, [])
369
- })
370
- })
371
-
372
- describe('validatePersonaRoles', () => {
373
- test('a declared system role is accepted', async () => {
374
- const { criticals } = await inspectSource(
375
- withRoles(
376
- 'definePersonas({',
377
- " susan: { name: 'Susan', roles: ['buyer', 'admin'] },",
378
- '})'
379
- )
380
- )
381
- assert.deepEqual(criticals, [])
382
- })
383
-
384
- // The failure this prevents: an admin deletes 'invoicing-clerk' from the
385
- // console, and Susan goes on claiming to test it while 403ing everywhere.
386
- test('a role that is not declared fails the build', async () => {
387
- const { criticals } = await inspectSource(
388
- withRoles(
389
- 'definePersonas({',
390
- " susan: { name: 'Susan', roles: ['invoicing-clerk'] },",
391
- '})'
392
- )
393
- )
394
- const critical = criticals.find((c) => /invoicing-clerk/.test(c.message))
395
- assert.ok(critical, `expected a critical, got ${JSON.stringify(criticals)}`)
396
- assert.match(critical!.message, /may only name a system role/)
397
- assert.match(critical!.message, /admin, buyer/)
398
- })
399
-
400
- test('a persona with no roles at all is fine', async () => {
401
- const { criticals } = await inspectSource(
402
- withRoles('definePersonas({', " susan: { name: 'Susan' },", '})')
403
- )
404
- assert.deepEqual(criticals, [])
405
- })
406
- })
@@ -1,287 +0,0 @@
1
- import * as ts from 'typescript'
2
- import { DISPOSITIONS } from '@pikku/core/virtual-user'
3
- import { isRunnablePersona } from '@pikku/core/persona'
4
- import type { PersonaMeta, PersonaAccountMeta } from '@pikku/core/persona'
5
- import { ErrorCode } from '../error-codes.js'
6
- import { claimSingleDeclaration } from '../utils/single-declaration.js'
7
- import type { AddWiring, InspectorLogger } from '../types.js'
8
- import {
9
- booleanProperty,
10
- getProperty,
11
- numberProperty,
12
- stringArrayProperty,
13
- stringProperty,
14
- unwrapAs,
15
- } from '../utils/literal-properties.js'
16
-
17
- type Tuning = NonNullable<PersonaMeta['tuning']>
18
-
19
- /** A dial and the range outside which it is a mistake rather than a choice. */
20
- const RATES: [keyof Tuning, number, number][] = [
21
- ['temperature', 0, 2],
22
- ['repeatRate', 0, 1],
23
- ['reReadRate', 0, 1],
24
- ]
25
-
26
- /**
27
- * `{ moves, temperature, repeatRate, … }` — a disposition's dials, overridden.
28
- *
29
- * The ranges are checked here rather than at run time because these are the
30
- * kind of typo that does not throw: `repeatRate: 18` meaning 18% would silently
31
- * double every call the persona makes, and you would read the run as a product
32
- * bug.
33
- */
34
- const tuningProperty = (
35
- config: ts.ObjectLiteralExpression,
36
- onError: (message: string) => void
37
- ): Tuning | undefined => {
38
- const value = getProperty(config, 'tuning')
39
- if (!value || !ts.isObjectLiteralExpression(value)) {
40
- return undefined
41
- }
42
-
43
- const tuning: Tuning = {}
44
-
45
- const movesValue = getProperty(value, 'moves')
46
- if (movesValue && ts.isObjectLiteralExpression(movesValue)) {
47
- const moves: NonNullable<Tuning['moves']> = {}
48
- for (const move of ['continue', 'suspend', 'resume', 'abandon'] as const) {
49
- const weight = numberProperty(movesValue, move)
50
- if (weight === undefined) {
51
- continue
52
- }
53
- if (weight < 0) {
54
- onError(`tuning.moves.${move} must not be negative (got ${weight}).`)
55
- continue
56
- }
57
- moves[move] = weight
58
- }
59
- if (Object.keys(moves).length) {
60
- tuning.moves = moves
61
- }
62
- }
63
-
64
- for (const [name, min, max] of RATES) {
65
- const rate = numberProperty(value, name)
66
- if (rate === undefined) {
67
- continue
68
- }
69
- if (rate < min || rate > max) {
70
- onError(`tuning.${name} must be between ${min} and ${max} (got ${rate}).`)
71
- continue
72
- }
73
- Object.assign(tuning, { [name]: rate })
74
- }
75
-
76
- for (const flag of ['emptyMemory', 'readOnly', 'invertedOracle'] as const) {
77
- const declared = booleanProperty(value, flag)
78
- if (declared !== undefined) {
79
- tuning[flag] = declared
80
- }
81
- }
82
-
83
- const instructions = stringProperty(value, 'instructions')
84
- if (instructions !== undefined) {
85
- tuning.instructions = instructions
86
- }
87
-
88
- return Object.keys(tuning).length ? tuning : undefined
89
- }
90
-
91
- const accountProperty = (
92
- value: ts.Expression | undefined
93
- ): PersonaAccountMeta | undefined => {
94
- if (!value || !ts.isObjectLiteralExpression(value)) {
95
- return undefined
96
- }
97
- const provider = stringProperty(value, 'provider')
98
- return provider === undefined ? {} : { provider }
99
- }
100
-
101
- const linkedAccountsProperty = (
102
- config: ts.ObjectLiteralExpression,
103
- personaId: string,
104
- logger: InspectorLogger
105
- ): Record<string, PersonaAccountMeta> | undefined => {
106
- const value = getProperty(config, 'linkedAccounts')
107
- if (!value || !ts.isObjectLiteralExpression(value)) {
108
- return undefined
109
- }
110
-
111
- const accounts: Record<string, PersonaAccountMeta> = {}
112
- for (const prop of value.properties) {
113
- if (!ts.isPropertyAssignment(prop)) {
114
- continue
115
- }
116
- if (!ts.isIdentifier(prop.name) && !ts.isStringLiteral(prop.name)) {
117
- logger.critical(
118
- ErrorCode.NON_LITERAL_WIRE_NAME,
119
- `Persona '${personaId}' has a linked account whose key is not a literal.`
120
- )
121
- continue
122
- }
123
- const account = accountProperty(prop.initializer)
124
- if (!account) {
125
- logger.critical(
126
- ErrorCode.INVALID_VALUE,
127
- `Persona '${personaId}' linked account '${prop.name.text}' must be an object literal.`
128
- )
129
- continue
130
- }
131
- accounts[prop.name.text] = account
132
- }
133
-
134
- return Object.keys(accounts).length ? accounts : undefined
135
- }
136
-
137
- /**
138
- * Inspector for `definePersonas()` calls.
139
- *
140
- * A persona is entirely declaration — a person, their roles, what they want —
141
- * so there is no code body and nothing to resolve at runtime. What is read here
142
- * is the whole thing, and it is generated straight to `personas.gen.json` for
143
- * the CLI and the console to read without loading the app.
144
- *
145
- * `name` is the one field that cannot be defaulted. A persona without one is
146
- * not a person, and the console has nothing to show.
147
- */
148
- export const addPersonas: AddWiring = (logger, node, _checker, state) => {
149
- if (!ts.isCallExpression(node)) {
150
- return
151
- }
152
-
153
- const expression = node.expression
154
- if (!ts.isIdentifier(expression) || expression.text !== 'definePersonas') {
155
- return
156
- }
157
-
158
- const firstArg = node.arguments[0]
159
- if (!firstArg) {
160
- return
161
- }
162
-
163
- const unwrapped = unwrapAs(firstArg)
164
- if (!ts.isObjectLiteralExpression(unwrapped)) {
165
- logger.critical(
166
- ErrorCode.INVALID_VALUE,
167
- 'definePersonas() needs an inline object — personas are read statically, never evaluated.'
168
- )
169
- return
170
- }
171
-
172
- const sourceFile = node.getSourceFile().fileName
173
-
174
- if (
175
- !claimSingleDeclaration(
176
- logger,
177
- state.personas.files,
178
- ErrorCode.DUPLICATE_PERSONAS_DEFINITION,
179
- 'definePersonas',
180
- sourceFile
181
- )
182
- ) {
183
- return
184
- }
185
-
186
- const known = Object.keys(DISPOSITIONS)
187
-
188
- for (const prop of unwrapped.properties) {
189
- if (!ts.isPropertyAssignment(prop)) {
190
- continue
191
- }
192
-
193
- if (!ts.isIdentifier(prop.name) && !ts.isStringLiteral(prop.name)) {
194
- logger.critical(
195
- ErrorCode.NON_LITERAL_WIRE_NAME,
196
- 'A persona is declared with a key that is not a literal.'
197
- )
198
- continue
199
- }
200
- const id = prop.name.text
201
-
202
- if (!ts.isObjectLiteralExpression(prop.initializer)) {
203
- logger.critical(
204
- ErrorCode.INVALID_VALUE,
205
- `Persona '${id}' must be an object literal.`
206
- )
207
- continue
208
- }
209
- const config = prop.initializer
210
-
211
- const name = stringProperty(config, 'name')
212
- if (!name) {
213
- logger.critical(
214
- ErrorCode.INVALID_VALUE,
215
- `Persona '${id}' needs a literal 'name' — a persona is a person, and the console has nothing to show without one.`
216
- )
217
- continue
218
- }
219
-
220
- // The generated `definePersonas` types this as a union, so a bad value only
221
- // reaches here in source that does not compile. Caught anyway: a
222
- // disposition is the entire behaviour, and defaulting a typo to `realistic`
223
- // would run a different person than the one that was written.
224
- const disposition = stringProperty(config, 'disposition')
225
- if (disposition !== undefined && !known.includes(disposition)) {
226
- logger.critical(
227
- ErrorCode.INVALID_VALUE,
228
- `Persona '${id}' has an unknown disposition '${disposition}'. One of: ${known.join(', ')}`
229
- )
230
- continue
231
- }
232
-
233
- let tuningFailed = false
234
- const tuning = tuningProperty(config, (message) => {
235
- tuningFailed = true
236
- logger.critical(ErrorCode.INVALID_VALUE, `Persona '${id}': ${message}`)
237
- })
238
- if (tuningFailed) {
239
- continue
240
- }
241
-
242
- const account = accountProperty(getProperty(config, 'account'))
243
- const declaredRunnable = booleanProperty(config, 'runnable')
244
-
245
- const persona: PersonaMeta = {
246
- id,
247
- name,
248
- roles: stringArrayProperty(config, 'roles') ?? [],
249
- goals: stringArrayProperty(config, 'goals') ?? [],
250
- tags: stringArrayProperty(config, 'tags') ?? [],
251
- runnable: isRunnablePersona({
252
- ...(declaredRunnable !== undefined
253
- ? { runnable: declaredRunnable }
254
- : {}),
255
- ...(account ? { account } : {}),
256
- }),
257
- sourceFile,
258
- }
259
-
260
- const jobTitle = stringProperty(config, 'jobTitle')
261
- if (jobTitle !== undefined) persona.jobTitle = jobTitle
262
- const description = stringProperty(config, 'description')
263
- if (description !== undefined) persona.description = description
264
- const avatarUrl = stringProperty(config, 'avatarUrl')
265
- if (avatarUrl !== undefined) persona.avatarUrl = avatarUrl
266
- const personality = stringProperty(config, 'personality')
267
- if (personality !== undefined) persona.personality = personality
268
- const app = stringProperty(config, 'app')
269
- if (app !== undefined) persona.app = app
270
- if (disposition !== undefined) {
271
- persona.disposition = disposition as PersonaMeta['disposition']
272
- }
273
- if (tuning !== undefined) persona.tuning = tuning
274
- const fixtures = stringArrayProperty(config, 'fixtures')
275
- if (fixtures !== undefined) persona.fixtures = fixtures
276
- // Left absent rather than resolved here: "everywhere but production" is a
277
- // fact about the config, and the inspector has no business baking today's
278
- // environment list into a meta file that outlives it.
279
- const environments = stringArrayProperty(config, 'environments')
280
- if (environments !== undefined) persona.environments = environments
281
- if (account !== undefined) persona.account = account
282
- const linkedAccounts = linkedAccountsProperty(config, id, logger)
283
- if (linkedAccounts !== undefined) persona.linkedAccounts = linkedAccounts
284
-
285
- state.personas.definitions.push(persona)
286
- }
287
- }