@pikku/inspector 0.12.48 → 0.12.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/LICENSE +106 -0
- package/dist/add/add-functions.js +14 -0
- package/dist/add/scenario-instrumentation.d.ts +24 -0
- package/dist/add/scenario-instrumentation.js +29 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/utils/post-process.js +7 -0
- package/package.json +3 -3
- package/src/add/add-functions.ts +18 -0
- package/src/add/scenario-instrumentation.test.ts +89 -0
- package/src/add/scenario-instrumentation.ts +36 -0
- package/src/add/scenario-marker.test.ts +89 -0
- package/src/index.ts +5 -0
- package/src/utils/post-process.test.ts +30 -1
- package/src/utils/post-process.ts +6 -0
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,36 @@
|
|
|
1
|
+
## 0.12.49
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- 426610a: Scenario instrumentation is no longer scaffolded into projects, and no longer deploys.
|
|
6
|
+
|
|
7
|
+
`scaffold.scenarios` generated four functions — `pikkuScenarioTakeLiveCoverage`, `pikkuScenarioResetLiveCoverage`, `pikkuScenarioResetStubs`, `pikkuScenarioGetStubCalls` — into the project's own source. As project source they were indistinguishable from application code: registered in the app bootstrap, listed in the app's function and RPC meta, and shipped `expose: true` inside every deployed bundle. Coverage and stub inspection are things you do to a development server; production carried two endpoints that fingerprint the build and one that resets a global tracker, gated only by whether a metadata file happened to sit beside the bundle.
|
|
8
|
+
|
|
9
|
+
`pikku dev` now registers the implementations itself, after the app bootstrap. Nothing is generated, nothing is written to the project, and a bundle cannot carry what was never in its bootstrap — `pikku serve` and every deployed unit have no trace of them. The scenario runner reaches them over `/rpc/<name>` exactly as before.
|
|
10
|
+
|
|
11
|
+
Also:
|
|
12
|
+
- The inspector ignores these four names wherever it finds them, so a project that has not regenerated — and still has the scaffolded file checked in — stops deploying it immediately. Codegen deletes the retired scaffold on its next run.
|
|
13
|
+
- They no longer count towards a project's function total, so `pikku scenario --coverage` stops reporting four permanently-uncovered functions that were never the project's to cover.
|
|
14
|
+
- The instrumentation no longer carries schemas (there was nothing to validate but one optional string), which drops the `zod` dependency the scaffold silently required of every project that enabled it.
|
|
15
|
+
- They are registered sessionless, so `scaffold.scenarios: true` — as opposed to `'auth'` — now genuinely means "no session required". As a sessioned `pikkuFunc` with `auth: false`, it demanded a session anyway and logged a warning saying so.
|
|
16
|
+
|
|
17
|
+
- 09973b9: Scenarios, features and steps no longer reach a deployment.
|
|
18
|
+
|
|
19
|
+
Steps were already held back from the app bootstrap, so a deployed server never imported a step body. Everything _about_ a scenario still travelled with the application: a `pikkuScenario(...)` is a function, so its name, schemas and hashes sat in the app function meta; the schemas it and its steps validate against sat in the app's `register.gen.ts` — on one project 458 of the 582 registered schemas belonged to tests; its name sat in the internal RPC meta; and because a scenario is _also_ a workflow, the inspector synthesised a `wf-orchestrator-<scenario>` queue worker for each one. The deploy analyzer, which reads inspector state rather than the partitioned codegen output, then read all of it back as application code: a unit per scenario, a `WorkflowDefinition` per scenario, and a real queue per scenario. A 13-scenario suite turned into 13 production queues named after tests, waiting for a provider to create them.
|
|
20
|
+
|
|
21
|
+
The existing scenario/app partition is now applied everywhere it was missing. `FunctionRuntimeMeta` gains a `scenario` marker (the counterpart of `scenarioStep`) so a scenario body is recognisable without walking the workflow graph; scenario bodies join their steps on the scenario side of the function-meta and registration split; schemas only a scenario or step needs are written and registered under `.pikku/scenarios/schemas/` and imported by the scenario bootstrap alone; scenario names are dropped from the internal RPC meta; no orchestrator queue worker is synthesised for a scenario; and the deploy analyzer drops both scenario functions and scenario workflows before it decides what a deployment contains.
|
|
22
|
+
|
|
23
|
+
The MCP metas are keyed by wiring rather than by function, so a scenario wired as an MCP tool, resource or prompt was the one id that still reached the manifest after the function and workflow filters — as an endpoint on the gateway plus a gateway dependency on a unit that was never emitted. Those ids are now filtered too.
|
|
24
|
+
|
|
25
|
+
`scenarioSchemaDirectory` is rejected when it resolves to the same directory as `schemaDirectory`. A schema write owns its directory — it emits `register.gen.ts` and prunes every schema file its own required-set does not name — so sharing one would replace the application register with the scenario-only one and delete the app's schema files, which nothing downstream can detect.
|
|
26
|
+
|
|
27
|
+
Nothing changes for `pikku scenario run` — the scenario bootstrap still registers every scenario, feature, step, meta and schema. What changes is that a bundle stops carrying them.
|
|
28
|
+
|
|
29
|
+
- Updated dependencies [8a2c993]
|
|
30
|
+
- Updated dependencies [a261006]
|
|
31
|
+
- Updated dependencies [09973b9]
|
|
32
|
+
- @pikku/core@0.12.71
|
|
33
|
+
|
|
1
34
|
## 0.12.48
|
|
2
35
|
|
|
3
36
|
### Patch Changes
|
package/LICENSE
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
Business Source License 1.1
|
|
2
|
+
|
|
3
|
+
Parameters
|
|
4
|
+
|
|
5
|
+
Licensor: Yasser Fadl and Pikku contributors
|
|
6
|
+
Licensed Work: TypeScript inspector for Pikku — discovers functions and wirings to drive code generation (@pikku/inspector)
|
|
7
|
+
Pikku Console (@pikku/console)
|
|
8
|
+
The Licensed Work is (c) 2021 - present Yasser Fadl and Pikku contributors
|
|
9
|
+
Additional Use Grant: You may make production use of the Licensed Work for any purpose,
|
|
10
|
+
including creating free or open source software. However, you may
|
|
11
|
+
not offer as a commercial product or service any software featuring
|
|
12
|
+
a visual or graphical interface for authoring, editing, building,
|
|
13
|
+
or managing Pikku code, Pikku functions, Pikku handlers, or Pikku
|
|
14
|
+
workflows.
|
|
15
|
+
Change Date: Four years from the date the Licensed Work was first made available
|
|
16
|
+
Change License: MIT License
|
|
17
|
+
|
|
18
|
+
For information about alternative licensing arrangements, contact yasser.fadl@gmail.com
|
|
19
|
+
|
|
20
|
+
Notice
|
|
21
|
+
|
|
22
|
+
The Business Source License (this document, or the "License") is not an Open Source
|
|
23
|
+
license. However, the Licensed Work will eventually be made available under an Open
|
|
24
|
+
Source License, as stated in this License.
|
|
25
|
+
|
|
26
|
+
License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
|
|
27
|
+
"Business Source License" is a trademark of MariaDB Corporation Ab.
|
|
28
|
+
|
|
29
|
+
-----------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
Business Source License 1.1
|
|
32
|
+
|
|
33
|
+
Terms
|
|
34
|
+
|
|
35
|
+
The Licensor hereby grants you the right to copy, modify, create derivative works,
|
|
36
|
+
redistribute, and make non-production use of the Licensed Work. The Licensor may make
|
|
37
|
+
an Additional Use Grant, above, permitting limited production use.
|
|
38
|
+
|
|
39
|
+
Effective on the Change Date, or the fourth anniversary of the first publicly
|
|
40
|
+
available distribution of a specific version of the Licensed Work under this License,
|
|
41
|
+
whichever comes first, the Licensor hereby grants you rights under the terms of the
|
|
42
|
+
Change License, and the rights granted in the paragraph above terminate.
|
|
43
|
+
|
|
44
|
+
If your use of the Licensed Work does not comply with the requirements currently in
|
|
45
|
+
effect as described in this License, you must purchase a commercial license from the
|
|
46
|
+
Licensor, its affiliated entities, or authorized resellers, or you must refrain from
|
|
47
|
+
using the Licensed Work.
|
|
48
|
+
|
|
49
|
+
All copies of the original and modified Licensed Work, and derivative works of the
|
|
50
|
+
Licensed Work, are subject to this License. This License applies separately for each
|
|
51
|
+
version of the Licensed Work and the Change Date may vary for each version of the
|
|
52
|
+
Licensed Work released by Licensor.
|
|
53
|
+
|
|
54
|
+
You must conspicuously display this License on each original or modified copy of the
|
|
55
|
+
Licensed Work. If you receive the Licensed Work in original or modified form from a
|
|
56
|
+
third party, the terms and conditions set forth in this License apply to your use of
|
|
57
|
+
that work.
|
|
58
|
+
|
|
59
|
+
Any use of the Licensed Work in violation of this License will automatically terminate
|
|
60
|
+
your rights under this License for the current and all other versions of the Licensed
|
|
61
|
+
Work.
|
|
62
|
+
|
|
63
|
+
This License does not grant you any right in any trademark or logo of Licensor or its
|
|
64
|
+
affiliates (provided that you may use a trademark or logo of Licensor as expressly
|
|
65
|
+
required by this License).
|
|
66
|
+
|
|
67
|
+
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON AN "AS IS"
|
|
68
|
+
BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, EXPRESS OR IMPLIED,
|
|
69
|
+
INCLUDING (WITHOUT LIMITATION) WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
|
70
|
+
PURPOSE, NON-INFRINGEMENT, AND TITLE.
|
|
71
|
+
|
|
72
|
+
MariaDB hereby grants you permission to use this License's text to license your works,
|
|
73
|
+
and to refer to it using the trademark "Business Source License", as long as you
|
|
74
|
+
comply with the Covenants of Licensor below.
|
|
75
|
+
|
|
76
|
+
Covenants of Licensor
|
|
77
|
+
|
|
78
|
+
In consideration of the right to use this License's text and the "Business Source
|
|
79
|
+
License" name and trademark, Licensor covenants to MariaDB, and to all other
|
|
80
|
+
recipients of the licensed work to be provided by Licensor:
|
|
81
|
+
|
|
82
|
+
1. To specify as the Change License the GPL Version 2.0 or any later version, or a
|
|
83
|
+
license that is compatible with GPL Version 2.0 or a later version, where
|
|
84
|
+
"compatible" means that software provided under the Change License can be included
|
|
85
|
+
in a program with software provided under GPL Version 2.0 or a later version.
|
|
86
|
+
Licensor may specify additional Change Licenses without limitation.
|
|
87
|
+
|
|
88
|
+
2. To either: (a) specify an additional grant of rights to use that does not impose
|
|
89
|
+
any additional restriction on the right granted in this License, as the Additional
|
|
90
|
+
Use Grant; or (b) insert the text "None".
|
|
91
|
+
|
|
92
|
+
3. To specify a Change Date.
|
|
93
|
+
|
|
94
|
+
4. Not to modify this License in any other way.
|
|
95
|
+
|
|
96
|
+
-----------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
Note: Every other Pikku package — @pikku/core, the runtimes, the services, the clients,
|
|
99
|
+
and @pikku/skills — is licensed under the MIT License. Each package ships its own LICENSE
|
|
100
|
+
file stating its terms; that file governs, and this one covers only the Licensed Work
|
|
101
|
+
named above.
|
|
102
|
+
|
|
103
|
+
In particular, the Pikku agent skills (@pikku/skills) are MIT. They are the instruction
|
|
104
|
+
set coding agents read to build, wire and deploy Pikku projects, and they are meant to be
|
|
105
|
+
adopted, forked and extended by any harness — so their terms do not depend on the CLI
|
|
106
|
+
that happens to install them.
|
|
@@ -11,6 +11,7 @@ import { resolvePermissions } from '../utils/permissions.js';
|
|
|
11
11
|
import { extractWireNames } from '../utils/post-process.js';
|
|
12
12
|
import { ErrorCode } from '../error-codes.js';
|
|
13
13
|
import { findPiiPaths } from '../utils/check-pii-output.js';
|
|
14
|
+
import { isScenarioInstrumentationFunction } from './scenario-instrumentation.js';
|
|
14
15
|
const isValidVariableName = (name) => {
|
|
15
16
|
const regex = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
|
16
17
|
return regex.test(name);
|
|
@@ -285,6 +286,10 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
285
286
|
// never RPC-registered: a step may drive a browser, so exposing it over the
|
|
286
287
|
// network would put a test harness on the public API surface.
|
|
287
288
|
const isScenarioStep = expression.text === 'pikkuScenarioStep';
|
|
289
|
+
// A scenario's own body. Marked so the codegen partitions and the deploy
|
|
290
|
+
// analyzer can tell it apart from an application workflow without having to
|
|
291
|
+
// reach into the workflow graph.
|
|
292
|
+
const isScenario = expression.text === 'pikkuScenario';
|
|
288
293
|
// only handle calls like pikkuFunc(...)
|
|
289
294
|
if (!ts.isIdentifier(expression) || !expression.text.startsWith('pikku')) {
|
|
290
295
|
return;
|
|
@@ -295,6 +300,14 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
295
300
|
if (!pikkuFuncId || pikkuFuncId.startsWith('__temp_')) {
|
|
296
301
|
return;
|
|
297
302
|
}
|
|
303
|
+
// pikku's own scenario instrumentation, which older projects still carry as a
|
|
304
|
+
// scaffolded file in their source. It is not application code and must not be
|
|
305
|
+
// registered, metadata'd or deployed as if it were — `pikku dev` registers the
|
|
306
|
+
// implementations itself, and that is the only server that should have them.
|
|
307
|
+
if (isScenarioInstrumentationFunction(pikkuFuncId) ||
|
|
308
|
+
isScenarioInstrumentationFunction(name)) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
298
311
|
let title;
|
|
299
312
|
let tags;
|
|
300
313
|
let summary;
|
|
@@ -784,6 +797,7 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
784
797
|
expose: expose || undefined,
|
|
785
798
|
remote: remote || undefined,
|
|
786
799
|
scenarioStep: isScenarioStep || undefined,
|
|
800
|
+
scenario: isScenario || undefined,
|
|
787
801
|
mcp: mcpEnabled || undefined,
|
|
788
802
|
readonly: readonly_ || undefined,
|
|
789
803
|
deploy: deploy || undefined,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The instrumentation RPCs `pikku scenario run` calls on the server it is
|
|
3
|
+
* testing: snapshot/reset live coverage, and reset/read recorded stub calls.
|
|
4
|
+
*
|
|
5
|
+
* They are pikku's own, not the application's. They used to be scaffolded into
|
|
6
|
+
* the project as ordinary `pikkuFunc`s, which made them indistinguishable from
|
|
7
|
+
* application code — so they were registered in the app bootstrap, listed in the
|
|
8
|
+
* app's function and RPC meta, and shipped, `expose: true`, in every deployed
|
|
9
|
+
* bundle. A deployment has no use for them: coverage and stub inspection are
|
|
10
|
+
* things you do to a development server.
|
|
11
|
+
*
|
|
12
|
+
* The inspector therefore ignores these names wherever it finds them, and
|
|
13
|
+
* `pikku dev` registers the implementations itself. Ignoring by name (rather
|
|
14
|
+
* than deleting the scaffold and requiring every project to regenerate) is what
|
|
15
|
+
* keeps an existing project's checked-in `scenarios.gen.ts` from deploying.
|
|
16
|
+
*/
|
|
17
|
+
export declare const SCENARIO_INSTRUMENTATION_FUNCTIONS: readonly ["pikkuScenarioTakeLiveCoverage", "pikkuScenarioResetLiveCoverage", "pikkuScenarioResetStubs", "pikkuScenarioGetStubCalls"];
|
|
18
|
+
export type ScenarioInstrumentationFunction = (typeof SCENARIO_INSTRUMENTATION_FUNCTIONS)[number];
|
|
19
|
+
/**
|
|
20
|
+
* True for a scenario instrumentation function, by name. Versioned ids
|
|
21
|
+
* (`name@v2`) are matched on their base so a project that bumps one is still
|
|
22
|
+
* covered.
|
|
23
|
+
*/
|
|
24
|
+
export declare const isScenarioInstrumentationFunction: (name: string | undefined) => boolean;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The instrumentation RPCs `pikku scenario run` calls on the server it is
|
|
3
|
+
* testing: snapshot/reset live coverage, and reset/read recorded stub calls.
|
|
4
|
+
*
|
|
5
|
+
* They are pikku's own, not the application's. They used to be scaffolded into
|
|
6
|
+
* the project as ordinary `pikkuFunc`s, which made them indistinguishable from
|
|
7
|
+
* application code — so they were registered in the app bootstrap, listed in the
|
|
8
|
+
* app's function and RPC meta, and shipped, `expose: true`, in every deployed
|
|
9
|
+
* bundle. A deployment has no use for them: coverage and stub inspection are
|
|
10
|
+
* things you do to a development server.
|
|
11
|
+
*
|
|
12
|
+
* The inspector therefore ignores these names wherever it finds them, and
|
|
13
|
+
* `pikku dev` registers the implementations itself. Ignoring by name (rather
|
|
14
|
+
* than deleting the scaffold and requiring every project to regenerate) is what
|
|
15
|
+
* keeps an existing project's checked-in `scenarios.gen.ts` from deploying.
|
|
16
|
+
*/
|
|
17
|
+
export const SCENARIO_INSTRUMENTATION_FUNCTIONS = [
|
|
18
|
+
'pikkuScenarioTakeLiveCoverage',
|
|
19
|
+
'pikkuScenarioResetLiveCoverage',
|
|
20
|
+
'pikkuScenarioResetStubs',
|
|
21
|
+
'pikkuScenarioGetStubCalls',
|
|
22
|
+
];
|
|
23
|
+
const names = new Set(SCENARIO_INSTRUMENTATION_FUNCTIONS);
|
|
24
|
+
/**
|
|
25
|
+
* True for a scenario instrumentation function, by name. Versioned ids
|
|
26
|
+
* (`name@v2`) are matched on their base so a project that bumps one is still
|
|
27
|
+
* covered.
|
|
28
|
+
*/
|
|
29
|
+
export const isScenarioInstrumentationFunction = (name) => (name ? names.has(name.split('@')[0]) : false);
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export type { FilesAndMethodsErrors } from './utils/get-files-and-methods.js';
|
|
|
5
5
|
export { ErrorCode } from './error-codes.js';
|
|
6
6
|
export type { DiagnosticSeverity, CodedDiagnostic } from './error-codes.js';
|
|
7
7
|
export { AUTH_HANDLER_FUNC_ID } from './add/add-auth.js';
|
|
8
|
+
export { SCENARIO_INSTRUMENTATION_FUNCTIONS, isScenarioInstrumentationFunction, } from './add/scenario-instrumentation.js';
|
|
9
|
+
export type { ScenarioInstrumentationFunction } from './add/scenario-instrumentation.js';
|
|
8
10
|
export { serializeInspectorState, deserializeInspectorState, } from './utils/serialize-inspector-state.js';
|
|
9
11
|
export type { SerializableInspectorState } from './utils/serialize-inspector-state.js';
|
|
10
12
|
export { filterInspectorState } from './utils/filter-inspector-state.js';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { inspect, getInitialInspectorState } from './inspector.js';
|
|
2
2
|
export { ErrorCode } from './error-codes.js';
|
|
3
3
|
export { AUTH_HANDLER_FUNC_ID } from './add/add-auth.js';
|
|
4
|
+
export { SCENARIO_INSTRUMENTATION_FUNCTIONS, isScenarioInstrumentationFunction, } from './add/scenario-instrumentation.js';
|
|
4
5
|
export { serializeInspectorState, deserializeInspectorState, } from './utils/serialize-inspector-state.js';
|
|
5
6
|
export { filterInspectorState } from './utils/filter-inspector-state.js';
|
|
6
7
|
export { resolveDeployTarget } from './utils/resolve-deploy-target.js';
|
|
@@ -212,6 +212,13 @@ export function aggregateRequiredServices(state) {
|
|
|
212
212
|
for (const [, graph] of Object.entries(state.workflows.graphMeta)) {
|
|
213
213
|
if (!graph.nodes || !graph.name)
|
|
214
214
|
continue;
|
|
215
|
+
// A scenario is a workflow, but it only ever runs in-process under
|
|
216
|
+
// `pikku scenario run` — no step of one is dispatched through a queue. The
|
|
217
|
+
// synthetic entries were pure leakage: they put a
|
|
218
|
+
// `wf-orchestrator-<scenario>` worker into the app's queue meta, which every
|
|
219
|
+
// bundle imports and a provider then creates as a real production queue.
|
|
220
|
+
if (graph.source === 'scenario')
|
|
221
|
+
continue;
|
|
215
222
|
const toKebab = (s) => s
|
|
216
223
|
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
217
224
|
.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/inspector",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.49",
|
|
4
4
|
"description": "TypeScript inspector for Pikku — discovers functions and wirings to drive code generation",
|
|
5
5
|
"author": "yasser.fadl@gmail.com",
|
|
6
6
|
"license": "BUSL-1.1",
|
|
@@ -45,10 +45,10 @@
|
|
|
45
45
|
"zod-to-ts": "^2.1.0"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
|
-
"@pikku/core": "^0.12.
|
|
48
|
+
"@pikku/core": "^0.12.71"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@pikku/core": "^0.12.
|
|
51
|
+
"@pikku/core": "^0.12.71",
|
|
52
52
|
"@types/node": "^24.11.0"
|
|
53
53
|
},
|
|
54
54
|
"engines": {
|
package/src/add/add-functions.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { resolvePermissions } from '../utils/permissions.js'
|
|
|
21
21
|
import { extractWireNames } from '../utils/post-process.js'
|
|
22
22
|
import { ErrorCode } from '../error-codes.js'
|
|
23
23
|
import { findPiiPaths } from '../utils/check-pii-output.js'
|
|
24
|
+
import { isScenarioInstrumentationFunction } from './scenario-instrumentation.js'
|
|
24
25
|
import type { NodeType } from '@pikku/core/node'
|
|
25
26
|
|
|
26
27
|
const isValidVariableName = (name: string) => {
|
|
@@ -402,6 +403,11 @@ export const addFunctions: AddWiring = (
|
|
|
402
403
|
// network would put a test harness on the public API surface.
|
|
403
404
|
const isScenarioStep = expression.text === 'pikkuScenarioStep'
|
|
404
405
|
|
|
406
|
+
// A scenario's own body. Marked so the codegen partitions and the deploy
|
|
407
|
+
// analyzer can tell it apart from an application workflow without having to
|
|
408
|
+
// reach into the workflow graph.
|
|
409
|
+
const isScenario = expression.text === 'pikkuScenario'
|
|
410
|
+
|
|
405
411
|
// only handle calls like pikkuFunc(...)
|
|
406
412
|
if (!ts.isIdentifier(expression) || !expression.text.startsWith('pikku')) {
|
|
407
413
|
return
|
|
@@ -419,6 +425,17 @@ export const addFunctions: AddWiring = (
|
|
|
419
425
|
return
|
|
420
426
|
}
|
|
421
427
|
|
|
428
|
+
// pikku's own scenario instrumentation, which older projects still carry as a
|
|
429
|
+
// scaffolded file in their source. It is not application code and must not be
|
|
430
|
+
// registered, metadata'd or deployed as if it were — `pikku dev` registers the
|
|
431
|
+
// implementations itself, and that is the only server that should have them.
|
|
432
|
+
if (
|
|
433
|
+
isScenarioInstrumentationFunction(pikkuFuncId) ||
|
|
434
|
+
isScenarioInstrumentationFunction(name)
|
|
435
|
+
) {
|
|
436
|
+
return
|
|
437
|
+
}
|
|
438
|
+
|
|
422
439
|
let title: string | undefined
|
|
423
440
|
let tags: string[] | undefined
|
|
424
441
|
let summary: string | undefined
|
|
@@ -1094,6 +1111,7 @@ export const addFunctions: AddWiring = (
|
|
|
1094
1111
|
expose: expose || undefined,
|
|
1095
1112
|
remote: remote || undefined,
|
|
1096
1113
|
scenarioStep: isScenarioStep || undefined,
|
|
1114
|
+
scenario: isScenario || undefined,
|
|
1097
1115
|
mcp: mcpEnabled || undefined,
|
|
1098
1116
|
readonly: readonly_ || undefined,
|
|
1099
1117
|
deploy: deploy || undefined,
|
|
@@ -0,0 +1,89 @@
|
|
|
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 {
|
|
9
|
+
SCENARIO_INSTRUMENTATION_FUNCTIONS,
|
|
10
|
+
isScenarioInstrumentationFunction,
|
|
11
|
+
} from './scenario-instrumentation.js'
|
|
12
|
+
|
|
13
|
+
const silentLogger: InspectorLogger = {
|
|
14
|
+
debug: () => {},
|
|
15
|
+
info: () => {},
|
|
16
|
+
warn: () => {},
|
|
17
|
+
error: () => {},
|
|
18
|
+
diagnostic: () => {},
|
|
19
|
+
critical: () => {},
|
|
20
|
+
hasCriticalErrors: () => false,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('scenario instrumentation', () => {
|
|
24
|
+
test('recognises the instrumentation names, including versioned ids', () => {
|
|
25
|
+
for (const name of SCENARIO_INSTRUMENTATION_FUNCTIONS) {
|
|
26
|
+
assert.ok(isScenarioInstrumentationFunction(name), name)
|
|
27
|
+
assert.ok(isScenarioInstrumentationFunction(`${name}@v2`), `${name}@v2`)
|
|
28
|
+
}
|
|
29
|
+
assert.ok(!isScenarioInstrumentationFunction('createUser'))
|
|
30
|
+
assert.ok(!isScenarioInstrumentationFunction(undefined))
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('a project still carrying the retired scaffold registers none of it', async () => {
|
|
34
|
+
const rootDir = await mkdtemp(join(tmpdir(), 'pikku-scenario-instr-'))
|
|
35
|
+
const file = join(rootDir, 'scenarios.gen.ts')
|
|
36
|
+
|
|
37
|
+
// What `scaffold.scenarios` used to write into the project. Whatever a
|
|
38
|
+
// project has checked in, none of it may reach the app's meta or RPC surface:
|
|
39
|
+
// `pikku dev` registers the real implementations itself.
|
|
40
|
+
await writeFile(
|
|
41
|
+
file,
|
|
42
|
+
[
|
|
43
|
+
"import { pikkuFunc } from '@pikku/core'",
|
|
44
|
+
'export const pikkuScenarioTakeLiveCoverage = pikkuFunc({',
|
|
45
|
+
" tags: ['pikku'],",
|
|
46
|
+
' expose: true,',
|
|
47
|
+
' auth: true,',
|
|
48
|
+
' func: async () => null,',
|
|
49
|
+
'})',
|
|
50
|
+
'export const pikkuScenarioResetStubs = pikkuFunc({',
|
|
51
|
+
" tags: ['pikku'],",
|
|
52
|
+
' expose: true,',
|
|
53
|
+
' auth: true,',
|
|
54
|
+
' func: async () => ({ enabled: true }),',
|
|
55
|
+
'})',
|
|
56
|
+
'export const createUser = pikkuFunc({',
|
|
57
|
+
' expose: true,',
|
|
58
|
+
' func: async () => ({ ok: true }),',
|
|
59
|
+
'})',
|
|
60
|
+
].join('\n')
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const state = await inspect(silentLogger, [file], { rootDir })
|
|
65
|
+
|
|
66
|
+
for (const name of SCENARIO_INSTRUMENTATION_FUNCTIONS) {
|
|
67
|
+
assert.ok(
|
|
68
|
+
!(name in state.functions.meta),
|
|
69
|
+
`${name} must not be in the app function meta`
|
|
70
|
+
)
|
|
71
|
+
assert.ok(
|
|
72
|
+
!(name in state.rpc.internalMeta),
|
|
73
|
+
`${name} must not be in the internal RPC meta`
|
|
74
|
+
)
|
|
75
|
+
assert.ok(
|
|
76
|
+
!(name in state.rpc.exposedMeta),
|
|
77
|
+
`${name} must not be exposed`
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// The application function in the same file is untouched — the drop is by
|
|
82
|
+
// name, not by file.
|
|
83
|
+
assert.ok(state.functions.meta['createUser'])
|
|
84
|
+
assert.strictEqual(state.rpc.exposedMeta['createUser'], 'createUser')
|
|
85
|
+
} finally {
|
|
86
|
+
await rm(rootDir, { recursive: true, force: true })
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
})
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The instrumentation RPCs `pikku scenario run` calls on the server it is
|
|
3
|
+
* testing: snapshot/reset live coverage, and reset/read recorded stub calls.
|
|
4
|
+
*
|
|
5
|
+
* They are pikku's own, not the application's. They used to be scaffolded into
|
|
6
|
+
* the project as ordinary `pikkuFunc`s, which made them indistinguishable from
|
|
7
|
+
* application code — so they were registered in the app bootstrap, listed in the
|
|
8
|
+
* app's function and RPC meta, and shipped, `expose: true`, in every deployed
|
|
9
|
+
* bundle. A deployment has no use for them: coverage and stub inspection are
|
|
10
|
+
* things you do to a development server.
|
|
11
|
+
*
|
|
12
|
+
* The inspector therefore ignores these names wherever it finds them, and
|
|
13
|
+
* `pikku dev` registers the implementations itself. Ignoring by name (rather
|
|
14
|
+
* than deleting the scaffold and requiring every project to regenerate) is what
|
|
15
|
+
* keeps an existing project's checked-in `scenarios.gen.ts` from deploying.
|
|
16
|
+
*/
|
|
17
|
+
export const SCENARIO_INSTRUMENTATION_FUNCTIONS = [
|
|
18
|
+
'pikkuScenarioTakeLiveCoverage',
|
|
19
|
+
'pikkuScenarioResetLiveCoverage',
|
|
20
|
+
'pikkuScenarioResetStubs',
|
|
21
|
+
'pikkuScenarioGetStubCalls',
|
|
22
|
+
] as const
|
|
23
|
+
|
|
24
|
+
export type ScenarioInstrumentationFunction =
|
|
25
|
+
(typeof SCENARIO_INSTRUMENTATION_FUNCTIONS)[number]
|
|
26
|
+
|
|
27
|
+
const names = new Set<string>(SCENARIO_INSTRUMENTATION_FUNCTIONS)
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* True for a scenario instrumentation function, by name. Versioned ids
|
|
31
|
+
* (`name@v2`) are matched on their base so a project that bumps one is still
|
|
32
|
+
* covered.
|
|
33
|
+
*/
|
|
34
|
+
export const isScenarioInstrumentationFunction = (
|
|
35
|
+
name: string | undefined
|
|
36
|
+
): boolean => (name ? names.has(name.split('@')[0]!) : false)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `scenario` / `scenarioStep` markers in the *function* meta are what every
|
|
3
|
+
* deployment boundary downstream reads: the codegen partition, the RPC meta
|
|
4
|
+
* filter, the schema split and the deploy analyzer. Asserting them on a real
|
|
5
|
+
* `pikkuScenario(...)` is what makes those filters mean anything — filters tested
|
|
6
|
+
* against hand-written meta would keep passing if extraction stopped setting it.
|
|
7
|
+
*/
|
|
8
|
+
import { strict as assert } from 'assert'
|
|
9
|
+
import { describe, test } from 'node:test'
|
|
10
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
11
|
+
import { tmpdir } from 'node:os'
|
|
12
|
+
import { join } from 'node:path'
|
|
13
|
+
import { inspect } from '../inspector.js'
|
|
14
|
+
import type { InspectorLogger } from '../types.js'
|
|
15
|
+
|
|
16
|
+
const logger = {
|
|
17
|
+
debug: () => {},
|
|
18
|
+
info: () => {},
|
|
19
|
+
warn: () => {},
|
|
20
|
+
error: () => {},
|
|
21
|
+
diagnostic: () => {},
|
|
22
|
+
critical: () => {},
|
|
23
|
+
hasCriticalErrors: () => false,
|
|
24
|
+
} as unknown as InspectorLogger
|
|
25
|
+
|
|
26
|
+
const STEPS = [
|
|
27
|
+
"import { pikkuScenarioStep } from '@pikku/core'",
|
|
28
|
+
'export const buysAnApple = pikkuScenarioStep({',
|
|
29
|
+
" name: 'buysAnApple',",
|
|
30
|
+
" description: 'buys an apple',",
|
|
31
|
+
' func: async ({ logger }, data: { qty: number }) => ({ ok: data.qty > 0 }),',
|
|
32
|
+
'})',
|
|
33
|
+
].join('\n')
|
|
34
|
+
|
|
35
|
+
const APP = [
|
|
36
|
+
"import { pikkuFunc } from '@pikku/core'",
|
|
37
|
+
'export const createTodo = pikkuFunc({',
|
|
38
|
+
' expose: true,',
|
|
39
|
+
' func: async (_services, data: { title: string }) => ({ id: data.title }),',
|
|
40
|
+
'})',
|
|
41
|
+
].join('\n')
|
|
42
|
+
|
|
43
|
+
const SCENARIO = [
|
|
44
|
+
"import { pikkuScenario } from '@pikku/core/workflow'",
|
|
45
|
+
'export const shopFlow = pikkuScenario({',
|
|
46
|
+
" title: 'Buys an apple',",
|
|
47
|
+
' func: async (_services, _input, { scenario }: any) => {',
|
|
48
|
+
" await scenario.when('buys an apple', 'buysAnApple', { qty: 1 })",
|
|
49
|
+
' return { ok: true }',
|
|
50
|
+
' },',
|
|
51
|
+
'})',
|
|
52
|
+
].join('\n')
|
|
53
|
+
|
|
54
|
+
describe('scenario markers in the function meta', () => {
|
|
55
|
+
test('a real pikkuScenario and pikkuScenarioStep are marked, an application function is not', async () => {
|
|
56
|
+
const rootDir = await mkdtemp(join(tmpdir(), 'pikku-scenario-marker-'))
|
|
57
|
+
const files = [
|
|
58
|
+
[join(rootDir, 'shop.steps.ts'), STEPS],
|
|
59
|
+
[join(rootDir, 'shop.scenario.ts'), SCENARIO],
|
|
60
|
+
[join(rootDir, 'todo.function.ts'), APP],
|
|
61
|
+
] as const
|
|
62
|
+
await Promise.all(files.map(([path, source]) => writeFile(path, source)))
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const state = await inspect(
|
|
66
|
+
logger,
|
|
67
|
+
files.map(([path]) => path),
|
|
68
|
+
{ rootDir }
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
const scenario = state.functions.meta['shopFlow']
|
|
72
|
+
assert.ok(scenario, 'the scenario body is a function and must have meta')
|
|
73
|
+
assert.equal(scenario.scenario, true)
|
|
74
|
+
assert.equal(scenario.scenarioStep, undefined)
|
|
75
|
+
|
|
76
|
+
const step = state.functions.meta['buysAnApple']
|
|
77
|
+
assert.ok(step)
|
|
78
|
+
assert.equal(step.scenarioStep, true)
|
|
79
|
+
assert.equal(step.scenario, undefined)
|
|
80
|
+
|
|
81
|
+
const app = state.functions.meta['createTodo']
|
|
82
|
+
assert.ok(app)
|
|
83
|
+
assert.equal(app.scenario, undefined)
|
|
84
|
+
assert.equal(app.scenarioStep, undefined)
|
|
85
|
+
} finally {
|
|
86
|
+
await rm(rootDir, { recursive: true, force: true })
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
})
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,11 @@ export type { FilesAndMethodsErrors } from './utils/get-files-and-methods.js'
|
|
|
5
5
|
export { ErrorCode } from './error-codes.js'
|
|
6
6
|
export type { DiagnosticSeverity, CodedDiagnostic } from './error-codes.js'
|
|
7
7
|
export { AUTH_HANDLER_FUNC_ID } from './add/add-auth.js'
|
|
8
|
+
export {
|
|
9
|
+
SCENARIO_INSTRUMENTATION_FUNCTIONS,
|
|
10
|
+
isScenarioInstrumentationFunction,
|
|
11
|
+
} from './add/scenario-instrumentation.js'
|
|
12
|
+
export type { ScenarioInstrumentationFunction } from './add/scenario-instrumentation.js'
|
|
8
13
|
export {
|
|
9
14
|
serializeInspectorState,
|
|
10
15
|
deserializeInspectorState,
|
|
@@ -64,6 +64,7 @@ function makeState(
|
|
|
64
64
|
addonFunctions?: Record<string, Record<string, any>>
|
|
65
65
|
addonRequiredParentServices?: string[]
|
|
66
66
|
authServices?: string[]
|
|
67
|
+
graphMeta?: Record<string, any>
|
|
67
68
|
} = {}
|
|
68
69
|
): Omit<InspectorState, 'typesLookup'> {
|
|
69
70
|
return {
|
|
@@ -95,7 +96,7 @@ function makeState(
|
|
|
95
96
|
scheduledTasks: { meta: {} },
|
|
96
97
|
mcpEndpoints: { toolsMeta: {}, promptsMeta: {}, resourcesMeta: {} },
|
|
97
98
|
agents: { agentsMeta: {} },
|
|
98
|
-
workflows: { meta: {}, graphMeta: {} },
|
|
99
|
+
workflows: { meta: {}, graphMeta: overrides.graphMeta ?? {} },
|
|
99
100
|
wireServicesMeta: new Map(),
|
|
100
101
|
rpc: { internalMeta: {}, exposedMeta: {} },
|
|
101
102
|
addonFunctions: overrides.addonFunctions ?? {},
|
|
@@ -117,6 +118,34 @@ const CONSOLE_PARENT_SERVICES = [
|
|
|
117
118
|
'credentialService',
|
|
118
119
|
]
|
|
119
120
|
|
|
121
|
+
describe('aggregateRequiredServices — synthetic workflow queue workers', () => {
|
|
122
|
+
const graphMeta = {
|
|
123
|
+
orderWorkflow: {
|
|
124
|
+
name: 'orderWorkflow',
|
|
125
|
+
source: 'dsl',
|
|
126
|
+
nodes: { 'step-1': { rpcName: 'chargeCard' } },
|
|
127
|
+
},
|
|
128
|
+
loginScenario: {
|
|
129
|
+
name: 'loginScenario',
|
|
130
|
+
source: 'scenario',
|
|
131
|
+
nodes: { 'step-1': { rpcName: 'opensPage' } },
|
|
132
|
+
},
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// A scenario IS a workflow, so it used to be handed an orchestrator queue
|
|
136
|
+
// worker in the app's queue meta — which the app bootstrap registers and every
|
|
137
|
+
// deployed bundle therefore carries, leaving a provider to create a production
|
|
138
|
+
// queue named after a test. Nothing dispatches a scenario through a queue:
|
|
139
|
+
// `pikku scenario run` executes its steps in-process.
|
|
140
|
+
test('a scenario gets no orchestrator queue worker', () => {
|
|
141
|
+
const state = makeState({ graphMeta })
|
|
142
|
+
aggregateRequiredServices(state)
|
|
143
|
+
assert.deepEqual(Object.keys(state.queueWorkers.meta), [
|
|
144
|
+
'wf-orchestrator-order-workflow',
|
|
145
|
+
])
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
|
|
120
149
|
describe('aggregateRequiredServices — per-function addon services', () => {
|
|
121
150
|
test('a used addon function adds only its own parent services', () => {
|
|
122
151
|
const state = makeState({
|
|
@@ -262,6 +262,12 @@ export function aggregateRequiredServices(
|
|
|
262
262
|
// per-workflow queue entries and falls back to shared queue names.
|
|
263
263
|
for (const [, graph] of Object.entries(state.workflows.graphMeta)) {
|
|
264
264
|
if (!graph.nodes || !graph.name) continue
|
|
265
|
+
// A scenario is a workflow, but it only ever runs in-process under
|
|
266
|
+
// `pikku scenario run` — no step of one is dispatched through a queue. The
|
|
267
|
+
// synthetic entries were pure leakage: they put a
|
|
268
|
+
// `wf-orchestrator-<scenario>` worker into the app's queue meta, which every
|
|
269
|
+
// bundle imports and a provider then creates as a real production queue.
|
|
270
|
+
if (graph.source === 'scenario') continue
|
|
265
271
|
|
|
266
272
|
const toKebab = (s: string) =>
|
|
267
273
|
s
|