@pikku/inspector 0.12.46 → 0.12.48
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 +197 -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 +24 -9
- package/dist/add/add-workflow.js +22 -29
- package/dist/error-codes.d.ts +3 -0
- package/dist/error-codes.js +3 -0
- package/dist/index.d.ts +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 +98 -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 +29 -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-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 +1 -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 +36 -2
- package/src/utils/post-process.ts +111 -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/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,200 @@
|
|
|
1
|
+
## 0.12.48
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- 04b8607: Make the unmodellable-for-of diagnostic tell you what is actually wrong. It always blamed the iterable ("its iterable must be a data array"), which was false whenever the real cause was a `workflow.do` nested inside an `if`/`switch` — a DSL fanout body is a flat list of steps with no branch member, so the diagnostic now says so and points at `.filter` or `pikkuWorkflowComplexFunc`. Also stops the same diagnostic firing on a `for-of` that contains no workflow call at all: a loop that only massages locals has no step to lose, so erroring on it was a false positive.
|
|
6
|
+
- 7c8f015: Hard-error on a for-of a DSL workflow can't model, instead of silently dropping it.
|
|
7
|
+
|
|
8
|
+
A `pikkuWorkflowFunc` (DSL) whose body contained a computed/counting loop — e.g. `for (const i of [...Array(n).keys()])` — used to serialize with **zero steps**: the DSL extractor only models a for-of as a sequential fanout over a data-array identifier/field (`data.items`), so a non-path iterable made `extractSequentialFanout` return null and the extractor dropped the loop _and every `workflow.do` inside it_. The invoked functions then never entered `invokedFunctions`, so they got no `addFunction()` registration and threw `Function not found` at runtime — a silent codegen footgun that bricked every prod sandbox create.
|
|
9
|
+
|
|
10
|
+
Now the extractor pushes a validation error naming the offending for-of, so `pikkuWorkflowFunc` reports `INVALID_DSL_WORKFLOW` at codegen. A genuine control-flow/counting loop belongs in `pikkuWorkflowComplexFunc`, which falls back to the basic AST walk that _does_ register loop-invoked functions.
|
|
11
|
+
|
|
12
|
+
- 5962e51: Give the inspector's program the project's tsconfig path mappings.
|
|
13
|
+
|
|
14
|
+
The inspector builds its own TypeScript program rather than reusing the project's, which is what keeps a cold run affordable — but it hand-rolled its compiler options and so had no `paths`. An import the project resolves only through a `paths` entry therefore did not resolve inside the inspector, and the factory the inspected function was passed to came back as `any`. A function whose input type comes from that factory's contextual type — rather than an explicit generic or a named `input:` schema — then recorded no input at all, silently, with no diagnostic.
|
|
15
|
+
|
|
16
|
+
`inspect` takes a new `tsconfig` option (the CLI passes `config.tsconfig`, including on a setup-only run, where `schemaConfig` is absent). Only resolution keys are copied across — `baseUrl`, `paths`, `rootDirs`, `pathsBasePath`. Type-inference options such as `strict` deliberately stay the inspector's own, because changing those would change the types it records.
|
|
17
|
+
|
|
18
|
+
This was latent rather than live: the generic path reads type _nodes_, the schema path reads an identifier, output types come from the handler body, and JSON schemas are generated by a separate tsconfig-aware program — so nothing in the tree reached the contextual-type fallback. Regenerating the e2e project and the `functions` template produces byte-identical output.
|
|
19
|
+
|
|
20
|
+
- 5962e51: Add `pikkuFeature`, a grouping primitive for scenarios.
|
|
21
|
+
|
|
22
|
+
A feature groups scenarios the way gherkin's `Feature:` groups `Scenario:`, and gets `Examples:` for free as an ordinary loop:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
export const credentialFeature = pikkuFeature({
|
|
26
|
+
name: 'Credential API',
|
|
27
|
+
tags: ['credential'],
|
|
28
|
+
before: startsMockOAuthServer,
|
|
29
|
+
after: stopsMockOAuthServer,
|
|
30
|
+
scenarios: [
|
|
31
|
+
credentialLazyLoadScenario,
|
|
32
|
+
...['stripe', 'google', 'hmac-key'].map((name) => ({
|
|
33
|
+
scenario: credentialRoundTripScenario,
|
|
34
|
+
data: { name },
|
|
35
|
+
})),
|
|
36
|
+
],
|
|
37
|
+
})
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
- Scenarios are referenced by **imported identifier**, not by string name, so a renamed or deleted scenario is a compile error rather than a silent skip. A `{ scenario, data }` entry's `data` is typed against that scenario's own input.
|
|
41
|
+
- Feature hooks run **once around the whole group** (`before → a → b → c → after`), not per scenario, and `after` runs in a `finally`. Per-scenario setup stays the scenario's own `before`; gherkin's `Background:` is deliberately not expressible.
|
|
42
|
+
- A scenario's effective tags are its own plus its feature's, so `--tags credential` selects through the feature.
|
|
43
|
+
- New `--features` selector on `pikku scenario run`, and `pikku scenario list` now prints features with their scenarios indented. Every filter narrows the same plan, so narrowing a feature to two of its five scenarios still runs its hooks exactly once around those two.
|
|
44
|
+
- The **feature is the run unit**: `--flows` on a scenario whose every feature entry carries `data` errors and names the features containing it, because the feature is what supplies that data. A scenario referenced bare anywhere, or in no feature at all, still runs standalone.
|
|
45
|
+
|
|
46
|
+
`pikkuFeature` infers its scenario list with a `const` generic, so `CoreFeature['scenarios']` is `readonly` — otherwise the emitted `addFeature(id, feature)` call does not typecheck.
|
|
47
|
+
|
|
48
|
+
Membership is resolved at runtime by object identity — `pikkuScenario` returns its config verbatim, so a feature holds the very object that was registered. That is what lets the scenario list be built by a loop, which no static analysis could enumerate. It also means a scenario constructed inline inside a feature is never registered, and is reported as unresolved rather than silently running as something else.
|
|
49
|
+
|
|
50
|
+
- 5962e51: Add `before` / `after` hooks to `pikkuScenario`, and make an unextractable scenario a hard error.
|
|
51
|
+
|
|
52
|
+
A scenario config now takes `before` and `after`. Both have the same signature as `func` — `(services, data, wire)` — with the return value discarded, so there is no new type to learn and a hook reaches the app the same way the body does, through `wire.actors`:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
export const credentialScenario = pikkuScenario({
|
|
56
|
+
title: 'A credential is loaded on first use',
|
|
57
|
+
tags: ['scenario', 'credential'],
|
|
58
|
+
before: resetsCredentials,
|
|
59
|
+
after: removesInstalledAddon,
|
|
60
|
+
func: async (services, data, { scenario, actors }) => { ... },
|
|
61
|
+
})
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
- `before` throwing skips the body and fails the run, but `after` still runs.
|
|
65
|
+
- `after` always runs, in a `finally`. Throwing fails a run that would otherwise have passed; on an already-failed run it attaches as the `cause` and never replaces the original error.
|
|
66
|
+
- Neither runs when the run is suspended or waiting — teardown only fires at a terminal outcome.
|
|
67
|
+
- Hooks are not ladder rows: the runner records nothing for them, and a failure is labelled by phase via the new `ScenarioHookError`.
|
|
68
|
+
- Hooks are scenario-only. A `before`/`after` on a `pikkuWorkflowFunc` never runs — a workflow is durable and resumable, so a callback that reran on every replay would have no honest meaning.
|
|
69
|
+
|
|
70
|
+
Two fixes that scenarios needed to be safe to write:
|
|
71
|
+
- A closure in a complex-workflow or scenario body is no longer held to the DSL statement whitelist. A single `try`/`catch` inside any callback previously failed extraction, and the fallback path understands `do`/`sleep` but not `step`/`given`/`when`/`then` — so the scenario registered with **zero steps** and passed vacuously, with no diagnostic. Plain DSL workflows still descend into callbacks, which is what validates fanout bodies.
|
|
72
|
+
- New `PKU679`: a scenario that fails DSL extraction is now a critical error and refuses to register, instead of silently registering empty. A scenario that declares no input parameter at all is legitimate and still extracts.
|
|
73
|
+
|
|
74
|
+
- a436645: Redesign the console's scenarios screen as living documentation of a project's BDD features.
|
|
75
|
+
|
|
76
|
+
The inspector now statically extracts `pikkuFeature` declarations — name, description, tags, the scenarios each one groups (including `{ scenario, data }` examples), and whether it declares `before`/`after` — and the CLI writes them to `<outDir>/scenarios/features.gen.json`, which `MetaService.getFeaturesMeta()` reads and the console addon returns from `getAllMeta`.
|
|
77
|
+
|
|
78
|
+
The scenarios page reads that back as a document: features on the left, and on the right the selected feature's scenarios, each rendered as the given/when/then ladder of prose its author actually wrote, with repeats shown as `for each x in xs`, `Examples:` tables for parameterised entries, skip reasons stated rather than hidden, and each scenario's cast of personas inline. The Flows/Personas segmented control is gone; tags filter the document the same way `pikku scenario run --tags` filters a run.
|
|
79
|
+
|
|
80
|
+
- 47478a4: Let a scenario declare why it is held out of a default run.
|
|
81
|
+
|
|
82
|
+
`pikkuScenario({ skip: 'why' })` keeps the scenario in the plan and reports it as `SKIP <name> (<reason>)` on the ladder, instead of the alternatives available until now: deleting it, commenting it out, or leaving it red. Naming it directly with `--flows` clears the quarantine and runs it; selecting the feature it belongs to does not, because a feature is a group and running the group should not silently drag a quarantined member in.
|
|
83
|
+
|
|
84
|
+
The run report's `skipped` list now carries a reason per scenario rather than assuming `--no-browser`, so a browser scenario held back on a machine with no browser reads differently from one the project quarantined itself.
|
|
85
|
+
|
|
86
|
+
`@pikku/console` gains a test id on the addon detail page's Setup tab, which was previously only reachable through its translated label.
|
|
87
|
+
|
|
88
|
+
- 2434f1b: Fix two silent losses in scenario step extraction.
|
|
89
|
+
|
|
90
|
+
Destructuring a scenario step result (`const { threadId } = await scenario.given(...)`) dropped the
|
|
91
|
+
step from the graph without a diagnostic — the step stayed fully typed and present in the step map,
|
|
92
|
+
then failed at runtime with `Function not found`. It now reports PKU679, like every other step form
|
|
93
|
+
the DSL cannot model.
|
|
94
|
+
|
|
95
|
+
A constant referenced as step input (`{ resourceId: RESOURCE_ID }`) was serialized as
|
|
96
|
+
`{ $ref: 'trigger', path: 'RESOURCE_ID' }` — a read of a trigger field that does not exist. A `const`
|
|
97
|
+
with a literal initializer is now inlined as that literal.
|
|
98
|
+
|
|
99
|
+
- 5962e51: Add `template` to `pikkuScenarioStep`, so a step's reported prose names the values it was called with.
|
|
100
|
+
|
|
101
|
+
`description` documents what a step does, for the console and for whoever reads the source. `template` is what a reader of the report sees, with `{placeholders}` filled from the input the step was actually called with:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
export const seesAddonCard = pikkuScenarioStep<
|
|
105
|
+
{ packageName: string; state?: 'installed' | 'available' },
|
|
106
|
+
{ visible: true },
|
|
107
|
+
true
|
|
108
|
+
>({
|
|
109
|
+
name: 'seesAddonCard',
|
|
110
|
+
description: 'sees an addon in the gallery',
|
|
111
|
+
template: 'sees {state} addon {packageName}',
|
|
112
|
+
browser: true,
|
|
113
|
+
func: async (_services, { packageName, state }, { browser }) => { … },
|
|
114
|
+
})
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
Then the admin sees at least 10 addons on offer ✓ 3ms
|
|
119
|
+
When the admin searches for stripe ✓ 10ms
|
|
120
|
+
Then the admin sees available addon @pikku/addon-stripe ✓ 77ms
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Previously the only way to get that was a `description` at every call site, which meant writing the sentence once per call rather than once per step — and a call site that forgot it reported the same sentence three times in a row.
|
|
124
|
+
- A placeholder with no recorded value renders as nothing and the surrounding whitespace collapses, so an omitted optional input reads as a shorter sentence rather than leaking a literal `{state}` into the report. Type placeholder values so they read as words (`state?: 'installed' | 'available'`, not `installed?: boolean`).
|
|
125
|
+
- A call-site `description` still wins, the same way it already won over the step's `description`.
|
|
126
|
+
- `renderStepTemplate` is exported from `@pikku/core/workflow` alongside `composeStepProse`, so the CLI reporter and the console render identically.
|
|
127
|
+
|
|
128
|
+
Scenario steps now record their input on the run (`inlineStep` persisted `null` for every inline step, so there was nothing for a reporter to interpolate). This is what `getRunSteps` already exposes as `data` for RPC steps.
|
|
129
|
+
|
|
130
|
+
A step called from a loop gets its template too. Its durable name is built at runtime (`sees @pikku/addon-todos`) from a declaration the static meta records verbatim (`sees ${packageName}`), so the two can never match by name — it used to fall back to the bare name, with no keyword, actor or template:
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
sees @pikku/addon-console ✓ 85ms
|
|
134
|
+
Then the admin sees installed addon @pikku/addon-console ✓ 92ms
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The join is by **step function**. A scenario step is dispatched by name exactly as an RPC is, so it now records that name in the run's existing `rpcName` slot — no new field, no schema change in any workflow store. Nothing dispatches off that value anywhere; step identity always comes from the code being replayed.
|
|
138
|
+
|
|
139
|
+
To keep the slot honest, a scenario step is now its own **kind of RPC**, alongside public / private / remote: `FunctionMeta.scenarioStep` marks it, and `rpcExposed` refuses it even if something marks it `expose: true`. Steps were already left out of the RPC registry; this makes "never network-callable" a property the runtime enforces rather than one the registration path happens to produce.
|
|
140
|
+
|
|
141
|
+
`collectScenarioStepProse` now returns `{ byStepName, byStepFunc }` rather than a bare `Map`, and `buildStepLadder` takes that. The step name still wins; the function index only decides steps recorded under a name no declaration carries, and a function called from several sites that disagree on their prose is left out rather than guessed at.
|
|
142
|
+
|
|
143
|
+
- 5962e51: Add `pikkuScenarioStep` — named, typed scenario steps whose body is an ordinary pikku function.
|
|
144
|
+
|
|
145
|
+
A scenario step is referenced by typed string name, the same way `workflow.do` references an RPC, and is checked against a generated `FlattenedScenarioStepMap`:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
export const buysAnApple = pikkuScenarioStep({
|
|
149
|
+
name: 'buysAnApple',
|
|
150
|
+
description: 'buys an apple',
|
|
151
|
+
func: async (services, data: { qty: number }) => { ... },
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
await scenario.given('buys an apple', 'buysAnApple', { qty: 1 }, { actor: actors.shopper })
|
|
155
|
+
// renders: Given the shopper buys an apple
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
- `given`/`when`/`then` are sugar over `step`, setting only the prose prefix. The runner renders a step ladder from the recorded run.
|
|
159
|
+
- Steps default to `retries: 0` — a failed assertion is not retried.
|
|
160
|
+
- Steps are deliberately **not** registered as RPCs, so a browser-driving step is never network-callable.
|
|
161
|
+
- `browser: true` steps receive a browser handle on the wire. `@pikku/playwright` is a new package providing the Playwright-backed provider, signing each actor's browser context in through the same actor path the HTTP actors use. Without a provider, `pikku scenario run --no-browser` **skips** browser scenarios instead of failing them.
|
|
162
|
+
- New diagnostics: PKU677 (a `browser: true` step called without an actor) and PKU678 (a step target that is not a static string literal).
|
|
163
|
+
- Fixes `--no-<flag>` boolean negation in the CLI command parser, which previously parsed as an unknown option.
|
|
164
|
+
- Fixes PKU673 (a scenario func destructuring services), which never fired because it ran before function meta existed; it now runs in post-processing.
|
|
165
|
+
- Fixes scenario/workflow steps nested in `for...of` and `Promise.all` being dropped from workflow meta.
|
|
166
|
+
|
|
167
|
+
- Updated dependencies [539ee0b]
|
|
168
|
+
- Updated dependencies [a1a6816]
|
|
169
|
+
- Updated dependencies [dc3e11e]
|
|
170
|
+
- Updated dependencies [24da616]
|
|
171
|
+
- Updated dependencies [04bfe3f]
|
|
172
|
+
- Updated dependencies [5962e51]
|
|
173
|
+
- Updated dependencies [5962e51]
|
|
174
|
+
- Updated dependencies [cd6453c]
|
|
175
|
+
- Updated dependencies [a436645]
|
|
176
|
+
- Updated dependencies [46cf63e]
|
|
177
|
+
- Updated dependencies [9e666bc]
|
|
178
|
+
- Updated dependencies [1c841d8]
|
|
179
|
+
- Updated dependencies [47478a4]
|
|
180
|
+
- Updated dependencies [9e666bc]
|
|
181
|
+
- Updated dependencies [5962e51]
|
|
182
|
+
- Updated dependencies [5962e51]
|
|
183
|
+
- Updated dependencies [61b9bf8]
|
|
184
|
+
- @pikku/core@0.12.70
|
|
185
|
+
|
|
186
|
+
## 0.12.47
|
|
187
|
+
|
|
188
|
+
### Patch Changes
|
|
189
|
+
|
|
190
|
+
- 91077ff: The auth factory's services are read from the auth definition, not from the handler generated out of it.
|
|
191
|
+
|
|
192
|
+
`pikkuBetterAuth((services) => ...)`'s destructured services are what `authorize` and the session callbacks actually use, and they reach the generated services map by being stamped onto the auth handler's function meta. On the first run in a clean checkout that handler's file has not been written yet — so there is nothing to stamp, nothing to aggregate, and the map comes out marking those services unused. Run it a second time, with the file now on disk, and it says the opposite.
|
|
193
|
+
|
|
194
|
+
The consequence is not cosmetic: `RequiredSingletonServices` is built from that map, so a clean build types the services as optional and tree-shakes them out of the deployed auth worker — the exact failure the stamping exists to prevent, reintroduced by the one case where it cannot run. A CI checkout is always the first run.
|
|
195
|
+
|
|
196
|
+
The definition is inspected from hand-written source on every pass, so its services are now folded into the required set directly. A clean build and an incremental one give the same answer.
|
|
197
|
+
|
|
1
198
|
## 0.12.46
|
|
2
199
|
|
|
3
200
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -1,3 +1,29 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @pikku/inspector
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The TypeScript inspector behind the Pikku CLI. Walks your source with the
|
|
4
|
+
TypeScript compiler API to discover functions, wirings and their types, and
|
|
5
|
+
produces the state the CLI generates code from.
|
|
6
|
+
|
|
7
|
+
Most users get this transitively via `@pikku/cli`; install it directly only if
|
|
8
|
+
you are building your own tooling on top of Pikku's introspection.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @pikku/inspector
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import { inspect, getInitialInspectorState } from '@pikku/inspector'
|
|
20
|
+
|
|
21
|
+
const state = getInitialInspectorState()
|
|
22
|
+
await inspect(['src'], tsconfigPath, state)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Diagnostics come back as `CodedDiagnostic` values with a stable `ErrorCode`.
|
|
26
|
+
|
|
27
|
+
## Docs
|
|
28
|
+
|
|
29
|
+
https://pikku.dev/docs
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { AddWiring } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Inspector for `pikkuFeature()` calls.
|
|
4
|
+
*
|
|
5
|
+
* A feature is the document structure the console renders — its name,
|
|
6
|
+
* description, tags and the order of its scenarios are what a reader sees — so
|
|
7
|
+
* everything the config states literally is extracted here.
|
|
8
|
+
*
|
|
9
|
+
* What cannot be extracted is a scenarios entry that is not literal:
|
|
10
|
+
* `...['stripe', 'google'].map((name) => ({ scenario, data: { name } }))` is
|
|
11
|
+
* ordinary TypeScript, and no AST walk can enumerate it. Those entries are
|
|
12
|
+
* counted in `unresolvedEntries` rather than dropped, and membership for them
|
|
13
|
+
* is resolved at runtime by object identity (`resolveFeatureScenarios`), where
|
|
14
|
+
* the array has actually been evaluated.
|
|
15
|
+
*
|
|
16
|
+
* The export location is recorded either way, which is what the CLI needs to
|
|
17
|
+
* emit its `addFeature(...)` wiring line.
|
|
18
|
+
*/
|
|
19
|
+
export declare const addFeature: AddWiring;
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
import { extractFunctionName } from '../utils/extract-function-name.js';
|
|
3
|
+
import { ErrorCode } from '../error-codes.js';
|
|
4
|
+
/**
|
|
5
|
+
* Reads a fully literal expression into a plain value.
|
|
6
|
+
*
|
|
7
|
+
* Used for a `{ scenario, data }` entry's `data`, which is gherkin's
|
|
8
|
+
* `Examples:` row. Anything not literal yields `undefined` — a computed row
|
|
9
|
+
* belongs to the runtime-resolved portion, not to the extracted meta.
|
|
10
|
+
*/
|
|
11
|
+
const literalValue = (node) => {
|
|
12
|
+
if (ts.isStringLiteralLike(node)) {
|
|
13
|
+
return node.text;
|
|
14
|
+
}
|
|
15
|
+
if (ts.isNumericLiteral(node)) {
|
|
16
|
+
return Number(node.text);
|
|
17
|
+
}
|
|
18
|
+
if (node.kind === ts.SyntaxKind.TrueKeyword) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
if (node.kind === ts.SyntaxKind.FalseKeyword) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
if (node.kind === ts.SyntaxKind.NullKeyword) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
if (ts.isPrefixUnaryExpression(node) &&
|
|
28
|
+
node.operator === ts.SyntaxKind.MinusToken &&
|
|
29
|
+
ts.isNumericLiteral(node.operand)) {
|
|
30
|
+
return -Number(node.operand.text);
|
|
31
|
+
}
|
|
32
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
33
|
+
const values = [];
|
|
34
|
+
for (const element of node.elements) {
|
|
35
|
+
const value = literalValue(element);
|
|
36
|
+
if (value === undefined) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
values.push(value);
|
|
40
|
+
}
|
|
41
|
+
return values;
|
|
42
|
+
}
|
|
43
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
44
|
+
const value = {};
|
|
45
|
+
for (const property of node.properties) {
|
|
46
|
+
if (!ts.isPropertyAssignment(property)) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
const key = ts.isIdentifier(property.name) || ts.isStringLiteralLike(property.name)
|
|
50
|
+
? property.name.text
|
|
51
|
+
: undefined;
|
|
52
|
+
if (key === undefined) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
const propertyValue = literalValue(property.initializer);
|
|
56
|
+
if (propertyValue === undefined) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
value[key] = propertyValue;
|
|
60
|
+
}
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Resolves a scenario reference to the name it is declared under, following an
|
|
67
|
+
* import alias back to the original export so `{ x as y }` records `x`.
|
|
68
|
+
*/
|
|
69
|
+
const scenarioName = (node, checker) => {
|
|
70
|
+
if (!ts.isIdentifier(node)) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
const symbol = checker.getSymbolAtLocation(node);
|
|
74
|
+
if (!symbol) {
|
|
75
|
+
return node.text;
|
|
76
|
+
}
|
|
77
|
+
const resolved = symbol.flags & ts.SymbolFlags.Alias
|
|
78
|
+
? checker.getAliasedSymbol(symbol)
|
|
79
|
+
: symbol;
|
|
80
|
+
return resolved.getName() || node.text;
|
|
81
|
+
};
|
|
82
|
+
const getProperty = (config, name) => {
|
|
83
|
+
const property = config.properties.find((p) => ts.isPropertyAssignment(p) &&
|
|
84
|
+
ts.isIdentifier(p.name) &&
|
|
85
|
+
p.name.text === name);
|
|
86
|
+
return property && ts.isPropertyAssignment(property)
|
|
87
|
+
? property.initializer
|
|
88
|
+
: undefined;
|
|
89
|
+
};
|
|
90
|
+
const hasProperty = (config, name) => config.properties.some((p) => (ts.isPropertyAssignment(p) ||
|
|
91
|
+
ts.isMethodDeclaration(p) ||
|
|
92
|
+
ts.isShorthandPropertyAssignment(p)) &&
|
|
93
|
+
ts.isIdentifier(p.name) &&
|
|
94
|
+
p.name.text === name);
|
|
95
|
+
const stringProperty = (config, name) => {
|
|
96
|
+
const value = getProperty(config, name);
|
|
97
|
+
return value && ts.isStringLiteralLike(value) ? value.text : undefined;
|
|
98
|
+
};
|
|
99
|
+
const stringArrayProperty = (config, name) => {
|
|
100
|
+
const value = getProperty(config, name);
|
|
101
|
+
if (!value || !ts.isArrayLiteralExpression(value)) {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
const values = value.elements
|
|
105
|
+
.filter(ts.isStringLiteralLike)
|
|
106
|
+
.map((e) => e.text);
|
|
107
|
+
return values.length === value.elements.length ? values : undefined;
|
|
108
|
+
};
|
|
109
|
+
const readEntry = (element, checker) => {
|
|
110
|
+
if (ts.isIdentifier(element)) {
|
|
111
|
+
const scenario = scenarioName(element, checker);
|
|
112
|
+
return scenario ? { scenario } : undefined;
|
|
113
|
+
}
|
|
114
|
+
if (ts.isObjectLiteralExpression(element)) {
|
|
115
|
+
const reference = getProperty(element, 'scenario');
|
|
116
|
+
if (!reference) {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
const scenario = scenarioName(reference, checker);
|
|
120
|
+
if (!scenario) {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
const dataNode = getProperty(element, 'data');
|
|
124
|
+
if (!dataNode) {
|
|
125
|
+
return { scenario };
|
|
126
|
+
}
|
|
127
|
+
const data = literalValue(dataNode);
|
|
128
|
+
return data === undefined ? undefined : { scenario, data };
|
|
129
|
+
}
|
|
130
|
+
return undefined;
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* Inspector for `pikkuFeature()` calls.
|
|
134
|
+
*
|
|
135
|
+
* A feature is the document structure the console renders — its name,
|
|
136
|
+
* description, tags and the order of its scenarios are what a reader sees — so
|
|
137
|
+
* everything the config states literally is extracted here.
|
|
138
|
+
*
|
|
139
|
+
* What cannot be extracted is a scenarios entry that is not literal:
|
|
140
|
+
* `...['stripe', 'google'].map((name) => ({ scenario, data: { name } }))` is
|
|
141
|
+
* ordinary TypeScript, and no AST walk can enumerate it. Those entries are
|
|
142
|
+
* counted in `unresolvedEntries` rather than dropped, and membership for them
|
|
143
|
+
* is resolved at runtime by object identity (`resolveFeatureScenarios`), where
|
|
144
|
+
* the array has actually been evaluated.
|
|
145
|
+
*
|
|
146
|
+
* The export location is recorded either way, which is what the CLI needs to
|
|
147
|
+
* emit its `addFeature(...)` wiring line.
|
|
148
|
+
*/
|
|
149
|
+
export const addFeature = (logger, node, checker, state) => {
|
|
150
|
+
if (!ts.isCallExpression(node)) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const expression = node.expression;
|
|
154
|
+
if (!ts.isIdentifier(expression) || expression.text !== 'pikkuFeature') {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const { exportedName } = extractFunctionName(node, checker, state.rootDir);
|
|
158
|
+
if (!exportedName) {
|
|
159
|
+
logger.critical(ErrorCode.MISSING_NAME, `A pikkuFeature() must be assigned to an export — the export identifier is the feature's id.`);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const feature = {
|
|
163
|
+
path: node.getSourceFile().fileName,
|
|
164
|
+
exportedName,
|
|
165
|
+
entries: [],
|
|
166
|
+
unresolvedEntries: 0,
|
|
167
|
+
hasBefore: false,
|
|
168
|
+
hasAfter: false,
|
|
169
|
+
};
|
|
170
|
+
const [config] = node.arguments;
|
|
171
|
+
if (config && ts.isObjectLiteralExpression(config)) {
|
|
172
|
+
const name = stringProperty(config, 'name');
|
|
173
|
+
const description = stringProperty(config, 'description');
|
|
174
|
+
const tags = stringArrayProperty(config, 'tags');
|
|
175
|
+
if (name !== undefined) {
|
|
176
|
+
feature.name = name;
|
|
177
|
+
}
|
|
178
|
+
if (description !== undefined) {
|
|
179
|
+
feature.description = description;
|
|
180
|
+
}
|
|
181
|
+
if (tags !== undefined) {
|
|
182
|
+
feature.tags = tags;
|
|
183
|
+
}
|
|
184
|
+
feature.hasBefore = hasProperty(config, 'before');
|
|
185
|
+
feature.hasAfter = hasProperty(config, 'after');
|
|
186
|
+
const scenarios = getProperty(config, 'scenarios');
|
|
187
|
+
if (scenarios && ts.isArrayLiteralExpression(scenarios)) {
|
|
188
|
+
for (const element of scenarios.elements) {
|
|
189
|
+
const entry = readEntry(element, checker);
|
|
190
|
+
if (entry) {
|
|
191
|
+
feature.entries.push(entry);
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
feature.unresolvedEntries += 1;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
state.workflows.featureFiles.set(exportedName, feature);
|
|
200
|
+
};
|
|
@@ -273,12 +273,18 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
273
273
|
return;
|
|
274
274
|
}
|
|
275
275
|
// Match identifiers that contain both "pikku" and "func" (case insensitive),
|
|
276
|
-
// plus pikkuScenario (
|
|
276
|
+
// plus pikkuScenario and pikkuScenarioStep (workflow wrappers without "func"
|
|
277
|
+
// in their names)
|
|
277
278
|
const pikkuFuncPattern = /pikku.*func/i;
|
|
278
279
|
if (!pikkuFuncPattern.test(expression.text) &&
|
|
279
|
-
expression.text !== 'pikkuScenario'
|
|
280
|
+
expression.text !== 'pikkuScenario' &&
|
|
281
|
+
expression.text !== 'pikkuScenarioStep') {
|
|
280
282
|
return;
|
|
281
283
|
}
|
|
284
|
+
// A scenario step is bundled like any invoked function, but is deliberately
|
|
285
|
+
// never RPC-registered: a step may drive a browser, so exposing it over the
|
|
286
|
+
// network would put a test harness on the public API surface.
|
|
287
|
+
const isScenarioStep = expression.text === 'pikkuScenarioStep';
|
|
282
288
|
// only handle calls like pikkuFunc(...)
|
|
283
289
|
if (!ts.isIdentifier(expression) || !expression.text.startsWith('pikku')) {
|
|
284
290
|
return;
|
|
@@ -304,6 +310,8 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
304
310
|
let workflowQueued;
|
|
305
311
|
let workflowRetries;
|
|
306
312
|
let workflowTimeout;
|
|
313
|
+
let scenarioStepBrowser;
|
|
314
|
+
let scenarioStepTemplate;
|
|
307
315
|
let scopes;
|
|
308
316
|
let version;
|
|
309
317
|
let objectNode;
|
|
@@ -375,6 +383,8 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
375
383
|
workflowQueued = getPropertyValue(firstArg, 'workflowQueued');
|
|
376
384
|
workflowRetries = getPropertyValue(firstArg, 'workflowRetries');
|
|
377
385
|
workflowTimeout = getPropertyValue(firstArg, 'workflowTimeout');
|
|
386
|
+
scenarioStepBrowser = getPropertyValue(firstArg, 'browser');
|
|
387
|
+
scenarioStepTemplate = getPropertyValue(firstArg, 'template');
|
|
378
388
|
scopes = getArrayPropertyValue(firstArg, 'scopes') ?? undefined;
|
|
379
389
|
// Extract approvalDescription identifier reference
|
|
380
390
|
for (const prop of firstArg.properties) {
|
|
@@ -773,6 +783,7 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
773
783
|
outputs: outputNames.filter((n) => n !== 'void') ?? null,
|
|
774
784
|
expose: expose || undefined,
|
|
775
785
|
remote: remote || undefined,
|
|
786
|
+
scenarioStep: isScenarioStep || undefined,
|
|
776
787
|
mcp: mcpEnabled || undefined,
|
|
777
788
|
readonly: readonly_ || undefined,
|
|
778
789
|
deploy: deploy || undefined,
|
|
@@ -781,6 +792,8 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
781
792
|
workflowQueued: workflowQueued === true ? true : undefined,
|
|
782
793
|
workflowRetries: workflowRetries ?? undefined,
|
|
783
794
|
workflowTimeout: workflowTimeout ?? undefined,
|
|
795
|
+
scenarioStepBrowser: scenarioStepBrowser === true ? true : undefined,
|
|
796
|
+
scenarioStepTemplate: scenarioStepTemplate || undefined,
|
|
784
797
|
scopes: scopes ?? undefined,
|
|
785
798
|
implementationHash,
|
|
786
799
|
version,
|
|
@@ -863,7 +876,7 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
863
876
|
logger.error(`• Function with explicit name '${name}' is not exported, this is not allowed.`);
|
|
864
877
|
return;
|
|
865
878
|
}
|
|
866
|
-
if (remote) {
|
|
879
|
+
if (remote && !isScenarioStep) {
|
|
867
880
|
state.rpc.invokedFunctions.add(pikkuFuncId);
|
|
868
881
|
// The consumer-facing surface a wireRemoteAddon imports (mirrors exposedMeta)
|
|
869
882
|
state.rpc.remoteMeta[name] = pikkuFuncId;
|
|
@@ -872,7 +885,7 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
872
885
|
exportedName,
|
|
873
886
|
});
|
|
874
887
|
}
|
|
875
|
-
if (expose) {
|
|
888
|
+
if (expose && !isScenarioStep) {
|
|
876
889
|
state.rpc.exposedMeta[name] = pikkuFuncId;
|
|
877
890
|
state.rpc.exposedFiles.set(name, {
|
|
878
891
|
path: node.getSourceFile().fileName,
|
|
@@ -881,11 +894,13 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
881
894
|
// Track exposed RPC function for service aggregation
|
|
882
895
|
state.serviceAggregation.usedFunctions.add(pikkuFuncId);
|
|
883
896
|
}
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
897
|
+
if (!isScenarioStep) {
|
|
898
|
+
// We add it to internal meta to allow autocomplete for everything
|
|
899
|
+
state.rpc.internalMeta[name] = pikkuFuncId;
|
|
900
|
+
if (version !== undefined) {
|
|
901
|
+
state.rpc.internalMeta[pikkuFuncId] = pikkuFuncId;
|
|
902
|
+
state.rpc.invokedFunctions.add(pikkuFuncId);
|
|
903
|
+
}
|
|
889
904
|
}
|
|
890
905
|
// But we only import the actual function if it's actually invoked to keep
|
|
891
906
|
// bundle size down
|
package/dist/add/add-workflow.js
CHANGED
|
@@ -86,6 +86,11 @@ export function collectInvokedRPCs(steps, rpcs) {
|
|
|
86
86
|
rpcs.add(step.options.onError);
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
|
+
else if (step.type === 'scenarioStep' && step.stepFunc) {
|
|
90
|
+
// Steps are bundled like RPCs — the runner calls them through
|
|
91
|
+
// runPikkuFunc — but they are deliberately never RPC-registered.
|
|
92
|
+
rpcs.add(step.stepFunc);
|
|
93
|
+
}
|
|
89
94
|
else if (step.type === 'branch') {
|
|
90
95
|
for (const branch of step.branches) {
|
|
91
96
|
collectInvokedRPCs(branch.steps, rpcs);
|
|
@@ -247,6 +252,7 @@ export const addWorkflow = (logger, node, checker, state) => {
|
|
|
247
252
|
let description;
|
|
248
253
|
let errors;
|
|
249
254
|
let expose;
|
|
255
|
+
let skip;
|
|
250
256
|
if (ts.isObjectLiteralExpression(firstArg)) {
|
|
251
257
|
const metadata = getCommonWireMetaData(firstArg, 'Workflow', workflowName, logger, checker);
|
|
252
258
|
if (metadata.disabled)
|
|
@@ -257,6 +263,7 @@ export const addWorkflow = (logger, node, checker, state) => {
|
|
|
257
263
|
description = metadata.description;
|
|
258
264
|
errors = metadata.errors;
|
|
259
265
|
expose = getPropertyValue(firstArg, 'expose');
|
|
266
|
+
skip = getPropertyValue(firstArg, 'skip') ?? undefined;
|
|
260
267
|
}
|
|
261
268
|
// Validate that we got a valid function
|
|
262
269
|
if (ts.isObjectLiteralExpression(firstArg) &&
|
|
@@ -324,6 +331,13 @@ export const addWorkflow = (logger, node, checker, state) => {
|
|
|
324
331
|
logger.critical(ErrorCode.INVALID_DSL_WORKFLOW, `Workflow '${workflowName}' does not conform to DSL workflow rules:\n${result.reason || 'Unknown error'}`);
|
|
325
332
|
return;
|
|
326
333
|
}
|
|
334
|
+
else if (wrapperType === 'scenario') {
|
|
335
|
+
// The fallback below understands do/sleep but not step/given/when/then,
|
|
336
|
+
// so a scenario that lands here registers with zero steps. Silence would
|
|
337
|
+
// leave the whole scenario passing vacuously.
|
|
338
|
+
logger.critical(ErrorCode.SCENARIO_NOT_EXTRACTABLE, `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'}`);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
327
341
|
else {
|
|
328
342
|
// For pikkuWorkflowComplexFunc, fall back to basic extraction
|
|
329
343
|
logger.debug(`Workflow '${workflowName}' could not be extracted as DSL workflow: ${result.reason || 'Unknown error'}. Falling back to basic extraction.`);
|
|
@@ -358,36 +372,15 @@ export const addWorkflow = (logger, node, checker, state) => {
|
|
|
358
372
|
errors,
|
|
359
373
|
tags,
|
|
360
374
|
expose,
|
|
375
|
+
skip,
|
|
361
376
|
scenario: wrapperType === 'scenario' ? true : undefined,
|
|
362
377
|
actors: actorNames.length > 0 ? actorNames : undefined,
|
|
363
378
|
};
|
|
364
|
-
//
|
|
365
|
-
//
|
|
366
|
-
//
|
|
367
|
-
//
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
if (disallowed.length > 0) {
|
|
372
|
-
logger.critical(ErrorCode.SCENARIO_HAS_SERVICES, `Scenario '${workflowName}' destructures services: ${disallowed.join(', ')}. ` +
|
|
373
|
-
`Scenarios may only use 'logger'/'config' — drive everything else through ` +
|
|
374
|
-
`actor steps (workflow.do(step, rpc, data, { actor: actors.x })) so the flow ` +
|
|
375
|
-
`runs against the target environment.`);
|
|
376
|
-
return;
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
// Workflow functions require platform services that aren't visible
|
|
380
|
-
// through parameter destructuring (they're accessed via workflow.do/sleep)
|
|
381
|
-
if (funcMeta?.services) {
|
|
382
|
-
for (const svc of [
|
|
383
|
-
'workflowService',
|
|
384
|
-
'workflowRunService',
|
|
385
|
-
'schedulerService',
|
|
386
|
-
'queueService',
|
|
387
|
-
]) {
|
|
388
|
-
if (!funcMeta.services.services.includes(svc)) {
|
|
389
|
-
funcMeta.services.services.push(svc);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
}
|
|
379
|
+
// The scenario "logger/config only" rule (PKU673) and the implicit
|
|
380
|
+
// workflow platform-service injection both used to live here, reading
|
|
381
|
+
// state.functions.meta[pikkuFuncId]. addWorkflow runs in the visitSetup
|
|
382
|
+
// pass, which completes before addFunctions populates that meta, so both
|
|
383
|
+
// were dead. The rule now lives in validateScenarioServices (post-process),
|
|
384
|
+
// and aggregateRequiredServices already adds the platform services globally
|
|
385
|
+
// whenever any workflow exists.
|
|
393
386
|
};
|
package/dist/error-codes.d.ts
CHANGED
|
@@ -20,6 +20,9 @@ export declare enum ErrorCode {
|
|
|
20
20
|
CLI_CLIENTSIDE_RENDERER_HAS_SERVICES = "PKU672",
|
|
21
21
|
SCENARIO_HAS_SERVICES = "PKU673",
|
|
22
22
|
EXPECT_EVENTUALLY_SCENARIO_ONLY = "PKU675",
|
|
23
|
+
SCENARIO_BROWSER_STEP_NEEDS_ACTOR = "PKU677",
|
|
24
|
+
SCENARIO_STEP_TARGET_NOT_STATIC = "PKU678",
|
|
25
|
+
SCENARIO_NOT_EXTRACTABLE = "PKU679",
|
|
23
26
|
WORKFLOW_ORCHESTRATOR_NOT_CONFIGURED = "PKU600",
|
|
24
27
|
INVALID_DSL_WORKFLOW = "PKU641",
|
|
25
28
|
WORKFLOW_GRAPH_ADDON_NOT_WIRED = "PKU642",
|
package/dist/error-codes.js
CHANGED
|
@@ -22,6 +22,9 @@ export var ErrorCode;
|
|
|
22
22
|
ErrorCode["CLI_CLIENTSIDE_RENDERER_HAS_SERVICES"] = "PKU672";
|
|
23
23
|
ErrorCode["SCENARIO_HAS_SERVICES"] = "PKU673";
|
|
24
24
|
ErrorCode["EXPECT_EVENTUALLY_SCENARIO_ONLY"] = "PKU675";
|
|
25
|
+
ErrorCode["SCENARIO_BROWSER_STEP_NEEDS_ACTOR"] = "PKU677";
|
|
26
|
+
ErrorCode["SCENARIO_STEP_TARGET_NOT_STATIC"] = "PKU678";
|
|
27
|
+
ErrorCode["SCENARIO_NOT_EXTRACTABLE"] = "PKU679";
|
|
25
28
|
ErrorCode["WORKFLOW_ORCHESTRATOR_NOT_CONFIGURED"] = "PKU600";
|
|
26
29
|
ErrorCode["INVALID_DSL_WORKFLOW"] = "PKU641";
|
|
27
30
|
ErrorCode["WORKFLOW_GRAPH_ADDON_NOT_WIRED"] = "PKU642";
|