@pikku/inspector 0.12.47 → 0.12.49
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 +218 -0
- package/LICENSE +106 -0
- package/README.md +28 -2
- package/dist/add/add-feature.d.ts +19 -0
- package/dist/add/add-feature.js +200 -0
- package/dist/add/add-functions.js +38 -9
- package/dist/add/add-workflow.js +22 -29
- package/dist/add/scenario-instrumentation.d.ts +24 -0
- package/dist/add/scenario-instrumentation.js +29 -0
- package/dist/error-codes.d.ts +3 -0
- package/dist/error-codes.js +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -0
- package/dist/inspector.js +39 -2
- package/dist/types.d.ts +43 -0
- package/dist/utils/extract-function-name.js +27 -0
- package/dist/utils/post-process.d.ts +26 -0
- package/dist/utils/post-process.js +92 -0
- package/dist/utils/serialize-inspector-state.d.ts +2 -1
- package/dist/utils/serialize-inspector-state.js +2 -0
- package/dist/utils/workflow/dsl/extract-dsl-workflow.js +203 -19
- package/dist/utils/workflow/dsl/patterns.d.ts +21 -0
- package/dist/utils/workflow/dsl/patterns.js +30 -0
- package/dist/utils/workflow/dsl/validation.d.ts +5 -0
- package/dist/utils/workflow/dsl/validation.js +9 -0
- package/dist/utils/workflow/graph/convert-dsl-to-graph.js +36 -0
- package/dist/utils/workflow/graph/workflow-graph.types.d.ts +5 -1
- package/dist/visit.js +2 -0
- package/package.json +4 -3
- package/src/add/add-feature.test.ts +306 -0
- package/src/add/add-feature.ts +253 -0
- package/src/add/add-functions.ts +47 -9
- package/src/add/add-scenario-step.test.ts +311 -0
- package/src/add/add-workflow-silent-drops.test.ts +98 -0
- package/src/add/add-workflow.ts +25 -37
- package/src/add/scenario-instrumentation.test.ts +89 -0
- package/src/add/scenario-instrumentation.ts +36 -0
- package/src/add/scenario-marker.test.ts +89 -0
- package/src/add/scenario-not-extractable.test.ts +159 -0
- package/src/add/scenario-services.test.ts +103 -0
- package/src/add/scenario-skip.test.ts +83 -0
- package/src/error-codes.ts +3 -0
- package/src/index.ts +6 -0
- package/src/inspector.ts +50 -1
- package/src/tsconfig-paths.test.ts +90 -0
- package/src/types.ts +45 -0
- package/src/utils/extract-function-name.ts +36 -0
- package/src/utils/post-process.test.ts +30 -1
- package/src/utils/post-process.ts +103 -0
- package/src/utils/serialize-inspector-state.ts +8 -1
- package/src/utils/workflow/dsl/extract-dsl-workflow.ts +236 -22
- package/src/utils/workflow/dsl/patterns.ts +38 -0
- package/src/utils/workflow/dsl/validation.ts +12 -0
- package/src/utils/workflow/graph/convert-dsl-to-graph-skip.test.ts +35 -0
- package/src/utils/workflow/graph/convert-dsl-to-graph.ts +36 -0
- package/src/utils/workflow/graph/workflow-graph.types.ts +5 -0
- package/src/visit.ts +2 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/types.ts
CHANGED
|
@@ -273,6 +273,12 @@ export type InspectorOptions = Partial<{
|
|
|
273
273
|
rootDir: string
|
|
274
274
|
isAddon: boolean
|
|
275
275
|
sourceFile: ts.SourceFile
|
|
276
|
+
/**
|
|
277
|
+
* The project's tsconfig, read for its path mappings only. The inspector's
|
|
278
|
+
* program needs them to resolve imports the project reaches through `paths`;
|
|
279
|
+
* unlike `schemaConfig.tsconfig` this is also needed on a setup-only run.
|
|
280
|
+
*/
|
|
281
|
+
tsconfig: string
|
|
276
282
|
types: Partial<{
|
|
277
283
|
configFileType: string
|
|
278
284
|
userSessionType: string
|
|
@@ -423,6 +429,36 @@ export interface AuthDefinition {
|
|
|
423
429
|
services: FunctionServicesMeta
|
|
424
430
|
}
|
|
425
431
|
|
|
432
|
+
/** One statically resolved entry of a feature's `scenarios` array. */
|
|
433
|
+
export interface InspectorFeatureEntry {
|
|
434
|
+
/** The scenario's declared export name, with any import alias followed back. */
|
|
435
|
+
scenario: string
|
|
436
|
+
/** A `{ scenario, data }` entry's literal data — gherkin's `Examples:`. */
|
|
437
|
+
data?: unknown
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** A `pikkuFeature` export, as far as it can be read from the source. */
|
|
441
|
+
export interface InspectorFeature {
|
|
442
|
+
path: string
|
|
443
|
+
exportedName: string
|
|
444
|
+
name?: string
|
|
445
|
+
description?: string
|
|
446
|
+
tags?: string[]
|
|
447
|
+
entries: InspectorFeatureEntry[]
|
|
448
|
+
/**
|
|
449
|
+
* Entries that are not literal — a spread, a `.map()`, a call. Counted rather
|
|
450
|
+
* than dropped so the console can say the listing is partial instead of
|
|
451
|
+
* quietly showing a short feature.
|
|
452
|
+
*/
|
|
453
|
+
unresolvedEntries: number
|
|
454
|
+
/**
|
|
455
|
+
* Feature hooks run once around the whole group, never per scenario, and are
|
|
456
|
+
* runtime-only — so only their presence is recorded.
|
|
457
|
+
*/
|
|
458
|
+
hasBefore: boolean
|
|
459
|
+
hasAfter: boolean
|
|
460
|
+
}
|
|
461
|
+
|
|
426
462
|
export interface InspectorState {
|
|
427
463
|
rootDir: string // Root directory inferred from source files
|
|
428
464
|
singletonServicesTypeImportMap: PathToNameAndType
|
|
@@ -466,6 +502,15 @@ export interface InspectorState {
|
|
|
466
502
|
files: Map<string, { path: string; exportedName: string }>
|
|
467
503
|
graphMeta: SerializedWorkflowGraphs
|
|
468
504
|
graphFiles: Map<string, { path: string; exportedName: string }>
|
|
505
|
+
/**
|
|
506
|
+
* `pikkuFeature` exports, keyed by export identifier.
|
|
507
|
+
*
|
|
508
|
+
* Everything the config states literally is extracted here, because a
|
|
509
|
+
* feature is the document structure the console renders. Entries built by
|
|
510
|
+
* an ordinary loop cannot be enumerated statically — those are counted in
|
|
511
|
+
* `unresolvedEntries` and resolved at runtime by object identity instead.
|
|
512
|
+
*/
|
|
513
|
+
featureFiles: Map<string, InspectorFeature>
|
|
469
514
|
invokedWorkflows: Set<string>
|
|
470
515
|
}
|
|
471
516
|
rpc: {
|
|
@@ -45,6 +45,40 @@ export function funcIdToTypeName(id: string): string {
|
|
|
45
45
|
.join('')
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* `pikkuScenarioStep({ name })` is the one wrapper whose `name` is its identity:
|
|
50
|
+
* scenarios reference a step by that string, so it must become the pikkuFuncId
|
|
51
|
+
* (and hence the generated step map's key), not just a cosmetic label the way
|
|
52
|
+
* `pikkuMiddleware`/`pikkuAgent` use it.
|
|
53
|
+
*/
|
|
54
|
+
function extractScenarioStepName(
|
|
55
|
+
callExpr: ts.Node,
|
|
56
|
+
result: ExtractedFunctionName
|
|
57
|
+
): void {
|
|
58
|
+
if (
|
|
59
|
+
!ts.isCallExpression(callExpr) ||
|
|
60
|
+
!ts.isIdentifier(callExpr.expression) ||
|
|
61
|
+
callExpr.expression.text !== 'pikkuScenarioStep'
|
|
62
|
+
) {
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
const firstArg = callExpr.arguments[0]
|
|
66
|
+
if (!firstArg || !ts.isObjectLiteralExpression(firstArg)) {
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
for (const prop of firstArg.properties) {
|
|
70
|
+
if (
|
|
71
|
+
ts.isPropertyAssignment(prop) &&
|
|
72
|
+
ts.isIdentifier(prop.name) &&
|
|
73
|
+
prop.name.text === 'name' &&
|
|
74
|
+
ts.isStringLiteral(prop.initializer)
|
|
75
|
+
) {
|
|
76
|
+
result.explicitName = prop.initializer.text
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
48
82
|
export function extractFunctionName(
|
|
49
83
|
callExpr: ts.Node,
|
|
50
84
|
checker: ts.TypeChecker,
|
|
@@ -63,6 +97,8 @@ export function extractFunctionName(
|
|
|
63
97
|
version: null,
|
|
64
98
|
}
|
|
65
99
|
|
|
100
|
+
extractScenarioStepName(callExpr, result)
|
|
101
|
+
|
|
66
102
|
const workflowHelpers = new Set([
|
|
67
103
|
'workflow',
|
|
68
104
|
'workflowStart',
|
|
@@ -64,6 +64,7 @@ function makeState(
|
|
|
64
64
|
addonFunctions?: Record<string, Record<string, any>>
|
|
65
65
|
addonRequiredParentServices?: string[]
|
|
66
66
|
authServices?: string[]
|
|
67
|
+
graphMeta?: Record<string, any>
|
|
67
68
|
} = {}
|
|
68
69
|
): Omit<InspectorState, 'typesLookup'> {
|
|
69
70
|
return {
|
|
@@ -95,7 +96,7 @@ function makeState(
|
|
|
95
96
|
scheduledTasks: { meta: {} },
|
|
96
97
|
mcpEndpoints: { toolsMeta: {}, promptsMeta: {}, resourcesMeta: {} },
|
|
97
98
|
agents: { agentsMeta: {} },
|
|
98
|
-
workflows: { meta: {}, graphMeta: {} },
|
|
99
|
+
workflows: { meta: {}, graphMeta: overrides.graphMeta ?? {} },
|
|
99
100
|
wireServicesMeta: new Map(),
|
|
100
101
|
rpc: { internalMeta: {}, exposedMeta: {} },
|
|
101
102
|
addonFunctions: overrides.addonFunctions ?? {},
|
|
@@ -117,6 +118,34 @@ const CONSOLE_PARENT_SERVICES = [
|
|
|
117
118
|
'credentialService',
|
|
118
119
|
]
|
|
119
120
|
|
|
121
|
+
describe('aggregateRequiredServices — synthetic workflow queue workers', () => {
|
|
122
|
+
const graphMeta = {
|
|
123
|
+
orderWorkflow: {
|
|
124
|
+
name: 'orderWorkflow',
|
|
125
|
+
source: 'dsl',
|
|
126
|
+
nodes: { 'step-1': { rpcName: 'chargeCard' } },
|
|
127
|
+
},
|
|
128
|
+
loginScenario: {
|
|
129
|
+
name: 'loginScenario',
|
|
130
|
+
source: 'scenario',
|
|
131
|
+
nodes: { 'step-1': { rpcName: 'opensPage' } },
|
|
132
|
+
},
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// A scenario IS a workflow, so it used to be handed an orchestrator queue
|
|
136
|
+
// worker in the app's queue meta — which the app bootstrap registers and every
|
|
137
|
+
// deployed bundle therefore carries, leaving a provider to create a production
|
|
138
|
+
// queue named after a test. Nothing dispatches a scenario through a queue:
|
|
139
|
+
// `pikku scenario run` executes its steps in-process.
|
|
140
|
+
test('a scenario gets no orchestrator queue worker', () => {
|
|
141
|
+
const state = makeState({ graphMeta })
|
|
142
|
+
aggregateRequiredServices(state)
|
|
143
|
+
assert.deepEqual(Object.keys(state.queueWorkers.meta), [
|
|
144
|
+
'wf-orchestrator-order-workflow',
|
|
145
|
+
])
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
|
|
120
149
|
describe('aggregateRequiredServices — per-function addon services', () => {
|
|
121
150
|
test('a used addon function adds only its own parent services', () => {
|
|
122
151
|
const state = makeState({
|
|
@@ -16,6 +16,8 @@ import { extractTypeKeys } from './type-utils.js'
|
|
|
16
16
|
import { ErrorCode } from '../error-codes.js'
|
|
17
17
|
import { AUTH_HANDLER_FUNC_ID } from '../add/add-auth.js'
|
|
18
18
|
import { flattenScopeDefinitions } from '@pikku/core/scope'
|
|
19
|
+
import type { WorkflowStepMeta } from '@pikku/core/workflow'
|
|
20
|
+
import { DYNAMIC_SCENARIO_STEP_TARGET } from './workflow/dsl/patterns.js'
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* Stamp the inspected authorize/callbacks service set onto the generated auth
|
|
@@ -260,6 +262,12 @@ export function aggregateRequiredServices(
|
|
|
260
262
|
// per-workflow queue entries and falls back to shared queue names.
|
|
261
263
|
for (const [, graph] of Object.entries(state.workflows.graphMeta)) {
|
|
262
264
|
if (!graph.nodes || !graph.name) continue
|
|
265
|
+
// A scenario is a workflow, but it only ever runs in-process under
|
|
266
|
+
// `pikku scenario run` — no step of one is dispatched through a queue. The
|
|
267
|
+
// synthetic entries were pure leakage: they put a
|
|
268
|
+
// `wf-orchestrator-<scenario>` worker into the app's queue meta, which every
|
|
269
|
+
// bundle imports and a provider then creates as a real production queue.
|
|
270
|
+
if (graph.source === 'scenario') continue
|
|
263
271
|
|
|
264
272
|
const toKebab = (s: string) =>
|
|
265
273
|
s
|
|
@@ -696,6 +704,101 @@ export function validateAgentModels(
|
|
|
696
704
|
}
|
|
697
705
|
}
|
|
698
706
|
|
|
707
|
+
/**
|
|
708
|
+
* Scenarios are pure stories of remote RPCs (same rule as client-side CLI
|
|
709
|
+
* renderers): the func may only destructure logger/config — everything else
|
|
710
|
+
* must go through actor steps so the flow runs against the TARGET
|
|
711
|
+
* environment, never local services.
|
|
712
|
+
*
|
|
713
|
+
* This runs in post-processing rather than in `addWorkflow` because
|
|
714
|
+
* `addWorkflow` runs in the `visitSetup` pass, which completes before
|
|
715
|
+
* `addFunctions` (`visitFunctions`) has populated `state.functions.meta` — so
|
|
716
|
+
* the check read `undefined` there and never fired.
|
|
717
|
+
*/
|
|
718
|
+
export function validateScenarioServices(
|
|
719
|
+
logger: InspectorLogger,
|
|
720
|
+
state: InspectorState | Omit<InspectorState, 'typesLookup'>
|
|
721
|
+
): void {
|
|
722
|
+
for (const [workflowName, meta] of Object.entries(state.workflows.meta)) {
|
|
723
|
+
if (!meta.scenario) continue
|
|
724
|
+
const funcMeta = state.functions.meta[meta.pikkuFuncId]
|
|
725
|
+
if (!funcMeta?.services) continue
|
|
726
|
+
const disallowed = funcMeta.services.services.filter(
|
|
727
|
+
(svc) => svc !== 'logger' && svc !== 'config'
|
|
728
|
+
)
|
|
729
|
+
if (disallowed.length > 0) {
|
|
730
|
+
logger.critical(
|
|
731
|
+
ErrorCode.SCENARIO_HAS_SERVICES,
|
|
732
|
+
`Scenario '${workflowName}' destructures services: ${disallowed.join(', ')}. ` +
|
|
733
|
+
`Scenarios may only use 'logger'/'config' — drive everything else through ` +
|
|
734
|
+
`actor steps (workflow.do(step, rpc, data, { actor: actors.x })) so the flow ` +
|
|
735
|
+
`runs against the target environment.`
|
|
736
|
+
)
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Walk every scenario step, wherever it is nested, and validate the two things
|
|
743
|
+
* that can only be known once both the workflow meta and the function meta
|
|
744
|
+
* exist:
|
|
745
|
+
*
|
|
746
|
+
* - the step target must be a static string literal (PKU678) — otherwise it
|
|
747
|
+
* can't be bundled, typed or drawn;
|
|
748
|
+
* - a `browser: true` step must be given an actor (PKU677), since the browser
|
|
749
|
+
* context signs in as that persona.
|
|
750
|
+
*
|
|
751
|
+
* It also marks the step's function as used, so `scenario run` boots the
|
|
752
|
+
* singletons the step actually needs.
|
|
753
|
+
*/
|
|
754
|
+
export function validateScenarioSteps(
|
|
755
|
+
logger: InspectorLogger,
|
|
756
|
+
state: InspectorState | Omit<InspectorState, 'typesLookup'>
|
|
757
|
+
): void {
|
|
758
|
+
const visit = (workflowName: string, steps: WorkflowStepMeta[]): void => {
|
|
759
|
+
for (const step of steps) {
|
|
760
|
+
if (step.type === 'scenarioStep') {
|
|
761
|
+
if (step.stepFunc === DYNAMIC_SCENARIO_STEP_TARGET) {
|
|
762
|
+
logger.critical(
|
|
763
|
+
ErrorCode.SCENARIO_STEP_TARGET_NOT_STATIC,
|
|
764
|
+
`Scenario '${workflowName}' calls step '${step.stepName}' with a target that isn't a string literal. ` +
|
|
765
|
+
`A step target must be statically resolvable so it can be bundled, typed and drawn — ` +
|
|
766
|
+
`loop over data sets instead of computing the step name.`
|
|
767
|
+
)
|
|
768
|
+
continue
|
|
769
|
+
}
|
|
770
|
+
state.serviceAggregation.usedFunctions.add(step.stepFunc)
|
|
771
|
+
const stepMeta = state.functions.meta[step.stepFunc]
|
|
772
|
+
if (stepMeta?.scenarioStepBrowser && !step.actor) {
|
|
773
|
+
logger.critical(
|
|
774
|
+
ErrorCode.SCENARIO_BROWSER_STEP_NEEDS_ACTOR,
|
|
775
|
+
`Scenario '${workflowName}' calls browser step '${step.stepFunc}' without an actor. ` +
|
|
776
|
+
`Pass { actor: actors.<name> } so the browser context signs in as that persona.`
|
|
777
|
+
)
|
|
778
|
+
}
|
|
779
|
+
continue
|
|
780
|
+
}
|
|
781
|
+
if (step.type === 'branch') {
|
|
782
|
+
for (const branch of step.branches) visit(workflowName, branch.steps)
|
|
783
|
+
if (step.elseSteps) visit(workflowName, step.elseSteps)
|
|
784
|
+
} else if (step.type === 'switch') {
|
|
785
|
+
for (const c of step.cases ?? []) {
|
|
786
|
+
if (c.steps) visit(workflowName, c.steps)
|
|
787
|
+
}
|
|
788
|
+
if (step.defaultSteps) visit(workflowName, step.defaultSteps)
|
|
789
|
+
} else if (step.type === 'fanout' && step.body) {
|
|
790
|
+
visit(workflowName, step.body as WorkflowStepMeta[])
|
|
791
|
+
} else if (step.type === 'parallel' && step.children) {
|
|
792
|
+
visit(workflowName, step.children as WorkflowStepMeta[])
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
for (const [workflowName, meta] of Object.entries(state.workflows.meta)) {
|
|
798
|
+
visit(workflowName, meta.steps ?? [])
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
699
802
|
/**
|
|
700
803
|
* A `pikkuWorkflowGraph` node that references the `graph:` namespace (e.g.
|
|
701
804
|
* `graph:editFields`) needs @pikku/addon-graph wired — otherwise the RPC never
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { JSONValue } from '@pikku/core'
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
InspectorDiagnostic,
|
|
4
|
+
InspectorFeature,
|
|
5
|
+
InspectorState,
|
|
6
|
+
} from '../types.js'
|
|
3
7
|
import { TypesMap } from '../types-map.js'
|
|
4
8
|
|
|
5
9
|
/**
|
|
@@ -139,6 +143,7 @@ export interface SerializableInspectorState {
|
|
|
139
143
|
files: Array<[string, { path: string; exportedName: string }]>
|
|
140
144
|
graphMeta: InspectorState['workflows']['graphMeta']
|
|
141
145
|
graphFiles: Array<[string, { path: string; exportedName: string }]>
|
|
146
|
+
featureFiles: Array<[string, InspectorFeature]>
|
|
142
147
|
invokedWorkflows: string[]
|
|
143
148
|
}
|
|
144
149
|
rpc: {
|
|
@@ -362,6 +367,7 @@ export function serializeInspectorState(
|
|
|
362
367
|
files: Array.from(state.workflows.files.entries()),
|
|
363
368
|
graphMeta: state.workflows.graphMeta,
|
|
364
369
|
graphFiles: Array.from(state.workflows.graphFiles.entries()),
|
|
370
|
+
featureFiles: Array.from(state.workflows.featureFiles.entries()),
|
|
365
371
|
invokedWorkflows: Array.from(state.workflows.invokedWorkflows),
|
|
366
372
|
},
|
|
367
373
|
rpc: {
|
|
@@ -558,6 +564,7 @@ export function deserializeInspectorState(
|
|
|
558
564
|
files: new Map(data.workflows.files),
|
|
559
565
|
graphMeta: data.workflows.graphMeta || {},
|
|
560
566
|
graphFiles: new Map(data.workflows.graphFiles || []),
|
|
567
|
+
featureFiles: new Map(data.workflows.featureFiles || []),
|
|
561
568
|
invokedWorkflows: new Set(data.workflows.invokedWorkflows || []),
|
|
562
569
|
},
|
|
563
570
|
rpc: {
|