@tea-agent/loop-agent 0.28.13 → 0.29.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/AGENTS.md +1 -1
- package/CHANGELOG.md +44 -15
- package/dist/commands/client-recovery.js +56 -1
- package/dist/commands/init-upgrade.js +186 -21
- package/dist/executors/dag-pi-executor.js +49 -2
- package/dist/executors/shell-executor.js +135 -0
- package/dist/worker/console/app-data.js +132 -11
- package/dist/worker/console/chat/pi-runtime.js +24 -42
- package/dist/worker/console/chat/resource-loader.js +11 -20
- package/dist/worker/console/chat/routes.js +7 -8
- package/dist/worker/console/chat/runtime-context.js +1 -1
- package/dist/worker/console/chat/tools.js +67 -54
- package/dist/worker/console/operation-runner.js +15 -1
- package/dist/worker/console/operation-store.js +70 -49
- package/dist/worker/console/operator-actions.js +57 -1
- package/dist/worker/console/static/assets/index-Cwx-ZVEQ.js +29 -0
- package/dist/worker/console/static/favicon.svg +37 -0
- package/dist/worker/console/static/index.html +2 -1
- package/dist/workflows/dag/backend-test-scenario-param.js +846 -0
- package/dist/workflows/dag/backend-test-writer-completeness.js +418 -0
- package/dist/workflows/dag/init-hybrid.js +20 -6
- package/dist/workflows/dag/node-execution.js +31 -2
- package/dist/workflows/dag/retry-policy.js +55 -18
- package/dist/workflows/dag/types.js +1 -0
- package/dist/workflows/dag/validate.js +38 -3
- package/docs/operations/README.md +1 -1
- package/docs/templates/README.md +1 -1
- package/docs/templates/agent-dag.schema.json +3 -3
- package/docs/templates/backend-test-dag.json +36 -11
- package/docs/templates/init-managed-agents.md +1 -1
- package/harness.json +2 -2
- package/package.json +2 -1
- package/dist/worker/console/static/assets/index-BfRgtLF4.js +0 -29
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { access, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { expectedBackendTestPytestScriptForMarkdownModule, normalizeBackendTestModuleStem, } from "./backend-test-markdown-workflow.js";
|
|
6
|
+
export const BACKEND_TEST_WRITER_PROGRESS_SCHEMA_ID = "backend-test-writer-progress-v1";
|
|
7
|
+
export const BACKEND_TEST_OUTPUT_LIMIT_RECOVERY_REPORT = "backend-test-output-limit-recovery.md";
|
|
8
|
+
const progressSchema = z
|
|
9
|
+
.object({
|
|
10
|
+
schemaId: z.literal(BACKEND_TEST_WRITER_PROGRESS_SCHEMA_ID),
|
|
11
|
+
role: z.enum(["md-generate", "pytest-generate", "report"]),
|
|
12
|
+
status: z.enum(["PASS", "RECOVERABLE", "NON_RECOVERABLE"]),
|
|
13
|
+
expectedPaths: z.array(z.string()),
|
|
14
|
+
actualPaths: z.array(z.string()),
|
|
15
|
+
missingPaths: z.array(z.string()),
|
|
16
|
+
brokenPaths: z.array(z.string()),
|
|
17
|
+
targetPaths: z.array(z.string()),
|
|
18
|
+
issues: z.array(z
|
|
19
|
+
.object({
|
|
20
|
+
code: z.enum(["T3", "T4", "T5", "T6"]),
|
|
21
|
+
path: z.string().optional(),
|
|
22
|
+
detail: z.string(),
|
|
23
|
+
recoverable: z.boolean(),
|
|
24
|
+
})
|
|
25
|
+
.strict()),
|
|
26
|
+
})
|
|
27
|
+
.strict();
|
|
28
|
+
async function exists(filePath) {
|
|
29
|
+
try {
|
|
30
|
+
await access(filePath);
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function orderedUnique(values) {
|
|
38
|
+
return [...new Set(values.filter(Boolean))];
|
|
39
|
+
}
|
|
40
|
+
function hasMarkdownTable(section, headerNeedle) {
|
|
41
|
+
const lines = section
|
|
42
|
+
.replaceAll("\r\n", "\n")
|
|
43
|
+
.replaceAll("\r", "\n")
|
|
44
|
+
.split("\n");
|
|
45
|
+
const headerIndex = lines.findIndex((line) => line.toLowerCase().includes(headerNeedle.toLowerCase()));
|
|
46
|
+
if (headerIndex < 0)
|
|
47
|
+
return false;
|
|
48
|
+
const header = lines[headerIndex] ?? "";
|
|
49
|
+
const separator = lines[headerIndex + 1] ?? "";
|
|
50
|
+
return (header.includes("|") &&
|
|
51
|
+
/-{3,}/.test(separator) &&
|
|
52
|
+
separator.includes("|"));
|
|
53
|
+
}
|
|
54
|
+
function extractModuleStemsFromReadme(readme) {
|
|
55
|
+
const stems = [];
|
|
56
|
+
for (const match of readme.matchAll(/`?testcase\/md\/([A-Za-z0-9_.-]+)\.md`?/g)) {
|
|
57
|
+
const stem = match[1];
|
|
58
|
+
if (stem && stem.toLowerCase() !== "readme")
|
|
59
|
+
stems.push(stem);
|
|
60
|
+
}
|
|
61
|
+
for (const match of readme.matchAll(/\|\s*`?([A-Za-z0-9_.-]+)`?\s*\|\s*`?testcase\/test_/g)) {
|
|
62
|
+
if (match[1])
|
|
63
|
+
stems.push(match[1]);
|
|
64
|
+
}
|
|
65
|
+
for (const match of readme.matchAll(/\[[^\]]+\]\(\.\/([A-Za-z0-9_.-]+)\.md\)/g)) {
|
|
66
|
+
if (match[1] && match[1].toLowerCase() !== "readme")
|
|
67
|
+
stems.push(match[1]);
|
|
68
|
+
}
|
|
69
|
+
return orderedUnique(stems.map((stem) => normalizeBackendTestModuleStem(stem)));
|
|
70
|
+
}
|
|
71
|
+
async function listMarkdownModules(workspaceRoot) {
|
|
72
|
+
const root = path.join(workspaceRoot, "testcase", "md");
|
|
73
|
+
if (!(await exists(root)))
|
|
74
|
+
return [];
|
|
75
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
76
|
+
return orderedUnique(entries
|
|
77
|
+
.filter((entry) => entry.isFile() &&
|
|
78
|
+
entry.name.toLowerCase().endsWith(".md") &&
|
|
79
|
+
entry.name.toLowerCase() !== "readme.md")
|
|
80
|
+
.map((entry) => path.posix.join("testcase/md", entry.name.replaceAll("\\", "/"))));
|
|
81
|
+
}
|
|
82
|
+
function moduleStructurallyComplete(markdown) {
|
|
83
|
+
if (!markdown.trim())
|
|
84
|
+
return false;
|
|
85
|
+
if (!/^##\s+BE-[A-Z0-9_-]+-\d{2,3}\b/m.test(markdown))
|
|
86
|
+
return false;
|
|
87
|
+
const required = [
|
|
88
|
+
"\u8986\u76d6\u89c4\u5219",
|
|
89
|
+
"\u6d4b\u8bd5\u70b9",
|
|
90
|
+
"\u573a\u666f\u7c7b\u578b",
|
|
91
|
+
"\u524d\u7f6e\u6761\u4ef6",
|
|
92
|
+
"\u64cd\u4f5c\u6b65\u9aa4",
|
|
93
|
+
"\u9884\u671f\u7ed3\u679c",
|
|
94
|
+
"\u81ea\u52a8\u5316\u6620\u5c04"
|
|
95
|
+
];
|
|
96
|
+
return required.every((heading) => markdown.includes(`### ${heading}`));
|
|
97
|
+
}
|
|
98
|
+
function pythonParseable(source) {
|
|
99
|
+
const text = source.replace(/\r\n/g, "\n");
|
|
100
|
+
if (!text.trim())
|
|
101
|
+
return false;
|
|
102
|
+
if (/^\s*(?:def|class|import|from)\b/m.test(text) === false)
|
|
103
|
+
return false;
|
|
104
|
+
const openParens = (text.match(/\(/g) ?? []).length;
|
|
105
|
+
const closeParens = (text.match(/\)/g) ?? []).length;
|
|
106
|
+
const openBrackets = (text.match(/\[/g) ?? []).length;
|
|
107
|
+
const closeBrackets = (text.match(/\]/g) ?? []).length;
|
|
108
|
+
const openBraces = (text.match(/\{/g) ?? []).length;
|
|
109
|
+
const closeBraces = (text.match(/\}/g) ?? []).length;
|
|
110
|
+
if (openParens !== closeParens)
|
|
111
|
+
return false;
|
|
112
|
+
if (openBrackets !== closeBrackets)
|
|
113
|
+
return false;
|
|
114
|
+
if (openBraces !== closeBraces)
|
|
115
|
+
return false;
|
|
116
|
+
if (/("""|''')[\s\S]*$/.test(text)) {
|
|
117
|
+
const triples = text.match(/("""|''')/g) ?? [];
|
|
118
|
+
if (triples.length % 2 !== 0)
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
if (/\bdef\s+\w+\s*\([^)]*$/m.test(text))
|
|
122
|
+
return false;
|
|
123
|
+
if (/\bpytest\.param\s*\([^)]*$/m.test(text))
|
|
124
|
+
return false;
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
export function buildOutputLimitRecoveryPrompt(input) {
|
|
128
|
+
const paths = input.targetPaths.length > 0
|
|
129
|
+
? input.targetPaths.map((item) => ` - ${item}`).join("\n")
|
|
130
|
+
: " - (none)";
|
|
131
|
+
return [
|
|
132
|
+
"<retry_instruction>",
|
|
133
|
+
`OUTPUT_LIMIT_RECOVERY attempt=${input.attempt}/${input.maxAttempts}`,
|
|
134
|
+
`reason=${input.reason}`,
|
|
135
|
+
"target_paths_only:",
|
|
136
|
+
paths,
|
|
137
|
+
"Rules:",
|
|
138
|
+
"- Continue or repair ONLY listed paths; do not delete or shrink in-scope cases/rules/TPs.",
|
|
139
|
+
"- One file per write/edit; no chat dump of full bodies.",
|
|
140
|
+
"- Prefer edit/append for truncated files; rewrite a file only if unparseable or empty.",
|
|
141
|
+
"- After targets are complete, short IMPLEMENTATION_OUTCOME only.",
|
|
142
|
+
"- Do not mark already-satisfied if any target_path still missing or structurally broken.",
|
|
143
|
+
"- Preserve quality invariants: no scope shrink, no skip/xfail, no secret-shaped samples.",
|
|
144
|
+
"</retry_instruction>",
|
|
145
|
+
].join("\n");
|
|
146
|
+
}
|
|
147
|
+
export async function assessBackendTestMdWriterCompleteness(workspaceRoot) {
|
|
148
|
+
const issues = [];
|
|
149
|
+
const expectedPaths = ["testcase/md/README.md"];
|
|
150
|
+
const actualPaths = [];
|
|
151
|
+
const missingPaths = [];
|
|
152
|
+
const brokenPaths = [];
|
|
153
|
+
const readmePath = path.join(workspaceRoot, "testcase", "md", "README.md");
|
|
154
|
+
let readme = "";
|
|
155
|
+
if (!(await exists(readmePath))) {
|
|
156
|
+
missingPaths.push("testcase/md/README.md");
|
|
157
|
+
issues.push({
|
|
158
|
+
code: "T3",
|
|
159
|
+
path: "testcase/md/README.md",
|
|
160
|
+
detail: "README.md is missing",
|
|
161
|
+
recoverable: true,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
actualPaths.push("testcase/md/README.md");
|
|
166
|
+
readme = await readFile(readmePath, "utf8");
|
|
167
|
+
if (!readme.includes("## Coverage Scope")) {
|
|
168
|
+
brokenPaths.push("testcase/md/README.md");
|
|
169
|
+
issues.push({
|
|
170
|
+
code: "T5",
|
|
171
|
+
path: "testcase/md/README.md",
|
|
172
|
+
detail: "README.md is missing ## Coverage Scope",
|
|
173
|
+
recoverable: true,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
if (!readme.includes("## Coverage Matrix") ||
|
|
177
|
+
!hasMarkdownTable(readme, "Rule Key")) {
|
|
178
|
+
if (!brokenPaths.includes("testcase/md/README.md")) {
|
|
179
|
+
brokenPaths.push("testcase/md/README.md");
|
|
180
|
+
}
|
|
181
|
+
issues.push({
|
|
182
|
+
code: "T5",
|
|
183
|
+
path: "testcase/md/README.md",
|
|
184
|
+
detail: "README.md Coverage Matrix table is missing or broken",
|
|
185
|
+
recoverable: true,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const indexedStems = extractModuleStemsFromReadme(readme);
|
|
190
|
+
const existingModules = await listMarkdownModules(workspaceRoot);
|
|
191
|
+
const existingByStem = new Map(existingModules.map((rel) => [
|
|
192
|
+
normalizeBackendTestModuleStem(path.posix.basename(rel)),
|
|
193
|
+
rel,
|
|
194
|
+
]));
|
|
195
|
+
for (const stem of indexedStems) {
|
|
196
|
+
const rel = `testcase/md/${stem}.md`;
|
|
197
|
+
expectedPaths.push(rel);
|
|
198
|
+
const existing = existingByStem.get(stem);
|
|
199
|
+
if (!existing) {
|
|
200
|
+
missingPaths.push(rel);
|
|
201
|
+
issues.push({
|
|
202
|
+
code: "T3",
|
|
203
|
+
path: rel,
|
|
204
|
+
detail: `module file missing for README index stem ${stem}`,
|
|
205
|
+
recoverable: true,
|
|
206
|
+
});
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
actualPaths.push(existing);
|
|
210
|
+
const body = await readFile(path.join(workspaceRoot, existing), "utf8");
|
|
211
|
+
if (!moduleStructurallyComplete(body)) {
|
|
212
|
+
brokenPaths.push(existing);
|
|
213
|
+
issues.push({
|
|
214
|
+
code: "T5",
|
|
215
|
+
path: existing,
|
|
216
|
+
detail: "module file is empty or missing required case sections",
|
|
217
|
+
recoverable: true,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
for (const rel of existingModules) {
|
|
222
|
+
if (!actualPaths.includes(rel))
|
|
223
|
+
actualPaths.push(rel);
|
|
224
|
+
const body = await readFile(path.join(workspaceRoot, rel), "utf8");
|
|
225
|
+
if (!moduleStructurallyComplete(body) && !brokenPaths.includes(rel)) {
|
|
226
|
+
brokenPaths.push(rel);
|
|
227
|
+
issues.push({
|
|
228
|
+
code: "T5",
|
|
229
|
+
path: rel,
|
|
230
|
+
detail: "module file is empty or missing required case sections",
|
|
231
|
+
recoverable: true,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (existingModules.length === 0 &&
|
|
236
|
+
indexedStems.length === 0 &&
|
|
237
|
+
(await exists(readmePath))) {
|
|
238
|
+
issues.push({
|
|
239
|
+
code: "T3",
|
|
240
|
+
detail: "no module Markdown files were produced",
|
|
241
|
+
recoverable: true,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
const targetPaths = orderedUnique([...missingPaths, ...brokenPaths]);
|
|
245
|
+
const nonRecoverable = issues.some((issue) => !issue.recoverable);
|
|
246
|
+
const status = nonRecoverable
|
|
247
|
+
? "NON_RECOVERABLE"
|
|
248
|
+
: targetPaths.length > 0 || issues.length > 0
|
|
249
|
+
? "RECOVERABLE"
|
|
250
|
+
: "PASS";
|
|
251
|
+
return {
|
|
252
|
+
schemaId: BACKEND_TEST_WRITER_PROGRESS_SCHEMA_ID,
|
|
253
|
+
role: "md-generate",
|
|
254
|
+
status,
|
|
255
|
+
expectedPaths: orderedUnique(expectedPaths),
|
|
256
|
+
actualPaths: orderedUnique(actualPaths),
|
|
257
|
+
missingPaths: orderedUnique(missingPaths),
|
|
258
|
+
brokenPaths: orderedUnique(brokenPaths),
|
|
259
|
+
targetPaths,
|
|
260
|
+
issues,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
export async function assessBackendTestPytestWriterCompleteness(workspaceRoot) {
|
|
264
|
+
const issues = [];
|
|
265
|
+
const expectedPaths = [];
|
|
266
|
+
const actualPaths = [];
|
|
267
|
+
const missingPaths = [];
|
|
268
|
+
const brokenPaths = [];
|
|
269
|
+
const modules = await listMarkdownModules(workspaceRoot);
|
|
270
|
+
for (const moduleRel of modules) {
|
|
271
|
+
const expected = expectedBackendTestPytestScriptForMarkdownModule(path.posix.basename(moduleRel));
|
|
272
|
+
expectedPaths.push(expected);
|
|
273
|
+
const absolute = path.join(workspaceRoot, expected);
|
|
274
|
+
if (!(await exists(absolute))) {
|
|
275
|
+
missingPaths.push(expected);
|
|
276
|
+
issues.push({
|
|
277
|
+
code: "T3",
|
|
278
|
+
path: expected,
|
|
279
|
+
detail: `mapped pytest script missing for ${moduleRel}`,
|
|
280
|
+
recoverable: true,
|
|
281
|
+
});
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
actualPaths.push(expected);
|
|
285
|
+
const source = await readFile(absolute, "utf8");
|
|
286
|
+
if (!pythonParseable(source)) {
|
|
287
|
+
brokenPaths.push(expected);
|
|
288
|
+
issues.push({
|
|
289
|
+
code: "T5",
|
|
290
|
+
path: expected,
|
|
291
|
+
detail: "pytest script appears truncated or unparseable",
|
|
292
|
+
recoverable: true,
|
|
293
|
+
});
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
const markdown = await readFile(path.join(workspaceRoot, moduleRel), "utf8");
|
|
297
|
+
const caseIds = orderedUnique([...markdown.matchAll(/\bBE-[A-Z0-9_-]+-\d{2,3}\b/g)].map((item) => item[0]));
|
|
298
|
+
const primaryHints = orderedUnique([
|
|
299
|
+
...markdown.matchAll(/primary\s*symbol\s*[::]\s*`?([A-Za-z0-9_]+)`?/gi),
|
|
300
|
+
].map((item) => item[1]));
|
|
301
|
+
for (const symbol of primaryHints) {
|
|
302
|
+
if (!source.includes(symbol)) {
|
|
303
|
+
brokenPaths.push(expected);
|
|
304
|
+
issues.push({
|
|
305
|
+
code: "T5",
|
|
306
|
+
path: expected,
|
|
307
|
+
detail: `primary symbol ${symbol} not found in mapped script`,
|
|
308
|
+
recoverable: true,
|
|
309
|
+
});
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (caseIds.length > 0 &&
|
|
314
|
+
!caseIds.some((caseId) => source.includes(caseId) || source.includes(caseId.replaceAll("-", "_")))) {
|
|
315
|
+
brokenPaths.push(expected);
|
|
316
|
+
issues.push({
|
|
317
|
+
code: "T5",
|
|
318
|
+
path: expected,
|
|
319
|
+
detail: "no Markdown Case ID found in mapped pytest script",
|
|
320
|
+
recoverable: true,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const targetPaths = orderedUnique([...missingPaths, ...brokenPaths]);
|
|
325
|
+
const status = targetPaths.length > 0 || issues.length > 0 ? "RECOVERABLE" : "PASS";
|
|
326
|
+
return {
|
|
327
|
+
schemaId: BACKEND_TEST_WRITER_PROGRESS_SCHEMA_ID,
|
|
328
|
+
role: "pytest-generate",
|
|
329
|
+
status,
|
|
330
|
+
expectedPaths: orderedUnique(expectedPaths),
|
|
331
|
+
actualPaths: orderedUnique(actualPaths),
|
|
332
|
+
missingPaths: orderedUnique(missingPaths),
|
|
333
|
+
brokenPaths: orderedUnique(brokenPaths),
|
|
334
|
+
targetPaths,
|
|
335
|
+
issues,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
export function classifyBackendTestWriterCompletenessFailure(progress) {
|
|
339
|
+
if (progress.status === "PASS") {
|
|
340
|
+
return {
|
|
341
|
+
failureCategory: "invalid-output",
|
|
342
|
+
reason: "T4",
|
|
343
|
+
recoverable: false,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
if (progress.status === "NON_RECOVERABLE") {
|
|
347
|
+
return {
|
|
348
|
+
failureCategory: "invalid-output",
|
|
349
|
+
reason: "T6",
|
|
350
|
+
recoverable: false,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
const preferred = progress.issues.find((issue) => issue.code === "T5")?.code ??
|
|
354
|
+
progress.issues.find((issue) => issue.code === "T3")?.code ??
|
|
355
|
+
progress.issues.find((issue) => issue.code === "T4")?.code ??
|
|
356
|
+
"T3";
|
|
357
|
+
return {
|
|
358
|
+
failureCategory: "incomplete-write-set",
|
|
359
|
+
reason: preferred,
|
|
360
|
+
recoverable: true,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
export async function writeBackendTestWriterProgressArtifacts(input) {
|
|
364
|
+
const contractsDir = path.join(input.runDir, "contracts");
|
|
365
|
+
await mkdir(contractsDir, { recursive: true });
|
|
366
|
+
const factsPath = path.join(contractsDir, `backend-test-writer-progress-${input.progress.role}.json`);
|
|
367
|
+
const parsed = progressSchema.parse(input.progress);
|
|
368
|
+
await writeFile(factsPath, JSON.stringify(parsed, null, 2), "utf8");
|
|
369
|
+
if (input.progress.status === "PASS") {
|
|
370
|
+
return { factsPath };
|
|
371
|
+
}
|
|
372
|
+
const reportsDir = path.join(input.runDir, "reports");
|
|
373
|
+
await mkdir(reportsDir, { recursive: true });
|
|
374
|
+
const reportPath = path.join(reportsDir, BACKEND_TEST_OUTPUT_LIMIT_RECOVERY_REPORT);
|
|
375
|
+
const attempt = input.attempt ?? 1;
|
|
376
|
+
const maxAttempts = input.maxAttempts ?? 3;
|
|
377
|
+
const classification = classifyBackendTestWriterCompletenessFailure(input.progress);
|
|
378
|
+
const body = [
|
|
379
|
+
"# Backend-test Output Limit Recovery",
|
|
380
|
+
"",
|
|
381
|
+
`- Role: ${input.progress.role}`,
|
|
382
|
+
`- Status: ${input.progress.status}`,
|
|
383
|
+
`- Attempt: ${attempt}/${maxAttempts}`,
|
|
384
|
+
`- Reason: ${classification.reason}`,
|
|
385
|
+
`- Recoverable: ${classification.recoverable}`,
|
|
386
|
+
"",
|
|
387
|
+
"## Target paths",
|
|
388
|
+
"",
|
|
389
|
+
...(input.progress.targetPaths.length
|
|
390
|
+
? input.progress.targetPaths.map((item) => `- ${item}`)
|
|
391
|
+
: ["- (none)"]),
|
|
392
|
+
"",
|
|
393
|
+
"## Issues",
|
|
394
|
+
"",
|
|
395
|
+
...(input.progress.issues.length
|
|
396
|
+
? input.progress.issues.map((issue) => `- [${issue.code}] ${issue.path ?? "(workspace)"}: ${issue.detail}`)
|
|
397
|
+
: ["- None"]),
|
|
398
|
+
"",
|
|
399
|
+
`<!-- sha256:${createHash("sha256").update(JSON.stringify(parsed)).digest("hex")} -->`,
|
|
400
|
+
"",
|
|
401
|
+
].join("\n");
|
|
402
|
+
await writeFile(reportPath, body, "utf8");
|
|
403
|
+
return { factsPath, reportPath };
|
|
404
|
+
}
|
|
405
|
+
export function isBackendTestCompletenessRetryCandidate(task) {
|
|
406
|
+
if (task.executor !== "pi" || task.role !== "implementer")
|
|
407
|
+
return false;
|
|
408
|
+
if (task.toolProfile !== "write" || task.writePolicy !== "exclusive") {
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
if ((task.writeSet?.length ?? 0) === 0)
|
|
412
|
+
return false;
|
|
413
|
+
if (task.writerOutcomePolicy?.type !== "implementation-outcome-v1") {
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
return (task.id === "generate-backend-md-cases-pi" ||
|
|
417
|
+
task.id === "generate-backend-pytest-pi");
|
|
418
|
+
}
|
|
@@ -9,7 +9,7 @@ import { planMavenVerification, } from "../../verification/maven/index.js";
|
|
|
9
9
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
10
10
|
import { BASELINE_FORBIDDEN_PATHS } from "./governance-constants.js";
|
|
11
11
|
import { buildDecisionEnvelopePromptContract } from "./decision-envelope.js";
|
|
12
|
-
import { DEFAULT_READ_ONLY_PI_RETRY_POLICY, PROTOCOL_AWARE_PI_RETRY_POLICY, STRUCTURED_REQUIRED_PI_RETRY_POLICY, isSafeReadOnlyPiRetryCandidate, } from "./retry-policy.js";
|
|
12
|
+
import { DEFAULT_READ_ONLY_PI_RETRY_POLICY, PROTOCOL_AWARE_PI_RETRY_POLICY, STRUCTURED_REQUIRED_PI_RETRY_POLICY, BACKEND_TEST_WRITER_COMPLETENESS_RETRY_POLICY, isSafeReadOnlyPiRetryCandidate, } from "./retry-policy.js";
|
|
13
13
|
import { REVIEW_JSON_VERDICT_OUTPUT_PROTOCOL, REVIEW_VERDICT_OUTPUT_PROTOCOL, } from "./output-protocol.js";
|
|
14
14
|
import { resolveAdapter } from "../../adapters/index.js";
|
|
15
15
|
import { loadHarnessManifest } from "../../governance/harness.js";
|
|
@@ -3716,9 +3716,11 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3716
3716
|
type: "implementation-outcome-v1",
|
|
3717
3717
|
requireChangedFiles: true,
|
|
3718
3718
|
},
|
|
3719
|
+
retryPolicy: BACKEND_TEST_WRITER_COMPLETENESS_RETRY_POLICY,
|
|
3719
3720
|
outputContract: "Write a Chinese, human-readable testcase/md/README.md plus module Markdown case cards using BE-<MODULE>-<NNN>; keep machine IDs/literals exact and do not execute pytest or modify production code/config.",
|
|
3720
3721
|
subtask_prompt: [
|
|
3721
3722
|
"This is a required file-generation node. After reading the bounded inputs, immediately use write/edit tools to create testcase/md/README.md and the module Markdown files. Do not end after analysis or planning, and do not return before a non-empty bounded diff exists.",
|
|
3723
|
+
"Output budget protocol (hard, max output <=16K per turn): Never paste full Matrix, case bodies, or source text into assistant chat. Each write/edit tool call touches at most one file. Order: README.md (Scope+Matrix+module index only) → one module file per turn → short IMPLEMENTATION_OUTCOME. Splitting modules preserves every in-scope rule/TP; it must not drop coverage. Compact tables/lists are required; omitting required sections or in-scope variants is forbidden. If a Completeness Gate / OUTPUT_LIMIT_RECOVERY retry is injected, continue only listed target paths.",
|
|
3722
3724
|
"The first non-empty response line must be exactly IMPLEMENTATION_OUTCOME: changed after the required files have been written, or IMPLEMENTATION_OUTCOME: blocked when precise missing evidence prevents safe generation. already-satisfied is not valid for this node.",
|
|
3723
3725
|
"Read the upstream environment report. Generate a Markdown-first backend test strategy and cases under testcase/md/**.",
|
|
3724
3726
|
"Write human-readable content in Simplified Chinese by default. Keep English only for machine-readable IDs and technical literals such as Case/AC/REQ/BR IDs, HTTP methods, paths, field names, enum values, commands, filenames, code symbols and exact source citations.",
|
|
@@ -3754,6 +3756,8 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3754
3756
|
outputContract: "Review source fidelity and directly revise only testcase/md/**; return concise Markdown, never JSON.",
|
|
3755
3757
|
subtask_prompt: [
|
|
3756
3758
|
"Independently review generated Markdown cases against the task requirements and environment evidence. Treat the files as human-facing test documentation: require clear preconditions, executable steps and assertable expected results; improve names, purpose, metadata and automation mapping where useful while preserving exact machine IDs and technical literals.",
|
|
3759
|
+
"Output budget protocol: default to local edit per file; never dump full Matrix/case bodies into assistant chat. Review order is README (Scope/Matrix) then one module file per turn. When adding omitted in-scope cases, write one file per tool call and keep every required section. Do not bulk-delete in-scope cases to save tokens.",
|
|
3760
|
+
"For every variant Test Point, ensure the Markdown scenario intent is machine-checkable: prefer an explicit line `场景意图: <empty|missing|null|min-1|min|max|max+1|pattern-invalid|enum-invalid|wrong-type|nominal|custom-literal:V>; field=<name>; bound=<n optional>; example=<optional>` near 测试数据/操作步骤, and keep pytest params later aligned to that intent.",
|
|
3757
3761
|
"Independently reconstruct the change classification, affected operations/rules, P0 product scenarios and applicable P1 documented API rules from authoritative sources before trusting the generated Coverage Scope or Coverage Matrix. Perform an explicit coverage-scope review: reject `new-operation` when the task only optimizes an existing implementation without contract change; reject narrow optimization scope when shared validator/helper/DTO/query builder evidence directly affects more operations; reject full-contract expansion across unrelated operations. For every in-scope operation, check applicable lifecycle/uniqueness states (including deleted-existing when in scope), valid enum values, bounded invalid classes, min-1/min/nominal/max/max+1, allowed/forbidden format classes, required/null/missing/wrong-type semantics, status/error codes, auth and state transitions. Directly add in-scope omissions; undefined impact remains GAP/CONFLICT rather than invented behavior.",
|
|
3758
3762
|
"Check AC completeness/meaning, endpoint, fields/shape, status/error codes, rules, states, documented boundaries/auth, positive/negative coverage, executable steps and assertable results. Require the exact `## Coverage Scope` Field/Value table with the `|---|---|` separator row, a valid classification-policy pair, non-empty Affected Operations/Rule Keys/Scope Evidence, and the classification-specific Regression Floor. Require the exact unnumbered `## Coverage Matrix` heading in `testcase/md/README.md`, exact headers, exactly 9 cells in every data row (including a non-empty Dimension), deterministic OpenAPI Rule Keys for every in-scope affected operation, exactly one Matrix row per Rule Key (merge multi-dimension product rows), and bidirectional Matrix Rule/Test Point ↔ Case bindings. Never describe affected-scope coverage as whole-API completeness. Every explicit AC ID must appear in at least one Case `验收标准`; every explicit in-scope AC/REQ/BR Rule Key cited by a Case must have exactly one Coverage Matrix row, and no Case may cite a source Rule Key omitted from the Matrix. Every Matrix Case ID must share at least one of that row's Required Test Points and the Case must cite that Rule Key. Perform an explicit execution-redundancy review: merge checkpoint-only parameter rows, repeated default/read-back assertions, DELETE status/body/follow-up-read checks, response schema/Content-Type checks, PUT full-update/timestamp checks, repeated list setup and identical null/empty inputs when endpoint, input partition, precondition state and expected outcome are the same. Preserve separate POST/PUT, boundary, enum, wrong-type, role/tenant and distinct business-state variants. Directly repair malformed headings/rows/keys and binding modes rather than merely commenting on them. Reject avoidable English prose, duplicated bilingual wording, repeated boilerplate, oversized unstructured sections, a `### 操作步骤` section that contains only a table without any numbered executable line, vague results such as ‘符合预期’, Case-ID-like module filenames (for example `BE-HEALTH.md`), dropped exact `### 操作步骤`/`### 预期结果` headings, and missing or drifted script/function mapping where it can be derived.",
|
|
3759
3763
|
"Correct testcase/md/** directly: add documented omissions, remove unsupported cases, rename module files to stable lowercase stems when needed, normalize every Case ID to hyphen-separated module segments plus exactly three zero-padded digits (`BE-RESOURCE_NOTES-01` → `BE-RESOURCE-NOTES-001`; `BE-RN-011A` must be renumbered or merged) consistently across headings/index/mappings, fix automation mappings so each case points at `testcase/test_<module>.py` derived from that module filename and declares exactly one primary symbol, assign every Test Point exactly one of `变体测试点`/`场景断言测试点`/`横切证据测试点`, ensure every binding-list Test Point is also present in that Case's `### 测试点`, expand every variant parameter row into its own atomic TP ID, make every non-cross-cutting TP Case-specific and owned by exactly one Case, require every primary symbol to start with the canonical Case prefix, ensure every explicit AC ID appears in an applicable Case `验收标准`, merge execution duplicates, improve navigation/tables/Chinese wording, or record gaps in Chinese. Remove every credential/header value, placeholder, fake token and anti-example from Markdown. Sensitive key names may remain only as a plain list; values must be described as runtime-only and omitted, with no colon/value pair or literal example anywhere, including details blocks and explanatory text. Keep Case IDs, AC/REQ/BR IDs, HTTP methods, paths, fields, enum values, filenames, code symbols and source citations as exact machine-readable identifiers; only normalize Case ID separator/sequence formatting as specified above. Recalculate predicted collected items as `sum(max(1, variant count per Case))`; when the task declares a budget, directly merge redundant journeys/reclassify same-request checkpoints until the prediction is within budget, while preserving all required coverage. The validator accepts Chinese and legacy English section aliases; retain or converge to the Chinese human-readable headings without losing structure.",
|
|
@@ -3780,9 +3784,16 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3780
3784
|
],
|
|
3781
3785
|
allowedPaths: Array.from(new Set([...ro, "testcase/**"])),
|
|
3782
3786
|
forbiddenPaths: forbidden,
|
|
3787
|
+
retryPolicy: BACKEND_TEST_WRITER_COMPLETENESS_RETRY_POLICY,
|
|
3788
|
+
writerOutcomePolicy: {
|
|
3789
|
+
type: "implementation-outcome-v1",
|
|
3790
|
+
requireChangedFiles: true,
|
|
3791
|
+
},
|
|
3783
3792
|
outputContract: "Convert every final automatable Markdown case into pytest assets whose actual test function region contains the exact Case ID, preferably in the function name or docstring. Each testcase/md/<module>.md (excluding README.md) maps one-to-one to testcase/test_<module>.py; never merge or split modules. No JSON and no pytest execution.",
|
|
3784
3793
|
subtask_prompt: [
|
|
3785
3794
|
"Convert testcase/md/** to pytest using upstream environment and advisory validation evidence plus only bounded pytest config/conftest. A FAIL advisory report does not authorize inventing missing behavior; use the final Markdown facts that are present.",
|
|
3795
|
+
"Output budget protocol (hard, max output <=16K per turn): Write helpers/factories first, then exactly one test_<module>.py per write/edit tool call following MD stems. Never paste full Python modules into assistant chat. Do not merge modules. Do not reduce params/assertions/skips to fit. If OUTPUT_LIMIT_RECOVERY is injected, continue only listed missing/broken scripts.",
|
|
3796
|
+
"Align every variant pytest.param payload with the Markdown scenario intent (empty/missing/null/length/pattern/enum/wrong-type/nominal). Prefer literal payloads over Faker for intent-critical fields so pre-execution scenario-param checks can verify them.",
|
|
3786
3797
|
'Ensure every final Markdown Case ID appears in exactly one primary pytest test function or pytest test class method region, using the exact `primary symbol` declared by Markdown. The symbol must start with `test_BE_<MODULE>_<NNN>_` so every parameterized collected item remains associated with its Case. Module-level functions and class-based pytest methods are both supported. Only `变体测试点` may use stable `pytest.param(..., id="TP-...")` IDs, and every atomic variant ID must appear exactly once with a genuine input/state/outcome change. Use `pytest.param(..., id=...)` for every row; do not use decorator-level `ids=[...]`, generated suffixes, or IDs that extend/shorten the exact Markdown TP. Do not parameterize `场景断言测试点` or `横切证据测试点`; execute all assertion checkpoints within the same business journey/item and use shared helpers for cross-cutting evidence. The primary symbol docstring must contain exact metadata lines `Case-ID: BE-...`, `Assertion-Test-Points: TP-...;TP-...` and `Cross-Cutting-Test-Points: TP-...;TP-...` (use `none` when empty). No Test Point may be invented, renamed, omitted or bound in two modes. The generated pytest collection shape must equal the Markdown prediction `sum(max(1, variant count per Case))`; keep it at or below the task\'s explicit budget by removing duplicate execution, never by collapsing multiple parameter rows under a coarse family TP. Assertions come only from 预期结果 and setup comes only from 前置条件/测试数据/自动化映射.',
|
|
3787
3798
|
"Name each generated pytest file so it corresponds one-to-one with its source Markdown module file: for each `testcase/md/<module>.md` (excluding README.md), emit exactly one `testcase/test_<module>.py`. The <module> stem is the Markdown filename without the `.md` extension, lowercased and with non-alphanumeric characters replaced by underscores. For example, `testcase/md/resource_notes.md` maps to `testcase/test_resource_notes.py`, `testcase/md/health.md` maps to `testcase/test_health.py`, `testcase/md/BE-HEALTH.md` maps to `testcase/test_be_health.py`, and `testcase/md/order-api.md` maps to `testcase/test_order_api.py`. If Markdown automation mapping names a different path than this module stem path, still write the module stem path and do not invent prefixes such as `test_be_*` unless the module filename itself normalizes to that stem. Never merge multiple Markdown modules into one pytest file, never split one module across several files, and never invent pytest filenames unrelated to the Markdown modules.",
|
|
3788
3799
|
"Generate a reusable HTTP logging helper (or equivalent client wrapper) and call it for every interface request. The request log must include method, URL/path, and request parameters (query plus JSON/body/payload summary). The response log must include status code and response result (JSON/text/body summary), and both records must be visible in pytest stdout/stderr without changing assertions.",
|
|
@@ -3822,6 +3833,7 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3822
3833
|
"Repair the generated backend pytest asset as one bounded program using the direct upstream collection assessment. This is the only repair attempt and happens before any business test body execution.",
|
|
3823
3834
|
"Fix only collection-proven generated testcase-local defects: create the exact safe missing mapped test_*.py paths listed by the initial facts, or repair syntax, module path, missing symbol, circular import, fixture-name, decorator or parameterization inconsistencies. Inspect all affected importers and providers so the repair is cross-file consistent; do not create unrelated pytest scripts.",
|
|
3824
3835
|
"Preserve final testcase/md/** semantics, every Case ID, Rule/Test Point binding, primary symbol, parameter ID, expected status/body/schema assertion, HTTP logging, redaction and truncation behavior.",
|
|
3836
|
+
"Use local edit only on assessment-listed paths; keep summaries short; never rewrite unrelated modules.",
|
|
3825
3837
|
"Do not read task source/** or reinterpret requirements. Do not modify Markdown, conftest, pytest config, production code or dependencies.",
|
|
3826
3838
|
"Do not add skip/skipif/xfail, remove tests, reduce collected items, loosen assertions, swallow exceptions, use try/except ImportError fallback, mutate sys.path/PYTHONPATH, or replace the real API with mocks.",
|
|
3827
3839
|
"Do not execute pytest; the deterministic effective collection gate owns the final collection attempt.",
|
|
@@ -3829,7 +3841,7 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3829
3841
|
};
|
|
3830
3842
|
const collectionEffective = shellNode("effective-backend-pytest-collection-gate-shell", [collectionAssess.id, repairPytest.id], "markdown-collection-effective", "If initial collection passed, verify unchanged asset hashes and reuse it without another collection. If the single repair ran, collect the final mapped scripts once and fail closed unless it passes. BLOCKED initial facts, repair failure, final collection failure or hash drift must prevent business pytest execution.", "Run-owned reports/backend-test-pytest-collection-effective.md and contracts/backend-test-pytest-collection-effective.json proving the exact final assets are collectable; initial PASS is reused, repair path records attempt=1.", [], 120000);
|
|
3831
3843
|
collectionEffective.dependsPolicy = "all-or-condition-skip";
|
|
3832
|
-
const traceability = shellNode("backend-test-traceability-gate-shell", [collectionEffective.id], "markdown-traceability", "Deterministically scan only Markdown-mapped pytest scripts after the effective hash-bound collection gate. Keep the existing traceability/logging checks and produce a bidirectional Markdown module/Case/Test Point ↔ pytest file/primary symbol correspondence analysis. Map variant Test Points from stable parameter IDs, assertion Test Points from the primary symbol docstring, and cross-cutting Test Points from the primary symbol evidence binding. Report 1:1, 1:0, 1:N, 0:1, script/primary-symbol mismatch, missing Case ID, missing variant parameter IDs, missing assertion/cross-cutting bindings, duplicate modes and extra bindings.
|
|
3844
|
+
const traceability = shellNode("backend-test-traceability-gate-shell", [collectionEffective.id], "markdown-traceability", "Deterministically scan only Markdown-mapped pytest scripts after the effective hash-bound collection gate. Keep the existing traceability/logging checks and produce a bidirectional Markdown module/Case/Test Point ↔ pytest file/primary symbol correspondence analysis. Map variant Test Points from stable parameter IDs, assertion Test Points from the primary symbol docstring, and cross-cutting Test Points from the primary symbol evidence binding. Report 1:1, 1:0, 1:N, 0:1, script/primary-symbol mismatch, missing Case ID, missing variant parameter IDs, missing assertion/cross-cutting bindings, duplicate modes and extra bindings. In the same shell, assess scenario-intent vs pytest.param literal features, apply at most one deterministic pre-execution scenario-param repair for repairable MISMATCH entries, reassess final consistency, and bind asset hashes for execute. Findings for correspondence remain advisory; residual scenario-param MISMATCH is advisory unless strictScenarioParamGate is enabled. Never block pytest solely on correspondence FAIL.", "Run-owned reports/backend-test-traceability.md, reports/backend-test-markdown-pytest-correspondence.md, contracts/backend-test-markdown-pytest-correspondence-facts.json, reports/backend-test-scenario-param-consistency.md and contracts/backend-test-scenario-param-consistency-facts.json (initial+final) with optional repair audit; PASS/FAIL/UNAVAILABLE correspondence facts bound after effective collection.");
|
|
3833
3845
|
const manifest = shellNode("backend-test-case-manifest-shell", [traceability.id], "markdown-manifest", "Materialize the canonical Backend Test Case Manifest only from contracts/backend-test-case-coverage-facts.json and contracts/backend-test-markdown-pytest-correspondence-facts.json. Validate schema, task binding, input hashes and freshness; never re-read source semantics, re-analyze Coverage Matrix, rescan pytest symbols or recompute a second set of metrics. Missing/stale/conflicting facts produce partial/unavailable diagnostics rather than fabricated zeros.", "Run-owned contracts/backend-test-case-manifest.json with materializationStatus, sourceFactsIssues, validated coverageScope, coverageSummary, ruleCoverageSummary and correspondenceSummary; this is the single machine input for L-5 and closeout.");
|
|
3834
3846
|
const pytestCommand = [
|
|
3835
3847
|
'mkdir -p "${HARNESS_DAG_RUN_DIR}/reports"',
|
|
@@ -3859,8 +3871,9 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3859
3871
|
? "Final Markdown report and L-5 conclusion under docs/test-reports/**; the deterministic L-5 dashboard at reports/backend-test-l5-dashboard.html is the authoritative visualization and must be linked, not re-rendered; no JSON."
|
|
3860
3872
|
: "Final Markdown report and L-5 conclusion in assistant output; the deterministic L-5 dashboard at reports/backend-test-l5-dashboard.html is the authoritative visualization and must be linked; no JSON or writes.",
|
|
3861
3873
|
subtask_prompt: [
|
|
3862
|
-
"Generate the final Markdown report only from authoritative run-owned artifacts. Read node 1 reports/backend-test-environment.md; node 4 backend-md-case-validation.md and backend-test-case-coverage-analysis.md; nodes 6/8 backend-test-pytest-collection initial/effective reports and facts; node 9 backend-test-traceability.md
|
|
3863
|
-
"Use this exact human-facing section order: 测试结论 → 执行概览 → 质量校验 → 失败分析 → 风险与建议 → 证据与 L-5. Put the decision and key numbers first, use compact tables/bullets, and keep headings concise. Do not paste entire upstream reports, duplicate per-case tables already present in facts, or repeat the same evidence in multiple sections; link to paths/hashes and quote only the findings needed for the conclusion.",
|
|
3874
|
+
"Generate the final Markdown report only from authoritative run-owned artifacts. Read node 1 reports/backend-test-environment.md; node 4 backend-md-case-validation.md and backend-test-case-coverage-analysis.md; nodes 6/8 backend-test-pytest-collection initial/effective reports and facts; node 9 backend-test-traceability.md, backend-test-markdown-pytest-correspondence.md and backend-test-scenario-param-consistency.md; node 10 contracts/backend-test-case-manifest.json; and node 11 backend-test-result.json, backend-test-facts.md, backend-test-failure-analysis.md, pytest-html/HTML and L-5 dashboard. Do not use node 2/3/5 assistant prose as facts. Do not emit JSON.",
|
|
3875
|
+
"Use this exact human-facing section order: 测试结论 → 执行概览 → 质量校验 → 失败分析 → 风险与建议 → 证据与 L-5. Put the decision and key numbers first, use compact tables/bullets, and keep headings concise. Do not paste entire upstream reports, duplicate per-case tables already present in facts, or repeat the same evidence in multiple sections; link to paths/hashes and quote only the findings needed for the conclusion. Prefer linking reports/backend-test-failure-analysis.md for structured failure analysis rather than inventing classifications.",
|
|
3876
|
+
"Output budget: list evidence paths first, then write a short fixed six-section report; never paste upstream full text into chat.",
|
|
3864
3877
|
"The L-5 metrics and visualization are produced deterministically by node 11 at reports/backend-test-l5-dashboard.html. Link to that dashboard as the authoritative L-5 view. Pytest execution facts come from node 11; coverage/correspondence numbers and materializationStatus come from node 10; collection authorization comes from nodes 6/8; detailed coverage findings come from node 4; detailed mapping findings come from node 9. Never recompute these values. If machine manifest and human reports disagree, report evidence inconsistency rather than silently choosing.",
|
|
3865
3878
|
"Always state the exact Coverage Scope classification, policy, affected operations, regression floor, completeness claim, PASS/FAIL/UNAVAILABLE status and findings from node 4 case validation + coverage, plus node 9 traceability + correspondence. Affected-scope or affected-operations-full coverage must never be described as whole-API completeness unless every operation is explicitly listed. Their FAIL status does not block pytest, but it must remain visible and must never be rewritten as PASS.",
|
|
3866
3879
|
"Include environment, case quality/review, automation mapping, exact pytest facts, failure classification/analysis, risks, regression recommendations, evidence paths/hashes, coverage availability, and L-5 READY/NOT READY. Distinguish Markdown Case count, primary pytest symbol count, collected pytest item count, variant/assertion/cross-cutting Test Point counts and execution amplification; never describe pytest item count as the number of business scenarios.",
|
|
@@ -3882,9 +3895,10 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3882
3895
|
...STANDARD_GLOBAL_CONSTRAINTS,
|
|
3883
3896
|
"backend-test-dag uses exactly 12 real top-level tasks. Pytest collection runs once on the green path and at most twice only when one bounded pre-execution repair is eligible; business pytest test bodies execute exactly once over safe scripts explicitly mapped by final Markdown cases.",
|
|
3884
3897
|
"Model nodes produce Markdown and pytest assets, never backend-test business JSON envelopes.",
|
|
3885
|
-
"Environment, advisory Markdown validation/coverage, collection initial/effective facts, advisory traceability/correspondence, canonical manifest, pytest-html, HTML and execution facts are deterministic evidence. Node 4 quality findings stay advisory; nodes 6/8 form the fail-closed collection authorization; node 9 traceability findings stay advisory; node 10 partial/unavailable manifest does not block node 11 when effective collection remains fresh.",
|
|
3898
|
+
"Environment, advisory Markdown validation/coverage, collection initial/effective facts, advisory traceability/correspondence, pre-execution scenario-param consistency (with at most one deterministic param repair), canonical manifest, pytest-html, HTML, failure-analysis and execution facts are deterministic evidence. Node 4 quality findings stay advisory; nodes 6/8 form the fail-closed collection authorization; node 9 traceability/scenario-param findings stay advisory by default; node 10 partial/unavailable manifest does not block node 11 when effective collection remains fresh.",
|
|
3886
3899
|
"Only Markdown case generation/review may read source facts; pytest generation must not read source/**.",
|
|
3887
|
-
"Functional case IDs use canonical BE-<MODULE>-<NNN> with exactly three digits and no alphabetic suffix. Every Test Point has exactly one variant/assertion/cross-cutting binding; only variant bindings create pytest parameter items. Production code/config, skip/xfail, execution-result repair and business pytest rerun are forbidden;
|
|
3900
|
+
"Functional case IDs use canonical BE-<MODULE>-<NNN> with exactly three digits and no alphabetic suffix. Every Test Point has exactly one variant/assertion/cross-cutting binding; only variant bindings create pytest parameter items. Production code/config, skip/xfail, execution-result repair and business pytest rerun are forbidden; pre-execution repairs are limited to one collection-proven generated-test asset repair and one scenario-param payload repair (deterministic preferred).",
|
|
3901
|
+
"Writer nodes must obey multi-file output-budget protocol under 16K max tokens: one file per write/edit, no chat dumps; Completeness Gate may trigger bounded incomplete-write-set recovery without lowering coverage quality.",
|
|
3888
3902
|
],
|
|
3889
3903
|
defaults: {
|
|
3890
3904
|
...BACKEND_TEST_DEFAULTS,
|
|
@@ -8,7 +8,7 @@ import { writeNodeRecord, writeNodeSkillArtifacts } from "./run-store.js";
|
|
|
8
8
|
import { resolveContextPolicy } from "./context-policy.js";
|
|
9
9
|
import { buildDagNodePromptEnvelope, formatConvergenceFeedbackBlock, } from "./prompt.js";
|
|
10
10
|
import { persistLongNodeOutputArtifacts } from "./upstream-artifacts.js";
|
|
11
|
-
import { computeBackoffDelayMs, isRetryablePiFailureCategory, isSafeReadOnlyPiRetryCandidate, } from "./retry-policy.js";
|
|
11
|
+
import { computeBackoffDelayMs, isRetryablePiFailureCategory, isSafeReadOnlyPiRetryCandidate, isWriterEmptyDiffRetryCandidate, } from "./retry-policy.js";
|
|
12
12
|
import { applyNodeActivity, evaluateNodeLiveness, resolveLivenessPolicy, } from "./liveness-policy.js";
|
|
13
13
|
import { buildProtocolRetryInstruction, normalizeReviewVerdictAfterRetries, parseJsonReviewVerdict, validateOutputProtocol, } from "./output-protocol.js";
|
|
14
14
|
import { writeDagNodeJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
|
|
@@ -137,6 +137,33 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
|
|
|
137
137
|
buildProtocolRetryInstruction(task.outputProtocol, previousProtocolReason),
|
|
138
138
|
].join("\n");
|
|
139
139
|
}
|
|
140
|
+
if (previousFailureCategory === "writer-empty-diff") {
|
|
141
|
+
return [
|
|
142
|
+
basePrompt,
|
|
143
|
+
"",
|
|
144
|
+
"<retry_instruction>",
|
|
145
|
+
"Previous attempt made no file changes.",
|
|
146
|
+
"Use write/edit tools immediately to create the required bounded files; do not repeat analysis or planning.",
|
|
147
|
+
"Return only after producing a non-empty bounded diff and the required implementation outcome.",
|
|
148
|
+
"</retry_instruction>",
|
|
149
|
+
].join("\n");
|
|
150
|
+
}
|
|
151
|
+
if (previousFailureCategory === "incomplete-write-set") {
|
|
152
|
+
const maxAttempts = task.retryPolicy?.maxAttempts ?? 3;
|
|
153
|
+
return [
|
|
154
|
+
basePrompt,
|
|
155
|
+
"",
|
|
156
|
+
"<retry_instruction>",
|
|
157
|
+
`OUTPUT_LIMIT_RECOVERY attempt=${attemptNumber}/${maxAttempts}`,
|
|
158
|
+
"reason=T3_or_T5",
|
|
159
|
+
"Previous attempt produced an incomplete or structurally broken write set (Completeness Gate).",
|
|
160
|
+
"Continue or repair ONLY missing/broken target paths listed in reports/backend-test-output-limit-recovery.md and contracts/backend-test-writer-progress-*.json when present.",
|
|
161
|
+
"One file per write/edit; no chat dump of full bodies; do not delete or shrink in-scope cases/rules/TPs.",
|
|
162
|
+
"Prefer edit/append for truncated files; rewrite a file only if unparseable or empty.",
|
|
163
|
+
"After targets are complete, short IMPLEMENTATION_OUTCOME only.",
|
|
164
|
+
"</retry_instruction>",
|
|
165
|
+
].join("\n");
|
|
166
|
+
}
|
|
140
167
|
if (task.outputMode !== "structured-required" ||
|
|
141
168
|
previousFailureCategory !== "output-too-large") {
|
|
142
169
|
return basePrompt;
|
|
@@ -513,7 +540,9 @@ export async function executeDagNode(input) {
|
|
|
513
540
|
// Invalid or missing harness preserves the previous no-override behavior.
|
|
514
541
|
}
|
|
515
542
|
}
|
|
516
|
-
const retryPolicy = task.retryPolicy &&
|
|
543
|
+
const retryPolicy = task.retryPolicy &&
|
|
544
|
+
(isSafeReadOnlyPiRetryCandidate(task) ||
|
|
545
|
+
isWriterEmptyDiffRetryCandidate(task))
|
|
517
546
|
? task.retryPolicy
|
|
518
547
|
: undefined;
|
|
519
548
|
const maxAttempts = retryPolicy?.maxAttempts ?? 1;
|