@pikku/inspector 0.12.34 → 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 +15 -0
- package/dist/add/add-http-route.js +4 -1
- package/dist/add/add-rpc-invocations.js +7 -0
- 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/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/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/tsconfig.tsbuildinfo +1 -1
|
@@ -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 {
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { strict as assert } from 'assert'
|
|
2
|
+
import { describe, test } from 'node:test'
|
|
3
|
+
import { aggregateRequiredServices } from './post-process.js'
|
|
4
|
+
import type { InspectorState } from '../types.js'
|
|
5
|
+
|
|
6
|
+
function makeState(
|
|
7
|
+
overrides: {
|
|
8
|
+
usedFunctions?: string[]
|
|
9
|
+
functionsMeta?: Record<string, any>
|
|
10
|
+
addonFunctions?: Record<string, Record<string, any>>
|
|
11
|
+
addonRequiredParentServices?: string[]
|
|
12
|
+
} = {}
|
|
13
|
+
): Omit<InspectorState, 'typesLookup'> {
|
|
14
|
+
return {
|
|
15
|
+
serviceAggregation: {
|
|
16
|
+
requiredServices: new Set<string>(),
|
|
17
|
+
usedFunctions: new Set(overrides.usedFunctions ?? []),
|
|
18
|
+
usedMiddleware: new Set<string>(),
|
|
19
|
+
usedPermissions: new Set<string>(),
|
|
20
|
+
allSingletonServices: [],
|
|
21
|
+
allWireServices: [],
|
|
22
|
+
},
|
|
23
|
+
functions: { meta: overrides.functionsMeta ?? {} },
|
|
24
|
+
middleware: { definitions: {}, tagMiddleware: new Map() },
|
|
25
|
+
permissions: { definitions: {}, tagPermissions: new Map() },
|
|
26
|
+
http: {
|
|
27
|
+
meta: {
|
|
28
|
+
get: {},
|
|
29
|
+
post: {},
|
|
30
|
+
put: {},
|
|
31
|
+
patch: {},
|
|
32
|
+
delete: {},
|
|
33
|
+
head: {},
|
|
34
|
+
options: {},
|
|
35
|
+
},
|
|
36
|
+
routeMiddleware: new Map(),
|
|
37
|
+
routePermissions: new Map(),
|
|
38
|
+
},
|
|
39
|
+
channels: { meta: {} },
|
|
40
|
+
queueWorkers: { meta: {} },
|
|
41
|
+
scheduledTasks: { meta: {} },
|
|
42
|
+
mcpEndpoints: { toolsMeta: {}, promptsMeta: {}, resourcesMeta: {} },
|
|
43
|
+
agents: { agentsMeta: {} },
|
|
44
|
+
workflows: { meta: {}, graphMeta: {} },
|
|
45
|
+
wireServicesMeta: new Map(),
|
|
46
|
+
rpc: { internalMeta: {}, exposedMeta: {} },
|
|
47
|
+
addonFunctions: overrides.addonFunctions ?? {},
|
|
48
|
+
addonRequiredParentServices: overrides.addonRequiredParentServices ?? [],
|
|
49
|
+
} as unknown as Omit<InspectorState, 'typesLookup'>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const CONSOLE_PARENT_SERVICES = [
|
|
53
|
+
'metaService',
|
|
54
|
+
'aiAgentRunner',
|
|
55
|
+
'deploymentService',
|
|
56
|
+
'credentialService',
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
describe('aggregateRequiredServices — per-function addon services', () => {
|
|
60
|
+
test('a used addon function adds only its own parent services', () => {
|
|
61
|
+
const state = makeState({
|
|
62
|
+
usedFunctions: ['console:getSchema'],
|
|
63
|
+
addonFunctions: {
|
|
64
|
+
console: {
|
|
65
|
+
getSchema: {
|
|
66
|
+
services: { optimized: true, services: ['metaService'] },
|
|
67
|
+
},
|
|
68
|
+
runAgent: {
|
|
69
|
+
services: {
|
|
70
|
+
optimized: true,
|
|
71
|
+
services: ['aiAgentRunner', 'deploymentService'],
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
addonRequiredParentServices: CONSOLE_PARENT_SERVICES,
|
|
77
|
+
})
|
|
78
|
+
aggregateRequiredServices(state)
|
|
79
|
+
const required = state.serviceAggregation.requiredServices
|
|
80
|
+
assert.ok(required.has('metaService'))
|
|
81
|
+
assert.ok(!required.has('aiAgentRunner'))
|
|
82
|
+
assert.ok(!required.has('deploymentService'))
|
|
83
|
+
assert.ok(!required.has('credentialService'))
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('an addon-created service falls back to the full parent blanket', () => {
|
|
87
|
+
const state = makeState({
|
|
88
|
+
usedFunctions: ['console:editCode'],
|
|
89
|
+
addonFunctions: {
|
|
90
|
+
console: {
|
|
91
|
+
editCode: {
|
|
92
|
+
services: {
|
|
93
|
+
optimized: true,
|
|
94
|
+
services: ['codeEditService', 'metaService'],
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
addonRequiredParentServices: CONSOLE_PARENT_SERVICES,
|
|
100
|
+
})
|
|
101
|
+
aggregateRequiredServices(state)
|
|
102
|
+
const required = state.serviceAggregation.requiredServices
|
|
103
|
+
for (const service of CONSOLE_PARENT_SERVICES) {
|
|
104
|
+
assert.ok(required.has(service), `expected blanket to add ${service}`)
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('missing services meta (old addon build) falls back to the blanket', () => {
|
|
109
|
+
const state = makeState({
|
|
110
|
+
usedFunctions: ['console:getSchema'],
|
|
111
|
+
addonFunctions: { console: { getSchema: {} } },
|
|
112
|
+
addonRequiredParentServices: CONSOLE_PARENT_SERVICES,
|
|
113
|
+
})
|
|
114
|
+
aggregateRequiredServices(state)
|
|
115
|
+
const required = state.serviceAggregation.requiredServices
|
|
116
|
+
for (const service of CONSOLE_PARENT_SERVICES) {
|
|
117
|
+
assert.ok(required.has(service), `expected blanket to add ${service}`)
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('a bare project function colliding with an addon function name adds nothing', () => {
|
|
122
|
+
const state = makeState({
|
|
123
|
+
usedFunctions: ['getAgentThreads'],
|
|
124
|
+
functionsMeta: {
|
|
125
|
+
getAgentThreads: {
|
|
126
|
+
services: { optimized: true, services: ['kysely'] },
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
addonFunctions: {
|
|
130
|
+
console: {
|
|
131
|
+
getAgentThreads: {
|
|
132
|
+
services: { optimized: true, services: ['metaService'] },
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
addonRequiredParentServices: CONSOLE_PARENT_SERVICES,
|
|
137
|
+
})
|
|
138
|
+
aggregateRequiredServices(state)
|
|
139
|
+
const required = state.serviceAggregation.requiredServices
|
|
140
|
+
assert.ok(required.has('kysely'))
|
|
141
|
+
assert.ok(!required.has('metaService'))
|
|
142
|
+
assert.ok(!required.has('aiAgentRunner'))
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test('no used addon functions adds no parent services', () => {
|
|
146
|
+
const state = makeState({
|
|
147
|
+
usedFunctions: ['listTasks'],
|
|
148
|
+
functionsMeta: {
|
|
149
|
+
listTasks: { services: { optimized: true, services: ['kysely'] } },
|
|
150
|
+
},
|
|
151
|
+
addonFunctions: {
|
|
152
|
+
console: {
|
|
153
|
+
getSchema: {
|
|
154
|
+
services: { optimized: true, services: ['metaService'] },
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
addonRequiredParentServices: CONSOLE_PARENT_SERVICES,
|
|
159
|
+
})
|
|
160
|
+
aggregateRequiredServices(state)
|
|
161
|
+
const required = state.serviceAggregation.requiredServices
|
|
162
|
+
assert.ok(!required.has('metaService'))
|
|
163
|
+
assert.ok(!required.has('aiAgentRunner'))
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
test('internal services in addon function meta never force the blanket', () => {
|
|
167
|
+
const state = makeState({
|
|
168
|
+
usedFunctions: ['console:getSchema'],
|
|
169
|
+
addonFunctions: {
|
|
170
|
+
console: {
|
|
171
|
+
getSchema: {
|
|
172
|
+
services: {
|
|
173
|
+
optimized: true,
|
|
174
|
+
services: ['metaService', 'rpc', 'channel'],
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
addonRequiredParentServices: CONSOLE_PARENT_SERVICES,
|
|
180
|
+
})
|
|
181
|
+
aggregateRequiredServices(state)
|
|
182
|
+
const required = state.serviceAggregation.requiredServices
|
|
183
|
+
assert.ok(required.has('metaService'))
|
|
184
|
+
assert.ok(!required.has('aiAgentRunner'))
|
|
185
|
+
assert.ok(!required.has('rpc'))
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
test('default framework services in addon function meta never force the blanket', () => {
|
|
189
|
+
const state = makeState({
|
|
190
|
+
usedFunctions: ['ext:goodbye'],
|
|
191
|
+
addonFunctions: {
|
|
192
|
+
ext: {
|
|
193
|
+
goodbye: {
|
|
194
|
+
services: { optimized: true, services: ['logger', 'config'] },
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
addonRequiredParentServices: ['greetingStore', 'auditSink'],
|
|
199
|
+
})
|
|
200
|
+
aggregateRequiredServices(state)
|
|
201
|
+
const required = state.serviceAggregation.requiredServices
|
|
202
|
+
assert.ok(!required.has('greetingStore'))
|
|
203
|
+
assert.ok(!required.has('auditSink'))
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
test('a ref()-wired route (inline id + namespaced target) aggregates the target services', () => {
|
|
207
|
+
const state = makeState({
|
|
208
|
+
usedFunctions: [
|
|
209
|
+
'http:get:/function-tests/stream',
|
|
210
|
+
'console:streamFunctionTests',
|
|
211
|
+
],
|
|
212
|
+
functionsMeta: {
|
|
213
|
+
'http:get:/function-tests/stream': {
|
|
214
|
+
services: { optimized: false, services: [] },
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
addonFunctions: {
|
|
218
|
+
console: {
|
|
219
|
+
streamFunctionTests: {
|
|
220
|
+
services: { optimized: true, services: ['metaService'] },
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
addonRequiredParentServices: CONSOLE_PARENT_SERVICES,
|
|
225
|
+
})
|
|
226
|
+
aggregateRequiredServices(state)
|
|
227
|
+
const required = state.serviceAggregation.requiredServices
|
|
228
|
+
assert.ok(required.has('metaService'))
|
|
229
|
+
assert.ok(!required.has('aiAgentRunner'))
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
test('multiple used addon functions union their parent services', () => {
|
|
233
|
+
const state = makeState({
|
|
234
|
+
usedFunctions: ['console:getSchema', 'console:runAgent'],
|
|
235
|
+
addonFunctions: {
|
|
236
|
+
console: {
|
|
237
|
+
getSchema: {
|
|
238
|
+
services: { optimized: true, services: ['metaService'] },
|
|
239
|
+
},
|
|
240
|
+
runAgent: {
|
|
241
|
+
services: { optimized: true, services: ['aiAgentRunner'] },
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
addonRequiredParentServices: CONSOLE_PARENT_SERVICES,
|
|
246
|
+
})
|
|
247
|
+
aggregateRequiredServices(state)
|
|
248
|
+
const required = state.serviceAggregation.requiredServices
|
|
249
|
+
assert.ok(required.has('metaService'))
|
|
250
|
+
assert.ok(required.has('aiAgentRunner'))
|
|
251
|
+
assert.ok(!required.has('deploymentService'))
|
|
252
|
+
assert.ok(!required.has('credentialService'))
|
|
253
|
+
})
|
|
254
|
+
})
|
|
@@ -325,20 +325,47 @@ export function aggregateRequiredServices(
|
|
|
325
325
|
}
|
|
326
326
|
|
|
327
327
|
// 7. Services that consumed addons need from the parent project.
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
}
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
328
|
+
const addonFnServices = new Map<string, string[] | undefined>()
|
|
329
|
+
for (const [namespace, fns] of Object.entries(state.addonFunctions ?? {})) {
|
|
330
|
+
for (const [id, meta] of Object.entries(fns)) {
|
|
331
|
+
addonFnServices.set(
|
|
332
|
+
`${namespace}:${id}`,
|
|
333
|
+
(meta as { services?: FunctionServicesMeta })?.services?.services
|
|
334
|
+
)
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
const parentDeclared = state.addonRequiredParentServices ?? []
|
|
338
|
+
const parentDeclaredSet = new Set(parentDeclared)
|
|
339
|
+
const defaultServices = new Set([
|
|
340
|
+
'config',
|
|
341
|
+
'logger',
|
|
342
|
+
'variables',
|
|
343
|
+
'schema',
|
|
344
|
+
'secrets',
|
|
345
|
+
])
|
|
346
|
+
let usesAddonFn = false
|
|
347
|
+
let addonFactoryNeeded = false
|
|
348
|
+
for (const funcId of usedFunctions) {
|
|
349
|
+
if (!addonFnServices.has(funcId)) continue
|
|
350
|
+
usesAddonFn = true
|
|
351
|
+
const services = addonFnServices.get(funcId)
|
|
352
|
+
if (!services) {
|
|
353
|
+
addonFactoryNeeded = true
|
|
354
|
+
continue
|
|
355
|
+
}
|
|
356
|
+
for (const service of services) {
|
|
357
|
+
if (parentDeclaredSet.has(service)) {
|
|
358
|
+
requiredServices.add(service)
|
|
359
|
+
} else if (
|
|
360
|
+
!internalServices.has(service) &&
|
|
361
|
+
!defaultServices.has(service)
|
|
362
|
+
) {
|
|
363
|
+
addonFactoryNeeded = true
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (usesAddonFn && addonFactoryNeeded) {
|
|
368
|
+
for (const service of parentDeclared) {
|
|
342
369
|
requiredServices.add(service)
|
|
343
370
|
}
|
|
344
371
|
}
|