@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
@@ -108,18 +108,16 @@ describe('annotateHttpRouteAuth', () => {
108
108
 
109
109
  test('a ref route resolves against its target', () => {
110
110
  const state = {
111
- functions: {
112
- meta: {
113
- inline: { pikkuFuncId: 'inline', sessionless: true },
114
- target: { pikkuFuncId: 'target', sessionless: false },
115
- },
111
+ functions: { meta: {} },
112
+ addonFunctions: {
113
+ console: { target: { pikkuFuncId: 'target', sessionless: false } },
116
114
  },
117
115
  http: {
118
116
  meta: {
119
117
  get: {
120
118
  '/thing': {
121
- pikkuFuncId: 'inline',
122
- refTarget: 'target',
119
+ pikkuFuncId: 'console:target',
120
+ packageName: ADDON_PACKAGE,
123
121
  route: '/thing',
124
122
  method: 'get',
125
123
  },
@@ -1,4 +1,5 @@
1
1
  import type { InspectorState } from '../types.js'
2
+ import { resolveFunctionMeta } from './resolve-function-meta.js'
2
3
 
3
4
  /**
4
5
  * Resolve, per HTTP route, whether reaching it requires a session.
@@ -25,7 +26,7 @@ export function annotateHttpRouteAuth(state: InspectorState): void {
25
26
 
26
27
  for (const routes of Object.values(state.http?.meta ?? {})) {
27
28
  for (const route of Object.values(routes)) {
28
- const meta = state.functions.meta[route.refTarget ?? route.pikkuFuncId]
29
+ const meta = resolveFunctionMeta(state, route.pikkuFuncId)
29
30
 
30
31
  route.requiresSession =
31
32
  // A pikkuFunc always requires a session, on every path.
@@ -53,7 +53,10 @@ export function findRevealedSecretSinks(
53
53
  const violations: SecretSinkViolation[] = []
54
54
 
55
55
  const visit = (node: ts.Node): void => {
56
- if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
56
+ if (
57
+ ts.isCallExpression(node) &&
58
+ ts.isPropertyAccessExpression(node.expression)
59
+ ) {
57
60
  const sink = describeSink(checker, node.expression)
58
61
  if (sink) {
59
62
  for (const arg of node.arguments) {
@@ -1,4 +1,4 @@
1
- import type { FunctionsMeta } from '@pikku/core'
1
+ import type { FunctionsMeta } from '@pikku/core/services'
2
2
  import type { TypesMap } from '../types-map.js'
3
3
  import type { SchemaRef } from '../types.js'
4
4
 
@@ -10,7 +10,7 @@ import {
10
10
  extractContractsFromMeta,
11
11
  } from './contract-hashes.js'
12
12
  import type { ContractEntry, VersionManifest } from './contract-hashes.js'
13
- import type { FunctionsMeta } from '@pikku/core'
13
+ import type { FunctionsMeta } from '@pikku/core/services'
14
14
  import { ErrorCode } from '../error-codes.js'
15
15
 
16
16
  function makeContracts(
@@ -1,5 +1,6 @@
1
- import type { FunctionsMeta, JSONValue } from '@pikku/core'
2
- import { parseVersionedId } from '@pikku/core'
1
+ import type { FunctionsMeta } from '@pikku/core/services'
2
+ import type { JSONValue } from '@pikku/core/utils'
3
+ import { parseVersionedId } from '@pikku/core/utils'
3
4
  import type { TypesMap } from '../types-map.js'
4
5
  import { ErrorCode } from '../error-codes.js'
5
6
  import { canonicalJSON, hashString } from './hash.js'
@@ -1,7 +1,7 @@
1
1
  import * as ts from 'typescript'
2
2
  import { createHash } from 'crypto'
3
3
  import { relative } from 'path'
4
- import { formatVersionedId } from '@pikku/core'
4
+ import { formatVersionedId } from '@pikku/core/utils'
5
5
 
6
6
  /**
7
7
  * Deterministic placeholder id for an anonymous/unnamed pikku function or
@@ -1,6 +1,8 @@
1
1
  import * as ts from 'typescript'
2
- import type { FunctionServicesMeta, FunctionWiresMeta } from '@pikku/core'
3
-
2
+ import type {
3
+ FunctionServicesMeta,
4
+ FunctionWiresMeta,
5
+ } from '@pikku/core/function'
4
6
  /**
5
7
  * Extract services from a function's first parameter destructuring pattern
6
8
  */
@@ -0,0 +1,147 @@
1
+ import assert from 'node:assert'
2
+ import { describe, test } from 'node:test'
3
+ import ts from 'typescript'
4
+
5
+ import {
6
+ accumulateSurfaceUsage,
7
+ surfaceUsageArea,
8
+ type SurfaceUsageCounts,
9
+ } from './extract-surface-usage.js'
10
+
11
+ const countsFor = (
12
+ files: Array<{ area: string; source: string }>
13
+ ): SurfaceUsageCounts => {
14
+ const counts: SurfaceUsageCounts = {}
15
+ files.forEach(({ area, source }, index) => {
16
+ const file = `src/file-${index}.ts`
17
+ accumulateSurfaceUsage(
18
+ ts.createSourceFile(
19
+ `/project/${file}`,
20
+ source,
21
+ ts.ScriptTarget.Latest,
22
+ true
23
+ ),
24
+ { area, file },
25
+ counts
26
+ )
27
+ })
28
+ return counts
29
+ }
30
+
31
+ describe('accumulateSurfaceUsage', () => {
32
+ test('counts an import site per named import', () => {
33
+ const counts = countsFor([
34
+ {
35
+ area: 'src',
36
+ source:
37
+ "import { pikkuFunc, pikkuSessionlessFunc } from '#pikku/function'\n",
38
+ },
39
+ { area: 'src', source: "import { pikkuFunc } from '#pikku/function'\n" },
40
+ ])
41
+
42
+ assert.deepStrictEqual(counts['#pikku/function'], {
43
+ pikkuFunc: {
44
+ imports: 2,
45
+ seenIn: ['src'],
46
+ files: ['src/file-0.ts', 'src/file-1.ts'],
47
+ },
48
+ pikkuSessionlessFunc: {
49
+ imports: 1,
50
+ seenIn: ['src'],
51
+ files: ['src/file-0.ts'],
52
+ },
53
+ })
54
+ })
55
+
56
+ test('counts a deep specifier against the leaf it belongs to', () => {
57
+ const counts = countsFor([
58
+ {
59
+ area: 'src',
60
+ source:
61
+ "import { pikkuWorkflowFunc } from '#pikku/workflow/pikku-workflow-types.gen.js'\n",
62
+ },
63
+ {
64
+ area: 'workers',
65
+ source: "import { pikkuWorkflowFunc } from '#pikku/workflow'\n",
66
+ },
67
+ ])
68
+
69
+ assert.deepStrictEqual(counts['#pikku/workflow'], {
70
+ pikkuWorkflowFunc: {
71
+ imports: 2,
72
+ seenIn: ['src', 'workers'],
73
+ files: ['src/file-0.ts', 'src/file-1.ts'],
74
+ },
75
+ })
76
+ })
77
+
78
+ test('records the aliased name, not the local one', () => {
79
+ const counts = countsFor([
80
+ {
81
+ area: 'src',
82
+ source: "import { cors as allowAll } from '#pikku/http'\n",
83
+ },
84
+ ])
85
+
86
+ assert.deepStrictEqual(counts['#pikku/http'], {
87
+ cors: { imports: 1, seenIn: ['src'], files: ['src/file-0.ts'] },
88
+ })
89
+ })
90
+
91
+ test('counts a re-export as a use of the leaf', () => {
92
+ const counts = countsFor([
93
+ { area: 'src', source: "export { wireHTTP } from '#pikku/http'\n" },
94
+ ])
95
+
96
+ assert.deepStrictEqual(counts['#pikku/http']!.wireHTTP!.imports, 1)
97
+ })
98
+
99
+ test('ignores imports from anywhere else', () => {
100
+ const counts = countsFor([
101
+ {
102
+ area: 'src',
103
+ source: [
104
+ "import { z } from 'zod'",
105
+ "import { thing } from './local.js'",
106
+ "import * as everything from '#pikku/function'",
107
+ "import bootstrap from '#pikku/pikku-bootstrap.gen.js'",
108
+ ].join('\n'),
109
+ },
110
+ ])
111
+
112
+ assert.deepStrictEqual(counts, {})
113
+ })
114
+ })
115
+
116
+ describe('surfaceUsageArea', () => {
117
+ test('falls back to the top-level source directory in a single package', () => {
118
+ assert.strictEqual(
119
+ surfaceUsageArea('/project/src/wirings/todo.ts', '/project', new Map()),
120
+ 'src'
121
+ )
122
+ })
123
+
124
+ test('does not label everything with the root package name', () => {
125
+ const cache = new Map<string, string | null>([
126
+ ['/project/src/wirings', null],
127
+ ['/project/src', null],
128
+ ])
129
+
130
+ assert.strictEqual(
131
+ surfaceUsageArea('/project/src/wirings/todo.ts', '/project', cache),
132
+ 'src'
133
+ )
134
+ })
135
+
136
+ test('prefers the workspace package a file belongs to', () => {
137
+ const cache = new Map<string, string | null>([
138
+ ['/project/packages/api/src', null],
139
+ ['/project/packages/api', '@app/api'],
140
+ ])
141
+
142
+ assert.strictEqual(
143
+ surfaceUsageArea('/project/packages/api/src/todo.ts', '/project', cache),
144
+ '@app/api'
145
+ )
146
+ })
147
+ })
@@ -0,0 +1,121 @@
1
+ import { existsSync, readFileSync } from 'fs'
2
+ import { basename, dirname, relative, sep } from 'path'
3
+ import ts from 'typescript'
4
+
5
+ /**
6
+ * How often each `#pikku/*` export is imported, the source areas it was
7
+ * imported from, and the files those imports are written in. Counters only:
8
+ * this is accumulated during the sweep that already visits every source file,
9
+ * so nothing here may retain a node.
10
+ */
11
+ export type SurfaceUsageCounts = Record<
12
+ string,
13
+ Record<string, { imports: number; seenIn: string[]; files: string[] }>
14
+ >
15
+
16
+ /**
17
+ * The leaf a specifier belongs to. `#pikku/workflow` and the deep
18
+ * `#pikku/workflow/pikku-workflow-types.gen.js` are the same door, so both
19
+ * count against `#pikku/workflow`.
20
+ */
21
+ const leafSpecifierOf = (specifier: string): string | null => {
22
+ if (!specifier.startsWith('#pikku/')) return null
23
+ const leaf = specifier.slice('#pikku/'.length).split('/')[0]
24
+ // A root-level generated file, e.g. `#pikku/pikku-bootstrap.gen.js`, is not a
25
+ // leaf and has no editorial entry to count against.
26
+ if (!leaf || leaf.endsWith('.js')) return null
27
+ return `#pikku/${leaf}`
28
+ }
29
+
30
+ const packageNameAt = (
31
+ directory: string,
32
+ cache: Map<string, string | null>
33
+ ): string | null => {
34
+ const cached = cache.get(directory)
35
+ if (cached !== undefined) return cached
36
+ let name: string | null = null
37
+ const path = `${directory}${sep}package.json`
38
+ if (existsSync(path)) {
39
+ try {
40
+ name =
41
+ (JSON.parse(readFileSync(path, 'utf8')) as { name?: string }).name ??
42
+ null
43
+ } catch {
44
+ name = null
45
+ }
46
+ }
47
+ cache.set(directory, name)
48
+ return name
49
+ }
50
+
51
+ /**
52
+ * A label a person recognises: the workspace package a file belongs to, or the
53
+ * top-level source directory it sits under when the project is one package.
54
+ * The root package is skipped deliberately — naming it would label every file
55
+ * in a single-package project identically.
56
+ */
57
+ export const surfaceUsageArea = (
58
+ fileName: string,
59
+ rootDir: string,
60
+ cache: Map<string, string | null>
61
+ ): string => {
62
+ let directory = dirname(fileName)
63
+ while (directory.startsWith(rootDir) && directory !== rootDir) {
64
+ const name = packageNameAt(directory, cache)
65
+ if (name) return name
66
+ directory = dirname(directory)
67
+ }
68
+ const [segment] = relative(rootDir, fileName).split(sep)
69
+ return segment && segment !== basename(fileName) ? segment : basename(rootDir)
70
+ }
71
+
72
+ const namesOf = (
73
+ clause: ts.NamedImportBindings | ts.NamedExportBindings | undefined
74
+ ): string[] => {
75
+ if (!clause) return []
76
+ if (ts.isNamedImports(clause) || ts.isNamedExports(clause)) {
77
+ return clause.elements.map(
78
+ (element) => (element.propertyName ?? element.name).text
79
+ )
80
+ }
81
+ return []
82
+ }
83
+
84
+ /**
85
+ * Counts the `#pikku/*` imports of one file into `into`. Only the file's own
86
+ * top-level statements are looked at — an import declaration can appear nowhere
87
+ * else — so this costs a single pass over the statement list and retains
88
+ * nothing.
89
+ */
90
+ export const accumulateSurfaceUsage = (
91
+ sourceFile: ts.SourceFile,
92
+ { area, file }: { area: string; file: string },
93
+ into: SurfaceUsageCounts
94
+ ): void => {
95
+ for (const statement of sourceFile.statements) {
96
+ let specifier: ts.Expression | undefined
97
+ let names: string[] = []
98
+
99
+ if (ts.isImportDeclaration(statement)) {
100
+ specifier = statement.moduleSpecifier
101
+ names = namesOf(statement.importClause?.namedBindings)
102
+ } else if (ts.isExportDeclaration(statement) && statement.moduleSpecifier) {
103
+ specifier = statement.moduleSpecifier
104
+ names = namesOf(statement.exportClause)
105
+ }
106
+
107
+ if (!specifier || names.length === 0 || !ts.isStringLiteral(specifier)) {
108
+ continue
109
+ }
110
+ const leaf = leafSpecifierOf(specifier.text)
111
+ if (!leaf) continue
112
+
113
+ const symbols = (into[leaf] ??= {})
114
+ for (const name of names) {
115
+ const usage = (symbols[name] ??= { imports: 0, seenIn: [], files: [] })
116
+ usage.imports += 1
117
+ if (!usage.seenIn.includes(area)) usage.seenIn.push(area)
118
+ if (!usage.files.includes(file)) usage.files.push(file)
119
+ }
120
+ }
121
+ }
@@ -1614,6 +1614,54 @@ describe('addonServerlessIncompatible scoping', () => {
1614
1614
  })
1615
1615
  })
1616
1616
 
1617
+ describe("an addon's own deploy conflict is the consumer's only when used", () => {
1618
+ // An addon publishes metadata for everything it exports. A function it marks
1619
+ // `deploy: 'serverless'` while also naming one of its services
1620
+ // serverless-incompatible is the addon author's contradiction — but the
1621
+ // deploy scan walks every published function, so without care it fails the
1622
+ // build of a consumer that never wires that function.
1623
+ const withConflictingAddonFunction = (route?: string) => {
1624
+ const state = createMockInspectorState()
1625
+ ;(state as any).addonFunctions = {
1626
+ ffmpeg: {
1627
+ transcode: {
1628
+ deploy: 'serverless',
1629
+ services: { services: ['FfmpegService'] },
1630
+ },
1631
+ },
1632
+ }
1633
+ state.addonServerlessIncompatible = new Map([['ffmpeg', ['FfmpegService']]])
1634
+ if (route) {
1635
+ ;(state.http.meta.get as any)[route] = {
1636
+ pikkuFuncId: 'ffmpeg:transcode',
1637
+ route,
1638
+ method: 'GET',
1639
+ tags: [],
1640
+ middleware: [],
1641
+ permissions: [],
1642
+ }
1643
+ }
1644
+ return state
1645
+ }
1646
+
1647
+ test('a conflicting addon function nothing wires does not fail the build', () => {
1648
+ const state = withConflictingAddonFunction()
1649
+
1650
+ assert.doesNotThrow(() =>
1651
+ filterInspectorState(state, { target: ['serverless'] }, mockLogger)
1652
+ )
1653
+ })
1654
+
1655
+ test('wiring that same function reports the conflict', () => {
1656
+ const state = withConflictingAddonFunction('/transcode')
1657
+
1658
+ assert.throws(
1659
+ () => filterInspectorState(state, { target: ['serverless'] }, mockLogger),
1660
+ /FfmpegService/
1661
+ )
1662
+ })
1663
+ })
1664
+
1617
1665
  describe('addonServerlessIncompatible serialization roundtrip', () => {
1618
1666
  // Uses getInitialInspectorState to guarantee all Map fields are initialized —
1619
1667
  // the lightweight mock in createMockInspectorState() omits fields like
@@ -1705,21 +1753,18 @@ describe('addon bootstrap tree-shake', () => {
1705
1753
  test('keeps an addon when a kept route ref()-targets one of its functions', () => {
1706
1754
  const state = withAddon((s) => {
1707
1755
  s.http.meta.get['/workflow-run/stream'] = {
1708
- pikkuFuncId: 'http:get:/workflow-run/stream',
1709
- refTarget: 'console:streamWorkflowRun',
1756
+ pikkuFuncId: 'console:streamWorkflowRun',
1757
+ packageName: '@pikku/console',
1710
1758
  route: '/workflow-run/stream',
1711
1759
  method: 'GET',
1712
- tags: [],
1760
+ tags: ['stream'],
1713
1761
  middleware: [],
1714
1762
  permissions: [],
1715
1763
  } as any
1716
- s.functions.meta['http:get:/workflow-run/stream'] = {
1717
- services: { optimized: false, services: [] },
1718
- } as any
1719
1764
  })
1720
1765
  const filtered = filterInspectorState(
1721
1766
  state,
1722
- { names: ['http:get:/workflow-run/stream'] },
1767
+ { tags: ['stream'] },
1723
1768
  mockLogger
1724
1769
  )
1725
1770
  assert.strictEqual(filtered.rpc.wireAddonDeclarations.size, 1)
@@ -1729,19 +1774,41 @@ describe('addon bootstrap tree-shake', () => {
1729
1774
  )
1730
1775
  })
1731
1776
 
1732
- test('drops the addon when the ref()-wired route is filtered out', () => {
1777
+ test('keeps a ref()-wired route selected by its wiring id', () => {
1733
1778
  const state = withAddon((s) => {
1734
1779
  s.http.meta.get['/workflow-run/stream'] = {
1735
- pikkuFuncId: 'http:get:/workflow-run/stream',
1736
- refTarget: 'console:streamWorkflowRun',
1780
+ pikkuFuncId: 'console:streamWorkflowRun',
1781
+ packageName: '@pikku/console',
1737
1782
  route: '/workflow-run/stream',
1738
1783
  method: 'GET',
1739
1784
  tags: [],
1740
1785
  middleware: [],
1741
1786
  permissions: [],
1742
1787
  } as any
1743
- s.functions.meta['http:get:/workflow-run/stream'] = {
1744
- services: { optimized: false, services: [] },
1788
+ })
1789
+ const filtered = filterInspectorState(
1790
+ state,
1791
+ { names: ['http:get:/workflow-run/stream'] },
1792
+ mockLogger
1793
+ )
1794
+ assert.ok(filtered.http.meta.get['/workflow-run/stream'])
1795
+ assert.ok(
1796
+ filtered.serviceAggregation.usedFunctions.has('console:streamWorkflowRun')
1797
+ )
1798
+ assert.strictEqual(filtered.rpc.wireAddonDeclarations.size, 1)
1799
+ assert.ok(filtered.rpc.wireAddonDeclarations.has('console'))
1800
+ })
1801
+
1802
+ test('drops the addon when the ref()-wired route is filtered out', () => {
1803
+ const state = withAddon((s) => {
1804
+ s.http.meta.get['/workflow-run/stream'] = {
1805
+ pikkuFuncId: 'console:streamWorkflowRun',
1806
+ packageName: '@pikku/console',
1807
+ route: '/workflow-run/stream',
1808
+ method: 'GET',
1809
+ tags: [],
1810
+ middleware: [],
1811
+ permissions: [],
1745
1812
  } as any
1746
1813
  })
1747
1814
  const filtered = filterInspectorState(
@@ -1843,6 +1910,65 @@ describe('addon bootstrap tree-shake', () => {
1843
1910
  const filtered = filterInspectorState(state, {}, mockLogger)
1844
1911
  assert.strictEqual(filtered.rpc.wireAddonDeclarations.size, 1)
1845
1912
  })
1913
+
1914
+ const withRefWiredAddonRoute = (
1915
+ addonFuncMeta: Record<string, unknown> = { services: [], optimized: false }
1916
+ ) =>
1917
+ withAddon((s) => {
1918
+ s.http.meta.get['/workflow-run/stream'] = {
1919
+ pikkuFuncId: 'console:streamWorkflowRun',
1920
+ packageName: '@pikku/addon-console',
1921
+ route: '/workflow-run/stream',
1922
+ method: 'GET',
1923
+ tags: [],
1924
+ middleware: [],
1925
+ permissions: [],
1926
+ } as any
1927
+ s.addonFunctions = {
1928
+ console: { streamWorkflowRun: addonFuncMeta as any },
1929
+ }
1930
+ })
1931
+
1932
+ test('the deploy filter keeps a ref()-wired route onto a serverless addon function', () => {
1933
+ const filtered = filterInspectorState(
1934
+ withRefWiredAddonRoute(),
1935
+ { target: ['serverless'] },
1936
+ mockLogger
1937
+ )
1938
+ assert.ok(filtered.http.meta.get['/workflow-run/stream'])
1939
+ assert.strictEqual(filtered.rpc.wireAddonDeclarations.size, 1)
1940
+ assert.ok(filtered.rpc.wireAddonDeclarations.has('console'))
1941
+ })
1942
+
1943
+ test('the deploy filter drops a ref()-wired route onto a server-only addon function', () => {
1944
+ const filtered = filterInspectorState(
1945
+ withRefWiredAddonRoute({
1946
+ services: [],
1947
+ optimized: false,
1948
+ deploy: 'server',
1949
+ }),
1950
+ { target: ['serverless'] },
1951
+ mockLogger
1952
+ )
1953
+ assert.ok(!filtered.http.meta.get['/workflow-run/stream'])
1954
+ assert.strictEqual(filtered.rpc.wireAddonDeclarations.size, 0)
1955
+ })
1956
+
1957
+ test("the deploy filter honours the addon's own serverlessIncompatible services", () => {
1958
+ const state = withRefWiredAddonRoute({
1959
+ services: { services: ['FfmpegService'] },
1960
+ optimized: false,
1961
+ })
1962
+ state.addonServerlessIncompatible = new Map([
1963
+ ['console', ['FfmpegService']],
1964
+ ])
1965
+ const filtered = filterInspectorState(
1966
+ state,
1967
+ { target: ['serverless'] },
1968
+ mockLogger
1969
+ )
1970
+ assert.ok(!filtered.http.meta.get['/workflow-run/stream'])
1971
+ })
1846
1972
  })
1847
1973
 
1848
1974
  describe('invokedFunctionsByFile serialization', () => {