@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,509 +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
|
-
import type { WorkflowStepMeta } from '@pikku/core/workflow'
|
|
9
|
-
import type { ScenarioStepMeta } from '@pikku/core/scenario'
|
|
10
|
-
|
|
11
|
-
function makeLogger(
|
|
12
|
-
criticals: Array<{ code: string; message: string }>
|
|
13
|
-
): InspectorLogger {
|
|
14
|
-
return {
|
|
15
|
-
debug: () => {},
|
|
16
|
-
info: () => {},
|
|
17
|
-
warn: () => {},
|
|
18
|
-
error: () => {},
|
|
19
|
-
diagnostic: ({ code, message }: any) => {
|
|
20
|
-
criticals.push({ code, message })
|
|
21
|
-
},
|
|
22
|
-
critical: (code: any, message: string) => {
|
|
23
|
-
criticals.push({ code, message })
|
|
24
|
-
},
|
|
25
|
-
hasCriticalErrors: () => criticals.length > 0,
|
|
26
|
-
} as InspectorLogger
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const STEPS = [
|
|
30
|
-
"import { pikkuScenarioStep } from '@pikku/core'",
|
|
31
|
-
'export const buysAnApple = pikkuScenarioStep({',
|
|
32
|
-
" name: 'buysAnApple',",
|
|
33
|
-
" description: 'buys an apple',",
|
|
34
|
-
' browser: async ({ logger }, data: { qty: number }) => ({ ok: data.qty > 0 }),',
|
|
35
|
-
' default: async ({ logger }, data: { qty: number }) => ({ ok: data.qty > 0 }),',
|
|
36
|
-
'})',
|
|
37
|
-
'export const seesAReceipt = pikkuScenarioStep({',
|
|
38
|
-
" name: 'seesAReceipt',",
|
|
39
|
-
" description: 'sees a receipt',",
|
|
40
|
-
' default: async ({ logger }) => ({ ok: true }),',
|
|
41
|
-
'})',
|
|
42
|
-
'export const asksForTheReceipt = pikkuScenarioStep({',
|
|
43
|
-
" name: 'asksForTheReceipt',",
|
|
44
|
-
" description: 'asks for the receipt',",
|
|
45
|
-
' actor: true,',
|
|
46
|
-
' default: async ({ logger }) => ({ ok: true }),',
|
|
47
|
-
'})',
|
|
48
|
-
].join('\n')
|
|
49
|
-
|
|
50
|
-
const scenarioSource = (body: string[], preamble: string[] = []) =>
|
|
51
|
-
[
|
|
52
|
-
"import { pikkuScenario } from '@pikku/core/workflow'",
|
|
53
|
-
'declare const actors: Record<string, any>',
|
|
54
|
-
...preamble,
|
|
55
|
-
'export const shopFlow = pikkuScenario(async (_services, _input, { scenario }: any) => {',
|
|
56
|
-
...body.map((line) => ` ${line}`),
|
|
57
|
-
' return { ok: true }',
|
|
58
|
-
'})',
|
|
59
|
-
].join('\n')
|
|
60
|
-
|
|
61
|
-
async function run(body: string[], preamble: string[] = []) {
|
|
62
|
-
const rootDir = await mkdtemp(join(tmpdir(), 'pikku-scenario-step-'))
|
|
63
|
-
const stepsFile = join(rootDir, 'shop.steps.ts')
|
|
64
|
-
const scenarioFile = join(rootDir, 'shop.scenario.ts')
|
|
65
|
-
await writeFile(stepsFile, STEPS)
|
|
66
|
-
await writeFile(scenarioFile, scenarioSource(body, preamble))
|
|
67
|
-
const criticals: Array<{ code: string; message: string }> = []
|
|
68
|
-
const state = await inspect(
|
|
69
|
-
makeLogger(criticals),
|
|
70
|
-
[stepsFile, scenarioFile],
|
|
71
|
-
{
|
|
72
|
-
rootDir,
|
|
73
|
-
}
|
|
74
|
-
)
|
|
75
|
-
return {
|
|
76
|
-
state,
|
|
77
|
-
criticals,
|
|
78
|
-
cleanup: () => rm(rootDir, { recursive: true, force: true }),
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const flatten = (steps: WorkflowStepMeta[]): WorkflowStepMeta[] =>
|
|
83
|
-
steps.flatMap((step) => [
|
|
84
|
-
step,
|
|
85
|
-
...('body' in step && step.body
|
|
86
|
-
? flatten(step.body as WorkflowStepMeta[])
|
|
87
|
-
: []),
|
|
88
|
-
])
|
|
89
|
-
|
|
90
|
-
const scenarioSteps = (state: any): ScenarioStepMeta[] =>
|
|
91
|
-
flatten(state.workflows.meta.shopFlow?.steps ?? []).filter(
|
|
92
|
-
(step): step is ScenarioStepMeta => step.type === 'scenarioStep'
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
describe('pikkuScenarioStep', () => {
|
|
96
|
-
test('a step is registered under its declared name, not its export name', async () => {
|
|
97
|
-
const { state, cleanup } = await run([
|
|
98
|
-
"await scenario.given('buys an apple', 'buysAnApple', { qty: 1 }, { actor: actors.shopper })",
|
|
99
|
-
])
|
|
100
|
-
try {
|
|
101
|
-
assert.ok(
|
|
102
|
-
state.functions.meta['buysAnApple'],
|
|
103
|
-
`expected a function registered as 'buysAnApple', got: ${Object.keys(state.functions.meta).join(', ')}`
|
|
104
|
-
)
|
|
105
|
-
} finally {
|
|
106
|
-
await cleanup()
|
|
107
|
-
}
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
test('a step is never registered as an RPC — a browser step must not be network-callable', async () => {
|
|
111
|
-
const { state, cleanup } = await run([
|
|
112
|
-
"await scenario.given('buys an apple', 'buysAnApple', { qty: 1 }, { actor: actors.shopper })",
|
|
113
|
-
])
|
|
114
|
-
try {
|
|
115
|
-
assert.equal(
|
|
116
|
-
state.rpc.internalMeta['buysAnApple'],
|
|
117
|
-
undefined,
|
|
118
|
-
'scenario steps must not be RPC-callable'
|
|
119
|
-
)
|
|
120
|
-
assert.equal(
|
|
121
|
-
state.rpc.exposedMeta['buysAnApple'],
|
|
122
|
-
undefined,
|
|
123
|
-
'scenario steps must not be exposed over RPC'
|
|
124
|
-
)
|
|
125
|
-
} finally {
|
|
126
|
-
await cleanup()
|
|
127
|
-
}
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
test('given/when/then record phase, target and actor', async () => {
|
|
131
|
-
const { state, cleanup } = await run([
|
|
132
|
-
"await scenario.given('buys an apple', 'buysAnApple', { qty: 1 }, { actor: actors.shopper })",
|
|
133
|
-
"await scenario.then('sees a receipt', 'seesAReceipt', {}, { actor: actors.shopper })",
|
|
134
|
-
])
|
|
135
|
-
try {
|
|
136
|
-
const steps = scenarioSteps(state)
|
|
137
|
-
assert.equal(
|
|
138
|
-
steps.length,
|
|
139
|
-
2,
|
|
140
|
-
`expected 2 scenario steps, got ${steps.length}`
|
|
141
|
-
)
|
|
142
|
-
assert.equal(steps[0]!.phase, 'given')
|
|
143
|
-
assert.equal(steps[0]!.stepFunc, 'buysAnApple')
|
|
144
|
-
assert.equal(steps[0]!.actor, 'shopper')
|
|
145
|
-
assert.equal(steps[1]!.phase, 'then')
|
|
146
|
-
assert.equal(steps[1]!.stepFunc, 'seesAReceipt')
|
|
147
|
-
} finally {
|
|
148
|
-
await cleanup()
|
|
149
|
-
}
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
test('the step target is bundled, so the step function is wired', async () => {
|
|
153
|
-
const { state, cleanup } = await run([
|
|
154
|
-
"await scenario.given('buys an apple', 'buysAnApple', { qty: 1 }, { actor: actors.shopper })",
|
|
155
|
-
])
|
|
156
|
-
try {
|
|
157
|
-
assert.ok(
|
|
158
|
-
state.rpc.invokedFunctions.has('buysAnApple'),
|
|
159
|
-
`expected 'buysAnApple' to be bundled, got: ${[...state.rpc.invokedFunctions].join(', ')}`
|
|
160
|
-
)
|
|
161
|
-
} finally {
|
|
162
|
-
await cleanup()
|
|
163
|
-
}
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
test('a step inside a for..of loop is extracted, not silently dropped', async () => {
|
|
167
|
-
const { state, cleanup } = await run([
|
|
168
|
-
'const quantities = [1, 2]',
|
|
169
|
-
'for (const qty of quantities) {',
|
|
170
|
-
" await scenario.when('buys an apple', 'buysAnApple', { qty }, { actor: actors.shopper })",
|
|
171
|
-
'}',
|
|
172
|
-
])
|
|
173
|
-
try {
|
|
174
|
-
const steps = scenarioSteps(state)
|
|
175
|
-
assert.equal(
|
|
176
|
-
steps.length,
|
|
177
|
-
1,
|
|
178
|
-
`expected the looped step to appear in the fanout body, got ${steps.length} scenario steps`
|
|
179
|
-
)
|
|
180
|
-
assert.equal(steps[0]!.stepFunc, 'buysAnApple')
|
|
181
|
-
assert.ok(
|
|
182
|
-
state.rpc.invokedFunctions.has('buysAnApple'),
|
|
183
|
-
'a looped step must still be bundled'
|
|
184
|
-
)
|
|
185
|
-
} finally {
|
|
186
|
-
await cleanup()
|
|
187
|
-
}
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
test('a non-literal step target is a PKU678 critical', async () => {
|
|
191
|
-
const { criticals, cleanup } = await run([
|
|
192
|
-
'const target = Math.random() > 0.5 ? "buysAnApple" : "seesAReceipt"',
|
|
193
|
-
"await scenario.when('buys something', target as any, {}, { actor: actors.shopper })",
|
|
194
|
-
])
|
|
195
|
-
try {
|
|
196
|
-
assert.ok(
|
|
197
|
-
criticals.find((c) => c.code === 'PKU678'),
|
|
198
|
-
`expected a PKU678 critical for a dynamic step target, got: ${JSON.stringify(criticals)}`
|
|
199
|
-
)
|
|
200
|
-
} finally {
|
|
201
|
-
await cleanup()
|
|
202
|
-
}
|
|
203
|
-
})
|
|
204
|
-
|
|
205
|
-
test('a browser step called without an actor is a PKU677 critical', async () => {
|
|
206
|
-
const { criticals, cleanup } = await run([
|
|
207
|
-
"await scenario.given('buys an apple', 'buysAnApple', { qty: 1 })",
|
|
208
|
-
])
|
|
209
|
-
try {
|
|
210
|
-
assert.ok(
|
|
211
|
-
criticals.find((c) => c.code === 'PKU677'),
|
|
212
|
-
`expected a PKU677 critical for a browser step without an actor, got: ${JSON.stringify(criticals)}`
|
|
213
|
-
)
|
|
214
|
-
} finally {
|
|
215
|
-
await cleanup()
|
|
216
|
-
}
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
test('a step declaring `actor: true` called without one is a PKU677 critical', async () => {
|
|
220
|
-
const { criticals, cleanup } = await run([
|
|
221
|
-
"await scenario.then('asks for the receipt', 'asksForTheReceipt', {})",
|
|
222
|
-
])
|
|
223
|
-
try {
|
|
224
|
-
assert.ok(
|
|
225
|
-
criticals.find((c) => c.code === 'PKU677'),
|
|
226
|
-
`a step that says it runs as somebody must be given somebody, browser binding or not, got: ${JSON.stringify(criticals)}`
|
|
227
|
-
)
|
|
228
|
-
} finally {
|
|
229
|
-
await cleanup()
|
|
230
|
-
}
|
|
231
|
-
})
|
|
232
|
-
|
|
233
|
-
test('a step that declares no actor is not flagged for running without one', async () => {
|
|
234
|
-
const { criticals, cleanup } = await run([
|
|
235
|
-
"await scenario.then('sees a receipt', 'seesAReceipt', {})",
|
|
236
|
-
])
|
|
237
|
-
try {
|
|
238
|
-
assert.equal(
|
|
239
|
-
criticals.filter((c) => c.code === 'PKU677').length,
|
|
240
|
-
0,
|
|
241
|
-
`an assertion over an earlier step's value has nobody to be, got: ${JSON.stringify(criticals)}`
|
|
242
|
-
)
|
|
243
|
-
} finally {
|
|
244
|
-
await cleanup()
|
|
245
|
-
}
|
|
246
|
-
})
|
|
247
|
-
|
|
248
|
-
test('a step whose prose opens with its own actor is a PKU681 critical', async () => {
|
|
249
|
-
const { criticals, cleanup } = await run([
|
|
250
|
-
"await scenario.given(`'shopper' buys an apple`, 'buysAnApple', { qty: 1 }, { actor: actors.shopper })",
|
|
251
|
-
"await scenario.then('sees a receipt', 'seesAReceipt', {}, { actor: actors.shopper })",
|
|
252
|
-
])
|
|
253
|
-
try {
|
|
254
|
-
assert.ok(
|
|
255
|
-
criticals.find((c) => c.code === 'PKU681'),
|
|
256
|
-
`the reporter already renders the actor as the subject, so this reads "Given shopper 'shopper' buys an apple", got: ${JSON.stringify(criticals)}`
|
|
257
|
-
)
|
|
258
|
-
} finally {
|
|
259
|
-
await cleanup()
|
|
260
|
-
}
|
|
261
|
-
})
|
|
262
|
-
|
|
263
|
-
test('an unquoted, capitalised actor opening the prose is caught too', async () => {
|
|
264
|
-
const { criticals, cleanup } = await run([
|
|
265
|
-
"await scenario.given('Shopper buys an apple', 'buysAnApple', { qty: 1 }, { actor: actors.shopper })",
|
|
266
|
-
"await scenario.then('sees a receipt', 'seesAReceipt', {}, { actor: actors.shopper })",
|
|
267
|
-
])
|
|
268
|
-
try {
|
|
269
|
-
assert.ok(
|
|
270
|
-
criticals.find((c) => c.code === 'PKU681'),
|
|
271
|
-
`quotes and case are decoration — the subject is still doubled, got: ${JSON.stringify(criticals)}`
|
|
272
|
-
)
|
|
273
|
-
} finally {
|
|
274
|
-
await cleanup()
|
|
275
|
-
}
|
|
276
|
-
})
|
|
277
|
-
|
|
278
|
-
test('a call-site description naming the actor is caught as well as the step name', async () => {
|
|
279
|
-
const { criticals, cleanup } = await run([
|
|
280
|
-
"await scenario.given('buys an apple', 'buysAnApple', { qty: 1 }, { actor: actors.shopper, description: `shopper buys an apple` })",
|
|
281
|
-
"await scenario.then('sees a receipt', 'seesAReceipt', {}, { actor: actors.shopper })",
|
|
282
|
-
])
|
|
283
|
-
try {
|
|
284
|
-
assert.ok(
|
|
285
|
-
criticals.find((c) => c.code === 'PKU681'),
|
|
286
|
-
`the description is what actually renders when it is given, got: ${JSON.stringify(criticals)}`
|
|
287
|
-
)
|
|
288
|
-
} finally {
|
|
289
|
-
await cleanup()
|
|
290
|
-
}
|
|
291
|
-
})
|
|
292
|
-
|
|
293
|
-
test('an actor named mid-sentence is left alone', async () => {
|
|
294
|
-
const { criticals, cleanup } = await run([
|
|
295
|
-
"await scenario.given('buys an apple', 'buysAnApple', { qty: 1 }, { actor: actors.shopper })",
|
|
296
|
-
"await scenario.then('sends shopper a receipt', 'seesAReceipt', {}, { actor: actors.shopper })",
|
|
297
|
-
])
|
|
298
|
-
try {
|
|
299
|
-
assert.equal(
|
|
300
|
-
criticals.filter((c) => c.code === 'PKU681').length,
|
|
301
|
-
0,
|
|
302
|
-
`only the subject position doubles up; naming someone mid-sentence is ordinary prose, got: ${JSON.stringify(criticals)}`
|
|
303
|
-
)
|
|
304
|
-
} finally {
|
|
305
|
-
await cleanup()
|
|
306
|
-
}
|
|
307
|
-
})
|
|
308
|
-
|
|
309
|
-
test('a scenario that never asserts is a PKU680 critical', async () => {
|
|
310
|
-
const { criticals, cleanup } = await run([
|
|
311
|
-
"await scenario.given('buys an apple', 'buysAnApple', { qty: 1 }, { actor: actors.shopper })",
|
|
312
|
-
"await scenario.when('buys an apple', 'buysAnApple', { qty: 2 }, { actor: actors.shopper })",
|
|
313
|
-
])
|
|
314
|
-
try {
|
|
315
|
-
assert.ok(
|
|
316
|
-
criticals.find((c) => c.code === 'PKU680'),
|
|
317
|
-
`a flow with no 'then' proves only that nothing threw, and contributes 0/0 to witness coverage, got: ${JSON.stringify(criticals)}`
|
|
318
|
-
)
|
|
319
|
-
} finally {
|
|
320
|
-
await cleanup()
|
|
321
|
-
}
|
|
322
|
-
})
|
|
323
|
-
|
|
324
|
-
test('a scenario whose witness is an expectation helper is not flagged', async () => {
|
|
325
|
-
// `expectService` is an inline step and carries no phase, so it reaches
|
|
326
|
-
// PKU680 as nothing at all — but a recorded service call is a witness, and
|
|
327
|
-
// a flow that has one is not the assertion-free flow the rule is for.
|
|
328
|
-
const { criticals, cleanup } = await run([
|
|
329
|
-
"await scenario.given('buys an apple', 'buysAnApple', { qty: 1 }, { actor: actors.shopper })",
|
|
330
|
-
"await scenario.expectService('the receipt was emailed', 'emailService.send', { actor: actors.shopper })",
|
|
331
|
-
])
|
|
332
|
-
try {
|
|
333
|
-
assert.equal(
|
|
334
|
-
criticals.filter((c) => c.code === 'PKU680').length,
|
|
335
|
-
0,
|
|
336
|
-
`got: ${JSON.stringify(criticals)}`
|
|
337
|
-
)
|
|
338
|
-
} finally {
|
|
339
|
-
await cleanup()
|
|
340
|
-
}
|
|
341
|
-
})
|
|
342
|
-
|
|
343
|
-
test('a graded agent run is a witness too', async () => {
|
|
344
|
-
// An agent's answer cannot be matched against a fixed string, so a scenario
|
|
345
|
-
// that grades one is asserting — even though `expectScore`, like the other
|
|
346
|
-
// helpers, is an inline step that reaches PKU680 as nothing at all.
|
|
347
|
-
const { criticals, cleanup } = await run([
|
|
348
|
-
"const answer = await scenario.when('asks for a summary', 'runAssistant', { prompt: 'hi' }, { actor: actors.shopper })",
|
|
349
|
-
"await scenario.expectScore('answered briefly', answer.runId, 'brevity', { atLeast: 0.8 })",
|
|
350
|
-
])
|
|
351
|
-
try {
|
|
352
|
-
assert.equal(
|
|
353
|
-
criticals.filter((c) => c.code === 'PKU680').length,
|
|
354
|
-
0,
|
|
355
|
-
`got: ${JSON.stringify(criticals)}`
|
|
356
|
-
)
|
|
357
|
-
} finally {
|
|
358
|
-
await cleanup()
|
|
359
|
-
}
|
|
360
|
-
})
|
|
361
|
-
|
|
362
|
-
test('an expectation parked in an uncalled helper is still no assertion', async () => {
|
|
363
|
-
const { criticals, cleanup } = await run([
|
|
364
|
-
'const neverCalled = async () => {',
|
|
365
|
-
" await scenario.expectService('the receipt was emailed', 'emailService.send')",
|
|
366
|
-
'}',
|
|
367
|
-
"await scenario.given('buys an apple', 'buysAnApple', { qty: 1 }, { actor: actors.shopper })",
|
|
368
|
-
])
|
|
369
|
-
try {
|
|
370
|
-
assert.ok(
|
|
371
|
-
criticals.find((c) => c.code === 'PKU680'),
|
|
372
|
-
`expected PKU680 — the helper never runs, so nothing is asserted; got: ${JSON.stringify(criticals)}`
|
|
373
|
-
)
|
|
374
|
-
} finally {
|
|
375
|
-
await cleanup()
|
|
376
|
-
}
|
|
377
|
-
})
|
|
378
|
-
|
|
379
|
-
test('a scenario with an assertion is not flagged', async () => {
|
|
380
|
-
const { criticals, cleanup } = await run([
|
|
381
|
-
"await scenario.given('buys an apple', 'buysAnApple', { qty: 1 }, { actor: actors.shopper })",
|
|
382
|
-
"await scenario.then('sees a receipt', 'seesAReceipt', {}, { actor: actors.shopper })",
|
|
383
|
-
])
|
|
384
|
-
try {
|
|
385
|
-
assert.equal(
|
|
386
|
-
criticals.filter((c) => c.code === 'PKU680').length,
|
|
387
|
-
0,
|
|
388
|
-
`got: ${JSON.stringify(criticals)}`
|
|
389
|
-
)
|
|
390
|
-
} finally {
|
|
391
|
-
await cleanup()
|
|
392
|
-
}
|
|
393
|
-
})
|
|
394
|
-
|
|
395
|
-
test('a scenario driving only rpc steps is not flagged — it has no step ladder to assert on', async () => {
|
|
396
|
-
const { criticals, cleanup } = await run([
|
|
397
|
-
"await scenario.do('reads the order', 'orderGet', { orderId: 'o1' })",
|
|
398
|
-
])
|
|
399
|
-
try {
|
|
400
|
-
assert.equal(
|
|
401
|
-
criticals.filter((c) => c.code === 'PKU680').length,
|
|
402
|
-
0,
|
|
403
|
-
`the rule is about scenarios written as given/when/then, got: ${JSON.stringify(criticals)}`
|
|
404
|
-
)
|
|
405
|
-
} finally {
|
|
406
|
-
await cleanup()
|
|
407
|
-
}
|
|
408
|
-
})
|
|
409
|
-
|
|
410
|
-
test('a non-browser step without an actor is allowed', async () => {
|
|
411
|
-
const { criticals, cleanup } = await run([
|
|
412
|
-
"await scenario.given('sees a receipt', 'seesAReceipt', {})",
|
|
413
|
-
])
|
|
414
|
-
try {
|
|
415
|
-
assert.equal(
|
|
416
|
-
criticals.filter((c) => c.code === 'PKU677').length,
|
|
417
|
-
0,
|
|
418
|
-
`setup steps need no actor, got: ${JSON.stringify(criticals)}`
|
|
419
|
-
)
|
|
420
|
-
} finally {
|
|
421
|
-
await cleanup()
|
|
422
|
-
}
|
|
423
|
-
})
|
|
424
|
-
|
|
425
|
-
test('a step is marked as a step RPC in its function meta', async () => {
|
|
426
|
-
const { state, cleanup } = await run([
|
|
427
|
-
"await scenario.given('buys an apple', 'buysAnApple', { qty: 1 }, { actor: actors.shopper })",
|
|
428
|
-
])
|
|
429
|
-
try {
|
|
430
|
-
// The fourth RPC kind, alongside public/private/remote: a name that is
|
|
431
|
-
// dispatched inside a run and refused everywhere else.
|
|
432
|
-
assert.equal(state.functions.meta['buysAnApple']?.scenarioStep, true)
|
|
433
|
-
assert.equal(state.functions.meta['seesAReceipt']?.scenarioStep, true)
|
|
434
|
-
} finally {
|
|
435
|
-
await cleanup()
|
|
436
|
-
}
|
|
437
|
-
})
|
|
438
|
-
|
|
439
|
-
test('an ordinary function is not a step RPC', async () => {
|
|
440
|
-
const { state, cleanup } = await run([
|
|
441
|
-
"await scenario.given('buys an apple', 'buysAnApple', { qty: 1 }, { actor: actors.shopper })",
|
|
442
|
-
])
|
|
443
|
-
try {
|
|
444
|
-
assert.equal(state.functions.meta['shopFlow']?.scenarioStep, undefined)
|
|
445
|
-
} finally {
|
|
446
|
-
await cleanup()
|
|
447
|
-
}
|
|
448
|
-
})
|
|
449
|
-
|
|
450
|
-
test('a closure in the scenario body does not have its statements whitelisted', async () => {
|
|
451
|
-
const { state, cleanup } = await run([
|
|
452
|
-
'const safely = async (fn: any) => { try { return await fn() } catch { return null } }',
|
|
453
|
-
'void safely',
|
|
454
|
-
"await scenario.given('buys an apple', 'buysAnApple', { qty: 1 }, { actor: actors.shopper })",
|
|
455
|
-
])
|
|
456
|
-
try {
|
|
457
|
-
const steps = scenarioSteps(state)
|
|
458
|
-
assert.equal(
|
|
459
|
-
steps.length,
|
|
460
|
-
1,
|
|
461
|
-
`a try/catch inside a closure must not drop the scenario's steps, got ${steps.length}`
|
|
462
|
-
)
|
|
463
|
-
assert.equal(steps[0]!.stepFunc, 'buysAnApple')
|
|
464
|
-
} finally {
|
|
465
|
-
await cleanup()
|
|
466
|
-
}
|
|
467
|
-
})
|
|
468
|
-
|
|
469
|
-
test('destructuring a step result is reported, not silently dropped', async () => {
|
|
470
|
-
const { state, criticals, cleanup } = await run([
|
|
471
|
-
"const { ok } = await scenario.given('sees a receipt', 'seesAReceipt', {})",
|
|
472
|
-
'void ok',
|
|
473
|
-
])
|
|
474
|
-
try {
|
|
475
|
-
const steps = scenarioSteps(state)
|
|
476
|
-
assert.equal(
|
|
477
|
-
steps.length,
|
|
478
|
-
0,
|
|
479
|
-
'the DSL does not model a destructured step result'
|
|
480
|
-
)
|
|
481
|
-
assert.ok(
|
|
482
|
-
criticals.find((c) => c.code === 'PKU679'),
|
|
483
|
-
`a step the DSL cannot model must be loud, got: ${JSON.stringify(criticals)}`
|
|
484
|
-
)
|
|
485
|
-
} finally {
|
|
486
|
-
await cleanup()
|
|
487
|
-
}
|
|
488
|
-
})
|
|
489
|
-
|
|
490
|
-
test('a module-level constant used as step input is inlined, not read off the trigger', async () => {
|
|
491
|
-
const { state, cleanup } = await run(
|
|
492
|
-
[
|
|
493
|
-
"await scenario.given('buys an apple', 'buysAnApple', { qty: QTY }, { actor: actors.shopper })",
|
|
494
|
-
],
|
|
495
|
-
['const QTY = 3']
|
|
496
|
-
)
|
|
497
|
-
try {
|
|
498
|
-
const steps = scenarioSteps(state)
|
|
499
|
-
assert.equal(steps.length, 1)
|
|
500
|
-
assert.deepEqual(
|
|
501
|
-
steps[0]!.inputs?.qty,
|
|
502
|
-
{ from: 'literal', value: 3 },
|
|
503
|
-
`a constant declared outside the scenario is not a trigger field, got: ${JSON.stringify(steps[0]!.inputs)}`
|
|
504
|
-
)
|
|
505
|
-
} finally {
|
|
506
|
-
await cleanup()
|
|
507
|
-
}
|
|
508
|
-
})
|
|
509
|
-
})
|
package/src/add/add-schedule.ts
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import * as ts from 'typescript'
|
|
2
|
-
import {
|
|
3
|
-
getPropertyValue,
|
|
4
|
-
getCommonWireMetaData,
|
|
5
|
-
} from '../utils/get-property-value.js'
|
|
6
|
-
import type { AddWiring } from '../types.js'
|
|
7
|
-
import {
|
|
8
|
-
extractFunctionName,
|
|
9
|
-
makeContextBasedId,
|
|
10
|
-
} from '../utils/extract-function-name.js'
|
|
11
|
-
import { getPropertyAssignmentInitializer } from '../utils/type-utils.js'
|
|
12
|
-
import { ensureInlineWiringFunction } from '../utils/ensure-function-metadata.js'
|
|
13
|
-
import { resolveMiddleware } from '../utils/middleware.js'
|
|
14
|
-
import { extractWireNames } from '../utils/post-process.js'
|
|
15
|
-
import { resolveAddonName } from '../utils/resolve-addon-package.js'
|
|
16
|
-
|
|
17
|
-
import { ErrorCode } from '../error-codes.js'
|
|
18
|
-
export const addSchedule: AddWiring = (
|
|
19
|
-
logger,
|
|
20
|
-
node,
|
|
21
|
-
checker,
|
|
22
|
-
state,
|
|
23
|
-
options
|
|
24
|
-
) => {
|
|
25
|
-
if (!ts.isCallExpression(node)) {
|
|
26
|
-
return
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const args = node.arguments
|
|
30
|
-
const firstArg = args[0]
|
|
31
|
-
const expression = node.expression
|
|
32
|
-
|
|
33
|
-
// Check if the call is to addScheduledTask
|
|
34
|
-
if (!ts.isIdentifier(expression) || expression.text !== 'wireScheduler') {
|
|
35
|
-
return
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (!firstArg) {
|
|
39
|
-
return
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (ts.isObjectLiteralExpression(firstArg)) {
|
|
43
|
-
const obj = firstArg
|
|
44
|
-
|
|
45
|
-
const nameValue = getPropertyValue(obj, 'name') as string | null
|
|
46
|
-
const scheduleValue = getPropertyValue(obj, 'schedule') as string | null
|
|
47
|
-
const { disabled, tags, summary, description, errors } =
|
|
48
|
-
getCommonWireMetaData(obj, 'Scheduler', nameValue, logger, checker)
|
|
49
|
-
|
|
50
|
-
if (disabled) return
|
|
51
|
-
|
|
52
|
-
const funcInitializer = getPropertyAssignmentInitializer(
|
|
53
|
-
obj,
|
|
54
|
-
'func',
|
|
55
|
-
true,
|
|
56
|
-
checker
|
|
57
|
-
)
|
|
58
|
-
if (!funcInitializer) {
|
|
59
|
-
logger.critical(
|
|
60
|
-
ErrorCode.MISSING_FUNC,
|
|
61
|
-
`No valid 'func' property for scheduled task '${nameValue}'.`
|
|
62
|
-
)
|
|
63
|
-
return
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const extracted = extractFunctionName(
|
|
67
|
-
funcInitializer,
|
|
68
|
-
checker,
|
|
69
|
-
state.rootDir
|
|
70
|
-
)
|
|
71
|
-
let pikkuFuncId = extracted.pikkuFuncId
|
|
72
|
-
if (pikkuFuncId.startsWith('__temp_') && nameValue) {
|
|
73
|
-
pikkuFuncId = makeContextBasedId('scheduler', nameValue)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const packageName = ts.isIdentifier(funcInitializer)
|
|
77
|
-
? resolveAddonName(
|
|
78
|
-
funcInitializer,
|
|
79
|
-
checker,
|
|
80
|
-
state.rpc.wireAddonDeclarations
|
|
81
|
-
)
|
|
82
|
-
: null
|
|
83
|
-
|
|
84
|
-
if (!nameValue || !scheduleValue) {
|
|
85
|
-
return
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Register metadata for a func inlined into the wiring (see helper).
|
|
89
|
-
ensureInlineWiringFunction(
|
|
90
|
-
state,
|
|
91
|
-
pikkuFuncId,
|
|
92
|
-
nameValue,
|
|
93
|
-
funcInitializer,
|
|
94
|
-
checker,
|
|
95
|
-
extracted.isHelper
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
// --- resolve middleware ---
|
|
99
|
-
const middleware = resolveMiddleware(state, obj, tags, checker)
|
|
100
|
-
|
|
101
|
-
// --- track used functions/middleware for service aggregation ---
|
|
102
|
-
state.serviceAggregation.usedFunctions.add(pikkuFuncId)
|
|
103
|
-
extractWireNames(middleware).forEach((name) =>
|
|
104
|
-
state.serviceAggregation.usedMiddleware.add(name)
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
state.scheduledTasks.files.add(node.getSourceFile().fileName)
|
|
108
|
-
state.scheduledTasks.meta[nameValue] = {
|
|
109
|
-
pikkuFuncId,
|
|
110
|
-
...(packageName && { packageName }),
|
|
111
|
-
name: nameValue,
|
|
112
|
-
schedule: scheduleValue,
|
|
113
|
-
summary,
|
|
114
|
-
description,
|
|
115
|
-
errors,
|
|
116
|
-
tags,
|
|
117
|
-
middleware,
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|