@allurereport/plugin-agent 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.
@@ -0,0 +1,193 @@
1
+ import type { TestStatus } from "@allurereport/core-api";
2
+ import type { AgentFindingCategory, AgentFindingSeverity, AgentOutputBundle, AgentTestManifestLine } from "./harness.js";
3
+ import type { AgentLabelFilter } from "./selection.js";
4
+ export declare const AGENT_QUERY_SCHEMA = "allure-agent-query/v1";
5
+ export declare const AGENT_QUERY_VIEWS: readonly ["summary", "tests", "findings", "test"];
6
+ export declare const AGENT_TEST_STATUSES: TestStatus[];
7
+ export declare const AGENT_FINDING_SEVERITIES: AgentFindingSeverity[];
8
+ export declare const AGENT_FINDING_CATEGORIES: AgentFindingCategory[];
9
+ export type AgentQueryView = (typeof AGENT_QUERY_VIEWS)[number];
10
+ export type AgentQueryFilters = {
11
+ environments?: string[];
12
+ labelFilters: AgentLabelFilter[];
13
+ statuses?: TestStatus[];
14
+ severities?: AgentFindingSeverity[];
15
+ categories?: AgentFindingCategory[];
16
+ checks?: string[];
17
+ test?: string;
18
+ limit?: number;
19
+ includeMarkdown?: boolean;
20
+ };
21
+ export declare const normalizeAgentQueryView: (value?: string) => AgentQueryView;
22
+ export declare const normalizeRepeatedEnumValues: <T extends string>(values: string[] | undefined, allowed: readonly T[], optionName: string) => T[] | undefined;
23
+ export declare const normalizeRepeatedStringValues: (values: string[] | undefined) => string[] | undefined;
24
+ export declare const normalizeAgentQueryLimit: (value?: string) => number | undefined;
25
+ export declare const buildAgentQueryPayload: (output: AgentOutputBundle, view: AgentQueryView, filters: AgentQueryFilters) => Promise<{
26
+ expected?: import("./harness.js").AgentExpectations | undefined;
27
+ schema: string;
28
+ view: string;
29
+ output_dir: string;
30
+ index_md: string | null;
31
+ run: {
32
+ schema_version: string;
33
+ generated_at: string;
34
+ phase: "running" | "done" | null;
35
+ command: string | null;
36
+ exit_code: {
37
+ original: number;
38
+ actual: number | null;
39
+ } | null;
40
+ expectations_present: boolean;
41
+ expectation_result: {
42
+ schema_version: "allure-agent-expectation-result/v1";
43
+ status: import("./harness.js").AgentExpectationResultStatus;
44
+ impact: import("./harness.js").AgentExpectationResultImpact;
45
+ source: {
46
+ kind: "inline" | "file" | "none";
47
+ path: string | null;
48
+ };
49
+ recognized_control_count: number;
50
+ unsupported_controls: string[];
51
+ degraded_controls: string[];
52
+ summary: {
53
+ expected_tests: number;
54
+ observed_tests: number;
55
+ missing_expected: number;
56
+ forbidden_observed: number;
57
+ unexpected_observed: number;
58
+ evidence_mismatches: number;
59
+ };
60
+ finding_ids: string[];
61
+ };
62
+ agent_context: {
63
+ agent_name: string | null;
64
+ loop_id: string | null;
65
+ task_id: string | null;
66
+ conversation_id: string | null;
67
+ };
68
+ };
69
+ summary: {
70
+ stats: import("@allurereport/core-api").Statistic;
71
+ modeled_stats?: {
72
+ total: number;
73
+ failed: number;
74
+ broken: number;
75
+ skipped: number;
76
+ unknown: number;
77
+ passed: number;
78
+ };
79
+ unmodeled_from_stats?: {
80
+ total: number;
81
+ failed: number;
82
+ broken: number;
83
+ skipped: number;
84
+ unknown: number;
85
+ passed: number;
86
+ };
87
+ compact?: {
88
+ visible_results: number;
89
+ logical_tests: number;
90
+ unmodeled_visible_results: number;
91
+ runner_failures_outside_logical_tests: number;
92
+ completeness: "complete" | "partial";
93
+ findings?: number;
94
+ };
95
+ duration_ms: {
96
+ total: number;
97
+ average: number;
98
+ max: number;
99
+ };
100
+ environments: Array<{
101
+ environmentId: string;
102
+ total: number;
103
+ failed: number;
104
+ broken: number;
105
+ skipped: number;
106
+ unknown: number;
107
+ passed: number;
108
+ }>;
109
+ };
110
+ modeling: {
111
+ completeness: "complete" | "partial";
112
+ reasons: string[];
113
+ modeledStats: {
114
+ total: number;
115
+ failed: number;
116
+ broken: number;
117
+ skipped: number;
118
+ unknown: number;
119
+ passed: number;
120
+ };
121
+ unmodeledFromStats: {
122
+ total: number;
123
+ failed: number;
124
+ broken: number;
125
+ skipped: number;
126
+ unknown: number;
127
+ passed: number;
128
+ };
129
+ runnerFailures: {
130
+ total: number;
131
+ globalErrors: number;
132
+ stderrActionable: number;
133
+ samples: Array<{
134
+ source: "stderr" | "global_error";
135
+ kind: "import" | "suite-load" | "setup" | "global-error";
136
+ message: string;
137
+ count: number;
138
+ }>;
139
+ };
140
+ stderr: {
141
+ actionableCount: number;
142
+ actionableSamples: string[];
143
+ noisyWarningCount: number;
144
+ noisyWarningSamples: string[];
145
+ };
146
+ compact: {
147
+ visible_results: number;
148
+ logical_tests: number;
149
+ unmodeled_visible_results: number;
150
+ runner_failures_outside_logical_tests: number;
151
+ completeness: "complete" | "partial";
152
+ };
153
+ } | null;
154
+ check_summary: {
155
+ total: number;
156
+ countsBySeverity: Record<AgentFindingSeverity, number>;
157
+ countsByCategory: Record<AgentFindingCategory, number>;
158
+ };
159
+ paths: {
160
+ index_md: string | null;
161
+ agents_md: string | null;
162
+ tests_manifest: string | null;
163
+ findings_manifest: string | null;
164
+ test_events_manifest: string | null;
165
+ expected_manifest: string | null;
166
+ process_logs: {
167
+ stdout: string | null;
168
+ stderr: string | null;
169
+ };
170
+ };
171
+ } | {
172
+ schema: string;
173
+ view: string;
174
+ output_dir: string;
175
+ total_matches: number;
176
+ returned: number;
177
+ tests: AgentTestManifestLine[];
178
+ } | {
179
+ schema: string;
180
+ view: string;
181
+ output_dir: string;
182
+ total_matches: number;
183
+ returned: number;
184
+ findings: import("./harness.js").AgentFindingManifestLine[];
185
+ } | {
186
+ markdown?: string | undefined;
187
+ schema: string;
188
+ view: string;
189
+ output_dir: string;
190
+ markdown_path: string | null;
191
+ test: AgentTestManifestLine;
192
+ findings: import("./harness.js").AgentFindingManifestLine[];
193
+ }>;
package/dist/query.js ADDED
@@ -0,0 +1,175 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { AgentUsageError } from "./errors.js";
4
+ export const AGENT_QUERY_SCHEMA = "allure-agent-query/v1";
5
+ export const AGENT_QUERY_VIEWS = ["summary", "tests", "findings", "test"];
6
+ export const AGENT_TEST_STATUSES = ["failed", "broken", "unknown", "skipped", "passed"];
7
+ export const AGENT_FINDING_SEVERITIES = ["high", "warning", "info"];
8
+ export const AGENT_FINDING_CATEGORIES = [
9
+ "bootstrap",
10
+ "scope",
11
+ "metadata",
12
+ "evidence",
13
+ "smells",
14
+ ];
15
+ export const normalizeAgentQueryView = (value) => {
16
+ if (!value) {
17
+ return "summary";
18
+ }
19
+ const normalized = value.trim().toLowerCase();
20
+ if (!AGENT_QUERY_VIEWS.includes(normalized)) {
21
+ throw new AgentUsageError(`Invalid query view ${JSON.stringify(value)}. Expected one of: ${AGENT_QUERY_VIEWS.join(", ")}`);
22
+ }
23
+ return normalized;
24
+ };
25
+ const normalizeOptionalStringValues = (values) => values?.map((value) => value.trim()).filter(Boolean) ?? [];
26
+ export const normalizeRepeatedEnumValues = (values, allowed, optionName) => {
27
+ const normalized = normalizeOptionalStringValues(values).map((value) => value.toLowerCase());
28
+ if (!normalized.length) {
29
+ return undefined;
30
+ }
31
+ const invalid = normalized.find((value) => !allowed.includes(value));
32
+ if (invalid) {
33
+ throw new AgentUsageError(`Invalid ${optionName} value ${JSON.stringify(invalid)}. Expected one of: ${allowed.join(", ")}`);
34
+ }
35
+ return normalized;
36
+ };
37
+ export const normalizeRepeatedStringValues = (values) => {
38
+ const normalized = normalizeOptionalStringValues(values);
39
+ return normalized.length ? normalized : undefined;
40
+ };
41
+ export const normalizeAgentQueryLimit = (value) => {
42
+ if (value === undefined) {
43
+ return undefined;
44
+ }
45
+ if (!/^\d+$/.test(value)) {
46
+ throw new AgentUsageError("--limit must be a non-negative integer");
47
+ }
48
+ const parsed = Number(value);
49
+ if (!Number.isSafeInteger(parsed)) {
50
+ throw new AgentUsageError("--limit must be a non-negative integer");
51
+ }
52
+ return parsed;
53
+ };
54
+ const matchesLabelFilters = (labels, filters) => filters.every((filter) => labels.some((label) => label.name === filter.name && label.value === filter.value));
55
+ const matchesAgentTestIdentifier = (test, identifier) => test.full_name === identifier ||
56
+ test.test_result_id === identifier ||
57
+ test.history_id === identifier ||
58
+ test.markdown_path === identifier;
59
+ const agentFindingSubjectRef = (finding) => {
60
+ if (finding.subject_ref) {
61
+ return finding.subject_ref;
62
+ }
63
+ if (typeof finding.subject === "string") {
64
+ return finding.subject;
65
+ }
66
+ return finding.subject.path ?? finding.subject.id ?? finding.subject.type;
67
+ };
68
+ const agentFindingCheckName = (finding) => finding.check_id ?? finding.check_name;
69
+ const filterAgentQueryTests = (tests, filters) => tests
70
+ .filter((test) => (filters.statuses?.length ? filters.statuses.includes(test.status) : true))
71
+ .filter((test) => (filters.environments?.length ? filters.environments.includes(test.environment_id) : true))
72
+ .filter((test) => (filters.labelFilters.length ? matchesLabelFilters(test.labels, filters.labelFilters) : true))
73
+ .filter((test) => (filters.test ? matchesAgentTestIdentifier(test, filters.test) : true));
74
+ const hasAgentQueryTestFilters = (filters) => Boolean(filters.statuses?.length || filters.environments?.length || filters.labelFilters.length || filters.test);
75
+ const filterAgentQueryFindings = (output, filters) => {
76
+ const matchedSubjects = hasAgentQueryTestFilters(filters)
77
+ ? new Set(filterAgentQueryTests(output.tests, filters).map((test) => test.markdown_path))
78
+ : undefined;
79
+ return output.findings
80
+ .filter((finding) => (matchedSubjects ? matchedSubjects.has(agentFindingSubjectRef(finding)) : true))
81
+ .filter((finding) => (filters.severities?.length ? filters.severities.includes(finding.severity) : true))
82
+ .filter((finding) => (filters.categories?.length ? filters.categories.includes(finding.category) : true))
83
+ .filter((finding) => (filters.checks?.length ? filters.checks.includes(agentFindingCheckName(finding)) : true));
84
+ };
85
+ const applyAgentQueryLimit = (items, limit) => limit === undefined ? items : items.slice(0, limit);
86
+ const resolveAgentOutputPath = (output, relativePath) => relativePath ? join(output.outputDir, relativePath) : null;
87
+ const buildAgentQuerySummaryPayload = (output) => ({
88
+ schema: AGENT_QUERY_SCHEMA,
89
+ view: "summary",
90
+ output_dir: output.outputDir,
91
+ index_md: resolveAgentOutputPath(output, output.run.paths.index_md),
92
+ run: {
93
+ schema_version: output.run.schema_version,
94
+ generated_at: output.run.generated_at,
95
+ phase: output.run.phase ?? null,
96
+ command: output.run.command,
97
+ exit_code: output.run.exit_code,
98
+ expectations_present: output.run.expectations_present,
99
+ expectation_result: output.run.expectation_result,
100
+ agent_context: output.run.agent_context,
101
+ },
102
+ summary: output.run.summary,
103
+ modeling: output.run.modeling ?? null,
104
+ check_summary: output.run.check_summary,
105
+ paths: {
106
+ index_md: resolveAgentOutputPath(output, output.run.paths.index_md),
107
+ agents_md: resolveAgentOutputPath(output, output.run.paths.agents_md),
108
+ tests_manifest: resolveAgentOutputPath(output, output.run.paths.tests_manifest),
109
+ findings_manifest: resolveAgentOutputPath(output, output.run.paths.findings_manifest),
110
+ test_events_manifest: resolveAgentOutputPath(output, output.run.paths.test_events_manifest),
111
+ expected_manifest: resolveAgentOutputPath(output, output.run.paths.expected_manifest),
112
+ process_logs: {
113
+ stdout: resolveAgentOutputPath(output, output.run.paths.process_logs.stdout),
114
+ stderr: resolveAgentOutputPath(output, output.run.paths.process_logs.stderr),
115
+ },
116
+ },
117
+ ...(output.expected ? { expected: output.expected } : {}),
118
+ });
119
+ const buildAgentQueryTestsPayload = (output, filters) => {
120
+ const matched = filterAgentQueryTests(output.tests, filters);
121
+ const returned = applyAgentQueryLimit(matched, filters.limit);
122
+ return {
123
+ schema: AGENT_QUERY_SCHEMA,
124
+ view: "tests",
125
+ output_dir: output.outputDir,
126
+ total_matches: matched.length,
127
+ returned: returned.length,
128
+ tests: returned,
129
+ };
130
+ };
131
+ const buildAgentQueryFindingsPayload = (output, filters) => {
132
+ const matched = filterAgentQueryFindings(output, filters);
133
+ const returned = applyAgentQueryLimit(matched, filters.limit);
134
+ return {
135
+ schema: AGENT_QUERY_SCHEMA,
136
+ view: "findings",
137
+ output_dir: output.outputDir,
138
+ total_matches: matched.length,
139
+ returned: returned.length,
140
+ findings: returned,
141
+ };
142
+ };
143
+ const buildAgentQueryTestPayload = async (output, filters) => {
144
+ const matched = filterAgentQueryTests(output.tests, filters);
145
+ if (!matched.length) {
146
+ throw new AgentUsageError(`No tests matched query in ${output.outputDir}`);
147
+ }
148
+ if (matched.length > 1) {
149
+ throw new AgentUsageError(`Query matched ${matched.length} tests in ${output.outputDir}. Use --test <full-name-or-id>.`);
150
+ }
151
+ const test = matched[0];
152
+ const markdownPath = resolveAgentOutputPath(output, test.markdown_path);
153
+ const findings = output.findings.filter((finding) => agentFindingSubjectRef(finding) === test.markdown_path);
154
+ return {
155
+ schema: AGENT_QUERY_SCHEMA,
156
+ view: "test",
157
+ output_dir: output.outputDir,
158
+ markdown_path: markdownPath,
159
+ test,
160
+ findings,
161
+ ...(filters.includeMarkdown && markdownPath ? { markdown: await readFile(markdownPath, "utf-8") } : {}),
162
+ };
163
+ };
164
+ export const buildAgentQueryPayload = async (output, view, filters) => {
165
+ switch (view) {
166
+ case "summary":
167
+ return buildAgentQuerySummaryPayload(output);
168
+ case "tests":
169
+ return buildAgentQueryTestsPayload(output, filters);
170
+ case "findings":
171
+ return buildAgentQueryFindingsPayload(output, filters);
172
+ case "test":
173
+ return buildAgentQueryTestPayload(output, filters);
174
+ }
175
+ };
@@ -0,0 +1,42 @@
1
+ import type { TestPlan } from "@allurereport/core-api";
2
+ import { type AgentTestManifestLine } from "./harness.js";
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 AGENT_RERUN_PRESETS: AgentRerunPreset[];
22
+ export declare const normalizeAgentRerunPreset: (value?: string) => AgentRerunPreset;
23
+ export declare const parseAgentLabelFilters: (values?: string[]) => AgentLabelFilter[];
24
+ export declare const resolveAgentSelectionOutputDir: (params: {
25
+ cwd: string;
26
+ from?: string;
27
+ latest?: boolean;
28
+ }) => Promise<string>;
29
+ export declare const selectAgentTestPlan: (params: {
30
+ outputDir: string;
31
+ preset?: AgentRerunPreset;
32
+ environments?: string[];
33
+ labelFilters?: AgentLabelFilter[];
34
+ }) => Promise<AgentSelectionResult>;
35
+ export declare const createAgentTestPlanContext: (params: {
36
+ cwd: string;
37
+ from?: string;
38
+ latest?: boolean;
39
+ preset?: AgentRerunPreset;
40
+ environments?: string[];
41
+ labelFilters?: AgentLabelFilter[];
42
+ }) => Promise<AgentTestPlanContext | undefined>;
@@ -0,0 +1,141 @@
1
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join, resolve } from "node:path";
4
+ import { AgentUsageError } from "./errors.js";
5
+ import { loadAgentOutput, planAgentEnrichmentReview, } from "./harness.js";
6
+ import { readLatestAgentState } from "./state.js";
7
+ export 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 AgentUsageError(`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 AgentUsageError(`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 AgentUsageError(`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 AgentUsageError("Use either --from or --latest, not both");
84
+ }
85
+ if (!from && !latest) {
86
+ throw new AgentUsageError("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 AgentUsageError(`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 AgentUsageError(`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
+ };
@@ -0,0 +1,15 @@
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>;
package/dist/state.js ADDED
@@ -0,0 +1,83 @@
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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@allurereport/plugin-agent",
3
- "version": "3.10.0",
3
+ "version": "3.11.0",
4
4
  "description": "Allure Agent Plugin – AI-friendly markdown report generator",
5
5
  "keywords": [
6
6
  "agent",
@@ -27,16 +27,16 @@
27
27
  "build": "run clean && tsc --project ./tsconfig.json",
28
28
  "clean": "rimraf ./dist",
29
29
  "test": "rimraf ./out && vitest run",
30
- "lint": "oxlint --import-plugin src test features stories",
31
- "lint:fix": "oxlint --import-plugin --fix src test features stories"
30
+ "lint": "yarn run -T oxlint --import-plugin src test features stories",
31
+ "lint:fix": "yarn run -T oxlint --import-plugin --fix src test features stories"
32
32
  },
33
33
  "dependencies": {
34
- "@allurereport/core-api": "3.10.0",
35
- "@allurereport/plugin-api": "3.10.0",
34
+ "@allurereport/core-api": "3.11.0",
35
+ "@allurereport/plugin-api": "3.11.0",
36
36
  "yaml": "^2.8.1"
37
37
  },
38
38
  "devDependencies": {
39
- "@allurereport/reader-api": "3.10.0",
39
+ "@allurereport/reader-api": "3.11.0",
40
40
  "@types/node": "^20",
41
41
  "@vitest/runner": "^2",
42
42
  "allure-js-commons": "^3",