@tea-agent/loop-agent 0.33.7-beta.0 → 0.33.7-beta.1

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
@@ -2,6 +2,22 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ### 改进
6
+
7
+ - 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
+ - scenario-param 兼容 `empty` 意图下的 omit/`missing-key`,规范化 `custom-literal` 外包引号与可见空白符,并把 `max+1`/`min`/`max` 的裸数字位置参数按长度 token 比较(含 oversized 数字本身);`custom-literal:trim` 视为 padded whitespace 语义标签
9
+ - backend-test writer 合同明确:`enum-invalid` 必须使用具体非法字面量且禁止 `_OMIT`/missing-key;`custom-literal:trim|ACTIVE|ARCHIVED` 必须使用真实 padded/enum 字面量,禁止 `filter-active` 类描述性伪值
10
+ - scenario-param 识别 `_*_MAX_LENGTH` / `_*_OVER_LENGTH` 等长度 helper 名,明确 `custom-literal:trim` 不能是全空白,并把无 pytest.param 的 request-level/health nominal 记为 MATCH 以免污染 eligibility
11
+
12
+ ## [0.33.7-beta.1] - 2026-08-12
13
+
14
+ ### Fixed
15
+ - scenario-param 识别 `_*_MAX_LENGTH` / `_*_OVER_LENGTH` 等长度 helper 名,明确 `custom-literal:trim` 不能是全空白,并把无 pytest.param 的 request-level/health nominal 记为 MATCH 以免污染 eligibility
16
+ - backend-test writer 合同明确:`enum-invalid` 必须使用具体非法字面量且禁止 `_OMIT`/missing-key;`custom-literal:trim|ACTIVE|ARCHIVED` 必须使用真实 padded/enum 字面量;max/min/max+1 应用长度表达式/helper
17
+
18
+ ### Changed
19
+ - 归档 R108–R110 live campaign 证据与 residual 收紧结果
20
+
5
21
  ## [0.33.7-beta.0] - 2026-08-11
6
22
 
7
23
  ### 重点更新
@@ -222,18 +222,48 @@ export function inferScenarioParamIntent(input) {
222
222
  const lower = rawIntent.toLowerCase();
223
223
  const normalizedIntent = lower === "nominal-operation" ? "nominal" : lower;
224
224
  const dataSection = sectionBody(caseBody, ["测试数据", "Test Data", "操作步骤", "Steps"]);
225
- const fieldScopedBound = field
226
- ? new RegExp(`${field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[^\\n]{0,80}?(?:maxLength|max(?:imum)?|上限|最大长度)\\s*[=::]?\\s*(\\d{1,4})`, "i").exec(`${caseBody}\n${dataSection}`) ??
227
- new RegExp(`(?:maxLength|max(?:imum)?|上限|最大长度)\\s*[=::]?\\s*(\\d{1,4})[^\\n]{0,80}?${field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "i").exec(`${caseBody}\n${dataSection}`)
225
+ const haystack = `${caseBody}\n${dataSection}`;
226
+ const fieldEsc = field
227
+ ? field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
228
228
  : undefined;
229
- const boundFromBody = fieldScopedBound ??
230
- /(?:maxLength|max(?:imum)?|上限|最大长度)\s*[=::]?\s*(\d{1,4})/i.exec(`${caseBody}\n${dataSection}`) ??
231
- /(?:minLength|min(?:imum)?|下限|最小长度)\s*[=::]?\s*(\d{1,4})/i.exec(`${caseBody}\n${dataSection}`);
229
+ const maxBoundFromBody = fieldEsc
230
+ ? new RegExp(`${fieldEsc}[^\\n]{0,80}?(?:maxLength|max(?:imum)?|上限|最大长度)\\s*[=::]?\\s*(\\d{1,4})`, "i").exec(haystack) ??
231
+ new RegExp(`(?:maxLength|max(?:imum)?|上限|最大长度)\\s*[=::]?\\s*(\\d{1,4})[^\\n]{0,80}?${fieldEsc}`, "i").exec(haystack) ??
232
+ /(?:maxLength|max(?:imum)?|上限|最大长度)\s*[=::]?\s*(\d{1,4})/i.exec(haystack)
233
+ : /(?:maxLength|max(?:imum)?|上限|最大长度)\s*[=::]?\s*(\d{1,4})/i.exec(haystack);
234
+ const minBoundFromBody = fieldEsc
235
+ ? new RegExp(`${fieldEsc}[^\\n]{0,80}?(?:minLength|min(?:imum)?|下限|最小长度)\\s*[=::]?\\s*(\\d{1,4})`, "i").exec(haystack) ??
236
+ new RegExp(`(?:minLength|min(?:imum)?|下限|最小长度)\\s*[=::]?\\s*(\\d{1,4})[^\\n]{0,80}?${fieldEsc}`, "i").exec(haystack) ??
237
+ /(?:minLength|min(?:imum)?|下限|最小长度)\s*[=::]?\s*(\d{1,4})/i.exec(haystack)
238
+ : /(?:minLength|min(?:imum)?|下限|最小长度)\s*[=::]?\s*(\d{1,4})/i.exec(haystack);
239
+ const prefersMinBound = normalizedIntent === "min" ||
240
+ normalizedIntent === "min-1" ||
241
+ /(?:MIN|LEN1|LENGTH-?1|最短|最小)/i.test(tpId);
242
+ const boundFromBody = prefersMinBound
243
+ ? minBoundFromBody ?? maxBoundFromBody
244
+ : maxBoundFromBody ?? minBoundFromBody;
245
+ const boundFromTp = /(?:LEN(?:GTH)?|长度)[_-]?(\d{1,5})(?:[_-]|$)/i.exec(tpId) ??
246
+ /(?:MIN|MAX)[_-]?(\d{1,5})(?:[_-]|$)/i.exec(tpId);
247
+ const lengthIntent = normalizedIntent === "min" ||
248
+ normalizedIntent === "min-1" ||
249
+ normalizedIntent === "max" ||
250
+ normalizedIntent === "max+1";
251
+ // Length intents: explicit machine bound >0, then TP-encoded LEN1/LEN100,
252
+ // then field min/maxLength. Prevents maxLength=100 shadowing min/LEN1.
253
+ const boundFromTpNumber = boundFromTp ? Number(boundFromTp[1]) : undefined;
254
+ // max+1 TP tokens encode the oversized length (LEN101), so convert to base bound.
255
+ const boundFromTpNormalized = boundFromTpNumber === undefined
256
+ ? undefined
257
+ : normalizedIntent === "max+1"
258
+ ? Math.max(0, boundFromTpNumber - 1)
259
+ : boundFromTpNumber;
232
260
  const bound = machine.bound && machine.bound > 0
233
261
  ? machine.bound
234
- : boundFromBody
235
- ? Number(boundFromBody[1])
236
- : machine.bound;
262
+ : lengthIntent && boundFromTpNormalized !== undefined
263
+ ? boundFromTpNormalized
264
+ : boundFromBody
265
+ ? Number(boundFromBody[1])
266
+ : machine.bound;
237
267
  if (rawIntent.toLowerCase().startsWith("custom-literal:")) {
238
268
  const intent = resolveCustomLiteralIntent(rawIntent, field);
239
269
  return {
@@ -275,6 +305,16 @@ export function inferScenarioParamIntent(input) {
275
305
  "VARIANT",
276
306
  "WHITESPACE",
277
307
  "UNKNOWN",
308
+ "HEALTH",
309
+ "UP",
310
+ "OK",
311
+ "GET",
312
+ "API",
313
+ "STATUS",
314
+ "CODE",
315
+ "HTTP",
316
+ "RESPONSE",
317
+ "REQUEST",
278
318
  ].includes(part.toUpperCase()))[0]
279
319
  ?.replace(/_/g, "") ?? undefined;
280
320
  const field = fieldFromTp && fieldFromTp.length > 1
@@ -326,6 +366,147 @@ export function inferScenarioParamIntent(input) {
326
366
  }
327
367
  return { intent: "unknown", field, bound, example, intentSource: "tp-fallback" };
328
368
  }
369
+ const OMIT_SENTINEL_RE = /^(?:_OMIT|OMIT|MISSING|Ellipsis|\.\.\.)\b/;
370
+ const HTTP_EXPECTED_CODE_RE = /^(?:2xx|3xx|4xx|5xx|[1-5]\d{2}|HTTP_[1-5]\d{2})$/i;
371
+ /** Semantic custom-literal tokens that describe a value class, not a raw string. */
372
+ function isWhitespaceSemanticLiteral(token) {
373
+ return /^(?:whitespace-only|ws-only|blank|blank-only|spaces-only|space-only|empty-ws|empty-whitespace)$/i.test(token.trim());
374
+ }
375
+ function isWhitespacePaddedSemanticLiteral(token) {
376
+ return /^(?:whitespace-padded|ws-padded|padded-whitespace|leading-trailing-ws|trim-target|trim|trimmed|pad|padded)$/i.test(token.trim());
377
+ }
378
+ function isWhitespaceOnlyString(value) {
379
+ return value.length > 0 && /^\s+$/.test(value);
380
+ }
381
+ function isWhitespacePaddedString(value) {
382
+ return value.length > 0 && value !== value.trim() && value.trim().length > 0;
383
+ }
384
+ function normalizeScenarioLiteralText(value) {
385
+ // Writers sometimes emit visible-space placeholders (U+2420) or NBSP variants.
386
+ return value
387
+ .replace(/␠/g, " ")
388
+ .replace(/ /g, " ")
389
+ .replace(/ /g, " ")
390
+ .replace(/ /g, " ");
391
+ }
392
+ function stripWrappingQuotes(value) {
393
+ const trimmed = value.trim();
394
+ if ((trimmed.startsWith('"') && trimmed.endsWith('"') && trimmed.length >= 2) ||
395
+ (trimmed.startsWith("'") && trimmed.endsWith("'") && trimmed.length >= 2)) {
396
+ return trimmed.slice(1, -1);
397
+ }
398
+ return value;
399
+ }
400
+ function normalizeCustomLiteralExpected(raw) {
401
+ return normalizeScenarioLiteralText(stripWrappingQuotes(raw));
402
+ }
403
+ function isHttpExpectedCodeLiteral(value) {
404
+ return HTTP_EXPECTED_CODE_RE.test(value.trim());
405
+ }
406
+ /** Decode common Python string escapes found in source text (not JSON). */
407
+ function unescapePythonStringLiteral(value) {
408
+ return value
409
+ .replace(/\\n/g, "\n")
410
+ .replace(/\\t/g, "\t")
411
+ .replace(/\\r/g, "\r")
412
+ .replace(/\\'/g, "'")
413
+ .replace(/\\"/g, '"')
414
+ .replace(/\\\\/g, "\\");
415
+ }
416
+ function featuresFromStringValue(value) {
417
+ const decoded = normalizeScenarioLiteralText(unescapePythonStringLiteral(value));
418
+ return {
419
+ kind: decoded.length === 0 ? "empty-string" : "string",
420
+ text: JSON.stringify(decoded),
421
+ length: decoded.length,
422
+ hasUppercase: /[A-Z]/.test(decoded),
423
+ literal: decoded,
424
+ };
425
+ }
426
+ function observeLiteralToken(raw, field) {
427
+ const trimmed = raw.trim();
428
+ if (!trimmed)
429
+ return undefined;
430
+ if (OMIT_SENTINEL_RE.test(trimmed) || trimmed === "...") {
431
+ return {
432
+ kind: "missing-key",
433
+ text: field ? `missing:${field}` : "missing-key",
434
+ };
435
+ }
436
+ if (trimmed === "None" || trimmed === "null") {
437
+ return { kind: "none", text: "None", literal: "None" };
438
+ }
439
+ if (trimmed === '""' || trimmed === "''") {
440
+ return { kind: "empty-string", text: '""', length: 0, literal: "" };
441
+ }
442
+ const repeatedValue = /^(['"])([^'"\\])\1\s*\*\s*(\d{1,5})$/.exec(trimmed);
443
+ if (repeatedValue) {
444
+ const ch = repeatedValue[2] ?? "x";
445
+ const length = Number(repeatedValue[3] ?? "0");
446
+ const value = ch.repeat(Math.max(0, length));
447
+ return featuresFromStringValue(value);
448
+ }
449
+ const triple = /^("""|''')([\s\S]*)\1$/.exec(trimmed);
450
+ if (triple) {
451
+ return featuresFromStringValue(triple[2] ?? "");
452
+ }
453
+ const str = /^['"]([\s\S]*)['"]$/.exec(trimmed);
454
+ if (str) {
455
+ return featuresFromStringValue(str[1] ?? "");
456
+ }
457
+ if (/^-?\d+(?:\.\d+)?$/.test(trimmed)) {
458
+ return { kind: "number", text: trimmed, literal: trimmed };
459
+ }
460
+ if (trimmed === "True" || trimmed === "False") {
461
+ return { kind: "boolean", text: trimmed, literal: trimmed };
462
+ }
463
+ if (trimmed.startsWith("["))
464
+ return { kind: "list", text: trimmed };
465
+ if (trimmed.startsWith("{"))
466
+ return { kind: "dict", text: trimmed };
467
+ if (/\(/.test(trimmed)) {
468
+ // Tuple/list positional wrappers: (_TITLE_LEN1, "2xx") → first meaningful token.
469
+ const inner = trimmed.replace(/^\(/, "").replace(/\)$/, "");
470
+ const first = inner.split(",")[0]?.trim();
471
+ if (first && first !== trimmed) {
472
+ return observeLiteralToken(first, field);
473
+ }
474
+ return { kind: "call", text: trimmed };
475
+ }
476
+ if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(trimmed)) {
477
+ if (OMIT_SENTINEL_RE.test(trimmed)) {
478
+ return {
479
+ kind: "missing-key",
480
+ text: field ? `missing:${field}` : "missing-key",
481
+ };
482
+ }
483
+ // Length helper names used by writers:
484
+ // _TITLE_LEN1 / _CONTENT_LEN2000 / _TITLE_MAX_LENGTH / _CONTENT_OVER_LENGTH
485
+ const lenFromName = /(?:^|_)LEN(?:GTH)?_?(\d{1,5})$/i.exec(trimmed) ??
486
+ /LEN(?:GTH)?_?(\d{1,5})$/i.exec(trimmed);
487
+ if (lenFromName) {
488
+ const length = Number(lenFromName[1] ?? "0");
489
+ return {
490
+ kind: "number",
491
+ text: trimmed,
492
+ length,
493
+ literal: String(length),
494
+ };
495
+ }
496
+ // Opaque max/min/oversize helpers resolve against Markdown bound in compareIntent.
497
+ if (/(?:OVER|OVERSIZE|TOO[_-]?LONG|MAX[_-]?PLUS[_-]?1|MAXPLUS1)/i.test(trimmed)) {
498
+ return { kind: "name", text: trimmed, length: -2, literal: "oversize-helper" };
499
+ }
500
+ if (/(?:MAX[_-]?LENGTH|LENGTH[_-]?MAX|_MAX_LEN(?:GTH)?$)/i.test(trimmed)) {
501
+ return { kind: "name", text: trimmed, length: -1, literal: "max-length-helper" };
502
+ }
503
+ if (/(?:MIN[_-]?LENGTH|LENGTH[_-]?MIN|_MIN_LEN(?:GTH)?$)/i.test(trimmed)) {
504
+ return { kind: "name", text: trimmed, length: -3, literal: "min-length-helper" };
505
+ }
506
+ return { kind: "name", text: trimmed };
507
+ }
508
+ return { kind: "unknown", text: trimmed };
509
+ }
329
510
  export function extractPytestParamBlock(source, tpId) {
330
511
  const idPattern = new RegExp(`id\\s*=\\s*["']${tpId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["']`);
331
512
  const matches = [...source.matchAll(/pytest\.param\s*\(/g)];
@@ -380,55 +561,12 @@ export function observeParamFeatures(block, field) {
380
561
  if (!dictHasField) {
381
562
  return { kind: "missing-key", text: `missing:${field}` };
382
563
  }
383
- const dict = dictText;
384
- const valueMatch = new RegExp(`["']${field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["']\\s*:\\s*([^,}\\n]+)`).exec(dict);
564
+ const valueMatch = new RegExp(`["']${field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["']\\s*:\\s*([^,}\n]+)`).exec(dictText);
385
565
  const raw = valueMatch?.[1]?.trim() ?? "";
386
- if (raw === "None" || raw === "null") {
387
- return { kind: "none", text: "None", literal: "None" };
388
- }
389
- if (raw === '""' || raw === "''") {
390
- return { kind: "empty-string", text: '""', length: 0, literal: "" };
391
- }
392
- const repeatedValue = /^(["'])([^"'\\])\1\s*\*\s*(\d{1,5})$/.exec(raw);
393
- if (repeatedValue) {
394
- const ch = repeatedValue[2] ?? "x";
395
- const length = Number(repeatedValue[3] ?? "0");
396
- const value = ch.repeat(Math.max(0, length));
397
- return {
398
- kind: length === 0 ? "empty-string" : "string",
399
- text: JSON.stringify(value),
400
- length,
401
- hasUppercase: /[A-Z]/.test(value),
402
- literal: value,
403
- };
404
- }
405
- const str = /^["']([\s\S]*)["']$/.exec(raw);
406
- if (str) {
407
- const value = str[1] ?? "";
408
- return {
409
- kind: "string",
410
- text: JSON.stringify(value),
411
- length: value.length,
412
- hasUppercase: /[A-Z]/.test(value),
413
- literal: value,
414
- };
415
- }
416
- if (/^\d+(?:\.\d+)?$/.test(raw)) {
417
- return { kind: "number", text: raw, literal: raw };
418
- }
419
- if (raw === "True" || raw === "False") {
420
- return { kind: "boolean", text: raw, literal: raw };
421
- }
422
- if (raw.startsWith("["))
423
- return { kind: "list", text: raw };
424
- if (raw.startsWith("{"))
425
- return { kind: "dict", text: raw };
426
- if (/\(/.test(raw))
427
- return { kind: "call", text: raw };
428
- if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(raw)) {
429
- return { kind: "name", text: raw };
430
- }
431
- return { kind: "unknown", text: raw || compact.slice(0, 120) };
566
+ return (observeLiteralToken(raw, field) ?? {
567
+ kind: "unknown",
568
+ text: raw || compact.slice(0, 120),
569
+ });
432
570
  }
433
571
  // Repeated string literal: "a" * 100 / 'x'*101
434
572
  const repeated = /pytest\.param\s*\(\s*(["'])([^"'\\])\1\s*\*\s*(\d{1,5})/.exec(block) ??
@@ -445,75 +583,113 @@ export function observeParamFeatures(block, field) {
445
583
  literal: value,
446
584
  };
447
585
  }
586
+ // Bare omit sentinel before id=...
587
+ if (/pytest\.param\s*\(\s*(?:_OMIT|OMIT|MISSING|\.\.\.|Ellipsis)\s*(?:,|\)|$)/.test(block)) {
588
+ return {
589
+ kind: "missing-key",
590
+ text: field ? `missing:${field}` : "missing-key",
591
+ };
592
+ }
448
593
  // Positional args: skip a leading TP-id label when writers emit
449
594
  // pytest.param("TP-...", actual_value, id="TP-...").
595
+ // Also skip HTTP expected-code tokens (2xx/400) that are not field values.
450
596
  const paramOpen = /pytest\.param\s*\(/.exec(block);
451
597
  if (paramOpen) {
452
598
  const afterOpen = block.slice((paramOpen.index ?? 0) + paramOpen[0].length);
453
- const positionalLiterals = [];
454
- const literalRe = /\s*(None|True|False|-?\d+(?:\.\d+)?|"""[\s\S]*?"""|'''[\s\S]*?'''|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')\s*(?:,|$)/g;
599
+ 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;
455
601
  let match;
456
- while ((match = literalRe.exec(afterOpen)) !== null) {
457
- // Stop once keyword args begin (id=/marks=) after a comma segment without a pure literal.
602
+ while ((match = tokenRe.exec(afterOpen)) !== null) {
458
603
  const between = afterOpen.slice(0, match.index);
459
604
  if (/(?:^|,)\s*(?:id|marks)\s*=/.test(between + match[0]))
460
605
  break;
461
- positionalLiterals.push(match[1]);
462
- if (positionalLiterals.length >= 4)
606
+ if (/^(?:id|marks)$/i.test(match[1] ?? ""))
607
+ break;
608
+ positionalTokens.push(match[1]);
609
+ if (positionalTokens.length >= 6)
463
610
  break;
464
611
  }
465
612
  const idMatch = /\bid\s*=\s*["'](TP-[A-Z0-9-]+)["']/i.exec(block);
466
613
  const idTp = idMatch?.[1]?.toUpperCase();
467
- const candidates = positionalLiterals.filter((raw) => {
468
- const str = /^["']([\s\S]*)["']$/.exec(raw) ?? /^("""|''')([\s\S]*)\1$/.exec(raw);
469
- if (!str)
470
- return true;
471
- const value = (str[2] ?? str[1] ?? "").replace(/^["']|["']$/g, "");
472
- // Drop pure TP label positionals that only echo the param id.
473
- if (/^TP-[A-Z0-9-]+$/i.test(value) && (!idTp || value.toUpperCase() === idTp)) {
614
+ const candidates = positionalTokens.filter((raw) => {
615
+ const observed = observeLiteralToken(raw, field);
616
+ if (!observed)
617
+ return false;
618
+ if (observed.kind === "string" &&
619
+ observed.literal &&
620
+ /^TP-[A-Z0-9-]+$/i.test(observed.literal) &&
621
+ (!idTp || observed.literal.toUpperCase() === idTp)) {
622
+ return false;
623
+ }
624
+ // Only drop string expected-code tokens ("2xx"/"400"). Bare numbers stay
625
+ // because they are commonly length bounds (e.g. 100 for maxLength).
626
+ if (observed.kind === "string" &&
627
+ observed.literal &&
628
+ isHttpExpectedCodeLiteral(observed.literal)) {
474
629
  return false;
475
630
  }
476
631
  return true;
477
632
  });
478
- const raw = candidates[0] ?? positionalLiterals[0];
633
+ const raw = candidates[0];
479
634
  if (raw) {
480
- if (raw === "None")
481
- return { kind: "none", text: "None", literal: "None" };
482
- if (raw === '""' || raw === "''") {
483
- return { kind: "empty-string", text: '""', length: 0, literal: "" };
484
- }
485
- const str = /^["']([\s\S]*)["']$/.exec(raw) ?? /^("""|''')([\s\S]*)\1$/.exec(raw);
486
- if (str) {
487
- const value = (str[2] ?? str[1] ?? "").replace(/^["']|["']$/g, "");
488
- return {
489
- kind: value.length === 0
490
- ? "empty-string"
491
- : "string",
492
- text: JSON.stringify(value),
493
- length: value.length,
494
- hasUppercase: /[A-Z]/.test(value),
495
- literal: value,
496
- };
497
- }
498
- if (/^-?\d+(?:\.\d+)?$/.test(raw)) {
499
- return { kind: "number", text: raw, literal: raw };
500
- }
501
- if (raw === "True" || raw === "False") {
502
- return { kind: "boolean", text: raw, literal: raw };
503
- }
504
- return { kind: "unknown", text: raw };
635
+ return (observeLiteralToken(raw, field) ?? {
636
+ kind: "unknown",
637
+ text: raw,
638
+ });
639
+ }
640
+ // Only expected-code / TP-label tokens remain -> treat as omitted field value.
641
+ if (positionalTokens.length > 0 && field) {
642
+ return { kind: "missing-key", text: `missing:${field}` };
505
643
  }
506
644
  }
507
645
  return { kind: "unknown", text: compact.slice(0, 160) };
508
646
  }
647
+ function observedLengthValue(observed) {
648
+ if (observed.kind === "number" && observed.literal !== undefined) {
649
+ const n = Number(observed.literal);
650
+ return Number.isFinite(n) ? n : undefined;
651
+ }
652
+ if (observed.kind === "string") {
653
+ // Bare digit strings in pytest.param are length tokens, not character payloads.
654
+ if (observed.literal && /^-?\d+$/.test(observed.literal.trim())) {
655
+ const n = Number(observed.literal.trim());
656
+ return Number.isFinite(n) ? n : undefined;
657
+ }
658
+ return observed.length;
659
+ }
660
+ return undefined;
661
+ }
662
+ function isMaxLengthHelper(observed) {
663
+ return (observed.literal === "max-length-helper" ||
664
+ (observed.kind === "name" &&
665
+ /(?:MAX[_-]?LENGTH|LENGTH[_-]?MAX|_MAX_LEN(?:GTH)?$)/i.test(observed.text)));
666
+ }
667
+ function isMinLengthHelper(observed) {
668
+ return (observed.literal === "min-length-helper" ||
669
+ (observed.kind === "name" &&
670
+ /(?:MIN[_-]?LENGTH|LENGTH[_-]?MIN|_MIN_LEN(?:GTH)?$)/i.test(observed.text)));
671
+ }
672
+ function isOversizeLengthHelper(observed) {
673
+ return (observed.literal === "oversize-helper" ||
674
+ (observed.kind === "name" &&
675
+ /(?:OVER|OVERSIZE|TOO[_-]?LONG|MAX[_-]?PLUS[_-]?1|MAXPLUS1)/i.test(observed.text)));
676
+ }
509
677
  function compareIntent(intent, observed, bound, example, field) {
510
678
  if (intent === "unknown")
511
679
  return "UNDETERMINED";
512
- if (observed.kind === "call" || observed.kind === "name") {
680
+ // Opaque length helpers are resolved below against bound; other names stay undetermined.
681
+ if (observed.kind === "call" ||
682
+ (observed.kind === "name" &&
683
+ !isMaxLengthHelper(observed) &&
684
+ !isMinLengthHelper(observed) &&
685
+ !isOversizeLengthHelper(observed))) {
513
686
  return "UNDETERMINED";
514
687
  }
515
688
  switch (intent) {
516
689
  case "empty":
690
+ // Empty-string samples match; OMIT/DEFAULT writers often use missing-key instead.
691
+ if (observed.kind === "missing-key")
692
+ return "MATCH";
517
693
  return observed.kind === "empty-string" ||
518
694
  (observed.kind === "string" &&
519
695
  (observed.length === 0 ||
@@ -538,37 +714,47 @@ function compareIntent(intent, observed, bound, example, field) {
538
714
  case "max":
539
715
  if (bound === undefined)
540
716
  return "UNDETERMINED";
541
- if (observed.kind === "string" && observed.length === bound)
542
- return "MATCH";
543
- // Writers may pass the exact length as a numeric positional instead of a repeated string.
544
- if (observed.kind === "number" && Number(observed.literal) === bound)
545
- return "MATCH";
546
- return observed.kind === "string" || observed.kind === "number" ? "MISMATCH" : "UNDETERMINED";
717
+ {
718
+ if (isMaxLengthHelper(observed))
719
+ return "MATCH";
720
+ const len = observedLengthValue(observed);
721
+ if (len === bound)
722
+ return "MATCH";
723
+ return len !== undefined ? "MISMATCH" : "UNDETERMINED";
724
+ }
547
725
  case "max+1":
548
726
  if (bound === undefined)
549
727
  return "UNDETERMINED";
550
- if (observed.kind === "string" && observed.length === bound + 1)
551
- return "MATCH";
552
- if (observed.kind === "number" && Number(observed.literal) === bound + 1)
553
- return "MATCH";
554
- return observed.kind === "string" || observed.kind === "number" ? "MISMATCH" : "UNDETERMINED";
728
+ {
729
+ if (isOversizeLengthHelper(observed))
730
+ return "MATCH";
731
+ const len = observedLengthValue(observed);
732
+ // Accept base+1 or the oversized length itself when bound already is oversized.
733
+ if (len === bound + 1 || len === bound)
734
+ return "MATCH";
735
+ return len !== undefined ? "MISMATCH" : "UNDETERMINED";
736
+ }
555
737
  case "min":
556
738
  if (bound === undefined)
557
739
  return "UNDETERMINED";
558
- if (observed.kind === "string" && observed.length === bound)
559
- return "MATCH";
560
- if (observed.kind === "number" && Number(observed.literal) === bound)
561
- return "MATCH";
562
- return observed.kind === "string" || observed.kind === "number" ? "MISMATCH" : "UNDETERMINED";
740
+ {
741
+ if (isMinLengthHelper(observed))
742
+ return "MATCH";
743
+ const len = observedLengthValue(observed);
744
+ if (len === bound)
745
+ return "MATCH";
746
+ return len !== undefined ? "MISMATCH" : "UNDETERMINED";
747
+ }
563
748
  case "min-1":
564
749
  if (bound === undefined)
565
750
  return "UNDETERMINED";
566
- const expectedMin1 = Math.max(0, bound - 1);
567
- if (observed.kind === "string" && observed.length === expectedMin1)
568
- return "MATCH";
569
- if (observed.kind === "number" && Number(observed.literal) === expectedMin1)
570
- return "MATCH";
571
- return observed.kind === "string" || observed.kind === "number" ? "MISMATCH" : "UNDETERMINED";
751
+ {
752
+ const expectedMin1 = Math.max(0, bound - 1);
753
+ const len = observedLengthValue(observed);
754
+ if (len === expectedMin1)
755
+ return "MATCH";
756
+ return len !== undefined ? "MISMATCH" : "UNDETERMINED";
757
+ }
572
758
  case "pattern-invalid":
573
759
  if (observed.kind === "string" && example !== undefined) {
574
760
  return observed.literal === example ? "MATCH" : "MISMATCH";
@@ -584,9 +770,16 @@ function compareIntent(intent, observed, bound, example, field) {
584
770
  ? "MISMATCH"
585
771
  : "UNDETERMINED";
586
772
  case "enum-invalid":
587
- return observed.kind === "string" || observed.kind === "number"
773
+ // A concrete non-empty invalid enum literal is a valid negative sample.
774
+ if (observed.kind === "string" && (observed.literal?.length ?? 0) > 0) {
775
+ return "MATCH";
776
+ }
777
+ if (observed.kind === "number" || observed.kind === "boolean") {
778
+ return "MATCH";
779
+ }
780
+ return observed.kind === "unknown" || observed.kind === "missing-key"
588
781
  ? "UNDETERMINED"
589
- : "UNDETERMINED";
782
+ : "MISMATCH";
590
783
  case "nominal":
591
784
  // Request-level nominal (no body field) may legitimately pass None/empty query filters.
592
785
  if (!field && (observed.kind === "none" || observed.kind === "missing-key")) {
@@ -603,8 +796,36 @@ function compareIntent(intent, observed, bound, example, field) {
603
796
  : "UNDETERMINED";
604
797
  default:
605
798
  if (intent.startsWith("custom-literal:")) {
606
- const expected = intent.slice("custom-literal:".length);
607
- return observed.literal === expected
799
+ const expectedRaw = intent.slice("custom-literal:".length);
800
+ const expected = normalizeCustomLiteralExpected(expectedRaw);
801
+ const literal = normalizeScenarioLiteralText(observed.literal ?? "");
802
+ if (isWhitespaceSemanticLiteral(expected) || isWhitespaceOnlyString(expected)) {
803
+ return observed.kind === "empty-string" ||
804
+ (observed.kind === "string" && isWhitespaceOnlyString(literal))
805
+ ? "MATCH"
806
+ : observed.kind === "unknown"
807
+ ? "UNDETERMINED"
808
+ : "MISMATCH";
809
+ }
810
+ if (isWhitespacePaddedSemanticLiteral(expected)) {
811
+ // trim/padded requires non-empty content after strip; all-whitespace is empty/ws-only.
812
+ if (isWhitespaceOnlyString(literal) || observed.kind === "empty-string") {
813
+ return "MISMATCH";
814
+ }
815
+ return observed.kind === "string" && isWhitespacePaddedString(literal)
816
+ ? "MATCH"
817
+ : observed.kind === "unknown"
818
+ ? "UNDETERMINED"
819
+ : "MISMATCH";
820
+ }
821
+ if (isWhitespacePaddedString(expected)) {
822
+ return observed.kind === "string" && literal === expected
823
+ ? "MATCH"
824
+ : observed.kind === "unknown"
825
+ ? "UNDETERMINED"
826
+ : "MISMATCH";
827
+ }
828
+ return literal === expected
608
829
  ? "MATCH"
609
830
  : observed.kind === "unknown"
610
831
  ? "UNDETERMINED"
@@ -670,8 +891,17 @@ function suggestedFixFor(intent, field, bound, example) {
670
891
  : undefined;
671
892
  default:
672
893
  if (intent.startsWith("custom-literal:")) {
894
+ const expected = intent.slice("custom-literal:".length);
895
+ if (isWhitespaceSemanticLiteral(expected)) {
896
+ return field ? `set ${field}=" "` : 'set value=" "';
897
+ }
898
+ if (isWhitespacePaddedSemanticLiteral(expected)) {
899
+ return field
900
+ ? `set ${field}=" ${field}-value "`
901
+ : 'set value=" value "';
902
+ }
673
903
  return field
674
- ? `set ${field}=${JSON.stringify(intent.slice("custom-literal:".length))}`
904
+ ? `set ${field}=${JSON.stringify(expected)}`
675
905
  : undefined;
676
906
  }
677
907
  return undefined;
@@ -697,9 +927,20 @@ export async function assessBackendScenarioParamConsistency(input) {
697
927
  const fallbackSourced = inferred.intentSource === "tp-fallback";
698
928
  const block = source ? extractPytestParamBlock(source, tpId) : undefined;
699
929
  const observed = observeParamFeatures(block, inferred.field);
700
- const status = !source || fallbackSourced
930
+ // Request-level / health checks often have no pytest.param payload row.
931
+ // Treat missing param blocks for nominal/unknown intents as MATCH so they
932
+ // do not pollute eligibility with false UNDETERMINED residuals.
933
+ const requestLevelNoParam = Boolean(source) &&
934
+ !block &&
935
+ observed.text === "param-block-missing" &&
936
+ (inferred.intent === "nominal" || inferred.intent === "unknown");
937
+ const status = !source
701
938
  ? "UNDETERMINED"
702
- : compareIntent(inferred.intent, observed, inferred.bound, inferred.example, inferred.field);
939
+ : requestLevelNoParam
940
+ ? "MATCH"
941
+ : fallbackSourced
942
+ ? "UNDETERMINED"
943
+ : compareIntent(inferred.intent, observed, inferred.bound, inferred.example, inferred.field);
703
944
  const repairability = repairabilityFor(status, inferred.intent, observed, inferred.example);
704
945
  entries.push({
705
946
  caseId: testCase.caseId,
@@ -3708,7 +3708,7 @@ async function buildBackendTestHybridDag(sources) {
3708
3708
  subtask_prompt: [
3709
3709
  "Perform one gap-targeted synchronization, not a full-suite rewrite or stylistic review. Start from explicit bound source IDs/error codes/DTO fields/normative quoted rules and the README Matrix; open and edit only modules that own a missing or conflicting rule. Preserve unrelated valid modules byte-for-byte and avoid optional wording cleanup.",
3710
3710
  "Output budget protocol: never dump full Matrix/case bodies into assistant chat. Inspect README first, build a concise target list, then read/write only target modules one file per tool call. Do not traverse every module when the Matrix and source token inventory show no gap; return `already-satisfied`. When adding omitted in-scope cases, keep every required section. Do not bulk-delete in-scope cases to save tokens.",
3711
- "For every variant Test Point, ensure the Markdown scenario intent is machine-checkable with an exact transport target: `场景意图: <TP-ID>; operation=<METHOD /path>; target=<body.field|query.field|path.field|header.field|request>; intent=<empty|missing|null|min-1|min|max|max+1|pattern-invalid|enum-invalid|wrong-type|nominal-operation|custom-literal:V>; bound=<n optional>; example=<optional>; expectedCode=<optional>`. Never use vague targets such as field=resource/health. Keep pytest params aligned to the exact target.",
3711
+ "For every variant Test Point, ensure the Markdown scenario intent is machine-checkable with an exact transport target: `场景意图: <TP-ID>; operation=<METHOD /path>; target=<body.field|query.field|path.field|header.field|request>; intent=<empty|missing|null|min-1|min|max|max+1|pattern-invalid|enum-invalid|wrong-type|nominal-operation|custom-literal:V>; bound=<n optional>; example=<optional>; expectedCode=<optional>`. Never use vague targets such as field=resource/health. Keep pytest params aligned to the exact target. For intent=missing/empty/default-omit, pytest may use `_OMIT` or delete the key; for intent=enum-invalid use a concrete invalid enum literal (for example `UNKNOWN_STATUS`), never `_OMIT`/missing-key; for trim/padded samples use `custom-literal:trim` or a real padded string, not a bare token like `filter-active` when the intent is `custom-literal:ACTIVE`.",
3712
3712
  "Treat the requirement document as the coverage baseline; scope is limited to operations/rules it (or its referenced API contract) describes, and API contract evidence supplements scenario dimensions. 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. Inspect shared validator/helper/DTO/query builder evidence and expand Affected Operations when the same affected path can affect them; unresolved impact stays visible as GAP/CONFLICT. Directly add in-scope omissions; reject scope expansion to operations absent from the requirement document; undefined impact remains GAP/CONFLICT rather than invented behavior.",
3713
3713
  "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.",
3714
3714
  "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 automatable case points at `testcase/test_<module>.py` derived from that module filename and declares exactly one primary symbol (evidence-only meta cases may keep `脚本/primary symbol=无` with empty variants), 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.",
@@ -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.",
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.",
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.",
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.",
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.",
@@ -269,7 +269,7 @@
269
269
  "type": "implementation-outcome-v1"
270
270
  },
271
271
  "outputContract": "First non-empty line is IMPLEMENTATION_OUTCOME: changed|already-satisfied|blocked. Perform exactly one bounded incremental synchronization of testcase/md/** against all bound source references; preserve valid Cases and report a concise summary.",
272
- "subtask_prompt": "Perform one gap-targeted synchronization, not a full-suite rewrite or stylistic review. Start from explicit bound source IDs/error codes/DTO fields/normative quoted rules and the README Matrix; open and edit only modules that own a missing or conflicting rule. Preserve unrelated valid modules byte-for-byte and avoid optional wording cleanup.\n\nOutput budget protocol: never dump full Matrix/case bodies into assistant chat. Inspect README first, build a concise target list, then read/write only target modules one file per tool call. Do not traverse every module when the Matrix and source token inventory show no gap; return `already-satisfied`. When adding omitted in-scope cases, keep every required section. Do not bulk-delete in-scope cases to save tokens.\n\nFor every variant Test Point, ensure the Markdown scenario intent is machine-checkable with an exact transport target: `场景意图: <TP-ID>; operation=<METHOD /path>; target=<body.field|query.field|path.field|header.field|request>; intent=<empty|missing|null|min-1|min|max|max+1|pattern-invalid|enum-invalid|wrong-type|nominal-operation|custom-literal:V>; bound=<n optional>; example=<optional>; expectedCode=<optional>`. Never use vague targets such as field=resource/health. Keep pytest params aligned to the exact target.\n\nTreat the requirement document as the coverage baseline; scope is limited to operations/rules it (or its referenced API contract) describes, and API contract evidence supplements scenario dimensions. 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. Inspect shared validator/helper/DTO/query builder evidence and expand Affected Operations when the same affected path can affect them; unresolved impact stays visible as GAP/CONFLICT. Directly add in-scope omissions; reject scope expansion to operations absent from the requirement document; undefined impact remains GAP/CONFLICT rather than invented behavior.\n\nCheck 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.\n\nCorrect 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 automatable case points at `testcase/test_<module>.py` derived from that module filename and declares exactly one primary symbol (evidence-only meta cases may keep `脚本/primary symbol=无` with empty variants), 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.\n\nThis is the single Markdown incremental synchronization round. Read every authoritative reference index entry whose role hints include acceptance-criteria, api-contract, data-contract or business-rule; do not rely on the derived PRD as a complete inventory. Preserve every explicit AC/REQ/BR ID, every documented HTTP/business error code, every DTO/JSON field, enum value, boundary, format, nested shape, transaction/state/idempotency/uniqueness/auth/tenant/cross-field rule. For each natural-language normative business rule preserved as required scope, include its exact source sentence without paraphrase together with source path and line/heading anchor so the deterministic ledger can verify quote/hash provenance. Ensure every Case declares exactly `Payload Contract: none` or the three labels `Payload Required Paths`, `Payload Allowed Paths`, and `Payload Enum`; never infer missing keys or enum values. Add only missing Matrix rows/Test Points/Cases/assertions or repair exact drift; do not rewrite already-valid unrelated modules. Work gap-targeted: inspect source anchors and affected modules first, leave unrelated valid modules byte-stable, and return `already-satisfied` without restating the full suite when no gap exists.\n\nFor affected API fields, use one valid nominal payload plus atomic required/missing/null/empty/wrong-type, every documented enum value plus bounded invalid classes, documented min-1/min/nominal/max/max+1, formats and nested object/array constraints. Do not generate a Cartesian product or invent undocumented constraints.\n\nBefore returning, verify that every explicit source AC/REQ/BR, error code and strong DTO field token appears in README or an applicable module Case. If a fact cannot be safely automated, retain it as GAP/CONFLICT with its exact source pointer instead of dropping it. Return already-satisfied only when no target file needs an incremental edit.\n\nRead only indexed source paths. Do not scan the repository, modify source/**, generate pytest, execute tests, or emit JSON.\n\n## Derived task contract: 需求.md\n\n# Backend test\n- AC-001 proof\n\n## Authoritative reference index\n\n[]\n\nFor each index entry, use `readPath` for Pi read-tool calls and keep `path` as the exact Markdown Source References citation. Bound files under .harness/tasks/<taskId>/source/** are read-only inputs: reading them is allowed even though writing .harness/** is forbidden. Never resolve `path` relative to the repository root, search for substitutes, or fall back to docs/** when a bound read fails."
272
+ "subtask_prompt": "Perform one gap-targeted synchronization, not a full-suite rewrite or stylistic review. Start from explicit bound source IDs/error codes/DTO fields/normative quoted rules and the README Matrix; open and edit only modules that own a missing or conflicting rule. Preserve unrelated valid modules byte-for-byte and avoid optional wording cleanup.\n\nOutput budget protocol: never dump full Matrix/case bodies into assistant chat. Inspect README first, build a concise target list, then read/write only target modules one file per tool call. Do not traverse every module when the Matrix and source token inventory show no gap; return `already-satisfied`. When adding omitted in-scope cases, keep every required section. Do not bulk-delete in-scope cases to save tokens.\n\nFor every variant Test Point, ensure the Markdown scenario intent is machine-checkable with an exact transport target: `场景意图: <TP-ID>; operation=<METHOD /path>; target=<body.field|query.field|path.field|header.field|request>; intent=<empty|missing|null|min-1|min|max|max+1|pattern-invalid|enum-invalid|wrong-type|nominal-operation|custom-literal:V>; bound=<n optional>; example=<optional>; expectedCode=<optional>`. Never use vague targets such as field=resource/health. Keep pytest params aligned to the exact target. For intent=missing/empty/default-omit, pytest may use `_OMIT` or delete the key; for intent=enum-invalid use a concrete invalid enum literal (for example `UNKNOWN_STATUS`), never `_OMIT`/missing-key; for trim/padded samples use `custom-literal:trim` or a real padded string, not a bare token like `filter-active` when the intent is `custom-literal:ACTIVE`.\n\nTreat the requirement document as the coverage baseline; scope is limited to operations/rules it (or its referenced API contract) describes, and API contract evidence supplements scenario dimensions. 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. Inspect shared validator/helper/DTO/query builder evidence and expand Affected Operations when the same affected path can affect them; unresolved impact stays visible as GAP/CONFLICT. Directly add in-scope omissions; reject scope expansion to operations absent from the requirement document; undefined impact remains GAP/CONFLICT rather than invented behavior.\n\nCheck 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.\n\nCorrect 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 automatable case points at `testcase/test_<module>.py` derived from that module filename and declares exactly one primary symbol (evidence-only meta cases may keep `脚本/primary symbol=无` with empty variants), 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.\n\nThis is the single Markdown incremental synchronization round. Read every authoritative reference index entry whose role hints include acceptance-criteria, api-contract, data-contract or business-rule; do not rely on the derived PRD as a complete inventory. Preserve every explicit AC/REQ/BR ID, every documented HTTP/business error code, every DTO/JSON field, enum value, boundary, format, nested shape, transaction/state/idempotency/uniqueness/auth/tenant/cross-field rule. For each natural-language normative business rule preserved as required scope, include its exact source sentence without paraphrase together with source path and line/heading anchor so the deterministic ledger can verify quote/hash provenance. Ensure every Case declares exactly `Payload Contract: none` or the three labels `Payload Required Paths`, `Payload Allowed Paths`, and `Payload Enum`; never infer missing keys or enum values. Add only missing Matrix rows/Test Points/Cases/assertions or repair exact drift; do not rewrite already-valid unrelated modules. Work gap-targeted: inspect source anchors and affected modules first, leave unrelated valid modules byte-stable, and return `already-satisfied` without restating the full suite when no gap exists.\n\nFor affected API fields, use one valid nominal payload plus atomic required/missing/null/empty/wrong-type, every documented enum value plus bounded invalid classes, documented min-1/min/nominal/max/max+1, formats and nested object/array constraints. Do not generate a Cartesian product or invent undocumented constraints.\n\nBefore returning, verify that every explicit source AC/REQ/BR, error code and strong DTO field token appears in README or an applicable module Case. If a fact cannot be safely automated, retain it as GAP/CONFLICT with its exact source pointer instead of dropping it. Return already-satisfied only when no target file needs an incremental edit.\n\nRead only indexed source paths. Do not scan the repository, modify source/**, generate pytest, execute tests, or emit JSON.\n\n## Derived task contract: 需求.md\n\n# Backend test\n- AC-001 proof\n\n## Authoritative reference index\n\n[]\n\nFor each index entry, use `readPath` for Pi read-tool calls and keep `path` as the exact Markdown Source References citation. Bound files under .harness/tasks/<taskId>/source/** are read-only inputs: reading them is allowed even though writing .harness/** is forbidden. Never resolve `path` relative to the repository root, search for substitutes, or fall back to docs/** when a bound read fails."
273
273
  },
274
274
  {
275
275
  "id": "validate-backend-md-cases-shell",
@@ -314,7 +314,7 @@
314
314
  "artifacts/**"
315
315
  ],
316
316
  "outputContract": "Deterministic pytest generation handoff. Per-module map children write self-contained test_<module>.py files with bounded local fixtures, HTTP logging/redaction and payload builders; no model invocation, JSON, shared-asset writes or pytest execution.",
317
- "subtask_prompt": "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.\n\nOutput 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.\n\nAlign 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.\n\nGenerate 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.\n\nHTTP 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.\n\nCompare 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.\n\nBefore logging, recursively redact sensitive keys and header values including authorization, proxy-authorization, cookie, set-cookie, token, password, secret, api key and credentials. Never print full Authorization/Cookie values. Apply bounded truncation to serialized request and response bodies (with an explicit truncation marker) so large payloads cannot flood pytest or report artifacts.\n\nDo not read source/**, add cases, reassign ACs, modify conftest/config/production code, use skip/xfail, swallow assertions, execute pytest, or emit JSON. For best-effort cleanup, catch only the narrow transport exception actually raised by the selected HTTP client (for example `requests.RequestException` or `urllib.error.URLError`); never use bare `except`, `Exception`, or `BaseException` with `pass`.",
317
+ "subtask_prompt": "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.\n\nOutput 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.\n\nAlign 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.\n\nGenerate 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.\n\nHTTP 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.\n\nCompare 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.\n\nBefore logging, recursively redact sensitive keys and header values including authorization, proxy-authorization, cookie, set-cookie, token, password, secret, api key and credentials. Never print full Authorization/Cookie values. Apply bounded truncation to serialized request and response bodies (with an explicit truncation marker) so large payloads cannot flood pytest or report artifacts.\n\nDo not read source/**, add cases, reassign ACs, modify conftest/config/production code, use skip/xfail, swallow assertions, execute pytest, or emit JSON. For best-effort cleanup, catch only the narrow transport exception actually raised by the selected HTTP client (for example `requests.RequestException` or `urllib.error.URLError`); never use bare `except`, `Exception`, or `BaseException` with `pass`.",
318
318
  "static": {
319
319
  "resultMarkdown": "Shared pytest plan is deterministic: each downstream module writer owns one self-contained test_<module>.py and must not depend on generated shared helper/factory files."
320
320
  }
@@ -420,7 +420,7 @@
420
420
  ]
421
421
  },
422
422
  "outputContract": "Write exactly one pytest module file testcase/test_<stem>.py whose actual test function region contains the exact Case ID, preferably in the function name or docstring. 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.",
423
- "subtaskPromptTemplate": "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.\n\nOutput 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.\n\nAlign 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.\n\nEnsure 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 前置条件/测试数据/自动化映射.\n\nName 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.\n\nKeep 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.\n\nMaterialize every automatable Markdown Case exactly once as one canonical primary pytest symbol. Preserve every explicit variant Test Point as a stable pytest.param id and every assertion/cross-cutting binding as declared. Build request payloads from the effective Markdown test data literally: keep all declared DTO keys, nested shapes, enum values, missing/null/boundary variants and business-state preconditions; never substitute guessed convenience fields or rename contract fields.\n\nDo not read source/**, add cases, reassign ACs, modify conftest/config/production code, use skip/xfail, swallow assertions, execute pytest, or emit JSON. For best-effort cleanup, catch only the narrow transport exception actually raised by the selected HTTP client (for example `requests.RequestException` or `urllib.error.URLError`); never use bare `except`, `Exception`, or `BaseException` with `pass`."
423
+ "subtaskPromptTemplate": "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.\n\nOutput 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.\n\nAlign 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.\n\nEnsure 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 前置条件/测试数据/自动化映射.\n\nName 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.\n\nKeep 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.\n\nMaterialize every automatable Markdown Case exactly once as one canonical primary pytest symbol. Preserve every explicit variant Test Point as a stable pytest.param id and every assertion/cross-cutting binding as declared. Build request payloads from the effective Markdown test data literally: keep all declared DTO keys, nested shapes, enum values, missing/null/boundary variants and business-state preconditions; never substitute guessed convenience fields or rename contract fields.\n\nDo not read source/**, add cases, reassign ACs, modify conftest/config/production code, use skip/xfail, swallow assertions, execute pytest, or emit JSON. For best-effort cleanup, catch only the narrow transport exception actually raised by the selected HTTP client (for example `requests.RequestException` or `urllib.error.URLError`); never use bare `except`, `Exception`, or `BaseException` with `pass`."
424
424
  }
425
425
  }
426
426
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tea-agent/loop-agent",
3
- "version": "0.33.7-beta.0",
3
+ "version": "0.33.7-beta.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "loop-agent": "bin/loop-agent.js",