@tea-agent/loop-agent 0.33.7-beta.1 → 0.33.7-beta.2
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/CHANGELOG.md
CHANGED
|
@@ -3,12 +3,24 @@
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
5
|
### 改进
|
|
6
|
+
- scenario-param 识别 `"x" * _TITLE_MAX_LENGTH` 等长度表达式与 helper 常量名相乘形式
|
|
7
|
+
- correspondence 忽略 pytest payload 中的 `expect`/`echo_*` 辅助键,避免把 DTO 外 helper 字段误判为 PAYLOAD_SHAPE_MISMATCH
|
|
8
|
+
- scenario-param 解析 `bound=LEN100` 机器行、解引用 `_STATUS_ACTIVE` 等 Python 字符串常量,并把 `NOMINAL` 长度样本从 min/max 纠正为 nominal
|
|
6
9
|
|
|
7
10
|
- scenario-param 观测识别 `_OMIT`/omit sentinel 为合法 `missing`,跳过 `"2xx"`/`"400"` 期望码位置参数,解码 Python `\t`/`\n` 空白字面量,识别 `_TITLE_LEN1` 等长度 helper 名,支持 `custom-literal:whitespace-only|whitespace-padded` 语义标签,`enum-invalid` 接受具体非法字面量,并在 `min`/`LEN1` 意图下优先读取 TP 编码长度与 `minLength` 边界
|
|
8
11
|
- scenario-param 兼容 `empty` 意图下的 omit/`missing-key`,规范化 `custom-literal` 外包引号与可见空白符,并把 `max+1`/`min`/`max` 的裸数字位置参数按长度 token 比较(含 oversized 数字本身);`custom-literal:trim` 视为 padded whitespace 语义标签
|
|
9
12
|
- backend-test writer 合同明确:`enum-invalid` 必须使用具体非法字面量且禁止 `_OMIT`/missing-key;`custom-literal:trim|ACTIVE|ARCHIVED` 必须使用真实 padded/enum 字面量,禁止 `filter-active` 类描述性伪值
|
|
10
13
|
- scenario-param 识别 `_*_MAX_LENGTH` / `_*_OVER_LENGTH` 等长度 helper 名,明确 `custom-literal:trim` 不能是全空白,并把无 pytest.param 的 request-level/health nominal 记为 MATCH 以免污染 eligibility
|
|
11
14
|
|
|
15
|
+
## [0.33.7-beta.2] - 2026-08-13
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- scenario-param 解析 `bound=LEN100`、解引用 `_STATUS_ACTIVE` 等 Python 字符串常量,识别 `"x" * _TITLE_MAX_LENGTH` 长度表达式
|
|
19
|
+
- correspondence 忽略 pytest payload 中的 `expect`/`echo_*` 辅助键,避免假 PAYLOAD_SHAPE_MISMATCH
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- my-webapp 五轮 backend-test 全流程 live campaign 归档(R1–R5 均 finished;R5 correspondence PASS)
|
|
23
|
+
|
|
12
24
|
## [0.33.7-beta.1] - 2026-08-12
|
|
13
25
|
|
|
14
26
|
### Fixed
|
|
@@ -1152,6 +1152,9 @@ def collect_dict(node, prefix, paths, values):
|
|
|
1152
1152
|
if not isinstance(node, ast.Dict): return
|
|
1153
1153
|
for key,value in zip(node.keys,node.values):
|
|
1154
1154
|
if not isinstance(key, ast.Constant) or not isinstance(key.value,str): continue
|
|
1155
|
+
# Ignore writer helper/meta keys that are not DTO request fields.
|
|
1156
|
+
if key.value in {'expect','expected','expected_status','expected_code','echo_title','echo_content','echo_status'} or key.value.startswith('echo_') or key.value.startswith('expected_'):
|
|
1157
|
+
continue
|
|
1155
1158
|
current = key.value if not prefix else prefix + '.' + key.value
|
|
1156
1159
|
paths.add(current)
|
|
1157
1160
|
lv=literal(value)
|
|
@@ -121,26 +121,43 @@ async function listMarkdownCases(workspaceRoot) {
|
|
|
121
121
|
}
|
|
122
122
|
return cases;
|
|
123
123
|
}
|
|
124
|
+
function parseScenarioBoundToken(raw) {
|
|
125
|
+
if (!raw)
|
|
126
|
+
return undefined;
|
|
127
|
+
const token = raw.trim();
|
|
128
|
+
if (!token || /^LEN<nominal>$/i.test(token) || /^NOMINAL$/i.test(token)) {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
const direct = Number(token);
|
|
132
|
+
if (Number.isFinite(direct) && direct >= 0)
|
|
133
|
+
return direct;
|
|
134
|
+
const len = /^(?:LEN(?:GTH)?[_-]?)?(\d{1,5})$/i.exec(token);
|
|
135
|
+
if (len) {
|
|
136
|
+
const n = Number(len[1]);
|
|
137
|
+
return Number.isFinite(n) ? n : undefined;
|
|
138
|
+
}
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
124
141
|
function parseScenarioIntentLine(line) {
|
|
125
142
|
// Target may contain commas (body.title,body.status). Intent may contain `|` / spaces.
|
|
126
143
|
// Stop field values only at known trailing keys or end-of-line.
|
|
127
|
-
const precise = /场景意图\s*[::]\s*(TP-[A-Z0-9-]+)\s*[|;]\s*operation\s*=\s*([^;\n]+?)\s*[|;]\s*target\s*=\s*([^;\n]+?)\s*[|;]\s*intent\s*=\s*(
|
|
144
|
+
const precise = /场景意图\s*[::]\s*(TP-[A-Z0-9-]+)\s*[|;]\s*operation\s*=\s*([^;\n]+?)\s*[|;]\s*target\s*=\s*([^;\n]+?)\s*[|;]\s*intent\s*=\s*([^;\n]+?)(?:\s*[|;]\s*bound\s*=\s*([^\s;|\n]+))?(?:\s*[|;]\s*example\s*=\s*([^;\n]+))?(?:\s*[|;]\s*expectedCode\s*=\s*[^;\n]+)?\s*$/i.exec(line.trim());
|
|
128
145
|
if (precise) {
|
|
129
146
|
return {
|
|
130
147
|
tpId: precise[1],
|
|
131
148
|
rawIntent: (precise[4] ?? "unknown").trim(),
|
|
132
149
|
target: precise[3]?.trim(),
|
|
133
|
-
bound:
|
|
150
|
+
bound: parseScenarioBoundToken(precise[5]),
|
|
134
151
|
example: precise[6]?.trim(),
|
|
135
152
|
legacy: false,
|
|
136
153
|
};
|
|
137
154
|
}
|
|
138
|
-
const legacy = /场景意图\s*[::]\s*([a-z0-9+._:-]+)(?:\s*[|;,,]\s*field\s*=\s*([A-Za-z0-9_.]+))?(?:\s*[|;,,]\s*bound\s*=\s*(
|
|
155
|
+
const legacy = /场景意图\s*[::]\s*([a-z0-9+._:-]+)(?:\s*[|;,,]\s*field\s*=\s*([A-Za-z0-9_.]+))?(?:\s*[|;,,]\s*bound\s*=\s*([^\s;|,,\n]+))?(?:\s*[|;,,]\s*example\s*=\s*([^|\n]+))?/i.exec(line.trim());
|
|
139
156
|
if (legacy) {
|
|
140
157
|
return {
|
|
141
158
|
rawIntent: (legacy[1] ?? "unknown").trim(),
|
|
142
159
|
field: legacy[2],
|
|
143
|
-
bound:
|
|
160
|
+
bound: parseScenarioBoundToken(legacy[3]),
|
|
144
161
|
example: legacy[4]?.trim(),
|
|
145
162
|
legacy: true,
|
|
146
163
|
};
|
|
@@ -220,7 +237,13 @@ export function inferScenarioParamIntent(input) {
|
|
|
220
237
|
const rawIntent = machine.rawIntent.trim();
|
|
221
238
|
const example = machine.example;
|
|
222
239
|
const lower = rawIntent.toLowerCase();
|
|
223
|
-
|
|
240
|
+
let normalizedIntent = lower === "nominal-operation" ? "nominal" : lower;
|
|
241
|
+
// Writers sometimes mark a short "typical" length sample as min/max with bound=LEN<nominal>.
|
|
242
|
+
// Treat that as nominal so a short helper/string is not forced to equal maxLength.
|
|
243
|
+
if ((normalizedIntent === "min" || normalizedIntent === "max") &&
|
|
244
|
+
/NOMINAL/i.test(tpId)) {
|
|
245
|
+
normalizedIntent = "nominal";
|
|
246
|
+
}
|
|
224
247
|
const dataSection = sectionBody(caseBody, ["测试数据", "Test Data", "操作步骤", "Steps"]);
|
|
225
248
|
const haystack = `${caseBody}\n${dataSection}`;
|
|
226
249
|
const fieldEsc = field
|
|
@@ -315,6 +338,24 @@ export function inferScenarioParamIntent(input) {
|
|
|
315
338
|
"HTTP",
|
|
316
339
|
"RESPONSE",
|
|
317
340
|
"REQUEST",
|
|
341
|
+
"NOTES",
|
|
342
|
+
"NOTE",
|
|
343
|
+
"REQUIRED",
|
|
344
|
+
"LENGTH",
|
|
345
|
+
"OVER",
|
|
346
|
+
"FILTER",
|
|
347
|
+
"LIST",
|
|
348
|
+
"ROUNDTRIP",
|
|
349
|
+
"READINESS",
|
|
350
|
+
"DEFAULT",
|
|
351
|
+
"TRIM",
|
|
352
|
+
"BLANK",
|
|
353
|
+
"WHITESPACE",
|
|
354
|
+
"NORMALIZE",
|
|
355
|
+
"FAIL",
|
|
356
|
+
"SUCCESS",
|
|
357
|
+
"LOWERCASE",
|
|
358
|
+
"UPPERCASE",
|
|
318
359
|
].includes(part.toUpperCase()))[0]
|
|
319
360
|
?.replace(/_/g, "") ?? undefined;
|
|
320
361
|
const field = fieldFromTp && fieldFromTp.length > 1
|
|
@@ -340,7 +381,7 @@ export function inferScenarioParamIntent(input) {
|
|
|
340
381
|
if (/\bNULL\b|空值/.test(upper) || /\bnull\b/i.test(caseBody)) {
|
|
341
382
|
return { intent: "null", field, bound, example, intentSource: "tp-fallback" };
|
|
342
383
|
}
|
|
343
|
-
if (/MAX[-_]?PLUS[-_]?1|MAX\+1
|
|
384
|
+
if (/MAX[-_]?PLUS[-_]?1|MAX\+1|超长|越界|LENGTH[-_]?OVER|OVER[-_]?LENGTH|OVER/.test(upper)) {
|
|
344
385
|
return { intent: "max+1", field, bound, example, intentSource: "tp-fallback" };
|
|
345
386
|
}
|
|
346
387
|
if (/\bMAX\b|最大/.test(upper)) {
|
|
@@ -423,7 +464,25 @@ function featuresFromStringValue(value) {
|
|
|
423
464
|
literal: decoded,
|
|
424
465
|
};
|
|
425
466
|
}
|
|
426
|
-
function
|
|
467
|
+
function collectPythonStringConstants(source) {
|
|
468
|
+
const constants = new Map();
|
|
469
|
+
const re = /\b([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(["'])([^"'\\]*)\2/g;
|
|
470
|
+
let match;
|
|
471
|
+
while ((match = re.exec(source)) !== null) {
|
|
472
|
+
const name = match[1] ?? "";
|
|
473
|
+
const value = match[3] ?? "";
|
|
474
|
+
if (name && !constants.has(name))
|
|
475
|
+
constants.set(name, value);
|
|
476
|
+
}
|
|
477
|
+
return constants;
|
|
478
|
+
}
|
|
479
|
+
function resolveNamedConstant(name, constants) {
|
|
480
|
+
if (!constants?.has(name))
|
|
481
|
+
return undefined;
|
|
482
|
+
const value = constants.get(name) ?? "";
|
|
483
|
+
return featuresFromStringValue(value);
|
|
484
|
+
}
|
|
485
|
+
function observeLiteralToken(raw, field, constants) {
|
|
427
486
|
const trimmed = raw.trim();
|
|
428
487
|
if (!trimmed)
|
|
429
488
|
return undefined;
|
|
@@ -446,6 +505,35 @@ function observeLiteralToken(raw, field) {
|
|
|
446
505
|
const value = ch.repeat(Math.max(0, length));
|
|
447
506
|
return featuresFromStringValue(value);
|
|
448
507
|
}
|
|
508
|
+
// "x" * _TITLE_MAX_LENGTH / "a" * _TITLE_LEN100
|
|
509
|
+
const repeatedConst = /^(['"])([^'"\\])\1\s*\*\s*([A-Za-z_][A-Za-z0-9_]*)$/.exec(trimmed);
|
|
510
|
+
if (repeatedConst) {
|
|
511
|
+
const helper = repeatedConst[3] ?? "";
|
|
512
|
+
const fromConst = resolveNamedConstant(helper, constants);
|
|
513
|
+
if (fromConst?.kind === "string" && typeof fromConst.length === "number") {
|
|
514
|
+
return {
|
|
515
|
+
kind: "number",
|
|
516
|
+
text: trimmed,
|
|
517
|
+
length: fromConst.length,
|
|
518
|
+
literal: String(fromConst.length),
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
const lenFromName = /(?:^|_)LEN(?:GTH)?_?(\d{1,5})$/i.exec(helper) ??
|
|
522
|
+
/LEN(?:GTH)?_?(\d{1,5})$/i.exec(helper);
|
|
523
|
+
if (lenFromName) {
|
|
524
|
+
const length = Number(lenFromName[1] ?? "0");
|
|
525
|
+
return { kind: "number", text: trimmed, length, literal: String(length) };
|
|
526
|
+
}
|
|
527
|
+
if (/(?:OVER|OVERSIZE|TOO[_-]?LONG|MAX[_-]?PLUS[_-]?1|MAXPLUS1)/i.test(helper)) {
|
|
528
|
+
return { kind: "name", text: helper, length: -2, literal: "oversize-helper" };
|
|
529
|
+
}
|
|
530
|
+
if (/(?:MAX[_-]?LENGTH|LENGTH[_-]?MAX|_MAX_LEN(?:GTH)?$)/i.test(helper)) {
|
|
531
|
+
return { kind: "name", text: helper, length: -1, literal: "max-length-helper" };
|
|
532
|
+
}
|
|
533
|
+
if (/(?:MIN[_-]?LENGTH|LENGTH[_-]?MIN|_MIN_LEN(?:GTH)?$)/i.test(helper)) {
|
|
534
|
+
return { kind: "name", text: helper, length: -3, literal: "min-length-helper" };
|
|
535
|
+
}
|
|
536
|
+
}
|
|
449
537
|
const triple = /^("""|''')([\s\S]*)\1$/.exec(trimmed);
|
|
450
538
|
if (triple) {
|
|
451
539
|
return featuresFromStringValue(triple[2] ?? "");
|
|
@@ -469,7 +557,7 @@ function observeLiteralToken(raw, field) {
|
|
|
469
557
|
const inner = trimmed.replace(/^\(/, "").replace(/\)$/, "");
|
|
470
558
|
const first = inner.split(",")[0]?.trim();
|
|
471
559
|
if (first && first !== trimmed) {
|
|
472
|
-
return observeLiteralToken(first, field);
|
|
560
|
+
return observeLiteralToken(first, field, constants);
|
|
473
561
|
}
|
|
474
562
|
return { kind: "call", text: trimmed };
|
|
475
563
|
}
|
|
@@ -503,6 +591,9 @@ function observeLiteralToken(raw, field) {
|
|
|
503
591
|
if (/(?:MIN[_-]?LENGTH|LENGTH[_-]?MIN|_MIN_LEN(?:GTH)?$)/i.test(trimmed)) {
|
|
504
592
|
return { kind: "name", text: trimmed, length: -3, literal: "min-length-helper" };
|
|
505
593
|
}
|
|
594
|
+
const resolved = resolveNamedConstant(trimmed, constants);
|
|
595
|
+
if (resolved)
|
|
596
|
+
return resolved;
|
|
506
597
|
return { kind: "name", text: trimmed };
|
|
507
598
|
}
|
|
508
599
|
return { kind: "unknown", text: trimmed };
|
|
@@ -534,7 +625,7 @@ export function extractPytestParamBlock(source, tpId) {
|
|
|
534
625
|
}
|
|
535
626
|
return undefined;
|
|
536
627
|
}
|
|
537
|
-
export function observeParamFeatures(block, field) {
|
|
628
|
+
export function observeParamFeatures(block, field, constants) {
|
|
538
629
|
if (!block)
|
|
539
630
|
return { kind: "unknown", text: "param-block-missing" };
|
|
540
631
|
const compact = block.replace(/\s+/g, " ");
|
|
@@ -563,7 +654,7 @@ export function observeParamFeatures(block, field) {
|
|
|
563
654
|
}
|
|
564
655
|
const valueMatch = new RegExp(`["']${field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["']\\s*:\\s*([^,}\n]+)`).exec(dictText);
|
|
565
656
|
const raw = valueMatch?.[1]?.trim() ?? "";
|
|
566
|
-
return (observeLiteralToken(raw, field) ?? {
|
|
657
|
+
return (observeLiteralToken(raw, field, constants) ?? {
|
|
567
658
|
kind: "unknown",
|
|
568
659
|
text: raw || compact.slice(0, 120),
|
|
569
660
|
});
|
|
@@ -597,7 +688,7 @@ export function observeParamFeatures(block, field) {
|
|
|
597
688
|
if (paramOpen) {
|
|
598
689
|
const afterOpen = block.slice((paramOpen.index ?? 0) + paramOpen[0].length);
|
|
599
690
|
const positionalTokens = [];
|
|
600
|
-
const tokenRe = /\s*(_OMIT|OMIT|MISSING|Ellipsis|\.\.\.|None|True|False|-?\d+(?:\.\d+)?|"""[\s\S]*?"""|\'\'\'[\s\S]*?\'\'\'|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[A-Za-z_][A-Za-z0-9_]*)\s*(?:,|\)|$)/g;
|
|
691
|
+
const tokenRe = /\s*(_OMIT|OMIT|MISSING|Ellipsis|\.\.\.|None|True|False|-?\d+(?:\.\d+)?|"""[\s\S]*?"""|\'\'\'[\s\S]*?\'\'\'|(?:["'][^"'\\]["']\s*\*\s*(?:\d{1,5}|[A-Za-z_][A-Za-z0-9_]*))|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[A-Za-z_][A-Za-z0-9_]*)\s*(?:,|\)|$)/g;
|
|
601
692
|
let match;
|
|
602
693
|
while ((match = tokenRe.exec(afterOpen)) !== null) {
|
|
603
694
|
const between = afterOpen.slice(0, match.index);
|
|
@@ -612,7 +703,7 @@ export function observeParamFeatures(block, field) {
|
|
|
612
703
|
const idMatch = /\bid\s*=\s*["'](TP-[A-Z0-9-]+)["']/i.exec(block);
|
|
613
704
|
const idTp = idMatch?.[1]?.toUpperCase();
|
|
614
705
|
const candidates = positionalTokens.filter((raw) => {
|
|
615
|
-
const observed = observeLiteralToken(raw, field);
|
|
706
|
+
const observed = observeLiteralToken(raw, field, constants);
|
|
616
707
|
if (!observed)
|
|
617
708
|
return false;
|
|
618
709
|
if (observed.kind === "string" &&
|
|
@@ -632,7 +723,7 @@ export function observeParamFeatures(block, field) {
|
|
|
632
723
|
});
|
|
633
724
|
const raw = candidates[0];
|
|
634
725
|
if (raw) {
|
|
635
|
-
return (observeLiteralToken(raw, field) ?? {
|
|
726
|
+
return (observeLiteralToken(raw, field, constants) ?? {
|
|
636
727
|
kind: "unknown",
|
|
637
728
|
text: raw,
|
|
638
729
|
});
|
|
@@ -925,15 +1016,21 @@ export async function assessBackendScenarioParamConsistency(input) {
|
|
|
925
1016
|
caseBody: testCase.body,
|
|
926
1017
|
});
|
|
927
1018
|
const fallbackSourced = inferred.intentSource === "tp-fallback";
|
|
1019
|
+
const constants = source ? collectPythonStringConstants(source) : undefined;
|
|
928
1020
|
const block = source ? extractPytestParamBlock(source, tpId) : undefined;
|
|
929
|
-
const observed = observeParamFeatures(block, inferred.field);
|
|
930
|
-
// Request-level / health checks often have no pytest.param payload row
|
|
931
|
-
//
|
|
932
|
-
// do not pollute eligibility with false UNDETERMINED residuals.
|
|
1021
|
+
const observed = observeParamFeatures(block, inferred.field, constants);
|
|
1022
|
+
// Request-level / health checks often have no pytest.param payload row,
|
|
1023
|
+
// or only a TP-id label positional (no field value).
|
|
933
1024
|
const requestLevelNoParam = Boolean(source) &&
|
|
934
|
-
|
|
935
|
-
observed.text === "param-block-missing"
|
|
936
|
-
|
|
1025
|
+
(inferred.intent === "nominal" || inferred.intent === "unknown") &&
|
|
1026
|
+
((!block && observed.text === "param-block-missing") ||
|
|
1027
|
+
(Boolean(block) &&
|
|
1028
|
+
!inferred.field &&
|
|
1029
|
+
(observed.kind === "unknown" ||
|
|
1030
|
+
observed.kind === "missing-key" ||
|
|
1031
|
+
(observed.kind === "string" &&
|
|
1032
|
+
Boolean(observed.literal) &&
|
|
1033
|
+
/^TP-[A-Z0-9-]+$/i.test(observed.literal ?? "")))));
|
|
937
1034
|
const status = !source
|
|
938
1035
|
? "UNDETERMINED"
|
|
939
1036
|
: requestLevelNoParam
|
|
@@ -3739,7 +3739,7 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3739
3739
|
subtask_prompt: [
|
|
3740
3740
|
"Convert testcase/md/** into pytest using upstream environment and advisory validation evidence plus only bounded pytest config/conftest. This node writes ONLY the shared pytest support assets (HTTP logging/redaction helper, request factories, shared fixtures); each module's test_<module>.py is written by a downstream sharded node that imports these helpers. A FAIL advisory report does not authorize inventing missing behavior; use the final Markdown facts that are present.",
|
|
3741
3741
|
"Output budget protocol (hard, max output <=16K per turn): Write helpers/factories one file per write/edit tool call. Never paste full Python modules into assistant chat. Do not reduce params/assertions/skips semantics to fit. If OUTPUT_LIMIT_RECOVERY is injected, continue only listed missing/broken shared files.",
|
|
3742
|
-
"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. Hard contract: intent=enum-invalid MUST pass a concrete invalid value literal (string/number/boolean), never `_OMIT`/None/missing key; intent=missing/empty may use `_OMIT` or delete the key; intent=custom-literal:trim|whitespace-padded requires a leading/trailing whitespace string with non-empty trimmed content (all-whitespace belongs to empty/whitespace-only, not trim); intent=custom-literal:ACTIVE|ARCHIVED requires the exact enum string, never descriptive tokens like filter-active; intent=max/min/max+1 should pass a repeated-string length expression, a bare length number N, or a helper named _*_LEN{N} / _*_MAX_LENGTH / _*_OVER_LENGTH — never a bare 1 for oversize.",
|
|
3742
|
+
"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. Hard contract: intent=enum-invalid MUST pass a concrete invalid value literal (string/number/boolean), never `_OMIT`/None/missing key; intent=missing/empty may use `_OMIT` or delete the key; intent=custom-literal:trim|whitespace-padded requires a leading/trailing whitespace string with non-empty trimmed content (all-whitespace belongs to empty/whitespace-only, not trim); intent=custom-literal:ACTIVE|ARCHIVED requires the exact enum string, never descriptive tokens like filter-active; intent=max/min/max+1 should pass a repeated-string length expression, a bare length number N, or a helper named _*_LEN{N} / _*_MAX_LENGTH / _*_OVER_LENGTH — never a bare 1 for oversize. Hard contract: request payload dicts may only contain DTO field keys from Payload Allowed Paths; never put expect/expected/echo_* helper keys inside the JSON body dict.",
|
|
3743
3743
|
"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. If shared pytest fixtures are generated, keep their dependency graph in one provider module and require each downstream test module to register that provider with an exact pytest_plugins tuple; importing only the outer fixture is insufficient and will be rejected by fixture-resolution preflight.",
|
|
3744
3744
|
"HTTP response header names are case-insensitive. If the helper stores a lower-case normalized header map, every Content-Type or other header assertion must query the lower-case key (for example `content-type`) or use an explicitly case-insensitive accessor; never call a case-sensitive plain dict with `Content-Type` when the stored key is lower-case. Preserve the actual media-type assertion rather than dropping it.",
|
|
3745
3745
|
"Compare timestamps and other semantically equivalent protocol values by parsed meaning, not byte-for-byte serialization. In particular, normalize valid ISO-8601 instants before equality/order assertions so differences such as omitted trailing fractional seconds do not create TestBug failures; preserve exact-string assertions only when the Markdown explicitly requires representation equality.",
|
|
@@ -3817,7 +3817,7 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3817
3817
|
subtaskPromptTemplate: [
|
|
3818
3818
|
"Convert the single Markdown module testcase/md/{{item.stem}}.md into one self-contained pytest module. After reading the module Markdown and the bounded pytest config/conftest, immediately use write tools to create the single file testcase/test_{{item.stem}}.py. Define any bounded HTTP client fixture, request logging/redaction/truncation helper and payload builders needed by this module inside that same file; do not import generated testcase/**/helpers/** or testcase/**/factories/** assets. Do not end after analysis or planning. Do not modify Markdown, conftest, helpers/factories, or any other module's pytest script.",
|
|
3819
3819
|
"Output budget protocol (hard, max output <=16K per turn): Write exactly one test_{{item.stem}}.py. Never paste full Python modules into assistant chat. Do not merge or split modules. Do not reduce params/assertions/skips to fit. If OUTPUT_LIMIT_RECOVERY is injected, continue only listed missing/broken scripts.",
|
|
3820
|
-
"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. Hard contract: intent=enum-invalid MUST pass a concrete invalid value literal (string/number/boolean), never `_OMIT`/None/missing key; intent=missing/empty may use `_OMIT` or delete the key; intent=custom-literal:trim|whitespace-padded requires a leading/trailing whitespace string with non-empty trimmed content (all-whitespace belongs to empty/whitespace-only, not trim); intent=custom-literal:ACTIVE|ARCHIVED requires the exact enum string, never descriptive tokens like filter-active; intent=max/min/max+1 should pass a repeated-string length expression, a bare length number N, or a helper named _*_LEN{N} / _*_MAX_LENGTH / _*_OVER_LENGTH — never a bare 1 for oversize.",
|
|
3820
|
+
"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. Hard contract: intent=enum-invalid MUST pass a concrete invalid value literal (string/number/boolean), never `_OMIT`/None/missing key; intent=missing/empty may use `_OMIT` or delete the key; intent=custom-literal:trim|whitespace-padded requires a leading/trailing whitespace string with non-empty trimmed content (all-whitespace belongs to empty/whitespace-only, not trim); intent=custom-literal:ACTIVE|ARCHIVED requires the exact enum string, never descriptive tokens like filter-active; intent=max/min/max+1 should pass a repeated-string length expression, a bare length number N, or a helper named _*_LEN{N} / _*_MAX_LENGTH / _*_OVER_LENGTH — never a bare 1 for oversize. Hard contract: request payload dicts may only contain DTO field keys from Payload Allowed Paths; never put expect/expected/echo_* helper keys inside the JSON body dict.",
|
|
3821
3821
|
'Ensure every automatable final Markdown Case ID in this module appears in exactly one primary pytest test function or pytest test class method region, using the exact `primary symbol` declared by Markdown. Skip evidence-only meta Cases that declare `脚本/primary symbol=无` with empty variants; do not invent a business pytest symbol for them. 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). Implement request dictionaries so their direct and nested key paths and enum literals exactly satisfy the Case `Payload Required Paths`, `Payload Allowed Paths`, and `Payload Enum`; for `Payload Contract: none`, do not invent a JSON/body DTO. 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 前置条件/测试数据/自动化映射.',
|
|
3822
3822
|
"Name the generated pytest file so it corresponds one-to-one with its source Markdown module file: this module stem `{{item.stem}}` maps to exactly one `testcase/test_{{item.stem}}.py`. The <module> stem is the Markdown filename without the `.md` extension, lowercased and with non-alphanumeric characters replaced by underscores. For example, `resource_notes` → `testcase/test_resource_notes.py`, `health` → `testcase/test_health.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. 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.",
|
|
3823
3823
|
"Keep this module self-contained: define module-local fixtures and helpers directly in testcase/test_{{item.stem}}.py, so pytest discovers every fixture dependency without external plugin registration. 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. Recursively redact sensitive values and apply bounded truncation before logging.",
|