@midscene/test 1.14.0 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,285 @@
1
+ import { runConnectivityTest } from "@midscene/core";
2
+ import { globalModelConfigManager } from "@midscene/shared/env";
3
+ import chalk from "chalk";
4
+ import { existsSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ import dotenv from "dotenv";
7
+ function loadDotenvConfig(options = {}) {
8
+ const dotEnvConfigFile = join(options.cwd ?? process.cwd(), '.env');
9
+ if (!existsSync(dotEnvConfigFile)) return;
10
+ options.log?.(` Env file: ${dotEnvConfigFile}`);
11
+ dotenv.config({
12
+ path: dotEnvConfigFile,
13
+ debug: options.dotenvDebug,
14
+ override: options.dotenvOverride
15
+ });
16
+ }
17
+ const DEFAULT_OPENAI_BASE_URL = 'https://api.openai.com/v1';
18
+ const MODEL_VERIFY_SEPARATOR = '────────────────────────────────────────';
19
+ const modelCommandUsage = (commandName)=>`Usage:
20
+ ${commandName} model verify
21
+
22
+ Verify model connectivity and Midscene compatibility for default, planning, and insight models.
23
+ Reads .env from the current working directory, overriding shell environment variables.
24
+ Shows endpoints, model names, request counts, and per-check results with duration.
25
+ Returns exit code 0 on success and 1 on failure.
26
+
27
+ Options:
28
+ --help, -h Show this help
29
+ `;
30
+ function assertNoModelVerifyOptions(args, commandName) {
31
+ for (const arg of args)throw new Error(`Unknown option for ${commandName} model verify: ${arg}`);
32
+ }
33
+ function formatTable(headers, rows) {
34
+ const widths = headers.map((header, index)=>Math.max(header.length, ...rows.map((row)=>row[index].length)));
35
+ const formatRow = (row)=>row.map((cell, index)=>cell.padEnd(widths[index])).join(' ').trimEnd();
36
+ return [
37
+ formatRow(headers),
38
+ ...rows.map(formatRow)
39
+ ].join('\n');
40
+ }
41
+ function displayEndpoint(endpoint) {
42
+ try {
43
+ const url = new URL(endpoint);
44
+ url.username = '';
45
+ url.password = '';
46
+ url.search = '';
47
+ url.hash = '';
48
+ return url.toString();
49
+ } catch {
50
+ return '(invalid endpoint URL)';
51
+ }
52
+ }
53
+ function shellSingleQuote(value) {
54
+ return `'${value.replace(/'/g, "'\\''")}'`;
55
+ }
56
+ function buildChatCompletionsUrl(baseURL) {
57
+ const normalizedBaseURL = (baseURL || DEFAULT_OPENAI_BASE_URL).replace(/\/+$/, '');
58
+ if (normalizedBaseURL.endsWith('/chat/completions')) return normalizedBaseURL;
59
+ return `${normalizedBaseURL}/chat/completions`;
60
+ }
61
+ function buildCurlCommand(modelConfig) {
62
+ const payload = {
63
+ model: modelConfig.modelName,
64
+ messages: [
65
+ {
66
+ role: 'user',
67
+ content: 'What is 1+1?'
68
+ }
69
+ ]
70
+ };
71
+ return [
72
+ `curl -X POST ${shellSingleQuote(buildChatCompletionsUrl(modelConfig.openaiBaseURL))} \\`,
73
+ ` -H ${shellSingleQuote(`Authorization: Bearer ${modelConfig.openaiApiKey || ''}`)} \\`,
74
+ ` -H ${shellSingleQuote('Content-Type: application/json')} \\`,
75
+ ` -d ${shellSingleQuote(JSON.stringify(payload, null, 2))}`
76
+ ].join('\n');
77
+ }
78
+ function buildCurlDedupKey(modelConfig) {
79
+ return JSON.stringify({
80
+ baseURL: modelConfig.openaiBaseURL || DEFAULT_OPENAI_BASE_URL,
81
+ apiKey: modelConfig.openaiApiKey || '',
82
+ modelName: modelConfig.modelName
83
+ });
84
+ }
85
+ function buildModelVerifyCurlCommands(configs) {
86
+ const commandMap = new Map();
87
+ for (const item of configs){
88
+ const key = buildCurlDedupKey(item.modelConfig);
89
+ const existing = commandMap.get(key);
90
+ if (existing) {
91
+ existing.intents.push(item.intent);
92
+ continue;
93
+ }
94
+ commandMap.set(key, {
95
+ intents: [
96
+ item.intent
97
+ ],
98
+ curl: buildCurlCommand(item.modelConfig),
99
+ usesDefaultBaseURL: !item.modelConfig.openaiBaseURL
100
+ });
101
+ }
102
+ return [
103
+ ...commandMap.values()
104
+ ];
105
+ }
106
+ function formatModelVerifyFailureOutput(message, curlCommands) {
107
+ const details = message?.trim() || 'No failure details were generated.';
108
+ const curlSection = curlCommands.map((item)=>{
109
+ const baseUrlNote = item.usesDefaultBaseURL ? ' (base URL not configured; using OpenAI SDK default)' : '';
110
+ return chalk.gray(`# ${item.intents.join(', ')}${baseUrlNote}\n${item.curl}`);
111
+ }).join('\n\n');
112
+ return [
113
+ chalk.red.bold('❌ Model verify failed with messages:'),
114
+ MODEL_VERIFY_SEPARATOR,
115
+ '',
116
+ details,
117
+ '',
118
+ MODEL_VERIFY_SEPARATOR,
119
+ 'Generated curl requests for basic API connectivity:',
120
+ 'If the error is a basic connectivity issue, use these requests to test the base URL, API key, and model name directly.',
121
+ 'These commands contain your API key. Do not share them publicly.',
122
+ '',
123
+ curlSection
124
+ ].join('\n');
125
+ }
126
+ function getDefaultDeps(io) {
127
+ return {
128
+ loadDotenv: ()=>{
129
+ loadDotenvConfig({
130
+ dotenvDebug: true,
131
+ dotenvOverride: true,
132
+ log: io.stdout
133
+ });
134
+ },
135
+ getModelConfig: (intent)=>globalModelConfigManager.getModelConfig(intent),
136
+ verifyModel: runConnectivityTest
137
+ };
138
+ }
139
+ async function runModelVerifyCommand(args, deps, io, commandName) {
140
+ try {
141
+ if (args.includes('--help') || args.includes('-h')) {
142
+ io.stdout(modelCommandUsage(commandName));
143
+ return 0;
144
+ }
145
+ assertNoModelVerifyOptions(args, commandName);
146
+ io.stdout('Model verify started. This usually completes within 30 seconds.');
147
+ deps.loadDotenv();
148
+ io.stdout('');
149
+ const defaultModelConfig = deps.getModelConfig('default');
150
+ const planningModelConfig = deps.getModelConfig('planning');
151
+ const insightModelConfig = deps.getModelConfig('insight');
152
+ const curlCommands = buildModelVerifyCurlCommands([
153
+ {
154
+ intent: 'default',
155
+ modelConfig: defaultModelConfig
156
+ },
157
+ {
158
+ intent: 'planning',
159
+ modelConfig: planningModelConfig
160
+ },
161
+ {
162
+ intent: 'insight',
163
+ modelConfig: insightModelConfig
164
+ }
165
+ ]);
166
+ const baseURLs = new Map();
167
+ const modelRows = [
168
+ [
169
+ 'planning',
170
+ planningModelConfig,
171
+ 'Text response'
172
+ ],
173
+ [
174
+ 'insight',
175
+ insightModelConfig,
176
+ 'Image understanding'
177
+ ],
178
+ [
179
+ 'default',
180
+ defaultModelConfig,
181
+ 'AI locate'
182
+ ]
183
+ ].map(([intent, config, check])=>{
184
+ const baseURL = config.openaiExtraConfig?.baseURL ?? config.openaiBaseURL;
185
+ const endpoint = baseURL ? displayEndpoint(String(baseURL)) : 'provider default';
186
+ if (!baseURLs.has(endpoint)) baseURLs.set(endpoint, baseURLs.size + 1);
187
+ return [
188
+ intent,
189
+ config.modelName,
190
+ check,
191
+ `[${baseURLs.get(endpoint)}]`
192
+ ];
193
+ });
194
+ io.stdout(formatTable([
195
+ 'Model role',
196
+ 'Model',
197
+ 'Check',
198
+ 'Endpoint'
199
+ ], modelRows));
200
+ io.stdout('');
201
+ for (const [endpoint, index] of baseURLs)io.stdout(`[${index}] Base URL: ${endpoint}`);
202
+ io.stdout('\nRunning 3 checks...\n');
203
+ const result = await deps.verifyModel({
204
+ defaultModelConfig,
205
+ planningModelConfig,
206
+ insightModelConfig
207
+ });
208
+ const checkLabels = {
209
+ text: 'Text response',
210
+ vision: 'Image understanding',
211
+ aiLocate: 'AI locate'
212
+ };
213
+ io.stdout(formatTable([
214
+ 'Model role',
215
+ 'Check',
216
+ 'Result',
217
+ 'Requests',
218
+ 'Time'
219
+ ], result.checks.map((check)=>[
220
+ check.intent,
221
+ checkLabels[check.name],
222
+ check.passed ? 'PASS' : 'FAIL',
223
+ String(check.requestCount),
224
+ `${check.durationMs} ms`
225
+ ])));
226
+ const requestEndpoints = new Map();
227
+ for (const check of result.checks){
228
+ for (const endpoint of check.endpoints){
229
+ const display = displayEndpoint(endpoint);
230
+ if (!requestEndpoints.has(display)) requestEndpoints.set(display, new Set());
231
+ requestEndpoints.get(display).add(check.intent);
232
+ }
233
+ if (!check.passed && check.message) io.stdout(`\n${check.intent}: ${check.message}`);
234
+ }
235
+ if (requestEndpoints.size > 0) {
236
+ io.stdout('\nRequest endpoints:');
237
+ for (const [endpoint, intents] of requestEndpoints)io.stdout(` ${[
238
+ ...intents
239
+ ].join(', ')}: ${endpoint}`);
240
+ }
241
+ io.stdout('');
242
+ const passedChecks = result.checks.filter((check)=>check.passed).length;
243
+ io.stdout(`Requests sent: ${result.requestCount}. Checks: ${passedChecks} passed, ${result.checks.length - passedChecks} failed.`);
244
+ if (result.passed) {
245
+ io.stdout('✅ Model verify passed.');
246
+ return 0;
247
+ }
248
+ io.stderr(formatModelVerifyFailureOutput(result.message, curlCommands));
249
+ return 1;
250
+ } catch (error) {
251
+ const message = error instanceof Error ? error.message : String(error);
252
+ io.stderr([
253
+ MODEL_VERIFY_SEPARATOR,
254
+ '❌ Model verify failed with messages:',
255
+ '',
256
+ message,
257
+ MODEL_VERIFY_SEPARATOR
258
+ ].join('\n'));
259
+ return 1;
260
+ }
261
+ }
262
+ async function runModelCommand(rawArgs, deps, io = {
263
+ stdout: console.log,
264
+ stderr: console.error
265
+ }, commandName = 'midscene-test') {
266
+ const [, action, ...restArgs] = rawArgs;
267
+ const mergedDeps = {
268
+ ...getDefaultDeps(io),
269
+ ...deps
270
+ };
271
+ if (!action || '--help' === action || '-h' === action) {
272
+ io.stdout(modelCommandUsage(commandName));
273
+ return 0;
274
+ }
275
+ if ('verify' === action) return runModelVerifyCommand(restArgs, mergedDeps, io, commandName);
276
+ if ('eval' === action) {
277
+ io.stderr(`${commandName} model eval is not implemented yet. It is reserved for future model evaluation suites.`);
278
+ return 1;
279
+ }
280
+ io.stderr(`Unknown ${commandName} model command: ${action}\n\n${modelCommandUsage(commandName)}`);
281
+ return 1;
282
+ }
283
+ export { buildModelVerifyCurlCommands, loadDotenvConfig, runModelCommand };
284
+
285
+ //# sourceMappingURL=model-command.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli/model-command.mjs","sources":["../../../src/cli/dotenv-loader.ts","../../../src/cli/model-command.ts"],"sourcesContent":["import { existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport dotenv from 'dotenv';\n\nexport interface DotenvLoadOptions {\n dotenvOverride?: boolean;\n dotenvDebug?: boolean;\n cwd?: string;\n log?: (message: string) => void;\n}\n\nexport function loadDotenvConfig(options: DotenvLoadOptions = {}) {\n const dotEnvConfigFile = join(options.cwd ?? process.cwd(), '.env');\n if (!existsSync(dotEnvConfigFile)) {\n return;\n }\n\n options.log?.(` Env file: ${dotEnvConfigFile}`);\n dotenv.config({\n path: dotEnvConfigFile,\n debug: options.dotenvDebug,\n override: options.dotenvOverride,\n });\n}\n","import { runConnectivityTest } from '@midscene/core';\nimport {\n type IModelConfig,\n type TIntent,\n globalModelConfigManager,\n} from '@midscene/shared/env';\nimport chalk from 'chalk';\nimport { loadDotenvConfig } from './dotenv-loader';\n\nconst DEFAULT_OPENAI_BASE_URL = 'https://api.openai.com/v1';\nconst MODEL_VERIFY_SEPARATOR = '────────────────────────────────────────';\nconst modelCommandUsage = (commandName: string) => `Usage:\n ${commandName} model verify\n\nVerify model connectivity and Midscene compatibility for default, planning, and insight models.\nReads .env from the current working directory, overriding shell environment variables.\nShows endpoints, model names, request counts, and per-check results with duration.\nReturns exit code 0 on success and 1 on failure.\n\nOptions:\n --help, -h Show this help\n`;\n\ninterface ModelCommandIO {\n stdout: (message: string) => void;\n stderr: (message: string) => void;\n}\n\ninterface ModelCommandDeps {\n loadDotenv: () => void;\n getModelConfig: (intent: TIntent) => IModelConfig;\n verifyModel: typeof runConnectivityTest;\n}\n\nexport interface CurlCommandItem {\n intents: TIntent[];\n curl: string;\n usesDefaultBaseURL: boolean;\n}\n\nfunction assertNoModelVerifyOptions(args: string[], commandName: string) {\n for (const arg of args) {\n throw new Error(`Unknown option for ${commandName} model verify: ${arg}`);\n }\n}\n\nfunction formatTable(headers: string[], rows: string[][]): string {\n const widths = headers.map((header, index) =>\n Math.max(header.length, ...rows.map((row) => row[index].length)),\n );\n const formatRow = (row: string[]) =>\n row\n .map((cell, index) => cell.padEnd(widths[index]))\n .join(' ')\n .trimEnd();\n return [formatRow(headers), ...rows.map(formatRow)].join('\\n');\n}\n\nfunction displayEndpoint(endpoint: string): string {\n try {\n const url = new URL(endpoint);\n url.username = '';\n url.password = '';\n url.search = '';\n url.hash = '';\n return url.toString();\n } catch {\n return '(invalid endpoint URL)';\n }\n}\n\nfunction shellSingleQuote(value: string): string {\n return `'${value.replace(/'/g, `'\\\\''`)}'`;\n}\n\nfunction buildChatCompletionsUrl(baseURL?: string): string {\n const normalizedBaseURL = (baseURL || DEFAULT_OPENAI_BASE_URL).replace(\n /\\/+$/,\n '',\n );\n if (normalizedBaseURL.endsWith('/chat/completions')) {\n return normalizedBaseURL;\n }\n return `${normalizedBaseURL}/chat/completions`;\n}\n\nfunction buildCurlCommand(modelConfig: IModelConfig): string {\n const payload = {\n model: modelConfig.modelName,\n messages: [{ role: 'user', content: 'What is 1+1?' }],\n };\n\n return [\n `curl -X POST ${shellSingleQuote(buildChatCompletionsUrl(modelConfig.openaiBaseURL))} \\\\`,\n ` -H ${shellSingleQuote(`Authorization: Bearer ${modelConfig.openaiApiKey || ''}`)} \\\\`,\n ` -H ${shellSingleQuote('Content-Type: application/json')} \\\\`,\n ` -d ${shellSingleQuote(JSON.stringify(payload, null, 2))}`,\n ].join('\\n');\n}\n\nfunction buildCurlDedupKey(modelConfig: IModelConfig): string {\n return JSON.stringify({\n baseURL: modelConfig.openaiBaseURL || DEFAULT_OPENAI_BASE_URL,\n apiKey: modelConfig.openaiApiKey || '',\n modelName: modelConfig.modelName,\n });\n}\n\nexport function buildModelVerifyCurlCommands(\n configs: Array<{ intent: TIntent; modelConfig: IModelConfig }>,\n): CurlCommandItem[] {\n const commandMap = new Map<string, CurlCommandItem>();\n\n for (const item of configs) {\n const key = buildCurlDedupKey(item.modelConfig);\n const existing = commandMap.get(key);\n if (existing) {\n existing.intents.push(item.intent);\n continue;\n }\n commandMap.set(key, {\n intents: [item.intent],\n curl: buildCurlCommand(item.modelConfig),\n usesDefaultBaseURL: !item.modelConfig.openaiBaseURL,\n });\n }\n\n return [...commandMap.values()];\n}\n\nfunction formatModelVerifyFailureOutput(\n message: string | undefined,\n curlCommands: CurlCommandItem[],\n): string {\n const details = message?.trim() || 'No failure details were generated.';\n const curlSection = curlCommands\n .map((item) => {\n const baseUrlNote = item.usesDefaultBaseURL\n ? ' (base URL not configured; using OpenAI SDK default)'\n : '';\n return chalk.gray(\n `# ${item.intents.join(', ')}${baseUrlNote}\\n${item.curl}`,\n );\n })\n .join('\\n\\n');\n\n return [\n chalk.red.bold('❌ Model verify failed with messages:'),\n MODEL_VERIFY_SEPARATOR,\n '',\n details,\n '',\n MODEL_VERIFY_SEPARATOR,\n 'Generated curl requests for basic API connectivity:',\n 'If the error is a basic connectivity issue, use these requests to test the base URL, API key, and model name directly.',\n 'These commands contain your API key. Do not share them publicly.',\n '',\n curlSection,\n ].join('\\n');\n}\n\nfunction getDefaultDeps(io: ModelCommandIO): ModelCommandDeps {\n return {\n loadDotenv: () => {\n loadDotenvConfig({\n dotenvDebug: true,\n dotenvOverride: true,\n log: io.stdout,\n });\n },\n getModelConfig: (intent) => globalModelConfigManager.getModelConfig(intent),\n verifyModel: runConnectivityTest,\n };\n}\n\nasync function runModelVerifyCommand(\n args: string[],\n deps: ModelCommandDeps,\n io: ModelCommandIO,\n commandName: string,\n): Promise<number> {\n try {\n if (args.includes('--help') || args.includes('-h')) {\n io.stdout(modelCommandUsage(commandName));\n return 0;\n }\n\n assertNoModelVerifyOptions(args, commandName);\n io.stdout(\n 'Model verify started. This usually completes within 30 seconds.',\n );\n deps.loadDotenv();\n io.stdout('');\n\n const defaultModelConfig = deps.getModelConfig('default');\n const planningModelConfig = deps.getModelConfig('planning');\n const insightModelConfig = deps.getModelConfig('insight');\n const curlCommands = buildModelVerifyCurlCommands([\n { intent: 'default', modelConfig: defaultModelConfig },\n { intent: 'planning', modelConfig: planningModelConfig },\n { intent: 'insight', modelConfig: insightModelConfig },\n ]);\n\n const baseURLs = new Map<string, number>();\n const modelRows = (\n [\n ['planning', planningModelConfig, 'Text response'],\n ['insight', insightModelConfig, 'Image understanding'],\n ['default', defaultModelConfig, 'AI locate'],\n ] as const\n ).map(([intent, config, check]) => {\n const baseURL = config.openaiExtraConfig?.baseURL ?? config.openaiBaseURL;\n const endpoint = baseURL\n ? displayEndpoint(String(baseURL))\n : 'provider default';\n if (!baseURLs.has(endpoint)) baseURLs.set(endpoint, baseURLs.size + 1);\n return [intent, config.modelName, check, `[${baseURLs.get(endpoint)}]`];\n });\n io.stdout(\n formatTable(['Model role', 'Model', 'Check', 'Endpoint'], modelRows),\n );\n io.stdout('');\n for (const [endpoint, index] of baseURLs) {\n io.stdout(`[${index}] Base URL: ${endpoint}`);\n }\n io.stdout('\\nRunning 3 checks...\\n');\n\n const result = await deps.verifyModel({\n defaultModelConfig,\n planningModelConfig,\n insightModelConfig,\n });\n\n const checkLabels = {\n text: 'Text response',\n vision: 'Image understanding',\n aiLocate: 'AI locate',\n };\n io.stdout(\n formatTable(\n ['Model role', 'Check', 'Result', 'Requests', 'Time'],\n result.checks.map((check) => [\n check.intent,\n checkLabels[check.name],\n check.passed ? 'PASS' : 'FAIL',\n String(check.requestCount),\n `${check.durationMs} ms`,\n ]),\n ),\n );\n const requestEndpoints = new Map<string, Set<string>>();\n for (const check of result.checks) {\n for (const endpoint of check.endpoints) {\n const display = displayEndpoint(endpoint);\n if (!requestEndpoints.has(display))\n requestEndpoints.set(display, new Set());\n requestEndpoints.get(display)!.add(check.intent);\n }\n if (!check.passed && check.message)\n io.stdout(`\\n${check.intent}: ${check.message}`);\n }\n if (requestEndpoints.size > 0) {\n io.stdout('\\nRequest endpoints:');\n for (const [endpoint, intents] of requestEndpoints) {\n io.stdout(` ${[...intents].join(', ')}: ${endpoint}`);\n }\n }\n io.stdout('');\n const passedChecks = result.checks.filter((check) => check.passed).length;\n io.stdout(\n `Requests sent: ${result.requestCount}. Checks: ${passedChecks} passed, ${result.checks.length - passedChecks} failed.`,\n );\n\n if (result.passed) {\n io.stdout('✅ Model verify passed.');\n return 0;\n }\n\n io.stderr(formatModelVerifyFailureOutput(result.message, curlCommands));\n return 1;\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n io.stderr(\n [\n MODEL_VERIFY_SEPARATOR,\n '❌ Model verify failed with messages:',\n '',\n message,\n MODEL_VERIFY_SEPARATOR,\n ].join('\\n'),\n );\n return 1;\n }\n}\n\nexport async function runModelCommand(\n rawArgs: string[],\n deps?: Partial<ModelCommandDeps>,\n io: ModelCommandIO = {\n stdout: console.log,\n stderr: console.error,\n },\n commandName = 'midscene-test',\n): Promise<number> {\n const [, action, ...restArgs] = rawArgs;\n const mergedDeps = {\n ...getDefaultDeps(io),\n ...deps,\n };\n\n if (!action || action === '--help' || action === '-h') {\n io.stdout(modelCommandUsage(commandName));\n return 0;\n }\n\n if (action === 'verify') {\n return runModelVerifyCommand(restArgs, mergedDeps, io, commandName);\n }\n\n if (action === 'eval') {\n io.stderr(\n `${commandName} model eval is not implemented yet. It is reserved for future model evaluation suites.`,\n );\n return 1;\n }\n\n io.stderr(\n `Unknown ${commandName} model command: ${action}\\n\\n${modelCommandUsage(commandName)}`,\n );\n return 1;\n}\n\nexport { loadDotenvConfig, type DotenvLoadOptions } from './dotenv-loader';\n"],"names":["loadDotenvConfig","options","dotEnvConfigFile","join","process","existsSync","dotenv","DEFAULT_OPENAI_BASE_URL","MODEL_VERIFY_SEPARATOR","modelCommandUsage","commandName","assertNoModelVerifyOptions","args","arg","Error","formatTable","headers","rows","widths","header","index","Math","row","formatRow","cell","displayEndpoint","endpoint","url","URL","shellSingleQuote","value","buildChatCompletionsUrl","baseURL","normalizedBaseURL","buildCurlCommand","modelConfig","payload","JSON","buildCurlDedupKey","buildModelVerifyCurlCommands","configs","commandMap","Map","item","key","existing","formatModelVerifyFailureOutput","message","curlCommands","details","curlSection","baseUrlNote","chalk","getDefaultDeps","io","intent","globalModelConfigManager","runConnectivityTest","runModelVerifyCommand","deps","defaultModelConfig","planningModelConfig","insightModelConfig","baseURLs","modelRows","config","check","String","result","checkLabels","requestEndpoints","display","Set","intents","passedChecks","error","runModelCommand","rawArgs","console","action","restArgs","mergedDeps"],"mappings":"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"}
@@ -1,10 +1,11 @@
1
1
  import * as __rspack_external_node_fs_5ea92f0c from "node:fs";
2
2
  import * as __rspack_external_node_path_c5b9b54f from "node:path";
3
+ import { NodeRegistry, WorkflowError, WorkflowExecutionFailure, WorkflowParseError, WorkflowPublicationError, asExecutionError, buildTestRunReportDump, classifyWorkflowDocumentFormat, collectTestRunReportSources, createProjectRuntime, isFatalDeviceError, normalizeSteps, runConcurrentJobs, runDocumentAttempts, runWorkflowDocument } from "@midscene/core/internal/test-runner";
4
+ import { inspect } from "node:util";
3
5
  import { z } from "zod/v4";
4
6
  import { pathToFileURL } from "node:url";
5
7
  import { require as api_require } from "tsx/cjs/api";
6
8
  import { tsImport } from "tsx/esm/api";
7
- import { NodeRegistry, WorkflowError, WorkflowExecutionFailure, WorkflowParseError, WorkflowPublicationError, asExecutionError, buildTestRunReportDump, classifyWorkflowDocumentFormat, collectTestRunReportSources, createProjectRuntime, isFatalDeviceError, normalizeSteps, runConcurrentJobs, runDocumentAttempts, runWorkflowDocument } from "@midscene/core/internal/test-runner";
8
9
  import { globSync } from "tinyglobby";
9
10
  import { JSON_SCHEMA, load } from "js-yaml";
10
11
  import { createHash, randomUUID } from "node:crypto";
@@ -21,7 +22,7 @@ var __webpack_modules__ = {
21
22
  },
22
23
  "./package.json" (module) {
23
24
  module.exports = {
24
- rE: "1.14.0"
25
+ rE: "1.15.0"
25
26
  };
26
27
  }
27
28
  };
@@ -97,6 +98,11 @@ __webpack_require__.m = __webpack_modules__;
97
98
  var external_node_fs_ = __webpack_require__("node:fs");
98
99
  var external_node_path_ = __webpack_require__("node:path");
99
100
  var package_0 = __webpack_require__("./package.json");
101
+ const formatCliError = (error)=>'string' == typeof error ? error : inspect(error, {
102
+ colors: false,
103
+ depth: null,
104
+ customInspect: false
105
+ });
100
106
  const GENERATED_COMMENT = '<!-- Generated by `midscene-test nodes`. Do not edit directly. -->';
101
107
  const sortNodesForSpec = (nodes)=>{
102
108
  const priority = (name)=>'aiAct' === name ? 0 : 'aiAssert' === name ? 1 : 2;
@@ -1580,6 +1586,7 @@ Usage:
1580
1586
  midscene-test --config <midscene.config.ts> [options]
1581
1587
  midscene-test nodes [directory] [--config midscene.config.ts]
1582
1588
  midscene-test create --help
1589
+ midscene-test model verify Check model connectivity and Midscene compatibility
1583
1590
 
1584
1591
  Options:
1585
1592
  --config <path> Native TypeScript or JavaScript Test config
@@ -1645,8 +1652,78 @@ const runNodesCommand = async (options, io)=>{
1645
1652
  io.log(`\nNode Spec generated: ${specPath}`);
1646
1653
  }
1647
1654
  };
1655
+ const reportTestProjectResult = (result, io, infrastructureErrors = result.errors ?? [])=>{
1656
+ const reportedErrors = new Set();
1657
+ const reportError = (source, error)=>{
1658
+ if (error instanceof Error && reportedErrors.has(error)) return;
1659
+ io.error(`midscene-test: ${source}: ${formatCliError(error)}`);
1660
+ const pending = [
1661
+ error
1662
+ ];
1663
+ while(pending.length > 0){
1664
+ const current = pending.pop();
1665
+ if (!(current instanceof Error) || reportedErrors.has(current)) continue;
1666
+ reportedErrors.add(current);
1667
+ pending.push(current.cause);
1668
+ if (current instanceof AggregateError) pending.push(...current.errors);
1669
+ }
1670
+ };
1671
+ for (const failure of result.collectionErrors)reportError(`${failure.projectName}/${failure.sourcePath}`, failure.error);
1672
+ const finalCases = new Map(result.cases.map((outcome)=>[
1673
+ outcome.caseId,
1674
+ outcome
1675
+ ]));
1676
+ for (const outcome of finalCases.values()){
1677
+ if ('failed' !== outcome.status || !outcome.run) continue;
1678
+ const run = outcome.run;
1679
+ const source = `${run.projectName}/${run.sourcePath} / ${run.name}`;
1680
+ for (const step of [
1681
+ ...run.beforeEach,
1682
+ ...run.steps,
1683
+ ...run.afterEach
1684
+ ])if (step.error) reportError(`${source} / ${step.phase}[${step.stepIndex + 1}] ${step.node}`, step.error);
1685
+ for (const error of [
1686
+ ...run.executionErrors ?? [],
1687
+ ...run.teardownErrors ?? []
1688
+ ])reportError(source, error);
1689
+ }
1690
+ const finalDocuments = new Map(result.documents.map((document)=>[
1691
+ document.documentId,
1692
+ document
1693
+ ]));
1694
+ for (const document of finalDocuments.values()){
1695
+ if ('failed' !== document.status) continue;
1696
+ const source = `${document.projectName}/${document.sourcePath}`;
1697
+ for (const step of [
1698
+ ...document.beforeAll,
1699
+ ...document.afterAll
1700
+ ])if (step.error) reportError(`${source} / ${step.phase}[${step.stepIndex + 1}] ${step.node}`, step.error);
1701
+ for (const error of [
1702
+ ...document.executionErrors ?? [],
1703
+ ...document.teardownErrors ?? []
1704
+ ])reportError(source, error);
1705
+ for (const failure of document.hostErrors ?? [])reportError(`${source} / host ${failure.phase}`, failure.error);
1706
+ }
1707
+ for (const project of result.projects){
1708
+ if (project.lifecycle?.setupError) reportError(`${project.name} / setup`, project.lifecycle.setupError);
1709
+ for (const error of project.lifecycle?.teardownErrors ?? [])reportError(`${project.name} / teardown`, error);
1710
+ }
1711
+ for (const error of infrastructureErrors)reportError('infrastructure', error);
1712
+ io.log(`midscene-test: ${result.summary.passed}/${result.summary.total} cases passed, ${result.summary.failed} failed, ${result.summary.notRun} not run`);
1713
+ io.log(`Results: ${result.resultDir}`);
1714
+ io.log(`Summary: ${result.summaryPath}`);
1715
+ if (result.reportPath) io.log(`Report: ${result.reportPath}`);
1716
+ return result.exitCode;
1717
+ };
1648
1718
  async function runTestCli(args, io = defaultCliIO) {
1649
1719
  try {
1720
+ if ('model' === args[0]) {
1721
+ const { runModelCommand } = await __webpack_require__.e("77").then(__webpack_require__.bind(__webpack_require__, "./src/cli/model-command.ts?bb10"));
1722
+ return runModelCommand(args, void 0, {
1723
+ stdout: io.log,
1724
+ stderr: io.error
1725
+ });
1726
+ }
1650
1727
  if ('create' === args[0]) {
1651
1728
  const { runCreateCommand } = await __webpack_require__.e("214").then(__webpack_require__.bind(__webpack_require__, "./src/cli/create-command.ts"));
1652
1729
  await runCreateCommand(args.slice(1), io);
@@ -1665,36 +1742,20 @@ async function runTestCli(args, io = defaultCliIO) {
1665
1742
  await runNodesCommand(options, io);
1666
1743
  return 0;
1667
1744
  }
1668
- const result = await runTestProject({
1669
- ...options,
1670
- onProgress: (message)=>io.log(message)
1671
- });
1672
- for (const failure of result.collectionErrors)io.error(`midscene-test: ${failure.projectName}/${failure.sourcePath}: ${failure.error.message}`);
1673
- const finalCases = new Map(result.cases.map((outcome)=>[
1674
- outcome.caseId,
1675
- outcome
1676
- ]));
1677
- for (const outcome of finalCases.values()){
1678
- if ('failed' !== outcome.status || !outcome.run) continue;
1679
- const run = outcome.run;
1680
- const source = `${run.projectName}/${run.sourcePath} / ${run.name}`;
1681
- for (const step of [
1682
- ...run.beforeEach,
1683
- ...run.steps,
1684
- ...run.afterEach
1685
- ])if (step.error) io.error(`midscene-test: ${source} / ${step.phase}[${step.stepIndex + 1}] ${step.node}: ${step.error.message}`);
1686
- for (const error of [
1687
- ...run.executionErrors ?? [],
1688
- ...run.teardownErrors ?? []
1689
- ])io.error(`midscene-test: ${source}: ${error.message}`);
1745
+ let result;
1746
+ try {
1747
+ result = await runTestProject({
1748
+ ...options,
1749
+ onProgress: (message)=>io.log(message)
1750
+ });
1751
+ } catch (error) {
1752
+ if (!(error instanceof WorkflowExecutionFailure)) throw error;
1753
+ reportTestProjectResult(error.result, io, error.errors);
1754
+ return 1;
1690
1755
  }
1691
- io.log(`midscene-test: ${result.summary.passed}/${result.summary.total} cases passed, ${result.summary.failed} failed, ${result.summary.notRun} not run`);
1692
- io.log(`Results: ${result.resultDir}`);
1693
- io.log(`Summary: ${result.summaryPath}`);
1694
- if (result.reportPath) io.log(`Report: ${result.reportPath}`);
1695
- return result.exitCode;
1756
+ return reportTestProjectResult(result, io);
1696
1757
  } catch (error) {
1697
- io.error(error instanceof Error ? error.message : String(error));
1758
+ io.error(formatCliError(error));
1698
1759
  return 1;
1699
1760
  }
1700
1761
  }