@kirrosh/zond 0.13.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +1 -1
- package/package.json +4 -7
- package/src/cli/commands/ci-init.ts +12 -1
- package/src/cli/commands/coverage.ts +21 -1
- package/src/cli/commands/db.ts +121 -0
- package/src/cli/commands/describe.ts +60 -0
- package/src/cli/commands/generate.ts +127 -0
- package/src/cli/commands/guide.ts +127 -0
- package/src/cli/commands/init.ts +50 -77
- package/src/cli/commands/request.ts +57 -0
- package/src/cli/commands/run.ts +53 -10
- package/src/cli/commands/serve.ts +62 -3
- package/src/cli/commands/validate.ts +18 -2
- package/src/cli/index.ts +213 -215
- package/src/cli/json-envelope.ts +19 -0
- package/src/core/diagnostics/db-analysis.ts +351 -0
- package/src/core/diagnostics/failure-hints.ts +1 -0
- package/src/core/generator/data-factory.ts +19 -8
- package/src/core/generator/describe.ts +250 -0
- package/src/core/generator/guide-builder.ts +20 -0
- package/src/core/generator/index.ts +0 -3
- package/src/core/generator/suite-generator.ts +133 -20
- package/src/core/runner/executor.ts +1 -0
- package/src/core/runner/send-request.ts +94 -0
- package/src/core/runner/types.ts +1 -0
- package/src/db/queries.ts +4 -2
- package/src/db/schema.ts +11 -3
- package/src/mcp/descriptions.ts +0 -24
- package/src/mcp/server.ts +1 -8
- package/src/mcp/tools/describe-endpoint.ts +3 -218
- package/src/mcp/tools/query-db.ts +6 -222
- package/src/mcp/tools/run-tests.ts +1 -0
- package/src/mcp/tools/send-request.ts +15 -61
- package/src/web/views/suites-tab.ts +1 -1
- package/src/cli/commands/add-api.ts +0 -53
- package/src/cli/commands/ai-generate.ts +0 -106
- package/src/cli/commands/chat.ts +0 -43
- package/src/cli/commands/collections.ts +0 -41
- package/src/cli/commands/compare.ts +0 -129
- package/src/cli/commands/doctor.ts +0 -127
- package/src/cli/commands/runs.ts +0 -108
- package/src/cli/commands/update.ts +0 -142
- package/src/core/agent/agent-loop.ts +0 -116
- package/src/core/agent/context-manager.ts +0 -41
- package/src/core/agent/system-prompt.ts +0 -27
- package/src/core/agent/tools/diagnose-failure.ts +0 -51
- package/src/core/agent/tools/index.ts +0 -42
- package/src/core/agent/tools/query-results.ts +0 -40
- package/src/core/agent/tools/run-tests.ts +0 -38
- package/src/core/agent/tools/send-request.ts +0 -44
- package/src/core/agent/types.ts +0 -22
- package/src/core/generator/ai/ai-generator.ts +0 -61
- package/src/core/generator/ai/llm-client.ts +0 -159
- package/src/core/generator/ai/output-parser.ts +0 -307
- package/src/core/generator/ai/prompt-builder.ts +0 -153
- package/src/core/generator/ai/types.ts +0 -56
- package/src/mcp/tools/generate-and-save.ts +0 -202
- package/src/mcp/tools/save-test-suite.ts +0 -218
- package/src/mcp/tools/set-work-dir.ts +0 -35
- package/src/tui/chat-ui.ts +0 -150
package/src/cli/commands/init.ts
CHANGED
|
@@ -1,84 +1,57 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
export interface
|
|
6
|
-
|
|
1
|
+
import { setupApi } from "../../core/setup-api.ts";
|
|
2
|
+
import { printError, printSuccess } from "../output.ts";
|
|
3
|
+
import { jsonOk, jsonError, printJson } from "../json-envelope.ts";
|
|
4
|
+
|
|
5
|
+
export interface InitOptions {
|
|
6
|
+
name?: string;
|
|
7
|
+
spec?: string;
|
|
8
|
+
baseUrl?: string;
|
|
9
|
+
dir?: string;
|
|
10
|
+
force?: boolean;
|
|
11
|
+
insecure?: boolean;
|
|
12
|
+
dbPath?: string;
|
|
13
|
+
json?: boolean;
|
|
7
14
|
}
|
|
8
15
|
|
|
9
|
-
|
|
10
|
-
base_url: "{{base_url}}"
|
|
11
|
-
|
|
12
|
-
tests:
|
|
13
|
-
- name: "List posts"
|
|
14
|
-
GET: /posts
|
|
15
|
-
expect:
|
|
16
|
-
status: 200
|
|
17
|
-
body:
|
|
18
|
-
id: { type: integer }
|
|
19
|
-
|
|
20
|
-
- name: "Get single post"
|
|
21
|
-
GET: /posts/1
|
|
22
|
-
expect:
|
|
23
|
-
status: 200
|
|
24
|
-
body:
|
|
25
|
-
id: { equals: 1 }
|
|
26
|
-
title: { type: string }
|
|
27
|
-
`;
|
|
28
|
-
|
|
29
|
-
const ENV_DEV = `base_url: https://jsonplaceholder.typicode.com
|
|
30
|
-
`;
|
|
31
|
-
|
|
32
|
-
const MCP_CONFIG = `{
|
|
33
|
-
"mcpServers": {
|
|
34
|
-
"zond": {
|
|
35
|
-
"command": "zond",
|
|
36
|
-
"args": ["mcp"]
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
`;
|
|
41
|
-
|
|
42
|
-
function writeIfMissing(filePath: string, content: string, force: boolean): boolean {
|
|
43
|
-
if (!force && existsSync(filePath)) {
|
|
44
|
-
console.log(` Skipped ${filePath} (already exists)`);
|
|
45
|
-
return false;
|
|
46
|
-
}
|
|
47
|
-
const dir = dirname(filePath);
|
|
48
|
-
if (!existsSync(dir)) {
|
|
49
|
-
mkdirSync(dir, { recursive: true });
|
|
50
|
-
}
|
|
51
|
-
writeFileSync(filePath, content, "utf-8");
|
|
52
|
-
console.log(` Created ${filePath}`);
|
|
53
|
-
return true;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function isClaudeCodeAvailable(): boolean {
|
|
16
|
+
export async function initCommand(options: InitOptions): Promise<number> {
|
|
57
17
|
try {
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
18
|
+
const envVars: Record<string, string> = {};
|
|
19
|
+
if (options.baseUrl) envVars.base_url = options.baseUrl;
|
|
20
|
+
|
|
21
|
+
const result = await setupApi({
|
|
22
|
+
name: options.name,
|
|
23
|
+
spec: options.spec,
|
|
24
|
+
dir: options.dir,
|
|
25
|
+
envVars: Object.keys(envVars).length > 0 ? envVars : undefined,
|
|
26
|
+
dbPath: options.dbPath,
|
|
27
|
+
force: options.force,
|
|
28
|
+
insecure: options.insecure,
|
|
61
29
|
});
|
|
62
|
-
return result.exitCode === 0;
|
|
63
|
-
} catch {
|
|
64
|
-
return false;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export async function initCommand(options: InitCommandOptions): Promise<number> {
|
|
69
|
-
const cwd = process.cwd();
|
|
70
30
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
31
|
+
if (options.json) {
|
|
32
|
+
printJson(jsonOk("init", {
|
|
33
|
+
collectionId: result.collectionId,
|
|
34
|
+
baseDir: result.baseDir,
|
|
35
|
+
testPath: result.testPath,
|
|
36
|
+
endpoints: result.specEndpoints,
|
|
37
|
+
warnings: result.warnings ?? [],
|
|
38
|
+
}, result.warnings));
|
|
39
|
+
} else {
|
|
40
|
+
printSuccess(`Created API '${options.name ?? "api"}' at ${result.baseDir} (${result.specEndpoints} endpoints)`);
|
|
41
|
+
if (result.warnings) {
|
|
42
|
+
for (const w of result.warnings) {
|
|
43
|
+
process.stderr.write(`Warning: ${w}\n`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return 0;
|
|
48
|
+
} catch (err) {
|
|
49
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
50
|
+
if (options.json) {
|
|
51
|
+
printJson(jsonError("init", [message]));
|
|
52
|
+
} else {
|
|
53
|
+
printError(message);
|
|
54
|
+
}
|
|
55
|
+
return 2;
|
|
80
56
|
}
|
|
81
|
-
|
|
82
|
-
console.log("\nReady! Run: zond run tests/");
|
|
83
|
-
return 0;
|
|
84
57
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { sendAdHocRequest } from "../../core/runner/send-request.ts";
|
|
2
|
+
import { printError } from "../output.ts";
|
|
3
|
+
import { jsonOk, jsonError, printJson } from "../json-envelope.ts";
|
|
4
|
+
|
|
5
|
+
export interface RequestOptions {
|
|
6
|
+
method: string;
|
|
7
|
+
url: string;
|
|
8
|
+
headers?: string[];
|
|
9
|
+
body?: string;
|
|
10
|
+
timeout?: number;
|
|
11
|
+
env?: string;
|
|
12
|
+
api?: string;
|
|
13
|
+
jsonPath?: string;
|
|
14
|
+
dbPath?: string;
|
|
15
|
+
json?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function requestCommand(options: RequestOptions): Promise<number> {
|
|
19
|
+
try {
|
|
20
|
+
const headers: Record<string, string> = {};
|
|
21
|
+
if (options.headers) {
|
|
22
|
+
for (const h of options.headers) {
|
|
23
|
+
const colonIdx = h.indexOf(":");
|
|
24
|
+
if (colonIdx > 0) {
|
|
25
|
+
headers[h.slice(0, colonIdx).trim()] = h.slice(colonIdx + 1).trim();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const result = await sendAdHocRequest({
|
|
31
|
+
method: options.method.toUpperCase(),
|
|
32
|
+
url: options.url,
|
|
33
|
+
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
|
34
|
+
body: options.body,
|
|
35
|
+
timeout: options.timeout,
|
|
36
|
+
envName: options.env,
|
|
37
|
+
collectionName: options.api,
|
|
38
|
+
jsonPath: options.jsonPath,
|
|
39
|
+
dbPath: options.dbPath,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (options.json) {
|
|
43
|
+
printJson(jsonOk("request", result));
|
|
44
|
+
} else {
|
|
45
|
+
console.log(JSON.stringify(result, null, 2));
|
|
46
|
+
}
|
|
47
|
+
return 0;
|
|
48
|
+
} catch (err) {
|
|
49
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
50
|
+
if (options.json) {
|
|
51
|
+
printJson(jsonError("request", [message]));
|
|
52
|
+
} else {
|
|
53
|
+
printError(message);
|
|
54
|
+
}
|
|
55
|
+
return 1;
|
|
56
|
+
}
|
|
57
|
+
}
|
package/src/cli/commands/run.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { ReporterName } from "../../core/reporter/types.ts";
|
|
|
9
9
|
import type { TestSuite } from "../../core/parser/types.ts";
|
|
10
10
|
import type { TestRunResult } from "../../core/runner/types.ts";
|
|
11
11
|
import { printError, printWarning } from "../output.ts";
|
|
12
|
+
import { jsonOk, jsonError, printJson } from "../json-envelope.ts";
|
|
12
13
|
import { getDb } from "../../db/schema.ts";
|
|
13
14
|
import { createRun, finalizeRun, saveResults, findCollectionByTestPath } from "../../db/queries.ts";
|
|
14
15
|
|
|
@@ -25,6 +26,7 @@ export interface RunOptions {
|
|
|
25
26
|
tag?: string[];
|
|
26
27
|
envVars?: string[];
|
|
27
28
|
dryRun?: boolean;
|
|
29
|
+
json?: boolean;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
export async function runCommand(options: RunOptions): Promise<number> {
|
|
@@ -51,14 +53,19 @@ export async function runCommand(options: RunOptions): Promise<number> {
|
|
|
51
53
|
}
|
|
52
54
|
}
|
|
53
55
|
|
|
54
|
-
// 1c. Safe mode:
|
|
56
|
+
// 1c. Safe mode: keep GET, set-only steps, and auth-related requests
|
|
55
57
|
if (options.safe) {
|
|
58
|
+
const AUTH_PATH_RE = /\/(auth|login|signin|token|oauth)\b/i;
|
|
56
59
|
for (const suite of suites) {
|
|
57
|
-
suite.tests = suite.tests.filter(t =>
|
|
60
|
+
suite.tests = suite.tests.filter(t => {
|
|
61
|
+
if (t.method === "GET" || !t.method) return true;
|
|
62
|
+
if (AUTH_PATH_RE.test(t.path)) return true;
|
|
63
|
+
return false;
|
|
64
|
+
});
|
|
58
65
|
}
|
|
59
66
|
suites = suites.filter(s => s.tests.length > 0);
|
|
60
67
|
if (suites.length === 0) {
|
|
61
|
-
printWarning("No
|
|
68
|
+
printWarning("No safe tests found. Nothing to run in safe mode.");
|
|
62
69
|
return 0;
|
|
63
70
|
}
|
|
64
71
|
}
|
|
@@ -128,29 +135,65 @@ export async function runCommand(options: RunOptions): Promise<number> {
|
|
|
128
135
|
results.push(...all);
|
|
129
136
|
}
|
|
130
137
|
|
|
131
|
-
// 5.
|
|
132
|
-
const
|
|
133
|
-
|
|
138
|
+
// 5. Collect warnings
|
|
139
|
+
const warnings: string[] = [];
|
|
140
|
+
const rateLimited = results.flatMap(r => r.steps)
|
|
141
|
+
.filter(s => s.response?.status === 429);
|
|
142
|
+
if (rateLimited.length > 0) {
|
|
143
|
+
warnings.push(`${rateLimited.length} request(s) hit rate limit (429). Consider: consolidating login steps, adding --bail, or using retry_until with delay.`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 5b. Report
|
|
147
|
+
if (!options.json) {
|
|
148
|
+
const reporter = getReporter(options.report);
|
|
149
|
+
reporter.report(results);
|
|
150
|
+
for (const w of warnings) {
|
|
151
|
+
printWarning(w);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
134
154
|
|
|
135
155
|
// 6. Save to DB
|
|
156
|
+
let savedRunId: number | undefined;
|
|
136
157
|
if (!options.noDb) {
|
|
137
158
|
try {
|
|
138
159
|
getDb(options.dbPath);
|
|
139
160
|
const collection = findCollectionByTestPath(options.path);
|
|
140
|
-
|
|
161
|
+
savedRunId = createRun({
|
|
141
162
|
started_at: results[0]?.started_at ?? new Date().toISOString(),
|
|
142
163
|
environment: options.env,
|
|
143
164
|
collection_id: collection?.id,
|
|
144
165
|
});
|
|
145
|
-
finalizeRun(
|
|
146
|
-
saveResults(
|
|
166
|
+
finalizeRun(savedRunId, results);
|
|
167
|
+
saveResults(savedRunId, results);
|
|
147
168
|
} catch (err) {
|
|
148
169
|
printWarning(`Failed to save results to DB: ${(err as Error).message}`);
|
|
149
170
|
}
|
|
150
171
|
}
|
|
151
172
|
|
|
152
173
|
// 7. Exit code (always 0 in dry-run mode)
|
|
153
|
-
if (dryRun)
|
|
174
|
+
if (dryRun) {
|
|
175
|
+
if (options.json) {
|
|
176
|
+
printJson(jsonOk("run", { summary: { total: results.length, passed: 0, failed: 0 }, dryRun: true }));
|
|
177
|
+
}
|
|
178
|
+
return 0;
|
|
179
|
+
}
|
|
154
180
|
const hasFailures = results.some((r) => r.failed > 0 || r.steps.some((s) => s.status === "error"));
|
|
181
|
+
|
|
182
|
+
if (options.json) {
|
|
183
|
+
const total = results.reduce((s, r) => s + r.total, 0);
|
|
184
|
+
const passed = results.reduce((s, r) => s + r.passed, 0);
|
|
185
|
+
const failed = results.reduce((s, r) => s + r.failed, 0);
|
|
186
|
+
const failures = results.flatMap(r =>
|
|
187
|
+
r.steps.filter(s => s.status === "fail" || s.status === "error").map(s => ({
|
|
188
|
+
suite: r.suite_name,
|
|
189
|
+
test: s.name,
|
|
190
|
+
...(r.suite_file ? { file: r.suite_file } : {}),
|
|
191
|
+
status: s.status,
|
|
192
|
+
error: s.error,
|
|
193
|
+
}))
|
|
194
|
+
);
|
|
195
|
+
printJson(jsonOk("run", { summary: { total, passed, failed }, failures, warnings, runId: savedRunId }));
|
|
196
|
+
}
|
|
197
|
+
|
|
155
198
|
return hasFailures ? 1 : 0;
|
|
156
199
|
}
|
|
@@ -3,20 +3,79 @@ import { startServer } from "../../web/server.ts";
|
|
|
3
3
|
export interface ServeOptions {
|
|
4
4
|
port?: number;
|
|
5
5
|
host?: string;
|
|
6
|
-
openapiSpec?: string;
|
|
7
|
-
testsDir?: string;
|
|
8
6
|
dbPath?: string;
|
|
9
7
|
watch?: boolean;
|
|
8
|
+
open?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Kill any existing process listening on the given port (Windows + Unix) */
|
|
12
|
+
async function killPortHolder(port: number): Promise<void> {
|
|
13
|
+
const isWin = process.platform === "win32";
|
|
14
|
+
try {
|
|
15
|
+
if (isWin) {
|
|
16
|
+
// PowerShell: find PID on port, then kill it
|
|
17
|
+
const find = Bun.spawn(["powershell", "-NoProfile", "-Command",
|
|
18
|
+
`(Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue).OwningProcess`], {
|
|
19
|
+
stdout: "pipe", stderr: "ignore",
|
|
20
|
+
});
|
|
21
|
+
const out = await new Response(find.stdout).text();
|
|
22
|
+
const pids = [...new Set(out.trim().split(/\s+/).filter(s => /^\d+$/.test(s) && s !== "0"))];
|
|
23
|
+
for (const pid of pids) {
|
|
24
|
+
Bun.spawn(["powershell", "-NoProfile", "-Command",
|
|
25
|
+
`Stop-Process -Id ${pid} -Force -ErrorAction SilentlyContinue`], {
|
|
26
|
+
stdout: "ignore", stderr: "ignore",
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
if (pids.length > 0) {
|
|
30
|
+
// Give OS time to release the port
|
|
31
|
+
await Bun.sleep(500);
|
|
32
|
+
}
|
|
33
|
+
} else {
|
|
34
|
+
// Unix: lsof + kill
|
|
35
|
+
const find = Bun.spawn(["lsof", "-ti", `:${port}`], {
|
|
36
|
+
stdout: "pipe", stderr: "ignore",
|
|
37
|
+
});
|
|
38
|
+
const out = await new Response(find.stdout).text();
|
|
39
|
+
const pids = out.trim().split(/\s+/).filter(s => /^\d+$/.test(s));
|
|
40
|
+
for (const pid of pids) {
|
|
41
|
+
Bun.spawn(["kill", "-9", pid], { stdout: "ignore", stderr: "ignore" });
|
|
42
|
+
}
|
|
43
|
+
if (pids.length > 0) {
|
|
44
|
+
await Bun.sleep(300);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// Best effort — if we can't kill, startServer will fail with port-in-use
|
|
49
|
+
}
|
|
10
50
|
}
|
|
11
51
|
|
|
12
52
|
export async function serveCommand(options: ServeOptions): Promise<number> {
|
|
53
|
+
const port = options.port ?? 8080;
|
|
54
|
+
|
|
55
|
+
// Kill previous instance on the same port
|
|
56
|
+
await killPortHolder(port);
|
|
57
|
+
|
|
13
58
|
await startServer({
|
|
14
|
-
port
|
|
59
|
+
port,
|
|
15
60
|
host: options.host,
|
|
16
61
|
dbPath: options.dbPath,
|
|
17
62
|
dev: options.watch,
|
|
18
63
|
});
|
|
19
64
|
|
|
65
|
+
// Open browser if requested
|
|
66
|
+
if (options.open) {
|
|
67
|
+
const host = options.host === "0.0.0.0" || !options.host ? "localhost" : options.host;
|
|
68
|
+
const url = `http://${host}:${port}`;
|
|
69
|
+
try {
|
|
70
|
+
const cmd = process.platform === "win32" ? ["cmd", "/c", "start", url]
|
|
71
|
+
: process.platform === "darwin" ? ["open", url]
|
|
72
|
+
: ["xdg-open", url];
|
|
73
|
+
Bun.spawn(cmd, { stdout: "ignore", stderr: "ignore" });
|
|
74
|
+
} catch {
|
|
75
|
+
// Best effort — if browser can't open, server still runs
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
20
79
|
// Keep running — Bun.serve keeps the process alive
|
|
21
80
|
return 0;
|
|
22
81
|
}
|
|
@@ -1,18 +1,34 @@
|
|
|
1
1
|
import { parse } from "../../core/parser/yaml-parser.ts";
|
|
2
2
|
import { printError, printSuccess } from "../output.ts";
|
|
3
|
+
import { jsonOk, jsonError, printJson } from "../json-envelope.ts";
|
|
3
4
|
|
|
4
5
|
export interface ValidateOptions {
|
|
5
6
|
path: string;
|
|
7
|
+
json?: boolean;
|
|
6
8
|
}
|
|
7
9
|
|
|
8
10
|
export async function validateCommand(options: ValidateOptions): Promise<number> {
|
|
9
11
|
try {
|
|
10
12
|
const suites = await parse(options.path);
|
|
11
13
|
const totalSteps = suites.reduce((sum, s) => sum + s.tests.length, 0);
|
|
12
|
-
|
|
14
|
+
if (options.json) {
|
|
15
|
+
printJson(jsonOk("validate", {
|
|
16
|
+
files: suites.length,
|
|
17
|
+
suites: suites.length,
|
|
18
|
+
tests: totalSteps,
|
|
19
|
+
valid: true,
|
|
20
|
+
}));
|
|
21
|
+
} else {
|
|
22
|
+
printSuccess(`OK: ${suites.length} suite(s), ${totalSteps} test(s) validated successfully`);
|
|
23
|
+
}
|
|
13
24
|
return 0;
|
|
14
25
|
} catch (err) {
|
|
15
|
-
|
|
26
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
27
|
+
if (options.json) {
|
|
28
|
+
printJson(jsonError("validate", [message]));
|
|
29
|
+
} else {
|
|
30
|
+
printError(message);
|
|
31
|
+
}
|
|
16
32
|
return 2;
|
|
17
33
|
}
|
|
18
34
|
}
|