@pikku/inspector 0.12.33 → 0.12.35
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.
- package/CHANGELOG.md +31 -0
- package/dist/add/add-http-route.js +4 -1
- package/dist/add/add-rpc-invocations.js +7 -0
- package/dist/add/add-workflow.js +35 -4
- package/dist/inspector.js +1 -0
- package/dist/types.d.ts +1 -0
- package/dist/utils/filter-inspector-state.js +43 -0
- package/dist/utils/load-addon-functions-meta.js +20 -14
- package/dist/utils/post-process.js +38 -13
- package/dist/utils/serialize-inspector-state.d.ts +1 -0
- package/dist/utils/serialize-inspector-state.js +2 -0
- package/dist/utils/workflow/dsl/extract-dsl-workflow.js +9 -4
- package/dist/utils/workflow/dsl/patterns.d.ts +12 -1
- package/dist/utils/workflow/dsl/patterns.js +37 -2
- package/dist/utils/workflow/graph/convert-dsl-to-graph.js +8 -0
- package/dist/utils/workflow/graph/workflow-graph.types.d.ts +8 -0
- package/package.json +2 -2
- package/run-tests.sh +4 -4
- package/src/add/add-http-route.ts +4 -1
- package/src/add/add-rpc-invocations.test.ts +113 -0
- package/src/add/add-rpc-invocations.ts +8 -0
- package/src/add/add-workflow.ts +42 -4
- package/src/inspector.ts +1 -0
- package/src/types.ts +1 -0
- package/src/utils/filter-inspector-state.test.ts +231 -0
- package/src/utils/filter-inspector-state.ts +56 -0
- package/src/utils/load-addon-functions-meta.ts +26 -16
- package/src/utils/post-process.test.ts +254 -0
- package/src/utils/post-process.ts +41 -14
- package/src/utils/serialize-inspector-state.ts +11 -0
- package/src/utils/workflow/dsl/extract-dsl-workflow.ts +12 -3
- package/src/utils/workflow/dsl/patterns.ts +48 -2
- package/src/utils/workflow/graph/convert-dsl-to-graph.ts +8 -0
- package/src/utils/workflow/graph/workflow-graph.types.ts +8 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { strict as assert } from 'assert'
|
|
2
|
+
import { describe, test } from 'node:test'
|
|
3
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import { inspect } from '../inspector.js'
|
|
7
|
+
import type { InspectorLogger, InspectorState } from '../types.js'
|
|
8
|
+
|
|
9
|
+
function makeLogger(): InspectorLogger {
|
|
10
|
+
return {
|
|
11
|
+
debug: () => {},
|
|
12
|
+
info: () => {},
|
|
13
|
+
warn: () => {},
|
|
14
|
+
error: () => {},
|
|
15
|
+
diagnostic: () => {},
|
|
16
|
+
critical: () => {},
|
|
17
|
+
hasCriticalErrors: () => false,
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function inspectFiles(
|
|
22
|
+
files: Record<string, string>
|
|
23
|
+
): Promise<{ state: InspectorState; dir: string }> {
|
|
24
|
+
const dir = await mkdtemp(join(tmpdir(), 'pikku-rpc-invocations-test-'))
|
|
25
|
+
const paths: string[] = []
|
|
26
|
+
for (const [name, source] of Object.entries(files)) {
|
|
27
|
+
const path = join(dir, name)
|
|
28
|
+
await writeFile(path, source)
|
|
29
|
+
paths.push(path)
|
|
30
|
+
}
|
|
31
|
+
const state = await inspect(makeLogger(), paths, { rootDir: dir })
|
|
32
|
+
return { state, dir }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('add-rpc-invocations — invokedFunctionsByFile', () => {
|
|
36
|
+
test('body-level rpc.invoke() is attributed to its source file', async () => {
|
|
37
|
+
const { state, dir } = await inspectFiles({
|
|
38
|
+
'caller.ts': `
|
|
39
|
+
declare const rpc: { invoke: (name: string, data?: unknown) => Promise<unknown> }
|
|
40
|
+
export async function doWork() {
|
|
41
|
+
return rpc.invoke('console:getSchema')
|
|
42
|
+
}
|
|
43
|
+
`,
|
|
44
|
+
'other.ts': `
|
|
45
|
+
export const unrelated = () => 'no invocations here'
|
|
46
|
+
`,
|
|
47
|
+
})
|
|
48
|
+
try {
|
|
49
|
+
assert.ok(state.rpc.invokedFunctions.has('console:getSchema'))
|
|
50
|
+
const byFile = state.rpc.invokedFunctionsByFile
|
|
51
|
+
assert.strictEqual(byFile.size, 1)
|
|
52
|
+
const [file, invoked] = [...byFile.entries()][0]!
|
|
53
|
+
assert.ok(file.endsWith('caller.ts'))
|
|
54
|
+
assert.deepStrictEqual([...invoked], ['console:getSchema'])
|
|
55
|
+
} finally {
|
|
56
|
+
await rm(dir, { recursive: true, force: true })
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('namespaced body invoke joins serviceAggregation.usedFunctions', async () => {
|
|
61
|
+
const { state, dir } = await inspectFiles({
|
|
62
|
+
'caller.ts': `
|
|
63
|
+
declare const rpc: { invoke: (name: string, data?: unknown) => Promise<unknown> }
|
|
64
|
+
export async function doWork() {
|
|
65
|
+
await rpc.invoke('ext:goodbye')
|
|
66
|
+
return rpc.invoke('localHelper')
|
|
67
|
+
}
|
|
68
|
+
`,
|
|
69
|
+
})
|
|
70
|
+
try {
|
|
71
|
+
assert.ok(state.serviceAggregation.usedFunctions.has('ext:goodbye'))
|
|
72
|
+
assert.ok(!state.serviceAggregation.usedFunctions.has('localHelper'))
|
|
73
|
+
} finally {
|
|
74
|
+
await rm(dir, { recursive: true, force: true })
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('multiple invocations in one file accumulate under that file', async () => {
|
|
79
|
+
const { state, dir } = await inspectFiles({
|
|
80
|
+
'caller.ts': `
|
|
81
|
+
declare const rpc: { invoke: (name: string, data?: unknown) => Promise<unknown> }
|
|
82
|
+
export async function doWork() {
|
|
83
|
+
await rpc.invoke('console:getSchema')
|
|
84
|
+
return rpc.invoke('listTasks')
|
|
85
|
+
}
|
|
86
|
+
`,
|
|
87
|
+
})
|
|
88
|
+
try {
|
|
89
|
+
const invoked = [...state.rpc.invokedFunctionsByFile.values()][0]!
|
|
90
|
+
assert.deepStrictEqual([...invoked].sort(), [
|
|
91
|
+
'console:getSchema',
|
|
92
|
+
'listTasks',
|
|
93
|
+
])
|
|
94
|
+
} finally {
|
|
95
|
+
await rm(dir, { recursive: true, force: true })
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('wiring-level ref() lands in invokedFunctions but NOT in the by-file map', async () => {
|
|
100
|
+
const { state, dir } = await inspectFiles({
|
|
101
|
+
'wiring.ts': `
|
|
102
|
+
declare const ref: (name: string) => unknown
|
|
103
|
+
export const routes = { stream: { func: ref('console:streamWorkflowRun') } }
|
|
104
|
+
`,
|
|
105
|
+
})
|
|
106
|
+
try {
|
|
107
|
+
assert.ok(state.rpc.invokedFunctions.has('console:streamWorkflowRun'))
|
|
108
|
+
assert.strictEqual(state.rpc.invokedFunctionsByFile.size, 0)
|
|
109
|
+
} finally {
|
|
110
|
+
await rm(dir, { recursive: true, force: true })
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
})
|
|
@@ -115,10 +115,18 @@ export function addRPCInvocations(
|
|
|
115
115
|
logger.debug(`• Found RPC invocation: ${functionRef}`)
|
|
116
116
|
state.rpc.invokedFunctions.add(functionRef)
|
|
117
117
|
|
|
118
|
+
let byFile = state.rpc.invokedFunctionsByFile.get(sourceFileName)
|
|
119
|
+
if (!byFile) {
|
|
120
|
+
byFile = new Set()
|
|
121
|
+
state.rpc.invokedFunctionsByFile.set(sourceFileName, byFile)
|
|
122
|
+
}
|
|
123
|
+
byFile.add(functionRef)
|
|
124
|
+
|
|
118
125
|
const namespace = extractNamespace(functionRef)
|
|
119
126
|
if (namespace) {
|
|
120
127
|
logger.debug(` → Addon detected: ${namespace}`)
|
|
121
128
|
state.rpc.usedAddons.add(namespace)
|
|
129
|
+
state.serviceAggregation.usedFunctions.add(functionRef)
|
|
122
130
|
}
|
|
123
131
|
}
|
|
124
132
|
// Handle template literals like `function-${name}`
|
package/src/add/add-workflow.ts
CHANGED
|
@@ -16,7 +16,10 @@ import {
|
|
|
16
16
|
getPropertyValue,
|
|
17
17
|
} from '../utils/get-property-value.js'
|
|
18
18
|
import { extractDSLWorkflow } from '../utils/workflow/dsl/extract-dsl-workflow.js'
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
getSourceText,
|
|
21
|
+
extractActorFromOptions,
|
|
22
|
+
} from '../utils/workflow/dsl/patterns.js'
|
|
20
23
|
|
|
21
24
|
/**
|
|
22
25
|
* Extract a workflow step's display name without letting a non-static name
|
|
@@ -86,7 +89,8 @@ export function collectInvokedRPCs(
|
|
|
86
89
|
}
|
|
87
90
|
|
|
88
91
|
/**
|
|
89
|
-
* Scan for workflow.do(), workflow.
|
|
92
|
+
* Scan for workflow.do(), workflow.expectEventually(), workflow.sleep(), and
|
|
93
|
+
* workflow.cancel() calls to extract workflow steps
|
|
90
94
|
*/
|
|
91
95
|
function getWorkflowInvocations(
|
|
92
96
|
node: ts.Node,
|
|
@@ -100,7 +104,12 @@ function getWorkflowInvocations(
|
|
|
100
104
|
const { name } = node
|
|
101
105
|
|
|
102
106
|
// Check if this is accessing 'do' or 'sleep' property
|
|
103
|
-
if (
|
|
107
|
+
if (
|
|
108
|
+
name.text === 'do' ||
|
|
109
|
+
name.text === 'sleep' ||
|
|
110
|
+
name.text === 'cancel' ||
|
|
111
|
+
name.text === 'expectEventually'
|
|
112
|
+
) {
|
|
104
113
|
// Check if the parent is a call expression
|
|
105
114
|
const parent = node.parent
|
|
106
115
|
if (ts.isCallExpression(parent) && parent.expression === node) {
|
|
@@ -125,6 +134,7 @@ function getWorkflowInvocations(
|
|
|
125
134
|
type: 'rpc',
|
|
126
135
|
stepName,
|
|
127
136
|
rpcName,
|
|
137
|
+
actor: extractActorFromOptions(optionsArg),
|
|
128
138
|
})
|
|
129
139
|
state.rpc.invokedFunctions.add(rpcName)
|
|
130
140
|
} else if (isFunctionLike(secondArg)) {
|
|
@@ -135,6 +145,20 @@ function getWorkflowInvocations(
|
|
|
135
145
|
description: description || '<dynamic>',
|
|
136
146
|
})
|
|
137
147
|
}
|
|
148
|
+
} else if (name.text === 'expectEventually' && args.length >= 4) {
|
|
149
|
+
// workflow.expectEventually(stepName, rpcName, data, predicate, options?)
|
|
150
|
+
const stepName = extractStepName(args[0], checker)
|
|
151
|
+
if (isStringLike(args[1], checker)) {
|
|
152
|
+
const rpcName = extractStringLiteral(args[1], checker)
|
|
153
|
+
steps.push({
|
|
154
|
+
type: 'rpc',
|
|
155
|
+
stepName,
|
|
156
|
+
rpcName,
|
|
157
|
+
expectEventually: true,
|
|
158
|
+
actor: extractActorFromOptions(args.length >= 5 ? args[4] : undefined),
|
|
159
|
+
})
|
|
160
|
+
state.rpc.invokedFunctions.add(rpcName)
|
|
161
|
+
}
|
|
138
162
|
} else if (name.text === 'sleep' && args.length >= 2) {
|
|
139
163
|
// workflow.sleep(stepName, duration)
|
|
140
164
|
const stepNameArg = args[0]
|
|
@@ -337,10 +361,23 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
|
|
|
337
361
|
// For pikkuWorkflowComplexFunc, also run basic extraction so RPCs in
|
|
338
362
|
// patterns the DSL extractor doesn't handle (array+push, nested Promise.all
|
|
339
363
|
// with identifier args, etc.) are still registered as invoked functions.
|
|
364
|
+
// When DSL extraction already produced steps this pass is registration-only:
|
|
365
|
+
// appending to `steps` would duplicate nodes and clobber the graph.
|
|
340
366
|
if (wrapperType !== 'dsl') {
|
|
341
|
-
|
|
367
|
+
const sink: WorkflowStepMeta[] = steps.length > 0 ? [] : steps
|
|
368
|
+
getWorkflowInvocations(resolvedFunc, checker, state, workflowName, sink)
|
|
342
369
|
}
|
|
343
370
|
|
|
371
|
+
// Actor names the flow's steps run as (user flows) — powers the console
|
|
372
|
+
// personas view and the User Flow graph badges.
|
|
373
|
+
const actorNames = [
|
|
374
|
+
...new Set(
|
|
375
|
+
steps
|
|
376
|
+
.map((s) => ('actor' in s ? s.actor : undefined))
|
|
377
|
+
.filter((a): a is string => typeof a === 'string')
|
|
378
|
+
),
|
|
379
|
+
]
|
|
380
|
+
|
|
344
381
|
state.workflows.meta[workflowName] = {
|
|
345
382
|
pikkuFuncId,
|
|
346
383
|
name: workflowName,
|
|
@@ -354,6 +391,7 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
|
|
|
354
391
|
tags,
|
|
355
392
|
expose,
|
|
356
393
|
userFlow: wrapperType === 'user-flow' ? true : undefined,
|
|
394
|
+
actors: actorNames.length > 0 ? actorNames : undefined,
|
|
357
395
|
}
|
|
358
396
|
|
|
359
397
|
// User flows are pure stories of remote RPCs (same rule as client-side CLI
|
package/src/inspector.ts
CHANGED
|
@@ -124,6 +124,7 @@ export function getInitialInspectorState(rootDir: string): InspectorState {
|
|
|
124
124
|
exposedMeta: {},
|
|
125
125
|
exposedFiles: new Map(),
|
|
126
126
|
invokedFunctions: new Set(),
|
|
127
|
+
invokedFunctionsByFile: new Map(),
|
|
127
128
|
usedAddons: new Set(),
|
|
128
129
|
wireAddonDeclarations: new Map(),
|
|
129
130
|
wireAddonFiles: new Set(),
|
package/src/types.ts
CHANGED
|
@@ -482,6 +482,7 @@ export interface InspectorState {
|
|
|
482
482
|
exposedMeta: Record<string, string>
|
|
483
483
|
exposedFiles: Map<string, { path: string; exportedName: string }>
|
|
484
484
|
invokedFunctions: Set<string>
|
|
485
|
+
invokedFunctionsByFile: Map<string, Set<string>>
|
|
485
486
|
usedAddons: Set<string>
|
|
486
487
|
wireAddonDeclarations: Map<
|
|
487
488
|
string,
|
|
@@ -234,6 +234,7 @@ function createMockInspectorState(): Omit<InspectorState, 'typesLookup'> {
|
|
|
234
234
|
exposedMeta: {},
|
|
235
235
|
exposedFiles: new Map(),
|
|
236
236
|
invokedFunctions: new Set(),
|
|
237
|
+
invokedFunctionsByFile: new Map(),
|
|
237
238
|
},
|
|
238
239
|
mcpEndpoints: {
|
|
239
240
|
toolsMeta: {
|
|
@@ -1657,3 +1658,233 @@ describe('addonServerlessIncompatible serialization roundtrip', () => {
|
|
|
1657
1658
|
assert.strictEqual(restored.addonServerlessIncompatible.size, 0)
|
|
1658
1659
|
})
|
|
1659
1660
|
})
|
|
1661
|
+
|
|
1662
|
+
describe('addon bootstrap tree-shake', () => {
|
|
1663
|
+
const withAddon = (
|
|
1664
|
+
mutate?: (state: Omit<InspectorState, 'typesLookup'>) => void
|
|
1665
|
+
) => {
|
|
1666
|
+
const state = createMockInspectorState()
|
|
1667
|
+
state.rpc.wireAddonDeclarations = new Map([
|
|
1668
|
+
['console', { package: '@pikku/addon-console' }],
|
|
1669
|
+
])
|
|
1670
|
+
state.rpc.usedAddons = new Set(['console'])
|
|
1671
|
+
state.rpc.invokedFunctions = new Set(['console:streamFunctionTests'])
|
|
1672
|
+
mutate?.(state)
|
|
1673
|
+
return state
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
test('drops an addon nothing kept references', () => {
|
|
1677
|
+
const state = withAddon()
|
|
1678
|
+
const filtered = filterInspectorState(
|
|
1679
|
+
state,
|
|
1680
|
+
{ names: ['getUsers'] },
|
|
1681
|
+
mockLogger
|
|
1682
|
+
)
|
|
1683
|
+
assert.strictEqual(filtered.rpc.wireAddonDeclarations.size, 0)
|
|
1684
|
+
assert.strictEqual(filtered.rpc.usedAddons.size, 0)
|
|
1685
|
+
})
|
|
1686
|
+
|
|
1687
|
+
test('keeps an addon when a kept wiring targets one of its functions', () => {
|
|
1688
|
+
const state = withAddon((s) => {
|
|
1689
|
+
s.http.meta.get['/console/stream'] = {
|
|
1690
|
+
pikkuFuncId: 'console:streamFunctionTests',
|
|
1691
|
+
route: '/console/stream',
|
|
1692
|
+
method: 'GET',
|
|
1693
|
+
tags: [],
|
|
1694
|
+
middleware: [],
|
|
1695
|
+
permissions: [],
|
|
1696
|
+
} as any
|
|
1697
|
+
})
|
|
1698
|
+
const filtered = filterInspectorState(
|
|
1699
|
+
state,
|
|
1700
|
+
{ names: ['console:streamFunctionTests'] },
|
|
1701
|
+
mockLogger
|
|
1702
|
+
)
|
|
1703
|
+
assert.strictEqual(filtered.rpc.wireAddonDeclarations.size, 1)
|
|
1704
|
+
assert.ok(filtered.rpc.wireAddonDeclarations.has('console'))
|
|
1705
|
+
})
|
|
1706
|
+
|
|
1707
|
+
test('keeps an addon when a kept route ref()-targets one of its functions', () => {
|
|
1708
|
+
const state = withAddon((s) => {
|
|
1709
|
+
s.http.meta.get['/function-tests/stream'] = {
|
|
1710
|
+
pikkuFuncId: 'http:get:/function-tests/stream',
|
|
1711
|
+
refTarget: 'console:streamFunctionTests',
|
|
1712
|
+
route: '/function-tests/stream',
|
|
1713
|
+
method: 'GET',
|
|
1714
|
+
tags: [],
|
|
1715
|
+
middleware: [],
|
|
1716
|
+
permissions: [],
|
|
1717
|
+
} as any
|
|
1718
|
+
s.functions.meta['http:get:/function-tests/stream'] = {
|
|
1719
|
+
services: { optimized: false, services: [] },
|
|
1720
|
+
} as any
|
|
1721
|
+
})
|
|
1722
|
+
const filtered = filterInspectorState(
|
|
1723
|
+
state,
|
|
1724
|
+
{ names: ['http:get:/function-tests/stream'] },
|
|
1725
|
+
mockLogger
|
|
1726
|
+
)
|
|
1727
|
+
assert.strictEqual(filtered.rpc.wireAddonDeclarations.size, 1)
|
|
1728
|
+
assert.ok(filtered.rpc.wireAddonDeclarations.has('console'))
|
|
1729
|
+
assert.ok(
|
|
1730
|
+
filtered.serviceAggregation.usedFunctions.has(
|
|
1731
|
+
'console:streamFunctionTests'
|
|
1732
|
+
)
|
|
1733
|
+
)
|
|
1734
|
+
})
|
|
1735
|
+
|
|
1736
|
+
test('drops the addon when the ref()-wired route is filtered out', () => {
|
|
1737
|
+
const state = withAddon((s) => {
|
|
1738
|
+
s.http.meta.get['/function-tests/stream'] = {
|
|
1739
|
+
pikkuFuncId: 'http:get:/function-tests/stream',
|
|
1740
|
+
refTarget: 'console:streamFunctionTests',
|
|
1741
|
+
route: '/function-tests/stream',
|
|
1742
|
+
method: 'GET',
|
|
1743
|
+
tags: [],
|
|
1744
|
+
middleware: [],
|
|
1745
|
+
permissions: [],
|
|
1746
|
+
} as any
|
|
1747
|
+
s.functions.meta['http:get:/function-tests/stream'] = {
|
|
1748
|
+
services: { optimized: false, services: [] },
|
|
1749
|
+
} as any
|
|
1750
|
+
})
|
|
1751
|
+
const filtered = filterInspectorState(
|
|
1752
|
+
state,
|
|
1753
|
+
{ names: ['getUsers'] },
|
|
1754
|
+
mockLogger
|
|
1755
|
+
)
|
|
1756
|
+
assert.strictEqual(filtered.rpc.wireAddonDeclarations.size, 0)
|
|
1757
|
+
assert.ok(
|
|
1758
|
+
!filtered.serviceAggregation.usedFunctions.has(
|
|
1759
|
+
'console:streamFunctionTests'
|
|
1760
|
+
)
|
|
1761
|
+
)
|
|
1762
|
+
})
|
|
1763
|
+
|
|
1764
|
+
test('keeps an addon when a kept MCP tool targets one of its functions', () => {
|
|
1765
|
+
const state = withAddon((s) => {
|
|
1766
|
+
s.mcpEndpoints.toolsMeta['console:getSchema'] = {
|
|
1767
|
+
name: 'console:getSchema',
|
|
1768
|
+
description: 'addon tool',
|
|
1769
|
+
pikkuFuncId: 'console:getSchema',
|
|
1770
|
+
tags: [],
|
|
1771
|
+
middleware: [],
|
|
1772
|
+
permissions: [],
|
|
1773
|
+
} as any
|
|
1774
|
+
})
|
|
1775
|
+
const filtered = filterInspectorState(
|
|
1776
|
+
state,
|
|
1777
|
+
{ names: ['console:getSchema'] },
|
|
1778
|
+
mockLogger
|
|
1779
|
+
)
|
|
1780
|
+
assert.strictEqual(filtered.rpc.wireAddonDeclarations.size, 1)
|
|
1781
|
+
})
|
|
1782
|
+
|
|
1783
|
+
test('keeps an addon when a kept agent lists one of its functions as a tool', () => {
|
|
1784
|
+
const state = withAddon((s) => {
|
|
1785
|
+
s.agents = s.agents ?? ({ agentsMeta: {} } as any)
|
|
1786
|
+
s.agents.agentsMeta['support-agent'] = {
|
|
1787
|
+
name: 'support-agent',
|
|
1788
|
+
pikkuFuncId: 'supportAgent',
|
|
1789
|
+
tools: ['console:getSchema'],
|
|
1790
|
+
tags: [],
|
|
1791
|
+
} as any
|
|
1792
|
+
})
|
|
1793
|
+
const filtered = filterInspectorState(
|
|
1794
|
+
state,
|
|
1795
|
+
{ names: ['support-agent'] },
|
|
1796
|
+
mockLogger
|
|
1797
|
+
)
|
|
1798
|
+
assert.strictEqual(filtered.rpc.wireAddonDeclarations.size, 1)
|
|
1799
|
+
})
|
|
1800
|
+
|
|
1801
|
+
test('keeps an addon when a kept function body-invokes it', () => {
|
|
1802
|
+
const state = withAddon((s) => {
|
|
1803
|
+
s.rpc.invokedFunctionsByFile = new Map([
|
|
1804
|
+
['/test/project/src/api/users.ts', new Set(['console:getSchema'])],
|
|
1805
|
+
])
|
|
1806
|
+
})
|
|
1807
|
+
const filtered = filterInspectorState(
|
|
1808
|
+
state,
|
|
1809
|
+
{ names: ['getUsers'] },
|
|
1810
|
+
mockLogger
|
|
1811
|
+
)
|
|
1812
|
+
assert.strictEqual(filtered.rpc.wireAddonDeclarations.size, 1)
|
|
1813
|
+
})
|
|
1814
|
+
|
|
1815
|
+
test('a kept-file body invoke joins the filtered usedFunctions', () => {
|
|
1816
|
+
const state = withAddon((s) => {
|
|
1817
|
+
s.rpc.invokedFunctionsByFile = new Map([
|
|
1818
|
+
['/test/project/src/api/users.ts', new Set(['console:getSchema'])],
|
|
1819
|
+
])
|
|
1820
|
+
})
|
|
1821
|
+
const filtered = filterInspectorState(
|
|
1822
|
+
state,
|
|
1823
|
+
{ names: ['getUsers'] },
|
|
1824
|
+
mockLogger
|
|
1825
|
+
)
|
|
1826
|
+
assert.ok(
|
|
1827
|
+
filtered.serviceAggregation.usedFunctions.has('console:getSchema')
|
|
1828
|
+
)
|
|
1829
|
+
})
|
|
1830
|
+
|
|
1831
|
+
test('drops an addon body-invoked only from filtered-out files', () => {
|
|
1832
|
+
const state = withAddon((s) => {
|
|
1833
|
+
s.rpc.invokedFunctionsByFile = new Map([
|
|
1834
|
+
['/test/project/src/admin/settings.ts', new Set(['console:getSchema'])],
|
|
1835
|
+
])
|
|
1836
|
+
})
|
|
1837
|
+
const filtered = filterInspectorState(
|
|
1838
|
+
state,
|
|
1839
|
+
{ names: ['getUsers'] },
|
|
1840
|
+
mockLogger
|
|
1841
|
+
)
|
|
1842
|
+
assert.strictEqual(filtered.rpc.wireAddonDeclarations.size, 0)
|
|
1843
|
+
})
|
|
1844
|
+
|
|
1845
|
+
test('unfiltered state keeps all addons', () => {
|
|
1846
|
+
const state = withAddon()
|
|
1847
|
+
const filtered = filterInspectorState(state, {}, mockLogger)
|
|
1848
|
+
assert.strictEqual(filtered.rpc.wireAddonDeclarations.size, 1)
|
|
1849
|
+
})
|
|
1850
|
+
})
|
|
1851
|
+
|
|
1852
|
+
describe('invokedFunctionsByFile serialization', () => {
|
|
1853
|
+
test('round-trips through serialize/deserialize', () => {
|
|
1854
|
+
const state = getInitialInspectorState('/test/project')
|
|
1855
|
+
state.rpc.invokedFunctionsByFile = new Map([
|
|
1856
|
+
['/test/project/src/api/users.ts', new Set(['console:getSchema'])],
|
|
1857
|
+
[
|
|
1858
|
+
'/test/project/src/api/tasks.ts',
|
|
1859
|
+
new Set(['listTasks', 'console:runAgent']),
|
|
1860
|
+
],
|
|
1861
|
+
])
|
|
1862
|
+
const restored = deserializeInspectorState(
|
|
1863
|
+
JSON.parse(JSON.stringify(serializeInspectorState(state as any)))
|
|
1864
|
+
)
|
|
1865
|
+
assert.ok(restored.rpc.invokedFunctionsByFile instanceof Map)
|
|
1866
|
+
assert.strictEqual(restored.rpc.invokedFunctionsByFile.size, 2)
|
|
1867
|
+
assert.deepStrictEqual(
|
|
1868
|
+
[
|
|
1869
|
+
...restored.rpc.invokedFunctionsByFile.get(
|
|
1870
|
+
'/test/project/src/api/tasks.ts'
|
|
1871
|
+
)!,
|
|
1872
|
+
].sort(),
|
|
1873
|
+
['console:runAgent', 'listTasks']
|
|
1874
|
+
)
|
|
1875
|
+
})
|
|
1876
|
+
|
|
1877
|
+
test('deserializing legacy state without the field yields an empty Map', () => {
|
|
1878
|
+
const serialized = JSON.parse(
|
|
1879
|
+
JSON.stringify(
|
|
1880
|
+
serializeInspectorState(
|
|
1881
|
+
getInitialInspectorState('/test/project') as any
|
|
1882
|
+
)
|
|
1883
|
+
)
|
|
1884
|
+
)
|
|
1885
|
+
delete serialized.rpc.invokedFunctionsByFile
|
|
1886
|
+
const restored = deserializeInspectorState(serialized)
|
|
1887
|
+
assert.ok(restored.rpc.invokedFunctionsByFile instanceof Map)
|
|
1888
|
+
assert.strictEqual(restored.rpc.invokedFunctionsByFile.size, 0)
|
|
1889
|
+
})
|
|
1890
|
+
})
|
|
@@ -468,6 +468,11 @@ export function filterInspectorState(
|
|
|
468
468
|
filteredState.serviceAggregation.usedFunctions.add(
|
|
469
469
|
routeMeta.pikkuFuncId
|
|
470
470
|
)
|
|
471
|
+
if (routeMeta.refTarget) {
|
|
472
|
+
filteredState.serviceAggregation.usedFunctions.add(
|
|
473
|
+
routeMeta.refTarget
|
|
474
|
+
)
|
|
475
|
+
}
|
|
471
476
|
// For workflow/agent routes, also add the base name
|
|
472
477
|
// so the workflow/agent definition survives pruning
|
|
473
478
|
const colonIdx = routeMeta.pikkuFuncId.indexOf(':')
|
|
@@ -1105,6 +1110,57 @@ export function filterInspectorState(
|
|
|
1105
1110
|
filteredState.serviceAggregation.requiredServices.add('queueService')
|
|
1106
1111
|
}
|
|
1107
1112
|
|
|
1113
|
+
if ((filteredState.rpc.wireAddonDeclarations?.size ?? 0) > 0) {
|
|
1114
|
+
const referencedIds = new Set<string>(
|
|
1115
|
+
filteredState.serviceAggregation.usedFunctions
|
|
1116
|
+
)
|
|
1117
|
+
const keptFiles = new Set<string>()
|
|
1118
|
+
for (const funcId of filteredState.serviceAggregation.usedFunctions) {
|
|
1119
|
+
const file = filteredState.functions.files.get(funcId)
|
|
1120
|
+
if (file?.path) keptFiles.add(file.path)
|
|
1121
|
+
}
|
|
1122
|
+
for (const [file, invoked] of state.rpc.invokedFunctionsByFile ??
|
|
1123
|
+
new Map<string, Set<string>>()) {
|
|
1124
|
+
if (!keptFiles.has(file)) continue
|
|
1125
|
+
for (const id of invoked) {
|
|
1126
|
+
referencedIds.add(id)
|
|
1127
|
+
if (id.includes(':')) {
|
|
1128
|
+
filteredState.serviceAggregation.usedFunctions.add(id)
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
for (const toolMeta of Object.values(
|
|
1133
|
+
filteredState.mcpEndpoints?.toolsMeta ?? {}
|
|
1134
|
+
) as Array<{ pikkuFuncId?: string }>) {
|
|
1135
|
+
if (toolMeta.pikkuFuncId) referencedIds.add(toolMeta.pikkuFuncId)
|
|
1136
|
+
}
|
|
1137
|
+
for (const agentMeta of Object.values(
|
|
1138
|
+
filteredState.agents?.agentsMeta ?? {}
|
|
1139
|
+
) as Array<{ tools?: string[] }>) {
|
|
1140
|
+
for (const tool of agentMeta.tools ?? []) referencedIds.add(tool)
|
|
1141
|
+
}
|
|
1142
|
+
const keptNamespaces = new Set<string>()
|
|
1143
|
+
for (const namespace of filteredState.rpc.wireAddonDeclarations.keys()) {
|
|
1144
|
+
const prefix = `${namespace}:`
|
|
1145
|
+
for (const id of referencedIds) {
|
|
1146
|
+
if (id.startsWith(prefix)) {
|
|
1147
|
+
keptNamespaces.add(namespace)
|
|
1148
|
+
break
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
filteredState.rpc.wireAddonDeclarations = new Map(
|
|
1153
|
+
[...filteredState.rpc.wireAddonDeclarations].filter(([namespace]) =>
|
|
1154
|
+
keptNamespaces.has(namespace)
|
|
1155
|
+
)
|
|
1156
|
+
)
|
|
1157
|
+
filteredState.rpc.usedAddons = new Set(
|
|
1158
|
+
[...filteredState.rpc.usedAddons].filter((namespace) =>
|
|
1159
|
+
keptNamespaces.has(namespace)
|
|
1160
|
+
)
|
|
1161
|
+
)
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1108
1164
|
// Recalculate requiredServices based on filtered functions/middleware/permissions
|
|
1109
1165
|
// Need to cast to InspectorState temporarily for aggregateRequiredServices
|
|
1110
1166
|
const stateForAggregation = filteredState as InspectorState
|
|
@@ -200,6 +200,7 @@ export async function loadAddonFunctionsMeta(
|
|
|
200
200
|
// No variables meta — that's fine
|
|
201
201
|
}
|
|
202
202
|
// Load addon serverlessIncompatible service names from pikku-addon-meta.gen.json
|
|
203
|
+
let loadedParentServices = false
|
|
203
204
|
try {
|
|
204
205
|
const addonMetaPath = require.resolve(
|
|
205
206
|
`${decl.package}/.pikku/console/pikku-addon-meta.gen.json`
|
|
@@ -218,29 +219,38 @@ export async function loadAddonFunctionsMeta(
|
|
|
218
219
|
`Addon '${namespace}' marks [${addonMeta.serverlessIncompatible.join(', ')}] as serverless-incompatible`
|
|
219
220
|
)
|
|
220
221
|
}
|
|
221
|
-
} catch {
|
|
222
|
-
// No addon meta or no serverlessIncompatible declared — that's fine
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Load addon required parent services from pikku-services.gen
|
|
226
|
-
try {
|
|
227
|
-
const servicesGenPath = require.resolve(
|
|
228
|
-
`${decl.package}/.pikku/pikku-services.gen.js`
|
|
229
|
-
)
|
|
230
|
-
const servicesModule = await import(servicesGenPath)
|
|
231
222
|
if (
|
|
232
|
-
|
|
233
|
-
|
|
223
|
+
Array.isArray(addonMeta.requiredParentServices) &&
|
|
224
|
+
addonMeta.requiredParentServices.length > 0
|
|
234
225
|
) {
|
|
235
|
-
for (const service of
|
|
226
|
+
for (const service of addonMeta.requiredParentServices) {
|
|
236
227
|
state.addonRequiredParentServices.push(service)
|
|
237
228
|
}
|
|
229
|
+
loadedParentServices = true
|
|
238
230
|
logger.debug(
|
|
239
|
-
`Loaded ${
|
|
231
|
+
`Loaded ${addonMeta.requiredParentServices.length} required parent services for '${namespace}' from addon meta`
|
|
240
232
|
)
|
|
241
233
|
}
|
|
242
|
-
} catch {
|
|
243
|
-
|
|
234
|
+
} catch {}
|
|
235
|
+
|
|
236
|
+
if (!loadedParentServices) {
|
|
237
|
+
try {
|
|
238
|
+
const servicesGenPath = require.resolve(
|
|
239
|
+
`${decl.package}/.pikku/pikku-services.gen.js`
|
|
240
|
+
)
|
|
241
|
+
const servicesModule = await import(servicesGenPath)
|
|
242
|
+
if (
|
|
243
|
+
servicesModule.requiredParentServices &&
|
|
244
|
+
Array.isArray(servicesModule.requiredParentServices)
|
|
245
|
+
) {
|
|
246
|
+
for (const service of servicesModule.requiredParentServices) {
|
|
247
|
+
state.addonRequiredParentServices.push(service)
|
|
248
|
+
}
|
|
249
|
+
logger.debug(
|
|
250
|
+
`Loaded ${servicesModule.requiredParentServices.length} required parent services for '${namespace}' from ${decl.package}`
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
} catch {}
|
|
244
254
|
}
|
|
245
255
|
|
|
246
256
|
try {
|