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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.33.7-beta.0] - 2026-08-11
6
+
7
+ ### 重点更新
8
+
9
+ - backend-test 收口 source-fidelity P0:绑定 source coverage、有界 Markdown/pytest 同步、item-level eligibility 与单次 eligible 业务执行;my-webapp R107 16 节点 finished
10
+
11
+ ### 新增
12
+
13
+ - backend-test 新增绑定 source references 的来源覆盖事实、Markdown 一轮增量同步和 pytest correspondence 一轮有界修复,在业务测试体执行前保留 initial/effective/residual 证据
14
+ - backend-test 新增 item-level execution eligibility:只授权 payload-safe 的 pytest items,excluded items 与真实业务失败分开展示
15
+
16
+ ### 改进
17
+
18
+ - 将 backend-test Markdown/pytest map 的默认总 token 预算从 `600000` 提升到 `3000000`,降低多模块生成时过早触发 `dynamic map token budget exhausted` 的概率;仍保持预算耗尽 fail-closed
19
+ - Coverage Matrix 接受 `R400001` 等错误码 Rule Key,并保留单元格内 `ACTIVE|ARCHIVED` 未转义枚举管道符,避免合法行被拆成 10 列后整表丢弃
20
+ - scenario-param 按 TP 精确匹配多行 `场景意图`,保留 `ACTIVE` 大小写字面量,识别 `"a" * N` 长度表达式与空白 `empty`,request-level `nominal` 允许 `None` 查询过滤;避免把合法 payload 误判为 MISMATCH 并错误排除 eligible items
21
+ - backend-test 支持 evidence-only 元用例:Markdown 显式声明 `脚本/primary symbol=无` 且仅绑定 assertion/cross-cutting TP 时,不再强制 1:1 pytest 符号,也不再把其算作 MISSING_PYTEST/SCRIPT_MISMATCH
22
+ - generated pytest 内的 `ScopeMismatch`/setup fixture 失败若完全归因于 `testcase/**` 映射脚本,则归为 REPAIRABLE 并进入唯一一轮 collection repair,不再直接 BLOCKED
23
+ - scenario-param 观测跳过 `pytest.param("TP-...", value, id="TP-...")` 的前导 TP 标签,并支持长度意图用数字位置参数表达
24
+
25
+ ### 修复
26
+
27
+ - 修复 readiness 错误信息截断与无效 Coverage Matrix 状态语义,避免超长错误或 advisory 矩阵状态误阻断后续节点
28
+ - 修复 scenario 排除项未绑定 payload 字段、request-level 不确定性被误升级为硬失败的问题
29
+
5
30
  ## [0.33.6-beta.0] - 2026-08-11
6
31
 
7
32
  ### 修复
@@ -31,7 +31,7 @@ import { analyzeBackendTestCaseCoverage, analyzeBackendTestMarkdownPytestCorresp
31
31
  import { materializeBackendTestResultFromPytestHtml, materializeBackendTestResultFromRunDir, parsePytestHtmlReport, } from "../workflows/dag/backend-test-result-contract.js";
32
32
  import { collectBackendTestHumanCaseCatalog, collectBackendTestMappedPytestScripts, resolveBackendTestMappedPytestScripts, collectJacocoCoverage, hasBlockingBackendMarkdownSafetyFindings, hasBlockingBackendMarkdownModuleStemFindings, inspectBackendTestEnvironment, requiredBackendMarkdownCaseAcIds, renderBackendTestFacts, renderBackendTestHtml, renderBackendTestL5Dashboard, redactBackendTestOutput, validateBackendMarkdownCases, validateBackendMarkdownTraceability, writeRunReport, } from "../workflows/dag/backend-test-markdown-workflow.js";
33
33
  import { applyDeterministicScenarioParamRepairs, assessBackendScenarioParamConsistency, classifyBackendTestFailureWithScenarioParam, readBackendScenarioParamFacts, renderBackendTestFailureAnalysis, writeBackendScenarioParamArtifacts, writeScenarioParamRepairAudit, } from "../workflows/dag/backend-test-scenario-param.js";
34
- import { assessBackendPytestCollection, assessMissingBackendPytestScripts, assessPriorityOnlyBackendPytestModules, assertBackendTestExecutionReadinessFresh, buildBackendPytestAssetInventory, materializeBackendTestExecutionReadiness, materializeEffectiveBackendPytestCollection, readBackendPytestCollectionFacts, readBackendTestExecutionReadiness, writeBackendPytestCollectionArtifacts, } from "../workflows/dag/backend-test-pytest-collection.js";
34
+ import { assessBackendPytestCollection, assessMissingBackendPytestScripts, assessPriorityOnlyBackendPytestModules, assertBackendTestExecutionReadinessFresh, buildBackendPytestAssetInventory, buildBackendTestItemEligibility, materializeBackendTestExecutionReadiness, materializeEffectiveBackendPytestCollection, readBackendPytestCollectionFacts, readBackendTestExecutionReadiness, writeBackendPytestCollectionArtifacts, } from "../workflows/dag/backend-test-pytest-collection.js";
35
35
  import { computeL5ReportMetrics } from "../workflows/dag/l5-report-metrics.js";
36
36
  import { buildBackendTestCanonicalResultFromInitialShellSnippet, materializeBackendTestClassification, } from "../workflows/dag/backend-test-classification-contract.js";
37
37
  import { backendTestSemanticReviewSchema, materializeBackendTestSemanticReview, } from "../workflows/dag/backend-test-semantic-review-contract.js";
@@ -678,6 +678,42 @@ async function executeBackendTestPipeline(input, meta) {
678
678
  fixtureResolution,
679
679
  });
680
680
  }
681
+ try {
682
+ const correspondence = await analyzeBackendTestMarkdownPytestCorrespondence({
683
+ workspaceRoot: input.cwd,
684
+ taskId: meta.spec.sourceBinding?.taskId ?? "unbound",
685
+ });
686
+ const correspondenceReportPath = await writeRunReport(meta.runDir, "backend-test-markdown-pytest-correspondence-initial.md", correspondence.markdown);
687
+ const contractsDir = path.join(meta.runDir, "contracts");
688
+ await mkdir(contractsDir, { recursive: true });
689
+ const correspondenceFactsPath = path.join(contractsDir, "backend-test-markdown-pytest-correspondence-initial.json");
690
+ await writeFile(correspondenceFactsPath, JSON.stringify(correspondence.facts, null, 2), "utf8");
691
+ outputs.push(`initialCorrespondence=${correspondenceReportPath}`, `initialCorrespondenceFacts=${correspondenceFactsPath}`);
692
+ if (correspondence.facts.status === "FAIL" && facts.status === "PASS") {
693
+ const correspondenceRepairPaths = Array.from(new Set(correspondence.facts.entries
694
+ .filter((entry) => entry.status !== "EXACT_1_TO_1")
695
+ .flatMap((entry) => [entry.expectedScript, ...entry.actualScripts])
696
+ .filter((candidate) => candidate.startsWith("testcase/") && candidate.endsWith(".py")))).sort();
697
+ if (correspondenceRepairPaths.length > 0) {
698
+ facts.status = "REPAIRABLE";
699
+ facts.repairEligible = true;
700
+ facts.repairPaths = Array.from(new Set([...facts.repairPaths, ...correspondenceRepairPaths])).sort();
701
+ facts.findings.push({
702
+ kind: "markdown-pytest-correspondence",
703
+ classification: "test-asset-defect",
704
+ repairability: "repairable",
705
+ detail: correspondence.facts.entries
706
+ .filter((entry) => entry.status !== "EXACT_1_TO_1")
707
+ .flatMap((entry) => entry.findings)
708
+ .join("; ")
709
+ .slice(0, 12_000) || "Markdown-to-pytest correspondence is incomplete",
710
+ });
711
+ }
712
+ }
713
+ }
714
+ catch (error) {
715
+ outputs.push(`initialCorrespondence=UNAVAILABLE: ${error instanceof Error ? error.message : String(error)}`);
716
+ }
681
717
  const artifacts = await writeBackendPytestCollectionArtifacts({
682
718
  runDir: meta.runDir,
683
719
  stem: "initial",
@@ -776,12 +812,56 @@ async function executeBackendTestPipeline(input, meta) {
776
812
  facts: effective,
777
813
  });
778
814
  const scenarioFacts = await readBackendScenarioParamFacts(path.join(meta.runDir, "contracts", "backend-test-scenario-param-consistency-facts.json"));
815
+ const correspondence = meta.spec.sourceBinding
816
+ ? await analyzeBackendTestMarkdownPytestCorrespondence({
817
+ workspaceRoot: input.cwd,
818
+ taskId: meta.spec.sourceBinding.taskId,
819
+ })
820
+ : undefined;
821
+ const eligibility = correspondence
822
+ ? buildBackendTestItemEligibility(effective.collectedItemIds, {
823
+ correspondenceEntries: correspondence.facts.entries,
824
+ scenarioEntries: scenarioFacts.entries,
825
+ })
826
+ : { eligibleItemIds: effective.collectedItemIds, excludedItems: [] };
827
+ const eligibilityInputHashes = Object.fromEntries((correspondence?.facts.inputFiles ?? []).map((item) => [item.path, item.sha256]));
828
+ const eligibilityFactsPath = path.join(meta.runDir, "contracts", "backend-test-execution-eligibility.json");
829
+ await mkdir(path.dirname(eligibilityFactsPath), { recursive: true });
830
+ await writeFile(eligibilityFactsPath, `${JSON.stringify({
831
+ schemaId: "backend-test-execution-eligibility-v1",
832
+ status: eligibility.eligibleItemIds.length === 0 ? "BLOCKED" : eligibility.excludedItems.length > 0 ? "PARTIAL" : "PASS",
833
+ collectedItemCount: effective.collectedItemIds.length,
834
+ eligibleItemCount: eligibility.eligibleItemIds.length,
835
+ excludedItemCount: eligibility.excludedItems.length,
836
+ eligibleItemIds: eligibility.eligibleItemIds,
837
+ excludedItems: eligibility.excludedItems,
838
+ inputHashes: eligibilityInputHashes,
839
+ }, null, 2)}\n`, "utf8");
840
+ const eligibilityReportPath = await writeRunReport(meta.runDir, "backend-test-execution-eligibility.md", [
841
+ "# Backend Test Execution Eligibility",
842
+ "",
843
+ "## Status",
844
+ "",
845
+ eligibility.eligibleItemIds.length === 0 ? "BLOCKED" : eligibility.excludedItems.length > 0 ? "PARTIAL" : "PASS",
846
+ "",
847
+ `- Collected items: ${effective.collectedItemIds.length}`,
848
+ `- Eligible items: ${eligibility.eligibleItemIds.length}`,
849
+ `- Excluded items: ${eligibility.excludedItems.length}`,
850
+ "",
851
+ "## Excluded Items",
852
+ "",
853
+ ...(eligibility.excludedItems.length > 0 ? eligibility.excludedItems.map((item) => `- \`${item.itemId}\` (${item.caseId ?? "unknown Case"}): ${item.reasons.join("; ")}`) : ["- None"]),
854
+ "",
855
+ ].join("\n"));
856
+ outputs.push(`executionEligibility=${eligibilityReportPath}`, `executionEligibilityFacts=${eligibilityFactsPath}`);
779
857
  const readiness = await materializeBackendTestExecutionReadiness({
780
858
  runDir: meta.runDir,
781
859
  workspaceRoot: input.cwd,
782
860
  effective,
783
861
  scenarioParamStatus: scenarioFacts.overallStatus,
784
862
  scenarioParamRepairAttempt: scenarioFacts.repairAttempt,
863
+ eligibility,
864
+ eligibilityInputHashes,
785
865
  });
786
866
  return {
787
867
  ok: true,
@@ -792,6 +872,8 @@ async function executeBackendTestPipeline(input, meta) {
792
872
  collectedItemCount: effective.collectedItemCount,
793
873
  fixtureResolutionStatus: effective.fixtureResolutionStatus,
794
874
  executionReadinessStatus: readiness.status,
875
+ eligibleItemCount: readiness.eligibleItemIds.length,
876
+ excludedItemCount: readiness.excludedItems.length,
795
877
  factsPath: "contracts/backend-test-pytest-collection-effective.json",
796
878
  readinessPath: "contracts/backend-test-execution-readiness.json",
797
879
  reportPath: artifacts.reportPath,
@@ -890,7 +972,8 @@ async function executeBackendTestPipeline(input, meta) {
890
972
  await assertBackendTestExecutionReadinessFresh(input.cwd, executionReadiness);
891
973
  const mappedScripts = await collectBackendTestMappedPytestScripts(input.cwd);
892
974
  const shellQuote = (value) => `'${value.replaceAll("'", `'"'"'`)}'`;
893
- const pytestTargets = mappedScripts.map(shellQuote).join(" ");
975
+ const eligibleItemIds = executionReadiness.eligibleItemIds;
976
+ const pytestTargets = eligibleItemIds.map(shellQuote).join(" ");
894
977
  // Split into short bash -c commands (aligned with markdown-environment).
895
978
  // A single ultra-long compound command has been observed on Windows to
896
979
  // exit 0xC0000142 (STATUS_DLL_INIT_FAILED) in ~40ms with empty stdout/
@@ -967,7 +1050,9 @@ async function executeBackendTestPipeline(input, meta) {
967
1050
  'PYTHON_BIN="$(command -v python || command -v python3 || true)"',
968
1051
  'if [ -z "${PYTHON_BIN}" ]; then echo "python/python3 is required for backend-test execution" | tee -a "${DIAG_FILE}" >&2; exit 127; fi',
969
1052
  'echo "STEP=pytest-run"',
970
- `echo "- pytestCommand: python -m pytest ${mappedScripts.join(" ")} -v -p no:cacheprovider --html=reports/backend-test.html --self-contained-html" >> "\${DIAG_FILE}"`,
1053
+ `echo "- pytestCommand: python -m pytest <${eligibleItemIds.length} eligibility-authorized-items> -v -p no:cacheprovider --html=reports/backend-test.html --self-contained-html" >> "\${DIAG_FILE}"`,
1054
+ `echo "- eligibleItemCount: ${eligibleItemIds.length}" >> "\${DIAG_FILE}"`,
1055
+ `echo "- excludedItemCount: ${executionReadiness.excludedItems.length}" >> "\${DIAG_FILE}"`,
971
1056
  `PYTHONUTF8=1 PYTHONIOENCODING=utf-8 PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 "\${PYTHON_BIN}" -m pytest ${pytestTargets} -v -p no:cacheprovider --html="\${HARNESS_DAG_RUN_DIR}/reports/backend-test.html" --self-contained-html`,
972
1057
  "STATUS=$?",
973
1058
  'printf "%s" "${STATUS}" > "${HARNESS_DAG_RUN_DIR}/reports/backend-test-pytest-exit.txt"',
@@ -1057,7 +1142,9 @@ async function executeBackendTestPipeline(input, meta) {
1057
1142
  const caseValidationSummary = await readRequiredRunReport(reportsDir, "backend-md-case-validation.md");
1058
1143
  const caseCoverageSummary = await readAdvisoryRunReport(reportsDir, "backend-test-case-coverage-analysis.md", "Backend Test Case Coverage Analysis");
1059
1144
  const traceabilitySummary = await readRequiredRunReport(reportsDir, "backend-test-traceability.md");
1060
- const correspondenceSummary = await readAdvisoryRunReport(reportsDir, "backend-test-markdown-pytest-correspondence.md", "Backend Test Markdown → pytest Correspondence");
1145
+ const correspondenceSummaryBase = await readAdvisoryRunReport(reportsDir, "backend-test-markdown-pytest-correspondence.md", "Backend Test Markdown → pytest Correspondence");
1146
+ const eligibilitySummary = await readAdvisoryRunReport(reportsDir, "backend-test-execution-eligibility.md", "Backend Test Execution Eligibility");
1147
+ const correspondenceSummary = `${correspondenceSummaryBase.trim()}\n\n${eligibilitySummary.trim()}\n`;
1061
1148
  const htmlContent = renderBackendTestHtml({
1062
1149
  title: meta.spec.title,
1063
1150
  parsed,