@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.
- package/README.md +85 -79
- package/dist/capabilities.d.ts +99 -0
- package/dist/capabilities.js +173 -0
- package/dist/errors.d.ts +9 -0
- package/dist/errors.js +15 -0
- package/dist/guidance.d.ts +4 -5
- package/dist/guidance.js +194 -57
- package/dist/harness.d.ts +68 -4
- package/dist/harness.js +45 -17
- package/dist/index.d.ts +9 -1
- package/dist/index.js +9 -0
- package/dist/inline-expectations.d.ts +23 -0
- package/dist/inline-expectations.js +186 -0
- package/dist/invalid-output.d.ts +58 -0
- package/dist/invalid-output.js +238 -0
- package/dist/model.d.ts +34 -0
- package/dist/model.js +8 -1
- package/dist/paths.d.ts +3 -0
- package/dist/paths.js +10 -0
- package/dist/plugin.js +847 -136
- package/dist/query.d.ts +193 -0
- package/dist/query.js +175 -0
- package/dist/selection.d.ts +42 -0
- package/dist/selection.js +141 -0
- package/dist/state.d.ts +15 -0
- package/dist/state.js +83 -0
- package/package.json +6 -6
package/dist/harness.js
CHANGED
|
@@ -10,20 +10,27 @@ const FALLBACK_ACTION = {
|
|
|
10
10
|
};
|
|
11
11
|
export const AGENT_ENRICHMENT_ACTIONS = Object.fromEntries(Object.entries(ENRICHMENT_ACTIONS_BY_CHECK_NAME).map(([checkName, action]) => [checkName, { checkName, ...action }]));
|
|
12
12
|
export const SCOPE_REJECTING_CHECKS = [
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
13
|
+
"expected-test-missing",
|
|
14
|
+
"expected-count-mismatch",
|
|
15
|
+
"expected-prefix-missing",
|
|
16
|
+
"expected-label-missing",
|
|
17
|
+
"expected-environment-missing",
|
|
18
|
+
"no-tests-observed",
|
|
16
19
|
"unexpected-environment",
|
|
17
|
-
"forbidden-
|
|
20
|
+
"forbidden-label-observed",
|
|
18
21
|
"unexpected-test",
|
|
19
22
|
];
|
|
20
23
|
export const ITERATION_REQUIRED_CHECKS = [
|
|
21
|
-
"
|
|
22
|
-
"
|
|
24
|
+
"expectations-invalid",
|
|
25
|
+
"expectations-empty",
|
|
26
|
+
"expectations-unsupported-control",
|
|
23
27
|
"runner-failures-outside-logical-results",
|
|
24
|
-
"missing-expected-label-selector",
|
|
25
28
|
"metadata-mismatch",
|
|
26
29
|
"history-id-collision",
|
|
30
|
+
"expected-step-containing-missing",
|
|
31
|
+
"insufficient-expected-steps",
|
|
32
|
+
"insufficient-expected-attachments",
|
|
33
|
+
"missing-expected-attachment",
|
|
27
34
|
"failed-without-useful-steps",
|
|
28
35
|
"failed-without-attachments",
|
|
29
36
|
"nontrivial-run-with-empty-trace",
|
|
@@ -42,6 +49,22 @@ const IMPACT_ORDER = {
|
|
|
42
49
|
advisory: 2,
|
|
43
50
|
};
|
|
44
51
|
const uniqueValues = (values) => Array.from(new Set(values));
|
|
52
|
+
const checkNameForFinding = (finding) => finding.check_id ?? finding.check_name;
|
|
53
|
+
const subjectRefForFinding = (finding) => {
|
|
54
|
+
if (finding.subject_ref) {
|
|
55
|
+
return finding.subject_ref;
|
|
56
|
+
}
|
|
57
|
+
if (typeof finding.subject === "string") {
|
|
58
|
+
return finding.subject;
|
|
59
|
+
}
|
|
60
|
+
return finding.subject.path ?? finding.subject.id ?? finding.subject.type;
|
|
61
|
+
};
|
|
62
|
+
const subjectTypeForFinding = (finding) => finding.subject_type ??
|
|
63
|
+
(typeof finding.subject === "object" && finding.subject.type === "test"
|
|
64
|
+
? "test"
|
|
65
|
+
: subjectRefForFinding(finding) === "run"
|
|
66
|
+
? "run"
|
|
67
|
+
: "test");
|
|
45
68
|
const normalizeStringArray = (value) => {
|
|
46
69
|
if (typeof value === "string") {
|
|
47
70
|
return value.length ? [value] : [];
|
|
@@ -112,14 +135,18 @@ const sortPlan = (items) => [...items].sort((left, right) => {
|
|
|
112
135
|
return left.checkName.localeCompare(right.checkName);
|
|
113
136
|
});
|
|
114
137
|
const impactForFinding = (finding, antiDummyConfidenceThreshold) => {
|
|
115
|
-
if (
|
|
138
|
+
if (finding.impact === "reject" || finding.impact === "iterate" || finding.impact === "advisory") {
|
|
139
|
+
return finding.impact;
|
|
140
|
+
}
|
|
141
|
+
const checkName = checkNameForFinding(finding);
|
|
142
|
+
if (SCOPE_REJECTING_CHECKS.includes(checkName)) {
|
|
116
143
|
return "reject";
|
|
117
144
|
}
|
|
118
|
-
if (ANTI_DUMMY_CHECKS.includes(
|
|
145
|
+
if (ANTI_DUMMY_CHECKS.includes(checkName) &&
|
|
119
146
|
(finding.confidence ?? 0) >= antiDummyConfidenceThreshold) {
|
|
120
147
|
return "reject";
|
|
121
148
|
}
|
|
122
|
-
if (ITERATION_REQUIRED_CHECKS.includes(
|
|
149
|
+
if (ITERATION_REQUIRED_CHECKS.includes(checkName)) {
|
|
123
150
|
return "iterate";
|
|
124
151
|
}
|
|
125
152
|
if (finding.severity === "high") {
|
|
@@ -140,7 +167,7 @@ export const buildAgentExpectations = (input) => {
|
|
|
140
167
|
};
|
|
141
168
|
};
|
|
142
169
|
export const mapFindingToEnrichmentAction = (finding) => {
|
|
143
|
-
const checkName = typeof finding === "string" ? finding : finding
|
|
170
|
+
const checkName = typeof finding === "string" ? finding : checkNameForFinding(finding);
|
|
144
171
|
const mapped = AGENT_ENRICHMENT_ACTIONS[checkName];
|
|
145
172
|
return mapped ?? { ...FALLBACK_ACTION, checkName };
|
|
146
173
|
};
|
|
@@ -166,16 +193,17 @@ export const planAgentEnrichmentReview = (output, options = {}) => {
|
|
|
166
193
|
const testsByPath = new Map(output.tests.map((test) => [test.markdown_path, test]));
|
|
167
194
|
const plan = sortPlan(output.findings.map((finding) => {
|
|
168
195
|
const action = mapFindingToEnrichmentAction(finding);
|
|
169
|
-
const
|
|
196
|
+
const subject = subjectRefForFinding(finding);
|
|
197
|
+
const matchedTest = testsByPath.get(subject);
|
|
170
198
|
return {
|
|
171
199
|
...action,
|
|
172
|
-
subject
|
|
173
|
-
subjectType: finding
|
|
200
|
+
subject,
|
|
201
|
+
subjectType: subjectTypeForFinding(finding),
|
|
174
202
|
severity: finding.severity,
|
|
175
203
|
message: finding.message,
|
|
176
204
|
explanation: finding.explanation,
|
|
177
|
-
remediationHint: finding.remediation_hint,
|
|
178
|
-
evidencePaths: finding.evidence_paths,
|
|
205
|
+
remediationHint: finding.action ?? finding.remediation_hint,
|
|
206
|
+
evidencePaths: finding.evidence?.paths ?? finding.evidence_paths,
|
|
179
207
|
expectedReference: finding.expected_reference,
|
|
180
208
|
confidence: finding.confidence,
|
|
181
209
|
acceptanceImpact: impactForFinding(finding, antiDummyConfidenceThreshold),
|
|
@@ -189,7 +217,7 @@ export const planAgentEnrichmentReview = (output, options = {}) => {
|
|
|
189
217
|
const advisory = plan.filter((item) => item.acceptanceImpact === "advisory");
|
|
190
218
|
const notes = [];
|
|
191
219
|
if (!output.run.expectations_present) {
|
|
192
|
-
notes.push("
|
|
220
|
+
notes.push("Declare inline expectations or provide an expectations file before the next enrichment iteration so scope checks are comparable.");
|
|
193
221
|
}
|
|
194
222
|
if (rejecting.some((item) => item.checkName === "noop-dominated-steps")) {
|
|
195
223
|
notes.push("Reject noop-dominated enrichment: keep only steps tied to real actions or checks, and use real runtime attachments instead of placeholders.");
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
-
export { type AgentPluginOptions } from "./model.js";
|
|
1
|
+
export { type AgentAttachmentExpectationInput, type AgentEvidenceExpectationInput, type AgentExpectationSelectorInput, type AgentExpectationsInput, type AgentPluginOptions, parseAgentExpectations, } from "./model.js";
|
|
2
|
+
export * from "./capabilities.js";
|
|
3
|
+
export * from "./errors.js";
|
|
2
4
|
export * from "./harness.js";
|
|
5
|
+
export * from "./inline-expectations.js";
|
|
6
|
+
export * from "./invalid-output.js";
|
|
7
|
+
export * from "./paths.js";
|
|
8
|
+
export * from "./query.js";
|
|
9
|
+
export * from "./selection.js";
|
|
10
|
+
export * from "./state.js";
|
|
3
11
|
export { AgentPlugin as default } from "./plugin.js";
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,11 @@
|
|
|
1
|
+
export { parseAgentExpectations, } from "./model.js";
|
|
2
|
+
export * from "./capabilities.js";
|
|
3
|
+
export * from "./errors.js";
|
|
1
4
|
export * from "./harness.js";
|
|
5
|
+
export * from "./inline-expectations.js";
|
|
6
|
+
export * from "./invalid-output.js";
|
|
7
|
+
export * from "./paths.js";
|
|
8
|
+
export * from "./query.js";
|
|
9
|
+
export * from "./selection.js";
|
|
10
|
+
export * from "./state.js";
|
|
2
11
|
export { AgentPlugin as default } from "./plugin.js";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { AgentExpectationsInput } from "./model.js";
|
|
2
|
+
type SingleStringOptionValue = string | string[] | undefined;
|
|
3
|
+
export type AgentInlineExpectationOptions = {
|
|
4
|
+
goal?: SingleStringOptionValue;
|
|
5
|
+
taskId?: SingleStringOptionValue;
|
|
6
|
+
expectTests?: SingleStringOptionValue;
|
|
7
|
+
expectLabels?: string[];
|
|
8
|
+
expectEnvironments?: string[];
|
|
9
|
+
expectFullNames?: string[];
|
|
10
|
+
expectPrefixes?: string[];
|
|
11
|
+
forbidLabels?: string[];
|
|
12
|
+
expectStepContains?: string[];
|
|
13
|
+
expectSteps?: SingleStringOptionValue;
|
|
14
|
+
expectAttachments?: SingleStringOptionValue;
|
|
15
|
+
expectAttachmentFilters?: string[];
|
|
16
|
+
};
|
|
17
|
+
export declare const buildAgentInlineExpectations: (options: AgentInlineExpectationOptions) => AgentExpectationsInput | undefined;
|
|
18
|
+
export declare const validateAgentExpectationsFile: (params: {
|
|
19
|
+
cwd: string;
|
|
20
|
+
output?: string;
|
|
21
|
+
expectations?: string;
|
|
22
|
+
}) => Promise<void>;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { AgentExpectationUsageError, AgentUsageError } from "./errors.js";
|
|
4
|
+
import { parseAgentExpectations } from "./model.js";
|
|
5
|
+
import { isPathInside } from "./paths.js";
|
|
6
|
+
const readNonNegativeInteger = (value, optionName) => {
|
|
7
|
+
if (value === undefined) {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
if (!/^\d+$/.test(value)) {
|
|
11
|
+
throw new AgentExpectationUsageError(`${optionName} must be a non-negative integer`, optionName);
|
|
12
|
+
}
|
|
13
|
+
const parsed = Number(value);
|
|
14
|
+
if (!Number.isSafeInteger(parsed)) {
|
|
15
|
+
throw new AgentExpectationUsageError(`${optionName} must be a non-negative integer`, optionName);
|
|
16
|
+
}
|
|
17
|
+
return parsed;
|
|
18
|
+
};
|
|
19
|
+
const readPositiveInteger = (value, optionName) => {
|
|
20
|
+
if (value === undefined) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
if (!/^[1-9]\d*$/.test(value)) {
|
|
24
|
+
throw new AgentExpectationUsageError(`${optionName} must be a positive integer`, optionName);
|
|
25
|
+
}
|
|
26
|
+
const parsed = Number(value);
|
|
27
|
+
if (!Number.isSafeInteger(parsed)) {
|
|
28
|
+
throw new AgentExpectationUsageError(`${optionName} must be a positive integer`, optionName);
|
|
29
|
+
}
|
|
30
|
+
return parsed;
|
|
31
|
+
};
|
|
32
|
+
const readSingleStringOption = (value, optionName) => {
|
|
33
|
+
const values = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
|
|
34
|
+
if (values.length > 1) {
|
|
35
|
+
throw new AgentExpectationUsageError(`Duplicate ${optionName} is not allowed`, optionName);
|
|
36
|
+
}
|
|
37
|
+
if (!values.length) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
const normalized = values[0].trim();
|
|
41
|
+
if (!normalized) {
|
|
42
|
+
throw new AgentExpectationUsageError(`${optionName} value must be non-empty`, optionName);
|
|
43
|
+
}
|
|
44
|
+
return normalized;
|
|
45
|
+
};
|
|
46
|
+
const parseNameValue = (value, optionName, example) => {
|
|
47
|
+
const parts = value.split("=");
|
|
48
|
+
if (parts.length !== 2) {
|
|
49
|
+
throw new AgentExpectationUsageError(`Invalid ${optionName} ${JSON.stringify(value)}. Expected ${example}`, optionName);
|
|
50
|
+
}
|
|
51
|
+
const name = parts[0].trim();
|
|
52
|
+
const filterValue = parts[1].trim();
|
|
53
|
+
if (!name || !filterValue) {
|
|
54
|
+
throw new AgentExpectationUsageError(`Invalid ${optionName} ${JSON.stringify(value)}. Expected ${example}`, optionName);
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
name,
|
|
58
|
+
value: filterValue,
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
const addLabelValues = (target, values, optionName) => {
|
|
62
|
+
for (const rawValue of values ?? []) {
|
|
63
|
+
const { name, value } = parseNameValue(rawValue, optionName, "the form name=value, for example module=cli");
|
|
64
|
+
const current = target[name] ?? [];
|
|
65
|
+
if (!current.includes(value)) {
|
|
66
|
+
current.push(value);
|
|
67
|
+
}
|
|
68
|
+
target[name] = current;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const normalizeStringValues = (values, optionName) => {
|
|
72
|
+
const normalized = [];
|
|
73
|
+
for (const value of values ?? []) {
|
|
74
|
+
const trimmed = value.trim();
|
|
75
|
+
if (!trimmed) {
|
|
76
|
+
throw new AgentExpectationUsageError(`${optionName} value must be non-empty`, optionName);
|
|
77
|
+
}
|
|
78
|
+
normalized.push(trimmed);
|
|
79
|
+
}
|
|
80
|
+
return normalized;
|
|
81
|
+
};
|
|
82
|
+
export const buildAgentInlineExpectations = (options) => {
|
|
83
|
+
const expectedLabels = {};
|
|
84
|
+
const forbiddenLabels = {};
|
|
85
|
+
const expected = {};
|
|
86
|
+
const forbidden = {};
|
|
87
|
+
const evidence = {};
|
|
88
|
+
const attachmentFilters = [];
|
|
89
|
+
addLabelValues(expectedLabels, options.expectLabels, "--expect-label");
|
|
90
|
+
addLabelValues(forbiddenLabels, options.forbidLabels, "--forbid-label");
|
|
91
|
+
const expectTests = readNonNegativeInteger(readSingleStringOption(options.expectTests, "--expect-tests"), "--expect-tests");
|
|
92
|
+
const expectSteps = readPositiveInteger(readSingleStringOption(options.expectSteps, "--expect-steps"), "--expect-steps");
|
|
93
|
+
const expectAttachments = readPositiveInteger(readSingleStringOption(options.expectAttachments, "--expect-attachments"), "--expect-attachments");
|
|
94
|
+
const expectedEnvironments = normalizeStringValues(options.expectEnvironments, "--expect-env");
|
|
95
|
+
const expectedFullNames = normalizeStringValues(options.expectFullNames, "--expect-test");
|
|
96
|
+
const expectedPrefixes = normalizeStringValues(options.expectPrefixes, "--expect-prefix");
|
|
97
|
+
const expectedStepContains = normalizeStringValues(options.expectStepContains, "--expect-step-containing");
|
|
98
|
+
for (const rawValue of options.expectAttachmentFilters ?? []) {
|
|
99
|
+
const parsed = rawValue.includes("=")
|
|
100
|
+
? parseNameValue(rawValue, "--expect-attachment", "a file name or a filter such as name=trace.zip or content-type=application/json")
|
|
101
|
+
: { name: "name", value: rawValue.trim() };
|
|
102
|
+
const normalizedName = parsed.name.toLowerCase().replace(/_/g, "-");
|
|
103
|
+
if (!parsed.value) {
|
|
104
|
+
throw new AgentExpectationUsageError("Invalid --expect-attachment value. Expected a non-empty file name or filter such as name=trace.zip", "--expect-attachment");
|
|
105
|
+
}
|
|
106
|
+
if (normalizedName === "name") {
|
|
107
|
+
attachmentFilters.push({ name: parsed.value });
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (normalizedName === "content-type" || normalizedName === "type") {
|
|
111
|
+
attachmentFilters.push({ content_type: parsed.value });
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
throw new AgentExpectationUsageError(`Invalid --expect-attachment key ${JSON.stringify(parsed.name)}. Expected name or content-type`, "--expect-attachment");
|
|
115
|
+
}
|
|
116
|
+
if (expectTests !== undefined) {
|
|
117
|
+
expected.test_count = expectTests;
|
|
118
|
+
}
|
|
119
|
+
if (expectedEnvironments.length) {
|
|
120
|
+
expected.environments = expectedEnvironments;
|
|
121
|
+
}
|
|
122
|
+
if (expectedFullNames.length) {
|
|
123
|
+
expected.full_names = expectedFullNames;
|
|
124
|
+
}
|
|
125
|
+
if (expectedPrefixes.length) {
|
|
126
|
+
expected.full_name_prefixes = expectedPrefixes;
|
|
127
|
+
}
|
|
128
|
+
if (Object.keys(expectedLabels).length) {
|
|
129
|
+
expected.label_values = expectedLabels;
|
|
130
|
+
}
|
|
131
|
+
if (Object.keys(forbiddenLabels).length) {
|
|
132
|
+
forbidden.label_values = forbiddenLabels;
|
|
133
|
+
}
|
|
134
|
+
if (expectSteps !== undefined) {
|
|
135
|
+
evidence.min_steps = expectSteps;
|
|
136
|
+
}
|
|
137
|
+
if (expectAttachments !== undefined) {
|
|
138
|
+
evidence.min_attachments = expectAttachments;
|
|
139
|
+
}
|
|
140
|
+
if (expectedStepContains.length) {
|
|
141
|
+
evidence.step_name_contains = expectedStepContains;
|
|
142
|
+
}
|
|
143
|
+
if (attachmentFilters.length) {
|
|
144
|
+
evidence.attachments = attachmentFilters;
|
|
145
|
+
}
|
|
146
|
+
if (expected.test_count === 0 &&
|
|
147
|
+
(expected.environments?.length ||
|
|
148
|
+
expected.full_names?.length ||
|
|
149
|
+
expected.full_name_prefixes?.length ||
|
|
150
|
+
Object.keys(expected.label_values ?? {}).length ||
|
|
151
|
+
evidence.step_name_contains?.length ||
|
|
152
|
+
evidence.min_steps !== undefined ||
|
|
153
|
+
evidence.min_attachments !== undefined ||
|
|
154
|
+
evidence.attachments?.length)) {
|
|
155
|
+
throw new AgentExpectationUsageError("--expect-tests 0 cannot be combined with positive scope or evidence expectations", "--expect-tests");
|
|
156
|
+
}
|
|
157
|
+
const inlineExpectations = {
|
|
158
|
+
...(readSingleStringOption(options.goal, "--goal") ? { goal: readSingleStringOption(options.goal, "--goal") } : {}),
|
|
159
|
+
...(readSingleStringOption(options.taskId, "--task-id")
|
|
160
|
+
? { task_id: readSingleStringOption(options.taskId, "--task-id") }
|
|
161
|
+
: {}),
|
|
162
|
+
...(Object.keys(expected).length ? { expected } : {}),
|
|
163
|
+
...(Object.keys(forbidden).length ? { forbidden } : {}),
|
|
164
|
+
...(Object.keys(evidence).length ? { evidence } : {}),
|
|
165
|
+
};
|
|
166
|
+
return Object.keys(inlineExpectations).length ? inlineExpectations : undefined;
|
|
167
|
+
};
|
|
168
|
+
export const validateAgentExpectationsFile = async (params) => {
|
|
169
|
+
const { cwd, output, expectations } = params;
|
|
170
|
+
if (!expectations) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const expectationsPath = resolve(cwd, expectations);
|
|
174
|
+
if (output) {
|
|
175
|
+
const outputDir = resolve(cwd, output);
|
|
176
|
+
if (isPathInside(outputDir, expectationsPath)) {
|
|
177
|
+
throw new AgentUsageError(`--expectations path ${JSON.stringify(expectationsPath)} must not be inside the agent output directory ${JSON.stringify(outputDir)}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
parseAgentExpectations(await readFile(expectationsPath, "utf-8"));
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
throw new AgentExpectationUsageError(`Could not load expectations from ${expectationsPath}: ${error.message}`, "--expectations");
|
|
185
|
+
}
|
|
186
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { AgentExpectationUsageError } from "./errors.js";
|
|
2
|
+
export declare const createInvalidExpectationFinding: (params: {
|
|
3
|
+
message: string;
|
|
4
|
+
sourceOption?: string;
|
|
5
|
+
}) => {
|
|
6
|
+
schema_version: string;
|
|
7
|
+
check_id: string;
|
|
8
|
+
instance_id: string;
|
|
9
|
+
severity: string;
|
|
10
|
+
impact: string;
|
|
11
|
+
confidence: number;
|
|
12
|
+
category: string;
|
|
13
|
+
title: string;
|
|
14
|
+
message: string;
|
|
15
|
+
subject: {
|
|
16
|
+
type: string;
|
|
17
|
+
};
|
|
18
|
+
expected: {
|
|
19
|
+
option: string;
|
|
20
|
+
expectations?: undefined;
|
|
21
|
+
} | {
|
|
22
|
+
expectations: string;
|
|
23
|
+
option?: undefined;
|
|
24
|
+
};
|
|
25
|
+
observed: {
|
|
26
|
+
error: string;
|
|
27
|
+
execution_skipped: boolean;
|
|
28
|
+
};
|
|
29
|
+
evidence: {
|
|
30
|
+
paths: string[];
|
|
31
|
+
};
|
|
32
|
+
action: string;
|
|
33
|
+
source: {
|
|
34
|
+
kind: string;
|
|
35
|
+
option: string;
|
|
36
|
+
} | undefined;
|
|
37
|
+
legacy: {
|
|
38
|
+
finding_id: string;
|
|
39
|
+
check_name: string;
|
|
40
|
+
remediation_hint: string;
|
|
41
|
+
};
|
|
42
|
+
finding_id: string;
|
|
43
|
+
subject_ref: string;
|
|
44
|
+
subject_type: string;
|
|
45
|
+
check_name: string;
|
|
46
|
+
explanation: string;
|
|
47
|
+
evidence_paths: string[];
|
|
48
|
+
remediation_hint: string;
|
|
49
|
+
expected_reference: string | undefined;
|
|
50
|
+
};
|
|
51
|
+
export declare const writeInvalidAgentExpectationOutput: (params: {
|
|
52
|
+
outputDir: string;
|
|
53
|
+
command: string;
|
|
54
|
+
error: AgentExpectationUsageError;
|
|
55
|
+
}) => Promise<{
|
|
56
|
+
outputDir: string;
|
|
57
|
+
generatedAt: string;
|
|
58
|
+
}>;
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
const isFileNotFoundError = (error) => typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
4
|
+
const emptyAgentStats = () => ({
|
|
5
|
+
total: 0,
|
|
6
|
+
failed: 0,
|
|
7
|
+
broken: 0,
|
|
8
|
+
skipped: 0,
|
|
9
|
+
unknown: 0,
|
|
10
|
+
passed: 0,
|
|
11
|
+
});
|
|
12
|
+
const writeJson = async (path, value) => {
|
|
13
|
+
await mkdir(dirname(path), { recursive: true });
|
|
14
|
+
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, "utf-8");
|
|
15
|
+
};
|
|
16
|
+
const writeText = async (path, value) => {
|
|
17
|
+
await mkdir(dirname(path), { recursive: true });
|
|
18
|
+
await writeFile(path, value, "utf-8");
|
|
19
|
+
};
|
|
20
|
+
const writeJsonl = async (path, values) => {
|
|
21
|
+
await writeText(path, values.map((value) => JSON.stringify(value)).join("\n") + (values.length ? "\n" : ""));
|
|
22
|
+
};
|
|
23
|
+
export const createInvalidExpectationFinding = (params) => {
|
|
24
|
+
const action = "Fix expectation syntax before using the run as validation.";
|
|
25
|
+
return {
|
|
26
|
+
schema_version: "allure-agent-finding/v2",
|
|
27
|
+
check_id: "expectations-invalid",
|
|
28
|
+
instance_id: "F0001",
|
|
29
|
+
severity: "high",
|
|
30
|
+
impact: "reject",
|
|
31
|
+
confidence: 1,
|
|
32
|
+
category: "bootstrap",
|
|
33
|
+
title: "Expectation input is invalid",
|
|
34
|
+
message: params.message,
|
|
35
|
+
subject: {
|
|
36
|
+
type: "run",
|
|
37
|
+
},
|
|
38
|
+
expected: params.sourceOption ? { option: params.sourceOption } : { expectations: "valid M1 expectation input" },
|
|
39
|
+
observed: {
|
|
40
|
+
error: params.message,
|
|
41
|
+
execution_skipped: true,
|
|
42
|
+
},
|
|
43
|
+
evidence: {
|
|
44
|
+
paths: ["manifest/run.json"],
|
|
45
|
+
},
|
|
46
|
+
action,
|
|
47
|
+
source: params.sourceOption
|
|
48
|
+
? {
|
|
49
|
+
kind: "inline-option",
|
|
50
|
+
option: params.sourceOption,
|
|
51
|
+
}
|
|
52
|
+
: undefined,
|
|
53
|
+
legacy: {
|
|
54
|
+
finding_id: "F0001",
|
|
55
|
+
check_name: "expectations-invalid",
|
|
56
|
+
remediation_hint: action,
|
|
57
|
+
},
|
|
58
|
+
finding_id: "F0001",
|
|
59
|
+
subject_ref: "run",
|
|
60
|
+
subject_type: "run",
|
|
61
|
+
check_name: "expectations-invalid",
|
|
62
|
+
explanation: "The agent expectation controls could not be parsed, so the test command was not executed.",
|
|
63
|
+
evidence_paths: ["manifest/run.json"],
|
|
64
|
+
remediation_hint: action,
|
|
65
|
+
expected_reference: params.sourceOption,
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
export const writeInvalidAgentExpectationOutput = async (params) => {
|
|
69
|
+
const { outputDir, command, error } = params;
|
|
70
|
+
const generatedAt = new Date().toISOString();
|
|
71
|
+
const finding = createInvalidExpectationFinding({
|
|
72
|
+
message: error.message,
|
|
73
|
+
sourceOption: error.sourceOption,
|
|
74
|
+
});
|
|
75
|
+
const stats = emptyAgentStats();
|
|
76
|
+
try {
|
|
77
|
+
await rm(outputDir, { recursive: true });
|
|
78
|
+
}
|
|
79
|
+
catch (rmError) {
|
|
80
|
+
if (!isFileNotFoundError(rmError)) {
|
|
81
|
+
console.error("could not clean output directory", rmError);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const runManifest = {
|
|
85
|
+
schema_version: "allure-agent-output/v1",
|
|
86
|
+
report_uuid: null,
|
|
87
|
+
generated_at: generatedAt,
|
|
88
|
+
phase: "done",
|
|
89
|
+
command,
|
|
90
|
+
actual_exit_code: null,
|
|
91
|
+
original_exit_code: null,
|
|
92
|
+
exit_code: null,
|
|
93
|
+
summary: {
|
|
94
|
+
stats,
|
|
95
|
+
modeled_stats: stats,
|
|
96
|
+
unmodeled_from_stats: stats,
|
|
97
|
+
compact: {
|
|
98
|
+
visible_results: 0,
|
|
99
|
+
logical_tests: 0,
|
|
100
|
+
unmodeled_visible_results: 0,
|
|
101
|
+
runner_failures_outside_logical_tests: 0,
|
|
102
|
+
completeness: "complete",
|
|
103
|
+
findings: 1,
|
|
104
|
+
},
|
|
105
|
+
duration_ms: {
|
|
106
|
+
total: 0,
|
|
107
|
+
average: 0,
|
|
108
|
+
max: 0,
|
|
109
|
+
},
|
|
110
|
+
environments: [],
|
|
111
|
+
},
|
|
112
|
+
modeling: {
|
|
113
|
+
completeness: "complete",
|
|
114
|
+
reasons: ["test command was skipped because agent expectations were invalid"],
|
|
115
|
+
modeledStats: stats,
|
|
116
|
+
unmodeledFromStats: stats,
|
|
117
|
+
runnerFailures: {
|
|
118
|
+
total: 0,
|
|
119
|
+
globalErrors: 0,
|
|
120
|
+
stderrActionable: 0,
|
|
121
|
+
samples: [],
|
|
122
|
+
},
|
|
123
|
+
stderr: {
|
|
124
|
+
actionableCount: 0,
|
|
125
|
+
actionableSamples: [],
|
|
126
|
+
noisyWarningCount: 0,
|
|
127
|
+
noisyWarningSamples: [],
|
|
128
|
+
},
|
|
129
|
+
compact: {
|
|
130
|
+
visible_results: 0,
|
|
131
|
+
logical_tests: 0,
|
|
132
|
+
unmodeled_visible_results: 0,
|
|
133
|
+
runner_failures_outside_logical_tests: 0,
|
|
134
|
+
completeness: "complete",
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
paths: {
|
|
138
|
+
index_md: "index.md",
|
|
139
|
+
agents_md: "AGENTS.md",
|
|
140
|
+
tests_manifest: "manifest/tests.jsonl",
|
|
141
|
+
findings_manifest: "manifest/findings.jsonl",
|
|
142
|
+
test_events_manifest: "manifest/test-events.jsonl",
|
|
143
|
+
expected_manifest: null,
|
|
144
|
+
process_logs: {
|
|
145
|
+
stdout: null,
|
|
146
|
+
stderr: null,
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
expectations_present: false,
|
|
150
|
+
expectations: null,
|
|
151
|
+
expectation_result: {
|
|
152
|
+
schema_version: "allure-agent-expectation-result/v1",
|
|
153
|
+
status: "unavailable",
|
|
154
|
+
impact: "reject",
|
|
155
|
+
source: {
|
|
156
|
+
kind: "none",
|
|
157
|
+
path: null,
|
|
158
|
+
},
|
|
159
|
+
recognized_control_count: 0,
|
|
160
|
+
unsupported_controls: [],
|
|
161
|
+
degraded_controls: [],
|
|
162
|
+
summary: {
|
|
163
|
+
expected_tests: 0,
|
|
164
|
+
observed_tests: 0,
|
|
165
|
+
missing_expected: 0,
|
|
166
|
+
forbidden_observed: 0,
|
|
167
|
+
unexpected_observed: 0,
|
|
168
|
+
evidence_mismatches: 0,
|
|
169
|
+
},
|
|
170
|
+
finding_ids: ["F0001"],
|
|
171
|
+
},
|
|
172
|
+
check_summary: {
|
|
173
|
+
total: 1,
|
|
174
|
+
countsBySeverity: {
|
|
175
|
+
high: 1,
|
|
176
|
+
warning: 0,
|
|
177
|
+
info: 0,
|
|
178
|
+
},
|
|
179
|
+
countsByCategory: {
|
|
180
|
+
bootstrap: 1,
|
|
181
|
+
scope: 0,
|
|
182
|
+
metadata: 0,
|
|
183
|
+
evidence: 0,
|
|
184
|
+
smells: 0,
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
agent_context: {
|
|
188
|
+
agent_name: null,
|
|
189
|
+
loop_id: null,
|
|
190
|
+
task_id: null,
|
|
191
|
+
conversation_id: null,
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
const index = [
|
|
195
|
+
"# Allure Agent Run",
|
|
196
|
+
"",
|
|
197
|
+
"- Phase: done",
|
|
198
|
+
`- Command: ${command || "(not executed)"}`,
|
|
199
|
+
"- Exit code: not available",
|
|
200
|
+
"",
|
|
201
|
+
"## Expectation Result",
|
|
202
|
+
"",
|
|
203
|
+
"- Status: unavailable",
|
|
204
|
+
"- Impact: reject",
|
|
205
|
+
"- Recognized controls: 0",
|
|
206
|
+
"- Summary: expectation input was invalid; test execution was skipped",
|
|
207
|
+
"",
|
|
208
|
+
"## Findings",
|
|
209
|
+
"",
|
|
210
|
+
`- [HIGH][reject][bootstrap] ${finding.title}`,
|
|
211
|
+
` Expected: ${error.sourceOption ?? "valid M1 expectation input"}`,
|
|
212
|
+
` Observed: ${error.message}`,
|
|
213
|
+
` Action: ${finding.action}`,
|
|
214
|
+
"",
|
|
215
|
+
"## Machine-Readable Artifacts",
|
|
216
|
+
"",
|
|
217
|
+
"- Run Manifest: [manifest/run.json](manifest/run.json)",
|
|
218
|
+
"- Findings Manifest: [manifest/findings.jsonl](manifest/findings.jsonl)",
|
|
219
|
+
"",
|
|
220
|
+
].join("\n");
|
|
221
|
+
await Promise.all([
|
|
222
|
+
writeJson(join(outputDir, "manifest", "run.json"), runManifest),
|
|
223
|
+
writeJsonl(join(outputDir, "manifest", "findings.jsonl"), [finding]),
|
|
224
|
+
writeJsonl(join(outputDir, "manifest", "tests.jsonl"), []),
|
|
225
|
+
writeJsonl(join(outputDir, "manifest", "test-events.jsonl"), []),
|
|
226
|
+
writeText(join(outputDir, "index.md"), index),
|
|
227
|
+
writeText(join(outputDir, "AGENTS.md"), [
|
|
228
|
+
"# Allure Agent Output",
|
|
229
|
+
"",
|
|
230
|
+
"Read `index.md`, `manifest/run.json`, and `manifest/findings.jsonl` before using this run.",
|
|
231
|
+
"",
|
|
232
|
+
].join("\n")),
|
|
233
|
+
]);
|
|
234
|
+
return {
|
|
235
|
+
outputDir,
|
|
236
|
+
generatedAt,
|
|
237
|
+
};
|
|
238
|
+
};
|