allure 3.11.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.
package/README.md CHANGED
@@ -74,7 +74,27 @@ For example:
74
74
  npx allure agent -- npm test
75
75
  ```
76
76
 
77
- `allure agent` runs with an agent-only profile by default. It creates a fresh output directory automatically, accepts compact inline expectations such as `--goal`, `--expect-tests`, `--expect-test`, `--expect-label`, and `--expect-step-containing`, and can still load an expectations file with `--expectations` when needed. Configured presentation or export plugins such as Awesome or TestOps are ignored for that run.
77
+ To analyze existing Allure results or dump archives downloaded from CI without
78
+ rerunning tests, use `agent inspect`:
79
+
80
+ ```bash
81
+ npx allure agent inspect path/to/allure-results
82
+ npx allure agent inspect --dump allure-results-linux.zip --dump allure-results-macos.zip
83
+ npx allure agent inspect --config ./allurerc.mjs --output ./agent-output path/to/allure-results
84
+ ```
85
+
86
+ `agent inspect` accepts the same result inputs and configuration-style options as
87
+ `allure generate`, including result directory globs, `--dump`, `--config`,
88
+ `--cwd`, `--report-name`, `--history-limit`, and `--hide-labels`. Its `--output`
89
+ option writes the agentic output directory.
90
+
91
+ `allure agent` and `allure agent inspect` use `--report auto` by default. This writes the agent-readable artifacts and, when the stored visible result count is 1000 or fewer, also writes a single-file Awesome report at `awesome/index.html` inside the agent output directory. Runs above that threshold skip the human report to avoid excessive output, and the status is recorded in `index.md`, `manifest/run.json`, and `manifest/human-report.json`.
92
+
93
+ Use `--report off` for agent-only artifacts, `--report awesome` to force the single-file Awesome report regardless of result count, or `--report config` to force the configured non-agent report plugins inside the agent output directory. Configured presentation or export plugins such as Dashboard or TestOps are otherwise ignored for agent runs.
94
+
95
+ If you need the human-readable report from the most recent agent run, first run `npx allure agent latest` when the output directory is unknown. Then check `<output>/manifest/human-report.json`; when its status is `generated`, open `<output>/<path>` from that manifest, usually `<output>/awesome/index.html`.
96
+
97
+ `allure agent` creates a fresh output directory automatically, accepts compact inline expectations such as `--goal`, `--expect-tests`, `--expect-test`, `--expect-label`, and `--expect-step-containing`, and can still load an expectations file with `--expectations` when needed.
78
98
 
79
99
  Agents and setup tools can inspect the local structured capability contract without scraping help text:
80
100
 
@@ -0,0 +1,23 @@
1
+ import { type FullConfig, type PluginInstance } from "@allurereport/core";
2
+ import type { AgentHumanReportMode, AgentHumanReportStatus, AgentHumanReportStatusProvider } from "@allurereport/plugin-agent";
3
+ export declare const AGENT_HUMAN_REPORT_THRESHOLD = 1000;
4
+ type AgentHumanReportConfigOverride = {
5
+ name?: FullConfig["name"];
6
+ open?: FullConfig["open"];
7
+ port?: FullConfig["port"];
8
+ hideLabels?: FullConfig["hideLabels"];
9
+ historyLimit?: FullConfig["historyLimit"];
10
+ };
11
+ type AgentHumanReportBuildParams = {
12
+ mode: AgentHumanReportMode;
13
+ cwd: string;
14
+ configPath?: string;
15
+ outputDir: string;
16
+ configOverride?: AgentHumanReportConfigOverride;
17
+ };
18
+ export declare const createAgentHumanReportConfig: ({ mode, cwd, configPath, outputDir, configOverride, }: AgentHumanReportBuildParams) => Promise<{
19
+ status: AgentHumanReportStatus;
20
+ statusProvider: AgentHumanReportStatusProvider;
21
+ plugins: PluginInstance[];
22
+ }>;
23
+ export {};
@@ -0,0 +1,171 @@
1
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
2
+ if (kind === "m") throw new TypeError("Private method is not writable");
3
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
4
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
5
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
6
+ };
7
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
8
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
9
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
10
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
+ };
12
+ var _AgentHumanReportPlugin_realtime, _AgentHumanReportPlugin_generated;
13
+ import * as console from "node:console";
14
+ import { readConfig, readRawConfig } from "@allurereport/core";
15
+ import AwesomePlugin from "@allurereport/plugin-awesome";
16
+ export const AGENT_HUMAN_REPORT_THRESHOLD = 1000;
17
+ const createInitialHumanReportStatus = (mode) => ({
18
+ schema_version: "allure-agent-human-report/v1",
19
+ mode,
20
+ status: mode === "off" ? "disabled" : "pending",
21
+ result_count: null,
22
+ threshold: AGENT_HUMAN_REPORT_THRESHOLD,
23
+ path: null,
24
+ reports: [],
25
+ reason: mode === "off" ? "disabled by --report off" : null,
26
+ error: null,
27
+ });
28
+ const isAgentPluginId = (id) => id === "agent" || id === "plugin-agent";
29
+ const isAwesomeDescriptor = (id, descriptor) => id === "awesome" ||
30
+ id === "@allurereport/plugin-awesome" ||
31
+ descriptor.import === "awesome" ||
32
+ descriptor.import === "@allurereport/plugin-awesome";
33
+ const configuredAwesomeOptions = async (cwd, configPath) => {
34
+ const rawConfig = await readRawConfig(cwd, configPath);
35
+ const plugins = rawConfig.plugins ?? {};
36
+ for (const id of Object.keys(plugins)) {
37
+ const descriptor = plugins[id];
38
+ if (isAwesomeDescriptor(id, descriptor)) {
39
+ return (descriptor.options ?? {});
40
+ }
41
+ }
42
+ return {};
43
+ };
44
+ const recordGeneratedReport = (status, pluginId, path) => {
45
+ const alreadyRecorded = status.reports.some((report) => report.plugin_id === pluginId && report.path === path);
46
+ if (!alreadyRecorded) {
47
+ status.reports.push({
48
+ plugin_id: pluginId,
49
+ path,
50
+ });
51
+ }
52
+ status.status = status.error ? "failed" : "generated";
53
+ status.path ?? (status.path = path);
54
+ status.reason = null;
55
+ status.generated_at = new Date().toISOString();
56
+ };
57
+ const recordFailedReport = (status, pluginId, error) => {
58
+ const message = error instanceof Error ? error.message : String(error);
59
+ status.status = "failed";
60
+ status.error ?? (status.error = message);
61
+ status.errors = [...(status.errors ?? []), { plugin_id: pluginId, message }];
62
+ };
63
+ class AgentHumanReportPlugin {
64
+ constructor(params) {
65
+ this.params = params;
66
+ _AgentHumanReportPlugin_realtime.set(this, void 0);
67
+ _AgentHumanReportPlugin_generated.set(this, false);
68
+ this.start = async (_context, _store, realtime) => {
69
+ __classPrivateFieldSet(this, _AgentHumanReportPlugin_realtime, realtime, "f");
70
+ };
71
+ this.done = async (context, store) => {
72
+ const { mode, status, threshold, pluginId, plugin, reportPath } = this.params;
73
+ const resultCount = (await store.allTestResults()).length;
74
+ status.result_count = resultCount;
75
+ if (status.status === "skipped") {
76
+ return;
77
+ }
78
+ if (mode === "auto" && resultCount > threshold) {
79
+ status.status = "skipped";
80
+ status.reason = `result count ${resultCount} exceeds threshold ${threshold}`;
81
+ return;
82
+ }
83
+ try {
84
+ await plugin.start?.(context, store, __classPrivateFieldGet(this, _AgentHumanReportPlugin_realtime, "f"));
85
+ await plugin.done?.(context, store);
86
+ __classPrivateFieldSet(this, _AgentHumanReportPlugin_generated, true, "f");
87
+ recordGeneratedReport(status, pluginId, reportPath);
88
+ }
89
+ catch (error) {
90
+ recordFailedReport(status, pluginId, error);
91
+ console.error(`agent human report ${pluginId} error`, error);
92
+ }
93
+ };
94
+ this.info = async (context, store) => {
95
+ if (!__classPrivateFieldGet(this, _AgentHumanReportPlugin_generated, "f")) {
96
+ return undefined;
97
+ }
98
+ return this.params.plugin.info?.(context, store);
99
+ };
100
+ }
101
+ }
102
+ _AgentHumanReportPlugin_realtime = new WeakMap(), _AgentHumanReportPlugin_generated = new WeakMap();
103
+ const wrapHumanReportPlugin = (params) => {
104
+ const { mode, status, plugin, reportPath = `${plugin.id}/` } = params;
105
+ return {
106
+ ...plugin,
107
+ plugin: new AgentHumanReportPlugin({
108
+ mode,
109
+ status,
110
+ threshold: AGENT_HUMAN_REPORT_THRESHOLD,
111
+ pluginId: plugin.id,
112
+ plugin: plugin.plugin,
113
+ reportPath,
114
+ }),
115
+ };
116
+ };
117
+ export const createAgentHumanReportConfig = async ({ mode, cwd, configPath, outputDir, configOverride, }) => {
118
+ const status = createInitialHumanReportStatus(mode);
119
+ const statusProvider = () => status;
120
+ if (mode === "off") {
121
+ return {
122
+ status,
123
+ statusProvider,
124
+ plugins: [],
125
+ };
126
+ }
127
+ if (mode === "auto" || mode === "awesome") {
128
+ const awesomeOptions = {
129
+ ...(await configuredAwesomeOptions(cwd, configPath)),
130
+ singleFile: true,
131
+ };
132
+ const awesomePlugin = {
133
+ id: "awesome",
134
+ enabled: true,
135
+ options: awesomeOptions,
136
+ plugin: new AwesomePlugin(awesomeOptions),
137
+ };
138
+ return {
139
+ status,
140
+ statusProvider,
141
+ plugins: [
142
+ wrapHumanReportPlugin({
143
+ mode,
144
+ status,
145
+ plugin: awesomePlugin,
146
+ reportPath: "awesome/index.html",
147
+ }),
148
+ ],
149
+ };
150
+ }
151
+ const reportConfig = await readConfig(cwd, configPath, {
152
+ ...configOverride,
153
+ output: outputDir,
154
+ });
155
+ const plugins = reportConfig.plugins
156
+ ?.filter((plugin) => !isAgentPluginId(plugin.id))
157
+ .map((plugin) => wrapHumanReportPlugin({
158
+ mode,
159
+ status,
160
+ plugin,
161
+ })) ?? [];
162
+ if (!plugins.length) {
163
+ status.status = "disabled";
164
+ status.reason = "no configured non-agent report plugins";
165
+ }
166
+ return {
167
+ status,
168
+ statusProvider,
169
+ plugins,
170
+ };
171
+ };
@@ -1,7 +1,16 @@
1
- import { writeLatestAgentState, type AgentExpectationsInput } from "@allurereport/plugin-agent";
1
+ import { writeAgentRunState, type AgentExpectationsInput, type AgentHumanReportMode } from "@allurereport/plugin-agent";
2
2
  export declare const formatAgentCommand: (args: string[]) => string;
3
+ export declare const formatAgentInspectCommand: (params: {
4
+ dumps?: string[];
5
+ resultsDir?: string[];
6
+ }) => string;
3
7
  export declare const printAgentOutputLinks: (outputDir: string) => void;
4
- export declare const persistLatestAgentState: (value: Parameters<typeof writeLatestAgentState>[0]) => Promise<void>;
8
+ export declare const persistAgentRunState: (value: Parameters<typeof writeAgentRunState>[0]) => Promise<void>;
9
+ export declare const cleanupManagedAgentOutputs: (params: {
10
+ cwd: string;
11
+ runId: string;
12
+ managedOutput: boolean;
13
+ }) => Promise<void>;
5
14
  export type ExecuteAgentModeParams = {
6
15
  configPath?: string;
7
16
  cwd?: string;
@@ -16,6 +25,25 @@ export type ExecuteAgentModeParams = {
16
25
  rerunPreset?: string;
17
26
  rerunEnvironments?: string[];
18
27
  rerunLabels?: string[];
28
+ reportMode: AgentHumanReportMode;
19
29
  args: string[];
20
30
  };
21
31
  export declare const executeAgentMode: (params: ExecuteAgentModeParams) => Promise<void>;
32
+ export type ExecuteAgentInspectModeParams = {
33
+ configPath?: string;
34
+ cwd?: string;
35
+ output?: string;
36
+ expectations?: string;
37
+ inlineExpectations?: AgentExpectationsInput;
38
+ environment?: string;
39
+ environmentName?: string;
40
+ reportName?: string;
41
+ open?: boolean;
42
+ port?: string;
43
+ historyLimit?: string;
44
+ hideLabels?: string[];
45
+ dumps?: string[];
46
+ reportMode: AgentHumanReportMode;
47
+ resultsDir: string[];
48
+ };
49
+ export declare const executeAgentInspectMode: (params: ExecuteAgentInspectModeParams) => Promise<never>;
@@ -1,29 +1,73 @@
1
1
  import * as console from "node:console";
2
+ import { randomUUID } from "node:crypto";
2
3
  import { mkdtemp, realpath, rm } from "node:fs/promises";
3
4
  import { tmpdir } from "node:os";
4
5
  import { join, resolve } from "node:path";
5
6
  import process, { exit } from "node:process";
6
7
  import { AllureReport, isFileNotFoundError, readConfig } from "@allurereport/core";
7
- import { createAgentTestPlanContext, AgentUsageError, formatAgentOutputLinks, isPathInside, normalizeAgentRerunPreset, parseAgentLabelFilters, resolveAgentStateDir, writeLatestAgentState, } from "@allurereport/plugin-agent";
8
+ import { createAgentTestPlanContext, AgentUsageError, formatAgentOutputLinks, isPathInside, normalizeAgentRerunPreset, parseAgentLabelFilters, cleanupAgentRunState, cleanupStaleAgentRunStates, resolveAgentStateDir, writeAgentRunState, } from "@allurereport/plugin-agent";
8
9
  import { normalizeCommandEnvironmentOptions, resolveCommandEnvironment } from "../utils/environment.js";
9
10
  import { createChildAllureCliEnvironment, getActiveAllureCliCommand } from "../utils/execution-context.js";
11
+ import { findAllureResultDirectories, findFilesByGlobs } from "../utils/fileSystem.js";
12
+ import { createAgentHumanReportConfig } from "./agent-human-report.js";
10
13
  import { executeAllureRun, executeNestedAllureCommand } from "./commons/run.js";
11
14
  export const formatAgentCommand = (args) => args.join(" ");
15
+ export const formatAgentInspectCommand = (params) => [
16
+ "allure",
17
+ "agent",
18
+ "inspect",
19
+ ...(params.dumps ?? []).flatMap((dump) => ["--dump", dump]),
20
+ ...(params.resultsDir ?? []),
21
+ ].join(" ");
12
22
  export const printAgentOutputLinks = (outputDir) => {
13
23
  for (const line of formatAgentOutputLinks(outputDir)) {
14
24
  console.log(line);
15
25
  }
16
26
  };
17
- export const persistLatestAgentState = async (value) => {
27
+ export const persistAgentRunState = async (value) => {
18
28
  try {
19
- await writeLatestAgentState(value);
29
+ await writeAgentRunState(value);
20
30
  }
21
31
  catch (error) {
22
- console.error(`Could not update latest agent output in ${resolveAgentStateDir(value.cwd)}: ${error.message}`);
32
+ console.error(`Could not update agent state in ${resolveAgentStateDir(value.cwd)}: ${error.message}`);
33
+ }
34
+ };
35
+ const logAgentCleanupFailures = (failures) => {
36
+ for (const failure of failures) {
37
+ console.error(`Could not clean stale agent output ${failure.state.outputDir}: ${failure.error.message}`);
38
+ }
39
+ };
40
+ const logAgentOrphanCleanupFailures = (failures) => {
41
+ for (const failure of failures) {
42
+ console.error(`Could not clean stale agent output ${failure.outputDir}: ${failure.error.message}`);
43
+ }
44
+ };
45
+ export const cleanupManagedAgentOutputs = async (params) => {
46
+ try {
47
+ const result = await cleanupAgentRunState({
48
+ cwd: params.cwd,
49
+ currentRunId: params.runId,
50
+ keepManagedRuns: params.managedOutput ? 1 : 0,
51
+ });
52
+ logAgentCleanupFailures(result.failed);
53
+ }
54
+ catch (error) {
55
+ console.error(`Could not clean agent state in ${resolveAgentStateDir(params.cwd)}: ${error.message}`);
56
+ }
57
+ try {
58
+ const staleResult = await cleanupStaleAgentRunStates({
59
+ cwd: params.cwd,
60
+ currentRunId: params.runId,
61
+ });
62
+ logAgentCleanupFailures(staleResult.failed);
63
+ logAgentOrphanCleanupFailures(staleResult.orphaned.failed);
64
+ }
65
+ catch (error) {
66
+ console.error(`Could not clean agent state in ${resolveAgentStateDir(params.cwd)}: ${error.message}`);
23
67
  }
24
68
  };
25
69
  export const executeAgentMode = async (params) => {
26
- const { configPath, cwd: configuredCwd, output, expectations, inlineExpectations, environment, environmentName, silent, rerunFrom, rerunLatest, rerunPreset, rerunEnvironments, rerunLabels, args, } = params;
70
+ const { configPath, cwd: configuredCwd, output, expectations, inlineExpectations, environment, environmentName, silent, rerunFrom, rerunLatest, rerunPreset, rerunEnvironments, rerunLabels, reportMode, args, } = params;
27
71
  const command = args[0];
28
72
  const commandArgs = args.slice(1);
29
73
  const cwd = await realpath(configuredCwd ?? process.cwd());
@@ -58,6 +102,8 @@ export const executeAgentMode = async (params) => {
58
102
  exit(exitCode ?? -1);
59
103
  return;
60
104
  }
105
+ const runId = randomUUID();
106
+ const managedOutput = !output;
61
107
  const outputDir = output ? resolve(cwd, output) : await mkdtemp(join(tmpdir(), "allure-agent-"));
62
108
  const expectationsPath = expectations ? resolve(cwd, expectations) : undefined;
63
109
  const environmentOptions = {
@@ -68,6 +114,12 @@ export const executeAgentMode = async (params) => {
68
114
  if (expectationsPath && isPathInside(outputDir, expectationsPath)) {
69
115
  throw new AgentUsageError(`--expectations path ${JSON.stringify(expectationsPath)} must not be inside the agent output directory ${JSON.stringify(outputDir)}`);
70
116
  }
117
+ const humanReport = await createAgentHumanReportConfig({
118
+ mode: reportMode,
119
+ cwd,
120
+ configPath,
121
+ outputDir,
122
+ });
71
123
  const config = await readConfig(cwd, configPath, {
72
124
  output: outputDir,
73
125
  plugins: {
@@ -75,6 +127,7 @@ export const executeAgentMode = async (params) => {
75
127
  options: {
76
128
  outputDir,
77
129
  command: commandString,
130
+ humanReport: humanReport.statusProvider,
78
131
  ...(expectationsPath ? { expectationsPath } : {}),
79
132
  ...(inlineExpectations ? { expectations: inlineExpectations } : {}),
80
133
  },
@@ -90,14 +143,17 @@ export const executeAgentMode = async (params) => {
90
143
  console.error("could not clean output directory", error);
91
144
  }
92
145
  }
93
- const startedAt = new Date().toISOString();
94
- await persistLatestAgentState({
146
+ const startedAt = Date.now();
147
+ await persistAgentRunState({
148
+ runId,
95
149
  cwd,
96
150
  outputDir,
151
+ managedOutput,
97
152
  expectationsPath,
98
153
  command: commandString,
99
154
  startedAt,
100
155
  status: "running",
156
+ pid: process.pid,
101
157
  });
102
158
  printAgentOutputLinks(outputDir);
103
159
  if (expectationsPath) {
@@ -116,7 +172,7 @@ export const executeAgentMode = async (params) => {
116
172
  qualityGate: undefined,
117
173
  allureService: undefined,
118
174
  realTime: false,
119
- plugins: config.plugins,
175
+ plugins: [...humanReport.plugins, ...(config.plugins ?? [])],
120
176
  });
121
177
  const knownIssues = await allureReport.store.allKnownIssues();
122
178
  const { globalExitCode } = await executeAllureRun({
@@ -133,19 +189,147 @@ export const executeAgentMode = async (params) => {
133
189
  ignoreLogs: false,
134
190
  logProcessExit: false,
135
191
  });
136
- await persistLatestAgentState({
192
+ await persistAgentRunState({
193
+ runId,
137
194
  cwd,
138
195
  outputDir,
196
+ managedOutput,
139
197
  expectationsPath,
140
198
  command: commandString,
141
199
  startedAt,
142
- finishedAt: new Date().toISOString(),
200
+ finishedAt: Date.now(),
143
201
  status: "finished",
144
202
  exitCode: globalExitCode.actual ?? globalExitCode.original,
203
+ pid: process.pid,
145
204
  });
205
+ await cleanupManagedAgentOutputs({ cwd, runId, managedOutput });
146
206
  exit(globalExitCode.actual ?? globalExitCode.original);
147
207
  }
148
208
  finally {
149
209
  await rerunContext?.cleanup();
150
210
  }
151
211
  };
212
+ export const executeAgentInspectMode = async (params) => {
213
+ const { configPath, cwd: configuredCwd, output, expectations, inlineExpectations, environment, environmentName, reportName, open, port, historyLimit, hideLabels, dumps = [], reportMode, resultsDir, } = params;
214
+ const cwd = await realpath(configuredCwd ?? process.cwd());
215
+ const dumpFiles = dumps.length ? await findFilesByGlobs(cwd, dumps) : [];
216
+ const shouldReadResults = resultsDir.length > 0 || dumps.length === 0;
217
+ const { resultDirectories = [], patterns = resultsDir } = shouldReadResults
218
+ ? await findAllureResultDirectories(cwd, resultsDir)
219
+ : {};
220
+ if (dumps.length > 0 && dumpFiles.length === 0) {
221
+ throw new AgentUsageError(`No dump files found matching pattern: ${dumps.join(", ")}`);
222
+ }
223
+ if (resultDirectories.length === 0 && dumpFiles.length === 0) {
224
+ const inspectedPatterns = [...(patterns ?? []), ...dumps];
225
+ throw new AgentUsageError(`No test results directories or dump files found matching pattern: ${inspectedPatterns}`);
226
+ }
227
+ const runId = randomUUID();
228
+ const managedOutput = !output;
229
+ const outputDir = output ? resolve(cwd, output) : await mkdtemp(join(tmpdir(), "allure-agent-"));
230
+ const expectationsPath = expectations ? resolve(cwd, expectations) : undefined;
231
+ const commandString = formatAgentInspectCommand({ dumps: dumpFiles, resultsDir: resultDirectories });
232
+ const hiddenLabels = hideLabels?.length ? hideLabels : undefined;
233
+ const environmentOptions = {
234
+ environment,
235
+ environmentName,
236
+ };
237
+ normalizeCommandEnvironmentOptions(environmentOptions);
238
+ if (expectationsPath && isPathInside(outputDir, expectationsPath)) {
239
+ throw new AgentUsageError(`--expectations path ${JSON.stringify(expectationsPath)} must not be inside the agent output directory ${JSON.stringify(outputDir)}`);
240
+ }
241
+ const historyLimitValue = historyLimit ? parseInt(historyLimit, 10) : undefined;
242
+ const humanReport = await createAgentHumanReportConfig({
243
+ mode: reportMode,
244
+ cwd,
245
+ configPath,
246
+ outputDir,
247
+ configOverride: {
248
+ name: reportName,
249
+ open,
250
+ port,
251
+ hideLabels: hiddenLabels,
252
+ historyLimit: historyLimitValue,
253
+ },
254
+ });
255
+ const config = await readConfig(cwd, configPath, {
256
+ name: reportName,
257
+ output: outputDir,
258
+ open,
259
+ port,
260
+ hideLabels: hiddenLabels,
261
+ historyLimit: historyLimitValue,
262
+ plugins: {
263
+ agent: {
264
+ options: {
265
+ outputDir,
266
+ command: commandString,
267
+ humanReport: humanReport.statusProvider,
268
+ ...(expectationsPath ? { expectationsPath } : {}),
269
+ ...(inlineExpectations ? { expectations: inlineExpectations } : {}),
270
+ },
271
+ },
272
+ },
273
+ });
274
+ const resolvedEnvironment = resolveCommandEnvironment(config, environmentOptions);
275
+ try {
276
+ await rm(outputDir, { recursive: true });
277
+ }
278
+ catch (error) {
279
+ if (!isFileNotFoundError(error)) {
280
+ console.error("could not clean output directory", error);
281
+ }
282
+ }
283
+ const startedAt = Date.now();
284
+ await persistAgentRunState({
285
+ runId,
286
+ cwd,
287
+ outputDir,
288
+ managedOutput,
289
+ expectationsPath,
290
+ command: commandString,
291
+ startedAt,
292
+ status: "running",
293
+ pid: process.pid,
294
+ });
295
+ printAgentOutputLinks(outputDir);
296
+ if (expectationsPath) {
297
+ console.log(`agent expectations: ${expectationsPath}`);
298
+ }
299
+ else if (inlineExpectations) {
300
+ console.log("agent expectations: CLI options");
301
+ }
302
+ console.log(commandString);
303
+ const allureReport = new AllureReport({
304
+ ...config,
305
+ output: outputDir,
306
+ environment: resolvedEnvironment?.id,
307
+ open: false,
308
+ port: undefined,
309
+ qualityGate: undefined,
310
+ allureService: undefined,
311
+ realTime: false,
312
+ plugins: [...humanReport.plugins, ...(config.plugins ?? [])],
313
+ });
314
+ await allureReport.restoreState(Array.from(dumpFiles));
315
+ await allureReport.start();
316
+ for (const dir of resultDirectories) {
317
+ await allureReport.readDirectory(dir);
318
+ }
319
+ await allureReport.done();
320
+ await persistAgentRunState({
321
+ runId,
322
+ cwd,
323
+ outputDir,
324
+ managedOutput,
325
+ expectationsPath,
326
+ command: commandString,
327
+ startedAt,
328
+ finishedAt: Date.now(),
329
+ status: "finished",
330
+ exitCode: 0,
331
+ pid: process.pid,
332
+ });
333
+ await cleanupManagedAgentOutputs({ cwd, runId, managedOutput });
334
+ exit(0);
335
+ };
@@ -13,6 +13,7 @@ export declare class AgentCommand extends Command {
13
13
  config: string | undefined;
14
14
  cwd: string | undefined;
15
15
  output: string | undefined;
16
+ report: string | undefined;
16
17
  expectations: string | undefined;
17
18
  goal: string[] | undefined;
18
19
  taskId: string[] | undefined;
@@ -37,6 +38,37 @@ export declare class AgentCommand extends Command {
37
38
  commandToRun: string[];
38
39
  execute(): Promise<void>;
39
40
  }
41
+ export declare class AgentInspectCommand extends Command {
42
+ static paths: string[][];
43
+ static usage: import("clipanion").Usage;
44
+ resultsDir: string[];
45
+ config: string | undefined;
46
+ cwd: string | undefined;
47
+ output: string | undefined;
48
+ report: string | undefined;
49
+ reportName: string | undefined;
50
+ dump: string[] | undefined;
51
+ open: boolean | undefined;
52
+ port: string | undefined;
53
+ historyLimit: string | undefined;
54
+ hideLabels: string[] | undefined;
55
+ expectations: string | undefined;
56
+ goal: string[] | undefined;
57
+ taskId: string[] | undefined;
58
+ expectTests: string[] | undefined;
59
+ expectLabels: string[] | undefined;
60
+ expectEnvironments: string[] | undefined;
61
+ expectFullNames: string[] | undefined;
62
+ expectPrefixes: string[] | undefined;
63
+ forbidLabels: string[] | undefined;
64
+ expectStepContains: string[] | undefined;
65
+ expectSteps: string[] | undefined;
66
+ expectAttachments: string[] | undefined;
67
+ expectAttachmentFilters: string[] | undefined;
68
+ environment: string | undefined;
69
+ environmentName: string | undefined;
70
+ execute(): Promise<void>;
71
+ }
40
72
  export declare class AgentLatestCommand extends Command {
41
73
  static paths: string[][];
42
74
  static usage: import("clipanion").Usage;
@@ -1,26 +1,83 @@
1
1
  import * as console from "node:console";
2
+ import { randomUUID } from "node:crypto";
2
3
  import { mkdir, mkdtemp, realpath, writeFile } from "node:fs/promises";
3
4
  import { tmpdir } from "node:os";
4
5
  import { dirname, join, resolve } from "node:path";
5
6
  import process, { exit } from "node:process";
6
- import { AGENT_FINDING_CATEGORIES, AGENT_FINDING_SEVERITIES, AGENT_TASK_MAP_HELP, AGENT_TEST_STATUSES, AgentExpectationUsageError, buildAgentInlineExpectations, buildAgentQueryPayload, createAgentCapabilities, formatAgentOutputLinks, isAgentExpectationUsageError, isAgentTaskMapHelpRequest, isAgentUsageError, loadAgentOutput, normalizeAgentQueryLimit, normalizeAgentQueryView, normalizeAgentRerunPreset, normalizeRepeatedEnumValues, normalizeRepeatedStringValues, parseAgentLabelFilters, readLatestAgentState, resolveAgentSelectionOutputDir, resolveAgentStateDir, selectAgentTestPlan, validateAgentExpectationsFile, writeLatestAgentState, writeInvalidAgentExpectationOutput, } from "@allurereport/plugin-agent";
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
9
  export { AGENT_TASK_MAP_HELP, createAgentCapabilities, isAgentTaskMapHelpRequest };
9
10
  const readOptionalString = (value) => (typeof value === "string" ? value : undefined);
10
11
  const readOptionalBoolean = (value) => value === true;
11
12
  const readOptionalStringArray = (value) => (Array.isArray(value) ? value : undefined);
12
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
+ });
13
36
  const printAgentOutputLinks = (outputDir) => {
14
37
  for (const line of formatAgentOutputLinks(outputDir)) {
15
38
  console.log(line);
16
39
  }
17
40
  };
18
- const persistLatestAgentState = async (value) => {
41
+ const persistAgentRunState = async (value) => {
42
+ try {
43
+ await writeAgentRunState(value);
44
+ }
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}`);
52
+ }
53
+ };
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
+ }
58
+ };
59
+ const cleanupManagedAgentOutputs = async (params) => {
19
60
  try {
20
- 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);
21
67
  }
22
68
  catch (error) {
23
- 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}`);
24
81
  }
25
82
  };
26
83
  const agentEnvironmentOption = () => Option.String("--environment,--env", {
@@ -35,6 +92,15 @@ const throwCliUsageError = (error) => {
35
92
  }
36
93
  throw error;
37
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
+ };
38
104
  export class AgentCapabilitiesCommand extends Command {
39
105
  constructor() {
40
106
  super(...arguments);
@@ -65,7 +131,10 @@ export class AgentCommand extends Command {
65
131
  description: "The working directory for the command to run (default: current working directory)",
66
132
  });
67
133
  this.output = Option.String("--output,-o", {
68
- 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)",
69
138
  });
70
139
  this.expectations = Option.String("--expectations", {
71
140
  description: "The path to a YAML or JSON expectations file",
@@ -133,24 +202,25 @@ export class AgentCommand extends Command {
133
202
  const configPath = readOptionalString(this.config);
134
203
  const configuredCwd = readOptionalString(this.cwd);
135
204
  const output = readOptionalString(this.output);
205
+ const reportMode = normalizeAgentHumanReportMode(readOptionalString(this.report));
136
206
  const expectations = readOptionalString(this.expectations);
137
- if (!args || !args.length) {
207
+ if (!args.length) {
138
208
  throw new UsageError("expecting command to be specified after --, e.g. allure agent -- npm run test");
139
209
  }
140
210
  try {
141
- const inlineExpectations = buildAgentInlineExpectations({
211
+ const inlineExpectations = buildInlineExpectationsFromOptions({
142
212
  goal: this.goal,
143
213
  taskId: this.taskId,
144
214
  expectTests: this.expectTests,
145
- expectLabels: readOptionalStringArray(this.expectLabels),
146
- expectEnvironments: readOptionalStringArray(this.expectEnvironments),
147
- expectFullNames: readOptionalStringArray(this.expectFullNames),
148
- expectPrefixes: readOptionalStringArray(this.expectPrefixes),
149
- forbidLabels: readOptionalStringArray(this.forbidLabels),
150
- expectStepContains: readOptionalStringArray(this.expectStepContains),
215
+ expectLabels: this.expectLabels,
216
+ expectEnvironments: this.expectEnvironments,
217
+ expectFullNames: this.expectFullNames,
218
+ expectPrefixes: this.expectPrefixes,
219
+ forbidLabels: this.forbidLabels,
220
+ expectStepContains: this.expectStepContains,
151
221
  expectSteps: this.expectSteps,
152
222
  expectAttachments: this.expectAttachments,
153
- expectAttachmentFilters: readOptionalStringArray(this.expectAttachmentFilters),
223
+ expectAttachmentFilters: this.expectAttachmentFilters,
154
224
  });
155
225
  if (expectations && inlineExpectations) {
156
226
  throw new AgentExpectationUsageError("Use either --expectations <file> or inline expectation flags, not both", "--expectations");
@@ -175,6 +245,7 @@ export class AgentCommand extends Command {
175
245
  rerunPreset: readOptionalString(this.rerunPreset),
176
246
  rerunEnvironments: readOptionalStringArray(this.rerunEnvironments),
177
247
  rerunLabels: readOptionalStringArray(this.rerunLabels),
248
+ reportMode,
178
249
  args,
179
250
  });
180
251
  }
@@ -184,6 +255,8 @@ export class AgentCommand extends Command {
184
255
  }
185
256
  const expectationError = error;
186
257
  const cwd = await realpath(configuredCwd ?? process.cwd());
258
+ const runId = randomUUID();
259
+ const managedOutput = !output;
187
260
  const outputDir = output ? resolve(cwd, output) : await mkdtemp(join(tmpdir(), "allure-agent-"));
188
261
  const commandString = formatAgentCommand(args);
189
262
  const { generatedAt } = await writeInvalidAgentExpectationOutput({
@@ -191,15 +264,21 @@ export class AgentCommand extends Command {
191
264
  command: commandString,
192
265
  error: expectationError,
193
266
  });
194
- await persistLatestAgentState({
267
+ const generatedAtMs = Date.parse(generatedAt);
268
+ const generatedAtTimestamp = Number.isFinite(generatedAtMs) ? generatedAtMs : Date.now();
269
+ await persistAgentRunState({
270
+ runId,
195
271
  cwd,
196
272
  outputDir,
273
+ managedOutput,
197
274
  command: commandString,
198
- startedAt: generatedAt,
199
- finishedAt: generatedAt,
275
+ startedAt: generatedAtTimestamp,
276
+ finishedAt: generatedAtTimestamp,
200
277
  status: "finished",
201
278
  exitCode: 1,
279
+ pid: process.pid,
202
280
  });
281
+ await cleanupManagedAgentOutputs({ cwd, runId, managedOutput });
203
282
  printAgentOutputLinks(outputDir);
204
283
  console.error(expectationError.message);
205
284
  exit(1);
@@ -209,15 +288,206 @@ export class AgentCommand extends Command {
209
288
  AgentCommand.paths = [["agent"]];
210
289
  AgentCommand.usage = Command.Usage({
211
290
  description: "Run specified command in Allure agent mode",
212
- 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.",
213
292
  examples: [
214
- ["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"],
215
295
  [
216
296
  "agent --expectations ./expected.yaml -- npm test",
217
297
  "Run npm test with agent-mode expectations loaded from ./expected.yaml",
218
298
  ],
219
299
  ],
220
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
+ });
221
491
  export class AgentLatestCommand extends Command {
222
492
  constructor() {
223
493
  super(...arguments);
@@ -247,7 +517,7 @@ export class AgentLatestCommand extends Command {
247
517
  AgentLatestCommand.paths = [["agent", "latest"]];
248
518
  AgentLatestCommand.usage = Command.Usage({
249
519
  description: "Print the latest Allure agent output directory and index path for the current project",
250
- details: "This command prints the latest agent output directory and index.md path recorded for the resolved project cwd.",
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.",
251
521
  examples: [
252
522
  ["agent latest", "Print the latest agent output directory and index path for the current project"],
253
523
  [
@@ -270,11 +540,11 @@ export class AgentStateDirCommand extends Command {
270
540
  }
271
541
  AgentStateDirCommand.paths = [["agent", "state-dir"]];
272
542
  AgentStateDirCommand.usage = Command.Usage({
273
- description: "Print the Allure agent state directory for the current project",
274
- 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.",
275
545
  examples: [
276
- ["agent state-dir", "Print the resolved state directory for the current project"],
277
- ["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"],
278
548
  ],
279
549
  });
280
550
  export class AgentQueryCommand extends Command {
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import process, { argv } from "node:process";
3
3
  import { Builtins, Cli } from "clipanion";
4
- import { AgentCommand, AGENT_TASK_MAP_HELP, AgentCapabilitiesCommand, AgentLatestCommand, AgentQueryCommand, AgentSelectCommand, AgentStateDirCommand, Allure2Command, AwesomeCommand, CheckCommand, ClassicCommand, CsvCommand, DashboardCommand, GenerateCommand, HistoryCommand, JiraClearCommand, KnownIssueCommand, LogCommand, OpenCommand, QualityGateCommand, ResultsPackCommand, ResultsUnpackCommand, RunCommand, SlackCommand, TestPlanCommand, WatchCommand, isAgentTaskMapHelpRequest, } from "./commands/index.js";
4
+ import { AgentCommand, AGENT_TASK_MAP_HELP, AgentCapabilitiesCommand, AgentInspectCommand, AgentLatestCommand, AgentQueryCommand, AgentSelectCommand, AgentStateDirCommand, Allure2Command, AwesomeCommand, CheckCommand, ClassicCommand, CsvCommand, DashboardCommand, GenerateCommand, HistoryCommand, JiraClearCommand, KnownIssueCommand, LogCommand, OpenCommand, QualityGateCommand, ResultsPackCommand, ResultsUnpackCommand, RunCommand, SlackCommand, TestPlanCommand, WatchCommand, isAgentTaskMapHelpRequest, } from "./commands/index.js";
5
5
  const [node, app, ...args] = argv;
6
6
  const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
7
7
  const cli = new Cli({
@@ -12,6 +12,7 @@ const cli = new Cli({
12
12
  cli.register(AwesomeCommand);
13
13
  cli.register(Allure2Command);
14
14
  cli.register(AgentCapabilitiesCommand);
15
+ cli.register(AgentInspectCommand);
15
16
  cli.register(AgentLatestCommand);
16
17
  cli.register(AgentQueryCommand);
17
18
  cli.register(AgentSelectCommand);
@@ -4,6 +4,50 @@ import { platform } from "process";
4
4
  import { invokeStdoutCliTool } from "@allurereport/reader-api";
5
5
  const IS_WIN = platform === "win32";
6
6
  const PS_OUTPUT_PATTERN = /(?<ppid>\d+)\s+(?<pid>\d+)\s+(?<comm>.*)/;
7
+ const quotePosixShellArg = (arg) => {
8
+ if (/^[A-Za-z0-9_/:=.,@%+-]+$/u.test(arg)) {
9
+ return arg;
10
+ }
11
+ return `'${arg.replace(/'/g, "'\\''")}'`;
12
+ };
13
+ const quoteWindowsShellArg = (arg) => {
14
+ if (arg && !/[\s"&()<>^|%]/u.test(arg)) {
15
+ return arg;
16
+ }
17
+ let quoted = '"';
18
+ let backslashes = 0;
19
+ for (const char of arg) {
20
+ if (char === "\\") {
21
+ backslashes += 1;
22
+ continue;
23
+ }
24
+ if (char === '"') {
25
+ quoted += "\\".repeat(backslashes * 2 + 1);
26
+ quoted += char;
27
+ backslashes = 0;
28
+ continue;
29
+ }
30
+ quoted += "\\".repeat(backslashes);
31
+ backslashes = 0;
32
+ quoted += char;
33
+ }
34
+ quoted += "\\".repeat(backslashes * 2);
35
+ quoted += '"';
36
+ return quoted;
37
+ };
38
+ const quoteShellArg = IS_WIN ? quoteWindowsShellArg : quotePosixShellArg;
39
+ const resolveShellInvocation = (command, commandArgs, shell) => {
40
+ if (!shell || commandArgs.length === 0) {
41
+ return {
42
+ command,
43
+ commandArgs,
44
+ };
45
+ }
46
+ return {
47
+ command: [command, ...commandArgs].map(quoteShellArg).join(" "),
48
+ commandArgs: [],
49
+ };
50
+ };
7
51
  export const runProcess = (params) => {
8
52
  const { command, commandArgs, cwd, environmentVariables = {}, logs = "inherit", shell = IS_WIN } = params;
9
53
  const env = {
@@ -20,7 +64,8 @@ export const runProcess = (params) => {
20
64
  });
21
65
  delete env.NO_COLOR;
22
66
  }
23
- return spawn(command, commandArgs, {
67
+ const processInvocation = resolveShellInvocation(command, commandArgs, shell);
68
+ return spawn(processInvocation.command, processInvocation.commandArgs, {
24
69
  env,
25
70
  cwd,
26
71
  stdio: logs,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "allure",
3
- "version": "3.11.0",
3
+ "version": "3.12.0",
4
4
  "description": "Allure Commandline Tool",
5
5
  "keywords": [
6
6
  "allure",
@@ -32,26 +32,26 @@
32
32
  "lint:fix": "oxlint --import-plugin --fix src test features stories"
33
33
  },
34
34
  "dependencies": {
35
- "@allurereport/charts-api": "3.11.0",
36
- "@allurereport/ci": "3.11.0",
37
- "@allurereport/core": "3.11.0",
38
- "@allurereport/core-api": "3.11.0",
39
- "@allurereport/directory-watcher": "3.11.0",
40
- "@allurereport/plugin-agent": "3.11.0",
41
- "@allurereport/plugin-allure2": "3.11.0",
42
- "@allurereport/plugin-api": "3.11.0",
43
- "@allurereport/plugin-awesome": "3.11.0",
44
- "@allurereport/plugin-classic": "3.11.0",
45
- "@allurereport/plugin-csv": "3.11.0",
46
- "@allurereport/plugin-dashboard": "3.11.0",
47
- "@allurereport/plugin-jira": "3.11.0",
48
- "@allurereport/plugin-log": "3.11.0",
49
- "@allurereport/plugin-progress": "3.11.0",
50
- "@allurereport/plugin-server-reload": "3.11.0",
51
- "@allurereport/plugin-slack": "3.11.0",
52
- "@allurereport/reader-api": "3.11.0",
53
- "@allurereport/service": "3.11.0",
54
- "@allurereport/static-server": "3.11.0",
35
+ "@allurereport/charts-api": "3.12.0",
36
+ "@allurereport/ci": "3.12.0",
37
+ "@allurereport/core": "3.12.0",
38
+ "@allurereport/core-api": "3.12.0",
39
+ "@allurereport/directory-watcher": "3.12.0",
40
+ "@allurereport/plugin-agent": "3.12.0",
41
+ "@allurereport/plugin-allure2": "3.12.0",
42
+ "@allurereport/plugin-api": "3.12.0",
43
+ "@allurereport/plugin-awesome": "3.12.0",
44
+ "@allurereport/plugin-classic": "3.12.0",
45
+ "@allurereport/plugin-csv": "3.12.0",
46
+ "@allurereport/plugin-dashboard": "3.12.0",
47
+ "@allurereport/plugin-jira": "3.12.0",
48
+ "@allurereport/plugin-log": "3.12.0",
49
+ "@allurereport/plugin-progress": "3.12.0",
50
+ "@allurereport/plugin-server-reload": "3.12.0",
51
+ "@allurereport/plugin-slack": "3.12.0",
52
+ "@allurereport/reader-api": "3.12.0",
53
+ "@allurereport/service": "3.12.0",
54
+ "@allurereport/static-server": "3.12.0",
55
55
  "adm-zip": "^0.5.16",
56
56
  "clipanion": "^4.0.0-rc.4",
57
57
  "glob": "^13.0.6",