@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.
Files changed (44) hide show
  1. package/dist/es/1~891.mjs +19 -39
  2. package/dist/es/1~891.mjs.map +1 -1
  3. package/dist/es/cli/index.mjs +26 -44
  4. package/dist/es/cli/index.mjs.map +1 -1
  5. package/dist/es/index.mjs +0 -3
  6. package/dist/es/index.mjs.map +1 -1
  7. package/dist/es/midscene/index.mjs +43 -32
  8. package/dist/es/midscene/index.mjs.map +1 -1
  9. package/dist/es/test-cli.mjs +37 -49
  10. package/dist/es/test-cli.mjs.map +1 -1
  11. package/dist/lib/0~458.js +19 -39
  12. package/dist/lib/0~458.js.map +1 -1
  13. package/dist/lib/cli/index.js +26 -44
  14. package/dist/lib/cli/index.js.map +1 -1
  15. package/dist/lib/index.js +0 -3
  16. package/dist/lib/index.js.map +1 -1
  17. package/dist/lib/midscene/index.js +253 -262
  18. package/dist/lib/midscene/index.js.map +1 -1
  19. package/dist/lib/test-cli.js +37 -49
  20. package/dist/lib/test-cli.js.map +1 -1
  21. package/dist/types/cli/index.d.ts +12 -13
  22. package/dist/types/index.d.ts +1 -7
  23. package/dist/types/midscene/index.d.ts +21 -21
  24. package/package.json +8 -48
  25. package/dist/es/android/index.mjs +0 -182
  26. package/dist/es/android/index.mjs.map +0 -1
  27. package/dist/es/harmony/index.mjs +0 -182
  28. package/dist/es/harmony/index.mjs.map +0 -1
  29. package/dist/es/ios/index.mjs +0 -182
  30. package/dist/es/ios/index.mjs.map +0 -1
  31. package/dist/es/playwright/index.mjs +0 -366
  32. package/dist/es/playwright/index.mjs.map +0 -1
  33. package/dist/lib/android/index.js +0 -255
  34. package/dist/lib/android/index.js.map +0 -1
  35. package/dist/lib/harmony/index.js +0 -252
  36. package/dist/lib/harmony/index.js.map +0 -1
  37. package/dist/lib/ios/index.js +0 -255
  38. package/dist/lib/ios/index.js.map +0 -1
  39. package/dist/lib/playwright/index.js +0 -412
  40. package/dist/lib/playwright/index.js.map +0 -1
  41. package/dist/types/android/index.d.ts +0 -140
  42. package/dist/types/harmony/index.d.ts +0 -137
  43. package/dist/types/ios/index.d.ts +0 -143
  44. 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-runner";
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, ctx, agent, call, captureExecutions = true)=>{
121
- const scopeId = 'case' === ctx.scope ? ctx.case.runId : ctx.document.documentRunId;
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
- activeAgentCalls.set(agent, {
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 = ()=>stopListening();
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, execution)=>{
139
- if (!execution) return;
140
- if (!execution.id) {
152
+ removeListener = agent.addDumpUpdateListener((_dump, executionRef)=>{
153
+ if (!executionRef) return;
154
+ if (!executionRef.id) {
141
155
  sawExecutionWithoutId = true;
142
156
  return;
143
157
  }
144
- ctx.report.addTrace({
158
+ execution.report.addTrace({
145
159
  type: 'midscene-execution',
146
- executionId: execution.id
160
+ executionId: executionRef.id
147
161
  });
148
162
  });
149
- if (ctx.signal.aborted) stopListening();
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
- ctx.signal.removeEventListener('abort', stopListeningOnAbort);
167
+ execution.signal.removeEventListener('abort', stopListeningOnAbort);
157
168
  try {
158
169
  stopListening();
159
170
  } finally{
160
- activeAgentCalls.delete(agent);
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 (ctx) {
178
- const agent = await getAgent(ctx);
179
- const call = async ()=>definition.execute(agent, ctx.input, {
180
- signal: ctx.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, ctx, agent, call);
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 = (ctx)=>'case' === ctx.scope ? ctx.case.runId : ctx.document.documentRunId;
196
- const getAgent = async (ctx)=>{
197
- if (!options.agentProvider) return options.getAgent(ctx);
198
- const runId = getExecutionId(ctx);
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
- ctx.onTeardown(async ()=>{
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, ctx);
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 (ctx) {
225
- const multiplier = 'min' === ctx.input.unit ? 60000 : 's' === ctx.input.unit ? 1000 : 1;
226
- const durationMs = ctx.input.duration * multiplier;
227
- await waitFor(durationMs, ctx.signal);
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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"}
@@ -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`. Node registrations are shared across the Test Project; each Execution Project supplies its own runtime environment.');
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 platforms = new Set([
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
- let platform = 'web';
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
- platform,
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, platform, `${label}.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) || !('nodes' in definition) || !Array.isArray(definition.nodes)) throw new TypeError('Midscene config must default export an object with a nodes array.');
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, resolveNode, runDir)=>{
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, definition.resolveNode, runDir));
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} (${project.platform})`));
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: definition.nodes.require.bind(definition.nodes),
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 > 0) throw new Error('--project is not supported by nodes.');
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 nodes = sortNodesForReference(project.nodes.definitions());
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: project.projects.map((executionProject)=>({
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."}`);