@midscene/test 1.12.6-beta-20260910093036.0 → 1.12.6
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/1~891.mjs +19 -39
- package/dist/es/1~891.mjs.map +1 -1
- package/dist/es/cli/index.mjs +26 -44
- package/dist/es/cli/index.mjs.map +1 -1
- package/dist/es/index.mjs +0 -3
- package/dist/es/index.mjs.map +1 -1
- package/dist/es/midscene/index.mjs +43 -32
- package/dist/es/midscene/index.mjs.map +1 -1
- package/dist/es/test-cli.mjs +37 -49
- package/dist/es/test-cli.mjs.map +1 -1
- package/dist/lib/0~458.js +19 -39
- package/dist/lib/0~458.js.map +1 -1
- package/dist/lib/cli/index.js +26 -44
- package/dist/lib/cli/index.js.map +1 -1
- package/dist/lib/index.js +0 -3
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/midscene/index.js +253 -262
- package/dist/lib/midscene/index.js.map +1 -1
- package/dist/lib/test-cli.js +37 -49
- package/dist/lib/test-cli.js.map +1 -1
- package/dist/types/cli/index.d.ts +12 -13
- package/dist/types/index.d.ts +1 -7
- package/dist/types/midscene/index.d.ts +21 -21
- package/package.json +8 -48
- package/dist/es/android/index.mjs +0 -182
- package/dist/es/android/index.mjs.map +0 -1
- package/dist/es/harmony/index.mjs +0 -182
- package/dist/es/harmony/index.mjs.map +0 -1
- package/dist/es/ios/index.mjs +0 -182
- package/dist/es/ios/index.mjs.map +0 -1
- package/dist/es/playwright/index.mjs +0 -366
- package/dist/es/playwright/index.mjs.map +0 -1
- package/dist/lib/android/index.js +0 -255
- package/dist/lib/android/index.js.map +0 -1
- package/dist/lib/harmony/index.js +0 -252
- package/dist/lib/harmony/index.js.map +0 -1
- package/dist/lib/ios/index.js +0 -255
- package/dist/lib/ios/index.js.map +0 -1
- package/dist/lib/playwright/index.js +0 -412
- package/dist/lib/playwright/index.js.map +0 -1
- package/dist/types/android/index.d.ts +0 -140
- package/dist/types/harmony/index.d.ts +0 -137
- package/dist/types/ios/index.d.ts +0 -143
- package/dist/types/playwright/index.d.ts +0 -229
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { aiActInputSchema, aiActOptionsInputSchema, aiAssertInputSchema, aiAssertOptionsInputSchema, aiTapInputSchema, insightInputSchema, insightOptionsInputSchema, locateOptionsInputSchema, promptImageInputSchema, recordToReportInputSchema, recordToReportOptionsInputSchema, reportScreenshotInputSchema, structuredUserPromptInputSchema, userPromptInputSchema } from "@midscene/core/agent/test
|
|
1
|
+
import { aiActInputSchema, aiActOptionsInputSchema, aiAssertInputSchema, aiAssertOptionsInputSchema, aiTapInputSchema, insightInputSchema, insightOptionsInputSchema, locateOptionsInputSchema, promptImageInputSchema, recordToReportInputSchema, recordToReportOptionsInputSchema, reportScreenshotInputSchema, structuredUserPromptInputSchema, userPromptInputSchema } from "@midscene/core/agent/test";
|
|
2
2
|
import { getDebug } from "@midscene/shared/logger";
|
|
3
3
|
import { z } from "zod/v4";
|
|
4
4
|
function _define_property(obj, key, value) {
|
|
@@ -117,14 +117,19 @@ const waitFor = async (durationMs, signal)=>{
|
|
|
117
117
|
};
|
|
118
118
|
const createAgentTestRunnerNodes = (definitions, getAgent)=>{
|
|
119
119
|
const activeAgentCalls = new WeakMap();
|
|
120
|
-
const runAgentCall = async (node,
|
|
121
|
-
|
|
120
|
+
const runAgentCall = async (node, execution, agent, call, captureExecutions = true)=>{
|
|
121
|
+
execution.signal.throwIfAborted();
|
|
122
|
+
const scopeId = 'case' === execution.scope ? execution.case.runId : execution.document.documentRunId;
|
|
122
123
|
const activeCall = activeAgentCalls.get(agent);
|
|
123
124
|
if (activeCall) throw new NodeExecutionError(node, new Error(`The same Agent instance cannot execute overlapping Midscene Test Steps. Active scope: ${activeCall.scopeId} (${activeCall.node}); requested scope: ${scopeId} (${node}).`));
|
|
124
|
-
|
|
125
|
+
const currentCall = {
|
|
125
126
|
scopeId,
|
|
126
127
|
node
|
|
127
|
-
}
|
|
128
|
+
};
|
|
129
|
+
activeAgentCalls.set(agent, currentCall);
|
|
130
|
+
const releaseCall = ()=>{
|
|
131
|
+
if (activeAgentCalls.get(agent) === currentCall) activeAgentCalls.delete(agent);
|
|
132
|
+
};
|
|
128
133
|
let removeListener;
|
|
129
134
|
let sawExecutionWithoutId = false;
|
|
130
135
|
const stopListening = ()=>{
|
|
@@ -132,32 +137,38 @@ const createAgentTestRunnerNodes = (definitions, getAgent)=>{
|
|
|
132
137
|
removeListener = void 0;
|
|
133
138
|
remove?.();
|
|
134
139
|
};
|
|
135
|
-
const stopListeningOnAbort = ()=>
|
|
140
|
+
const stopListeningOnAbort = ()=>{
|
|
141
|
+
try {
|
|
142
|
+
stopListening();
|
|
143
|
+
} finally{
|
|
144
|
+
releaseCall();
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
execution.signal.addEventListener('abort', stopListeningOnAbort, {
|
|
148
|
+
once: true
|
|
149
|
+
});
|
|
136
150
|
try {
|
|
137
151
|
if (captureExecutions && agent.addDumpUpdateListener) {
|
|
138
|
-
removeListener = agent.addDumpUpdateListener((_dump,
|
|
139
|
-
if (!
|
|
140
|
-
if (!
|
|
152
|
+
removeListener = agent.addDumpUpdateListener((_dump, executionRef)=>{
|
|
153
|
+
if (!executionRef) return;
|
|
154
|
+
if (!executionRef.id) {
|
|
141
155
|
sawExecutionWithoutId = true;
|
|
142
156
|
return;
|
|
143
157
|
}
|
|
144
|
-
|
|
158
|
+
execution.report.addTrace({
|
|
145
159
|
type: 'midscene-execution',
|
|
146
|
-
executionId:
|
|
160
|
+
executionId: executionRef.id
|
|
147
161
|
});
|
|
148
162
|
});
|
|
149
|
-
if (
|
|
150
|
-
else ctx.signal.addEventListener('abort', stopListeningOnAbort, {
|
|
151
|
-
once: true
|
|
152
|
-
});
|
|
163
|
+
if (execution.signal.aborted) stopListening();
|
|
153
164
|
}
|
|
154
165
|
return await call();
|
|
155
166
|
} finally{
|
|
156
|
-
|
|
167
|
+
execution.signal.removeEventListener('abort', stopListeningOnAbort);
|
|
157
168
|
try {
|
|
158
169
|
stopListening();
|
|
159
170
|
} finally{
|
|
160
|
-
|
|
171
|
+
releaseCall();
|
|
161
172
|
}
|
|
162
173
|
if (sawExecutionWithoutId) warnReportTrace(`Skipped a report trace without a stable execution id for ${node} in scope ${scopeId}.`);
|
|
163
174
|
}
|
|
@@ -174,13 +185,13 @@ const createAgentTestRunnerNodes = (definitions, getAgent)=>{
|
|
|
174
185
|
stringInputKey: definition.stringInputKey
|
|
175
186
|
},
|
|
176
187
|
inputSchema: definition.inputSchema,
|
|
177
|
-
async execute (
|
|
178
|
-
const agent = await getAgent(
|
|
179
|
-
const call = async ()=>definition.execute(agent,
|
|
180
|
-
signal:
|
|
188
|
+
async execute (execution) {
|
|
189
|
+
const agent = await getAgent(execution);
|
|
190
|
+
const call = async ()=>definition.execute(agent, execution.input, {
|
|
191
|
+
signal: execution.signal
|
|
181
192
|
});
|
|
182
193
|
if ('object' != typeof agent || null === agent) return call();
|
|
183
|
-
return runAgentCall(definition.name,
|
|
194
|
+
return runAgentCall(definition.name, execution, agent, call);
|
|
184
195
|
}
|
|
185
196
|
}));
|
|
186
197
|
};
|
|
@@ -192,13 +203,13 @@ function createMidsceneNodes(options) {
|
|
|
192
203
|
if ('function' != typeof options.agentProvider?.getAgent && 'function' != typeof options.getAgent) throw new NodeDefinitionError('createMidsceneNodes() requires getAgent or agentProvider.getAgent.');
|
|
193
204
|
if (!options.agentClass || 'function' != typeof options.agentClass.getTestRunnerNodeDefinitions) throw new NodeDefinitionError('createMidsceneNodes() requires agentClass.getTestRunnerNodeDefinitions().');
|
|
194
205
|
const registeredAgentScopes = new Set();
|
|
195
|
-
const getExecutionId = (
|
|
196
|
-
const getAgent = async (
|
|
197
|
-
if (!options.agentProvider) return options.getAgent(
|
|
198
|
-
const runId = getExecutionId(
|
|
206
|
+
const getExecutionId = (execution)=>'case' === execution.scope ? execution.case.runId : execution.document.documentRunId;
|
|
207
|
+
const getAgent = async (execution)=>{
|
|
208
|
+
if (!options.agentProvider) return options.getAgent(execution);
|
|
209
|
+
const runId = getExecutionId(execution);
|
|
199
210
|
if (options.agentProvider.releaseAgent && !registeredAgentScopes.has(runId)) {
|
|
200
211
|
registeredAgentScopes.add(runId);
|
|
201
|
-
|
|
212
|
+
execution.onTeardown(async ()=>{
|
|
202
213
|
try {
|
|
203
214
|
const released = await options.agentProvider.releaseAgent(runId);
|
|
204
215
|
return released?.reportPath ? {
|
|
@@ -211,7 +222,7 @@ function createMidsceneNodes(options) {
|
|
|
211
222
|
}
|
|
212
223
|
});
|
|
213
224
|
}
|
|
214
|
-
return options.agentProvider.getAgent(runId,
|
|
225
|
+
return options.agentProvider.getAgent(runId, execution);
|
|
215
226
|
};
|
|
216
227
|
const agentDefinitions = options.agentClass.getTestRunnerNodeDefinitions();
|
|
217
228
|
return [
|
|
@@ -221,10 +232,10 @@ function createMidsceneNodes(options) {
|
|
|
221
232
|
description: 'Wait for a fixed duration while honoring cancellation.',
|
|
222
233
|
stringInputKey: false,
|
|
223
234
|
inputSchema: waitInputSchema,
|
|
224
|
-
async execute (
|
|
225
|
-
const multiplier = 'min' ===
|
|
226
|
-
const durationMs =
|
|
227
|
-
await waitFor(durationMs,
|
|
235
|
+
async execute (execution) {
|
|
236
|
+
const multiplier = 'min' === execution.input.unit ? 60000 : 's' === execution.input.unit ? 1000 : 1;
|
|
237
|
+
const durationMs = execution.input.duration * multiplier;
|
|
238
|
+
await waitFor(durationMs, execution.signal);
|
|
228
239
|
return {
|
|
229
240
|
summary: `Waited ${durationMs}ms`
|
|
230
241
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"midscene/index.mjs","sources":["../../../src/errors.ts","../../../src/node/define-node.ts","../../../src/midscene/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 { getDebug } from '@midscene/shared/logger';\nimport { z } from 'zod/v4';\nimport type { Awaitable } from '../engine/types';\nimport { NodeDefinitionError, NodeExecutionError } from '../errors';\nimport { defineNode } from '../node/define-node';\nimport type { NodeDefinition, NodeExecutionContext } from '../node/types';\n\nexport type MidsceneUIAgent = CommonAgentTestRunnerApi & {\n addDumpUpdateListener?(\n listener: (dump: string, execution?: MidsceneExecutionRef) => void,\n ): () => void;\n};\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 MidsceneExecutionRef {\n id?: string;\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 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 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>;\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}\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 Midscene Test 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 const activeAgentCalls = new WeakMap<\n MidsceneUIAgent,\n { scopeId: string; node: string }\n >();\n const runAgentCall = async <T>(\n node: string,\n ctx: NodeExecutionContext<unknown, TContext>,\n agent: MidsceneUIAgent,\n call: () => Promise<T>,\n captureExecutions = true,\n ): Promise<T> => {\n const scopeId =\n ctx.scope === 'case' ? ctx.case.runId : ctx.document.documentRunId;\n const activeCall = activeAgentCalls.get(agent);\n if (activeCall) {\n throw new NodeExecutionError(\n node,\n new Error(\n `The same Agent instance cannot execute overlapping Midscene Test Steps. Active scope: ${activeCall.scopeId} (${activeCall.node}); requested scope: ${scopeId} (${node}).`,\n ),\n );\n }\n\n activeAgentCalls.set(agent, { scopeId, node });\n let removeListener: (() => void) | undefined;\n let sawExecutionWithoutId = false;\n const stopListening = () => {\n const remove = removeListener;\n removeListener = undefined;\n remove?.();\n };\n const stopListeningOnAbort = () => stopListening();\n try {\n if (captureExecutions && agent.addDumpUpdateListener) {\n removeListener = agent.addDumpUpdateListener((_dump, execution) => {\n if (!execution) return;\n if (!execution.id) {\n sawExecutionWithoutId = true;\n return;\n }\n ctx.report.addTrace({\n type: 'midscene-execution',\n executionId: execution.id,\n });\n });\n if (ctx.signal.aborted) stopListening();\n else {\n ctx.signal.addEventListener('abort', stopListeningOnAbort, {\n once: true,\n });\n }\n }\n return await call();\n } finally {\n ctx.signal.removeEventListener('abort', stopListeningOnAbort);\n try {\n stopListening();\n } finally {\n activeAgentCalls.delete(agent);\n }\n if (sawExecutionWithoutId) {\n warnReportTrace(\n `Skipped a report trace without a stable execution id for ${node} in scope ${scopeId}.`,\n );\n }\n }\n };\n\n return 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 const call = async () =>\n definition.execute(agent, ctx.input, { signal: ctx.signal });\n if (typeof agent !== 'object' || agent === null) return call();\n return runAgentCall(\n definition.name,\n ctx,\n agent as MidsceneUIAgent,\n call,\n );\n },\n }),\n );\n};\n\nconst warnReportTrace = getDebug('test-runner:report-trace', {\n console: true,\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 ];\n}\n"],"names":["WorkflowError","Error","undefined","message","options","NodeDefinitionError","details","NodeExecutionError","node","cause","causeMessage","String","validateOptionalText","value","field","validateInputSchema","schema","z","validateDefinition","defineNode","waitInputSchema","waitFor","durationMs","signal","Promise","resolve","reject","timeout","setTimeout","abort","clearTimeout","createAgentTestRunnerNodes","definitions","getAgent","activeAgentCalls","WeakMap","runAgentCall","ctx","agent","call","captureExecutions","scopeId","activeCall","removeListener","sawExecutionWithoutId","stopListening","remove","stopListeningOnAbort","_dump","execution","warnReportTrace","definition","getDebug","createMidsceneNodes","registeredAgentScopes","Set","getExecutionId","runId","released","agentDefinitions","multiplier"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"midscene/index.mjs","sources":["../../../src/errors.ts","../../../src/node/define-node.ts","../../../src/midscene/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';\nimport { getDebug } from '@midscene/shared/logger';\nimport { z } from 'zod/v4';\nimport type { Awaitable } from '../engine/types';\nimport { NodeDefinitionError, NodeExecutionError } from '../errors';\nimport { defineNode } from '../node/define-node';\nimport type { NodeDefinition, NodeExecutionContext } from '../node/types';\n\nexport type MidsceneUIAgent = CommonAgentTestRunnerApi & {\n addDumpUpdateListener?(\n listener: (dump: string, execution?: MidsceneExecutionRef) => void,\n ): () => void;\n};\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 MidsceneExecutionRef {\n id?: string;\n}\n\nexport interface AgentProvider<TContext> {\n getAgent(\n runId: string,\n execution: 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 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 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>;\n\nexport interface CreateMidsceneNodesOptions<TContext> {\n getAgent?(\n execution: 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}\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 Midscene Test Nodes. */\nexport const createAgentTestRunnerNodes = <TContext>(\n definitions: readonly AgentTestRunnerNodeDefinition[],\n getAgent: (\n execution: NodeExecutionContext<unknown, TContext>,\n ) => Awaitable<unknown>,\n): readonly NodeDefinition<any, any, TContext>[] => {\n const activeAgentCalls = new WeakMap<\n MidsceneUIAgent,\n { scopeId: string; node: string }\n >();\n const runAgentCall = async <T>(\n node: string,\n execution: NodeExecutionContext<unknown, TContext>,\n agent: MidsceneUIAgent,\n call: () => Promise<T>,\n captureExecutions = true,\n ): Promise<T> => {\n execution.signal.throwIfAborted();\n const scopeId =\n execution.scope === 'case'\n ? execution.case.runId\n : execution.document.documentRunId;\n const activeCall = activeAgentCalls.get(agent);\n if (activeCall) {\n throw new NodeExecutionError(\n node,\n new Error(\n `The same Agent instance cannot execute overlapping Midscene Test Steps. Active scope: ${activeCall.scopeId} (${activeCall.node}); requested scope: ${scopeId} (${node}).`,\n ),\n );\n }\n\n const currentCall = { scopeId, node };\n activeAgentCalls.set(agent, currentCall);\n const releaseCall = () => {\n // An aborted call may finish after another Step has acquired this Agent.\n if (activeAgentCalls.get(agent) === currentCall) {\n activeAgentCalls.delete(agent);\n }\n };\n let removeListener: (() => void) | undefined;\n let sawExecutionWithoutId = false;\n const stopListening = () => {\n const remove = removeListener;\n removeListener = undefined;\n remove?.();\n };\n const stopListeningOnAbort = () => {\n try {\n stopListening();\n } finally {\n releaseCall();\n }\n };\n execution.signal.addEventListener('abort', stopListeningOnAbort, {\n once: true,\n });\n try {\n if (captureExecutions && agent.addDumpUpdateListener) {\n removeListener = agent.addDumpUpdateListener((_dump, executionRef) => {\n if (!executionRef) return;\n if (!executionRef.id) {\n sawExecutionWithoutId = true;\n return;\n }\n execution.report.addTrace({\n type: 'midscene-execution',\n executionId: executionRef.id,\n });\n });\n if (execution.signal.aborted) stopListening();\n }\n return await call();\n } finally {\n execution.signal.removeEventListener('abort', stopListeningOnAbort);\n try {\n stopListening();\n } finally {\n releaseCall();\n }\n if (sawExecutionWithoutId) {\n warnReportTrace(\n `Skipped a report trace without a stable execution id for ${node} in scope ${scopeId}.`,\n );\n }\n }\n };\n\n return 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(execution) {\n const agent = await getAgent(execution);\n const call = async () =>\n definition.execute(agent, execution.input, {\n signal: execution.signal,\n });\n if (typeof agent !== 'object' || agent === null) return call();\n return runAgentCall(\n definition.name,\n execution,\n agent as MidsceneUIAgent,\n call,\n );\n },\n }),\n );\n};\n\nconst warnReportTrace = getDebug('test-runner:report-trace', {\n console: true,\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 = (\n execution: NodeExecutionContext<unknown, TContext>,\n ) =>\n execution.scope === 'case'\n ? execution.case.runId\n : execution.document.documentRunId;\n const getAgent = async (\n execution: NodeExecutionContext<unknown, TContext>,\n ): Promise<MidsceneUIAgent> => {\n if (!options.agentProvider) return options.getAgent!(execution);\n const runId = getExecutionId(execution);\n if (\n options.agentProvider.releaseAgent &&\n !registeredAgentScopes.has(runId)\n ) {\n registeredAgentScopes.add(runId);\n execution.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, execution);\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(execution) {\n const multiplier =\n execution.input.unit === 'min'\n ? 60_000\n : execution.input.unit === 's'\n ? 1_000\n : 1;\n const durationMs = execution.input.duration * multiplier;\n await waitFor(durationMs, execution.signal);\n return { summary: `Waited ${durationMs}ms` };\n },\n }),\n ];\n}\n"],"names":["WorkflowError","Error","undefined","message","options","NodeDefinitionError","details","NodeExecutionError","node","cause","causeMessage","String","validateOptionalText","value","field","validateInputSchema","schema","z","validateDefinition","defineNode","waitInputSchema","waitFor","durationMs","signal","Promise","resolve","reject","timeout","setTimeout","abort","clearTimeout","createAgentTestRunnerNodes","definitions","getAgent","activeAgentCalls","WeakMap","runAgentCall","execution","agent","call","captureExecutions","scopeId","activeCall","currentCall","releaseCall","removeListener","sawExecutionWithoutId","stopListening","remove","stopListeningOnAbort","_dump","executionRef","warnReportTrace","definition","getDebug","createMidsceneNodes","registeredAgentScopes","Set","getExecutionId","runId","released","agentDefinitions","multiplier"],"mappings":"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"}
|
package/dist/es/test-cli.mjs
CHANGED
|
@@ -117,12 +117,12 @@ const renderWorkflowLocations = (location)=>{
|
|
|
117
117
|
sections.push(`**Config file:** ${location.configPath ? `\`${location.configPath}\`` : 'No config file loaded.'}`);
|
|
118
118
|
for (const project of location.projects){
|
|
119
119
|
const patterns = project.files.include.map((pattern)=>`\`${pattern}\``).join(', ');
|
|
120
|
-
const projectLabel = location.projects.length > 1 ? ` (Execution Project: ${project.name})` : '';
|
|
120
|
+
const projectLabel = location.projects.length > 1 || 'default' !== project.name ? ` (Execution Project: ${project.name})` : '';
|
|
121
121
|
const excludes = project.files.exclude?.length ? `; excludes: ${project.files.exclude.map((pattern)=>`\`${pattern}\``).join(', ')}` : '';
|
|
122
122
|
sections.push(`**Case files:** ${patterns}${projectLabel}${excludes}`);
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
|
-
sections.push('Files matching `files.exclude` are not selected. Midscene Test always ignores `.git/`, `.midscene/`, `midscene_run/`, and `node_modules/` directories, including nested ones.', 'Call a Node in `cases[].steps`, or in the Workflow Document lifecycle steps `beforeAll`, `beforeEach`, `afterEach`, and `afterAll`.
|
|
125
|
+
sections.push('Files matching `files.exclude` are not selected. Midscene Test always ignores `.git/`, `.midscene/`, `midscene_run/`, and `node_modules/` directories, including nested ones.', 'Call a Node in `cases[].steps`, or in the Workflow Document lifecycle steps `beforeAll`, `beforeEach`, `afterEach`, and `afterAll`. Root Nodes are shared defaults; `projects[].nodes` adds local Nodes and overrides same-named root Nodes only within that Execution Project. Each Execution Project supplies its own runtime environment.');
|
|
126
126
|
return sections.join('\n\n');
|
|
127
127
|
};
|
|
128
128
|
const WORKFLOW_STEP_CONTRACT = `## Workflow Step Contract
|
|
@@ -579,26 +579,15 @@ const validateVariables = (value, label)=>{
|
|
|
579
579
|
assertJsonValue(value, label, new Set());
|
|
580
580
|
return deepFreezeJson(value);
|
|
581
581
|
};
|
|
582
|
-
const
|
|
583
|
-
'web',
|
|
584
|
-
'android',
|
|
585
|
-
'ios',
|
|
586
|
-
'harmony',
|
|
587
|
-
'computer'
|
|
588
|
-
]);
|
|
589
|
-
const validatePlatform = (value, label)=>{
|
|
590
|
-
if ('string' != typeof value || !platforms.has(value)) throw new TypeError(`Midscene config ${label} must be one of web, android, ios, harmony, computer.`);
|
|
591
|
-
return value;
|
|
592
|
-
};
|
|
593
|
-
const validateProjectSetup = (value, projectPlatform, label)=>{
|
|
582
|
+
const validateProjectSetup = (value, label)=>{
|
|
594
583
|
if (void 0 === value) return;
|
|
595
584
|
if (!isRecord(value)) throw new TypeError(`Midscene config ${label} must be an object.`);
|
|
585
|
+
rejectUnknownKeys(value, [
|
|
586
|
+
'name',
|
|
587
|
+
'setup'
|
|
588
|
+
], label);
|
|
596
589
|
if ('string' != typeof value.name || 0 === value.name.trim().length) throw new TypeError(`Midscene config ${label}.name must be non-empty.`);
|
|
597
590
|
if ('function' != typeof value.setup) throw new TypeError(`Midscene config ${label}.setup must be a function.`);
|
|
598
|
-
const supported = void 0 === value.platform ? void 0 : Array.isArray(value.platform) ? value.platform.map((item, index)=>validatePlatform(item, `${label}.platform[${index}]`)) : [
|
|
599
|
-
validatePlatform(value.platform, `${label}.platform`)
|
|
600
|
-
];
|
|
601
|
-
if (supported && !supported.includes(projectPlatform)) throw new TypeError(`Midscene config ${label} does not support project platform "${projectPlatform}".`);
|
|
602
591
|
return value;
|
|
603
592
|
};
|
|
604
593
|
const validatePositiveInteger = (value, fallback, label)=>{
|
|
@@ -611,24 +600,15 @@ const validateNonNegativeInteger = (value, fallback, label)=>{
|
|
|
611
600
|
if (!Number.isInteger(value) || value < 0) throw new TypeError(`Midscene config ${label} must be a non-negative integer.`);
|
|
612
601
|
return value;
|
|
613
602
|
};
|
|
614
|
-
const validateExecutionProjects = (value, defaultSetup)=>{
|
|
603
|
+
const validateExecutionProjects = (value, defaultSetup, globalNodes)=>{
|
|
615
604
|
if (void 0 === value) {
|
|
616
|
-
|
|
617
|
-
if (void 0 !== defaultSetup) {
|
|
618
|
-
if (!isRecord(defaultSetup)) throw new TypeError('Midscene config setup must be an object.');
|
|
619
|
-
if (Array.isArray(defaultSetup.platform)) {
|
|
620
|
-
if (1 !== defaultSetup.platform.length) throw new TypeError('Midscene config setup.platform must select exactly one platform when projects is omitted.');
|
|
621
|
-
platform = validatePlatform(defaultSetup.platform[0], 'setup.platform[0]');
|
|
622
|
-
} else if (void 0 !== defaultSetup.platform) platform = validatePlatform(defaultSetup.platform, 'setup.platform');
|
|
623
|
-
}
|
|
624
|
-
const setup = validateProjectSetup(defaultSetup, platform, 'setup');
|
|
605
|
+
const setup = validateProjectSetup(defaultSetup, 'setup');
|
|
625
606
|
return {
|
|
626
607
|
hasExplicitProjects: false,
|
|
627
608
|
projects: Object.freeze([
|
|
628
609
|
Object.freeze({
|
|
629
610
|
projectId: projectIdFromIndex(0),
|
|
630
611
|
name: 'default',
|
|
631
|
-
platform,
|
|
632
612
|
...setup ? {
|
|
633
613
|
setup
|
|
634
614
|
} : {},
|
|
@@ -637,7 +617,8 @@ const validateExecutionProjects = (value, defaultSetup)=>{
|
|
|
637
617
|
exclude: []
|
|
638
618
|
}),
|
|
639
619
|
retry: 0,
|
|
640
|
-
variables: Object.freeze({})
|
|
620
|
+
variables: Object.freeze({}),
|
|
621
|
+
nodes: new NodeRegistry(globalNodes.definitions())
|
|
641
622
|
})
|
|
642
623
|
])
|
|
643
624
|
};
|
|
@@ -650,8 +631,8 @@ const validateExecutionProjects = (value, defaultSetup)=>{
|
|
|
650
631
|
if (!isRecord(candidate)) throw new TypeError(`Midscene config ${label} must be an object.`);
|
|
651
632
|
rejectUnknownKeys(candidate, [
|
|
652
633
|
'name',
|
|
653
|
-
'platform',
|
|
654
634
|
'setup',
|
|
635
|
+
'nodes',
|
|
655
636
|
'files',
|
|
656
637
|
'tags',
|
|
657
638
|
'retry',
|
|
@@ -660,12 +641,17 @@ const validateExecutionProjects = (value, defaultSetup)=>{
|
|
|
660
641
|
if ('string' != typeof candidate.name || 0 === candidate.name.trim().length) throw new TypeError(`Midscene config ${label}.name must be non-empty.`);
|
|
661
642
|
if (names.has(candidate.name)) throw new TypeError(`Midscene config project name "${candidate.name}" must be unique.`);
|
|
662
643
|
names.add(candidate.name);
|
|
663
|
-
const platform = validatePlatform(candidate.platform, `${label}.platform`);
|
|
664
644
|
const files = validateTestFileSelection(candidate.files, `${label}.files`);
|
|
645
|
+
if (void 0 !== candidate.nodes && !Array.isArray(candidate.nodes)) throw new TypeError(`Midscene config ${label}.nodes must be an array.`);
|
|
646
|
+
const localNodes = new NodeRegistry(candidate.nodes);
|
|
647
|
+
const nodes = new NodeRegistry([
|
|
648
|
+
...globalNodes.definitions().filter((node)=>!localNodes.has(node.name)),
|
|
649
|
+
...localNodes.definitions()
|
|
650
|
+
]);
|
|
665
651
|
return Object.freeze({
|
|
666
652
|
projectId: projectIdFromIndex(index),
|
|
667
653
|
name: candidate.name,
|
|
668
|
-
|
|
654
|
+
nodes,
|
|
669
655
|
...files ? {
|
|
670
656
|
files
|
|
671
657
|
} : {},
|
|
@@ -673,7 +659,7 @@ const validateExecutionProjects = (value, defaultSetup)=>{
|
|
|
673
659
|
retry: validateNonNegativeInteger(candidate.retry, 0, `${label}.retry`),
|
|
674
660
|
variables: validateVariables(candidate.variables, `${label}.variables`),
|
|
675
661
|
...void 0 === candidate.setup ? {} : {
|
|
676
|
-
setup: validateProjectSetup(candidate.setup,
|
|
662
|
+
setup: validateProjectSetup(candidate.setup, `${label}.setup`)
|
|
677
663
|
}
|
|
678
664
|
});
|
|
679
665
|
});
|
|
@@ -707,7 +693,8 @@ const validateOutput = (value)=>{
|
|
|
707
693
|
});
|
|
708
694
|
};
|
|
709
695
|
const validateTestProjectDefinition = (definition)=>{
|
|
710
|
-
if (!isRecord(definition)
|
|
696
|
+
if (!isRecord(definition)) throw new TypeError('Midscene config must default export an object.');
|
|
697
|
+
if (void 0 !== definition.nodes && !Array.isArray(definition.nodes)) throw new TypeError('Midscene config nodes must be an array.');
|
|
711
698
|
if ('setupWorkflow' in definition) throw new TypeError('Midscene config setupWorkflow is no longer supported. Use setup or projects[].setup instead.');
|
|
712
699
|
if ('setupDocument' in definition) throw new TypeError('Midscene config setupDocument is not supported. Use setup or projects[].setup for resources, and beforeAll/afterAll for document lifecycle steps.');
|
|
713
700
|
if ('root' in definition) throw new TypeError('Midscene config root is not supported. Pass the Test Project directory to midscene-test instead.');
|
|
@@ -720,8 +707,8 @@ const validateTestProjectDefinition = (definition)=>{
|
|
|
720
707
|
'output',
|
|
721
708
|
'nodes'
|
|
722
709
|
], 'root');
|
|
723
|
-
const resolvedProjects = validateExecutionProjects(definition.projects, definition.setup);
|
|
724
710
|
const nodes = new NodeRegistry(definition.nodes);
|
|
711
|
+
const resolvedProjects = validateExecutionProjects(definition.projects, definition.setup, nodes);
|
|
725
712
|
return {
|
|
726
713
|
...resolvedProjects,
|
|
727
714
|
test: validateTestOptions(definition.test),
|
|
@@ -785,7 +772,6 @@ const createProjectRuntime = (options)=>{
|
|
|
785
772
|
const endedAt = new Date();
|
|
786
773
|
return {
|
|
787
774
|
projectName: options.project.name,
|
|
788
|
-
platform: options.project.platform,
|
|
789
775
|
status: 'failed' === requestedStatus || setupError || teardownErrors.length > 0 ? 'failed' : 'success',
|
|
790
776
|
...setupError ? {
|
|
791
777
|
setupError
|
|
@@ -1086,7 +1072,6 @@ function createDocumentRuntime(document, options) {
|
|
|
1086
1072
|
const project = options.project ?? Object.freeze({
|
|
1087
1073
|
projectId: document.projectId,
|
|
1088
1074
|
name: document.projectId,
|
|
1089
|
-
platform: 'web',
|
|
1090
1075
|
tags: Object.freeze({
|
|
1091
1076
|
include: [],
|
|
1092
1077
|
exclude: []
|
|
@@ -2019,7 +2004,6 @@ const buildProject = (project, context)=>{
|
|
|
2019
2004
|
return {
|
|
2020
2005
|
projectId: project.projectId,
|
|
2021
2006
|
name: project.name,
|
|
2022
|
-
platform: project.platform,
|
|
2023
2007
|
status: project.status,
|
|
2024
2008
|
retry: project.retry,
|
|
2025
2009
|
...project.lifecycle ? {
|
|
@@ -2170,7 +2154,6 @@ const writeTestProjectRunResult = (options)=>{
|
|
|
2170
2154
|
projects: result.projects.map((project)=>({
|
|
2171
2155
|
projectId: project.projectId,
|
|
2172
2156
|
name: project.name,
|
|
2173
|
-
platform: project.platform,
|
|
2174
2157
|
status: project.status,
|
|
2175
2158
|
retry: project.retry,
|
|
2176
2159
|
fileSelection: project.fileSelection,
|
|
@@ -2381,7 +2364,7 @@ const selectProjects = (projects, names)=>{
|
|
|
2381
2364
|
if (unknown) throw new Error(`Unknown Midscene project: ${unknown}`);
|
|
2382
2365
|
return projects.filter((project)=>requested.has(project.name));
|
|
2383
2366
|
};
|
|
2384
|
-
const prepareProject = (project, projectRoot,
|
|
2367
|
+
const prepareProject = (project, projectRoot, runDir)=>{
|
|
2385
2368
|
const fileSelection = project.files ?? DEFAULT_TEST_FILE_SELECTION;
|
|
2386
2369
|
const files = discoverTestFiles(projectRoot, fileSelection);
|
|
2387
2370
|
const sources = files.map((absolutePath)=>({
|
|
@@ -2403,7 +2386,7 @@ const prepareProject = (project, projectRoot, resolveNode, runDir)=>{
|
|
|
2403
2386
|
}
|
|
2404
2387
|
for (const source of sources)try {
|
|
2405
2388
|
const collected = collectWorkflowDocument(source, {
|
|
2406
|
-
resolveNode,
|
|
2389
|
+
resolveNode: project.nodes.get.bind(project.nodes),
|
|
2407
2390
|
variables: project.variables,
|
|
2408
2391
|
env: process.env
|
|
2409
2392
|
});
|
|
@@ -2447,7 +2430,7 @@ async function runTestProject(options = {}) {
|
|
|
2447
2430
|
});
|
|
2448
2431
|
(0, external_node_fs_.mkdirSync)(runDir);
|
|
2449
2432
|
const selectedProjects = selectProjects(definition.projects, options.projectNames);
|
|
2450
|
-
const preparedProjects = selectedProjects.map((project)=>prepareProject(project, projectRoot,
|
|
2433
|
+
const preparedProjects = selectedProjects.map((project)=>prepareProject(project, projectRoot, runDir));
|
|
2451
2434
|
const progress = options.onProgress ?? (()=>{});
|
|
2452
2435
|
const totalDocuments = preparedProjects.reduce((total, prepared)=>total + prepared.documents.length, 0);
|
|
2453
2436
|
const totalCases = preparedProjects.reduce((total, prepared)=>total + prepared.selectedCaseCount, 0);
|
|
@@ -2483,7 +2466,7 @@ async function runTestProject(options = {}) {
|
|
|
2483
2466
|
};
|
|
2484
2467
|
const announceProject = (prepared, projectIndex)=>{
|
|
2485
2468
|
const { project } = prepared;
|
|
2486
|
-
runInfrastructureCallback(()=>progress(`[project ${projectIndex + 1}/${preparedProjects.length}] ${project.name}
|
|
2469
|
+
runInfrastructureCallback(()=>progress(`[project ${projectIndex + 1}/${preparedProjects.length}] ${project.name}`));
|
|
2487
2470
|
};
|
|
2488
2471
|
const buildProjectResult = (prepared, cases, documents, lifecycle)=>{
|
|
2489
2472
|
const { project } = prepared;
|
|
@@ -2491,7 +2474,6 @@ async function runTestProject(options = {}) {
|
|
|
2491
2474
|
return {
|
|
2492
2475
|
projectId: project.projectId,
|
|
2493
2476
|
name: project.name,
|
|
2494
|
-
platform: project.platform,
|
|
2495
2477
|
status: projectFailed ? 'failed' : 'success',
|
|
2496
2478
|
retry: project.retry,
|
|
2497
2479
|
fileSelection: prepared.fileSelection,
|
|
@@ -2540,7 +2522,7 @@ async function runTestProject(options = {}) {
|
|
|
2540
2522
|
}
|
|
2541
2523
|
projectProgress(` [document ${documentIndex + 1}/${prepared.documents.length}] ${document.sourcePath}`);
|
|
2542
2524
|
const execution = await runWorkflowDocument(document, {
|
|
2543
|
-
resolveNode:
|
|
2525
|
+
resolveNode: project.nodes.require.bind(project.nodes),
|
|
2544
2526
|
project,
|
|
2545
2527
|
projectContext: runtime.context,
|
|
2546
2528
|
retry: project.retry,
|
|
@@ -2696,7 +2678,7 @@ const parseTestCliArgs = (args, cwd = process.cwd())=>{
|
|
|
2696
2678
|
} else throw new Error(`Unknown option: ${arg}`);
|
|
2697
2679
|
}
|
|
2698
2680
|
if ('nodes' === command && resultDir) throw new Error('--result-dir is not supported by nodes.');
|
|
2699
|
-
if ('nodes' === command && projectNames.length >
|
|
2681
|
+
if ('nodes' === command && projectNames.length > 1) throw new Error('nodes accepts only one --project name.');
|
|
2700
2682
|
return {
|
|
2701
2683
|
...command ? {
|
|
2702
2684
|
command
|
|
@@ -2727,10 +2709,15 @@ const runNodesCommand = async (options, io)=>{
|
|
|
2727
2709
|
const configPath = options.configPath ? (0, external_node_path_.resolve)(configSearchRoot, options.configPath) : discoverTestConfig(configSearchRoot);
|
|
2728
2710
|
if (options.configPath && (!configPath || !(0, external_node_fs_.existsSync)(configPath))) throw new Error(`Midscene config does not exist: ${configPath}`);
|
|
2729
2711
|
const project = await loadTestProject(configPath);
|
|
2730
|
-
const
|
|
2712
|
+
const projectName = options.projectNames?.[0];
|
|
2713
|
+
const selectedProjects = projectName ? project.projects.filter((candidate)=>candidate.name === projectName) : project.projects;
|
|
2714
|
+
if (0 === selectedProjects.length) throw new Error(`Unknown Midscene project: ${projectName}`);
|
|
2715
|
+
const registry = selectedProjects[0].nodes;
|
|
2716
|
+
if (selectedProjects.some((candidate)=>candidate.nodes.names().length !== registry.names().length || candidate.nodes.definitions().some((node)=>registry.get(node.name) !== node))) throw new Error('Projects have different Nodes. Use nodes --project <name> to select one.');
|
|
2717
|
+
const nodes = sortNodesForReference(registry.definitions());
|
|
2731
2718
|
const document = renderNodeReference(nodes, {
|
|
2732
2719
|
configPath: configPath ? (0, external_node_path_.relative)(configSearchRoot, configPath).split(external_node_path_.sep).join('/') : void 0,
|
|
2733
|
-
projects:
|
|
2720
|
+
projects: selectedProjects.map((executionProject)=>({
|
|
2734
2721
|
name: executionProject.name,
|
|
2735
2722
|
files: executionProject.files ?? DEFAULT_TEST_FILE_SELECTION
|
|
2736
2723
|
}))
|
|
@@ -2738,6 +2725,7 @@ const runNodesCommand = async (options, io)=>{
|
|
|
2738
2725
|
for (const warning of document.warnings)io.error(`midscene-test nodes: ${warning}`);
|
|
2739
2726
|
const referencePath = (0, external_node_path_.resolve)(configSearchRoot, 'midscene-node-reference.md');
|
|
2740
2727
|
(0, external_node_fs_.writeFileSync)(referencePath, document.markdown);
|
|
2728
|
+
if (projectName) io.log(`Execution Project: ${projectName}`);
|
|
2741
2729
|
io.log(`Registered Nodes (${nodes.length}):`);
|
|
2742
2730
|
if (0 === nodes.length) io.log('No nodes are registered by the current Test Project.');
|
|
2743
2731
|
for (const node of nodes)io.log(`- ${node.name}: ${node.description?.trim() || "Description not declared."}`);
|