@pikku/inspector 0.12.59 → 0.12.61

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 (132) hide show
  1. package/CHANGELOG.md +373 -0
  2. package/OPTIMIZATION-PLAN.md +19 -19
  3. package/dist/add/add-agent-scorer.d.ts +2 -0
  4. package/dist/add/{add-ai-scorer.js → add-agent-scorer.js} +4 -4
  5. package/dist/add/{add-ai-agent.d.ts → add-agent.d.ts} +1 -1
  6. package/dist/add/{add-ai-agent.js → add-agent.js} +11 -11
  7. package/dist/add/add-auth.js +1 -1
  8. package/dist/add/add-cli.js +5 -0
  9. package/dist/add/add-functions.js +10 -1
  10. package/dist/add/add-http-route.js +18 -18
  11. package/dist/add/add-keyed-wiring.d.ts +2 -0
  12. package/dist/add/add-keyed-wiring.js +3 -1
  13. package/dist/add/add-middleware.js +5 -5
  14. package/dist/add/add-personas.js +3 -0
  15. package/dist/add/add-secret.js +1 -0
  16. package/dist/add/add-variable.js +1 -0
  17. package/dist/add/add-wire-remote-addon.js +2 -1
  18. package/dist/add/add-workflow-graph.js +63 -6
  19. package/dist/error-codes.d.ts +3 -1
  20. package/dist/error-codes.js +3 -1
  21. package/dist/index.d.ts +3 -1
  22. package/dist/index.js +1 -0
  23. package/dist/inspector.js +13 -3
  24. package/dist/types.d.ts +10 -5
  25. package/dist/utils/annotate-http-route-auth.js +2 -1
  26. package/dist/utils/check-secret-sinks.js +2 -1
  27. package/dist/utils/compute-required-schemas.d.ts +1 -1
  28. package/dist/utils/contract-hashes.d.ts +2 -1
  29. package/dist/utils/contract-hashes.js +1 -1
  30. package/dist/utils/extract-function-name.js +1 -1
  31. package/dist/utils/extract-services.d.ts +1 -1
  32. package/dist/utils/extract-surface-usage.d.ts +29 -0
  33. package/dist/utils/extract-surface-usage.js +97 -0
  34. package/dist/utils/filter-inspector-state.js +53 -16
  35. package/dist/utils/filter-utils.d.ts +1 -1
  36. package/dist/utils/get-property-value.js +42 -3
  37. package/dist/utils/load-addon-functions-meta.js +31 -0
  38. package/dist/utils/middleware.d.ts +2 -2
  39. package/dist/utils/middleware.js +5 -5
  40. package/dist/utils/permissions.d.ts +1 -1
  41. package/dist/utils/post-process.d.ts +28 -8
  42. package/dist/utils/post-process.js +83 -18
  43. package/dist/utils/resolve-deploy-target.d.ts +1 -1
  44. package/dist/utils/resolve-function-meta.d.ts +1 -1
  45. package/dist/utils/resolve-function-types.d.ts +1 -1
  46. package/dist/utils/resolve-versions.js +1 -1
  47. package/dist/utils/schema-generator.d.ts +1 -1
  48. package/dist/utils/serialize-inspector-state.d.ts +4 -3
  49. package/dist/utils/serialize-inspector-state.js +6 -4
  50. package/dist/utils/serialize-openapi-json.d.ts +1 -1
  51. package/dist/utils/validate-auth-sessionless.js +2 -1
  52. package/dist/utils/workflow/graph/finalize-workflows.js +1 -1
  53. package/dist/utils/workflow/graph/serialize-workflow-graph.d.ts +6 -1
  54. package/dist/utils/workflow/graph/serialize-workflow-graph.js +26 -2
  55. package/dist/utils/workflow/graph/workflow-graph.types.d.ts +9 -0
  56. package/dist/visit.js +4 -4
  57. package/package.json +10 -10
  58. package/run-tests.sh +0 -0
  59. package/src/add/{add-ai-scorer.test.ts → add-agent-scorer.test.ts} +11 -11
  60. package/src/add/{add-ai-scorer.ts → add-agent-scorer.ts} +4 -4
  61. package/src/add/{add-ai-agent.test.ts → add-agent.test.ts} +1 -1
  62. package/src/add/{add-ai-agent.ts → add-agent.ts} +13 -21
  63. package/src/add/add-auth.ts +2 -2
  64. package/src/add/add-cli.ts +6 -0
  65. package/src/add/add-credential.ts +3 -6
  66. package/src/add/add-file-with-factory.ts +2 -6
  67. package/src/add/add-functions.ts +23 -25
  68. package/src/add/add-http-route-ref.test.ts +149 -0
  69. package/src/add/add-http-route.ts +25 -28
  70. package/src/add/add-keyed-wiring.ts +8 -7
  71. package/src/add/add-middleware.ts +7 -5
  72. package/src/add/add-permission.ts +1 -1
  73. package/src/add/add-personas.test.ts +24 -0
  74. package/src/add/add-personas.ts +3 -1
  75. package/src/add/add-scenario-step.test.ts +35 -0
  76. package/src/add/add-scope.test.ts +4 -4
  77. package/src/add/add-scope.ts +4 -8
  78. package/src/add/add-secret.ts +1 -0
  79. package/src/add/add-system-role.test.ts +8 -8
  80. package/src/add/add-system-role.ts +2 -4
  81. package/src/add/add-variable.ts +1 -0
  82. package/src/add/add-wire-remote-addon.ts +4 -1
  83. package/src/add/add-workflow-graph-foreach.test.ts +174 -0
  84. package/src/add/add-workflow-graph.ts +78 -6
  85. package/src/add/add-workflow-sleep-expression.test.ts +3 -4
  86. package/src/add/addon-contracts.test.ts +1 -1
  87. package/src/add/config-value-metadata.test.ts +94 -0
  88. package/src/add/secret-sink-check.test.ts +4 -1
  89. package/src/error-codes.ts +3 -1
  90. package/src/index.ts +3 -4
  91. package/src/inspector.ts +20 -3
  92. package/src/types.ts +10 -7
  93. package/src/utils/annotate-http-route-auth.test.ts +5 -7
  94. package/src/utils/annotate-http-route-auth.ts +2 -1
  95. package/src/utils/check-secret-sinks.ts +4 -1
  96. package/src/utils/compute-required-schemas.ts +1 -1
  97. package/src/utils/contract-hashes.test.ts +1 -1
  98. package/src/utils/contract-hashes.ts +3 -2
  99. package/src/utils/extract-function-name.ts +1 -1
  100. package/src/utils/extract-services.ts +4 -2
  101. package/src/utils/extract-surface-usage.test.ts +147 -0
  102. package/src/utils/extract-surface-usage.ts +121 -0
  103. package/src/utils/filter-inspector-state.test.ts +138 -12
  104. package/src/utils/filter-inspector-state.ts +82 -21
  105. package/src/utils/filter-utils.ts +1 -1
  106. package/src/utils/get-property-value.ts +43 -5
  107. package/src/utils/load-addon-functions-meta.ts +40 -0
  108. package/src/utils/middleware.test.ts +23 -23
  109. package/src/utils/middleware.ts +6 -6
  110. package/src/utils/permissions.ts +1 -1
  111. package/src/utils/post-process.test.ts +68 -11
  112. package/src/utils/post-process.ts +91 -28
  113. package/src/utils/resolve-deploy-target.ts +1 -1
  114. package/src/utils/resolve-function-meta.ts +1 -1
  115. package/src/utils/resolve-function-types.ts +1 -1
  116. package/src/utils/resolve-versions.ts +1 -1
  117. package/src/utils/schema-generator.ts +2 -1
  118. package/src/utils/serialize-inspector-state.ts +10 -7
  119. package/src/utils/serialize-mcp-json.ts +1 -1
  120. package/src/utils/serialize-openapi-json.ts +1 -1
  121. package/src/utils/validate-auth-sessionless.ts +2 -1
  122. package/src/utils/validate-tags-resolve-to-middleware.test.ts +4 -1
  123. package/src/utils/workflow/dsl/extract-approval-policy.test.ts +4 -1
  124. package/src/utils/workflow/graph/convert-dsl-to-graph-skip.test.ts +1 -1
  125. package/src/utils/workflow/graph/finalize-workflows.ts +6 -2
  126. package/src/utils/workflow/graph/serialize-workflow-graph.ts +34 -3
  127. package/src/utils/workflow/graph/workflow-graph.types.ts +10 -0
  128. package/src/visit.ts +4 -4
  129. package/tsconfig.tsbuildinfo +1 -1
  130. package/dist/add/add-ai-scorer.d.ts +0 -2
  131. package/dist/utils/secret-brokers.d.ts +0 -2
  132. package/dist/utils/secret-brokers.js +0 -9
@@ -8,9 +8,10 @@ import {
8
8
  } from '../utils/extract-function-name.js'
9
9
  import { extractFunctionNode } from '../utils/extract-function-node.js'
10
10
  import { extractUsedWires } from '../utils/extract-services.js'
11
- import type { AuditDurability, FunctionServicesMeta } from '@pikku/core'
12
- import type { ScenarioStepKind, ScenarioSurface } from '@pikku/core/scenario'
13
- import { formatVersionedId, parseVersionedId } from '@pikku/core'
11
+ import type { AuditDurability } from '@pikku/core/services'
12
+ import type { FunctionServicesMeta } from '@pikku/core/function'
13
+ import type { ScenarioSurface, ScenarioStepKind } from '@pikku/core/scenario'
14
+ import { formatVersionedId, parseVersionedId } from '@pikku/core/utils'
14
15
 
15
16
  /** Binding keys a `pikkuScenarioStep` may declare, in report order. */
16
17
  const SCENARIO_SURFACE_KEYS: readonly ScenarioSurface[] = [
@@ -592,6 +593,7 @@ export const addFunctions: AddWiring = (
592
593
  let workflowRetries: number | undefined
593
594
  let workflowTimeout: string | undefined
594
595
  let scenarioStepSurfaces: ScenarioSurface[] | undefined
596
+ let scenarioStepRequiresActor: boolean | undefined
595
597
  let scenarioStepAddon: string | undefined
596
598
  let scenarioStepTemplate: string | undefined
597
599
  let scopes: string[] | undefined
@@ -683,8 +685,7 @@ export const addFunctions: AddWiring = (
683
685
  mcp = getPropertyValue(firstArg, 'mcp') as boolean | undefined
684
686
  auth = getPropertyValue(firstArg, 'auth') as boolean | undefined
685
687
  permissionsInBody = getPropertyValue(firstArg, 'permissionsInBody') as
686
- | boolean
687
- | undefined
688
+ boolean | undefined
688
689
  if (permissionsInBody && !options.allow?.permissionsInBody) {
689
690
  logger.critical(
690
691
  ErrorCode.PERMISSIONS_IN_BODY_NOT_ALLOWED,
@@ -721,22 +722,15 @@ export const addFunctions: AddWiring = (
721
722
  }
722
723
  readonly_ = getPropertyValue(firstArg, 'readonly') as boolean | undefined
723
724
  deploy = getPropertyValue(firstArg, 'deploy') as
724
- | 'serverless'
725
- | 'server'
726
- | 'auto'
727
- | undefined
725
+ 'serverless' | 'server' | 'auto' | undefined
728
726
  approvalRequired = getPropertyValue(firstArg, 'approvalRequired') as
729
- | boolean
730
- | undefined
727
+ boolean | undefined
731
728
  workflowQueued = getPropertyValue(firstArg, 'workflowQueued') as
732
- | boolean
733
- | undefined
729
+ boolean | undefined
734
730
  workflowRetries = getPropertyValue(firstArg, 'workflowRetries') as
735
- | number
736
- | undefined
731
+ number | undefined
737
732
  workflowTimeout = getPropertyValue(firstArg, 'workflowTimeout') as
738
- | string
739
- | undefined
733
+ string | undefined
740
734
  // A step declares one implementation per surface it supports; the binding
741
735
  // keys that are present ARE the declaration, so there is nothing to read a
742
736
  // value from — only names to collect.
@@ -767,8 +761,7 @@ export const addFunctions: AddWiring = (
767
761
  scenarioStepSurfaces = ['default']
768
762
  if (scenarioStepKind === 'addon') {
769
763
  scenarioStepAddon = getPropertyValue(firstArg, 'addon') as
770
- | string
771
- | undefined
764
+ string | undefined
772
765
  if (!scenarioStepAddon) {
773
766
  logger.error(
774
767
  `• pikkuAddonScenarioStep('${name}') needs a string literal \`addon\` naming the addon whose system acts — the same name its wireAddon declares.`
@@ -780,10 +773,16 @@ export const addFunctions: AddWiring = (
780
773
  scenarioStepSurfaces = declaredSurfaces.length
781
774
  ? declaredSurfaces
782
775
  : undefined
776
+ // A browser binding cannot be provisioned without a window to open, so it
777
+ // requires an actor whether or not the step said so.
778
+ scenarioStepRequiresActor =
779
+ getPropertyValue(firstArg, 'actor') === true ||
780
+ declaredSurfaces.includes('browser')
781
+ ? true
782
+ : undefined
783
783
  }
784
784
  scenarioStepTemplate = getPropertyValue(firstArg, 'template') as
785
- | string
786
- | undefined
785
+ string | undefined
787
786
  scopes = getArrayPropertyValue(firstArg, 'scopes') ?? undefined
788
787
 
789
788
  // Extract approvalDescription identifier reference
@@ -827,13 +826,11 @@ export const addFunctions: AddWiring = (
827
826
  ) {
828
827
  const nodeObj = prop.initializer
829
828
  nodeDisplayName = getPropertyValue(nodeObj, 'displayName') as
830
- | string
831
- | null
829
+ string | null
832
830
  nodeCategory = getPropertyValue(nodeObj, 'category') as string | null
833
831
  nodeType = getPropertyValue(nodeObj, 'type') as NodeType | null
834
832
  nodeErrorOutput = getPropertyValue(nodeObj, 'errorOutput') as
835
- | boolean
836
- | null
833
+ boolean | null
837
834
 
838
835
  if (!nodeDisplayName) {
839
836
  logger.critical(
@@ -1376,6 +1373,7 @@ export const addFunctions: AddWiring = (
1376
1373
  workflowRetries: workflowRetries ?? undefined,
1377
1374
  workflowTimeout: workflowTimeout ?? undefined,
1378
1375
  scenarioStepSurfaces,
1376
+ scenarioStepRequiresActor,
1379
1377
  // `persona` is the default reading of an unmarked step, so it is left off
1380
1378
  // rather than written into every step meta a project already has.
1381
1379
  scenarioStepKind:
@@ -0,0 +1,149 @@
1
+ import { strict as assert } from 'assert'
2
+ import { describe, test, before, after } from 'node:test'
3
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'fs'
4
+ import { tmpdir } from 'os'
5
+ import { join } from 'path'
6
+ import { inspect } from '../inspector.js'
7
+ import { filterInspectorState } from '../utils/filter-inspector-state.js'
8
+ import type { InspectorLogger, InspectorState } from '../types.js'
9
+
10
+ const ADDON_PACKAGE = '@addon/greeter'
11
+ const NAMESPACE = 'ext'
12
+ const TARGET = `${NAMESPACE}:greet`
13
+
14
+ const logger: InspectorLogger = {
15
+ debug: () => {},
16
+ info: () => {},
17
+ warn: () => {},
18
+ error: () => {},
19
+ diagnostic: () => {},
20
+ critical: () => {},
21
+ hasCriticalErrors: () => false,
22
+ } as unknown as InspectorLogger
23
+
24
+ const writeAddonFixture = (rootDir: string) => {
25
+ writeFileSync(
26
+ join(rootDir, 'package.json'),
27
+ JSON.stringify({ name: 'consumer' })
28
+ )
29
+ const addonDir = join(rootDir, 'node_modules', ADDON_PACKAGE)
30
+ const pikku = join(addonDir, '.pikku')
31
+ mkdirSync(join(pikku, 'function'), { recursive: true })
32
+ mkdirSync(join(pikku, 'schemas', 'schemas'), { recursive: true })
33
+ writeFileSync(
34
+ join(addonDir, 'package.json'),
35
+ JSON.stringify({ name: ADDON_PACKAGE })
36
+ )
37
+ writeFileSync(
38
+ join(pikku, 'function', 'pikku-functions-meta.gen.json'),
39
+ JSON.stringify({
40
+ greet: {
41
+ pikkuFuncId: 'greet',
42
+ name: 'greet',
43
+ sessionless: true,
44
+ inputSchemaName: 'GreetInput',
45
+ outputSchemaName: 'GreetOutput',
46
+ inputs: ['GreetInput'],
47
+ outputs: ['GreetOutput'],
48
+ services: { optimized: true, services: ['logger'] },
49
+ },
50
+ })
51
+ )
52
+ writeFileSync(
53
+ join(pikku, 'schemas', 'schemas', 'GreetInput.schema.json'),
54
+ JSON.stringify({
55
+ type: 'object',
56
+ properties: {
57
+ id: { type: 'string' },
58
+ loud: { type: 'boolean' },
59
+ name: { type: 'string' },
60
+ },
61
+ })
62
+ )
63
+ }
64
+
65
+ const writeWiring = (rootDir: string): string => {
66
+ const file = join(rootDir, 'greet.wiring.ts')
67
+ writeFileSync(
68
+ file,
69
+ [
70
+ `import { ref, wireAddon } from '@pikku/core'`,
71
+ `import { wireHTTP } from '@pikku/core/http'`,
72
+ ``,
73
+ `wireAddon({ name: '${NAMESPACE}', package: '${ADDON_PACKAGE}' })`,
74
+ ``,
75
+ `wireHTTP({`,
76
+ ` auth: false,`,
77
+ ` method: 'get',`,
78
+ ` route: '/greet/:id',`,
79
+ ` func: ref('${TARGET}'),`,
80
+ `})`,
81
+ ``,
82
+ `wireHTTP({`,
83
+ ` auth: false,`,
84
+ ` method: 'post',`,
85
+ ` route: '/greet',`,
86
+ ` query: ['loud'],`,
87
+ ` func: ref('${TARGET}'),`,
88
+ `})`,
89
+ ].join('\n')
90
+ )
91
+ return file
92
+ }
93
+
94
+ describe('an HTTP route wired with ref() to an addon function', () => {
95
+ let rootDir: string
96
+ let state: InspectorState
97
+
98
+ before(async () => {
99
+ rootDir = mkdtempSync(join(tmpdir(), 'pikku-http-ref-'))
100
+ writeAddonFixture(rootDir)
101
+ const file = writeWiring(rootDir)
102
+ state = await inspect(logger, [file], { rootDir })
103
+ })
104
+
105
+ after(() => {
106
+ rmSync(rootDir, { recursive: true, force: true })
107
+ })
108
+
109
+ test('records the ref target as the route function id', () => {
110
+ assert.equal(state.http.meta.get['/greet/:id']?.pikkuFuncId, TARGET)
111
+ assert.equal(state.http.meta.post['/greet']?.pikkuFuncId, TARGET)
112
+ })
113
+
114
+ // packageName names the package a wire runs in — its middleware, its
115
+ // services, its scoped credentials. The route belongs to the consuming app
116
+ // however its function is reached, so tagging it with the addon's package
117
+ // would silently swap the app's global middleware for the addon's.
118
+ test("leaves the route in the consuming app's package", () => {
119
+ assert.equal(state.http.meta.get['/greet/:id']?.packageName, undefined)
120
+ assert.equal(state.http.meta.post['/greet']?.packageName, undefined)
121
+ })
122
+
123
+ test('mints no wrapper function metadata for the route', () => {
124
+ const wrapperIds = Object.keys(state.functions.meta).filter((id) =>
125
+ id.startsWith('http:')
126
+ )
127
+ assert.deepEqual(wrapperIds, [])
128
+ assert.equal(state.functions.meta[TARGET], undefined)
129
+ })
130
+
131
+ test('keeps each route its own params and query', () => {
132
+ assert.deepEqual(state.http.meta.get['/greet/:id']?.params, ['id'])
133
+ assert.equal(state.http.meta.get['/greet/:id']?.query, undefined)
134
+ assert.equal(state.http.meta.post['/greet']?.params, undefined)
135
+ assert.deepEqual(state.http.meta.post['/greet']?.query, ['loud'])
136
+ })
137
+
138
+ test('marks the addon target as used so tree-shaking keeps it', () => {
139
+ assert.ok(state.serviceAggregation.usedFunctions.has(TARGET))
140
+ })
141
+
142
+ test('survives a filtered bundle together with its addon declaration', () => {
143
+ const filtered = filterInspectorState(state, { names: [TARGET] }, logger)
144
+ assert.equal(filtered.http.meta.get['/greet/:id']?.pikkuFuncId, TARGET)
145
+ assert.equal(filtered.http.meta.post['/greet']?.pikkuFuncId, TARGET)
146
+ assert.ok(filtered.rpc.wireAddonDeclarations.has(NAMESPACE))
147
+ assert.ok(filtered.serviceAggregation.usedFunctions.has(TARGET))
148
+ })
149
+ })
@@ -243,6 +243,17 @@ export function registerHTTPRoute({
243
243
  )
244
244
  : null
245
245
 
246
+ // A ref()-wired route points straight at the addon function, the same way the
247
+ // CLI and channel adders record one. The addon's own metadata is the contract
248
+ // — and it is authoritative where the checker is not: `ref('ns:fn')` only
249
+ // types precisely once the consumer's own rpc map already lists the addon's
250
+ // functions, so a cold run inferred the whole `FlattenedRPCMap` and a warm one
251
+ // inferred the real input. The addon's metadata says which it is either way.
252
+ //
253
+ // The route stays the consuming app's all the same, so it records no
254
+ // packageName: that names the package a wire *runs in*, and running this one
255
+ // inside the addon would hand it the addon's middleware and services instead
256
+ // of the app's. `ref` reaches the addon over RPC, which enters it properly.
246
257
  if (refAddonTarget) {
247
258
  const targetMeta = resolveFunctionMeta(state, refAddonTarget)
248
259
  if (!targetMeta) {
@@ -251,25 +262,16 @@ export function registerHTTPRoute({
251
262
  )
252
263
  return
253
264
  }
254
- }
255
-
256
- ensureFunctionMetadata(
257
- state,
258
- funcName,
259
- fullRoute,
260
- funcInitializer,
261
- checker,
262
- extracted.isHelper
263
- )
264
-
265
- // Propagate sessionless from the addon target so the runtime can correctly
266
- // determine whether a session is required for ref()-based routes.
267
- if (refAddonTarget) {
268
- const targetMeta = resolveFunctionMeta(state, refAddonTarget)
269
- const inlineMeta = state.functions.meta[funcName]
270
- if (targetMeta && inlineMeta && targetMeta.sessionless !== undefined) {
271
- inlineMeta.sessionless = targetMeta.sessionless
272
- }
265
+ funcName = refAddonTarget
266
+ } else {
267
+ ensureFunctionMetadata(
268
+ state,
269
+ funcName,
270
+ fullRoute,
271
+ funcInitializer,
272
+ checker,
273
+ extracted.isHelper
274
+ )
273
275
  }
274
276
 
275
277
  // The route's own `auth`, falling back to the group's. Recorded on the meta
@@ -322,13 +324,12 @@ export function registerHTTPRoute({
322
324
  const input = fnMeta.inputs?.[0] || null
323
325
 
324
326
  const getRouteInputKeys = (): string[] | null => {
325
- const targetFuncName = refAddonTarget ?? funcName
326
- const inputTypes = state.typesLookup.get(targetFuncName)
327
+ const inputTypes = state.typesLookup.get(funcName)
327
328
  if (inputTypes && inputTypes.length > 0) {
328
329
  return extractTypeKeys(inputTypes[0])
329
330
  }
330
331
 
331
- const targetMeta = resolveFunctionMeta(state, targetFuncName)
332
+ const targetMeta = resolveFunctionMeta(state, funcName)
332
333
  if (targetMeta?.inputSchemaName) {
333
334
  const schema = state.schemas[targetMeta.inputSchemaName] as any
334
335
  const properties = schema?.properties
@@ -354,7 +355,7 @@ export function registerHTTPRoute({
354
355
  logger.critical(
355
356
  ErrorCode.ROUTE_PARAM_MISMATCH,
356
357
  `Route '${fullRoute}' has path parameter(s) [${missingParams.join(', ')}] ` +
357
- `not found in function '${refAddonTarget ?? funcName}' input type. ` +
358
+ `not found in function '${funcName}' input type. ` +
358
359
  `Input type has: [${inputKeys.join(', ')}]`
359
360
  )
360
361
  return
@@ -368,7 +369,7 @@ export function registerHTTPRoute({
368
369
  logger.critical(
369
370
  ErrorCode.ROUTE_QUERY_MISMATCH,
370
371
  `Route '${fullRoute}' has query parameter(s) [${missingQuery.join(', ')}] ` +
371
- `not found in function '${refAddonTarget ?? funcName}' input type. ` +
372
+ `not found in function '${funcName}' input type. ` +
372
373
  `Input type has: [${inputKeys.join(', ')}]`
373
374
  )
374
375
  return
@@ -393,9 +394,6 @@ export function registerHTTPRoute({
393
394
  const permissions = resolvePermissions(state, obj, tags, checker)
394
395
 
395
396
  state.serviceAggregation.usedFunctions.add(funcName)
396
- if (refAddonTarget) {
397
- state.serviceAggregation.usedFunctions.add(refAddonTarget)
398
- }
399
397
  extractWireNames(middleware).forEach((name) =>
400
398
  state.serviceAggregation.usedMiddleware.add(name)
401
399
  )
@@ -420,7 +418,6 @@ export function registerHTTPRoute({
420
418
  state.http.files.add(sourceFile.fileName)
421
419
  state.http.meta[method][fullRoute] = {
422
420
  pikkuFuncId: funcName,
423
- ...(refAddonTarget && { refTarget: refAddonTarget }),
424
421
  ...(packageName && { packageName }),
425
422
  route: fullRoute,
426
423
  sourceFile: sourceFile.fileName,
@@ -7,13 +7,15 @@ import {
7
7
  import type { AddWiring, InspectorState } from '../types.js'
8
8
  import { ErrorCode } from '../error-codes.js'
9
9
  import { detectSchemaVendorOrError } from '../utils/detect-schema-vendor.js'
10
- import { parseDurationString } from '@pikku/core'
10
+ import { parseDurationString } from '@pikku/core/utils'
11
11
 
12
12
  export interface KeyedWiringConfig {
13
13
  functionName: string
14
14
  idField: string
15
15
  label: string
16
16
  schemaPrefix: string
17
+ /** Boolean property carrying this wiring kind's deploy-gate opt-in/opt-out. */
18
+ flagField: string
17
19
  getState: (state: InspectorState) => {
18
20
  definitions: any[]
19
21
  files: Set<string>
@@ -49,17 +51,15 @@ export const createAddKeyedWiring = (config: KeyedWiringConfig): AddWiring => {
49
51
 
50
52
  const nameValue = getPropertyValue(obj, 'name') as string | null
51
53
  const displayNameValue = getPropertyValue(obj, 'displayName') as
52
- | string
53
- | null
54
+ string | null
54
55
  const descriptionValue = getPropertyValue(obj, 'description') as
55
- | string
56
- | null
56
+ string | null
57
57
  const rotationPeriodValue = getPropertyValue(obj, 'rotationPeriod') as
58
- | string
59
- | null
58
+ string | null
60
59
  const docsUrlValue = getPropertyValue(obj, 'docsUrl') as string | null
61
60
  const allowedHostsValue = getArrayPropertyValue(obj, 'allowedHosts')
62
61
  const idValue = getPropertyValue(obj, config.idField) as string | null
62
+ const flagValue = getPropertyValue(obj, config.flagField) === true
63
63
 
64
64
  let schemaVariableName: string | null = null
65
65
  let schemaSourceFile: string | null = null
@@ -171,6 +171,7 @@ export const createAddKeyedWiring = (config: KeyedWiringConfig): AddWiring => {
171
171
  docsUrl: docsUrlValue || undefined,
172
172
  allowedHosts: allowedHostsValue || undefined,
173
173
  [config.idField]: idValue,
174
+ [config.flagField]: flagValue || undefined,
174
175
  schema: schemaLookupName,
175
176
  sourceFile,
176
177
  })
@@ -698,12 +698,14 @@ export const addMiddleware: AddWiring = (logger, node, checker, state) => {
698
698
  return
699
699
  }
700
700
 
701
- if (expression.text === 'pikkuAIMiddleware') {
701
+ if (expression.text === 'pikkuAgentMiddleware') {
702
702
  const arg = args[0]
703
703
  if (!arg) return
704
704
 
705
705
  if (!ts.isObjectLiteralExpression(arg)) {
706
- logger.error(`• pikkuAIMiddleware() requires an object literal argument.`)
706
+ logger.error(
707
+ `• pikkuAgentMiddleware() requires an object literal argument.`
708
+ )
707
709
  return
708
710
  }
709
711
 
@@ -744,13 +746,13 @@ export const addMiddleware: AddWiring = (logger, node, checker, state) => {
744
746
  pikkuFuncId = node.parent.name.text
745
747
  } else {
746
748
  logger.error(
747
- `• pikkuAIMiddleware() must be assigned to a variable or object property. ` +
748
- `Extract it to a const: const myMiddleware = pikkuAIMiddleware(...)`
749
+ `• pikkuAgentMiddleware() must be assigned to a variable or object property. ` +
750
+ `Extract it to a const: const myMiddleware = pikkuAgentMiddleware(...)`
749
751
  )
750
752
  return
751
753
  }
752
754
  }
753
- state.aiMiddleware.definitions[pikkuFuncId] = {
755
+ state.agentMiddleware.definitions[pikkuFuncId] = {
754
756
  services,
755
757
  sourceFile: node.getSourceFile().fileName,
756
758
  position: node.getStart(),
@@ -1,5 +1,5 @@
1
1
  import * as ts from 'typescript'
2
- import type { FunctionWiresMeta } from '@pikku/core'
2
+ import type { FunctionWiresMeta } from '@pikku/core/function'
3
3
  import type { AddWiring } from '../types.js'
4
4
  import { extractFunctionName } from '../utils/extract-function-name.js'
5
5
  import {
@@ -108,6 +108,30 @@ describe('addPersonas inspector', () => {
108
108
  assert.equal(susan.runnable, true)
109
109
  })
110
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
+
111
135
  test('extracts a declared avatar', async () => {
112
136
  const { state } = await inspectSource(
113
137
  withRoles(
@@ -1,7 +1,7 @@
1
1
  import * as ts from 'typescript'
2
2
  import { DISPOSITIONS } from '@pikku/core/virtual-user'
3
3
  import { isRunnablePersona } from '@pikku/core/persona'
4
- import type { PersonaAccountMeta, PersonaMeta } from '@pikku/core/persona'
4
+ import type { PersonaMeta, PersonaAccountMeta } from '@pikku/core/persona'
5
5
  import { ErrorCode } from '../error-codes.js'
6
6
  import { claimSingleDeclaration } from '../utils/single-declaration.js'
7
7
  import type { AddWiring, InspectorLogger } from '../types.js'
@@ -265,6 +265,8 @@ export const addPersonas: AddWiring = (logger, node, _checker, state) => {
265
265
  if (avatarUrl !== undefined) persona.avatarUrl = avatarUrl
266
266
  const personality = stringProperty(config, 'personality')
267
267
  if (personality !== undefined) persona.personality = personality
268
+ const app = stringProperty(config, 'app')
269
+ if (app !== undefined) persona.app = app
268
270
  if (disposition !== undefined) {
269
271
  persona.disposition = disposition as PersonaMeta['disposition']
270
272
  }
@@ -39,6 +39,12 @@ const STEPS = [
39
39
  " description: 'sees a receipt',",
40
40
  ' default: async ({ logger }) => ({ ok: true }),',
41
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
+ '})',
42
48
  ].join('\n')
43
49
 
44
50
  const scenarioSource = (body: string[], preamble: string[] = []) =>
@@ -210,6 +216,35 @@ describe('pikkuScenarioStep', () => {
210
216
  }
211
217
  })
212
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
+
213
248
  test('a scenario that never asserts is a PKU680 critical', async () => {
214
249
  const { criticals, cleanup } = await run([
215
250
  "await scenario.given('buys an apple', 'buysAnApple', { qty: 1 }, { actor: actors.shopper })",
@@ -202,10 +202,10 @@ describe('addScope inspector', () => {
202
202
  [],
203
203
  `expected no duplicate critical, got ${JSON.stringify(criticals)}`
204
204
  )
205
- assert.deepEqual(
206
- state.scopes.definitions.map((d) => d.name).sort(),
207
- ['admin', 'billing']
208
- )
205
+ assert.deepEqual(state.scopes.definitions.map((d) => d.name).sort(), [
206
+ 'admin',
207
+ 'billing',
208
+ ])
209
209
  })
210
210
 
211
211
  // The CLI scaffolds a scope tree of its own — `user-admin.gen.ts` declares the
@@ -88,14 +88,12 @@ const extractScopeNodes = (
88
88
 
89
89
  const node: ScopeNodeMeta = {}
90
90
  const displayName = getPropertyValue(prop.initializer, 'displayName') as
91
- | string
92
- | null
91
+ string | null
93
92
  if (displayName) {
94
93
  node.displayName = displayName
95
94
  }
96
95
  const description = getPropertyValue(prop.initializer, 'description') as
97
- | string
98
- | null
96
+ string | null
99
97
  if (description) {
100
98
  node.description = description
101
99
  }
@@ -192,11 +190,9 @@ export const addScope: AddWiring = (logger, node, checker, state, _options) => {
192
190
 
193
191
  const root = prop.initializer
194
192
  const displayNameValue = getPropertyValue(root, 'displayName') as
195
- | string
196
- | null
193
+ string | null
197
194
  const descriptionValue = getPropertyValue(root, 'description') as
198
- | string
199
- | null
195
+ string | null
200
196
 
201
197
  let scopes: Record<string, ScopeNodeMeta> | undefined
202
198
  const scopesProp = root.properties.find(
@@ -5,5 +5,6 @@ export const addSecret = createAddKeyedWiring({
5
5
  idField: 'secretId',
6
6
  label: 'Secret',
7
7
  schemaPrefix: 'SecretSchema',
8
+ flagField: 'optional',
8
9
  getState: (state) => state.secrets,
9
10
  })
@@ -118,10 +118,10 @@ describe('addSystemRole inspector', () => {
118
118
  )
119
119
  )
120
120
 
121
- assert.deepEqual(
122
- state.systemRoles.definitions.map((r) => r.name).sort(),
123
- ['admin', 'buyer']
124
- )
121
+ assert.deepEqual(state.systemRoles.definitions.map((r) => r.name).sort(), [
122
+ 'admin',
123
+ 'buyer',
124
+ ])
125
125
  })
126
126
 
127
127
  // Roles follow scopes: a project scaffolding user-admin has a generated
@@ -142,10 +142,10 @@ describe('addSystemRole inspector', () => {
142
142
  [],
143
143
  `expected no duplicate critical, got ${JSON.stringify(criticals)}`
144
144
  )
145
- assert.deepEqual(
146
- state.systemRoles.definitions.map((r) => r.name).sort(),
147
- ['admin', 'buyer']
148
- )
145
+ assert.deepEqual(state.systemRoles.definitions.map((r) => r.name).sort(), [
146
+ 'admin',
147
+ 'buyer',
148
+ ])
149
149
  })
150
150
 
151
151
  // A generated file is not the one place a person adds a role to, so it neither
@@ -146,11 +146,9 @@ export const addSystemRole: AddWiring = (
146
146
  }
147
147
 
148
148
  const displayName = getPropertyValue(prop.initializer, 'displayName') as
149
- | string
150
- | null
149
+ string | null
151
150
  const description = getPropertyValue(prop.initializer, 'description') as
152
- | string
153
- | null
151
+ string | null
154
152
 
155
153
  state.systemRoles.files.add(sourceFile)
156
154
  state.systemRoles.definitions.push({
@@ -5,5 +5,6 @@ export const addVariable = createAddKeyedWiring({
5
5
  idField: 'variableId',
6
6
  label: 'Variable',
7
7
  schemaPrefix: 'VariableSchema',
8
+ flagField: 'required',
8
9
  getState: (state) => state.variables,
9
10
  })