@midscene/test 1.12.4-beta-20260903111111.0 → 1.12.4
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/es/android/index.mjs +50 -106
- package/dist/es/android/index.mjs.map +1 -1
- package/dist/es/cli/index.mjs +99 -149
- package/dist/es/cli/index.mjs.map +1 -1
- package/dist/es/harmony/index.mjs +119 -0
- package/dist/es/harmony/index.mjs.map +1 -0
- package/dist/es/index.mjs +36 -87
- package/dist/es/index.mjs.map +1 -1
- package/dist/es/ios/index.mjs +51 -101
- package/dist/es/ios/index.mjs.map +1 -1
- package/dist/es/midscene/index.mjs +37 -235
- package/dist/es/midscene/index.mjs.map +1 -1
- package/dist/es/playwright/index.mjs +14 -10
- package/dist/es/playwright/index.mjs.map +1 -1
- package/dist/es/test-cli.mjs +102 -150
- package/dist/es/test-cli.mjs.map +1 -1
- package/dist/lib/android/index.js +151 -178
- package/dist/lib/android/index.js.map +1 -1
- package/dist/lib/cli/index.js +99 -149
- package/dist/lib/cli/index.js.map +1 -1
- package/dist/lib/harmony/index.js +188 -0
- package/dist/lib/harmony/index.js.map +1 -0
- package/dist/lib/index.js +37 -100
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/ios/index.js +152 -173
- package/dist/lib/ios/index.js.map +1 -1
- package/dist/lib/midscene/index.js +264 -412
- package/dist/lib/midscene/index.js.map +1 -1
- package/dist/lib/playwright/index.js +14 -10
- package/dist/lib/playwright/index.js.map +1 -1
- package/dist/lib/test-cli.js +102 -150
- package/dist/lib/test-cli.js.map +1 -1
- package/dist/types/android/index.d.ts +27 -101
- package/dist/types/cli/index.d.ts +9 -32
- package/dist/types/harmony/index.d.ts +124 -0
- package/dist/types/index.d.ts +18 -45
- package/dist/types/ios/index.d.ts +28 -107
- package/dist/types/midscene/index.d.ts +54 -153
- package/dist/types/playwright/index.d.ts +9 -63
- package/package.json +25 -6
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { harmonyAgentTestRunnerNodeDefinitions, launchInputSchema, runHdcShellInputSchema, terminateInputSchema } from "@midscene/harmony/test-runner";
|
|
2
|
+
import "@midscene/core/agent/test-runner";
|
|
3
|
+
import { z } from "zod/v4";
|
|
4
|
+
function _define_property(obj, key, value) {
|
|
5
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
6
|
+
value: value,
|
|
7
|
+
enumerable: true,
|
|
8
|
+
configurable: true,
|
|
9
|
+
writable: true
|
|
10
|
+
});
|
|
11
|
+
else obj[key] = value;
|
|
12
|
+
return obj;
|
|
13
|
+
}
|
|
14
|
+
class WorkflowError extends Error {
|
|
15
|
+
toJSON() {
|
|
16
|
+
return {
|
|
17
|
+
name: this.name,
|
|
18
|
+
message: this.message,
|
|
19
|
+
code: this.code,
|
|
20
|
+
...void 0 === this.details ? {} : {
|
|
21
|
+
details: this.details
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
constructor(message, options = {}){
|
|
26
|
+
super(message, {
|
|
27
|
+
cause: options.cause
|
|
28
|
+
}), _define_property(this, "code", void 0), _define_property(this, "details", void 0);
|
|
29
|
+
this.name = new.target.name;
|
|
30
|
+
this.code = options.code ?? 'WORKFLOW_ERROR';
|
|
31
|
+
this.details = options.details;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
class errors_NodeDefinitionError extends WorkflowError {
|
|
35
|
+
constructor(message, details){
|
|
36
|
+
super(message, {
|
|
37
|
+
code: 'NODE_DEFINITION_ERROR',
|
|
38
|
+
details
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const validateOptionalText = (value, field, node)=>{
|
|
43
|
+
if (void 0 !== value && ('string' != typeof value || 0 === value.trim().length)) throw new errors_NodeDefinitionError(`Node "${node}" ${field} must be a non-empty string.`, {
|
|
44
|
+
node,
|
|
45
|
+
field
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
const validateInputSchema = (schema, node)=>{
|
|
49
|
+
if (void 0 === schema) return;
|
|
50
|
+
if (!(schema instanceof z.ZodObject)) throw new errors_NodeDefinitionError(`Node "${node}" inputSchema must be a Zod object schema.`, {
|
|
51
|
+
node,
|
|
52
|
+
field: 'inputSchema'
|
|
53
|
+
});
|
|
54
|
+
if ('$' in schema.shape) throw new errors_NodeDefinitionError(`Node "${node}" inputSchema must not declare "$" as an input property.`, {
|
|
55
|
+
node,
|
|
56
|
+
field: 'inputSchema.$'
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
const validateDefinition = (options)=>{
|
|
60
|
+
if (!options || 'object' != typeof options) throw new errors_NodeDefinitionError('Node definition must be an object.');
|
|
61
|
+
if ('string' != typeof options.name || 0 === options.name.trim().length) throw new errors_NodeDefinitionError('Node name must be a non-empty string.');
|
|
62
|
+
validateOptionalText(options.title, 'title', options.name);
|
|
63
|
+
validateOptionalText(options.description, "description", options.name);
|
|
64
|
+
if (void 0 !== options.stringInputKey && false !== options.stringInputKey && ('string' != typeof options.stringInputKey || 0 === options.stringInputKey.trim().length)) throw new errors_NodeDefinitionError(`Node "${options.name}" stringInputKey must be a non-empty string or false.`, {
|
|
65
|
+
node: options.name,
|
|
66
|
+
field: 'stringInputKey'
|
|
67
|
+
});
|
|
68
|
+
validateInputSchema(options.inputSchema, options.name);
|
|
69
|
+
if ('string' == typeof options.stringInputKey && options.inputSchema instanceof z.ZodObject && !(options.stringInputKey in options.inputSchema.shape)) throw new errors_NodeDefinitionError(`Node "${options.name}" stringInputKey must reference a field in inputSchema.`, {
|
|
70
|
+
node: options.name,
|
|
71
|
+
field: 'stringInputKey'
|
|
72
|
+
});
|
|
73
|
+
if ('function' != typeof options.execute) throw new errors_NodeDefinitionError(`Node "${options.name}" must provide an execute function.`, {
|
|
74
|
+
node: options.name
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
function define_node_defineNode(options) {
|
|
78
|
+
validateDefinition(options);
|
|
79
|
+
return options;
|
|
80
|
+
}
|
|
81
|
+
const nonBlankPrompt = (description)=>z.string().regex(/\S/, 'prompt must contain a non-whitespace character').describe(description);
|
|
82
|
+
z.strictObject({
|
|
83
|
+
duration: z.number().positive().describe('How long to wait.'),
|
|
84
|
+
unit: z["enum"]([
|
|
85
|
+
'ms',
|
|
86
|
+
's',
|
|
87
|
+
'min'
|
|
88
|
+
]).default('ms').describe('Duration unit: milliseconds, seconds, or minutes.')
|
|
89
|
+
});
|
|
90
|
+
z.strictObject({
|
|
91
|
+
prompt: nonBlankPrompt('A self-contained task, including allowed tools and success conditions.')
|
|
92
|
+
});
|
|
93
|
+
const createAgentTestRunnerNodes = (definitions, getAgent)=>definitions.map((definition)=>define_node_defineNode({
|
|
94
|
+
name: definition.name,
|
|
95
|
+
...void 0 === definition.title ? {} : {
|
|
96
|
+
title: definition.title
|
|
97
|
+
},
|
|
98
|
+
...void 0 === definition.description ? {} : {
|
|
99
|
+
description: definition.description
|
|
100
|
+
},
|
|
101
|
+
...void 0 === definition.stringInputKey ? {} : {
|
|
102
|
+
stringInputKey: definition.stringInputKey
|
|
103
|
+
},
|
|
104
|
+
inputSchema: definition.inputSchema,
|
|
105
|
+
async execute (ctx) {
|
|
106
|
+
const agent = await getAgent(ctx);
|
|
107
|
+
return definition.execute(agent, ctx.input, {
|
|
108
|
+
signal: ctx.signal
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}));
|
|
112
|
+
function createHarmonyNodes(options) {
|
|
113
|
+
if (!options || 'object' != typeof options) throw new errors_NodeDefinitionError('createHarmonyNodes() options must be an object.');
|
|
114
|
+
if ('function' != typeof options.getAgent) throw new errors_NodeDefinitionError('createHarmonyNodes() requires getAgent().');
|
|
115
|
+
return createAgentTestRunnerNodes(harmonyAgentTestRunnerNodeDefinitions, options.getAgent);
|
|
116
|
+
}
|
|
117
|
+
export { createHarmonyNodes, launchInputSchema, runHdcShellInputSchema, terminateInputSchema };
|
|
118
|
+
|
|
119
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"harmony/index.mjs","sources":["../../../src/errors.ts","../../../src/node/define-node.ts","../../../src/midscene/index.ts","../../../src/harmony/index.ts"],"sourcesContent":["import type { z } from 'zod/v4';\n\nexport interface WorkflowErrorOptions {\n code?: string;\n details?: unknown;\n cause?: unknown;\n}\n\nexport class WorkflowError extends Error {\n readonly code: string;\n readonly details?: unknown;\n\n constructor(message: string, options: WorkflowErrorOptions = {}) {\n super(message, { cause: options.cause });\n this.name = new.target.name;\n this.code = options.code ?? 'WORKFLOW_ERROR';\n this.details = options.details;\n }\n\n toJSON(): Record<string, unknown> {\n return {\n name: this.name,\n message: this.message,\n code: this.code,\n ...(this.details === undefined ? {} : { details: this.details }),\n };\n }\n}\n\nexport class WorkflowParseError extends WorkflowError {\n constructor(message: string, details?: unknown, cause?: unknown) {\n super(message, { code: 'WORKFLOW_PARSE_ERROR', details, cause });\n }\n}\n\nexport class NodeDefinitionError extends WorkflowError {\n constructor(message: string, details?: unknown) {\n super(message, { code: 'NODE_DEFINITION_ERROR', details });\n }\n}\n\nexport class DuplicateNodeError extends WorkflowError {\n readonly node: string;\n\n constructor(node: string) {\n super(`Node \"${node}\" is already registered.`, {\n code: 'DUPLICATE_NODE',\n details: { node },\n });\n this.node = node;\n }\n}\n\nexport class NodeNotFoundError extends WorkflowError {\n readonly node: string;\n\n constructor(node: string) {\n super(`Node \"${node}\" is not registered.`, {\n code: 'NODE_NOT_FOUND',\n details: { node },\n });\n this.node = node;\n }\n}\n\nexport class NodeInputValidationError extends WorkflowError {\n constructor(message: string, details?: unknown) {\n super(message, { code: 'NODE_INPUT_VALIDATION_ERROR', details });\n }\n\n static fromZod(node: string, error: z.ZodError): NodeInputValidationError {\n const issues = error.issues.map((issue) => ({\n code: issue.code,\n path: issue.path.map(String).join('.'),\n message: issue.message,\n }));\n const firstIssue = issues[0];\n const path = firstIssue?.path || '<root>';\n const message = firstIssue?.message ?? 'invalid input';\n return new NodeInputValidationError(\n `Node \"${node}\" input validation failed at \"${path}\": ${message}`,\n { node, issues },\n );\n }\n}\n\nexport class StepTimeoutError extends WorkflowError {\n readonly timeoutMs: number;\n readonly node?: string;\n\n constructor(timeoutMs: number, node?: string) {\n super(\n node\n ? `Node \"${node}\" timed out after ${timeoutMs}ms.`\n : `Step timed out after ${timeoutMs}ms.`,\n {\n code: 'STEP_TIMEOUT',\n details: { timeoutMs, ...(node === undefined ? {} : { node }) },\n },\n );\n this.timeoutMs = timeoutMs;\n this.node = node;\n }\n}\n\nexport class NodeExecutionError extends WorkflowError {\n readonly node: string;\n\n constructor(node: string, cause: unknown) {\n const causeMessage =\n cause instanceof Error ? cause.message : String(cause ?? 'Unknown error');\n super(`Node \"${node}\" failed: ${causeMessage}`, {\n code: 'NODE_EXECUTION_ERROR',\n details: { node },\n cause,\n });\n this.node = node;\n }\n}\n\nexport class WorkflowLifecycleError extends WorkflowError {\n constructor(message: string, details?: unknown) {\n super(message, { code: 'WORKFLOW_LIFECYCLE_ERROR', details });\n }\n}\n\nexport class ProjectSetupError extends WorkflowError {\n constructor(cause: unknown, details: { projectName: string }) {\n const causeMessage =\n cause instanceof Error ? cause.message : String(cause ?? 'Unknown error');\n super(`Project \"${details.projectName}\" setup failed: ${causeMessage}`, {\n code: 'PROJECT_SETUP_ERROR',\n details,\n cause,\n });\n }\n}\n\nexport class ProjectTeardownError extends WorkflowError {\n constructor(\n cause: unknown,\n details: { projectName: string; registrationIndex: number },\n ) {\n const causeMessage =\n cause instanceof Error ? cause.message : String(cause ?? 'Unknown error');\n super(`Project \"${details.projectName}\" teardown failed: ${causeMessage}`, {\n code: 'PROJECT_TEARDOWN_ERROR',\n details,\n cause,\n });\n }\n}\n\nexport class NodeScopeTeardownError extends WorkflowError {\n constructor(\n cause: unknown,\n details: {\n scope: 'case' | 'document';\n scopeId: string;\n node: string;\n registrationIndex: number;\n },\n ) {\n const causeMessage =\n cause instanceof Error ? cause.message : String(cause ?? 'Unknown error');\n super(\n `${details.scope === 'case' ? 'Case attempt' : 'Workflow document'} node teardown failed for \"${details.node}\": ${causeMessage}`,\n { code: 'NODE_SCOPE_TEARDOWN_ERROR', details, cause },\n );\n }\n}\n\nexport class FatalDeviceError extends WorkflowError {\n constructor(message: string, cause?: unknown) {\n super(message, { code: 'FATAL_DEVICE_ERROR', cause });\n }\n}\n\nexport const isFatalDeviceError = (error: unknown): boolean => {\n if (error instanceof FatalDeviceError) return true;\n if (error instanceof WorkflowError && error.code === 'FATAL_DEVICE_ERROR') {\n return true;\n }\n if (\n error instanceof Error &&\n /device offline|device not found|(?:adb|bdc|device) connection (?:was )?closed/i.test(\n error.message,\n )\n ) {\n return true;\n }\n return error instanceof Error && error.cause !== undefined\n ? isFatalDeviceError(error.cause)\n : false;\n};\n\nexport class CaseExecutionError extends WorkflowError {\n readonly result: import('./engine/types').CaseRunResult;\n\n constructor(result: import('./engine/types').CaseRunResult) {\n super(`Case \"${result.name}\" failed.`, {\n code: 'CASE_EXECUTION_FAILED',\n details: { caseId: result.caseId, runId: result.runId },\n });\n this.result = result;\n }\n}\n\nexport class WorkflowDocumentExecutionError extends WorkflowError {\n readonly result: import('./engine/types').WorkflowDocumentRunResult;\n\n constructor(result: import('./engine/types').WorkflowDocumentRunResult) {\n super(`Workflow document \"${result.sourcePath}\" failed.`, {\n code: 'WORKFLOW_DOCUMENT_EXECUTION_FAILED',\n details: {\n documentId: result.documentId,\n documentRunId: result.documentRunId,\n },\n });\n this.result = result;\n }\n}\n\nexport function normalizeNodeExecutionError(\n error: unknown,\n node: string,\n): WorkflowError {\n return error instanceof WorkflowError\n ? error\n : new NodeExecutionError(node, error);\n}\n","import { z } from 'zod/v4';\nimport { NodeDefinitionError } from '../errors';\nimport type {\n DefineNodeOptions,\n DefineNodeWithSchemaOptions,\n NodeDefinition,\n NodeDefinitionWithSchema,\n NodeInputSchema,\n} from './types';\n\nconst validateOptionalText = (\n value: unknown,\n field: 'title' | 'description',\n node: string,\n): void => {\n if (\n value !== undefined &&\n (typeof value !== 'string' || value.trim().length === 0)\n ) {\n throw new NodeDefinitionError(\n `Node \"${node}\" ${field} must be a non-empty string.`,\n { node, field },\n );\n }\n};\n\nconst validateInputSchema = (schema: unknown, node: string): void => {\n if (schema === undefined) return;\n if (!(schema instanceof z.ZodObject)) {\n throw new NodeDefinitionError(\n `Node \"${node}\" inputSchema must be a Zod object schema.`,\n { node, field: 'inputSchema' },\n );\n }\n if ('$' in schema.shape) {\n throw new NodeDefinitionError(\n `Node \"${node}\" inputSchema must not declare \"$\" as an input property.`,\n { node, field: 'inputSchema.$' },\n );\n }\n};\n\nconst validateDefinition = (options: {\n name: string;\n title?: unknown;\n description?: unknown;\n stringInputKey?: unknown;\n inputSchema?: unknown;\n execute: unknown;\n}): void => {\n if (!options || typeof options !== 'object') {\n throw new NodeDefinitionError('Node definition must be an object.');\n }\n\n if (typeof options.name !== 'string' || options.name.trim().length === 0) {\n throw new NodeDefinitionError('Node name must be a non-empty string.');\n }\n\n validateOptionalText(options.title, 'title', options.name);\n validateOptionalText(options.description, 'description', options.name);\n if (\n options.stringInputKey !== undefined &&\n options.stringInputKey !== false &&\n (typeof options.stringInputKey !== 'string' ||\n options.stringInputKey.trim().length === 0)\n ) {\n throw new NodeDefinitionError(\n `Node \"${options.name}\" stringInputKey must be a non-empty string or false.`,\n { node: options.name, field: 'stringInputKey' },\n );\n }\n validateInputSchema(options.inputSchema, options.name);\n\n if (\n typeof options.stringInputKey === 'string' &&\n options.inputSchema instanceof z.ZodObject &&\n !(options.stringInputKey in options.inputSchema.shape)\n ) {\n throw new NodeDefinitionError(\n `Node \"${options.name}\" stringInputKey must reference a field in inputSchema.`,\n { node: options.name, field: 'stringInputKey' },\n );\n }\n\n if (typeof options.execute !== 'function') {\n throw new NodeDefinitionError(\n `Node \"${options.name}\" must provide an execute function.`,\n { node: options.name },\n );\n }\n};\n\nexport function defineNode<\n TSchema extends NodeInputSchema,\n TData = unknown,\n TContext = unknown,\n>(\n options: DefineNodeWithSchemaOptions<TSchema, TData, TContext>,\n): NodeDefinitionWithSchema<TSchema, TData, TContext>;\n\nexport function defineNode<\n TInput = unknown,\n TData = unknown,\n TContext = unknown,\n>(\n options: DefineNodeOptions<TInput, TData, TContext>,\n): NodeDefinition<TInput, TData, TContext>;\n\nexport function defineNode(\n options: DefineNodeOptions<any, any, any>,\n): NodeDefinition<any, any, any> {\n validateDefinition(options);\n return options;\n}\n","import {\n type AgentTestRunnerNodeDefinition,\n type AgentTestRunnerNodeProvider,\n type CommonAgentTestRunnerApi,\n aiActInputSchema,\n aiActOptionsInputSchema,\n aiAssertInputSchema,\n aiAssertOptionsInputSchema,\n aiTapInputSchema,\n insightInputSchema,\n insightOptionsInputSchema,\n locateOptionsInputSchema,\n promptImageInputSchema,\n recordToReportInputSchema,\n recordToReportOptionsInputSchema,\n reportScreenshotInputSchema,\n structuredUserPromptInputSchema,\n userPromptInputSchema,\n} from '@midscene/core/agent/test-runner';\nimport { z } from 'zod/v4';\nimport type { Awaitable } from '../engine/types';\nimport { NodeDefinitionError, NodeExecutionError } from '../errors';\nimport { defineNode } from '../node/define-node';\nimport type {\n NodeDefinition,\n NodeExecutionContext,\n NodeResult,\n} from '../node/types';\n\nexport type MidsceneUIAgent = CommonAgentTestRunnerApi;\n\nexport {\n aiActInputSchema,\n aiActOptionsInputSchema,\n aiAssertInputSchema,\n aiAssertOptionsInputSchema,\n aiTapInputSchema,\n insightInputSchema,\n insightOptionsInputSchema,\n locateOptionsInputSchema,\n promptImageInputSchema,\n recordToReportInputSchema,\n recordToReportOptionsInputSchema,\n reportScreenshotInputSchema,\n structuredUserPromptInputSchema,\n userPromptInputSchema,\n};\n\nexport interface AgentProvider<TContext> {\n getAgent(\n runId: string,\n ctx: NodeExecutionContext<unknown, TContext>,\n ): Awaitable<MidsceneUIAgent>;\n // biome-ignore lint/suspicious/noConfusingVoidType: providers without a report intentionally return void.\n releaseAgent?(runId: string): Awaitable<AgentReleaseResult | void>;\n dispose?(): Awaitable<void>;\n}\n\nexport interface AgentReleaseResult {\n /** Absolute path to the finalized report for this Agent scope. */\n reportPath?: string;\n}\n\nexport interface AgentExecutorInput<TContext> {\n prompt: string;\n context: TContext;\n signal: AbortSignal;\n execution:\n | { scope: 'case'; runId: string }\n | { scope: 'document'; runId: string };\n}\n\nexport interface AgentExecutor<TContext> {\n // biome-ignore lint/suspicious/noConfusingVoidType: executors may perform side effects without returning a summary.\n execute(input: AgentExecutorInput<TContext>): Awaitable<NodeResult | void>;\n}\n\nconst nonBlankPrompt = (description: string) =>\n z\n .string()\n .regex(/\\S/, 'prompt must contain a non-whitespace character')\n .describe(description);\n\nexport const waitInputSchema = z.strictObject({\n duration: z.number().positive().describe('How long to wait.'),\n unit: z\n .enum(['ms', 's', 'min'])\n .default('ms')\n .describe('Duration unit: milliseconds, seconds, or minutes.'),\n});\n\nexport const agentInputSchema = z.strictObject({\n prompt: nonBlankPrompt(\n 'A self-contained task, including allowed tools and success conditions.',\n ),\n});\n\nexport type AiActNodeInput = z.infer<typeof aiActInputSchema>;\nexport type AiAssertNodeInput = z.infer<typeof aiAssertInputSchema>;\nexport type AiTapNodeInput = z.infer<typeof aiTapInputSchema>;\nexport type InsightNodeInput = z.infer<typeof insightInputSchema>;\nexport type RecordToReportNodeInput = z.infer<typeof recordToReportInputSchema>;\nexport type WaitNodeInput = z.infer<typeof waitInputSchema>;\nexport type AgentNodeInput = z.infer<typeof agentInputSchema>;\n\nexport interface CreateMidsceneNodesOptions<TContext> {\n getAgent?(\n ctx: NodeExecutionContext<unknown, TContext>,\n ): Awaitable<MidsceneUIAgent>;\n agentProvider?: AgentProvider<TContext>;\n /** Agent class that declares the Agent-backed Nodes to register. */\n agentClass: AgentTestRunnerNodeProvider;\n agentExecutor?: AgentExecutor<TContext>;\n}\n\nconst waitFor = async (durationMs: number, signal: AbortSignal) => {\n if (signal.aborted) {\n throw signal.reason ?? new Error('Wait aborted.');\n }\n await new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(() => {\n signal.removeEventListener('abort', abort);\n resolve();\n }, durationMs);\n const abort = () => {\n clearTimeout(timeout);\n reject(signal.reason ?? new Error('Wait aborted.'));\n };\n signal.addEventListener('abort', abort, { once: true });\n });\n};\n\n/** Adapt Agent-owned descriptions to executable Test Runner Nodes. */\nexport const createAgentTestRunnerNodes = <TContext>(\n definitions: readonly AgentTestRunnerNodeDefinition[],\n getAgent: (\n ctx: NodeExecutionContext<unknown, TContext>,\n ) => Awaitable<unknown>,\n): readonly NodeDefinition<any, any, TContext>[] =>\n definitions.map((definition) =>\n defineNode({\n name: definition.name,\n ...(definition.title === undefined ? {} : { title: definition.title }),\n ...(definition.description === undefined\n ? {}\n : { description: definition.description }),\n ...(definition.stringInputKey === undefined\n ? {}\n : { stringInputKey: definition.stringInputKey }),\n inputSchema: definition.inputSchema,\n async execute(ctx) {\n const agent = await getAgent(ctx);\n return definition.execute(agent, ctx.input, {\n signal: ctx.signal,\n });\n },\n }),\n );\n\nexport function createMidsceneNodes<TContext>(\n options: CreateMidsceneNodesOptions<TContext>,\n): readonly NodeDefinition<any, any, TContext>[] {\n if (!options || typeof options !== 'object') {\n throw new NodeDefinitionError(\n 'createMidsceneNodes() options must be an object.',\n );\n }\n if (\n typeof options.agentProvider?.getAgent !== 'function' &&\n typeof options.getAgent !== 'function'\n ) {\n throw new NodeDefinitionError(\n 'createMidsceneNodes() requires getAgent or agentProvider.getAgent.',\n );\n }\n if (\n !options.agentClass ||\n typeof options.agentClass.getTestRunnerNodeDefinitions !== 'function'\n ) {\n throw new NodeDefinitionError(\n 'createMidsceneNodes() requires agentClass.getTestRunnerNodeDefinitions().',\n );\n }\n\n const registeredAgentScopes = new Set<string>();\n const getExecutionId = (ctx: NodeExecutionContext<unknown, TContext>) =>\n ctx.scope === 'case' ? ctx.case.runId : ctx.document.documentRunId;\n const getAgent = async (\n ctx: NodeExecutionContext<unknown, TContext>,\n ): Promise<MidsceneUIAgent> => {\n if (!options.agentProvider) return options.getAgent!(ctx);\n const runId = getExecutionId(ctx);\n if (\n options.agentProvider.releaseAgent &&\n !registeredAgentScopes.has(runId)\n ) {\n registeredAgentScopes.add(runId);\n ctx.onTeardown(async () => {\n try {\n const released = await options.agentProvider!.releaseAgent!(runId);\n return released?.reportPath\n ? { reportPaths: [released.reportPath] }\n : undefined;\n } finally {\n registeredAgentScopes.delete(runId);\n }\n });\n }\n return options.agentProvider.getAgent(runId, ctx);\n };\n\n const agentDefinitions = options.agentClass.getTestRunnerNodeDefinitions();\n\n return [\n ...createAgentTestRunnerNodes(agentDefinitions, getAgent),\n defineNode<typeof waitInputSchema, unknown, TContext>({\n name: 'wait',\n description: 'Wait for a fixed duration while honoring cancellation.',\n stringInputKey: false,\n inputSchema: waitInputSchema,\n async execute(ctx) {\n const multiplier =\n ctx.input.unit === 'min'\n ? 60_000\n : ctx.input.unit === 's'\n ? 1_000\n : 1;\n const durationMs = ctx.input.duration * multiplier;\n await waitFor(durationMs, ctx.signal);\n return { summary: `Waited ${durationMs}ms` };\n },\n }),\n defineNode<typeof agentInputSchema, unknown, TContext>({\n name: 'agent',\n description:\n 'Execute one self-contained natural-language task with an injected Agent executor.',\n stringInputKey: 'prompt',\n inputSchema: agentInputSchema,\n async execute(ctx) {\n if (!options.agentExecutor) {\n throw new NodeExecutionError(\n 'agent',\n new TypeError('createMidsceneNodes() requires an agentExecutor.'),\n );\n }\n const execution =\n ctx.scope === 'case'\n ? { scope: 'case' as const, runId: ctx.case.runId }\n : {\n scope: 'document' as const,\n runId: ctx.document.documentRunId,\n };\n const result = await options.agentExecutor.execute({\n prompt: ctx.input.prompt,\n context: ctx.context,\n signal: ctx.signal,\n execution,\n });\n return result ?? { summary: 'Agent task completed.' };\n },\n }),\n ];\n}\n","import type { HarmonyAgent } from '@midscene/harmony';\nimport {\n harmonyAgentTestRunnerNodeDefinitions,\n launchInputSchema,\n runHdcShellInputSchema,\n terminateInputSchema,\n} from '@midscene/harmony/test-runner';\nexport type {\n LaunchNodeInput,\n RunHdcShellNodeInput,\n TerminateNodeInput,\n} from '@midscene/harmony/test-runner';\nimport type { Awaitable } from '../engine/types';\nimport { NodeDefinitionError } from '../errors';\nimport { createAgentTestRunnerNodes } from '../midscene';\nimport type { NodeDefinition, NodeExecutionContext } from '../node/types';\n\nexport { launchInputSchema, runHdcShellInputSchema, terminateInputSchema };\n\nexport type HarmonyRunnerAgent = Pick<\n HarmonyAgent,\n 'launch' | 'terminate' | 'runHdcShell' | 'back' | 'home' | 'recentApps'\n>;\n\nexport interface CreateHarmonyNodesOptions<TContext> {\n getAgent(\n ctx: NodeExecutionContext<unknown, TContext>,\n ): Awaitable<HarmonyRunnerAgent>;\n}\n\nexport function createHarmonyNodes<TContext>(\n options: CreateHarmonyNodesOptions<TContext>,\n): readonly NodeDefinition<any, any, TContext>[] {\n if (!options || typeof options !== 'object') {\n throw new NodeDefinitionError(\n 'createHarmonyNodes() options must be an object.',\n );\n }\n if (typeof options.getAgent !== 'function') {\n throw new NodeDefinitionError('createHarmonyNodes() requires getAgent().');\n }\n\n return createAgentTestRunnerNodes(\n harmonyAgentTestRunnerNodeDefinitions,\n options.getAgent,\n );\n}\n"],"names":["WorkflowError","Error","undefined","message","options","NodeDefinitionError","details","validateOptionalText","value","field","node","validateInputSchema","schema","z","validateDefinition","defineNode","nonBlankPrompt","description","createAgentTestRunnerNodes","definitions","getAgent","definition","ctx","agent","createHarmonyNodes","harmonyAgentTestRunnerNodeDefinitions"],"mappings":"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"}
|
package/dist/es/index.mjs
CHANGED
|
@@ -239,13 +239,7 @@ function normalizeMeta(value, index) {
|
|
|
239
239
|
continueOnError: continueOnError ?? false
|
|
240
240
|
};
|
|
241
241
|
}
|
|
242
|
-
function
|
|
243
|
-
if (void 0 !== input.prompt && 'string' != typeof input.prompt) throw new WorkflowParseError(`${formatStep(index)} "prompt" must be a string.`, {
|
|
244
|
-
index,
|
|
245
|
-
prompt: input.prompt
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
function normalizeStep(value, index = 0) {
|
|
242
|
+
function normalizeStep(value, index = 0, resolveNode) {
|
|
249
243
|
if (!isMapping(value)) throw new WorkflowParseError(`${formatStep(index)} must be a mapping.`, {
|
|
250
244
|
index
|
|
251
245
|
});
|
|
@@ -258,30 +252,37 @@ function normalizeStep(value, index = 0) {
|
|
|
258
252
|
if (0 === node.trim().length) throw new WorkflowParseError(`${formatStep(index)} node name must not be empty.`, {
|
|
259
253
|
index
|
|
260
254
|
});
|
|
261
|
-
if ('string' == typeof rawValue)
|
|
262
|
-
node
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
255
|
+
if ('string' == typeof rawValue) {
|
|
256
|
+
const resolvedNode = resolveNode?.(node);
|
|
257
|
+
const stringInputKey = resolvedNode ? resolvedNode.stringInputKey : 'prompt';
|
|
258
|
+
if (false === stringInputKey || void 0 === stringInputKey) throw new WorkflowParseError(`${formatStep(index)} node "${node}" does not accept string shorthand.`, {
|
|
259
|
+
index,
|
|
260
|
+
node
|
|
261
|
+
});
|
|
262
|
+
return {
|
|
263
|
+
node,
|
|
264
|
+
input: {
|
|
265
|
+
[stringInputKey]: rawValue
|
|
266
|
+
},
|
|
267
|
+
meta: {
|
|
268
|
+
continueOnError: false
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
}
|
|
270
272
|
if (!isMapping(rawValue)) throw new WorkflowParseError(`${formatStep(index)} value must be a string shorthand or a mapping.`, {
|
|
271
273
|
index,
|
|
272
274
|
node
|
|
273
275
|
});
|
|
274
276
|
const { $: rawMeta, ...input } = rawValue;
|
|
275
|
-
validateCommonNodeInput(input, index);
|
|
276
277
|
return {
|
|
277
278
|
node,
|
|
278
279
|
input,
|
|
279
280
|
meta: normalizeMeta(rawMeta, index)
|
|
280
281
|
};
|
|
281
282
|
}
|
|
282
|
-
function normalizeSteps(steps) {
|
|
283
|
+
function normalizeSteps(steps, resolveNode) {
|
|
283
284
|
if (!Array.isArray(steps)) throw new WorkflowParseError('Steps must be an array.');
|
|
284
|
-
return steps.map((step, index)=>normalizeStep(step, index));
|
|
285
|
+
return steps.map((step, index)=>normalizeStep(step, index, resolveNode));
|
|
285
286
|
}
|
|
286
287
|
const validateOptionalText = (value, field, node)=>{
|
|
287
288
|
if (void 0 !== value && ('string' != typeof value || 0 === value.trim().length)) throw new NodeDefinitionError(`Node "${node}" ${field} must be a non-empty string.`, {
|
|
@@ -305,7 +306,15 @@ const validateDefinition = (options)=>{
|
|
|
305
306
|
if ('string' != typeof options.name || 0 === options.name.trim().length) throw new NodeDefinitionError('Node name must be a non-empty string.');
|
|
306
307
|
validateOptionalText(options.title, 'title', options.name);
|
|
307
308
|
validateOptionalText(options.description, "description", options.name);
|
|
309
|
+
if (void 0 !== options.stringInputKey && false !== options.stringInputKey && ('string' != typeof options.stringInputKey || 0 === options.stringInputKey.trim().length)) throw new NodeDefinitionError(`Node "${options.name}" stringInputKey must be a non-empty string or false.`, {
|
|
310
|
+
node: options.name,
|
|
311
|
+
field: 'stringInputKey'
|
|
312
|
+
});
|
|
308
313
|
validateInputSchema(options.inputSchema, options.name);
|
|
314
|
+
if ('string' == typeof options.stringInputKey && options.inputSchema instanceof z.ZodObject && !(options.stringInputKey in options.inputSchema.shape)) throw new NodeDefinitionError(`Node "${options.name}" stringInputKey must reference a field in inputSchema.`, {
|
|
315
|
+
node: options.name,
|
|
316
|
+
field: 'stringInputKey'
|
|
317
|
+
});
|
|
309
318
|
if ('function' != typeof options.execute) throw new NodeDefinitionError(`Node "${options.name}" must provide an execute function.`, {
|
|
310
319
|
node: options.name
|
|
311
320
|
});
|
|
@@ -392,45 +401,6 @@ const assertJsonSerializable = (value, label, node)=>{
|
|
|
392
401
|
throw new NodeExecutionError(node, error);
|
|
393
402
|
}
|
|
394
403
|
};
|
|
395
|
-
const cloneAndFreezeJson = (value)=>{
|
|
396
|
-
if (Array.isArray(value)) return Object.freeze(value.map(cloneAndFreezeJson));
|
|
397
|
-
if ('object' == typeof value && null !== value) return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, child])=>[
|
|
398
|
-
key,
|
|
399
|
-
cloneAndFreezeJson(child)
|
|
400
|
-
])));
|
|
401
|
-
return value;
|
|
402
|
-
};
|
|
403
|
-
const toHistoryEntry = (result, scope)=>{
|
|
404
|
-
assertJsonSerializable(result.input, 'input', result.node);
|
|
405
|
-
if (result.output?.data !== void 0) assertJsonSerializable(result.output.data, 'output.data', result.node);
|
|
406
|
-
return Object.freeze({
|
|
407
|
-
scope,
|
|
408
|
-
phase: result.phase,
|
|
409
|
-
stepIndex: result.stepIndex,
|
|
410
|
-
node: result.node,
|
|
411
|
-
input: cloneAndFreezeJson(result.input),
|
|
412
|
-
...'string' == typeof result.input?.prompt ? {
|
|
413
|
-
intent: result.input.prompt
|
|
414
|
-
} : {},
|
|
415
|
-
status: 'success' === result.status ? 'passed' : 'failed',
|
|
416
|
-
...result.output?.summary === void 0 ? {} : {
|
|
417
|
-
summary: result.output.summary
|
|
418
|
-
},
|
|
419
|
-
...result.output?.data === void 0 ? {} : {
|
|
420
|
-
data: cloneAndFreezeJson(result.output.data)
|
|
421
|
-
},
|
|
422
|
-
...result.error ? {
|
|
423
|
-
error: Object.freeze({
|
|
424
|
-
name: result.error.name,
|
|
425
|
-
message: result.error.message
|
|
426
|
-
})
|
|
427
|
-
} : {}
|
|
428
|
-
});
|
|
429
|
-
};
|
|
430
|
-
const createHistory = (prefix, results, scope)=>Object.freeze([
|
|
431
|
-
...prefix,
|
|
432
|
-
...results.map((result)=>toHistoryEntry(result, scope))
|
|
433
|
-
]);
|
|
434
404
|
function validateNodeOutput(output, node) {
|
|
435
405
|
if (void 0 === output) return;
|
|
436
406
|
if ('object' != typeof output || null === output || Array.isArray(output)) throw new NodeExecutionError(node, new TypeError('Node output must be an object or undefined.'));
|
|
@@ -452,7 +422,6 @@ function createStepResultBase(step, startedAt, phase, stepIndex) {
|
|
|
452
422
|
};
|
|
453
423
|
}
|
|
454
424
|
async function executeNode(step, execute, phase, stepIndex, options) {
|
|
455
|
-
validateCommonNodeInput(step.input, stepIndex);
|
|
456
425
|
const startedAt = new Date();
|
|
457
426
|
const abortController = new AbortController();
|
|
458
427
|
const parentSignal = options.parentSignal;
|
|
@@ -536,7 +505,6 @@ async function executeStep(step, node, target, context, execution = {}) {
|
|
|
536
505
|
$: step.meta,
|
|
537
506
|
signal,
|
|
538
507
|
context,
|
|
539
|
-
history: execution.history ?? Object.freeze([]),
|
|
540
508
|
onTeardown: (teardown)=>{
|
|
541
509
|
if (!execution.onTeardown) throw new NodeExecutionError(step.node, new Error('The current execution scope cannot register teardown.'));
|
|
542
510
|
execution.onTeardown(step.node, teardown);
|
|
@@ -584,7 +552,6 @@ async function runCollectedCase(collectedCase, options) {
|
|
|
584
552
|
const beforeEach = [];
|
|
585
553
|
const steps = [];
|
|
586
554
|
const afterEach = [];
|
|
587
|
-
const completedNodes = [];
|
|
588
555
|
const teardownStack = [];
|
|
589
556
|
let acceptingTeardowns = true;
|
|
590
557
|
const onTeardown = (node, teardown)=>{
|
|
@@ -613,13 +580,7 @@ async function runCollectedCase(collectedCase, options) {
|
|
|
613
580
|
sourcePath: collectedCase.sourcePath,
|
|
614
581
|
caseIndex: collectedCase.caseIndex,
|
|
615
582
|
phase,
|
|
616
|
-
stepIndex
|
|
617
|
-
completedSteps: Object.freeze([
|
|
618
|
-
...steps
|
|
619
|
-
]),
|
|
620
|
-
completedNodes: Object.freeze([
|
|
621
|
-
...completedNodes
|
|
622
|
-
])
|
|
583
|
+
stepIndex
|
|
623
584
|
};
|
|
624
585
|
const stepInfo = {
|
|
625
586
|
scope: 'case',
|
|
@@ -632,14 +593,12 @@ async function runCollectedCase(collectedCase, options) {
|
|
|
632
593
|
scope: 'case',
|
|
633
594
|
case: caseContext
|
|
634
595
|
}, options.context, {
|
|
635
|
-
history: createHistory(options.documentHistory ?? [], completedNodes, 'case'),
|
|
636
596
|
signal,
|
|
637
597
|
defaultTimeoutMs: options.defaultTimeoutMs,
|
|
638
598
|
onTeardown
|
|
639
599
|
});
|
|
640
600
|
await options.onStepResult?.(stepInfo, result);
|
|
641
601
|
results.push(result);
|
|
642
|
-
completedNodes.push(result);
|
|
643
602
|
if ('failed' === result.status && !result.continuedAfterError) break;
|
|
644
603
|
}
|
|
645
604
|
};
|
|
@@ -679,7 +638,7 @@ async function runCollectedCase(collectedCase, options) {
|
|
|
679
638
|
name: collectedCase.definition.name,
|
|
680
639
|
sourcePath: collectedCase.sourcePath,
|
|
681
640
|
caseIndex: collectedCase.caseIndex,
|
|
682
|
-
status:
|
|
641
|
+
status: beforeEach.some((step)=>'failed' === step.status) || steps.some((step)=>'failed' === step.status) || afterEach.some((step)=>'failed' === step.status) || teardownErrors.length > 0 ? 'failed' : 'success',
|
|
683
642
|
beforeEach,
|
|
684
643
|
steps,
|
|
685
644
|
afterEach,
|
|
@@ -722,7 +681,7 @@ class CaseRunner {
|
|
|
722
681
|
definition: {
|
|
723
682
|
name: input.name ?? 'case',
|
|
724
683
|
tags: input.tags ?? [],
|
|
725
|
-
steps: normalizeSteps(input.steps)
|
|
684
|
+
steps: normalizeSteps(input.steps, (name)=>this.registry.get(name))
|
|
726
685
|
}
|
|
727
686
|
};
|
|
728
687
|
const result = await runCollectedCase(collectedCase, {
|
|
@@ -749,7 +708,6 @@ function createDocumentRuntime(document, options) {
|
|
|
749
708
|
const startedAt = new Date();
|
|
750
709
|
const beforeAll = [];
|
|
751
710
|
const afterAll = [];
|
|
752
|
-
const completedNodes = [];
|
|
753
711
|
const nodes = {
|
|
754
712
|
beforeAll: document.lifecycle.beforeAll.map((step)=>options.resolveNode(step.node)),
|
|
755
713
|
afterAll: document.lifecycle.afterAll.map((step)=>options.resolveNode(step.node))
|
|
@@ -780,7 +738,7 @@ function createDocumentRuntime(document, options) {
|
|
|
780
738
|
projectId: document.projectId,
|
|
781
739
|
projectName: project.name,
|
|
782
740
|
sourcePath: document.sourcePath,
|
|
783
|
-
status:
|
|
741
|
+
status: beforeAll.some((step)=>'failed' === step.status) || afterAll.some((step)=>'failed' === step.status) || teardownErrors.length > 0 ? 'failed' : 'success',
|
|
784
742
|
startedAt: startedAt.toISOString(),
|
|
785
743
|
endedAt: endedAt.toISOString(),
|
|
786
744
|
durationMs: Math.max(0, endedAt.getTime() - startedAt.getTime()),
|
|
@@ -809,10 +767,7 @@ function createDocumentRuntime(document, options) {
|
|
|
809
767
|
projectName: project.name,
|
|
810
768
|
sourcePath: document.sourcePath,
|
|
811
769
|
phase,
|
|
812
|
-
stepIndex
|
|
813
|
-
completedNodes: Object.freeze([
|
|
814
|
-
...completedNodes
|
|
815
|
-
])
|
|
770
|
+
stepIndex
|
|
816
771
|
};
|
|
817
772
|
const stepInfo = {
|
|
818
773
|
scope: 'document',
|
|
@@ -825,7 +780,6 @@ function createDocumentRuntime(document, options) {
|
|
|
825
780
|
scope: 'document',
|
|
826
781
|
document: documentContext
|
|
827
782
|
}, context, {
|
|
828
|
-
history: createHistory([], completedNodes, 'document'),
|
|
829
783
|
signal: executionSignal,
|
|
830
784
|
defaultTimeoutMs: options.defaultTimeoutMs,
|
|
831
785
|
onTeardown (node, teardown) {
|
|
@@ -848,7 +802,6 @@ function createDocumentRuntime(document, options) {
|
|
|
848
802
|
});
|
|
849
803
|
await options.onStepResult?.(stepInfo, result);
|
|
850
804
|
results.push(result);
|
|
851
|
-
completedNodes.push(result);
|
|
852
805
|
if ('failed' === result.status && !result.continuedAfterError) break;
|
|
853
806
|
}
|
|
854
807
|
};
|
|
@@ -856,9 +809,6 @@ function createDocumentRuntime(document, options) {
|
|
|
856
809
|
get context () {
|
|
857
810
|
return context;
|
|
858
811
|
},
|
|
859
|
-
get history () {
|
|
860
|
-
return createHistory([], completedNodes, 'document');
|
|
861
|
-
},
|
|
862
812
|
get canRunCases () {
|
|
863
813
|
return started && !signal.aborted && !beforeAll.some((step)=>'failed' === step.status);
|
|
864
814
|
},
|
|
@@ -953,7 +903,6 @@ async function runWorkflowDocument(document, options) {
|
|
|
953
903
|
context: runtime.context,
|
|
954
904
|
projectName,
|
|
955
905
|
attemptIndex,
|
|
956
|
-
documentHistory: runtime.history,
|
|
957
906
|
signal: options.signal,
|
|
958
907
|
defaultTimeoutMs: options.defaultTimeoutMs,
|
|
959
908
|
onStepStart: options.onStepStart,
|
|
@@ -1214,7 +1163,7 @@ function collectWorkflowDocument(source, options) {
|
|
|
1214
1163
|
if (!Array.isArray(value)) throw new WorkflowParseError(`Workflow document ${phase} must be an array.`, {
|
|
1215
1164
|
phase
|
|
1216
1165
|
});
|
|
1217
|
-
return normalizeSteps(value).map((normalized, stepIndex)=>{
|
|
1166
|
+
return normalizeSteps(value, options.resolveNode).map((normalized, stepIndex)=>{
|
|
1218
1167
|
const resolved = options.resolveNode(normalized.node);
|
|
1219
1168
|
if (!resolved) throw new WorkflowParseError(`Workflow document ${phase} step ${stepIndex + 1} references unknown node "${normalized.node}".`, {
|
|
1220
1169
|
phase,
|
|
@@ -1250,7 +1199,7 @@ function collectWorkflowDocument(source, options) {
|
|
|
1250
1199
|
if (!Array.isArray(tags) || tags.some((tag)=>'string' != typeof tag || 0 === tag.trim().length)) throw new WorkflowParseError(`Case ${caseIndex + 1} tags must be an array of non-empty strings.`, {
|
|
1251
1200
|
caseIndex
|
|
1252
1201
|
});
|
|
1253
|
-
const steps = normalizeSteps(definition.steps).map((normalized, stepIndex)=>{
|
|
1202
|
+
const steps = normalizeSteps(definition.steps, options.resolveNode).map((normalized, stepIndex)=>{
|
|
1254
1203
|
if (!options.resolveNode(normalized.node)) throw new WorkflowParseError(`Case ${caseIndex + 1} step ${stepIndex + 1} references unknown node "${normalized.node}".`, {
|
|
1255
1204
|
caseIndex,
|
|
1256
1205
|
stepIndex,
|
|
@@ -1283,6 +1232,6 @@ function collectWorkflowDocument(source, options) {
|
|
|
1283
1232
|
cases
|
|
1284
1233
|
};
|
|
1285
1234
|
}
|
|
1286
|
-
export { CaseExecutionError, CaseRunner, DuplicateNodeError, FatalDeviceError, NodeDefinitionError, NodeExecutionError, NodeInputValidationError, NodeNotFoundError, NodeRegistry, NodeScopeTeardownError, ProjectSetupError, ProjectTeardownError, StepTimeoutError, WorkflowDocumentExecutionError, WorkflowError, WorkflowLifecycleError, WorkflowParseError,
|
|
1235
|
+
export { CaseExecutionError, CaseRunner, DuplicateNodeError, FatalDeviceError, NodeDefinitionError, NodeExecutionError, NodeInputValidationError, NodeNotFoundError, NodeRegistry, NodeScopeTeardownError, ProjectSetupError, ProjectTeardownError, StepTimeoutError, WorkflowDocumentExecutionError, WorkflowError, WorkflowLifecycleError, WorkflowParseError, collectWorkflowDocument, createCaseId, createCaseRunner, createDocumentRuntime, createProjectRuntime, createWorkflowDocumentId, defineNode, isFatalDeviceError, normalizeNodeExecutionError, normalizeStep, normalizeSteps, resolveWorkflowVariables, runWorkflowDocument, z };
|
|
1287
1236
|
|
|
1288
1237
|
//# sourceMappingURL=index.mjs.map
|