@itmetsam/samurai-framework 0.3.0 → 0.5.0
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/dist/cli/flow-reporter.d.ts +5 -2
- package/dist/cli/flow-reporter.js +4 -2
- package/dist/cli/flow.d.ts +1 -1
- package/dist/cli/flow.js +4 -2
- package/dist/config/config.d.ts +5 -0
- package/dist/config/config.js +13 -0
- package/dist/flows/core/check.d.ts +4 -1
- package/dist/flows/core/check.js +17 -14
- package/dist/flows/core/expression/check.d.ts +2 -0
- package/dist/flows/core/expression/check.js +3 -3
- package/dist/flows/core/expression/evaluate.d.ts +1 -1
- package/dist/flows/core/expression/scope.d.ts +3 -2
- package/dist/flows/core/expression/scope.js +15 -6
- package/dist/flows/core/index.d.ts +2 -1
- package/dist/flows/core/index.js +2 -1
- package/dist/flows/core/nodes/define.d.ts +4 -0
- package/dist/flows/core/nodes/index.d.ts +4 -66
- package/dist/flows/core/nodes/index.js +12 -38
- package/dist/flows/core/steps.d.ts +42 -0
- package/dist/flows/core/steps.js +58 -0
- package/dist/flows/run/events.d.ts +3 -1
- package/dist/flows/run/handlers/index.d.ts +1 -3
- package/dist/flows/run/handlers/index.js +3 -8
- package/dist/flows/run/handlers/support.d.ts +4 -1
- package/dist/flows/run/handlers/support.js +1 -1
- package/dist/flows/run/index.d.ts +2 -1
- package/dist/flows/run/index.js +2 -1
- package/dist/flows/run/project.js +18 -19
- package/dist/flows/run/run.d.ts +1 -1
- package/dist/flows/run/run.js +7 -1
- package/dist/flows/steps/api.d.ts +23 -0
- package/dist/flows/steps/api.js +63 -0
- package/dist/flows/steps/catalog.d.ts +22 -0
- package/dist/flows/steps/catalog.js +36 -0
- package/dist/flows/steps/define.d.ts +51 -0
- package/dist/flows/steps/define.js +51 -0
- package/dist/flows/steps/handler.d.ts +4 -0
- package/dist/flows/steps/handler.js +82 -0
- package/dist/flows/steps/index.d.ts +6 -0
- package/dist/flows/steps/index.js +5 -0
- package/dist/flows/{core/nodes → steps}/set-variable.d.ts +4 -1
- package/dist/flows/steps/set-variable.js +31 -0
- package/dist/flows/steps/set.d.ts +26 -0
- package/dist/flows/steps/set.js +43 -0
- package/dist/flows/steps/wait.d.ts +8 -0
- package/dist/flows/steps/wait.js +21 -0
- package/dist/runner/groups.d.ts +5 -0
- package/dist/runner/groups.js +13 -0
- package/dist/runner/test-runner.js +4 -3
- package/dist/types/config.d.ts +7 -0
- package/package.json +5 -1
- package/dist/flows/core/nodes/api.d.ts +0 -9
- package/dist/flows/core/nodes/api.js +0 -12
- package/dist/flows/core/nodes/database.d.ts +0 -9
- package/dist/flows/core/nodes/database.js +0 -12
- package/dist/flows/core/nodes/email.d.ts +0 -9
- package/dist/flows/core/nodes/email.js +0 -12
- package/dist/flows/core/nodes/script.d.ts +0 -9
- package/dist/flows/core/nodes/script.js +0 -12
- package/dist/flows/core/nodes/set-variable.js +0 -39
- package/dist/flows/core/nodes/wait.d.ts +0 -15
- package/dist/flows/core/nodes/wait.js +0 -18
- package/dist/flows/run/handlers/set-variable.d.ts +0 -2
- package/dist/flows/run/handlers/set-variable.js +0 -22
- package/dist/flows/run/handlers/wait.d.ts +0 -2
- package/dist/flows/run/handlers/wait.js +0 -10
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { STEP_HANDLERS } from "../../steps/catalog.js";
|
|
2
2
|
import { condition } from "./condition.js";
|
|
3
3
|
import { end } from "./end.js";
|
|
4
4
|
import { group } from "./group.js";
|
|
5
5
|
import { parallel } from "./parallel.js";
|
|
6
|
-
import { setVariable } from "./set-variable.js";
|
|
7
6
|
import { start } from "./start.js";
|
|
8
7
|
import { test } from "./test.js";
|
|
9
|
-
|
|
10
|
-
/** The kinds a run can execute, by kind */
|
|
8
|
+
/** The kinds a run can execute, by kind: the closed ones and every built-in step */
|
|
11
9
|
export const HANDLERS = {
|
|
12
10
|
start,
|
|
13
11
|
end,
|
|
@@ -15,8 +13,5 @@ export const HANDLERS = {
|
|
|
15
13
|
group,
|
|
16
14
|
condition,
|
|
17
15
|
parallel,
|
|
18
|
-
|
|
19
|
-
"set-variable": setVariable,
|
|
16
|
+
...STEP_HANDLERS,
|
|
20
17
|
};
|
|
21
|
-
/** Kinds that exist in `flows/core` and have no handler yet (core's one list) */
|
|
22
|
-
export const UNSUPPORTED = UNSUPPORTED_KINDS;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ExpressionContext, type FlowFile, type FlowNodeModel } from "../../core/index.js";
|
|
1
|
+
import { type AnyNodeDefinition, type ExpressionContext, type FlowFile, type FlowNodeModel } from "../../core/index.js";
|
|
2
2
|
import type { RunEvent } from "../../../runner/reporter.js";
|
|
3
3
|
import type { NodeResult, NodeTest } from "../events.js";
|
|
4
4
|
/** What a handler can use of the run */
|
|
@@ -14,6 +14,9 @@ export interface HandlerRun {
|
|
|
14
14
|
runTests: (tests: NodeTest[], onEvent: (e: RunEvent) => void) => Promise<"passed" | "failed" | "cancelled">;
|
|
15
15
|
sleep: (ms: number, signal: AbortSignal) => Promise<void>;
|
|
16
16
|
onTestEvent: (nodeId: string, e: RunEvent) => void;
|
|
17
|
+
/** Definitions of kinds core doesn't have (steps), for expression scopes */
|
|
18
|
+
nodes: readonly AnyNodeDefinition[];
|
|
19
|
+
log: (nodeId: string, level: "info" | "warn" | "error", text: string) => void;
|
|
17
20
|
}
|
|
18
21
|
/** What a handler decides; the walker adds timing */
|
|
19
22
|
export type Outcome = Omit<NodeResult, "durationMs"> | "cancelled";
|
|
@@ -19,4 +19,4 @@ export async function runNodeTests(node, run, list) {
|
|
|
19
19
|
: { status: "failed", error: "A test failed." };
|
|
20
20
|
}
|
|
21
21
|
/** The checker's problem with an expression, as the builder would show it: a bypassed check fails the node the same way */
|
|
22
|
-
export const problemAt = (node, run, text) => checkExpression(parseExpression(text), scopeAt(run.flow, node.id, run.environment))?.message;
|
|
22
|
+
export const problemAt = (node, run, text) => checkExpression(parseExpression(text), scopeAt(run.flow, node.id, run.environment, run.nodes))?.message;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/** Running flows: handlers, the walker, a project's flow files. Node only; see `flows/core` for the pure half. */
|
|
2
2
|
export * from "./events.js";
|
|
3
|
-
export { HANDLERS
|
|
3
|
+
export { HANDLERS } from "./handlers/index.js";
|
|
4
4
|
export type { Handler, HandlerRun, Outcome } from "./handlers/index.js";
|
|
5
5
|
export { runFlow, type RunFlowOptions } from "./run.js";
|
|
6
6
|
export { FLOW_SUFFIX, flowsDir, listFlows, readFlow, readFlowPath, readFlows, } from "./files.js";
|
|
7
7
|
export * from "./report.js";
|
|
8
|
+
export { projectSteps, type StepSet } from "../steps/set.js";
|
|
8
9
|
export { FlowCheckError, checkFlowInProject, chooseEnvironment, runFlowInProject, type RunFlowInProjectOptions, } from "./project.js";
|
package/dist/flows/run/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/** Running flows: handlers, the walker, a project's flow files. Node only; see `flows/core` for the pure half. */
|
|
2
2
|
export * from "./events.js";
|
|
3
|
-
export { HANDLERS
|
|
3
|
+
export { HANDLERS } from "./handlers/index.js";
|
|
4
4
|
export { runFlow } from "./run.js";
|
|
5
5
|
export { FLOW_SUFFIX, flowsDir, listFlows, readFlow, readFlowPath, readFlows, } from "./files.js";
|
|
6
6
|
export * from "./report.js";
|
|
7
|
+
export { projectSteps } from "../steps/set.js";
|
|
7
8
|
export { FlowCheckError, checkFlowInProject, chooseEnvironment, runFlowInProject, } from "./project.js";
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { projectConfig } from "../../config/config.js";
|
|
3
3
|
import { checkFlow, withKeys, } from "../core/index.js";
|
|
4
|
-
import { DEFAULT_SRC_DIR, resolveGroups, testId, toPosix, } from "../../runner/groups.js";
|
|
4
|
+
import { DEFAULT_SRC_DIR, realPath, resolveGroups, testId, toPosix, } from "../../runner/groups.js";
|
|
5
5
|
import { withProject } from "../../runner/project.js";
|
|
6
6
|
import { runTests } from "../../runner/run.js";
|
|
7
7
|
import TestRunner from "../../runner/test-runner.js";
|
|
8
8
|
import { readFlow } from "./files.js";
|
|
9
|
+
import { projectSteps } from "../steps/set.js";
|
|
9
10
|
import { HANDLERS } from "./handlers/index.js";
|
|
10
11
|
import { runFlow } from "./run.js";
|
|
11
12
|
/** The flow has errors; nothing was run */
|
|
@@ -16,18 +17,6 @@ export class FlowCheckError extends Error {
|
|
|
16
17
|
this.name = "FlowCheckError";
|
|
17
18
|
}
|
|
18
19
|
}
|
|
19
|
-
/** Reads the project's config without keeping the project active (`withProject` is one at a time) */
|
|
20
|
-
async function configOf(options) {
|
|
21
|
-
if (options.config)
|
|
22
|
-
return options.config;
|
|
23
|
-
setActiveProject({ dir: path.resolve(options.projectDir ?? process.cwd()) });
|
|
24
|
-
try {
|
|
25
|
-
return await loadConfig();
|
|
26
|
-
}
|
|
27
|
-
finally {
|
|
28
|
-
setActiveProject(undefined);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
20
|
/** The environment: the option, else the flow's `envDefault` (when the project has it), else the config's default, else the first, else "default" */
|
|
32
21
|
export function chooseEnvironment(config, flow, option) {
|
|
33
22
|
const names = Object.keys(config.environments ?? {});
|
|
@@ -45,7 +34,8 @@ async function prepare(options) {
|
|
|
45
34
|
const flow = withKeys(options.flow ?? (await readFlow(projectDir, options.flowId)));
|
|
46
35
|
const unsupported = options.unsupported ?? "fail";
|
|
47
36
|
// Config, environment, tests and groups (one project session, then it ends)
|
|
48
|
-
const config = await
|
|
37
|
+
const config = await projectConfig({ ...options, projectDir });
|
|
38
|
+
const steps = await projectSteps({ config });
|
|
49
39
|
const name = chooseEnvironment(config, flow, options.environment);
|
|
50
40
|
const environment = {
|
|
51
41
|
name,
|
|
@@ -55,7 +45,9 @@ async function prepare(options) {
|
|
|
55
45
|
: {}),
|
|
56
46
|
},
|
|
57
47
|
};
|
|
58
|
-
|
|
48
|
+
// Real paths, like the registered test files
|
|
49
|
+
const realDir = realPath(projectDir);
|
|
50
|
+
const srcDir = realPath(path.resolve(realDir, config.srcDir ?? DEFAULT_SRC_DIR));
|
|
59
51
|
const { tests, groups } = await withProject({
|
|
60
52
|
projectDir,
|
|
61
53
|
config,
|
|
@@ -69,21 +61,26 @@ async function prepare(options) {
|
|
|
69
61
|
name: t.name,
|
|
70
62
|
relative: toPosix(path.relative(srcDir, t.file)),
|
|
71
63
|
}));
|
|
72
|
-
const groups = resolveGroups(config, tests.map((t) => ({ file: t.relative, name: t.name })),
|
|
64
|
+
const groups = resolveGroups(config, tests.map((t) => ({ file: t.relative, name: t.name })), realDir, srcDir);
|
|
73
65
|
return { tests, groups };
|
|
74
66
|
});
|
|
75
67
|
const problems = checkFlow(flow, {
|
|
76
68
|
tests: tests.map((t) => t.id),
|
|
77
69
|
groups: groups.map((g) => g.name),
|
|
78
70
|
environment,
|
|
79
|
-
handled: new Set(
|
|
71
|
+
handled: new Set([
|
|
72
|
+
...Object.keys(HANDLERS),
|
|
73
|
+
...Object.keys(steps.handlers),
|
|
74
|
+
]),
|
|
80
75
|
unsupported,
|
|
76
|
+
nodes: steps.nodes,
|
|
81
77
|
});
|
|
82
78
|
return {
|
|
83
79
|
projectDir,
|
|
84
80
|
flow,
|
|
85
81
|
unsupported,
|
|
86
82
|
config,
|
|
83
|
+
steps,
|
|
87
84
|
name,
|
|
88
85
|
environment,
|
|
89
86
|
tests,
|
|
@@ -105,7 +102,7 @@ export async function checkFlowInProject(options) {
|
|
|
105
102
|
* its tests through `runTests`, one after the other. One at a time per process.
|
|
106
103
|
*/
|
|
107
104
|
export async function runFlowInProject(options) {
|
|
108
|
-
const { projectDir, flow, unsupported, config, name, environment, tests, groups, problems, } = await prepare(options);
|
|
105
|
+
const { projectDir, flow, unsupported, config, steps, name, environment, tests, groups, problems, } = await prepare(options);
|
|
109
106
|
const errors = problems.filter((p) => p.level === "error");
|
|
110
107
|
if (errors.length)
|
|
111
108
|
throw new FlowCheckError(errors);
|
|
@@ -134,6 +131,8 @@ export async function runFlowInProject(options) {
|
|
|
134
131
|
...(options.onEvent && { onEvent: options.onEvent }),
|
|
135
132
|
...(options.sleep && { sleep: options.sleep }),
|
|
136
133
|
unsupported,
|
|
134
|
+
handlers: { ...HANDLERS, ...steps.handlers },
|
|
135
|
+
nodes: steps.nodes,
|
|
137
136
|
runTests: async ({ tests, onEvent, signal }) => {
|
|
138
137
|
const summary = await run({
|
|
139
138
|
projectDir,
|
package/dist/flows/run/run.d.ts
CHANGED
|
@@ -30,7 +30,7 @@ export interface RunFlowOptions {
|
|
|
30
30
|
unsupported?: "fail" | "skip";
|
|
31
31
|
/** The handlers by kind. @default HANDLERS */
|
|
32
32
|
handlers?: Readonly<Record<string, Handler>>;
|
|
33
|
-
/** Definitions of kinds
|
|
33
|
+
/** Definitions of kinds core doesn't have. @default the built-in steps */
|
|
34
34
|
nodes?: readonly AnyNodeDefinition[];
|
|
35
35
|
}
|
|
36
36
|
/**
|
package/dist/flows/run/run.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { inFillOrder, meetOf, nodeDef, } from "../core/index.js";
|
|
2
|
+
import logger from "../../logger/index.js";
|
|
3
|
+
import { STEP_NODES } from "../steps/catalog.js";
|
|
2
4
|
import { HANDLERS } from "./handlers/index.js";
|
|
3
5
|
const defaultSleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
4
6
|
if (signal.aborted)
|
|
@@ -26,7 +28,8 @@ export async function runFlow(options) {
|
|
|
26
28
|
const { flow, onEvent = () => { } } = options;
|
|
27
29
|
const signal = options.signal ?? new AbortController().signal;
|
|
28
30
|
const handlers = options.handlers ?? HANDLERS;
|
|
29
|
-
const
|
|
31
|
+
const nodes = options.nodes ?? STEP_NODES;
|
|
32
|
+
const defOf = (kind) => nodes.find((d) => d.kind === kind) ??
|
|
30
33
|
(() => {
|
|
31
34
|
try {
|
|
32
35
|
return nodeDef(kind);
|
|
@@ -66,6 +69,8 @@ export async function runFlow(options) {
|
|
|
66
69
|
sleep: options.sleep ?? defaultSleep,
|
|
67
70
|
runTests: (tests, onTestEvent) => options.runTests({ tests, onEvent: onTestEvent, signal }),
|
|
68
71
|
onTestEvent: (nodeId, event) => onEvent({ type: "test", nodeId, event }),
|
|
72
|
+
nodes,
|
|
73
|
+
log: (nodeId, level, text) => logger[level](`[${nodeId}] ${text}`),
|
|
69
74
|
};
|
|
70
75
|
let stopped;
|
|
71
76
|
const visited = new Set();
|
|
@@ -73,6 +78,7 @@ export async function runFlow(options) {
|
|
|
73
78
|
results[node.id] = result;
|
|
74
79
|
if (node.key)
|
|
75
80
|
context.nodes[node.key] = {
|
|
81
|
+
...result.outputs,
|
|
76
82
|
status: result.status,
|
|
77
83
|
durationMs: result.durationMs,
|
|
78
84
|
error: result.error,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Any response passes and outputs its status; a network error or a bad URL fails the node */
|
|
2
|
+
export declare const api: import("./define.js").StepDefinition<import("./define.js").RunConfigOf<readonly [{
|
|
3
|
+
readonly path: "config.method";
|
|
4
|
+
readonly type: "select";
|
|
5
|
+
readonly label: "Method";
|
|
6
|
+
readonly options: {
|
|
7
|
+
label: string;
|
|
8
|
+
value: string;
|
|
9
|
+
}[];
|
|
10
|
+
readonly default: "GET";
|
|
11
|
+
}, {
|
|
12
|
+
readonly path: "config.url";
|
|
13
|
+
readonly type: "expression";
|
|
14
|
+
readonly label: "URL";
|
|
15
|
+
readonly placeholder: "env.baseUrl + \"/health\"";
|
|
16
|
+
readonly default: "";
|
|
17
|
+
}, {
|
|
18
|
+
readonly path: "config.body";
|
|
19
|
+
readonly type: "code";
|
|
20
|
+
readonly label: "Body";
|
|
21
|
+
readonly placeholder: "{ \"name\": \"test\" }";
|
|
22
|
+
readonly default: "";
|
|
23
|
+
}]>>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { defineStep } from "./define.js";
|
|
2
|
+
const METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"].map((m) => ({
|
|
3
|
+
label: m,
|
|
4
|
+
value: m,
|
|
5
|
+
}));
|
|
6
|
+
const isJson = (text) => {
|
|
7
|
+
try {
|
|
8
|
+
JSON.parse(text);
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
/** Any response passes and outputs its status; a network error or a bad URL fails the node */
|
|
16
|
+
export const api = defineStep({
|
|
17
|
+
kind: "api",
|
|
18
|
+
label: "HTTP request",
|
|
19
|
+
icon: "globe",
|
|
20
|
+
section: "integrations",
|
|
21
|
+
fields: [
|
|
22
|
+
{
|
|
23
|
+
path: "config.method",
|
|
24
|
+
type: "select",
|
|
25
|
+
label: "Method",
|
|
26
|
+
options: METHODS,
|
|
27
|
+
default: "GET",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
path: "config.url",
|
|
31
|
+
type: "expression",
|
|
32
|
+
label: "URL",
|
|
33
|
+
placeholder: 'env.baseUrl + "/health"',
|
|
34
|
+
default: "",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
path: "config.body",
|
|
38
|
+
type: "code",
|
|
39
|
+
label: "Body",
|
|
40
|
+
placeholder: '{ "name": "test" }',
|
|
41
|
+
default: "",
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
summary: "{config.method} {config.url}",
|
|
45
|
+
outputs: [
|
|
46
|
+
{ name: "statusCode", label: "Status code" },
|
|
47
|
+
{ name: "body", label: "Response body" },
|
|
48
|
+
],
|
|
49
|
+
async run({ config, signal }) {
|
|
50
|
+
const body = config.method !== "GET" && config.body ? config.body : undefined;
|
|
51
|
+
const res = await fetch(String(config.url), {
|
|
52
|
+
method: config.method,
|
|
53
|
+
signal,
|
|
54
|
+
...(body !== undefined && {
|
|
55
|
+
body,
|
|
56
|
+
headers: {
|
|
57
|
+
"content-type": isJson(body) ? "application/json" : "text/plain",
|
|
58
|
+
},
|
|
59
|
+
}),
|
|
60
|
+
});
|
|
61
|
+
return { outputs: { statusCode: res.status, body: await res.text() } };
|
|
62
|
+
},
|
|
63
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AnyNodeDefinition, FlowFile, FlowProblem, StepInfo } from "../core/index.js";
|
|
2
|
+
import type { Handler } from "../run/handlers/support.js";
|
|
3
|
+
import { type StepDefinition } from "./define.js";
|
|
4
|
+
import type { SamuraiTestConfig } from "../../types/config.js";
|
|
5
|
+
/** The built-in steps, in library order */
|
|
6
|
+
export declare const STEPS: readonly StepDefinition<any>[];
|
|
7
|
+
export declare const STEP_NODES: readonly AnyNodeDefinition[];
|
|
8
|
+
export declare const STEP_HANDLERS: Readonly<Record<string, Handler>>;
|
|
9
|
+
/** The steps as data, for the app */
|
|
10
|
+
export declare const stepCatalog: (steps?: readonly StepDefinition<any>[]) => StepInfo[];
|
|
11
|
+
/** The project's steps as data: the built-ins, then its config's. `projectDir` defaults to the working directory. */
|
|
12
|
+
export declare function listSteps(options?: {
|
|
13
|
+
projectDir?: string;
|
|
14
|
+
config?: SamuraiTestConfig;
|
|
15
|
+
}): Promise<StepInfo[]>;
|
|
16
|
+
/** Only the steps' own `validate` problems (the project's steps included): the app checks everything else itself */
|
|
17
|
+
export declare function validateSteps(flow: FlowFile, options: {
|
|
18
|
+
tests?: string[];
|
|
19
|
+
groups?: string[];
|
|
20
|
+
projectDir?: string;
|
|
21
|
+
config?: SamuraiTestConfig;
|
|
22
|
+
}): Promise<FlowProblem[]>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { infoOf } from "./define.js";
|
|
2
|
+
import { BUILTIN_SET, BUILTIN_STEPS, projectSteps } from "./set.js";
|
|
3
|
+
/** The built-in steps, in library order */
|
|
4
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5
|
+
export const STEPS = BUILTIN_STEPS;
|
|
6
|
+
export const STEP_NODES = BUILTIN_SET.nodes;
|
|
7
|
+
export const STEP_HANDLERS = BUILTIN_SET.handlers;
|
|
8
|
+
/** The steps as data, for the app */
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
|
+
export const stepCatalog = (steps = STEPS) => steps.map(infoOf);
|
|
11
|
+
/** The project's steps as data: the built-ins, then its config's. `projectDir` defaults to the working directory. */
|
|
12
|
+
export async function listSteps(options = {}) {
|
|
13
|
+
return [...(await projectSteps(options)).catalog];
|
|
14
|
+
}
|
|
15
|
+
/** Only the steps' own `validate` problems (the project's steps included): the app checks everything else itself */
|
|
16
|
+
export async function validateSteps(flow, options) {
|
|
17
|
+
const set = await projectSteps(options);
|
|
18
|
+
const tests = options.tests ? new Set(options.tests) : undefined;
|
|
19
|
+
const groups = options.groups ? new Set(options.groups) : undefined;
|
|
20
|
+
return flow.nodes.flatMap((n) => {
|
|
21
|
+
const step = set.steps.find((s) => s.kind === n.kind);
|
|
22
|
+
if (!step?.validate)
|
|
23
|
+
return [];
|
|
24
|
+
const def = set.nodes.find((d) => d.kind === n.kind);
|
|
25
|
+
return step.validate({
|
|
26
|
+
...n,
|
|
27
|
+
config: {
|
|
28
|
+
...Object.fromEntries(step.fields.map((f) => [
|
|
29
|
+
f.path.slice(7),
|
|
30
|
+
"default" in f ? f.default : undefined,
|
|
31
|
+
])),
|
|
32
|
+
...n.config,
|
|
33
|
+
},
|
|
34
|
+
}, { label: def.label, tests, groups });
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type AnyNodeDefinition, type NodeCoreDefinition, type StepField, type StepInfo } from "../core/index.js";
|
|
2
|
+
type RunValueOf<F> = F extends {
|
|
3
|
+
type: "expression";
|
|
4
|
+
} ? unknown : F extends {
|
|
5
|
+
type: "number";
|
|
6
|
+
} ? number : F extends {
|
|
7
|
+
type: "switch";
|
|
8
|
+
} ? boolean : string;
|
|
9
|
+
/** `config` as `run` sees it: expression fields arrive evaluated */
|
|
10
|
+
export type RunConfigOf<F extends readonly StepField[]> = {
|
|
11
|
+
[K in F[number] as K["path"] extends `config.${infer P}` ? P : never]: RunValueOf<K>;
|
|
12
|
+
};
|
|
13
|
+
export interface StepRunInput<C> {
|
|
14
|
+
config: C;
|
|
15
|
+
env: Readonly<Record<string, unknown>>;
|
|
16
|
+
vars: Readonly<Record<string, unknown>>;
|
|
17
|
+
node: {
|
|
18
|
+
id: string;
|
|
19
|
+
key?: string;
|
|
20
|
+
title?: string;
|
|
21
|
+
};
|
|
22
|
+
/** Aborted when the run is stopped */
|
|
23
|
+
signal: AbortSignal;
|
|
24
|
+
/** Waits, unless the run is stopped first */
|
|
25
|
+
sleep: (ms: number) => Promise<void>;
|
|
26
|
+
log: (level: "info" | "warn" | "error", text: string) => void;
|
|
27
|
+
}
|
|
28
|
+
export interface StepResult {
|
|
29
|
+
/** Declared in `outputs`; readable later as `nodes.<key>.<name>` */
|
|
30
|
+
outputs?: Record<string, unknown>;
|
|
31
|
+
/** Only the name the `writesVar` field holds */
|
|
32
|
+
vars?: Record<string, unknown>;
|
|
33
|
+
note?: string;
|
|
34
|
+
}
|
|
35
|
+
export interface StepDefinition<C = Record<string, unknown>> extends StepInfo {
|
|
36
|
+
/** Problems with this node alone; runs in the engine (checks and before a run) */
|
|
37
|
+
validate?: NodeCoreDefinition<string, any>["validate"];
|
|
38
|
+
/** What the step does in a run: return to pass, throw to fail */
|
|
39
|
+
run: (input: StepRunInput<C>) => Promise<StepResult | void>;
|
|
40
|
+
}
|
|
41
|
+
/** The checks every step passes: in `defineStep`, and again when a project lists it. Throws for a step that can't work. */
|
|
42
|
+
export declare function checkStep(def: StepDefinition<any>, where?: string): void;
|
|
43
|
+
/** Declares a flow step: one way in, one way out. Throws for a definition that can't work. */
|
|
44
|
+
export declare function defineStep<const F extends readonly StepField[]>(def: Omit<StepDefinition<RunConfigOf<F>>, "fields"> & {
|
|
45
|
+
fields: F;
|
|
46
|
+
}): StepDefinition<RunConfigOf<F>>;
|
|
47
|
+
/** The step as data, without its functions: what the catalog holds */
|
|
48
|
+
export declare function infoOf(step: StepDefinition<never>): StepInfo;
|
|
49
|
+
/** The definition the walker and checkFlow use for a step */
|
|
50
|
+
export declare const stepNode: (step: StepDefinition<any>) => AnyNodeDefinition;
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { CLOSED_KINDS, RESERVED_OUTPUTS, STEP_KIND, stepNodeOf, } from "../core/index.js";
|
|
2
|
+
/** The checks every step passes: in `defineStep`, and again when a project lists it. Throws for a step that can't work. */
|
|
3
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
4
|
+
export function checkStep(def, where = "") {
|
|
5
|
+
const name = `Step "${def.kind}"${where}`;
|
|
6
|
+
if (CLOSED_KINDS.includes(def.kind))
|
|
7
|
+
throw new Error(`${name}: "${def.kind}" is a closed kind; steps can't replace it.`);
|
|
8
|
+
if (!STEP_KIND.test(def.kind))
|
|
9
|
+
throw new Error(`${name}: "${def.kind}" isn't a valid step kind. Use lowercase letters, digits and -.`);
|
|
10
|
+
if (!Array.isArray(def.fields))
|
|
11
|
+
throw new Error(`${name}: fields must be a list.`);
|
|
12
|
+
for (const f of def.fields) {
|
|
13
|
+
if (!f.path.startsWith("config."))
|
|
14
|
+
throw new Error(`${name}: field "${f.path}" must be a config. path.`);
|
|
15
|
+
if (f.pattern !== undefined)
|
|
16
|
+
try {
|
|
17
|
+
new RegExp(f.pattern);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
throw new Error(`${name}: field "${f.path}" has a pattern that isn't a valid regular expression.`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const outputs = (def.outputs ?? []).map((o) => o.name);
|
|
24
|
+
for (const o of outputs) {
|
|
25
|
+
if (RESERVED_OUTPUTS.includes(o))
|
|
26
|
+
throw new Error(`${name}: output "${o}" is reserved.`);
|
|
27
|
+
if (outputs.indexOf(o) !== outputs.lastIndexOf(o))
|
|
28
|
+
throw new Error(`${name}: output "${o}" is declared twice.`);
|
|
29
|
+
}
|
|
30
|
+
if (def.writesVar !== undefined &&
|
|
31
|
+
!def.fields.some((f) => f.path === def.writesVar))
|
|
32
|
+
throw new Error(`${name}: writesVar "${def.writesVar}" isn't one of its fields.`);
|
|
33
|
+
if (typeof def.run !== "function")
|
|
34
|
+
throw new Error(`${name}: run must be a function.`);
|
|
35
|
+
}
|
|
36
|
+
/** Declares a flow step: one way in, one way out. Throws for a definition that can't work. */
|
|
37
|
+
export function defineStep(def) {
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
39
|
+
checkStep(def);
|
|
40
|
+
return def;
|
|
41
|
+
}
|
|
42
|
+
/** The step as data, without its functions: what the catalog holds */
|
|
43
|
+
export function infoOf(step) {
|
|
44
|
+
const { run: _run, validate: _validate, ...info } = step;
|
|
45
|
+
void _run;
|
|
46
|
+
void _validate;
|
|
47
|
+
return JSON.parse(JSON.stringify(info));
|
|
48
|
+
}
|
|
49
|
+
/** The definition the walker and checkFlow use for a step */
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
51
|
+
export const stepNode = (step) => stepNodeOf(infoOf(step), step.validate);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { evaluateText, getPath, withDefaults } from "../core/index.js";
|
|
2
|
+
import { fail, problemAt } from "../run/handlers/support.js";
|
|
3
|
+
import { stepNode } from "./define.js";
|
|
4
|
+
const keysOf = (o) => Object.keys(o ?? {});
|
|
5
|
+
/** The one handler every step runs through */
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
7
|
+
export function stepHandler(step) {
|
|
8
|
+
const def = stepNode(step);
|
|
9
|
+
return async (node, run) => {
|
|
10
|
+
const typed = withDefaults(def, node);
|
|
11
|
+
const config = { ...typed.config };
|
|
12
|
+
for (const f of step.fields) {
|
|
13
|
+
if (f.type !== "expression")
|
|
14
|
+
continue;
|
|
15
|
+
const text = String(getPath(typed, f.path) ?? "");
|
|
16
|
+
const problem = problemAt(node, run, text);
|
|
17
|
+
if (problem)
|
|
18
|
+
return { status: "failed", error: problem };
|
|
19
|
+
try {
|
|
20
|
+
config[f.path.slice("config.".length)] = evaluateText(text, run.context);
|
|
21
|
+
}
|
|
22
|
+
catch (e) {
|
|
23
|
+
return fail(e);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// A step that ignores the signal must not hold the run: Stop wins the race
|
|
27
|
+
const stopped = new Promise((resolve) => {
|
|
28
|
+
if (run.signal.aborted)
|
|
29
|
+
resolve("stopped");
|
|
30
|
+
run.signal.addEventListener("abort", () => resolve("stopped"), {
|
|
31
|
+
once: true,
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
let result;
|
|
35
|
+
try {
|
|
36
|
+
result = await Promise.race([
|
|
37
|
+
step.run({
|
|
38
|
+
config,
|
|
39
|
+
env: { ...run.context.env },
|
|
40
|
+
vars: { ...run.context.vars },
|
|
41
|
+
node: {
|
|
42
|
+
id: node.id,
|
|
43
|
+
...(node.key && { key: node.key }),
|
|
44
|
+
...(node.title && { title: node.title }),
|
|
45
|
+
},
|
|
46
|
+
signal: run.signal,
|
|
47
|
+
sleep: (ms) => run.sleep(ms, run.signal),
|
|
48
|
+
log: (level, text) => run.log(node.id, level, text),
|
|
49
|
+
}),
|
|
50
|
+
stopped,
|
|
51
|
+
]);
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
if (run.signal.aborted)
|
|
55
|
+
return "cancelled";
|
|
56
|
+
return fail(e);
|
|
57
|
+
}
|
|
58
|
+
if (result === "stopped" || run.signal.aborted)
|
|
59
|
+
return "cancelled";
|
|
60
|
+
const declared = new Set((step.outputs ?? []).map((o) => o.name));
|
|
61
|
+
const extra = keysOf(result?.outputs).filter((k) => !declared.has(k));
|
|
62
|
+
if (extra.length)
|
|
63
|
+
return {
|
|
64
|
+
status: "failed",
|
|
65
|
+
error: `${step.label} returned outputs it doesn't declare: ${extra.join(", ")}.`,
|
|
66
|
+
};
|
|
67
|
+
const allowed = step.writesVar ? getPath(typed, step.writesVar) : undefined;
|
|
68
|
+
const wrongVars = keysOf(result?.vars).filter((k) => k !== allowed);
|
|
69
|
+
if (wrongVars.length)
|
|
70
|
+
return {
|
|
71
|
+
status: "failed",
|
|
72
|
+
error: `${step.label} wrote variables it doesn't declare: ${wrongVars.join(", ")}.`,
|
|
73
|
+
};
|
|
74
|
+
Object.assign(run.context.vars, result?.vars);
|
|
75
|
+
return {
|
|
76
|
+
status: "passed",
|
|
77
|
+
...(keysOf(result?.outputs).length > 0 && { outputs: result.outputs }),
|
|
78
|
+
...(keysOf(result?.vars).length > 0 && { wrote: result.vars }),
|
|
79
|
+
...(result?.note && { note: result.note }),
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Flow steps: define your own, and the built-in ones. Node only. */
|
|
2
|
+
export { checkStep, defineStep, stepNode, type RunConfigOf, type StepDefinition, type StepResult, type StepRunInput, } from "./define.js";
|
|
3
|
+
export { stepHandler } from "./handler.js";
|
|
4
|
+
export { BUILTIN_SET, BUILTIN_STEPS, projectSteps, stepSet, type StepSet, } from "./set.js";
|
|
5
|
+
export { STEPS, STEP_HANDLERS, STEP_NODES, listSteps, stepCatalog, validateSteps, } from "./catalog.js";
|
|
6
|
+
export type { StepField, StepInfo, StepOutput, StepSection, } from "../core/index.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Flow steps: define your own, and the built-in ones. Node only. */
|
|
2
|
+
export { checkStep, defineStep, stepNode, } from "./define.js";
|
|
3
|
+
export { stepHandler } from "./handler.js";
|
|
4
|
+
export { BUILTIN_SET, BUILTIN_STEPS, projectSteps, stepSet, } from "./set.js";
|
|
5
|
+
export { STEPS, STEP_HANDLERS, STEP_NODES, listSteps, stepCatalog, validateSteps, } from "./catalog.js";
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
export declare const setVariable: import("./define.js").
|
|
1
|
+
export declare const setVariable: import("./define.js").StepDefinition<import("./define.js").RunConfigOf<readonly [{
|
|
2
2
|
readonly path: "config.name";
|
|
3
3
|
readonly type: "text";
|
|
4
4
|
readonly label: "Name";
|
|
5
5
|
readonly placeholder: "orderId";
|
|
6
6
|
readonly default: "";
|
|
7
|
+
readonly required: true;
|
|
8
|
+
readonly pattern: "^[a-z][a-z0-9_]*$";
|
|
9
|
+
readonly patternMessage: "use lowercase letters, digits and _ for the name.";
|
|
7
10
|
}, {
|
|
8
11
|
readonly path: "config.value";
|
|
9
12
|
readonly type: "expression";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { defineStep } from "./define.js";
|
|
2
|
+
export const setVariable = defineStep({
|
|
3
|
+
kind: "set-variable",
|
|
4
|
+
label: "Set variable",
|
|
5
|
+
icon: "braces",
|
|
6
|
+
section: "control",
|
|
7
|
+
fields: [
|
|
8
|
+
{
|
|
9
|
+
path: "config.name",
|
|
10
|
+
type: "text",
|
|
11
|
+
label: "Name",
|
|
12
|
+
placeholder: "orderId",
|
|
13
|
+
default: "",
|
|
14
|
+
required: true,
|
|
15
|
+
pattern: "^[a-z][a-z0-9_]*$",
|
|
16
|
+
patternMessage: "use lowercase letters, digits and _ for the name.",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
path: "config.value",
|
|
20
|
+
type: "expression",
|
|
21
|
+
label: "Value",
|
|
22
|
+
placeholder: '"order-" + run.environment',
|
|
23
|
+
default: "",
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
summary: "vars.{config.name} = {config.value}",
|
|
27
|
+
writesVar: "config.name",
|
|
28
|
+
async run({ config }) {
|
|
29
|
+
return { vars: { [config.name]: config.value } };
|
|
30
|
+
},
|
|
31
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { SamuraiTestConfig } from "../../types/config.js";
|
|
2
|
+
import type { AnyNodeDefinition, StepInfo } from "../core/index.js";
|
|
3
|
+
import type { Handler } from "../run/handlers/support.js";
|
|
4
|
+
import { type StepDefinition } from "./define.js";
|
|
5
|
+
type AnyStep = StepDefinition<any>;
|
|
6
|
+
/** The built-in steps, in library order */
|
|
7
|
+
export declare const BUILTIN_STEPS: readonly AnyStep[];
|
|
8
|
+
/** Everything a project's steps give the walker, the check and the app */
|
|
9
|
+
export interface StepSet {
|
|
10
|
+
/** The built-ins, then the project's in config order */
|
|
11
|
+
steps: readonly AnyStep[];
|
|
12
|
+
nodes: readonly AnyNodeDefinition[];
|
|
13
|
+
handlers: Readonly<Record<string, Handler>>;
|
|
14
|
+
/** The steps as data, for the app */
|
|
15
|
+
catalog: StepInfo[];
|
|
16
|
+
}
|
|
17
|
+
/** The built-in steps plus a project's (its config's `steps`). Throws for a list that can't work. */
|
|
18
|
+
export declare function stepSet(projectSteps?: unknown): StepSet;
|
|
19
|
+
/** The built-in steps alone */
|
|
20
|
+
export declare const BUILTIN_SET: StepSet;
|
|
21
|
+
/** The project's steps: its config's `steps` after the built-ins. `projectDir` defaults to the working directory. */
|
|
22
|
+
export declare function projectSteps(options?: {
|
|
23
|
+
projectDir?: string;
|
|
24
|
+
config?: SamuraiTestConfig;
|
|
25
|
+
}): Promise<StepSet>;
|
|
26
|
+
export {};
|