allure 3.5.0 → 3.6.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/README.md CHANGED
@@ -60,6 +60,22 @@ To successfully generate a report, ensure that your test setup outputs results i
60
60
 
61
61
  After the tests complete, the report is generated automatically. Existing results from previous runs are ignored, as Allure 3 focuses solely on new data to ensure accurate and up-to-date reporting.
62
62
 
63
+ ### Running Tests In Agent Mode
64
+
65
+ When you need agent-friendly markdown output for review, debugging, or scope validation, use the `agent` command:
66
+
67
+ ```bash
68
+ npx allure agent -- <test_command>
69
+ ```
70
+
71
+ For example:
72
+
73
+ ```bash
74
+ npx allure agent -- npm test
75
+ ```
76
+
77
+ `allure agent` runs with an agent-only profile by default. It creates a fresh output directory automatically, can load an expectations file with `--expectations`, and ignores configured presentation or export plugins such as Awesome or TestOps unless you explicitly fall back to the lower-level `ALLURE_AGENT_*` plus `allure run` flow.
78
+
63
79
  ### Generating Reports Manually
64
80
 
65
81
  If you already have test results and wish to generate a report manually, use the `generate` command:
@@ -102,6 +118,7 @@ The Allure CLI includes several helpful global options. Use `--help` to explore
102
118
 
103
119
  ```bash
104
120
  npx allure run --help
121
+ npx allure agent --help
105
122
  npx allure watch --help
106
123
  ```
107
124
 
@@ -0,0 +1,58 @@
1
+ import { Command } from "clipanion";
2
+ export declare class AgentCommand extends Command {
3
+ static paths: string[][];
4
+ static usage: import("clipanion").Usage;
5
+ config: string | undefined;
6
+ cwd: string | undefined;
7
+ output: string | undefined;
8
+ expectations: string | undefined;
9
+ environment: string | undefined;
10
+ environmentName: string | undefined;
11
+ silent: boolean | undefined;
12
+ rerunFrom: string | undefined;
13
+ rerunLatest: boolean | undefined;
14
+ rerunPreset: string | undefined;
15
+ rerunEnvironments: string[] | undefined;
16
+ rerunLabels: string[] | undefined;
17
+ commandToRun: string[];
18
+ execute(): Promise<void>;
19
+ }
20
+ export declare class AgentLatestCommand extends Command {
21
+ static paths: string[][];
22
+ static usage: import("clipanion").Usage;
23
+ cwd: string | undefined;
24
+ execute(): Promise<void>;
25
+ }
26
+ export declare class AgentStateDirCommand extends Command {
27
+ static paths: string[][];
28
+ static usage: import("clipanion").Usage;
29
+ cwd: string | undefined;
30
+ execute(): Promise<void>;
31
+ }
32
+ export declare class AgentSelectCommand extends Command {
33
+ static paths: string[][];
34
+ static usage: import("clipanion").Usage;
35
+ cwd: string | undefined;
36
+ from: string | undefined;
37
+ latest: boolean | undefined;
38
+ preset: string | undefined;
39
+ environments: string[] | undefined;
40
+ labels: string[] | undefined;
41
+ output: string | undefined;
42
+ execute(): Promise<void>;
43
+ }
44
+ export declare const executeAgentMode: (params: {
45
+ configPath?: string;
46
+ cwd?: string;
47
+ output?: string;
48
+ expectations?: string;
49
+ environment?: string;
50
+ environmentName?: string;
51
+ silent?: boolean;
52
+ rerunFrom?: string;
53
+ rerunLatest?: boolean;
54
+ rerunPreset?: string;
55
+ rerunEnvironments?: string[];
56
+ rerunLabels?: string[];
57
+ args?: string[];
58
+ }) => Promise<void>;
@@ -0,0 +1,374 @@
1
+ import * as console from "node:console";
2
+ import { mkdir, mkdtemp, realpath, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { dirname, join, relative, resolve } from "node:path";
5
+ import process, { exit } from "node:process";
6
+ import { AllureReport, isFileNotFoundError, readConfig } from "@allurereport/core";
7
+ import { Command, Option, UsageError } from "clipanion";
8
+ import { createAgentTestPlanContext, normalizeAgentRerunPreset, parseAgentLabelFilters, resolveAgentSelectionOutputDir, selectAgentTestPlan, } from "../utils/agent-select.js";
9
+ import { readLatestAgentState, resolveAgentStateDir, writeLatestAgentState } from "../utils/agent-state.js";
10
+ import { environmentNameOption, environmentOption, normalizeCommandEnvironmentOptions, resolveCommandEnvironment, } from "../utils/environment.js";
11
+ import { createChildAllureCliEnvironment, getActiveAllureCliCommand } from "../utils/execution-context.js";
12
+ import { executeAllureRun, executeNestedAllureCommand } from "./commons/run.js";
13
+ const withProcessEnv = async (overrides, fn) => {
14
+ const previousValues = new Map();
15
+ for (const [key, value] of Object.entries(overrides)) {
16
+ previousValues.set(key, process.env[key]);
17
+ if (value === undefined) {
18
+ delete process.env[key];
19
+ continue;
20
+ }
21
+ process.env[key] = value;
22
+ }
23
+ try {
24
+ return await fn();
25
+ }
26
+ finally {
27
+ for (const [key, value] of previousValues) {
28
+ if (value === undefined) {
29
+ delete process.env[key];
30
+ continue;
31
+ }
32
+ process.env[key] = value;
33
+ }
34
+ }
35
+ };
36
+ const isPathInside = (parentPath, candidatePath) => {
37
+ const rel = relative(parentPath, candidatePath);
38
+ return rel === "" || (!rel.startsWith("..") && rel !== "." && !rel.startsWith("../"));
39
+ };
40
+ const persistLatestAgentState = async (value) => {
41
+ try {
42
+ await writeLatestAgentState(value);
43
+ }
44
+ catch (error) {
45
+ console.error(`Could not update latest agent output in ${resolveAgentStateDir(value.cwd)}: ${error.message}`);
46
+ }
47
+ };
48
+ const readOptionalString = (value) => (typeof value === "string" ? value : undefined);
49
+ const readOptionalBoolean = (value) => value === true;
50
+ const readOptionalStringArray = (value) => (Array.isArray(value) ? value : undefined);
51
+ export class AgentCommand extends Command {
52
+ constructor() {
53
+ super(...arguments);
54
+ this.config = Option.String("--config,-c", {
55
+ description: "The path Allure config file",
56
+ });
57
+ this.cwd = Option.String("--cwd", {
58
+ description: "The working directory for the command to run (default: current working directory)",
59
+ });
60
+ this.output = Option.String("--output,-o", {
61
+ description: "The output directory for agent artifacts. Absolute paths are accepted as well",
62
+ });
63
+ this.expectations = Option.String("--expectations", {
64
+ description: "The path to a YAML or JSON expectations file",
65
+ });
66
+ this.environment = environmentOption();
67
+ this.environmentName = environmentNameOption();
68
+ this.silent = Option.Boolean("--silent", {
69
+ description: "Don't pipe the process output logs to console (default: false)",
70
+ });
71
+ this.rerunFrom = Option.String("--rerun-from", {
72
+ description: "Select tests for rerun from an existing agent output directory",
73
+ });
74
+ this.rerunLatest = Option.Boolean("--rerun-latest", {
75
+ description: "Select tests for rerun from the latest recorded agent output for the current project",
76
+ });
77
+ this.rerunPreset = Option.String("--rerun-preset", {
78
+ description: "The rerun selection preset: review, failed, unsuccessful, or all (default: review)",
79
+ });
80
+ this.rerunEnvironments = Option.Array("--rerun-environment", {
81
+ description: "Filter rerun selection by environment id. Repeat the option for multiple environments",
82
+ });
83
+ this.rerunLabels = Option.Array("--rerun-label", {
84
+ description: "Filter rerun selection by exact label name=value. Repeat the option for multiple filters",
85
+ });
86
+ this.commandToRun = Option.Rest();
87
+ }
88
+ async execute() {
89
+ const args = this.commandToRun.filter((arg) => arg !== "--");
90
+ await executeAgentMode({
91
+ configPath: readOptionalString(this.config),
92
+ cwd: readOptionalString(this.cwd),
93
+ output: readOptionalString(this.output),
94
+ expectations: readOptionalString(this.expectations),
95
+ environment: readOptionalString(this.environment),
96
+ environmentName: readOptionalString(this.environmentName),
97
+ silent: readOptionalBoolean(this.silent),
98
+ rerunFrom: readOptionalString(this.rerunFrom),
99
+ rerunLatest: readOptionalBoolean(this.rerunLatest),
100
+ rerunPreset: readOptionalString(this.rerunPreset),
101
+ rerunEnvironments: readOptionalStringArray(this.rerunEnvironments),
102
+ rerunLabels: readOptionalStringArray(this.rerunLabels),
103
+ args,
104
+ });
105
+ }
106
+ }
107
+ AgentCommand.paths = [["agent"]];
108
+ AgentCommand.usage = Command.Usage({
109
+ description: "Run specified command in Allure agent mode",
110
+ details: "This command runs the specified command with an agent-only Allure profile.",
111
+ examples: [
112
+ ["agent -- npm test", "Run npm test and capture only the agent-mode output"],
113
+ [
114
+ "agent --expectations ./expected.yaml -- npm test",
115
+ "Run npm test with agent-mode expectations loaded from ./expected.yaml",
116
+ ],
117
+ ],
118
+ });
119
+ export class AgentLatestCommand extends Command {
120
+ constructor() {
121
+ super(...arguments);
122
+ this.cwd = Option.String("--cwd", {
123
+ description: "The project directory used to resolve the latest agent output (default: current working directory)",
124
+ });
125
+ }
126
+ async execute() {
127
+ const cwd = await realpath(this.cwd ?? process.cwd());
128
+ let latestState;
129
+ try {
130
+ latestState = await readLatestAgentState(cwd);
131
+ }
132
+ catch (error) {
133
+ console.error(`Could not read the latest agent output for ${cwd}: ${error.message}`);
134
+ exit(1);
135
+ return;
136
+ }
137
+ if (!latestState) {
138
+ console.error(`No latest agent output found for ${cwd}`);
139
+ exit(1);
140
+ return;
141
+ }
142
+ console.log(latestState.outputDir);
143
+ }
144
+ }
145
+ AgentLatestCommand.paths = [["agent", "latest"]];
146
+ AgentLatestCommand.usage = Command.Usage({
147
+ description: "Print the latest Allure agent output directory for the current project",
148
+ details: "This command prints the latest agent output directory recorded for the resolved project cwd.",
149
+ examples: [
150
+ ["agent latest", "Print the latest agent output directory for the current project"],
151
+ ["agent latest --cwd ./packages/cli", "Print the latest agent output directory for a specific project cwd"],
152
+ ],
153
+ });
154
+ export class AgentStateDirCommand extends Command {
155
+ constructor() {
156
+ super(...arguments);
157
+ this.cwd = Option.String("--cwd", {
158
+ description: "The project directory used to resolve the agent state directory (default: current working directory)",
159
+ });
160
+ }
161
+ async execute() {
162
+ const cwd = await realpath(readOptionalString(this.cwd) ?? process.cwd());
163
+ console.log(resolveAgentStateDir(cwd));
164
+ }
165
+ }
166
+ AgentStateDirCommand.paths = [["agent", "state-dir"]];
167
+ AgentStateDirCommand.usage = Command.Usage({
168
+ description: "Print the Allure agent state directory for the current project",
169
+ details: "This command prints the resolved state directory used to persist latest-agent pointers for the current project cwd.",
170
+ examples: [
171
+ ["agent state-dir", "Print the resolved state directory for the current project"],
172
+ ["agent state-dir --cwd ./packages/cli", "Print the resolved state directory for a specific project cwd"],
173
+ ],
174
+ });
175
+ export class AgentSelectCommand extends Command {
176
+ constructor() {
177
+ super(...arguments);
178
+ this.cwd = Option.String("--cwd", {
179
+ description: "The project directory used to resolve --latest and relative paths (default: current working directory)",
180
+ });
181
+ this.from = Option.String("--from", {
182
+ description: "The prior agent output directory to select tests from",
183
+ });
184
+ this.latest = Option.Boolean("--latest", {
185
+ description: "Use the latest recorded agent output for the current project cwd",
186
+ });
187
+ this.preset = Option.String("--preset", {
188
+ description: "The selection preset: review, failed, unsuccessful, or all (default: review)",
189
+ });
190
+ this.environments = Option.Array("--environment", {
191
+ description: "Filter selected tests by environment id. Repeat the option for multiple environments",
192
+ });
193
+ this.labels = Option.Array("--label", {
194
+ description: "Filter selected tests by exact label name=value. Repeat the option for multiple filters",
195
+ });
196
+ this.output = Option.String("--output,-o", {
197
+ description: "Write the resulting test plan to this file instead of printing it to stdout",
198
+ });
199
+ }
200
+ async execute() {
201
+ const cwd = await realpath(readOptionalString(this.cwd) ?? process.cwd());
202
+ const environments = readOptionalStringArray(this.environments);
203
+ const labels = readOptionalStringArray(this.labels);
204
+ const outputDir = await resolveAgentSelectionOutputDir({
205
+ cwd,
206
+ from: readOptionalString(this.from),
207
+ latest: readOptionalBoolean(this.latest),
208
+ });
209
+ const selection = await selectAgentTestPlan({
210
+ outputDir,
211
+ preset: normalizeAgentRerunPreset(readOptionalString(this.preset)),
212
+ environments: environments?.length ? environments : undefined,
213
+ labelFilters: parseAgentLabelFilters(labels),
214
+ });
215
+ if (!selection.testPlan.tests.length) {
216
+ console.error(`No tests matched selection in ${selection.outputDir}`);
217
+ exit(1);
218
+ return;
219
+ }
220
+ const serialized = `${JSON.stringify(selection.testPlan, null, 2)}\n`;
221
+ const output = readOptionalString(this.output);
222
+ if (!output) {
223
+ console.log(serialized.trimEnd());
224
+ return;
225
+ }
226
+ const outputPath = resolve(cwd, output);
227
+ await mkdir(dirname(outputPath), { recursive: true });
228
+ await writeFile(outputPath, serialized, "utf-8");
229
+ console.log(outputPath);
230
+ }
231
+ }
232
+ AgentSelectCommand.paths = [["agent", "select"]];
233
+ AgentSelectCommand.usage = Command.Usage({
234
+ description: "Select tests from an existing agent output and emit a test plan",
235
+ details: "This command resolves a set of tests from a prior agent run and prints or writes a testplan.json payload.",
236
+ examples: [
237
+ ["agent select --from ./out/agent-output", "Print a test plan for the default review-targeted tests"],
238
+ ["agent select --latest --preset failed", "Print a test plan for failed tests from the latest project run"],
239
+ ["agent select --from ./out/agent-output --output ./testplan.json", "Write the selected test plan to a file"],
240
+ ],
241
+ });
242
+ export const executeAgentMode = async (params) => {
243
+ const { configPath, cwd: configuredCwd, output, expectations, environment, environmentName, silent, rerunFrom, rerunLatest, rerunPreset, rerunEnvironments, rerunLabels, args, } = params;
244
+ if (!args || !args.length) {
245
+ throw new UsageError("expecting command to be specified after --, e.g. allure agent -- npm run test");
246
+ }
247
+ const command = args[0];
248
+ const commandArgs = args.slice(1);
249
+ const cwd = await realpath(configuredCwd ?? process.cwd());
250
+ const commandString = `${command} ${commandArgs.join(" ")}`;
251
+ const hasRerunSource = !!rerunFrom || !!rerunLatest;
252
+ const hasRerunFilters = !!rerunPreset || !!rerunEnvironments?.length || !!rerunLabels?.length;
253
+ if (!hasRerunSource && hasRerunFilters) {
254
+ throw new UsageError("Use rerun filters only together with --rerun-from <path> or --rerun-latest");
255
+ }
256
+ const rerunContext = await createAgentTestPlanContext({
257
+ cwd,
258
+ from: rerunFrom,
259
+ latest: rerunLatest,
260
+ preset: normalizeAgentRerunPreset(rerunPreset),
261
+ environments: rerunEnvironments?.length ? rerunEnvironments : undefined,
262
+ labelFilters: parseAgentLabelFilters(rerunLabels),
263
+ });
264
+ const childEnvironmentVariables = {
265
+ ...createChildAllureCliEnvironment("agent"),
266
+ ...(rerunContext ? { ALLURE_TESTPLAN_PATH: rerunContext.testPlanPath } : {}),
267
+ };
268
+ try {
269
+ if (getActiveAllureCliCommand()) {
270
+ console.log(commandString);
271
+ const exitCode = await executeNestedAllureCommand({
272
+ command,
273
+ commandArgs,
274
+ cwd,
275
+ ...(rerunContext ? { environmentVariables: { ALLURE_TESTPLAN_PATH: rerunContext.testPlanPath } } : {}),
276
+ silent,
277
+ });
278
+ exit(exitCode ?? -1);
279
+ return;
280
+ }
281
+ const outputDir = output ? resolve(cwd, output) : await mkdtemp(join(tmpdir(), "allure-agent-"));
282
+ const expectationsPath = expectations ? resolve(cwd, expectations) : undefined;
283
+ const environmentOptions = {
284
+ environment,
285
+ environmentName,
286
+ };
287
+ normalizeCommandEnvironmentOptions(environmentOptions);
288
+ if (expectationsPath && isPathInside(outputDir, expectationsPath)) {
289
+ throw new UsageError(`--expectations path ${JSON.stringify(expectationsPath)} must not be inside the agent output directory ${JSON.stringify(outputDir)}`);
290
+ }
291
+ const config = await readConfig(cwd, configPath, {
292
+ output: outputDir,
293
+ plugins: {
294
+ agent: {
295
+ options: {
296
+ outputDir,
297
+ },
298
+ },
299
+ },
300
+ });
301
+ const resolvedEnvironment = resolveCommandEnvironment(config, environmentOptions);
302
+ try {
303
+ await rm(outputDir, { recursive: true });
304
+ }
305
+ catch (error) {
306
+ if (!isFileNotFoundError(error)) {
307
+ console.error("could not clean output directory", error);
308
+ }
309
+ }
310
+ const startedAt = new Date().toISOString();
311
+ await persistLatestAgentState({
312
+ cwd,
313
+ outputDir,
314
+ expectationsPath,
315
+ command: commandString,
316
+ startedAt,
317
+ status: "running",
318
+ });
319
+ console.log(`agent output: ${outputDir}`);
320
+ if (expectationsPath) {
321
+ console.log(`agent expectations: ${expectationsPath}`);
322
+ }
323
+ console.log(commandString);
324
+ const allureReport = new AllureReport({
325
+ ...config,
326
+ output: outputDir,
327
+ environment: resolvedEnvironment?.id,
328
+ open: false,
329
+ port: undefined,
330
+ qualityGate: undefined,
331
+ allureService: undefined,
332
+ realTime: false,
333
+ plugins: config.plugins,
334
+ });
335
+ const knownIssues = await allureReport.store.allKnownIssues();
336
+ const { globalExitCode } = await withProcessEnv({
337
+ ALLURE_AGENT_OUTPUT: outputDir,
338
+ ALLURE_AGENT_EXPECTATIONS: expectationsPath,
339
+ ALLURE_AGENT_COMMAND: commandString,
340
+ ALLURE_AGENT_PROJECT_ROOT: cwd,
341
+ ALLURE_AGENT_NAME: undefined,
342
+ ALLURE_AGENT_LOOP_ID: undefined,
343
+ ALLURE_AGENT_TASK_ID: undefined,
344
+ ALLURE_AGENT_CONVERSATION_ID: undefined,
345
+ }, async () => await executeAllureRun({
346
+ allureReport,
347
+ knownIssues,
348
+ cwd,
349
+ command,
350
+ commandArgs,
351
+ environmentVariables: childEnvironmentVariables,
352
+ environment: resolvedEnvironment?.id,
353
+ withQualityGate: false,
354
+ logs: "pipe",
355
+ silent,
356
+ ignoreLogs: false,
357
+ logProcessExit: false,
358
+ }));
359
+ await persistLatestAgentState({
360
+ cwd,
361
+ outputDir,
362
+ expectationsPath,
363
+ command: commandString,
364
+ startedAt,
365
+ finishedAt: new Date().toISOString(),
366
+ status: "finished",
367
+ exitCode: globalExitCode.actual ?? globalExitCode.original,
368
+ });
369
+ exit(globalExitCode.actual ?? globalExitCode.original);
370
+ }
371
+ finally {
372
+ await rerunContext?.cleanup();
373
+ }
374
+ };
@@ -0,0 +1,48 @@
1
+ import { AllureReport } from "@allurereport/core";
2
+ import { type KnownTestFailure } from "@allurereport/core-api";
3
+ import type { ExitCode, QualityGateValidationResult } from "@allurereport/plugin-api";
4
+ export type TestProcessResult = {
5
+ code: number | null;
6
+ stdout: string;
7
+ stderr: string;
8
+ qualityGateResults: QualityGateValidationResult[];
9
+ };
10
+ export type RunLogsMode = "pipe" | "inherit" | "ignore";
11
+ export declare const executeNestedAllureCommand: (params: {
12
+ command: string;
13
+ commandArgs: string[];
14
+ cwd: string;
15
+ environmentVariables?: Record<string, string>;
16
+ silent?: boolean;
17
+ }) => Promise<number | null>;
18
+ export declare const runTests: (params: {
19
+ allureReport: AllureReport;
20
+ knownIssues: KnownTestFailure[];
21
+ cwd: string;
22
+ command: string;
23
+ commandArgs: string[];
24
+ environmentVariables: Record<string, string>;
25
+ environment?: string;
26
+ withQualityGate: boolean;
27
+ silent?: boolean;
28
+ logs?: RunLogsMode;
29
+ logProcessExit?: boolean;
30
+ }) => Promise<TestProcessResult | null>;
31
+ export declare const executeAllureRun: (params: {
32
+ allureReport: AllureReport;
33
+ knownIssues: KnownTestFailure[];
34
+ cwd: string;
35
+ command: string;
36
+ commandArgs: string[];
37
+ environmentVariables?: Record<string, string>;
38
+ environment?: string;
39
+ withQualityGate: boolean;
40
+ silent?: boolean;
41
+ logs?: RunLogsMode;
42
+ ignoreLogs?: boolean;
43
+ maxRerun?: number;
44
+ logProcessExit?: boolean;
45
+ }) => Promise<{
46
+ globalExitCode: ExitCode;
47
+ testProcessResult: TestProcessResult | null;
48
+ }>;