allure 3.10.0 → 3.11.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,21 @@ 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, can load an expectations file with `--expectations`, and ignores configured presentation or export plugins such as Awesome or TestOps unless you explicitly fall back to the lower-level `ALLURE_AGENT_*` plus `allure run` flow.
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.
78
+
79
+ Agents and setup tools can inspect the local structured capability contract without scraping help text:
80
+
81
+ ```bash
82
+ npx allure agent capabilities --json
83
+ ```
84
+
85
+ After a run, agents can query the output directory without manually reading every manifest:
86
+
87
+ ```bash
88
+ npx allure agent query --latest summary
89
+ npx allure agent query --latest tests --status failed
90
+ npx allure agent query --from ./agent-output findings --severity high
91
+ ```
78
92
 
79
93
  ### Generating Reports Manually
80
94
 
@@ -118,6 +132,7 @@ The Allure CLI includes several helpful global options. Use `--help` to explore
118
132
 
119
133
  ```bash
120
134
  npx allure run --help
135
+ npx allure agent capabilities --json
121
136
  npx allure agent --help
122
137
  npx allure watch --help
123
138
  ```
@@ -0,0 +1,21 @@
1
+ import { writeLatestAgentState, type AgentExpectationsInput } from "@allurereport/plugin-agent";
2
+ export declare const formatAgentCommand: (args: string[]) => string;
3
+ export declare const printAgentOutputLinks: (outputDir: string) => void;
4
+ export declare const persistLatestAgentState: (value: Parameters<typeof writeLatestAgentState>[0]) => Promise<void>;
5
+ export type ExecuteAgentModeParams = {
6
+ configPath?: string;
7
+ cwd?: string;
8
+ output?: string;
9
+ expectations?: string;
10
+ inlineExpectations?: AgentExpectationsInput;
11
+ environment?: string;
12
+ environmentName?: string;
13
+ silent?: boolean;
14
+ rerunFrom?: string;
15
+ rerunLatest?: boolean;
16
+ rerunPreset?: string;
17
+ rerunEnvironments?: string[];
18
+ rerunLabels?: string[];
19
+ args: string[];
20
+ };
21
+ export declare const executeAgentMode: (params: ExecuteAgentModeParams) => Promise<void>;
@@ -0,0 +1,151 @@
1
+ import * as console from "node:console";
2
+ import { mkdtemp, realpath, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join, resolve } from "node:path";
5
+ import process, { exit } from "node:process";
6
+ import { AllureReport, isFileNotFoundError, readConfig } from "@allurereport/core";
7
+ import { createAgentTestPlanContext, AgentUsageError, formatAgentOutputLinks, isPathInside, normalizeAgentRerunPreset, parseAgentLabelFilters, resolveAgentStateDir, writeLatestAgentState, } from "@allurereport/plugin-agent";
8
+ import { normalizeCommandEnvironmentOptions, resolveCommandEnvironment } from "../utils/environment.js";
9
+ import { createChildAllureCliEnvironment, getActiveAllureCliCommand } from "../utils/execution-context.js";
10
+ import { executeAllureRun, executeNestedAllureCommand } from "./commons/run.js";
11
+ export const formatAgentCommand = (args) => args.join(" ");
12
+ export const printAgentOutputLinks = (outputDir) => {
13
+ for (const line of formatAgentOutputLinks(outputDir)) {
14
+ console.log(line);
15
+ }
16
+ };
17
+ export const persistLatestAgentState = async (value) => {
18
+ try {
19
+ await writeLatestAgentState(value);
20
+ }
21
+ catch (error) {
22
+ console.error(`Could not update latest agent output in ${resolveAgentStateDir(value.cwd)}: ${error.message}`);
23
+ }
24
+ };
25
+ export const executeAgentMode = async (params) => {
26
+ const { configPath, cwd: configuredCwd, output, expectations, inlineExpectations, environment, environmentName, silent, rerunFrom, rerunLatest, rerunPreset, rerunEnvironments, rerunLabels, args, } = params;
27
+ const command = args[0];
28
+ const commandArgs = args.slice(1);
29
+ const cwd = await realpath(configuredCwd ?? process.cwd());
30
+ const commandString = formatAgentCommand(args);
31
+ const hasRerunSource = !!rerunFrom || !!rerunLatest;
32
+ const hasRerunFilters = !!rerunPreset || !!rerunEnvironments?.length || !!rerunLabels?.length;
33
+ if (!hasRerunSource && hasRerunFilters) {
34
+ throw new AgentUsageError("Use rerun filters only together with --rerun-from <path> or --rerun-latest");
35
+ }
36
+ const rerunContext = await createAgentTestPlanContext({
37
+ cwd,
38
+ from: rerunFrom,
39
+ latest: rerunLatest,
40
+ preset: normalizeAgentRerunPreset(rerunPreset),
41
+ environments: rerunEnvironments?.length ? rerunEnvironments : undefined,
42
+ labelFilters: parseAgentLabelFilters(rerunLabels),
43
+ });
44
+ const childEnvironmentVariables = {
45
+ ...createChildAllureCliEnvironment("agent"),
46
+ ...(rerunContext ? { ALLURE_TESTPLAN_PATH: rerunContext.testPlanPath } : {}),
47
+ };
48
+ try {
49
+ if (getActiveAllureCliCommand()) {
50
+ console.log(commandString);
51
+ const exitCode = await executeNestedAllureCommand({
52
+ command,
53
+ commandArgs,
54
+ cwd,
55
+ ...(rerunContext ? { environmentVariables: { ALLURE_TESTPLAN_PATH: rerunContext.testPlanPath } } : {}),
56
+ silent,
57
+ });
58
+ exit(exitCode ?? -1);
59
+ return;
60
+ }
61
+ const outputDir = output ? resolve(cwd, output) : await mkdtemp(join(tmpdir(), "allure-agent-"));
62
+ const expectationsPath = expectations ? resolve(cwd, expectations) : undefined;
63
+ const environmentOptions = {
64
+ environment,
65
+ environmentName,
66
+ };
67
+ normalizeCommandEnvironmentOptions(environmentOptions);
68
+ if (expectationsPath && isPathInside(outputDir, expectationsPath)) {
69
+ throw new AgentUsageError(`--expectations path ${JSON.stringify(expectationsPath)} must not be inside the agent output directory ${JSON.stringify(outputDir)}`);
70
+ }
71
+ const config = await readConfig(cwd, configPath, {
72
+ output: outputDir,
73
+ plugins: {
74
+ agent: {
75
+ options: {
76
+ outputDir,
77
+ command: commandString,
78
+ ...(expectationsPath ? { expectationsPath } : {}),
79
+ ...(inlineExpectations ? { expectations: inlineExpectations } : {}),
80
+ },
81
+ },
82
+ },
83
+ });
84
+ const resolvedEnvironment = resolveCommandEnvironment(config, environmentOptions);
85
+ try {
86
+ await rm(outputDir, { recursive: true });
87
+ }
88
+ catch (error) {
89
+ if (!isFileNotFoundError(error)) {
90
+ console.error("could not clean output directory", error);
91
+ }
92
+ }
93
+ const startedAt = new Date().toISOString();
94
+ await persistLatestAgentState({
95
+ cwd,
96
+ outputDir,
97
+ expectationsPath,
98
+ command: commandString,
99
+ startedAt,
100
+ status: "running",
101
+ });
102
+ printAgentOutputLinks(outputDir);
103
+ if (expectationsPath) {
104
+ console.log(`agent expectations: ${expectationsPath}`);
105
+ }
106
+ else if (inlineExpectations) {
107
+ console.log("agent expectations: CLI options");
108
+ }
109
+ console.log(commandString);
110
+ const allureReport = new AllureReport({
111
+ ...config,
112
+ output: outputDir,
113
+ environment: resolvedEnvironment?.id,
114
+ open: false,
115
+ port: undefined,
116
+ qualityGate: undefined,
117
+ allureService: undefined,
118
+ realTime: false,
119
+ plugins: config.plugins,
120
+ });
121
+ const knownIssues = await allureReport.store.allKnownIssues();
122
+ const { globalExitCode } = await executeAllureRun({
123
+ allureReport,
124
+ knownIssues,
125
+ cwd,
126
+ command,
127
+ commandArgs,
128
+ environmentVariables: childEnvironmentVariables,
129
+ environment: resolvedEnvironment?.id,
130
+ withQualityGate: false,
131
+ logs: "pipe",
132
+ silent,
133
+ ignoreLogs: false,
134
+ logProcessExit: false,
135
+ });
136
+ await persistLatestAgentState({
137
+ cwd,
138
+ outputDir,
139
+ expectationsPath,
140
+ command: commandString,
141
+ startedAt,
142
+ finishedAt: new Date().toISOString(),
143
+ status: "finished",
144
+ exitCode: globalExitCode.actual ?? globalExitCode.original,
145
+ });
146
+ exit(globalExitCode.actual ?? globalExitCode.original);
147
+ }
148
+ finally {
149
+ await rerunContext?.cleanup();
150
+ }
151
+ };
@@ -1,4 +1,12 @@
1
+ import { AGENT_TASK_MAP_HELP, createAgentCapabilities, isAgentTaskMapHelpRequest } from "@allurereport/plugin-agent";
1
2
  import { Command } from "clipanion";
3
+ export { AGENT_TASK_MAP_HELP, createAgentCapabilities, isAgentTaskMapHelpRequest };
4
+ export declare class AgentCapabilitiesCommand extends Command {
5
+ static paths: string[][];
6
+ static usage: import("clipanion").Usage;
7
+ json: boolean;
8
+ execute(): Promise<void>;
9
+ }
2
10
  export declare class AgentCommand extends Command {
3
11
  static paths: string[][];
4
12
  static usage: import("clipanion").Usage;
@@ -6,6 +14,18 @@ export declare class AgentCommand extends Command {
6
14
  cwd: string | undefined;
7
15
  output: string | undefined;
8
16
  expectations: string | undefined;
17
+ goal: string[] | undefined;
18
+ taskId: string[] | undefined;
19
+ expectTests: string[] | undefined;
20
+ expectLabels: string[] | undefined;
21
+ expectEnvironments: string[] | undefined;
22
+ expectFullNames: string[] | undefined;
23
+ expectPrefixes: string[] | undefined;
24
+ forbidLabels: string[] | undefined;
25
+ expectStepContains: string[] | undefined;
26
+ expectSteps: string[] | undefined;
27
+ expectAttachments: string[] | undefined;
28
+ expectAttachmentFilters: string[] | undefined;
9
29
  environment: string | undefined;
10
30
  environmentName: string | undefined;
11
31
  silent: boolean | undefined;
@@ -29,6 +49,24 @@ export declare class AgentStateDirCommand extends Command {
29
49
  cwd: string | undefined;
30
50
  execute(): Promise<void>;
31
51
  }
52
+ export declare class AgentQueryCommand extends Command {
53
+ static paths: string[][];
54
+ static usage: import("clipanion").Usage;
55
+ view: string | undefined;
56
+ cwd: string | undefined;
57
+ from: string | undefined;
58
+ latest: boolean | undefined;
59
+ statuses: string[] | undefined;
60
+ environments: string[] | undefined;
61
+ labels: string[] | undefined;
62
+ severities: string[] | undefined;
63
+ categories: string[] | undefined;
64
+ checks: string[] | undefined;
65
+ test: string | undefined;
66
+ limit: string | undefined;
67
+ includeMarkdown: boolean | undefined;
68
+ execute(): Promise<void>;
69
+ }
32
70
  export declare class AgentSelectCommand extends Command {
33
71
  static paths: string[][];
34
72
  static usage: import("clipanion").Usage;
@@ -41,18 +79,3 @@ export declare class AgentSelectCommand extends Command {
41
79
  output: string | undefined;
42
80
  execute(): Promise<void>;
43
81
  }
44
- export declare const executeAgentMode: (params: {
45
- configPath?: string;
46
- cwd?: string;
47
- output?: string;
48
- expectations?: string;
49
- environment?: string;
50
- environmentName?: string;
51
- silent?: boolean;
52
- rerunFrom?: string;
53
- rerunLatest?: boolean;
54
- rerunPreset?: string;
55
- rerunEnvironments?: string[];
56
- rerunLabels?: string[];
57
- args?: string[];
58
- }) => Promise<void>;
@@ -1,42 +1,20 @@
1
1
  import * as console from "node:console";
2
- import { mkdir, mkdtemp, realpath, rm, writeFile } from "node:fs/promises";
2
+ import { mkdir, mkdtemp, realpath, writeFile } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
- import { dirname, join, relative, resolve } from "node:path";
4
+ import { dirname, join, resolve } from "node:path";
5
5
  import process, { exit } from "node:process";
6
- import { AllureReport, isFileNotFoundError, readConfig } from "@allurereport/core";
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
7
  import { Command, Option, UsageError } from "clipanion";
8
- import { createAgentTestPlanContext, normalizeAgentRerunPreset, parseAgentLabelFilters, resolveAgentSelectionOutputDir, selectAgentTestPlan, } from "../utils/agent-select.js";
9
- import { readLatestAgentState, resolveAgentStateDir, writeLatestAgentState } from "../utils/agent-state.js";
10
- import { environmentNameOption, environmentOption, normalizeCommandEnvironmentOptions, resolveCommandEnvironment, } from "../utils/environment.js";
11
- import { createChildAllureCliEnvironment, getActiveAllureCliCommand } from "../utils/execution-context.js";
12
- import { executeAllureRun, executeNestedAllureCommand } from "./commons/run.js";
13
- const withProcessEnv = async (overrides, fn) => {
14
- const previousValues = new Map();
15
- for (const [key, value] of Object.entries(overrides)) {
16
- previousValues.set(key, process.env[key]);
17
- if (value === undefined) {
18
- delete process.env[key];
19
- continue;
20
- }
21
- process.env[key] = value;
22
- }
23
- try {
24
- return await fn();
25
- }
26
- finally {
27
- for (const [key, value] of previousValues) {
28
- if (value === undefined) {
29
- delete process.env[key];
30
- continue;
31
- }
32
- process.env[key] = value;
33
- }
8
+ export { AGENT_TASK_MAP_HELP, createAgentCapabilities, isAgentTaskMapHelpRequest };
9
+ const readOptionalString = (value) => (typeof value === "string" ? value : undefined);
10
+ const readOptionalBoolean = (value) => value === true;
11
+ const readOptionalStringArray = (value) => (Array.isArray(value) ? value : undefined);
12
+ const formatAgentCommand = (args) => args.join(" ");
13
+ const printAgentOutputLinks = (outputDir) => {
14
+ for (const line of formatAgentOutputLinks(outputDir)) {
15
+ console.log(line);
34
16
  }
35
17
  };
36
- const isPathInside = (parentPath, candidatePath) => {
37
- const rel = relative(parentPath, candidatePath);
38
- return rel === "" || (!rel.startsWith("..") && rel !== "." && !rel.startsWith("../"));
39
- };
40
18
  const persistLatestAgentState = async (value) => {
41
19
  try {
42
20
  await writeLatestAgentState(value);
@@ -45,9 +23,38 @@ const persistLatestAgentState = async (value) => {
45
23
  console.error(`Could not update latest agent output in ${resolveAgentStateDir(value.cwd)}: ${error.message}`);
46
24
  }
47
25
  };
48
- const readOptionalString = (value) => (typeof value === "string" ? value : undefined);
49
- const readOptionalBoolean = (value) => value === true;
50
- const readOptionalStringArray = (value) => (Array.isArray(value) ? value : undefined);
26
+ const agentEnvironmentOption = () => Option.String("--environment,--env", {
27
+ 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)",
28
+ });
29
+ const agentEnvironmentNameOption = () => Option.String("--environment-name", {
30
+ 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)",
31
+ });
32
+ const throwCliUsageError = (error) => {
33
+ if (isAgentUsageError(error)) {
34
+ throw new UsageError(error.message);
35
+ }
36
+ throw error;
37
+ };
38
+ export class AgentCapabilitiesCommand extends Command {
39
+ constructor() {
40
+ super(...arguments);
41
+ this.json = Option.Boolean("--json", true, {
42
+ description: "Print capabilities as JSON (default: true)",
43
+ });
44
+ }
45
+ async execute() {
46
+ console.log(JSON.stringify(createAgentCapabilities(), null, 2));
47
+ }
48
+ }
49
+ AgentCapabilitiesCommand.paths = [["agent", "capabilities"]];
50
+ AgentCapabilitiesCommand.usage = Command.Usage({
51
+ description: "Print structured Allure agent capability information",
52
+ details: "This command prints the locally supported agent-mode commands, expectation controls, output files, rerun support, and known unsupported capability families as JSON.",
53
+ examples: [
54
+ ["agent capabilities", "Print agent capabilities as JSON"],
55
+ ["agent capabilities --json", "Print agent capabilities as JSON explicitly"],
56
+ ],
57
+ });
51
58
  export class AgentCommand extends Command {
52
59
  constructor() {
53
60
  super(...arguments);
@@ -63,8 +70,44 @@ export class AgentCommand extends Command {
63
70
  this.expectations = Option.String("--expectations", {
64
71
  description: "The path to a YAML or JSON expectations file",
65
72
  });
66
- this.environment = environmentOption();
67
- this.environmentName = environmentNameOption();
73
+ this.goal = Option.Array("--goal", {
74
+ description: "The review goal to record in inline agent expectations",
75
+ });
76
+ this.taskId = Option.Array("--task-id", {
77
+ description: "The task or feature id to record in inline agent expectations",
78
+ });
79
+ this.expectTests = Option.Array("--expect-tests", {
80
+ description: "The expected number of visible logical tests in the intended scope",
81
+ });
82
+ this.expectLabels = Option.Array("--expect-label", {
83
+ description: "Expected label selector in name=value form. Repeat the option for multiple selectors",
84
+ });
85
+ this.expectEnvironments = Option.Array("--expect-env", {
86
+ description: "Expected environment id. Repeat the option for multiple environments",
87
+ });
88
+ this.expectFullNames = Option.Array("--expect-test", {
89
+ description: "Expected full test name. Repeat the option for multiple tests",
90
+ });
91
+ this.expectPrefixes = Option.Array("--expect-prefix", {
92
+ description: "Expected full-name prefix. Repeat the option for multiple prefixes",
93
+ });
94
+ this.forbidLabels = Option.Array("--forbid-label", {
95
+ description: "Forbidden label selector in name=value form. Repeat the option for multiple selectors",
96
+ });
97
+ this.expectStepContains = Option.Array("--expect-step-containing", {
98
+ description: "Require a test-scoped step name containing this text per evidence-target logical test",
99
+ });
100
+ this.expectSteps = Option.Array("--expect-steps", {
101
+ description: "Require at least this many meaningful steps per expected logical test",
102
+ });
103
+ this.expectAttachments = Option.Array("--expect-attachments", {
104
+ description: "Require at least this many non-missing attachments per expected logical test",
105
+ });
106
+ this.expectAttachmentFilters = Option.Array("--expect-attachment", {
107
+ description: "Require a matching non-missing attachment per expected logical test. Use a file name or name=value/content-type=value",
108
+ });
109
+ this.environment = agentEnvironmentOption();
110
+ this.environmentName = agentEnvironmentNameOption();
68
111
  this.silent = Option.Boolean("--silent", {
69
112
  description: "Don't pipe the process output logs to console (default: false)",
70
113
  });
@@ -87,21 +130,80 @@ export class AgentCommand extends Command {
87
130
  }
88
131
  async execute() {
89
132
  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
- });
133
+ const configPath = readOptionalString(this.config);
134
+ const configuredCwd = readOptionalString(this.cwd);
135
+ const output = readOptionalString(this.output);
136
+ const expectations = readOptionalString(this.expectations);
137
+ if (!args || !args.length) {
138
+ throw new UsageError("expecting command to be specified after --, e.g. allure agent -- npm run test");
139
+ }
140
+ try {
141
+ const inlineExpectations = buildAgentInlineExpectations({
142
+ goal: this.goal,
143
+ taskId: this.taskId,
144
+ 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),
151
+ expectSteps: this.expectSteps,
152
+ expectAttachments: this.expectAttachments,
153
+ expectAttachmentFilters: readOptionalStringArray(this.expectAttachmentFilters),
154
+ });
155
+ if (expectations && inlineExpectations) {
156
+ throw new AgentExpectationUsageError("Use either --expectations <file> or inline expectation flags, not both", "--expectations");
157
+ }
158
+ await validateAgentExpectationsFile({
159
+ cwd: await realpath(configuredCwd ?? process.cwd()),
160
+ output,
161
+ expectations,
162
+ });
163
+ const { executeAgentMode } = await import("./agent-run.js");
164
+ await executeAgentMode({
165
+ configPath,
166
+ cwd: configuredCwd,
167
+ output,
168
+ expectations,
169
+ inlineExpectations: inlineExpectations,
170
+ environment: readOptionalString(this.environment),
171
+ environmentName: readOptionalString(this.environmentName),
172
+ silent: readOptionalBoolean(this.silent),
173
+ rerunFrom: readOptionalString(this.rerunFrom),
174
+ rerunLatest: readOptionalBoolean(this.rerunLatest),
175
+ rerunPreset: readOptionalString(this.rerunPreset),
176
+ rerunEnvironments: readOptionalStringArray(this.rerunEnvironments),
177
+ rerunLabels: readOptionalStringArray(this.rerunLabels),
178
+ args,
179
+ });
180
+ }
181
+ catch (error) {
182
+ if (!isAgentExpectationUsageError(error)) {
183
+ throwCliUsageError(error);
184
+ }
185
+ const expectationError = error;
186
+ const cwd = await realpath(configuredCwd ?? process.cwd());
187
+ const outputDir = output ? resolve(cwd, output) : await mkdtemp(join(tmpdir(), "allure-agent-"));
188
+ const commandString = formatAgentCommand(args);
189
+ const { generatedAt } = await writeInvalidAgentExpectationOutput({
190
+ outputDir,
191
+ command: commandString,
192
+ error: expectationError,
193
+ });
194
+ await persistLatestAgentState({
195
+ cwd,
196
+ outputDir,
197
+ command: commandString,
198
+ startedAt: generatedAt,
199
+ finishedAt: generatedAt,
200
+ status: "finished",
201
+ exitCode: 1,
202
+ });
203
+ printAgentOutputLinks(outputDir);
204
+ console.error(expectationError.message);
205
+ exit(1);
206
+ }
105
207
  }
106
208
  }
107
209
  AgentCommand.paths = [["agent"]];
@@ -139,16 +241,19 @@ export class AgentLatestCommand extends Command {
139
241
  exit(1);
140
242
  return;
141
243
  }
142
- console.log(latestState.outputDir);
244
+ printAgentOutputLinks(latestState.outputDir);
143
245
  }
144
246
  }
145
247
  AgentLatestCommand.paths = [["agent", "latest"]];
146
248
  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.",
249
+ 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.",
149
251
  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"],
252
+ ["agent latest", "Print the latest agent output directory and index path for the current project"],
253
+ [
254
+ "agent latest --cwd ./packages/cli",
255
+ "Print the latest agent output directory and index path for a specific project cwd",
256
+ ],
152
257
  ],
153
258
  });
154
259
  export class AgentStateDirCommand extends Command {
@@ -172,6 +277,95 @@ AgentStateDirCommand.usage = Command.Usage({
172
277
  ["agent state-dir --cwd ./packages/cli", "Print the resolved state directory for a specific project cwd"],
173
278
  ],
174
279
  });
280
+ export class AgentQueryCommand extends Command {
281
+ constructor() {
282
+ super(...arguments);
283
+ this.view = Option.String({
284
+ required: false,
285
+ name: "Query view: summary, tests, findings, or test (default: summary)",
286
+ });
287
+ this.cwd = Option.String("--cwd", {
288
+ description: "The project directory used to resolve --latest and relative paths (default: current working directory)",
289
+ });
290
+ this.from = Option.String("--from", {
291
+ description: "The prior agent output directory to query",
292
+ });
293
+ this.latest = Option.Boolean("--latest", {
294
+ description: "Use the latest recorded agent output for the current project cwd",
295
+ });
296
+ this.statuses = Option.Array("--status", {
297
+ description: "Filter tests by status: failed, broken, unknown, skipped, or passed. Repeat for multiple statuses",
298
+ });
299
+ this.environments = Option.Array("--environment", {
300
+ description: "Filter tests by environment id. Repeat the option for multiple environments",
301
+ });
302
+ this.labels = Option.Array("--label", {
303
+ description: "Filter tests by exact label name=value. Repeat the option for multiple filters",
304
+ });
305
+ this.severities = Option.Array("--severity", {
306
+ description: "Filter findings by severity: high, warning, or info. Repeat for multiple severities",
307
+ });
308
+ this.categories = Option.Array("--category", {
309
+ description: "Filter findings by category. Repeat the option for multiple categories",
310
+ });
311
+ this.checks = Option.Array("--check", {
312
+ description: "Filter findings by check name. Repeat the option for multiple checks",
313
+ });
314
+ this.test = Option.String("--test", {
315
+ description: "Filter to one test by full name, test result id, history id, or markdown path",
316
+ });
317
+ this.limit = Option.String("--limit", {
318
+ description: "Limit returned tests or findings to this non-negative count",
319
+ });
320
+ this.includeMarkdown = Option.Boolean("--include-markdown", {
321
+ description: "Include the per-test markdown content for the test view",
322
+ });
323
+ }
324
+ async execute() {
325
+ try {
326
+ const cwd = await realpath(readOptionalString(this.cwd) ?? process.cwd());
327
+ const view = normalizeAgentQueryView(readOptionalString(this.view));
328
+ const outputDir = await resolveAgentSelectionOutputDir({
329
+ cwd,
330
+ from: readOptionalString(this.from),
331
+ latest: readOptionalBoolean(this.latest),
332
+ });
333
+ const output = await loadAgentOutput(outputDir);
334
+ const payload = await buildAgentQueryPayload(output, view, {
335
+ environments: normalizeRepeatedStringValues(readOptionalStringArray(this.environments)),
336
+ labelFilters: parseAgentLabelFilters(readOptionalStringArray(this.labels)),
337
+ statuses: normalizeRepeatedEnumValues(readOptionalStringArray(this.statuses), AGENT_TEST_STATUSES, "--status"),
338
+ severities: normalizeRepeatedEnumValues(readOptionalStringArray(this.severities), AGENT_FINDING_SEVERITIES, "--severity"),
339
+ categories: normalizeRepeatedEnumValues(readOptionalStringArray(this.categories), AGENT_FINDING_CATEGORIES, "--category"),
340
+ checks: normalizeRepeatedStringValues(readOptionalStringArray(this.checks)),
341
+ test: readOptionalString(this.test),
342
+ limit: normalizeAgentQueryLimit(readOptionalString(this.limit)),
343
+ includeMarkdown: readOptionalBoolean(this.includeMarkdown),
344
+ });
345
+ console.log(JSON.stringify(payload, null, 2));
346
+ }
347
+ catch (error) {
348
+ throwCliUsageError(error);
349
+ }
350
+ }
351
+ }
352
+ AgentQueryCommand.paths = [["agent", "query"]];
353
+ AgentQueryCommand.usage = Command.Usage({
354
+ description: "Query an existing Allure agent output directory as focused JSON",
355
+ 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.",
356
+ examples: [
357
+ ["agent query --latest summary", "Print a summary for the latest agent output"],
358
+ ["agent query --from ./out/agent-output tests --status failed", "List failed tests from a prior output"],
359
+ [
360
+ "agent query --from ./out/agent-output findings --severity high",
361
+ "List high-severity findings from a prior output",
362
+ ],
363
+ [
364
+ 'agent query --latest test --test "suite should pass" --include-markdown',
365
+ "Print one test summary with its per-test markdown",
366
+ ],
367
+ ],
368
+ });
175
369
  export class AgentSelectCommand extends Command {
176
370
  constructor() {
177
371
  super(...arguments);
@@ -198,177 +392,52 @@ export class AgentSelectCommand extends Command {
198
392
  });
199
393
  }
200
394
  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;
395
+ try {
396
+ const cwd = await realpath(readOptionalString(this.cwd) ?? process.cwd());
397
+ const environments = readOptionalStringArray(this.environments);
398
+ const labels = readOptionalStringArray(this.labels);
399
+ const outputDir = await resolveAgentSelectionOutputDir({
400
+ cwd,
401
+ from: readOptionalString(this.from),
402
+ latest: readOptionalBoolean(this.latest),
403
+ });
404
+ const selection = await selectAgentTestPlan({
405
+ outputDir,
406
+ preset: normalizeAgentRerunPreset(readOptionalString(this.preset)),
407
+ environments: environments?.length ? environments : undefined,
408
+ labelFilters: parseAgentLabelFilters(labels),
409
+ });
410
+ if (!selection.testPlan.tests.length) {
411
+ console.error(`No tests matched selection in ${selection.outputDir}`);
412
+ exit(1);
413
+ return;
414
+ }
415
+ const serialized = `${JSON.stringify(selection.testPlan, null, 2)}\n`;
416
+ const output = readOptionalString(this.output);
417
+ if (!output) {
418
+ console.log(serialized.trimEnd());
419
+ return;
420
+ }
421
+ const outputPath = resolve(cwd, output);
422
+ await mkdir(dirname(outputPath), { recursive: true });
423
+ await writeFile(outputPath, serialized, "utf-8");
424
+ console.log(`agent testplan: ${outputPath}`);
425
+ console.log(`agent selection source: ${selection.outputDir}`);
426
+ console.log(`agent selection preset: ${selection.preset}`);
427
+ console.log(`agent selection tests: ${selection.selectedTests.length}`);
219
428
  }
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;
429
+ catch (error) {
430
+ throwCliUsageError(error);
225
431
  }
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
432
  }
231
433
  }
232
434
  AgentSelectCommand.paths = [["agent", "select"]];
233
435
  AgentSelectCommand.usage = Command.Usage({
234
436
  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.",
437
+ 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
438
  examples: [
237
439
  ["agent select --from ./out/agent-output", "Print a test plan for the default review-targeted tests"],
238
440
  ["agent select --latest --preset failed", "Print a test plan for failed tests from the latest project run"],
239
441
  ["agent select --from ./out/agent-output --output ./testplan.json", "Write the selected test plan to a file"],
240
442
  ],
241
443
  });
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
- };
@@ -1,6 +1,5 @@
1
1
  import * as console from "node:console";
2
2
  import { realpath, rm } from "node:fs/promises";
3
- import { resolve } from "node:path";
4
3
  import process, { exit } from "node:process";
5
4
  import { AllureReport, isFileNotFoundError, readConfig } from "@allurereport/core";
6
5
  import Awesome from "@allurereport/plugin-awesome";
@@ -9,7 +8,6 @@ import { Command, Option, UsageError } from "clipanion";
9
8
  import { red } from "yoctocolors";
10
9
  import { environmentNameOption, environmentOption, normalizeCommandEnvironmentOptions, resolveCommandEnvironment, } from "../utils/environment.js";
11
10
  import { createChildAllureCliEnvironment, getActiveAllureCliCommand } from "../utils/execution-context.js";
12
- import { executeAgentMode } from "./agent.js";
13
11
  import { executeAllureRun, executeNestedAllureCommand } from "./commons/run.js";
14
12
  export class RunCommand extends Command {
15
13
  constructor() {
@@ -65,22 +63,6 @@ export class RunCommand extends Command {
65
63
  if (!args || !args.length) {
66
64
  throw new UsageError("expecting command to be specified after --, e.g. allure run -- npm run test");
67
65
  }
68
- const legacyAgentOutput = process.env.ALLURE_AGENT_OUTPUT;
69
- if (legacyAgentOutput) {
70
- await executeAgentMode({
71
- configPath: this.config,
72
- cwd: this.cwd,
73
- output: resolve(process.cwd(), legacyAgentOutput),
74
- expectations: process.env.ALLURE_AGENT_EXPECTATIONS
75
- ? resolve(process.cwd(), process.env.ALLURE_AGENT_EXPECTATIONS)
76
- : undefined,
77
- environment: this.environment,
78
- environmentName: this.environmentName,
79
- silent: this.silent,
80
- args,
81
- });
82
- return;
83
- }
84
66
  const before = new Date().getTime();
85
67
  process.on("exit", (exitCode) => {
86
68
  const after = new Date().getTime();
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
- import { argv } from "node:process";
2
+ import process, { argv } from "node:process";
3
3
  import { Builtins, Cli } from "clipanion";
4
- import { AgentCommand, AgentLatestCommand, AgentSelectCommand, AgentStateDirCommand, Allure2Command, AwesomeCommand, CheckCommand, ClassicCommand, CsvCommand, DashboardCommand, GenerateCommand, HistoryCommand, JiraClearCommand, KnownIssueCommand, LogCommand, OpenCommand, QualityGateCommand, ResultsPackCommand, ResultsUnpackCommand, RunCommand, SlackCommand, TestPlanCommand, WatchCommand, } from "./commands/index.js";
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";
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({
@@ -11,7 +11,9 @@ const cli = new Cli({
11
11
  });
12
12
  cli.register(AwesomeCommand);
13
13
  cli.register(Allure2Command);
14
+ cli.register(AgentCapabilitiesCommand);
14
15
  cli.register(AgentLatestCommand);
16
+ cli.register(AgentQueryCommand);
15
17
  cli.register(AgentSelectCommand);
16
18
  cli.register(AgentStateDirCommand);
17
19
  cli.register(AgentCommand);
@@ -34,6 +36,17 @@ cli.register(ResultsPackCommand);
34
36
  cli.register(ResultsUnpackCommand);
35
37
  cli.register(Builtins.HelpCommand);
36
38
  cli.register(Builtins.VersionCommand);
37
- cli.runExit(args);
39
+ void cli
40
+ .run(args)
41
+ .then((exitCode) => {
42
+ if (exitCode === 0 && isAgentTaskMapHelpRequest(args)) {
43
+ process.stdout.write(`\n${AGENT_TASK_MAP_HELP}`);
44
+ }
45
+ process.exitCode = exitCode;
46
+ })
47
+ .catch((error) => {
48
+ console.error(error);
49
+ process.exitCode = 1;
50
+ });
38
51
  export { defineConfig } from "@allurereport/plugin-api";
39
52
  export { defaultChartsConfig } from "@allurereport/charts-api";
@@ -2,6 +2,4 @@ export * from "./process.js";
2
2
  export * from "./terminal.js";
3
3
  export * from "./logs.js";
4
4
  export * from "./execution-context.js";
5
- export * from "./agent-state.js";
6
- export * from "./agent-select.js";
7
5
  export * from "./fileSystem.js";
@@ -2,6 +2,4 @@ export * from "./process.js";
2
2
  export * from "./terminal.js";
3
3
  export * from "./logs.js";
4
4
  export * from "./execution-context.js";
5
- export * from "./agent-state.js";
6
- export * from "./agent-select.js";
7
5
  export * from "./fileSystem.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "allure",
3
- "version": "3.10.0",
3
+ "version": "3.11.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.10.0",
36
- "@allurereport/ci": "3.10.0",
37
- "@allurereport/core": "3.10.0",
38
- "@allurereport/core-api": "3.10.0",
39
- "@allurereport/directory-watcher": "3.10.0",
40
- "@allurereport/plugin-agent": "3.10.0",
41
- "@allurereport/plugin-allure2": "3.10.0",
42
- "@allurereport/plugin-api": "3.10.0",
43
- "@allurereport/plugin-awesome": "3.10.0",
44
- "@allurereport/plugin-classic": "3.10.0",
45
- "@allurereport/plugin-csv": "3.10.0",
46
- "@allurereport/plugin-dashboard": "3.10.0",
47
- "@allurereport/plugin-jira": "3.10.0",
48
- "@allurereport/plugin-log": "3.10.0",
49
- "@allurereport/plugin-progress": "3.10.0",
50
- "@allurereport/plugin-server-reload": "3.10.0",
51
- "@allurereport/plugin-slack": "3.10.0",
52
- "@allurereport/reader-api": "3.10.0",
53
- "@allurereport/service": "3.10.0",
54
- "@allurereport/static-server": "3.10.0",
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",
55
55
  "adm-zip": "^0.5.16",
56
56
  "clipanion": "^4.0.0-rc.4",
57
57
  "glob": "^13.0.6",
@@ -1,41 +0,0 @@
1
- import type { TestPlan } from "@allurereport/core-api";
2
- import { type AgentTestManifestLine } from "@allurereport/plugin-agent";
3
- export type AgentRerunPreset = "review" | "failed" | "unsuccessful" | "all";
4
- export type AgentLabelFilter = {
5
- name: string;
6
- value: string;
7
- };
8
- export type AgentSelectionResult = {
9
- outputDir: string;
10
- preset: AgentRerunPreset;
11
- selectedTests: AgentTestManifestLine[];
12
- testPlan: TestPlan;
13
- };
14
- export type AgentTestPlanContext = {
15
- outputDir: string;
16
- preset: AgentRerunPreset;
17
- selectedCount: number;
18
- testPlanPath: string;
19
- cleanup: () => Promise<void>;
20
- };
21
- export declare const normalizeAgentRerunPreset: (value?: string) => AgentRerunPreset;
22
- export declare const parseAgentLabelFilters: (values?: string[]) => AgentLabelFilter[];
23
- export declare const resolveAgentSelectionOutputDir: (params: {
24
- cwd: string;
25
- from?: string;
26
- latest?: boolean;
27
- }) => Promise<string>;
28
- export declare const selectAgentTestPlan: (params: {
29
- outputDir: string;
30
- preset?: AgentRerunPreset;
31
- environments?: string[];
32
- labelFilters?: AgentLabelFilter[];
33
- }) => Promise<AgentSelectionResult>;
34
- export declare const createAgentTestPlanContext: (params: {
35
- cwd: string;
36
- from?: string;
37
- latest?: boolean;
38
- preset?: AgentRerunPreset;
39
- environments?: string[];
40
- labelFilters?: AgentLabelFilter[];
41
- }) => Promise<AgentTestPlanContext | undefined>;
@@ -1,141 +0,0 @@
1
- import { mkdtemp, rm, writeFile } from "node:fs/promises";
2
- import { tmpdir } from "node:os";
3
- import { join, resolve } from "node:path";
4
- import { loadAgentOutput, planAgentEnrichmentReview, } from "@allurereport/plugin-agent";
5
- import { UsageError } from "clipanion";
6
- import { readLatestAgentState } from "./agent-state.js";
7
- const AGENT_RERUN_PRESETS = ["review", "failed", "unsuccessful", "all"];
8
- const ALLURE_ID_LABEL = "ALLURE_ID";
9
- const isAgentRerunPreset = (value) => AGENT_RERUN_PRESETS.includes(value);
10
- const readAllureId = (labels) => labels.find((label) => label.name === ALLURE_ID_LABEL)?.value;
11
- const matchesLabelFilters = (labels, filters) => filters.every((filter) => labels.some((label) => label.name === filter.name && label.value === filter.value));
12
- const buildTestPlan = (tests) => {
13
- const seenSelectors = new Set();
14
- const seenIds = new Set();
15
- const selected = [];
16
- for (const test of tests) {
17
- const selector = test.full_name || undefined;
18
- const id = readAllureId(test.labels);
19
- if (selector && !seenSelectors.has(selector)) {
20
- seenSelectors.add(selector);
21
- if (id) {
22
- seenIds.add(id);
23
- }
24
- selected.push({
25
- selector,
26
- ...(id ? { id } : {}),
27
- });
28
- continue;
29
- }
30
- if (id && !seenIds.has(id)) {
31
- seenIds.add(id);
32
- selected.push({ id });
33
- }
34
- }
35
- return {
36
- version: "1.0",
37
- tests: selected,
38
- };
39
- };
40
- const selectTestsByPreset = (output, preset) => {
41
- switch (preset) {
42
- case "review": {
43
- const review = planAgentEnrichmentReview(output);
44
- const targeted = new Set(review.rerun.targetedTests);
45
- return output.tests.filter((test) => targeted.has(test.full_name));
46
- }
47
- case "failed":
48
- return output.tests.filter((test) => test.status === "failed" || test.status === "broken");
49
- case "unsuccessful":
50
- return output.tests.filter((test) => test.status !== "passed");
51
- case "all":
52
- return output.tests;
53
- }
54
- };
55
- export const normalizeAgentRerunPreset = (value) => {
56
- if (!value) {
57
- return "review";
58
- }
59
- const normalized = value.trim().toLowerCase();
60
- if (!isAgentRerunPreset(normalized)) {
61
- throw new UsageError(`Invalid rerun preset ${JSON.stringify(value)}. Expected one of: ${AGENT_RERUN_PRESETS.join(", ")}`);
62
- }
63
- return normalized;
64
- };
65
- export const parseAgentLabelFilters = (values) => (values ?? []).map((value) => {
66
- const separatorIndex = value.indexOf("=");
67
- if (separatorIndex <= 0 || separatorIndex === value.length - 1) {
68
- throw new UsageError(`Invalid label filter ${JSON.stringify(value)}. Expected the form name=value, for example feature=checkout`);
69
- }
70
- const name = value.slice(0, separatorIndex).trim();
71
- const filterValue = value.slice(separatorIndex + 1).trim();
72
- if (!name || !filterValue) {
73
- throw new UsageError(`Invalid label filter ${JSON.stringify(value)}. Expected the form name=value, for example feature=checkout`);
74
- }
75
- return {
76
- name,
77
- value: filterValue,
78
- };
79
- });
80
- export const resolveAgentSelectionOutputDir = async (params) => {
81
- const { cwd, from, latest } = params;
82
- if (from && latest) {
83
- throw new UsageError("Use either --from or --latest, not both");
84
- }
85
- if (!from && !latest) {
86
- throw new UsageError("Expected either --from <path> or --latest");
87
- }
88
- if (from) {
89
- return resolve(cwd, from);
90
- }
91
- const latestState = await readLatestAgentState(cwd);
92
- if (!latestState) {
93
- throw new UsageError(`No latest agent output found for ${cwd}`);
94
- }
95
- return latestState.outputDir;
96
- };
97
- export const selectAgentTestPlan = async (params) => {
98
- const preset = params.preset ?? "review";
99
- const output = await loadAgentOutput(params.outputDir);
100
- const selectedTests = selectTestsByPreset(output, preset)
101
- .filter((test) => (params.environments?.length ? params.environments.includes(test.environment_id) : true))
102
- .filter((test) => (params.labelFilters?.length ? matchesLabelFilters(test.labels, params.labelFilters) : true));
103
- return {
104
- outputDir: output.outputDir,
105
- preset,
106
- selectedTests,
107
- testPlan: buildTestPlan(selectedTests),
108
- };
109
- };
110
- export const createAgentTestPlanContext = async (params) => {
111
- const { from, latest } = params;
112
- if (!from && !latest) {
113
- return undefined;
114
- }
115
- const outputDir = await resolveAgentSelectionOutputDir({
116
- cwd: params.cwd,
117
- from,
118
- latest,
119
- });
120
- const selection = await selectAgentTestPlan({
121
- outputDir,
122
- preset: params.preset,
123
- environments: params.environments,
124
- labelFilters: params.labelFilters,
125
- });
126
- if (!selection.testPlan.tests.length) {
127
- throw new UsageError(`No tests matched rerun selection in ${selection.outputDir}. Adjust the preset or filters before rerunning.`);
128
- }
129
- const tempDir = await mkdtemp(join(tmpdir(), "allure-agent-select-"));
130
- const testPlanPath = join(tempDir, "testplan.json");
131
- await writeFile(testPlanPath, `${JSON.stringify(selection.testPlan, null, 2)}\n`, "utf-8");
132
- return {
133
- outputDir: selection.outputDir,
134
- preset: selection.preset,
135
- selectedCount: selection.testPlan.tests.length,
136
- testPlanPath,
137
- cleanup: async () => {
138
- await rm(tempDir, { recursive: true, force: true });
139
- },
140
- };
141
- };
@@ -1,15 +0,0 @@
1
- export type AgentLatestState = {
2
- schema: "allure-agent-latest/v1";
3
- cwd: string;
4
- outputDir: string;
5
- expectationsPath?: string;
6
- command: string;
7
- startedAt: string;
8
- finishedAt?: string;
9
- status: "running" | "finished";
10
- exitCode?: number | null;
11
- };
12
- export declare const ALLURE_AGENT_STATE_DIR_ENV = "ALLURE_AGENT_STATE_DIR";
13
- export declare const resolveAgentStateDir: (cwd: string) => string;
14
- export declare const writeLatestAgentState: (value: Omit<AgentLatestState, "schema">) => Promise<AgentLatestState>;
15
- export declare const readLatestAgentState: (cwd: string) => Promise<AgentLatestState | undefined>;
@@ -1,83 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
4
- import { dirname, join, resolve } from "node:path";
5
- const AGENT_STATE_SCHEMA = "allure-agent-latest/v1";
6
- export const ALLURE_AGENT_STATE_DIR_ENV = "ALLURE_AGENT_STATE_DIR";
7
- const isFileNotFoundError = (error) => typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
8
- const projectHash = (cwd) => createHash("sha256").update(cwd).digest("hex").slice(0, 16);
9
- export const resolveAgentStateDir = (cwd) => {
10
- const configuredDir = process.env[ALLURE_AGENT_STATE_DIR_ENV]?.trim();
11
- if (configuredDir) {
12
- return resolve(configuredDir);
13
- }
14
- return join(tmpdir(), `allure-agent-state-${projectHash(resolve(cwd))}`);
15
- };
16
- const projectStatePath = (cwd) => join(resolveAgentStateDir(cwd), "latest.json");
17
- const writeJsonAtomic = async (filePath, value) => {
18
- await mkdir(dirname(filePath), { recursive: true });
19
- const tempPath = `${filePath}.${process.pid}.tmp`;
20
- await writeFile(tempPath, `${JSON.stringify(value, null, 2)}\n`, "utf-8");
21
- await rename(tempPath, filePath);
22
- };
23
- const isAgentLatestState = (value) => {
24
- if (typeof value !== "object" || value === null) {
25
- return false;
26
- }
27
- const candidate = value;
28
- return (candidate.schema === AGENT_STATE_SCHEMA &&
29
- typeof candidate.cwd === "string" &&
30
- typeof candidate.outputDir === "string" &&
31
- typeof candidate.command === "string" &&
32
- typeof candidate.startedAt === "string" &&
33
- (candidate.expectationsPath === undefined || typeof candidate.expectationsPath === "string") &&
34
- (candidate.finishedAt === undefined || typeof candidate.finishedAt === "string") &&
35
- (candidate.status === "running" || candidate.status === "finished") &&
36
- (candidate.exitCode === undefined || typeof candidate.exitCode === "number" || candidate.exitCode === null));
37
- };
38
- export const writeLatestAgentState = async (value) => {
39
- const normalizedState = {
40
- schema: AGENT_STATE_SCHEMA,
41
- cwd: resolve(value.cwd),
42
- outputDir: resolve(value.outputDir),
43
- expectationsPath: value.expectationsPath ? resolve(value.expectationsPath) : undefined,
44
- command: value.command,
45
- startedAt: value.startedAt,
46
- finishedAt: value.finishedAt,
47
- status: value.status,
48
- exitCode: value.exitCode,
49
- };
50
- await writeJsonAtomic(projectStatePath(normalizedState.cwd), normalizedState);
51
- return normalizedState;
52
- };
53
- export const readLatestAgentState = async (cwd) => {
54
- const normalizedCwd = resolve(cwd);
55
- const statePath = projectStatePath(normalizedCwd);
56
- let raw;
57
- try {
58
- raw = await readFile(statePath, "utf-8");
59
- }
60
- catch (error) {
61
- if (isFileNotFoundError(error)) {
62
- return undefined;
63
- }
64
- throw error;
65
- }
66
- const parsed = JSON.parse(raw);
67
- if (!isAgentLatestState(parsed)) {
68
- throw new Error(`Invalid latest agent state in ${statePath}`);
69
- }
70
- if (parsed.cwd !== normalizedCwd) {
71
- return undefined;
72
- }
73
- try {
74
- await stat(parsed.outputDir);
75
- }
76
- catch (error) {
77
- if (isFileNotFoundError(error)) {
78
- return undefined;
79
- }
80
- throw error;
81
- }
82
- return parsed;
83
- };