allure 3.10.0 → 3.12.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.
@@ -1,53 +1,126 @@
1
1
  import * as console from "node:console";
2
- import { mkdir, mkdtemp, realpath, rm, writeFile } from "node:fs/promises";
2
+ import { randomUUID } from "node:crypto";
3
+ import { mkdir, mkdtemp, realpath, writeFile } from "node:fs/promises";
3
4
  import { tmpdir } from "node:os";
4
- import { dirname, join, relative, resolve } from "node:path";
5
+ import { dirname, join, resolve } from "node:path";
5
6
  import process, { exit } from "node:process";
6
- import { AllureReport, isFileNotFoundError, readConfig } from "@allurereport/core";
7
+ import { AGENT_FINDING_CATEGORIES, AGENT_FINDING_SEVERITIES, AGENT_TASK_MAP_HELP, AGENT_TEST_STATUSES, AgentExpectationUsageError, buildAgentInlineExpectations, buildAgentQueryPayload, cleanupAgentRunState, cleanupStaleAgentRunStates, createAgentCapabilities, formatAgentOutputLinks, isAgentExpectationUsageError, isAgentTaskMapHelpRequest, isAgentUsageError, loadAgentOutput, normalizeAgentQueryLimit, normalizeAgentQueryView, normalizeAgentRerunPreset, normalizeRepeatedEnumValues, normalizeRepeatedStringValues, parseAgentLabelFilters, readLatestAgentState, resolveAgentSelectionOutputDir, resolveAgentStateDir, selectAgentTestPlan, validateAgentExpectationsFile, writeAgentRunState, writeInvalidAgentExpectationOutput, } from "@allurereport/plugin-agent";
7
8
  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;
9
+ export { AGENT_TASK_MAP_HELP, createAgentCapabilities, isAgentTaskMapHelpRequest };
10
+ const readOptionalString = (value) => (typeof value === "string" ? value : undefined);
11
+ const readOptionalBoolean = (value) => value === true;
12
+ const readOptionalStringArray = (value) => (Array.isArray(value) ? value : undefined);
13
+ const formatAgentCommand = (args) => args.join(" ");
14
+ const AGENT_HUMAN_REPORT_MODES = ["auto", "off", "awesome", "config"];
15
+ const formatAgentInspectCommand = (params) => [
16
+ "allure",
17
+ "agent",
18
+ "inspect",
19
+ ...(params.dumps ?? []).flatMap((dump) => ["--dump", dump]),
20
+ ...(params.resultsDir ?? []),
21
+ ].join(" ");
22
+ const buildInlineExpectationsFromOptions = (options) => buildAgentInlineExpectations({
23
+ goal: options.goal,
24
+ taskId: options.taskId,
25
+ expectTests: options.expectTests,
26
+ expectLabels: readOptionalStringArray(options.expectLabels),
27
+ expectEnvironments: readOptionalStringArray(options.expectEnvironments),
28
+ expectFullNames: readOptionalStringArray(options.expectFullNames),
29
+ expectPrefixes: readOptionalStringArray(options.expectPrefixes),
30
+ forbidLabels: readOptionalStringArray(options.forbidLabels),
31
+ expectStepContains: readOptionalStringArray(options.expectStepContains),
32
+ expectSteps: options.expectSteps,
33
+ expectAttachments: options.expectAttachments,
34
+ expectAttachmentFilters: readOptionalStringArray(options.expectAttachmentFilters),
35
+ });
36
+ const printAgentOutputLinks = (outputDir) => {
37
+ for (const line of formatAgentOutputLinks(outputDir)) {
38
+ console.log(line);
22
39
  }
40
+ };
41
+ const persistAgentRunState = async (value) => {
23
42
  try {
24
- return await fn();
43
+ await writeAgentRunState(value);
25
44
  }
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
- }
45
+ catch (error) {
46
+ console.error(`Could not update agent state in ${resolveAgentStateDir(value.cwd)}: ${error.message}`);
47
+ }
48
+ };
49
+ const logAgentCleanupFailures = (failures) => {
50
+ for (const failure of failures) {
51
+ console.error(`Could not clean stale agent output ${failure.state.outputDir}: ${failure.error.message}`);
34
52
  }
35
53
  };
36
- const isPathInside = (parentPath, candidatePath) => {
37
- const rel = relative(parentPath, candidatePath);
38
- return rel === "" || (!rel.startsWith("..") && rel !== "." && !rel.startsWith("../"));
54
+ const logAgentOrphanCleanupFailures = (failures) => {
55
+ for (const failure of failures) {
56
+ console.error(`Could not clean stale agent output ${failure.outputDir}: ${failure.error.message}`);
57
+ }
39
58
  };
40
- const persistLatestAgentState = async (value) => {
59
+ const cleanupManagedAgentOutputs = async (params) => {
41
60
  try {
42
- await writeLatestAgentState(value);
61
+ const result = await cleanupAgentRunState({
62
+ cwd: params.cwd,
63
+ currentRunId: params.runId,
64
+ keepManagedRuns: params.managedOutput ? 1 : 0,
65
+ });
66
+ logAgentCleanupFailures(result.failed);
43
67
  }
44
68
  catch (error) {
45
- console.error(`Could not update latest agent output in ${resolveAgentStateDir(value.cwd)}: ${error.message}`);
69
+ console.error(`Could not clean agent state in ${resolveAgentStateDir(params.cwd)}: ${error.message}`);
70
+ }
71
+ try {
72
+ const staleResult = await cleanupStaleAgentRunStates({
73
+ cwd: params.cwd,
74
+ currentRunId: params.runId,
75
+ });
76
+ logAgentCleanupFailures(staleResult.failed);
77
+ logAgentOrphanCleanupFailures(staleResult.orphaned.failed);
78
+ }
79
+ catch (error) {
80
+ console.error(`Could not clean agent state in ${resolveAgentStateDir(params.cwd)}: ${error.message}`);
46
81
  }
47
82
  };
48
- const readOptionalString = (value) => (typeof value === "string" ? value : undefined);
49
- const readOptionalBoolean = (value) => value === true;
50
- const readOptionalStringArray = (value) => (Array.isArray(value) ? value : undefined);
83
+ const agentEnvironmentOption = () => Option.String("--environment,--env", {
84
+ description: "Force specific environment ID to all tests in the run. Given environment has higher priority than the one defined in the config file (default: empty string)",
85
+ });
86
+ const agentEnvironmentNameOption = () => Option.String("--environment-name", {
87
+ description: "Force specific environment display name to all tests in the run. Has lower priority than --environment and higher than the config value (default: empty string)",
88
+ });
89
+ const throwCliUsageError = (error) => {
90
+ if (isAgentUsageError(error)) {
91
+ throw new UsageError(error.message);
92
+ }
93
+ throw error;
94
+ };
95
+ const normalizeAgentHumanReportMode = (value) => {
96
+ if (value === undefined) {
97
+ return "auto";
98
+ }
99
+ if (AGENT_HUMAN_REPORT_MODES.includes(value)) {
100
+ return value;
101
+ }
102
+ throw new UsageError(`Unsupported --report mode ${JSON.stringify(value)}. Use auto, off, awesome, or config.`);
103
+ };
104
+ export class AgentCapabilitiesCommand extends Command {
105
+ constructor() {
106
+ super(...arguments);
107
+ this.json = Option.Boolean("--json", true, {
108
+ description: "Print capabilities as JSON (default: true)",
109
+ });
110
+ }
111
+ async execute() {
112
+ console.log(JSON.stringify(createAgentCapabilities(), null, 2));
113
+ }
114
+ }
115
+ AgentCapabilitiesCommand.paths = [["agent", "capabilities"]];
116
+ AgentCapabilitiesCommand.usage = Command.Usage({
117
+ description: "Print structured Allure agent capability information",
118
+ details: "This command prints the locally supported agent-mode commands, expectation controls, output files, rerun support, and known unsupported capability families as JSON.",
119
+ examples: [
120
+ ["agent capabilities", "Print agent capabilities as JSON"],
121
+ ["agent capabilities --json", "Print agent capabilities as JSON explicitly"],
122
+ ],
123
+ });
51
124
  export class AgentCommand extends Command {
52
125
  constructor() {
53
126
  super(...arguments);
@@ -58,13 +131,52 @@ export class AgentCommand extends Command {
58
131
  description: "The working directory for the command to run (default: current working directory)",
59
132
  });
60
133
  this.output = Option.String("--output,-o", {
61
- description: "The output directory for agent artifacts. Absolute paths are accepted as well",
134
+ description: "The output directory for agent artifacts. Absolute paths are accepted. Explicit output is caller-managed; remove or archive it when no longer needed so later state compaction can drop its record",
135
+ });
136
+ this.report = Option.String("--report", {
137
+ description: "Human report mode: auto, off, awesome, or config. Auto writes awesome/index.html for 1000 or fewer stored visible results and records manifest/human-report.json (default: auto)",
62
138
  });
63
139
  this.expectations = Option.String("--expectations", {
64
140
  description: "The path to a YAML or JSON expectations file",
65
141
  });
66
- this.environment = environmentOption();
67
- this.environmentName = environmentNameOption();
142
+ this.goal = Option.Array("--goal", {
143
+ description: "The review goal to record in inline agent expectations",
144
+ });
145
+ this.taskId = Option.Array("--task-id", {
146
+ description: "The task or feature id to record in inline agent expectations",
147
+ });
148
+ this.expectTests = Option.Array("--expect-tests", {
149
+ description: "The expected number of visible logical tests in the intended scope",
150
+ });
151
+ this.expectLabels = Option.Array("--expect-label", {
152
+ description: "Expected label selector in name=value form. Repeat the option for multiple selectors",
153
+ });
154
+ this.expectEnvironments = Option.Array("--expect-env", {
155
+ description: "Expected environment id. Repeat the option for multiple environments",
156
+ });
157
+ this.expectFullNames = Option.Array("--expect-test", {
158
+ description: "Expected full test name. Repeat the option for multiple tests",
159
+ });
160
+ this.expectPrefixes = Option.Array("--expect-prefix", {
161
+ description: "Expected full-name prefix. Repeat the option for multiple prefixes",
162
+ });
163
+ this.forbidLabels = Option.Array("--forbid-label", {
164
+ description: "Forbidden label selector in name=value form. Repeat the option for multiple selectors",
165
+ });
166
+ this.expectStepContains = Option.Array("--expect-step-containing", {
167
+ description: "Require a test-scoped step name containing this text per evidence-target logical test",
168
+ });
169
+ this.expectSteps = Option.Array("--expect-steps", {
170
+ description: "Require at least this many meaningful steps per expected logical test",
171
+ });
172
+ this.expectAttachments = Option.Array("--expect-attachments", {
173
+ description: "Require at least this many non-missing attachments per expected logical test",
174
+ });
175
+ this.expectAttachmentFilters = Option.Array("--expect-attachment", {
176
+ description: "Require a matching non-missing attachment per expected logical test. Use a file name or name=value/content-type=value",
177
+ });
178
+ this.environment = agentEnvironmentOption();
179
+ this.environmentName = agentEnvironmentNameOption();
68
180
  this.silent = Option.Boolean("--silent", {
69
181
  description: "Don't pipe the process output logs to console (default: false)",
70
182
  });
@@ -87,35 +199,295 @@ export class AgentCommand extends Command {
87
199
  }
88
200
  async execute() {
89
201
  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
- });
202
+ const configPath = readOptionalString(this.config);
203
+ const configuredCwd = readOptionalString(this.cwd);
204
+ const output = readOptionalString(this.output);
205
+ const reportMode = normalizeAgentHumanReportMode(readOptionalString(this.report));
206
+ const expectations = readOptionalString(this.expectations);
207
+ if (!args.length) {
208
+ throw new UsageError("expecting command to be specified after --, e.g. allure agent -- npm run test");
209
+ }
210
+ try {
211
+ const inlineExpectations = buildInlineExpectationsFromOptions({
212
+ goal: this.goal,
213
+ taskId: this.taskId,
214
+ expectTests: this.expectTests,
215
+ expectLabels: this.expectLabels,
216
+ expectEnvironments: this.expectEnvironments,
217
+ expectFullNames: this.expectFullNames,
218
+ expectPrefixes: this.expectPrefixes,
219
+ forbidLabels: this.forbidLabels,
220
+ expectStepContains: this.expectStepContains,
221
+ expectSteps: this.expectSteps,
222
+ expectAttachments: this.expectAttachments,
223
+ expectAttachmentFilters: this.expectAttachmentFilters,
224
+ });
225
+ if (expectations && inlineExpectations) {
226
+ throw new AgentExpectationUsageError("Use either --expectations <file> or inline expectation flags, not both", "--expectations");
227
+ }
228
+ await validateAgentExpectationsFile({
229
+ cwd: await realpath(configuredCwd ?? process.cwd()),
230
+ output,
231
+ expectations,
232
+ });
233
+ const { executeAgentMode } = await import("./agent-run.js");
234
+ await executeAgentMode({
235
+ configPath,
236
+ cwd: configuredCwd,
237
+ output,
238
+ expectations,
239
+ inlineExpectations: inlineExpectations,
240
+ environment: readOptionalString(this.environment),
241
+ environmentName: readOptionalString(this.environmentName),
242
+ silent: readOptionalBoolean(this.silent),
243
+ rerunFrom: readOptionalString(this.rerunFrom),
244
+ rerunLatest: readOptionalBoolean(this.rerunLatest),
245
+ rerunPreset: readOptionalString(this.rerunPreset),
246
+ rerunEnvironments: readOptionalStringArray(this.rerunEnvironments),
247
+ rerunLabels: readOptionalStringArray(this.rerunLabels),
248
+ reportMode,
249
+ args,
250
+ });
251
+ }
252
+ catch (error) {
253
+ if (!isAgentExpectationUsageError(error)) {
254
+ throwCliUsageError(error);
255
+ }
256
+ const expectationError = error;
257
+ const cwd = await realpath(configuredCwd ?? process.cwd());
258
+ const runId = randomUUID();
259
+ const managedOutput = !output;
260
+ const outputDir = output ? resolve(cwd, output) : await mkdtemp(join(tmpdir(), "allure-agent-"));
261
+ const commandString = formatAgentCommand(args);
262
+ const { generatedAt } = await writeInvalidAgentExpectationOutput({
263
+ outputDir,
264
+ command: commandString,
265
+ error: expectationError,
266
+ });
267
+ const generatedAtMs = Date.parse(generatedAt);
268
+ const generatedAtTimestamp = Number.isFinite(generatedAtMs) ? generatedAtMs : Date.now();
269
+ await persistAgentRunState({
270
+ runId,
271
+ cwd,
272
+ outputDir,
273
+ managedOutput,
274
+ command: commandString,
275
+ startedAt: generatedAtTimestamp,
276
+ finishedAt: generatedAtTimestamp,
277
+ status: "finished",
278
+ exitCode: 1,
279
+ pid: process.pid,
280
+ });
281
+ await cleanupManagedAgentOutputs({ cwd, runId, managedOutput });
282
+ printAgentOutputLinks(outputDir);
283
+ console.error(expectationError.message);
284
+ exit(1);
285
+ }
105
286
  }
106
287
  }
107
288
  AgentCommand.paths = [["agent"]];
108
289
  AgentCommand.usage = Command.Usage({
109
290
  description: "Run specified command in Allure agent mode",
110
- details: "This command runs the specified command with an agent-only Allure profile.",
291
+ details: "This command runs the specified command with an agent-mode Allure profile and optional human-readable report output. With the default --report auto mode, runs with 1000 or fewer stored visible logical results also write a single-file Awesome report at <output>/awesome/index.html and record its status in <output>/manifest/human-report.json. If a user asks for the report after a run, use `allure agent latest` to recover the output directory and check the human-report manifest before regenerating anything.",
111
292
  examples: [
112
- ["agent -- npm test", "Run npm test and capture only the agent-mode output"],
293
+ ["agent -- npm test", "Run npm test and capture agent-mode output with the default human report policy"],
294
+ ["agent --report off -- npm test", "Run npm test and capture only the agent-mode artifacts"],
113
295
  [
114
296
  "agent --expectations ./expected.yaml -- npm test",
115
297
  "Run npm test with agent-mode expectations loaded from ./expected.yaml",
116
298
  ],
117
299
  ],
118
300
  });
301
+ export class AgentInspectCommand extends Command {
302
+ constructor() {
303
+ super(...arguments);
304
+ this.resultsDir = Option.Rest({
305
+ name: "Patterns to match test results directories in the current working directory (default: ./**/allure-results)",
306
+ });
307
+ this.config = Option.String("--config,-c", {
308
+ description: "The path Allure config file",
309
+ });
310
+ this.cwd = Option.String("--cwd", {
311
+ description: "The working directory for resolving results and dumps (default: current working directory)",
312
+ });
313
+ this.output = Option.String("--output,-o", {
314
+ description: "The output directory for agent artifacts. Absolute paths are accepted. Explicit output is caller-managed; remove or archive it when no longer needed so later state compaction can drop its record",
315
+ });
316
+ this.report = Option.String("--report", {
317
+ description: "Human report mode: auto, off, awesome, or config. Auto writes awesome/index.html for 1000 or fewer stored visible results and records manifest/human-report.json (default: auto)",
318
+ });
319
+ this.reportName = Option.String("--report-name,--name", {
320
+ description: "The report name to pass through configuration defaults (default: Allure Report)",
321
+ });
322
+ this.dump = Option.Array("--dump", {
323
+ description: "Path or pattern that matches one or more archives created by `allure run --dump ...`. " +
324
+ "This option can be specified multiple times.",
325
+ });
326
+ this.open = Option.Boolean("--open", {
327
+ description: "Accepted for parity with generate. Agent inspect writes agent artifacts and prints their paths",
328
+ });
329
+ this.port = Option.String("--port", {
330
+ description: "Accepted for parity with generate. Agent inspect doesn't serve the output",
331
+ });
332
+ this.historyLimit = Option.String("--history-limit", {
333
+ description: "Limits the number of history entries to keep (default: unlimited)",
334
+ });
335
+ this.hideLabels = Option.Array("--hide-labels", {
336
+ description: "Hide labels by exact name in generated data. Repeat the option for multiple labels",
337
+ });
338
+ this.expectations = Option.String("--expectations", {
339
+ description: "The path to a YAML or JSON expectations file",
340
+ });
341
+ this.goal = Option.Array("--goal", {
342
+ description: "The review goal to record in inline agent expectations",
343
+ });
344
+ this.taskId = Option.Array("--task-id", {
345
+ description: "The task or feature id to record in inline agent expectations",
346
+ });
347
+ this.expectTests = Option.Array("--expect-tests", {
348
+ description: "The expected number of visible logical tests in the intended scope",
349
+ });
350
+ this.expectLabels = Option.Array("--expect-label", {
351
+ description: "Expected label selector in name=value form. Repeat the option for multiple selectors",
352
+ });
353
+ this.expectEnvironments = Option.Array("--expect-env", {
354
+ description: "Expected environment id. Repeat the option for multiple environments",
355
+ });
356
+ this.expectFullNames = Option.Array("--expect-test", {
357
+ description: "Expected full test name. Repeat the option for multiple tests",
358
+ });
359
+ this.expectPrefixes = Option.Array("--expect-prefix", {
360
+ description: "Expected full-name prefix. Repeat the option for multiple prefixes",
361
+ });
362
+ this.forbidLabels = Option.Array("--forbid-label", {
363
+ description: "Forbidden label selector in name=value form. Repeat the option for multiple selectors",
364
+ });
365
+ this.expectStepContains = Option.Array("--expect-step-containing", {
366
+ description: "Require a test-scoped step name containing this text per evidence-target logical test",
367
+ });
368
+ this.expectSteps = Option.Array("--expect-steps", {
369
+ description: "Require at least this many meaningful steps per expected logical test",
370
+ });
371
+ this.expectAttachments = Option.Array("--expect-attachments", {
372
+ description: "Require at least this many non-missing attachments per expected logical test",
373
+ });
374
+ this.expectAttachmentFilters = Option.Array("--expect-attachment", {
375
+ description: "Require a matching non-missing attachment per expected logical test. Use a file name or name=value/content-type=value",
376
+ });
377
+ this.environment = agentEnvironmentOption();
378
+ this.environmentName = agentEnvironmentNameOption();
379
+ }
380
+ async execute() {
381
+ const configPath = readOptionalString(this.config);
382
+ const configuredCwd = readOptionalString(this.cwd);
383
+ const output = readOptionalString(this.output);
384
+ const reportMode = normalizeAgentHumanReportMode(readOptionalString(this.report));
385
+ const dumps = readOptionalStringArray(this.dump) ?? [];
386
+ const resultsDir = this.resultsDir;
387
+ const expectations = readOptionalString(this.expectations);
388
+ if (resultsDir.includes("--")) {
389
+ throw new UsageError("agent inspect does not run commands; pass result directories directly, e.g. allure agent inspect ./allure-results");
390
+ }
391
+ try {
392
+ const inlineExpectations = buildInlineExpectationsFromOptions({
393
+ goal: this.goal,
394
+ taskId: this.taskId,
395
+ expectTests: this.expectTests,
396
+ expectLabels: this.expectLabels,
397
+ expectEnvironments: this.expectEnvironments,
398
+ expectFullNames: this.expectFullNames,
399
+ expectPrefixes: this.expectPrefixes,
400
+ forbidLabels: this.forbidLabels,
401
+ expectStepContains: this.expectStepContains,
402
+ expectSteps: this.expectSteps,
403
+ expectAttachments: this.expectAttachments,
404
+ expectAttachmentFilters: this.expectAttachmentFilters,
405
+ });
406
+ if (expectations && inlineExpectations) {
407
+ throw new AgentExpectationUsageError("Use either --expectations <file> or inline expectation flags, not both", "--expectations");
408
+ }
409
+ await validateAgentExpectationsFile({
410
+ cwd: await realpath(configuredCwd ?? process.cwd()),
411
+ output,
412
+ expectations,
413
+ });
414
+ const { executeAgentInspectMode } = await import("./agent-run.js");
415
+ await executeAgentInspectMode({
416
+ configPath,
417
+ cwd: configuredCwd,
418
+ output,
419
+ expectations,
420
+ inlineExpectations: inlineExpectations,
421
+ environment: readOptionalString(this.environment),
422
+ environmentName: readOptionalString(this.environmentName),
423
+ reportName: readOptionalString(this.reportName),
424
+ open: readOptionalBoolean(this.open),
425
+ port: readOptionalString(this.port),
426
+ historyLimit: readOptionalString(this.historyLimit),
427
+ hideLabels: readOptionalStringArray(this.hideLabels),
428
+ dumps,
429
+ reportMode,
430
+ resultsDir,
431
+ });
432
+ }
433
+ catch (error) {
434
+ if (!isAgentExpectationUsageError(error)) {
435
+ throwCliUsageError(error);
436
+ }
437
+ const expectationError = error;
438
+ const cwd = await realpath(configuredCwd ?? process.cwd());
439
+ const runId = randomUUID();
440
+ const managedOutput = !output;
441
+ const outputDir = output ? resolve(cwd, output) : await mkdtemp(join(tmpdir(), "allure-agent-"));
442
+ const commandString = formatAgentInspectCommand({ dumps, resultsDir });
443
+ const { generatedAt } = await writeInvalidAgentExpectationOutput({
444
+ outputDir,
445
+ command: commandString,
446
+ error: expectationError,
447
+ });
448
+ const generatedAtMs = Date.parse(generatedAt);
449
+ const generatedAtTimestamp = Number.isFinite(generatedAtMs) ? generatedAtMs : Date.now();
450
+ await persistAgentRunState({
451
+ runId,
452
+ cwd,
453
+ outputDir,
454
+ managedOutput,
455
+ command: commandString,
456
+ startedAt: generatedAtTimestamp,
457
+ finishedAt: generatedAtTimestamp,
458
+ status: "finished",
459
+ exitCode: 1,
460
+ pid: process.pid,
461
+ });
462
+ await cleanupManagedAgentOutputs({ cwd, runId, managedOutput });
463
+ printAgentOutputLinks(outputDir);
464
+ console.error(expectationError.message);
465
+ exit(1);
466
+ }
467
+ }
468
+ }
469
+ AgentInspectCommand.paths = [["agent", "inspect"]];
470
+ AgentInspectCommand.usage = Command.Usage({
471
+ description: "Inspect existing Allure results or dump archives in Allure agent mode",
472
+ details: "This command restores dump archives and reads existing Allure results directories, then writes agent-mode output and optional human-readable report output without running a test command. With the default --report auto mode, inputs with 1000 or fewer stored visible logical results also write a single-file Awesome report at <output>/awesome/index.html and record its status in <output>/manifest/human-report.json. If a user asks for the report after an inspect run, use `allure agent latest` to recover the output directory and check the human-report manifest before regenerating anything.",
473
+ examples: [
474
+ ["agent inspect ./allure-results", "Inspect an existing Allure results directory"],
475
+ ["agent inspect ./packages/*/out/allure-results", "Inspect matching Allure results directories"],
476
+ ["agent inspect --dump allure-results-linux.zip", "Inspect a dump archive downloaded from CI"],
477
+ [
478
+ "agent inspect --dump allure-results-linux.zip --dump allure-results-macos.zip",
479
+ "Inspect multiple CI dump archives together",
480
+ ],
481
+ [
482
+ "agent inspect --dump allure-results-linux.zip ./local/allure-results",
483
+ "Inspect dump archives together with local results directories",
484
+ ],
485
+ [
486
+ "agent inspect --config ./allurerc.mjs --output ./agent-output ./allure-results",
487
+ "Inspect existing results with a config file and write agent artifacts to ./agent-output",
488
+ ],
489
+ ],
490
+ });
119
491
  export class AgentLatestCommand extends Command {
120
492
  constructor() {
121
493
  super(...arguments);
@@ -139,16 +511,19 @@ export class AgentLatestCommand extends Command {
139
511
  exit(1);
140
512
  return;
141
513
  }
142
- console.log(latestState.outputDir);
514
+ printAgentOutputLinks(latestState.outputDir);
143
515
  }
144
516
  }
145
517
  AgentLatestCommand.paths = [["agent", "latest"]];
146
518
  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.",
519
+ description: "Print the latest Allure agent output directory and index path for the current project",
520
+ details: "This command prints the latest agent output directory and index.md path recorded for the resolved project cwd. When a user asks for a human-readable report from the last run, use this output directory to check manifest/human-report.json; if its status is generated, the report path is relative to that same output directory, usually awesome/index.html.",
149
521
  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"],
522
+ ["agent latest", "Print the latest agent output directory and index path for the current project"],
523
+ [
524
+ "agent latest --cwd ./packages/cli",
525
+ "Print the latest agent output directory and index path for a specific project cwd",
526
+ ],
152
527
  ],
153
528
  });
154
529
  export class AgentStateDirCommand extends Command {
@@ -165,11 +540,100 @@ export class AgentStateDirCommand extends Command {
165
540
  }
166
541
  AgentStateDirCommand.paths = [["agent", "state-dir"]];
167
542
  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.",
543
+ description: "Print the shared Allure agent state directory",
544
+ details: "This command prints the resolved state directory used to persist per-project agent run registries.",
170
545
  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"],
546
+ ["agent state-dir", "Print the resolved shared state directory"],
547
+ ["agent state-dir --cwd ./packages/cli", "Print the shared state directory using a specific project cwd"],
548
+ ],
549
+ });
550
+ export class AgentQueryCommand extends Command {
551
+ constructor() {
552
+ super(...arguments);
553
+ this.view = Option.String({
554
+ required: false,
555
+ name: "Query view: summary, tests, findings, or test (default: summary)",
556
+ });
557
+ this.cwd = Option.String("--cwd", {
558
+ description: "The project directory used to resolve --latest and relative paths (default: current working directory)",
559
+ });
560
+ this.from = Option.String("--from", {
561
+ description: "The prior agent output directory to query",
562
+ });
563
+ this.latest = Option.Boolean("--latest", {
564
+ description: "Use the latest recorded agent output for the current project cwd",
565
+ });
566
+ this.statuses = Option.Array("--status", {
567
+ description: "Filter tests by status: failed, broken, unknown, skipped, or passed. Repeat for multiple statuses",
568
+ });
569
+ this.environments = Option.Array("--environment", {
570
+ description: "Filter tests by environment id. Repeat the option for multiple environments",
571
+ });
572
+ this.labels = Option.Array("--label", {
573
+ description: "Filter tests by exact label name=value. Repeat the option for multiple filters",
574
+ });
575
+ this.severities = Option.Array("--severity", {
576
+ description: "Filter findings by severity: high, warning, or info. Repeat for multiple severities",
577
+ });
578
+ this.categories = Option.Array("--category", {
579
+ description: "Filter findings by category. Repeat the option for multiple categories",
580
+ });
581
+ this.checks = Option.Array("--check", {
582
+ description: "Filter findings by check name. Repeat the option for multiple checks",
583
+ });
584
+ this.test = Option.String("--test", {
585
+ description: "Filter to one test by full name, test result id, history id, or markdown path",
586
+ });
587
+ this.limit = Option.String("--limit", {
588
+ description: "Limit returned tests or findings to this non-negative count",
589
+ });
590
+ this.includeMarkdown = Option.Boolean("--include-markdown", {
591
+ description: "Include the per-test markdown content for the test view",
592
+ });
593
+ }
594
+ async execute() {
595
+ try {
596
+ const cwd = await realpath(readOptionalString(this.cwd) ?? process.cwd());
597
+ const view = normalizeAgentQueryView(readOptionalString(this.view));
598
+ const outputDir = await resolveAgentSelectionOutputDir({
599
+ cwd,
600
+ from: readOptionalString(this.from),
601
+ latest: readOptionalBoolean(this.latest),
602
+ });
603
+ const output = await loadAgentOutput(outputDir);
604
+ const payload = await buildAgentQueryPayload(output, view, {
605
+ environments: normalizeRepeatedStringValues(readOptionalStringArray(this.environments)),
606
+ labelFilters: parseAgentLabelFilters(readOptionalStringArray(this.labels)),
607
+ statuses: normalizeRepeatedEnumValues(readOptionalStringArray(this.statuses), AGENT_TEST_STATUSES, "--status"),
608
+ severities: normalizeRepeatedEnumValues(readOptionalStringArray(this.severities), AGENT_FINDING_SEVERITIES, "--severity"),
609
+ categories: normalizeRepeatedEnumValues(readOptionalStringArray(this.categories), AGENT_FINDING_CATEGORIES, "--category"),
610
+ checks: normalizeRepeatedStringValues(readOptionalStringArray(this.checks)),
611
+ test: readOptionalString(this.test),
612
+ limit: normalizeAgentQueryLimit(readOptionalString(this.limit)),
613
+ includeMarkdown: readOptionalBoolean(this.includeMarkdown),
614
+ });
615
+ console.log(JSON.stringify(payload, null, 2));
616
+ }
617
+ catch (error) {
618
+ throwCliUsageError(error);
619
+ }
620
+ }
621
+ }
622
+ AgentQueryCommand.paths = [["agent", "query"]];
623
+ AgentQueryCommand.usage = Command.Usage({
624
+ description: "Query an existing Allure agent output directory as focused JSON",
625
+ details: "This command reads a prior agent output directory and prints focused JSON for a run summary, test list, findings list, or one test. Use --latest to query the latest recorded output for the project, or --from to query a specific output directory.",
626
+ examples: [
627
+ ["agent query --latest summary", "Print a summary for the latest agent output"],
628
+ ["agent query --from ./out/agent-output tests --status failed", "List failed tests from a prior output"],
629
+ [
630
+ "agent query --from ./out/agent-output findings --severity high",
631
+ "List high-severity findings from a prior output",
632
+ ],
633
+ [
634
+ 'agent query --latest test --test "suite should pass" --include-markdown',
635
+ "Print one test summary with its per-test markdown",
636
+ ],
173
637
  ],
174
638
  });
175
639
  export class AgentSelectCommand extends Command {
@@ -198,177 +662,52 @@ export class AgentSelectCommand extends Command {
198
662
  });
199
663
  }
200
664
  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;
665
+ try {
666
+ const cwd = await realpath(readOptionalString(this.cwd) ?? process.cwd());
667
+ const environments = readOptionalStringArray(this.environments);
668
+ const labels = readOptionalStringArray(this.labels);
669
+ const outputDir = await resolveAgentSelectionOutputDir({
670
+ cwd,
671
+ from: readOptionalString(this.from),
672
+ latest: readOptionalBoolean(this.latest),
673
+ });
674
+ const selection = await selectAgentTestPlan({
675
+ outputDir,
676
+ preset: normalizeAgentRerunPreset(readOptionalString(this.preset)),
677
+ environments: environments?.length ? environments : undefined,
678
+ labelFilters: parseAgentLabelFilters(labels),
679
+ });
680
+ if (!selection.testPlan.tests.length) {
681
+ console.error(`No tests matched selection in ${selection.outputDir}`);
682
+ exit(1);
683
+ return;
684
+ }
685
+ const serialized = `${JSON.stringify(selection.testPlan, null, 2)}\n`;
686
+ const output = readOptionalString(this.output);
687
+ if (!output) {
688
+ console.log(serialized.trimEnd());
689
+ return;
690
+ }
691
+ const outputPath = resolve(cwd, output);
692
+ await mkdir(dirname(outputPath), { recursive: true });
693
+ await writeFile(outputPath, serialized, "utf-8");
694
+ console.log(`agent testplan: ${outputPath}`);
695
+ console.log(`agent selection source: ${selection.outputDir}`);
696
+ console.log(`agent selection preset: ${selection.preset}`);
697
+ console.log(`agent selection tests: ${selection.selectedTests.length}`);
219
698
  }
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;
699
+ catch (error) {
700
+ throwCliUsageError(error);
225
701
  }
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
702
  }
231
703
  }
232
704
  AgentSelectCommand.paths = [["agent", "select"]];
233
705
  AgentSelectCommand.usage = Command.Usage({
234
706
  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.",
707
+ details: "This command resolves a set of tests from a prior agent run and prints or writes a testplan.json payload. When --output is used, stdout contains the written test plan path, source output directory, preset, and selected test count.",
236
708
  examples: [
237
709
  ["agent select --from ./out/agent-output", "Print a test plan for the default review-targeted tests"],
238
710
  ["agent select --latest --preset failed", "Print a test plan for failed tests from the latest project run"],
239
711
  ["agent select --from ./out/agent-output --output ./testplan.json", "Write the selected test plan to a file"],
240
712
  ],
241
713
  });
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
- };