@msalaam/xray-qe-toolkit 1.1.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/.env.example +16 -0
- package/README.md +893 -0
- package/bin/cli.js +137 -0
- package/commands/createExecution.js +46 -0
- package/commands/editJson.js +165 -0
- package/commands/genPipeline.js +42 -0
- package/commands/genPostman.js +70 -0
- package/commands/genTests.js +138 -0
- package/commands/importResults.js +50 -0
- package/commands/init.js +141 -0
- package/commands/pushTests.js +114 -0
- package/lib/config.js +108 -0
- package/lib/index.js +31 -0
- package/lib/logger.js +43 -0
- package/lib/postmanGenerator.js +305 -0
- package/lib/testCaseBuilder.js +202 -0
- package/lib/xrayClient.js +416 -0
- package/package.json +61 -0
- package/schema/tests.schema.json +112 -0
- package/templates/README.template.md +161 -0
- package/templates/azure-pipelines.yml +65 -0
- package/templates/knowledge-README.md +121 -0
- package/templates/tests.json +72 -0
- package/templates/xray-mapping.json +1 -0
- package/ui/editor/editor.js +484 -0
- package/ui/editor/index.html +150 -0
- package/ui/editor/styles.css +550 -0
package/bin/cli.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @msalaam/xray-qe-toolkit — CLI Entry Point
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* npx xqt <command> [options]
|
|
8
|
+
*
|
|
9
|
+
* Commands:
|
|
10
|
+
* init Scaffold tests.json, xray-mapping.json, knowledge/ folder, .env.example
|
|
11
|
+
* gen-tests Generate test cases from knowledge/ using AI
|
|
12
|
+
* edit-json Launch browser-based QE review gate editor
|
|
13
|
+
* push-tests Push / update tests in Xray Cloud
|
|
14
|
+
* gen-postman Generate Postman collection from tests.json (with optional AI)
|
|
15
|
+
* create-execution Create a new Test Execution in JIRA
|
|
16
|
+
* import-results Import JUnit/Newman results into Xray
|
|
17
|
+
* gen-pipeline Generate an Azure Pipelines YAML template
|
|
18
|
+
* mcp-server Start Model Context Protocol server for agent integration
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { Command } from "commander";
|
|
22
|
+
import { readFileSync } from "node:fs";
|
|
23
|
+
import { fileURLToPath } from "node:url";
|
|
24
|
+
import path from "node:path";
|
|
25
|
+
|
|
26
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
27
|
+
const __dirname = path.dirname(__filename);
|
|
28
|
+
const pkg = JSON.parse(readFileSync(path.join(__dirname, "..", "package.json"), "utf8"));
|
|
29
|
+
|
|
30
|
+
const program = new Command();
|
|
31
|
+
|
|
32
|
+
program
|
|
33
|
+
.name("xray-qe")
|
|
34
|
+
.description("QE workflow toolkit for Xray Cloud — test management, Postman generation, and CI integration")
|
|
35
|
+
.version(pkg.version)
|
|
36
|
+
.option("--verbose", "Enable debug output")
|
|
37
|
+
.option("--env <path>", "Custom path to .env file");
|
|
38
|
+
|
|
39
|
+
// ─── Commands ──────────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
program
|
|
42
|
+
.command("init")
|
|
43
|
+
.description("Initialize a new project with knowledge/ folder, tests.json, xray-mapping.json, and .env.example")
|
|
44
|
+
.action(async (opts, cmd) => {
|
|
45
|
+
const mod = await import("../commands/init.js");
|
|
46
|
+
await mod.default(cmd.optsWithGlobals());
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
program
|
|
50
|
+
.command("gen-tests")
|
|
51
|
+
.description("Generate test cases from knowledge/ folder using AI")
|
|
52
|
+
.option("--ai", "Enable AI-assisted generation (requires connected provider)")
|
|
53
|
+
.option("--spec <path>", "OpenAPI/Swagger spec file to analyze")
|
|
54
|
+
.option("--knowledge <path>", "Custom knowledge folder path")
|
|
55
|
+
.option("--ticket <key>", "JIRA ticket key to fetch and analyze")
|
|
56
|
+
.option("--prompt <text>", "Natural language prompt for test generation")
|
|
57
|
+
.action(async (opts, cmd) => {
|
|
58
|
+
const mod = await import("../commands/genTests.js");
|
|
59
|
+
await mod.default({ ...opts, ...cmd.optsWithGlobals() });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
program
|
|
63
|
+
.command("edit-json")
|
|
64
|
+
.description("Launch browser-based QE review gate editor for tests.json")
|
|
65
|
+
.option("--port <number>", "Port for the local editor server", "0")
|
|
66
|
+
.action(async (opts, cmd) => {
|
|
67
|
+
const mod = await import("../commands/editJson.js");
|
|
68
|
+
await mod.default({ ...opts, ...cmd.optsWithGlobals() });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
program
|
|
72
|
+
.command("push-tests")
|
|
73
|
+
.description("Push or update tests in Xray Cloud from tests.json")
|
|
74
|
+
.option("--testExecKey <key>", "Link tests to an existing Test Execution key instead of creating a new one")
|
|
75
|
+
.option("--skip-exec", "Push tests without creating or linking a Test Execution")
|
|
76
|
+
.action(async (opts, cmd) => {
|
|
77
|
+
const mod = await import("../commands/pushTests.js");
|
|
78
|
+
await mod.default({ ...opts, ...cmd.optsWithGlobals() });
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
program
|
|
82
|
+
.command("gen-postman")
|
|
83
|
+
.description("Generate a Postman collection from tests.json (with optional AI enhancement)")
|
|
84
|
+
.option("--base-url <url>", "Base URL for API requests", "{{baseUrl}}")
|
|
85
|
+
.option("--ai", "Enable AI-assisted generation for better assertions")
|
|
86
|
+
.option("--spec <path>", "OpenAPI/Swagger spec file for schema-driven generation")
|
|
87
|
+
.option("--knowledge <path>", "Custom knowledge folder path")
|
|
88
|
+
.action(async (opts, cmd) => {
|
|
89
|
+
const mod = await import("../commands/genPostman.js");
|
|
90
|
+
await mod.default({ ...opts, ...cmd.optsWithGlobals() });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
program
|
|
94
|
+
.command("create-execution")
|
|
95
|
+
.description("Create a new Test Execution issue in JIRA")
|
|
96
|
+
.requiredOption("--summary <text>", "Test Execution summary/title")
|
|
97
|
+
.option("--description <text>", "Test Execution description")
|
|
98
|
+
.option("--issue <key>", "Parent issue key to link the execution to")
|
|
99
|
+
.action(async (opts, cmd) => {
|
|
100
|
+
const mod = await import("../commands/createExecution.js");
|
|
101
|
+
await mod.default({ ...opts, ...cmd.optsWithGlobals() });
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
program
|
|
105
|
+
.command("import-results")
|
|
106
|
+
.description("Import JUnit/Newman XML results into Xray Cloud")
|
|
107
|
+
.requiredOption("--file <path>", "Path to the results XML file")
|
|
108
|
+
.requiredOption("--testExecKey <key>", "Test Execution key to associate results with")
|
|
109
|
+
.action(async (opts, cmd) => {
|
|
110
|
+
const mod = await import("../commands/importResults.js");
|
|
111
|
+
await mod.default({ ...opts, ...cmd.optsWithGlobals() });
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
program
|
|
115
|
+
.command("gen-pipeline")
|
|
116
|
+
.description("Generate an Azure Pipelines YAML template")
|
|
117
|
+
.option("--output <path>", "Output file path", "azure-pipelines.yml")
|
|
118
|
+
.action(async (opts, cmd) => {
|
|
119
|
+
const mod = await import("../commands/genPipeline.js");
|
|
120
|
+
await mod.default({ ...opts, ...cmd.optsWithGlobals() });
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
program
|
|
124
|
+
.command("mcp-server")
|
|
125
|
+
.description("Start Model Context Protocol server for Copilot agent integration")
|
|
126
|
+
.option("--port <number>", "Server port (default: stdio mode)")
|
|
127
|
+
.action(async (opts, cmd) => {
|
|
128
|
+
const mod = await import("../mcp/server.js");
|
|
129
|
+
await mod.default({ ...opts, ...cmd.optsWithGlobals() });
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// ─── Parse & run ───────────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
135
|
+
console.error(`\n❌ ${err.message}\n`);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command: xray-qe create-execution --summary "..." [--description "..."] [--issue QE-123]
|
|
3
|
+
*
|
|
4
|
+
* Creates a standalone Test Execution issue in JIRA.
|
|
5
|
+
* Optionally links it to a parent issue via --issue.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import logger, { setVerbose } from "../lib/logger.js";
|
|
9
|
+
import { loadConfig, validateConfig } from "../lib/config.js";
|
|
10
|
+
import { createIssue, linkIssues } from "../lib/xrayClient.js";
|
|
11
|
+
|
|
12
|
+
export default async function createExecution(opts = {}) {
|
|
13
|
+
if (opts.verbose) setVerbose(true);
|
|
14
|
+
|
|
15
|
+
logger.rocket("@msalaam/xray-qe-toolkit — Create Execution\n");
|
|
16
|
+
|
|
17
|
+
const cfg = loadConfig({ envPath: opts.env });
|
|
18
|
+
validateConfig(cfg);
|
|
19
|
+
|
|
20
|
+
logger.pin(`Creating Test Execution: "${opts.summary}"...`);
|
|
21
|
+
|
|
22
|
+
const execution = await createIssue(cfg, "Test Execution", {
|
|
23
|
+
summary: opts.summary,
|
|
24
|
+
description: opts.description || opts.summary,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
logger.success(`${execution.key} created`);
|
|
28
|
+
logger.link(`View: ${cfg.jiraUrl}/browse/${execution.key}`);
|
|
29
|
+
|
|
30
|
+
// Optionally link to parent issue
|
|
31
|
+
if (opts.issue) {
|
|
32
|
+
logger.blank();
|
|
33
|
+
logger.link(`Linking to ${opts.issue}...`);
|
|
34
|
+
try {
|
|
35
|
+
await linkIssues(cfg, execution.key, opts.issue);
|
|
36
|
+
logger.step(`Linked to ${opts.issue}`);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
logger.warn(`Could not link: ${err.message}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
logger.blank();
|
|
43
|
+
console.log(`Test Execution Key: ${execution.key}`);
|
|
44
|
+
console.log(`Test Execution ID: ${execution.id}`);
|
|
45
|
+
logger.blank();
|
|
46
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command: xray-qe edit-json [--port <number>]
|
|
3
|
+
*
|
|
4
|
+
* QE Review Gate — launches a local Express server with a browser-based
|
|
5
|
+
* JSON editor that lets QEs:
|
|
6
|
+
* • View, add, edit, delete tests
|
|
7
|
+
* • Tag test types (regression, edge, critical, etc.)
|
|
8
|
+
* • Toggle skip/push per test
|
|
9
|
+
* • Validate against tests.schema.json in real-time
|
|
10
|
+
* • Save changes back to tests.json
|
|
11
|
+
*
|
|
12
|
+
* The command blocks until the QE clicks "Save & Exit" or closes the browser.
|
|
13
|
+
* Nothing is pushed to Xray until the QE explicitly runs push-tests.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
import { createServer } from "node:http";
|
|
20
|
+
import express from "express";
|
|
21
|
+
import open from "open";
|
|
22
|
+
import logger, { setVerbose } from "../lib/logger.js";
|
|
23
|
+
import { loadConfig } from "../lib/config.js";
|
|
24
|
+
|
|
25
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
26
|
+
const __dirname = path.dirname(__filename);
|
|
27
|
+
const UI_DIR = path.join(__dirname, "..", "ui", "editor");
|
|
28
|
+
const SCHEMA_PATH = path.join(__dirname, "..", "schema", "tests.schema.json");
|
|
29
|
+
|
|
30
|
+
export default async function editJson(opts = {}) {
|
|
31
|
+
if (opts.verbose) setVerbose(true);
|
|
32
|
+
|
|
33
|
+
logger.rocket("@msalaam/xray-qe-toolkit — QE Review Gate\n");
|
|
34
|
+
|
|
35
|
+
const cfg = loadConfig({ envPath: opts.env });
|
|
36
|
+
const testsPath = cfg.testsPath;
|
|
37
|
+
|
|
38
|
+
// If tests.json doesn't exist, create from template
|
|
39
|
+
if (!fs.existsSync(testsPath)) {
|
|
40
|
+
const templatePath = path.join(__dirname, "..", "templates", "tests.json");
|
|
41
|
+
fs.copyFileSync(templatePath, testsPath);
|
|
42
|
+
logger.info("Created tests.json from template");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Create Express app
|
|
46
|
+
const app = express();
|
|
47
|
+
app.use(express.json({ limit: "10mb" }));
|
|
48
|
+
|
|
49
|
+
// Serve static UI files
|
|
50
|
+
app.use(express.static(UI_DIR));
|
|
51
|
+
|
|
52
|
+
// ─── API Routes ────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
// GET /api/tests — read tests.json from disk
|
|
55
|
+
app.get("/api/tests", (req, res) => {
|
|
56
|
+
try {
|
|
57
|
+
const content = JSON.parse(fs.readFileSync(testsPath, "utf8"));
|
|
58
|
+
res.json(content);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
res.status(500).json({ error: `Failed to read tests.json: ${err.message}` });
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// PUT /api/tests — validate and write tests.json
|
|
65
|
+
app.put("/api/tests", (req, res) => {
|
|
66
|
+
try {
|
|
67
|
+
const data = req.body;
|
|
68
|
+
|
|
69
|
+
// Basic structural validation
|
|
70
|
+
if (!data || !Array.isArray(data.tests)) {
|
|
71
|
+
return res.status(400).json({ error: "Invalid structure: must have a tests array" });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Check for duplicate test_ids
|
|
75
|
+
const ids = data.tests.map((t) => t.test_id);
|
|
76
|
+
const dupes = ids.filter((id, i) => ids.indexOf(id) !== i);
|
|
77
|
+
if (dupes.length > 0) {
|
|
78
|
+
return res.status(400).json({ error: `Duplicate test_id(s): ${[...new Set(dupes)].join(", ")}` });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
fs.writeFileSync(testsPath, JSON.stringify(data, null, 2));
|
|
82
|
+
logger.save(`tests.json saved (${data.tests.length} tests)`);
|
|
83
|
+
res.json({ success: true, count: data.tests.length });
|
|
84
|
+
} catch (err) {
|
|
85
|
+
res.status(500).json({ error: `Failed to save: ${err.message}` });
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// GET /api/schema — return the JSON schema for client-side validation
|
|
90
|
+
app.get("/api/schema", (req, res) => {
|
|
91
|
+
try {
|
|
92
|
+
const schema = JSON.parse(fs.readFileSync(SCHEMA_PATH, "utf8"));
|
|
93
|
+
res.json(schema);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
res.status(500).json({ error: `Failed to read schema: ${err.message}` });
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// GET /api/mapping — return current xray-mapping.json
|
|
100
|
+
app.get("/api/mapping", (req, res) => {
|
|
101
|
+
try {
|
|
102
|
+
if (fs.existsSync(cfg.mappingPath)) {
|
|
103
|
+
const mapping = JSON.parse(fs.readFileSync(cfg.mappingPath, "utf8"));
|
|
104
|
+
res.json(mapping);
|
|
105
|
+
} else {
|
|
106
|
+
res.json({});
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
res.json({});
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// POST /api/shutdown — graceful server shutdown
|
|
114
|
+
app.post("/api/shutdown", (req, res) => {
|
|
115
|
+
res.json({ success: true });
|
|
116
|
+
logger.blank();
|
|
117
|
+
logger.success("QE review gate complete — editor closed");
|
|
118
|
+
logger.info("Run 'npx xqt push-tests' to push changes to Xray\n");
|
|
119
|
+
// Give the response time to flush, then exit
|
|
120
|
+
setTimeout(() => {
|
|
121
|
+
server.close();
|
|
122
|
+
process.exit(0);
|
|
123
|
+
}, 500);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// ─── Start server ──────────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
const port = parseInt(opts.port) || 0; // 0 = random available port
|
|
129
|
+
const server = createServer(app);
|
|
130
|
+
|
|
131
|
+
await new Promise((resolve, reject) => {
|
|
132
|
+
server.listen(port, "127.0.0.1", () => {
|
|
133
|
+
const addr = server.address();
|
|
134
|
+
const url = `http://127.0.0.1:${addr.port}`;
|
|
135
|
+
|
|
136
|
+
logger.success(`Editor running at ${url}`);
|
|
137
|
+
logger.info(`Editing: ${testsPath}`);
|
|
138
|
+
logger.blank();
|
|
139
|
+
console.log(" Open the URL above in your browser to review tests.");
|
|
140
|
+
console.log(" Click 'Save & Exit' when done. The server will shut down automatically.");
|
|
141
|
+
console.log(" Press Ctrl+C to cancel without saving.\n");
|
|
142
|
+
|
|
143
|
+
// Auto-open browser
|
|
144
|
+
open(url).catch(() => {
|
|
145
|
+
// Ignore errors if browser can't be opened (e.g., headless environment)
|
|
146
|
+
logger.warn("Could not auto-open browser. Open the URL manually.");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
resolve();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
server.on("error", reject);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Keep process alive until shutdown
|
|
156
|
+
await new Promise((resolve) => {
|
|
157
|
+
server.on("close", resolve);
|
|
158
|
+
process.on("SIGINT", () => {
|
|
159
|
+
logger.blank();
|
|
160
|
+
logger.warn("Editor cancelled (Ctrl+C) — changes may not be saved");
|
|
161
|
+
server.close();
|
|
162
|
+
resolve();
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command: xray-qe gen-pipeline [--output <path>]
|
|
3
|
+
*
|
|
4
|
+
* Copies the Azure Pipelines YAML template into the consuming project.
|
|
5
|
+
* The template runs Newman and imports results — no QE logic in CI.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import logger, { setVerbose } from "../lib/logger.js";
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = path.dirname(__filename);
|
|
15
|
+
|
|
16
|
+
export default async function genPipeline(opts = {}) {
|
|
17
|
+
if (opts.verbose) setVerbose(true);
|
|
18
|
+
|
|
19
|
+
logger.rocket("@msalaam/xray-qe-toolkit — Generate Pipeline\n");
|
|
20
|
+
|
|
21
|
+
const templatePath = path.join(__dirname, "..", "templates", "azure-pipelines.yml");
|
|
22
|
+
const outputPath = path.resolve(opts.output || "azure-pipelines.yml");
|
|
23
|
+
|
|
24
|
+
if (fs.existsSync(outputPath)) {
|
|
25
|
+
logger.warn(`${path.basename(outputPath)} already exists — overwriting`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
fs.copyFileSync(templatePath, outputPath);
|
|
29
|
+
|
|
30
|
+
logger.success(`Pipeline template saved to ${path.basename(outputPath)}`);
|
|
31
|
+
logger.blank();
|
|
32
|
+
|
|
33
|
+
console.log("📖 Next steps:");
|
|
34
|
+
console.log(" 1. Set pipeline variables in Azure DevOps:");
|
|
35
|
+
console.log(" XRAY_ID, XRAY_SECRET, JIRA_PROJECT_KEY, JIRA_URL,");
|
|
36
|
+
console.log(" JIRA_API_TOKEN, JIRA_EMAIL, TEST_EXEC_KEY");
|
|
37
|
+
console.log(" 2. Commit the generated file to your repo");
|
|
38
|
+
console.log(" 3. Create a pipeline in Azure DevOps pointing to this YAML");
|
|
39
|
+
console.log("");
|
|
40
|
+
console.log("⚠️ Remember: edit-json and QE review gates are NOT part of CI.");
|
|
41
|
+
console.log(" Run those steps locally before committing.\n");
|
|
42
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command: xray-qe gen-postman [--base-url <url>] [--ai] [--spec <path>] [--knowledge <path>]
|
|
3
|
+
*
|
|
4
|
+
* Generates a Postman Collection v2.1 JSON file from tests.json.
|
|
5
|
+
* Optionally uses xray-mapping.json to embed JIRA keys for better traceability.
|
|
6
|
+
* With --ai flag, enables AI-assisted generation (requires connected provider).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import logger, { setVerbose } from "../lib/logger.js";
|
|
11
|
+
import { loadConfig } from "../lib/config.js";
|
|
12
|
+
import { generate } from "../lib/postmanGenerator.js";
|
|
13
|
+
import { loadMapping } from "../lib/testCaseBuilder.js";
|
|
14
|
+
|
|
15
|
+
export default async function genPostman(opts = {}) {
|
|
16
|
+
if (opts.verbose) setVerbose(true);
|
|
17
|
+
|
|
18
|
+
logger.rocket("@msalaam/xray-qe-toolkit — Generate Postman Collection\n");
|
|
19
|
+
|
|
20
|
+
const cfg = loadConfig({ envPath: opts.env });
|
|
21
|
+
|
|
22
|
+
if (!fs.existsSync(cfg.testsPath)) {
|
|
23
|
+
logger.error(`tests.json not found at ${cfg.testsPath}`);
|
|
24
|
+
logger.info("Run 'npx xray-qe init' to create a starter tests.json");
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const testsConfig = JSON.parse(fs.readFileSync(cfg.testsPath, "utf8"));
|
|
29
|
+
|
|
30
|
+
// Load mapping to embed JIRA keys when available
|
|
31
|
+
const mapping = loadMapping(cfg.mappingPath);
|
|
32
|
+
|
|
33
|
+
// Filter out skipped tests
|
|
34
|
+
const filteredConfig = {
|
|
35
|
+
...testsConfig,
|
|
36
|
+
tests: (testsConfig.tests || []).filter((t) => !t.skip),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// AI flag handling
|
|
40
|
+
if (opts.ai) {
|
|
41
|
+
logger.info("🤖 AI-assisted generation enabled");
|
|
42
|
+
logger.warn("AI provider not yet connected — using schema-driven generation as fallback");
|
|
43
|
+
logger.blank();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Knowledge path override
|
|
47
|
+
const knowledgePath = opts.knowledge || cfg.knowledgePath;
|
|
48
|
+
if (opts.knowledge) {
|
|
49
|
+
logger.info(`Using custom knowledge path: ${knowledgePath}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Spec file handling (schema-driven generation without AI)
|
|
53
|
+
if (opts.spec && fs.existsSync(opts.spec)) {
|
|
54
|
+
logger.info(`Using OpenAPI spec: ${opts.spec}`);
|
|
55
|
+
logger.info("📝 Schema-driven endpoint inference enabled");
|
|
56
|
+
// TODO: Parse OpenAPI spec and enhance endpoint/method/body inference
|
|
57
|
+
logger.blank();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
generate(filteredConfig, cfg.collectionPath, {
|
|
61
|
+
baseUrl: opts.baseUrl || "{{baseUrl}}",
|
|
62
|
+
mapping: mapping,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
logger.blank();
|
|
66
|
+
logger.success(`Collection saved to ${cfg.collectionPath}`);
|
|
67
|
+
logger.info("Import into Postman or run with Newman:");
|
|
68
|
+
console.log(` npx newman run ${cfg.collectionPath}`);
|
|
69
|
+
logger.blank();
|
|
70
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command: xray-qe gen-tests [--ai] [--spec <path>] [--knowledge <path>] [--ticket <key>] [--prompt <text>]
|
|
3
|
+
*
|
|
4
|
+
* Generate test cases from knowledge/ folder documentation and specifications.
|
|
5
|
+
* Uses AI to analyze specs, requirements, and business logic to create structured test definitions.
|
|
6
|
+
*
|
|
7
|
+
* Without --ai: Provides guidance on manual test creation.
|
|
8
|
+
* With --ai: Uses connected AI provider to generate tests from knowledge sources.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import logger, { setVerbose } from "../lib/logger.js";
|
|
14
|
+
import { loadConfig } from "../lib/config.js";
|
|
15
|
+
|
|
16
|
+
export default async function genTests(opts = {}) {
|
|
17
|
+
if (opts.verbose) setVerbose(true);
|
|
18
|
+
|
|
19
|
+
logger.rocket("@msalaam/xray-qe-toolkit — Generate Test Cases\n");
|
|
20
|
+
|
|
21
|
+
const cfg = loadConfig({ envPath: opts.env });
|
|
22
|
+
const knowledgePath = opts.knowledge || cfg.knowledgePath;
|
|
23
|
+
|
|
24
|
+
// Check if knowledge folder exists
|
|
25
|
+
if (!fs.existsSync(knowledgePath)) {
|
|
26
|
+
logger.error(`Knowledge folder not found at ${knowledgePath}`);
|
|
27
|
+
logger.info("Run 'npx xray-qe init' to create the knowledge/ folder structure");
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Validate --ai flag requirement
|
|
32
|
+
if (!opts.ai) {
|
|
33
|
+
logger.error("AI-assisted generation is not enabled");
|
|
34
|
+
logger.info("Use the --ai flag to enable AI-powered test generation");
|
|
35
|
+
logger.blank();
|
|
36
|
+
console.log("📖 Manual test creation workflow:");
|
|
37
|
+
console.log(" 1. Review documentation in knowledge/ folder");
|
|
38
|
+
console.log(" 2. Create test cases manually: npx xray-qe edit-json");
|
|
39
|
+
console.log(" 3. Or enable AI generation: npx xray-qe gen-tests --ai");
|
|
40
|
+
logger.blank();
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
logger.info("🤖 AI-assisted test generation enabled");
|
|
45
|
+
logger.blank();
|
|
46
|
+
|
|
47
|
+
// Scan knowledge folder for source documents
|
|
48
|
+
const sources = scanKnowledgeFolder(knowledgePath);
|
|
49
|
+
|
|
50
|
+
// Handle specific source flags
|
|
51
|
+
if (opts.spec && fs.existsSync(opts.spec)) {
|
|
52
|
+
logger.info(`📄 OpenAPI spec: ${opts.spec}`);
|
|
53
|
+
sources.apiSpecs.push(opts.spec);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (opts.ticket) {
|
|
57
|
+
logger.info(`🎫 JIRA ticket: ${opts.ticket}`);
|
|
58
|
+
sources.tickets.push(opts.ticket);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (opts.prompt) {
|
|
62
|
+
logger.info(`💬 Prompt: "${opts.prompt}"`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
logger.blank();
|
|
66
|
+
logger.info("📚 Knowledge sources discovered:");
|
|
67
|
+
console.log(` API Specs: ${sources.apiSpecs.length} file(s)`);
|
|
68
|
+
console.log(` Requirements: ${sources.requirements.length} file(s)`);
|
|
69
|
+
console.log(` Tickets: ${sources.tickets.length} file(s)`);
|
|
70
|
+
logger.blank();
|
|
71
|
+
|
|
72
|
+
if (sources.apiSpecs.length === 0 && sources.requirements.length === 0 && sources.tickets.length === 0) {
|
|
73
|
+
logger.warn("No knowledge sources found!");
|
|
74
|
+
logger.info("Add OpenAPI specs, requirements docs, or JIRA exports to knowledge/ folders");
|
|
75
|
+
logger.info("See knowledge/README.md for supported file types");
|
|
76
|
+
logger.blank();
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// AI provider connection scaffold (not yet implemented)
|
|
81
|
+
logger.warn("⚠️ AI provider not yet connected");
|
|
82
|
+
logger.blank();
|
|
83
|
+
console.log("🔧 To connect an AI provider:");
|
|
84
|
+
console.log(" 1. Implement AI provider integration in mcp/server.js");
|
|
85
|
+
console.log(" 2. Configure provider credentials in .env");
|
|
86
|
+
console.log(" 3. Or use the MCP server: npx xray-qe mcp-server");
|
|
87
|
+
console.log(" 4. Or use VS Code @xray-qe chat participant");
|
|
88
|
+
logger.blank();
|
|
89
|
+
console.log("📋 For now, you can:");
|
|
90
|
+
console.log(" • Review knowledge sources and create tests manually via 'npx xray-qe edit-json'");
|
|
91
|
+
console.log(" • Use the browser-based editor to build your test suite");
|
|
92
|
+
console.log(" • Run 'npx xray-qe gen-postman --spec <file>' for schema-driven Postman generation");
|
|
93
|
+
logger.blank();
|
|
94
|
+
|
|
95
|
+
logger.info("Scaffold ready. Connect an AI provider to enable automated test generation.");
|
|
96
|
+
logger.blank();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Scan knowledge folder and return categorized file paths
|
|
101
|
+
*/
|
|
102
|
+
function scanKnowledgeFolder(knowledgePath) {
|
|
103
|
+
const sources = {
|
|
104
|
+
apiSpecs: [],
|
|
105
|
+
requirements: [],
|
|
106
|
+
tickets: [],
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const apiSpecsDir = path.join(knowledgePath, "api-specs");
|
|
110
|
+
const requirementsDir = path.join(knowledgePath, "requirements");
|
|
111
|
+
const ticketsDir = path.join(knowledgePath, "tickets");
|
|
112
|
+
|
|
113
|
+
// Scan api-specs/
|
|
114
|
+
if (fs.existsSync(apiSpecsDir)) {
|
|
115
|
+
const files = fs.readdirSync(apiSpecsDir);
|
|
116
|
+
sources.apiSpecs = files
|
|
117
|
+
.filter((f) => /\.(yaml|yml|json|graphql|gql)$/i.test(f))
|
|
118
|
+
.map((f) => path.join(apiSpecsDir, f));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Scan requirements/
|
|
122
|
+
if (fs.existsSync(requirementsDir)) {
|
|
123
|
+
const files = fs.readdirSync(requirementsDir);
|
|
124
|
+
sources.requirements = files
|
|
125
|
+
.filter((f) => /\.(md|txt|docx|pdf)$/i.test(f))
|
|
126
|
+
.map((f) => path.join(requirementsDir, f));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Scan tickets/
|
|
130
|
+
if (fs.existsSync(ticketsDir)) {
|
|
131
|
+
const files = fs.readdirSync(ticketsDir);
|
|
132
|
+
sources.tickets = files
|
|
133
|
+
.filter((f) => /\.(json|xml|html|md|txt)$/i.test(f))
|
|
134
|
+
.map((f) => path.join(ticketsDir, f));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return sources;
|
|
138
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command: xray-qe import-results --file <path> --testExecKey <key>
|
|
3
|
+
*
|
|
4
|
+
* Imports JUnit/Newman XML results into Xray Cloud and associates
|
|
5
|
+
* them with the specified Test Execution.
|
|
6
|
+
*
|
|
7
|
+
* Designed to run in CI — no human interaction required.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import logger, { setVerbose } from "../lib/logger.js";
|
|
13
|
+
import { loadConfig, validateConfig } from "../lib/config.js";
|
|
14
|
+
import { authenticate, importResults as importResultsApi } from "../lib/xrayClient.js";
|
|
15
|
+
|
|
16
|
+
export default async function importResults(opts = {}) {
|
|
17
|
+
if (opts.verbose) setVerbose(true);
|
|
18
|
+
|
|
19
|
+
logger.rocket("@msalaam/xray-qe-toolkit — Import Results\n");
|
|
20
|
+
|
|
21
|
+
const cfg = loadConfig({ envPath: opts.env });
|
|
22
|
+
validateConfig(cfg);
|
|
23
|
+
|
|
24
|
+
const filePath = path.resolve(opts.file);
|
|
25
|
+
if (!fs.existsSync(filePath)) {
|
|
26
|
+
logger.error(`Results file not found: ${filePath}`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
logger.info(`File: ${path.basename(filePath)}`);
|
|
31
|
+
logger.info(`Test Execution: ${opts.testExecKey}`);
|
|
32
|
+
logger.blank();
|
|
33
|
+
|
|
34
|
+
// Authenticate
|
|
35
|
+
const xrayToken = await authenticate(cfg);
|
|
36
|
+
|
|
37
|
+
// Read XML file
|
|
38
|
+
const xmlBuffer = fs.readFileSync(filePath);
|
|
39
|
+
logger.send(`Importing ${xmlBuffer.length} bytes to Xray...`);
|
|
40
|
+
|
|
41
|
+
const result = await importResultsApi(cfg, xrayToken, xmlBuffer, opts.testExecKey);
|
|
42
|
+
|
|
43
|
+
logger.success("Results imported successfully");
|
|
44
|
+
|
|
45
|
+
if (result?.key) {
|
|
46
|
+
logger.link(`View: ${cfg.jiraUrl}/browse/${result.key}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
logger.blank();
|
|
50
|
+
}
|