@pikku/inspector 0.12.74 → 0.12.81
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 +347 -0
- package/dist/add/add-analytics.d.ts +9 -0
- package/dist/add/add-analytics.js +178 -0
- package/dist/add/add-auth.js +0 -54
- package/dist/add/add-feature-flag.d.ts +2 -0
- package/dist/add/add-feature-flag.js +162 -0
- package/dist/add/add-wire-addon.js +1 -0
- package/dist/add/add-wire-remote-addon.js +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/inspector.js +4 -0
- package/dist/surface/collect-surface.d.ts +58 -0
- package/dist/surface/collect-surface.js +613 -0
- package/dist/types.d.ts +20 -11
- package/dist/utils/filter-inspector-state.js +23 -13
- package/dist/utils/post-process.js +15 -0
- package/dist/utils/read-module-specifiers.d.ts +8 -0
- package/dist/utils/read-module-specifiers.js +23 -0
- package/dist/utils/read-tsconfig-out-dir.d.ts +5 -0
- package/dist/utils/read-tsconfig-out-dir.js +12 -0
- package/dist/utils/resolve-deploy-target.d.ts +7 -0
- package/dist/utils/resolve-deploy-target.js +15 -7
- package/dist/utils/serialize-inspector-state.d.ts +5 -0
- package/dist/utils/serialize-inspector-state.js +8 -0
- package/dist/visit.js +4 -0
- package/package.json +16 -6
- package/run-tests.sh +21 -6
- package/src/add/add-addon-bans.ts +0 -85
- package/src/add/add-agent-scorer.test.ts +0 -197
- package/src/add/add-agent-scorer.ts +0 -83
- package/src/add/add-agent.test.ts +0 -87
- package/src/add/add-agent.ts +0 -638
- package/src/add/add-approval-description.ts +0 -76
- package/src/add/add-auth.test.ts +0 -791
- package/src/add/add-auth.ts +0 -424
- package/src/add/add-channel.ts +0 -778
- package/src/add/add-cli-renderers.test.ts +0 -74
- package/src/add/add-cli.ts +0 -1006
- package/src/add/add-credential.ts +0 -186
- package/src/add/add-feature.test.ts +0 -306
- package/src/add/add-feature.ts +0 -253
- package/src/add/add-file-extends-core-type.ts +0 -66
- package/src/add/add-file-with-config.ts +0 -45
- package/src/add/add-file-with-factory.test.ts +0 -134
- package/src/add/add-file-with-factory.ts +0 -288
- package/src/add/add-functions.test.ts +0 -743
- package/src/add/add-functions.ts +0 -1548
- package/src/add/add-gateway.ts +0 -143
- package/src/add/add-http-route-ref.test.ts +0 -149
- package/src/add/add-http-route.ts +0 -530
- package/src/add/add-http-routes.ts +0 -471
- package/src/add/add-keyed-wiring.ts +0 -180
- package/src/add/add-mcp-prompt.ts +0 -157
- package/src/add/add-mcp-resource.ts +0 -175
- package/src/add/add-middleware.test.ts +0 -196
- package/src/add/add-middleware.ts +0 -879
- package/src/add/add-permission-auth.test.ts +0 -59
- package/src/add/add-permission.ts +0 -311
- package/src/add/add-personas.test.ts +0 -406
- package/src/add/add-personas.ts +0 -287
- package/src/add/add-queue-worker.ts +0 -117
- package/src/add/add-rpc-invocations.test.ts +0 -287
- package/src/add/add-rpc-invocations.ts +0 -221
- package/src/add/add-scenario-step.test.ts +0 -509
- package/src/add/add-schedule.ts +0 -120
- package/src/add/add-scope.test.ts +0 -419
- package/src/add/add-scope.ts +0 -221
- package/src/add/add-secret.ts +0 -10
- package/src/add/add-subject-scenario-steps.test.ts +0 -161
- package/src/add/add-system-role.test.ts +0 -297
- package/src/add/add-system-role.ts +0 -162
- package/src/add/add-trigger.ts +0 -165
- package/src/add/add-variable.ts +0 -10
- package/src/add/add-wire-addon.test.ts +0 -185
- package/src/add/add-wire-addon.ts +0 -146
- package/src/add/add-wire-remote-addon.ts +0 -80
- package/src/add/add-workflow-edge-cases.test.ts +0 -201
- package/src/add/add-workflow-fanout.test.ts +0 -166
- package/src/add/add-workflow-graph-foreach.test.ts +0 -174
- package/src/add/add-workflow-graph-input.test.ts +0 -94
- package/src/add/add-workflow-graph-notes.test.ts +0 -74
- package/src/add/add-workflow-graph.ts +0 -644
- package/src/add/add-workflow-on-error.test.ts +0 -118
- package/src/add/add-workflow-return-switch.test.ts +0 -121
- package/src/add/add-workflow-silent-drops.test.ts +0 -261
- package/src/add/add-workflow-sleep-expression.test.ts +0 -96
- package/src/add/add-workflow.test.ts +0 -155
- package/src/add/add-workflow.ts +0 -482
- package/src/add/addon-bans.test.ts +0 -121
- package/src/add/addon-contracts.test.ts +0 -221
- package/src/add/allow-escape-hatches.test.ts +0 -144
- package/src/add/audit-meta.test.ts +0 -75
- package/src/add/classified-sink-check.test.ts +0 -347
- package/src/add/config-value-metadata.test.ts +0 -263
- package/src/add/dynamic-import-check.test.ts +0 -119
- package/src/add/expect-eventually-scenario-only.test.ts +0 -108
- package/src/add/function-body-span.test.ts +0 -96
- package/src/add/inline-wiring-function.test.ts +0 -104
- package/src/add/pii-check.test.ts +0 -258
- package/src/add/rpc-type-cast.test.ts +0 -123
- package/src/add/scenario-instrumentation.test.ts +0 -89
- package/src/add/scenario-instrumentation.ts +0 -42
- package/src/add/scenario-marker.test.ts +0 -89
- package/src/add/scenario-not-extractable.test.ts +0 -159
- package/src/add/scenario-services.test.ts +0 -103
- package/src/add/scenario-skip.test.ts +0 -83
- package/src/add/validate-workflow-graph-addons.test.ts +0 -102
- package/src/add/wire-name-literal.test.ts +0 -117
- package/src/error-codes-emitted.test.ts +0 -76
- package/src/error-codes.ts +0 -145
- package/src/index.ts +0 -66
- package/src/inspector.ts +0 -575
- package/src/schema-generator.ts +0 -1
- package/src/source-file-cache.ts +0 -73
- package/src/tsconfig-paths.test.ts +0 -90
- package/src/types-map.ts +0 -141
- package/src/types.ts +0 -775
- package/src/utils/annotate-http-route-auth.test.ts +0 -161
- package/src/utils/annotate-http-route-auth.ts +0 -53
- package/src/utils/check-classified-sinks.ts +0 -215
- package/src/utils/check-pii-output.ts +0 -120
- package/src/utils/compute-diagnostics.test.ts +0 -106
- package/src/utils/compute-required-schemas.ts +0 -48
- package/src/utils/contract-hashes.test.ts +0 -553
- package/src/utils/contract-hashes.ts +0 -387
- package/src/utils/custom-types-generator.test.ts +0 -124
- package/src/utils/custom-types-generator.ts +0 -150
- package/src/utils/detect-schema-vendor.test.ts +0 -123
- package/src/utils/detect-schema-vendor.ts +0 -120
- package/src/utils/does-type-extend-core-type.ts +0 -53
- package/src/utils/ensure-function-metadata.ts +0 -385
- package/src/utils/extract-function-name.ts +0 -612
- package/src/utils/extract-function-node.ts +0 -70
- package/src/utils/extract-node-value.test.ts +0 -117
- package/src/utils/extract-node-value.ts +0 -172
- package/src/utils/extract-secret-usage.test.ts +0 -81
- package/src/utils/extract-secret-usage.ts +0 -73
- package/src/utils/extract-services.ts +0 -70
- package/src/utils/extract-surface-usage.test.ts +0 -147
- package/src/utils/extract-surface-usage.ts +0 -121
- package/src/utils/filter-inspector-state.test.ts +0 -2207
- package/src/utils/filter-inspector-state.ts +0 -1302
- package/src/utils/filter-utils.test.ts +0 -843
- package/src/utils/filter-utils.ts +0 -161
- package/src/utils/find-root-dir.ts +0 -68
- package/src/utils/find-zod-transform.test.ts +0 -70
- package/src/utils/find-zod-transform.ts +0 -103
- package/src/utils/get-exported-variable-name.ts +0 -48
- package/src/utils/get-files-and-methods.ts +0 -157
- package/src/utils/get-property-value.test.ts +0 -141
- package/src/utils/get-property-value.ts +0 -331
- package/src/utils/hash.ts +0 -26
- package/src/utils/literal-properties.ts +0 -97
- package/src/utils/load-addon-functions-meta.test.ts +0 -157
- package/src/utils/load-addon-functions-meta.ts +0 -508
- package/src/utils/load-addon-scopes.test.ts +0 -138
- package/src/utils/middleware.test.ts +0 -204
- package/src/utils/middleware.ts +0 -303
- package/src/utils/nested-project-filter.test.ts +0 -53
- package/src/utils/nested-project-filter.ts +0 -28
- package/src/utils/permissions-shorthand.test.ts +0 -79
- package/src/utils/permissions.test.ts +0 -125
- package/src/utils/permissions.ts +0 -129
- package/src/utils/post-process.test.ts +0 -1094
- package/src/utils/post-process.ts +0 -1604
- package/src/utils/resolve-addon-package.ts +0 -106
- package/src/utils/resolve-core-type.test.ts +0 -142
- package/src/utils/resolve-core-type.ts +0 -35
- package/src/utils/resolve-deploy-target.test.ts +0 -135
- package/src/utils/resolve-deploy-target.ts +0 -65
- package/src/utils/resolve-function-meta.ts +0 -25
- package/src/utils/resolve-function-types.ts +0 -42
- package/src/utils/resolve-identifier.ts +0 -46
- package/src/utils/resolve-ref-contract.ts +0 -71
- package/src/utils/resolve-versions.test.ts +0 -499
- package/src/utils/resolve-versions.ts +0 -228
- package/src/utils/schema-cache.test.ts +0 -124
- package/src/utils/schema-generator.test.ts +0 -272
- package/src/utils/schema-generator.ts +0 -780
- package/src/utils/secret-alias-services.test.ts +0 -146
- package/src/utils/secret-alias-services.ts +0 -58
- package/src/utils/serialize-inspector-state.ts +0 -742
- package/src/utils/serialize-mcp-json.ts +0 -150
- package/src/utils/serialize-middleware-groups-meta.ts +0 -36
- package/src/utils/serialize-openapi-json.ts +0 -277
- package/src/utils/serialize-permissions-groups-meta.ts +0 -7
- package/src/utils/single-declaration.ts +0 -57
- package/src/utils/type-utils.ts +0 -145
- package/src/utils/validate-auth-sessionless.ts +0 -30
- package/src/utils/validate-exposed-functions-gated.test.ts +0 -230
- package/src/utils/validate-exposed-functions-gated.ts +0 -103
- package/src/utils/validate-tags-resolve-to-middleware.test.ts +0 -108
- package/src/utils/validate-tags-resolve-to-middleware.ts +0 -48
- package/src/utils/workflow/derive-workflow-plan.test.ts +0 -178
- package/src/utils/workflow/derive-workflow-plan.ts +0 -117
- package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +0 -1465
- package/src/utils/workflow/dsl/deserialize-fanout-body.test.ts +0 -71
- package/src/utils/workflow/dsl/deserialize-fanout-fidelity.test.ts +0 -99
- package/src/utils/workflow/dsl/deserialize-graph-next.test.ts +0 -161
- package/src/utils/workflow/dsl/deserialize-roundtrip-bindings.test.ts +0 -121
- package/src/utils/workflow/dsl/deserialize-roundtrip-values.test.ts +0 -116
- package/src/utils/workflow/dsl/deserialize-roundtrip.test.ts +0 -145
- package/src/utils/workflow/dsl/extract-approval-policy.test.ts +0 -71
- package/src/utils/workflow/dsl/extract-dsl-workflow.ts +0 -2254
- package/src/utils/workflow/dsl/index.ts +0 -11
- package/src/utils/workflow/dsl/patterns.ts +0 -440
- package/src/utils/workflow/dsl/validation.ts +0 -203
- package/src/utils/workflow/graph/convert-dsl-to-graph-skip.test.ts +0 -35
- package/src/utils/workflow/graph/convert-dsl-to-graph.ts +0 -543
- package/src/utils/workflow/graph/fanout-node-identity.test.ts +0 -105
- package/src/utils/workflow/graph/finalize-workflow-wires.ts +0 -310
- package/src/utils/workflow/graph/finalize-workflows.test.ts +0 -50
- package/src/utils/workflow/graph/finalize-workflows.ts +0 -130
- package/src/utils/workflow/graph/index.ts +0 -28
- package/src/utils/workflow/graph/serialize-workflow-graph.ts +0 -240
- package/src/utils/workflow/graph/workflow-graph.types.ts +0 -281
- package/src/visit.ts +0 -181
- package/tsconfig.json +0 -19
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -1,155 +0,0 @@
|
|
|
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 } from '../types.js'
|
|
8
|
-
|
|
9
|
-
function makeLogger(
|
|
10
|
-
criticals: Array<{ code: string; message: string }>
|
|
11
|
-
): InspectorLogger {
|
|
12
|
-
return {
|
|
13
|
-
debug: () => {},
|
|
14
|
-
info: () => {},
|
|
15
|
-
warn: () => {},
|
|
16
|
-
error: () => {},
|
|
17
|
-
diagnostic: ({ code, message }) => {
|
|
18
|
-
criticals.push({ code, message })
|
|
19
|
-
},
|
|
20
|
-
critical: (code: any, message: string) => {
|
|
21
|
-
criticals.push({ code, message })
|
|
22
|
-
},
|
|
23
|
-
hasCriticalErrors: () => criticals.length > 0,
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
describe('addWorkflow — workflow.do RPC detection', () => {
|
|
28
|
-
test('detects RPC step when result is assigned to a const', async () => {
|
|
29
|
-
const rootDir = await mkdtemp(join(tmpdir(), 'pikku-workflow-'))
|
|
30
|
-
const wfFile = join(rootDir, 'my.workflow.ts')
|
|
31
|
-
const stepFile = join(rootDir, 'my.steps.ts')
|
|
32
|
-
|
|
33
|
-
await writeFile(
|
|
34
|
-
stepFile,
|
|
35
|
-
[
|
|
36
|
-
"import { pikkuSessionlessFunc } from '@pikku/core'",
|
|
37
|
-
'export const doThing = pikkuSessionlessFunc({',
|
|
38
|
-
' func: async ({ logger }) => ({ ok: true }),',
|
|
39
|
-
'})',
|
|
40
|
-
].join('\n')
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
await writeFile(
|
|
44
|
-
wfFile,
|
|
45
|
-
[
|
|
46
|
-
"import { pikkuWorkflowFunc } from '@pikku/core/workflow'",
|
|
47
|
-
'export const myWorkflow = pikkuWorkflowFunc(async (_, _input, { workflow }) => {',
|
|
48
|
-
" const result = await workflow.do('Do thing', 'doThing', {})",
|
|
49
|
-
' return { id: result.ok }',
|
|
50
|
-
'})',
|
|
51
|
-
].join('\n')
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
const criticals: Array<{ code: string; message: string }> = []
|
|
55
|
-
try {
|
|
56
|
-
const state = await inspect(makeLogger(criticals), [stepFile, wfFile], {
|
|
57
|
-
rootDir,
|
|
58
|
-
})
|
|
59
|
-
assert.ok(
|
|
60
|
-
state.rpc.invokedFunctions.has('doThing'),
|
|
61
|
-
'doThing should be in invokedFunctions'
|
|
62
|
-
)
|
|
63
|
-
assert.ok(
|
|
64
|
-
state.rpc.internalFiles.has('doThing'),
|
|
65
|
-
'doThing should be in internalFiles'
|
|
66
|
-
)
|
|
67
|
-
} finally {
|
|
68
|
-
await rm(rootDir, { recursive: true, force: true })
|
|
69
|
-
}
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
test('detects RPC step when result is reassigned to a pre-declared null variable', async () => {
|
|
73
|
-
// Regression: `let x = null; x = await workflow.do(...)` was treated as a
|
|
74
|
-
// set-step instead of an RPC step, so the referenced function was never registered.
|
|
75
|
-
const rootDir = await mkdtemp(join(tmpdir(), 'pikku-workflow-reassign-'))
|
|
76
|
-
const wfFile = join(rootDir, 'project.workflow.ts')
|
|
77
|
-
const stepFile = join(rootDir, 'project.steps.ts')
|
|
78
|
-
|
|
79
|
-
await writeFile(
|
|
80
|
-
stepFile,
|
|
81
|
-
[
|
|
82
|
-
"import { pikkuSessionlessFunc } from '@pikku/core'",
|
|
83
|
-
'export const launchSandbox = pikkuSessionlessFunc({',
|
|
84
|
-
' func: async ({ logger }) => ({ sandboxId: "abc" }),',
|
|
85
|
-
'})',
|
|
86
|
-
].join('\n')
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
await writeFile(
|
|
90
|
-
wfFile,
|
|
91
|
-
[
|
|
92
|
-
"import { pikkuWorkflowFunc } from '@pikku/core/workflow'",
|
|
93
|
-
'export const createProjectWorkflow = pikkuWorkflowFunc(async (_, input, { workflow }) => {',
|
|
94
|
-
' let launched: { sandboxId: string } | null = null',
|
|
95
|
-
' if (input.createSandbox) {',
|
|
96
|
-
" launched = await workflow.do('Launch sandbox', 'launchSandbox', { projectId: input.projectId })",
|
|
97
|
-
' }',
|
|
98
|
-
' return { sandboxId: launched?.sandboxId ?? null }',
|
|
99
|
-
'})',
|
|
100
|
-
].join('\n')
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
const criticals: Array<{ code: string; message: string }> = []
|
|
104
|
-
try {
|
|
105
|
-
const state = await inspect(makeLogger(criticals), [stepFile, wfFile], {
|
|
106
|
-
rootDir,
|
|
107
|
-
})
|
|
108
|
-
assert.ok(
|
|
109
|
-
state.rpc.invokedFunctions.has('launchSandbox'),
|
|
110
|
-
'launchSandbox should be in invokedFunctions even when assigned to a pre-declared null variable'
|
|
111
|
-
)
|
|
112
|
-
assert.ok(
|
|
113
|
-
state.rpc.internalFiles.has('launchSandbox'),
|
|
114
|
-
'launchSandbox should be in internalFiles so it gets registered in pikku-functions.gen.ts'
|
|
115
|
-
)
|
|
116
|
-
} finally {
|
|
117
|
-
await rm(rootDir, { recursive: true, force: true })
|
|
118
|
-
}
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
test('still treats plain reassignment to context var as a set step', async () => {
|
|
122
|
-
const rootDir = await mkdtemp(join(tmpdir(), 'pikku-workflow-set-'))
|
|
123
|
-
const wfFile = join(rootDir, 'set.workflow.ts')
|
|
124
|
-
|
|
125
|
-
await writeFile(
|
|
126
|
-
wfFile,
|
|
127
|
-
[
|
|
128
|
-
"import { pikkuWorkflowFunc } from '@pikku/core/workflow'",
|
|
129
|
-
'export const setWorkflow = pikkuWorkflowFunc(async (_, input, { workflow }) => {',
|
|
130
|
-
' let status = "pending"',
|
|
131
|
-
' status = "done"',
|
|
132
|
-
" await workflow.do('No-op', 'noopStep', {})",
|
|
133
|
-
' return { status }',
|
|
134
|
-
'})',
|
|
135
|
-
].join('\n')
|
|
136
|
-
)
|
|
137
|
-
|
|
138
|
-
const criticals: Array<{ code: string; message: string }> = []
|
|
139
|
-
try {
|
|
140
|
-
const state = await inspect(makeLogger(criticals), [wfFile], { rootDir })
|
|
141
|
-
const meta = state.workflows.meta['setWorkflow']
|
|
142
|
-
assert.ok(meta, 'workflow should be registered')
|
|
143
|
-
const steps = meta.steps ?? []
|
|
144
|
-
const setStep = steps.find(
|
|
145
|
-
(s: any) => s.type === 'set' && s.variable === 'status'
|
|
146
|
-
)
|
|
147
|
-
assert.ok(
|
|
148
|
-
setStep,
|
|
149
|
-
'plain string reassignment should still produce a set step'
|
|
150
|
-
)
|
|
151
|
-
} finally {
|
|
152
|
-
await rm(rootDir, { recursive: true, force: true })
|
|
153
|
-
}
|
|
154
|
-
})
|
|
155
|
-
})
|
package/src/add/add-workflow.ts
DELETED
|
@@ -1,482 +0,0 @@
|
|
|
1
|
-
import * as ts from 'typescript'
|
|
2
|
-
import type { AddWiring, InspectorState } from '../types.js'
|
|
3
|
-
import { extractFunctionName } from '../utils/extract-function-name.js'
|
|
4
|
-
import { extractFunctionNode } from '../utils/extract-function-node.js'
|
|
5
|
-
import { ErrorCode } from '../error-codes.js'
|
|
6
|
-
import type { WorkflowStepMeta, WorkflowContext } from '@pikku/core/workflow'
|
|
7
|
-
import {
|
|
8
|
-
extractStringLiteral,
|
|
9
|
-
isStringLike,
|
|
10
|
-
isFunctionLike,
|
|
11
|
-
extractDescription,
|
|
12
|
-
extractDuration,
|
|
13
|
-
} from '../utils/extract-node-value.js'
|
|
14
|
-
import {
|
|
15
|
-
getCommonWireMetaData,
|
|
16
|
-
getPropertyValue,
|
|
17
|
-
} from '../utils/get-property-value.js'
|
|
18
|
-
import { extractDSLWorkflow } from '../utils/workflow/dsl/extract-dsl-workflow.js'
|
|
19
|
-
import {
|
|
20
|
-
getSourceText,
|
|
21
|
-
extractActorFromOptions,
|
|
22
|
-
} from '../utils/workflow/dsl/patterns.js'
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Extract a workflow step's display name without letting a non-static name
|
|
26
|
-
* (e.g. a function call) abort the scan. The step name is cosmetic, so a
|
|
27
|
-
* resolution failure must never prevent the RPC from being registered.
|
|
28
|
-
*/
|
|
29
|
-
function extractStepName(node: ts.Node, checker: ts.TypeChecker): string {
|
|
30
|
-
try {
|
|
31
|
-
return extractStringLiteral(node, checker)
|
|
32
|
-
} catch {
|
|
33
|
-
return getSourceText(node)
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Walk a function body for any `X.expectEventually(...)` call. Used to enforce
|
|
39
|
-
* that the durable-polling assertion is a scenario-only primitive — regular
|
|
40
|
-
* workflows must not carry test/assertion semantics.
|
|
41
|
-
*/
|
|
42
|
-
function containsExpectEventuallyCall(node: ts.Node): boolean {
|
|
43
|
-
if (
|
|
44
|
-
ts.isCallExpression(node) &&
|
|
45
|
-
ts.isPropertyAccessExpression(node.expression) &&
|
|
46
|
-
node.expression.name.text === 'expectEventually'
|
|
47
|
-
) {
|
|
48
|
-
return true
|
|
49
|
-
}
|
|
50
|
-
let found = false
|
|
51
|
-
ts.forEachChild(node, (child) => {
|
|
52
|
-
if (!found && !ts.isFunctionDeclaration(child)) {
|
|
53
|
-
found = containsExpectEventuallyCall(child)
|
|
54
|
-
}
|
|
55
|
-
})
|
|
56
|
-
return found
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Recursively check if any step has inline type (non-serializable)
|
|
61
|
-
*/
|
|
62
|
-
function hasInlineSteps(steps: WorkflowStepMeta[]): boolean {
|
|
63
|
-
for (const step of steps) {
|
|
64
|
-
if (step.type === 'inline') {
|
|
65
|
-
return true
|
|
66
|
-
} else if (step.type === 'branch') {
|
|
67
|
-
for (const branch of step.branches) {
|
|
68
|
-
if (hasInlineSteps(branch.steps)) return true
|
|
69
|
-
}
|
|
70
|
-
if (step.elseSteps && hasInlineSteps(step.elseSteps)) return true
|
|
71
|
-
} else if (step.type === 'switch' && step.cases) {
|
|
72
|
-
for (const c of step.cases) {
|
|
73
|
-
if (c.steps && hasInlineSteps(c.steps)) return true
|
|
74
|
-
}
|
|
75
|
-
if (step.defaultSteps && hasInlineSteps(step.defaultSteps)) return true
|
|
76
|
-
} else if (step.type === 'fanout' && step.body) {
|
|
77
|
-
if (hasInlineSteps(step.body)) return true
|
|
78
|
-
} else if (step.type === 'parallel' && step.children) {
|
|
79
|
-
if (hasInlineSteps(step.children)) return true
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
return false
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Recursively collect all RPC names from workflow steps
|
|
87
|
-
*/
|
|
88
|
-
export function collectInvokedRPCs(
|
|
89
|
-
steps: WorkflowStepMeta[],
|
|
90
|
-
rpcs: Set<string>
|
|
91
|
-
): void {
|
|
92
|
-
for (const step of steps) {
|
|
93
|
-
if (step.type === 'rpc' && step.rpcName) {
|
|
94
|
-
rpcs.add(step.rpcName)
|
|
95
|
-
// The compensation handler is invoked at runtime too, so it has to be
|
|
96
|
-
// wired like any other step.
|
|
97
|
-
if (step.options?.onError) {
|
|
98
|
-
rpcs.add(step.options.onError)
|
|
99
|
-
}
|
|
100
|
-
} else if (step.type === 'scenarioStep' && step.stepFunc) {
|
|
101
|
-
// Steps are bundled like RPCs — the runner calls them through
|
|
102
|
-
// runPikkuFunc — but they are deliberately never RPC-registered.
|
|
103
|
-
rpcs.add(step.stepFunc)
|
|
104
|
-
} else if (step.type === 'branch') {
|
|
105
|
-
for (const branch of step.branches) {
|
|
106
|
-
collectInvokedRPCs(branch.steps, rpcs)
|
|
107
|
-
}
|
|
108
|
-
if (step.elseSteps) collectInvokedRPCs(step.elseSteps, rpcs)
|
|
109
|
-
} else if (step.type === 'switch' && step.cases) {
|
|
110
|
-
for (const c of step.cases) {
|
|
111
|
-
if (c.steps) collectInvokedRPCs(c.steps, rpcs)
|
|
112
|
-
}
|
|
113
|
-
if (step.defaultSteps) collectInvokedRPCs(step.defaultSteps, rpcs)
|
|
114
|
-
} else if (step.type === 'fanout' && step.body) {
|
|
115
|
-
collectInvokedRPCs(step.body as WorkflowStepMeta[], rpcs)
|
|
116
|
-
} else if (step.type === 'parallel' && step.children) {
|
|
117
|
-
collectInvokedRPCs(step.children as WorkflowStepMeta[], rpcs)
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Scan for workflow.do(), workflow.expectEventually(), workflow.sleep(), and
|
|
124
|
-
* workflow.cancel() calls to extract workflow steps
|
|
125
|
-
*/
|
|
126
|
-
function getWorkflowInvocations(
|
|
127
|
-
node: ts.Node,
|
|
128
|
-
checker: ts.TypeChecker,
|
|
129
|
-
state: InspectorState,
|
|
130
|
-
workflowName: string,
|
|
131
|
-
steps: WorkflowStepMeta[]
|
|
132
|
-
) {
|
|
133
|
-
// Look for property access expressions: workflow.do or workflow.sleep
|
|
134
|
-
if (ts.isPropertyAccessExpression(node)) {
|
|
135
|
-
const { name } = node
|
|
136
|
-
|
|
137
|
-
// Check if this is accessing 'do' or 'sleep' property
|
|
138
|
-
if (
|
|
139
|
-
name.text === 'do' ||
|
|
140
|
-
name.text === 'sleep' ||
|
|
141
|
-
name.text === 'cancel' ||
|
|
142
|
-
name.text === 'expectEventually'
|
|
143
|
-
) {
|
|
144
|
-
// Check if the parent is a call expression
|
|
145
|
-
const parent = node.parent
|
|
146
|
-
if (ts.isCallExpression(parent) && parent.expression === node) {
|
|
147
|
-
const args = parent.arguments
|
|
148
|
-
|
|
149
|
-
if (name.text === 'do' && args.length >= 2) {
|
|
150
|
-
// workflow.do(stepName, rpcName|fn, data?, options?)
|
|
151
|
-
const stepNameArg = args[0]
|
|
152
|
-
const secondArg = args[1]
|
|
153
|
-
const optionsArg =
|
|
154
|
-
args.length >= 4 ? args[args.length - 1] : undefined
|
|
155
|
-
|
|
156
|
-
const stepName = extractStepName(stepNameArg, checker)
|
|
157
|
-
const description =
|
|
158
|
-
extractDescription(optionsArg, checker) ?? undefined
|
|
159
|
-
|
|
160
|
-
// Determine form by checking 2nd argument type
|
|
161
|
-
if (isStringLike(secondArg, checker)) {
|
|
162
|
-
// RPC form: workflow.do(stepName, rpcName, data, options?)
|
|
163
|
-
const rpcName = extractStringLiteral(secondArg, checker)
|
|
164
|
-
steps.push({
|
|
165
|
-
type: 'rpc',
|
|
166
|
-
stepName,
|
|
167
|
-
rpcName,
|
|
168
|
-
actor: extractActorFromOptions(optionsArg),
|
|
169
|
-
})
|
|
170
|
-
state.rpc.invokedFunctions.add(rpcName)
|
|
171
|
-
} else if (isFunctionLike(secondArg)) {
|
|
172
|
-
// Inline form: workflow.do(stepName, fn, options?)
|
|
173
|
-
steps.push({
|
|
174
|
-
type: 'inline',
|
|
175
|
-
stepName: stepName || '<dynamic>',
|
|
176
|
-
description: description || '<dynamic>',
|
|
177
|
-
})
|
|
178
|
-
}
|
|
179
|
-
} else if (name.text === 'expectEventually' && args.length >= 4) {
|
|
180
|
-
// workflow.expectEventually(stepName, rpcName, data, predicate, options?)
|
|
181
|
-
const stepName = extractStepName(args[0], checker)
|
|
182
|
-
if (isStringLike(args[1], checker)) {
|
|
183
|
-
const rpcName = extractStringLiteral(args[1], checker)
|
|
184
|
-
steps.push({
|
|
185
|
-
type: 'rpc',
|
|
186
|
-
stepName,
|
|
187
|
-
rpcName,
|
|
188
|
-
expectEventually: true,
|
|
189
|
-
actor: extractActorFromOptions(
|
|
190
|
-
args.length >= 5 ? args[4] : undefined
|
|
191
|
-
),
|
|
192
|
-
})
|
|
193
|
-
state.rpc.invokedFunctions.add(rpcName)
|
|
194
|
-
}
|
|
195
|
-
} else if (name.text === 'sleep' && args.length >= 2) {
|
|
196
|
-
// workflow.sleep(stepName, duration)
|
|
197
|
-
const stepNameArg = args[0]
|
|
198
|
-
const durationArg = args[1]
|
|
199
|
-
|
|
200
|
-
const stepName = extractStepName(stepNameArg, checker)
|
|
201
|
-
const duration = extractDuration(durationArg, checker)
|
|
202
|
-
|
|
203
|
-
steps.push({
|
|
204
|
-
type: 'sleep',
|
|
205
|
-
stepName: stepName || '<dynamic>',
|
|
206
|
-
duration: duration || '<dynamic>',
|
|
207
|
-
})
|
|
208
|
-
} else if (name.text === 'cancel') {
|
|
209
|
-
// workflow.cancel(reason?)
|
|
210
|
-
steps.push({
|
|
211
|
-
type: 'cancel',
|
|
212
|
-
})
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Recurse into children, including arrow functions (for Promise.all callbacks)
|
|
219
|
-
// but skip function declarations (which would be separate functions)
|
|
220
|
-
ts.forEachChild(node, (child) => {
|
|
221
|
-
if (ts.isFunctionDeclaration(child)) {
|
|
222
|
-
return
|
|
223
|
-
}
|
|
224
|
-
getWorkflowInvocations(child, checker, state, workflowName, steps)
|
|
225
|
-
})
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Inspector for pikkuWorkflowFunc(), pikkuWorkflowComplexFunc() and
|
|
230
|
-
* pikkuScenario() calls. Detects workflow registration and extracts metadata.
|
|
231
|
-
*/
|
|
232
|
-
export const addWorkflow: AddWiring = (
|
|
233
|
-
logger,
|
|
234
|
-
node,
|
|
235
|
-
checker,
|
|
236
|
-
state,
|
|
237
|
-
options
|
|
238
|
-
) => {
|
|
239
|
-
if (!ts.isCallExpression(node)) {
|
|
240
|
-
return
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const args = node.arguments
|
|
244
|
-
const firstArg = args[0]
|
|
245
|
-
const expression = node.expression
|
|
246
|
-
|
|
247
|
-
if (!ts.isIdentifier(expression)) {
|
|
248
|
-
return
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
let wrapperType: 'dsl' | 'complex' | 'scenario' | null = null
|
|
252
|
-
if (expression.text === 'pikkuWorkflowFunc') {
|
|
253
|
-
wrapperType = 'dsl'
|
|
254
|
-
} else if (expression.text === 'pikkuWorkflowComplexFunc') {
|
|
255
|
-
if (!options.allow?.complexWorkflows) {
|
|
256
|
-
logger.critical(
|
|
257
|
-
ErrorCode.COMPLEX_WORKFLOW_NOT_ALLOWED,
|
|
258
|
-
`'${expression.text}' is used here, but this project has not enabled complex workflows. ` +
|
|
259
|
-
`Their inline steps cannot be serialized, so they are absent from the workflow graph and ` +
|
|
260
|
-
`cannot be replayed or migrated across a definition change. Express the workflow with ` +
|
|
261
|
-
`pikkuWorkflowFunc and the step DSL, or set \`allow: { complexWorkflows: true }\` in ` +
|
|
262
|
-
`pikku.config.json if the project accepts that trade.`
|
|
263
|
-
)
|
|
264
|
-
return
|
|
265
|
-
}
|
|
266
|
-
wrapperType = 'complex'
|
|
267
|
-
} else if (expression.text === 'pikkuScenario') {
|
|
268
|
-
// A scenario is a complex workflow whose steps run as actors over the
|
|
269
|
-
// real transport — same extraction rules as complex, distinct meta.
|
|
270
|
-
wrapperType = 'scenario'
|
|
271
|
-
} else {
|
|
272
|
-
return
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
if (!firstArg) {
|
|
276
|
-
return
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Extract workflow name and metadata using same logic as add-functions
|
|
280
|
-
const { pikkuFuncId, name, exportedName } = extractFunctionName(
|
|
281
|
-
node,
|
|
282
|
-
checker,
|
|
283
|
-
state.rootDir
|
|
284
|
-
)
|
|
285
|
-
|
|
286
|
-
const workflowName = exportedName || name
|
|
287
|
-
|
|
288
|
-
if (!workflowName) {
|
|
289
|
-
logger.critical(
|
|
290
|
-
ErrorCode.MISSING_NAME,
|
|
291
|
-
`Could not determine workflow name from export.`
|
|
292
|
-
)
|
|
293
|
-
return
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// Extract the function node (either direct function or from config.func)
|
|
297
|
-
const { funcNode, resolvedFunc } = extractFunctionNode(firstArg, checker)
|
|
298
|
-
|
|
299
|
-
// Extract metadata if using object form
|
|
300
|
-
let tags: string[] | undefined
|
|
301
|
-
let title: string | undefined
|
|
302
|
-
let summary: string | undefined
|
|
303
|
-
let description: string | undefined
|
|
304
|
-
let errors: string[] | undefined
|
|
305
|
-
let expose: boolean | undefined
|
|
306
|
-
let skip: string | undefined
|
|
307
|
-
|
|
308
|
-
if (ts.isObjectLiteralExpression(firstArg)) {
|
|
309
|
-
const metadata = getCommonWireMetaData(
|
|
310
|
-
firstArg,
|
|
311
|
-
'Workflow',
|
|
312
|
-
workflowName,
|
|
313
|
-
logger,
|
|
314
|
-
checker
|
|
315
|
-
)
|
|
316
|
-
if (metadata.disabled) return
|
|
317
|
-
tags = metadata.tags
|
|
318
|
-
title = metadata.title
|
|
319
|
-
summary = metadata.summary
|
|
320
|
-
description = metadata.description
|
|
321
|
-
errors = metadata.errors
|
|
322
|
-
|
|
323
|
-
expose = getPropertyValue(firstArg, 'expose') as boolean | undefined
|
|
324
|
-
skip = (getPropertyValue(firstArg, 'skip') as string | null) ?? undefined
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// Validate that we got a valid function
|
|
328
|
-
if (
|
|
329
|
-
ts.isObjectLiteralExpression(firstArg) &&
|
|
330
|
-
(!funcNode || funcNode === firstArg)
|
|
331
|
-
) {
|
|
332
|
-
logger.critical(
|
|
333
|
-
ErrorCode.MISSING_FUNC,
|
|
334
|
-
`No valid 'func' property for workflow '${workflowName}'.`
|
|
335
|
-
)
|
|
336
|
-
return
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
if (!resolvedFunc) {
|
|
340
|
-
logger.critical(
|
|
341
|
-
ErrorCode.MISSING_FUNC,
|
|
342
|
-
`Could not resolve workflow function for '${workflowName}'.`
|
|
343
|
-
)
|
|
344
|
-
return
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
if (
|
|
348
|
-
wrapperType !== 'scenario' &&
|
|
349
|
-
containsExpectEventuallyCall(resolvedFunc)
|
|
350
|
-
) {
|
|
351
|
-
logger.critical(
|
|
352
|
-
ErrorCode.EXPECT_EVENTUALLY_SCENARIO_ONLY,
|
|
353
|
-
`Workflow '${workflowName}' calls workflow.expectEventually(), which is only ` +
|
|
354
|
-
`available in scenarios (pikkuScenario). Move it into a scenario, or drive ` +
|
|
355
|
-
`the assertion outside the workflow.`
|
|
356
|
-
)
|
|
357
|
-
return
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// Track workflow file for wiring generation
|
|
361
|
-
if (exportedName) {
|
|
362
|
-
state.workflows.files.set(pikkuFuncId, {
|
|
363
|
-
path: node.getSourceFile().fileName,
|
|
364
|
-
exportedName,
|
|
365
|
-
})
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
let steps: WorkflowStepMeta[] = []
|
|
369
|
-
let context: WorkflowContext | undefined = undefined
|
|
370
|
-
let dsl: boolean | undefined = undefined
|
|
371
|
-
let asserts: boolean | undefined = undefined
|
|
372
|
-
|
|
373
|
-
// Try DSL workflow extraction first
|
|
374
|
-
// Pass the whole CallExpression node so findWorkflowFunction can find the arrow function
|
|
375
|
-
const result = extractDSLWorkflow(node, checker, {
|
|
376
|
-
allowInline: wrapperType !== 'dsl',
|
|
377
|
-
})
|
|
378
|
-
|
|
379
|
-
if (result.status === 'ok' && result.steps) {
|
|
380
|
-
// Extraction succeeded
|
|
381
|
-
steps = result.steps
|
|
382
|
-
context = result.context
|
|
383
|
-
asserts = result.asserts
|
|
384
|
-
|
|
385
|
-
// Check if workflow contains inline steps (non-serializable)
|
|
386
|
-
if (hasInlineSteps(steps)) {
|
|
387
|
-
if (wrapperType === 'dsl') {
|
|
388
|
-
// pikkuWorkflowFunc should not have inline steps
|
|
389
|
-
logger.critical(
|
|
390
|
-
ErrorCode.INVALID_DSL_WORKFLOW,
|
|
391
|
-
`Workflow '${workflowName}' uses pikkuWorkflowFunc but contains inline steps which are not allowed in DSL workflows. Use pikkuWorkflowComplexFunc instead.`
|
|
392
|
-
)
|
|
393
|
-
return
|
|
394
|
-
}
|
|
395
|
-
// pikkuWorkflowComplexFunc with inline steps is marked as non-dsl
|
|
396
|
-
dsl = false
|
|
397
|
-
} else {
|
|
398
|
-
// pikkuWorkflowComplexFunc is always non-dsl, pikkuWorkflowFunc is dsl
|
|
399
|
-
dsl = wrapperType === 'dsl'
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// Collect all invoked RPCs from workflow steps
|
|
403
|
-
const rpcs = new Set<string>()
|
|
404
|
-
collectInvokedRPCs(steps, rpcs)
|
|
405
|
-
for (const rpc of rpcs) {
|
|
406
|
-
state.rpc.invokedFunctions.add(rpc)
|
|
407
|
-
}
|
|
408
|
-
} else {
|
|
409
|
-
// DSL extraction failed
|
|
410
|
-
if (wrapperType === 'dsl') {
|
|
411
|
-
// For pikkuWorkflowFunc, this is a critical error
|
|
412
|
-
// But still track RPC invocations for function registration
|
|
413
|
-
getWorkflowInvocations(resolvedFunc, checker, state, workflowName, steps)
|
|
414
|
-
logger.critical(
|
|
415
|
-
ErrorCode.INVALID_DSL_WORKFLOW,
|
|
416
|
-
`Workflow '${workflowName}' does not conform to DSL workflow rules:\n${result.reason || 'Unknown error'}`
|
|
417
|
-
)
|
|
418
|
-
return
|
|
419
|
-
} else if (wrapperType === 'scenario') {
|
|
420
|
-
// The fallback below understands do/sleep but not step/given/when/then,
|
|
421
|
-
// so a scenario that lands here registers with zero steps. Silence would
|
|
422
|
-
// leave the whole scenario passing vacuously.
|
|
423
|
-
logger.critical(
|
|
424
|
-
ErrorCode.SCENARIO_NOT_EXTRACTABLE,
|
|
425
|
-
`Scenario '${workflowName}' could not be extracted, so none of its steps were recorded and it would run as an empty scenario:\n${result.reason || 'Unknown error'}`
|
|
426
|
-
)
|
|
427
|
-
return
|
|
428
|
-
} else {
|
|
429
|
-
// For pikkuWorkflowComplexFunc, fall back to basic extraction
|
|
430
|
-
logger.debug(
|
|
431
|
-
`Workflow '${workflowName}' could not be extracted as DSL workflow: ${result.reason || 'Unknown error'}. Falling back to basic extraction.`
|
|
432
|
-
)
|
|
433
|
-
dsl = false
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
// For pikkuWorkflowComplexFunc, also run basic extraction so RPCs in
|
|
438
|
-
// patterns the DSL extractor doesn't handle (array+push, nested Promise.all
|
|
439
|
-
// with identifier args, etc.) are still registered as invoked functions.
|
|
440
|
-
// When DSL extraction already produced steps this pass is registration-only:
|
|
441
|
-
// appending to `steps` would duplicate nodes and clobber the graph.
|
|
442
|
-
if (wrapperType !== 'dsl') {
|
|
443
|
-
const sink: WorkflowStepMeta[] = steps.length > 0 ? [] : steps
|
|
444
|
-
getWorkflowInvocations(resolvedFunc, checker, state, workflowName, sink)
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// Actor names the flow's steps run as (scenarios) — powers the console
|
|
448
|
-
// personas view and the Scenario graph badges.
|
|
449
|
-
const actorNames = [
|
|
450
|
-
...new Set(
|
|
451
|
-
steps
|
|
452
|
-
.map((s) => ('actor' in s ? s.actor : undefined))
|
|
453
|
-
.filter((a): a is string => typeof a === 'string')
|
|
454
|
-
),
|
|
455
|
-
]
|
|
456
|
-
|
|
457
|
-
state.workflows.meta[workflowName] = {
|
|
458
|
-
pikkuFuncId,
|
|
459
|
-
name: workflowName,
|
|
460
|
-
steps,
|
|
461
|
-
context,
|
|
462
|
-
dsl,
|
|
463
|
-
title,
|
|
464
|
-
summary,
|
|
465
|
-
description,
|
|
466
|
-
errors,
|
|
467
|
-
tags,
|
|
468
|
-
expose,
|
|
469
|
-
skip,
|
|
470
|
-
scenario: wrapperType === 'scenario' ? true : undefined,
|
|
471
|
-
asserts,
|
|
472
|
-
actors: actorNames.length > 0 ? actorNames : undefined,
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// The scenario "logger/config only" rule (PKU673) and the implicit
|
|
476
|
-
// workflow platform-service injection both used to live here, reading
|
|
477
|
-
// state.functions.meta[pikkuFuncId]. addWorkflow runs in the visitSetup
|
|
478
|
-
// pass, which completes before addFunctions populates that meta, so both
|
|
479
|
-
// were dead. The rule now lives in validateScenarioServices (post-process),
|
|
480
|
-
// and aggregateRequiredServices already adds the platform services globally
|
|
481
|
-
// whenever any workflow exists.
|
|
482
|
-
}
|