@tea-agent/loop-agent 0.31.0 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +60 -0
- package/README.md +1 -1
- package/dist/executors/dag-pi-executor.js +69 -2
- package/dist/executors/pi-sdk-executor.js +61 -0
- package/dist/executors/shell-executor.js +136 -79
- package/dist/shared/operator/capabilities.js +22 -1
- package/dist/shared/operator/command-lifecycle.js +94 -0
- package/dist/shared/operator/index.js +1 -0
- package/dist/worker/cli.js +10 -24
- package/dist/worker/console/doctor.js +11 -4
- package/dist/worker/console/observe-health-match.js +18 -15
- package/dist/worker/console/operator-actions.js +2 -1
- package/dist/worker/feature/acceptance-policy.js +227 -0
- package/dist/worker/feature/decision-loader.js +1 -1
- package/dist/worker/feature/next-action.js +56 -6
- package/dist/worker/feature/profile-schema.js +3 -0
- package/dist/worker/feature/reducer.js +1 -0
- package/dist/worker/feature/review.js +72 -8
- package/dist/worker/feature/scaffold.js +14 -0
- package/dist/worker/loop-agent/controller-protocol.js +143 -0
- package/dist/worker/materialize/harness-task-lifecycle-probe.js +126 -0
- package/dist/worker/materialize/harness-task-lineage.js +220 -0
- package/dist/worker/materialize/harness-task-materializer.js +350 -80
- package/dist/worker/observability/progress-composite.js +1 -0
- package/dist/worker/observability/read-model.js +66 -19
- package/dist/worker/pool/attempt-identity.js +41 -0
- package/dist/worker/pool/attempt-lease.js +184 -0
- package/dist/worker/pool/attempt-transition.js +210 -0
- package/dist/worker/pool/begin-attempt-with-lease.js +26 -0
- package/dist/worker/pool/begin-attempt.js +35 -0
- package/dist/worker/pool/failure-routing.js +49 -0
- package/dist/worker/pool/recovery-decision.js +163 -0
- package/dist/worker/pool/run-owner-store.js +126 -0
- package/dist/worker/pool/run-store.js +32 -46
- package/dist/worker/pool/runtime-reconcile-inventory.js +127 -0
- package/dist/worker/pool/state-projection.js +57 -0
- package/dist/worker/run-task/run-task.js +31 -4
- package/dist/worker/runner/run-ready.js +64 -14
- package/dist/worker/runner/single-task-attempt.js +42 -13
- package/dist/worker/task-graph/acceptance-schema.js +3 -0
- package/dist/workflows/dag/backend-test-pytest-collection.js +162 -7
- package/dist/workflows/dag/backend-test-result-contract.js +105 -67
- package/dist/workflows/dag/backend-test-scenario-param.js +92 -30
- package/dist/workflows/dag/backend-test-writer-completeness.js +55 -0
- package/dist/workflows/dag/init-hybrid.js +44 -47
- package/dist/workflows/dag/rerun-task.js +86 -0
- package/docs/README.md +3 -1
- package/docs/architecture/evolution.md +1 -1
- package/docs/operations/README.md +1 -0
- package/docs/templates/README.md +1 -0
- package/docs/templates/agent-worker-production-readiness-checklist.md +45 -0
- package/docs/templates/backend-test-dag.json +40 -60
- package/docs/templates/evaluation/agents-map-slim-v1.md +1 -1
- package/docs/templates/evaluation/agents-map-verbose-v0.md +1 -1
- package/docs/templates/init-managed-agents.md +1 -1
- package/docs/templates/product-line/scaffold-samples/backend-only/acceptance.yaml +3 -0
- package/docs/templates/product-line/scaffold-samples/backend-only/feature.yaml +11 -0
- package/docs/templates/product-line/scaffold-samples/fe-with-api/acceptance.yaml +3 -0
- package/docs/templates/product-line/scaffold-samples/fe-with-api/feature.yaml +11 -0
- package/docs/templates/product-line/scaffold-samples/frontend-only/acceptance.yaml +3 -0
- package/docs/templates/product-line/scaffold-samples/frontend-only/feature.yaml +11 -0
- package/package.json +1 -1
- package/skills/agent-worker/SKILL.md +1 -1
- package/skills/agent-worker/references/agent-worker-operator.md +1 -1
- package/skills/loop-agent/references/command-reference.md +2 -2
- package/skills/loop-agent/references/harness-policy.md +1 -1
package/AGENTS.md
CHANGED
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
4. 涉及测试纪律/验证声明/调试时继续读:`docs/governance/harness-methodology-*.md`。
|
|
39
39
|
5. 查看最近提交、相关 plan/progress/report;`git status --short --branch`;跑最小基线验证。
|
|
40
40
|
6. 后端/接口/pytest → `taskKind: "backend-test"`(不是 `--profile`);知识回写 `knowledge-sync`;图谱开荒 `knowledge-graph-bootstrap`。`--profile` 仅 `auto|minimal|standard|reviewed|supervised`。
|
|
41
|
-
7. 看板/observe → `agent-worker console`(默认 repo=当前目录、port=8790;兼容入口 `agent-worker console serve --repo . --port 8790`)(`/inspect/` 只读);`observe serve`
|
|
41
|
+
7. 看板/observe → `agent-worker console`(默认 repo=当前目录、port=8790;兼容入口 `agent-worker console serve --repo . --port 8790`)(`/inspect/` 只读);`observe serve` 已下线(`OBSERVE_SERVE_REMOVED` + exit 2);`observe snapshot` 仍可用。
|
|
42
42
|
8. 分支合并 → 先读 `docs/operations/branch-merge-guideline.md`。
|
|
43
43
|
|
|
44
44
|
## 会话协议
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,66 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.32.0] - 2026-08-09
|
|
6
|
+
|
|
7
|
+
### 重点更新
|
|
8
|
+
|
|
9
|
+
- 启动 Agent Worker Production Readiness v1 分阶段落地:Running Attempt 归属不再依赖 terminal ledger;safe `run-error` 可直接 `task retry`,不再要求 emergency `mark-failed`
|
|
10
|
+
- Harness Task 新 identity 改为 lineage/revision digest,同日 retry 不再依赖修改 title
|
|
11
|
+
- 新增 Task-scoped attempt lease、controller protocol compatibility 评估,以及 agent-worker production readiness 操作合同/checklist
|
|
12
|
+
- Attempt 终态释放 lease;写入前评估 controllerProtocol;scaffold 强制 ADR 0007 acceptance_policy;reconcile dry-run inventory 给出唯一恢复命令
|
|
13
|
+
- 硬下线 `agent-worker observe serve`,只读检视统一走 Console `/inspect/`
|
|
14
|
+
|
|
15
|
+
### 新增
|
|
16
|
+
|
|
17
|
+
- `WorkerAttemptIdentity` / immutable run-owner store / `beginWorkerAttempt`(含 lease 路径)
|
|
18
|
+
- `WorkerAttemptFailureV2`、`resolveWorkerRecoveryDecision`、Attempt transition reducer
|
|
19
|
+
- Harness Task lineage/revision digests 与 `MaterializeOutcome` 元数据
|
|
20
|
+
- `docs/operations/agent-worker-production-readiness.md` 与 checklist 模板
|
|
21
|
+
- operator capabilities `controllerProtocol` envelope;`buildRuntimeReconcileInventoryDryRun`;command lifecycle official 命令清单
|
|
22
|
+
- Feature scaffold `acceptance_policy` 与 AC `verification.authority` / `required_evidence_kinds`
|
|
23
|
+
|
|
24
|
+
### 改进
|
|
25
|
+
|
|
26
|
+
- Feature Review / Observe 对 Running 与 terminal 分层投影,ownership 告警按 `workerRunId` 聚合
|
|
27
|
+
- Task-scoped WorkerEvent 携带 `featureId`
|
|
28
|
+
- `batch run-ready` / `runSingleTaskAttempt` 在 terminal 后 CAS 释放 lease;有 controller identity 时先做 protocol preflight
|
|
29
|
+
- Feature next-action 优先消费 acceptanceGaps.nextCommand
|
|
30
|
+
|
|
31
|
+
### 修复
|
|
32
|
+
|
|
33
|
+
- 硬下线 `agent-worker observe serve`:调用后不再监听端口,stderr 输出稳定标记 `OBSERVE_SERVE_REMOVED` 与替代入口,进程以退出码 2 结束
|
|
34
|
+
- 只读检视统一走 `agent-worker console`(默认 `127.0.0.1:8790`)的 `/inspect/`;`observe snapshot` 仍输出 GlobalSnapshot JSON
|
|
35
|
+
- Console doctor / health-match 不再以外部 8787 Observe 进程为健康前提
|
|
36
|
+
|
|
37
|
+
## [0.31.1] - 2026-08-09
|
|
38
|
+
|
|
39
|
+
### 重点更新
|
|
40
|
+
|
|
41
|
+
- 后端测试生成新增确定性冲突预检与执行就绪性校验,在执行前精准拦截潜在错误并安全自动修复局部缺陷
|
|
42
|
+
- 更精细地区分多种写入失败状态(如思考耗尽、部分写入等),避免无效重试并优先恢复可用进度
|
|
43
|
+
- 收紧测试参数校验与结果真实性,准确区分失败与错误,并前置语法门禁防止生成错误代码
|
|
44
|
+
- 优化 16 个节点的执行成本,将多个审查与计划环节改为确定性交接,在保持质量的同时显著降低资源消耗
|
|
45
|
+
|
|
46
|
+
### 新增
|
|
47
|
+
|
|
48
|
+
- 为 `dag rerun-task` 新增确定性 generated-output-conflict 预检,执行前扫描并拦截与磁盘已有文件的冲突,提供精确修复建议
|
|
49
|
+
- writer 执行新增 `writer-thinking-exhausted` 终态分类,精准识别模型思考耗尽且无实际写入的情况,避免无限重试
|
|
50
|
+
- 后端 pytest 模块新增 self-contained 静态导入门禁,在收集前拦截对生成共享命名空间的非法导入
|
|
51
|
+
- 新增 `backend-test-execution-readiness-v1` 执行就绪预检,在收集通过后验证传递 fixture/plugin 注册,支持对局部缺陷的安全自动修复
|
|
52
|
+
|
|
53
|
+
### 改进
|
|
54
|
+
|
|
55
|
+
- 在已确认存在实质变更且完整性校验通过时,允许确定性推断缺失的实现结果,避免因模型漏报而中断已完成的流程
|
|
56
|
+
- 收紧 scenario-param 结论判定,全量不可判定显示 `UNAVAILABLE`,混合情况显示 `PARTIAL`,并支持精确到具体传输目标的校验与修复
|
|
57
|
+
- 大幅降低 16 节点运行成本:将部分审查与计划环节改为确定性静态交接,统一质量度量并优化模块聚合策略
|
|
58
|
+
- 在修复写入前新增 `ast.parse` 语法门禁并修正尾逗号算法,严格禁止生成 `,,` 等错误语法污染收集流程
|
|
59
|
+
|
|
60
|
+
### 修复
|
|
61
|
+
|
|
62
|
+
- 修复 pytest 结果真实性问题:setup/teardown 错误不再被误报为 collection error,准确区分并独立展示 Failed 与 Error
|
|
63
|
+
- 修复结果合同丢失标识的问题,现完整保留原 pytest node ID、Case ID 与 TP ID,并明确记录业务测试体的实际执行数
|
|
64
|
+
|
|
5
65
|
## [0.31.0] - 2026-08-08
|
|
6
66
|
|
|
7
67
|
### 重点更新
|
package/README.md
CHANGED
|
@@ -100,7 +100,7 @@ loop-agent dag report --run-id <run-id>
|
|
|
100
100
|
| --- | --- | --- |
|
|
101
101
|
| 受治理 CLI | `loop-agent …` / `agent-worker …` | 权威写入与诊断 |
|
|
102
102
|
| Loop Operator Console | `agent-worker console` | Official 本地控制面(默认 repo=当前目录、`127.0.0.1:8790`,含 Operate 与 Inspect) |
|
|
103
|
-
|
|
|
103
|
+
| Inspect(只读) | `agent-worker console` → `/inspect/`;`observe snapshot` | 统一 Console 内只读检视;`observe serve` 已下线(REMOVED / exit 2) |
|
|
104
104
|
|
|
105
105
|
```bash
|
|
106
106
|
# 终端 A:Console(任务/运行操作面;canonical mutation 只经 sibling loop-agent)
|
|
@@ -11,6 +11,55 @@ import { GitStatusUnavailableError, pathsChangedDuringRun, readGitStatusPorcelai
|
|
|
11
11
|
import { isWriterEmptyDiffRetryCandidate, INCOMPLETE_WRITE_SET_RETRY_CATEGORY, WRITER_EMPTY_DIFF_RETRY_CATEGORY, } from "../workflows/dag/retry-policy.js";
|
|
12
12
|
import { assessBackendTestMdPlanCompleteness, assessBackendTestMdWriterCompleteness, assessBackendTestPytestPlanCompleteness, assessBackendTestPytestWriterCompleteness, assessBackendTestShardChildCompleteness, backendTestWriterProgressRoleForTask, classifyBackendTestWriterCompletenessFailure, isBackendTestCompletenessRetryCandidate, isBackendTestMdPlanTask, isBackendTestPytestPlanTask, isBackendTestShardChildTask, writeBackendTestWriterProgressArtifacts, } from "../workflows/dag/backend-test-writer-completeness.js";
|
|
13
13
|
import { redactSecrets, truncateUtf8Preview } from "../shared/preview.js";
|
|
14
|
+
/**
|
|
15
|
+
* Writer classification for a length-stopped thinking-only attempt: the model
|
|
16
|
+
* exhausted its output budget thinking but never issued a write/edit tool call
|
|
17
|
+
* and produced zero attributed diff. This is a terminal, non-retryable
|
|
18
|
+
* diagnosis (the same prompt + model will hit the same budget wall); the
|
|
19
|
+
* recommendation is a model switch plus a fresh run. It must NOT mask a
|
|
20
|
+
* recoverable partial-write-set (incomplete-write-set) upgrade.
|
|
21
|
+
*/
|
|
22
|
+
export const WRITER_THINKING_EXHAUSTED_CATEGORY = "writer-thinking-exhausted";
|
|
23
|
+
function readWriterThinkingExhaustionEvidence(result) {
|
|
24
|
+
const wider = result;
|
|
25
|
+
return {
|
|
26
|
+
...(typeof wider.stopReason === "string"
|
|
27
|
+
? { stopReason: wider.stopReason }
|
|
28
|
+
: {}),
|
|
29
|
+
...(typeof wider.thinkingObserved === "boolean"
|
|
30
|
+
? { thinkingObserved: wider.thinkingObserved }
|
|
31
|
+
: {}),
|
|
32
|
+
...(typeof wider.writeToolCallCount === "number"
|
|
33
|
+
? { writeToolCallCount: wider.writeToolCallCount }
|
|
34
|
+
: {}),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Pure classification predicate for writer-thinking-exhausted (AC-002).
|
|
39
|
+
* Requires ALL of: empty-output base category, stopReason exactly "length",
|
|
40
|
+
* thinking observed, zero write/edit tool calls, and a confirmed empty
|
|
41
|
+
* run-attributed diff. Recoverable partial writes stay incomplete-write-set
|
|
42
|
+
* because the caller only consults this predicate when the completeness gate
|
|
43
|
+
* did not upgrade the category.
|
|
44
|
+
*/
|
|
45
|
+
export function isWriterThinkingExhausted(result, mapped, changeManifestChangedFiles) {
|
|
46
|
+
if (mapped.ok)
|
|
47
|
+
return false;
|
|
48
|
+
if (mapped.failureCategory !== "empty-output")
|
|
49
|
+
return false;
|
|
50
|
+
const evidence = readWriterThinkingExhaustionEvidence(result);
|
|
51
|
+
if (evidence.stopReason !== "length")
|
|
52
|
+
return false;
|
|
53
|
+
if (evidence.thinkingObserved !== true)
|
|
54
|
+
return false;
|
|
55
|
+
if ((evidence.writeToolCallCount ?? 0) !== 0)
|
|
56
|
+
return false;
|
|
57
|
+
if (changeManifestChangedFiles === undefined)
|
|
58
|
+
return false;
|
|
59
|
+
if (changeManifestChangedFiles.length !== 0)
|
|
60
|
+
return false;
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
14
63
|
export const DAG_PI_READONLY_TOOLS = ["read", "grep", "find", "ls"];
|
|
15
64
|
/** Bounded writer tools include bash; edit/write remain policy-wrapped via SDK customTools. */
|
|
16
65
|
export const DAG_PI_WRITE_TOOLS = [
|
|
@@ -664,6 +713,7 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
664
713
|
else {
|
|
665
714
|
const outcomeValidation = validateWriterImplementationOutcome(mapped.assistantText || mapped.stdout, changeManifestChangedFiles, {
|
|
666
715
|
requireChangedFiles: input.task.writerOutcomePolicy.requireChangedFiles === true,
|
|
716
|
+
allowMissingChangedOutcomeWhenDiffPresent: isBackendTestCompletenessRetryCandidate(input.task),
|
|
667
717
|
});
|
|
668
718
|
if (!outcomeValidation.ok) {
|
|
669
719
|
writerOutcomeViolation = outcomeValidation.reason;
|
|
@@ -739,9 +789,10 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
739
789
|
};
|
|
740
790
|
}
|
|
741
791
|
}
|
|
742
|
-
if (writeGuardOk && !writerOutcomeViolation && !completenessFailure) {
|
|
792
|
+
if (mapped.ok && writeGuardOk && !writerOutcomeViolation && !completenessFailure) {
|
|
743
793
|
return mapped;
|
|
744
794
|
}
|
|
795
|
+
const writerThinkingExhausted = isWriterThinkingExhausted(result, mapped, changeManifestChangedFiles);
|
|
745
796
|
const stderrParts = [mapped.stderr];
|
|
746
797
|
if (!writeGuardOk) {
|
|
747
798
|
stderrParts.push(`write guard failed: ${writeGuardViolations.join(", ")}`);
|
|
@@ -752,6 +803,9 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
752
803
|
if (completenessFailure) {
|
|
753
804
|
stderrParts.push(completenessFailure.detail);
|
|
754
805
|
}
|
|
806
|
+
if (writerThinkingExhausted) {
|
|
807
|
+
stderrParts.push(`${WRITER_THINKING_EXHAUSTED_CATEGORY}: stopReason=length, thinking observed, 0 write tool calls, 0 attributed diff; recommend a model switch and a new run`);
|
|
808
|
+
}
|
|
755
809
|
if (meta.writeGuardAttribution === "best-effort") {
|
|
756
810
|
stderrParts.push("write guard note: concurrent rank writers use best-effort per-node attribution; keep same-rank writeSet entries disjoint");
|
|
757
811
|
}
|
|
@@ -783,13 +837,26 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
783
837
|
mapped.failureCategory === "invalid-output" ||
|
|
784
838
|
mapped.failureCategory === WRITER_EMPTY_DIFF_RETRY_CATEGORY)
|
|
785
839
|
? INCOMPLETE_WRITE_SET_RETRY_CATEGORY
|
|
786
|
-
:
|
|
840
|
+
: // writer-thinking-exhausted: a length-stopped thinking-only attempt with
|
|
841
|
+
// zero write tool calls and zero attributed diff is terminal and
|
|
842
|
+
// non-retryable; recommend a model switch + fresh run. Only applies on
|
|
843
|
+
// the empty-output base category so provider/transport failures keep
|
|
844
|
+
// their original category, and only when the completeness gate did not
|
|
845
|
+
// upgrade to incomplete-write-set above.
|
|
846
|
+
writerThinkingExhausted
|
|
847
|
+
? WRITER_THINKING_EXHAUSTED_CATEGORY
|
|
848
|
+
: mapped.failureCategory,
|
|
787
849
|
durationMs: mapped.durationMs || Date.now() - started,
|
|
788
850
|
};
|
|
789
851
|
}
|
|
790
852
|
export function validateWriterImplementationOutcome(text, changedFiles, options) {
|
|
791
853
|
const parsed = parseWriterImplementationOutcome(text);
|
|
792
854
|
const diagnostics = writerOutcomeDiagnostics(text, parsed, changedFiles.length);
|
|
855
|
+
if (parsed.kind === "missing" &&
|
|
856
|
+
options?.allowMissingChangedOutcomeWhenDiffPresent === true &&
|
|
857
|
+
changedFiles.length > 0) {
|
|
858
|
+
return { ok: true, outcome: "changed" };
|
|
859
|
+
}
|
|
793
860
|
if (parsed.kind !== "valid") {
|
|
794
861
|
return {
|
|
795
862
|
ok: false,
|
|
@@ -275,6 +275,11 @@ function classifySdkActivityKind(event) {
|
|
|
275
275
|
}
|
|
276
276
|
return "provider";
|
|
277
277
|
}
|
|
278
|
+
/** Write/edit-style tool names emitted by DAG writer customTools.
|
|
279
|
+
* Used to attribute write-tool activity for writer-thinking-exhausted diagnosis.
|
|
280
|
+
* Kept as a module-local constant so tests stay deterministic regardless of the
|
|
281
|
+
* SDK's tool registration quirks. */
|
|
282
|
+
const SDK_WRITE_TOOL_NAMES = new Set(["edit", "write", "str_replace_editor"]);
|
|
278
283
|
function createSessionEventAppender(filePath, onSessionEvent) {
|
|
279
284
|
let chain = Promise.resolve();
|
|
280
285
|
let dirEnsured = false;
|
|
@@ -365,6 +370,25 @@ function extractSdkUsageSample(event) {
|
|
|
365
370
|
const responseKey = responseKeyCandidates.find((value) => typeof value === "string" && value.length > 0);
|
|
366
371
|
return responseKey ? { responseKey, tokens } : { tokens };
|
|
367
372
|
}
|
|
373
|
+
/** Extract a non-empty stop reason from a terminal SDK event.
|
|
374
|
+
* Probes multiple field shapes across SDK versions so a missing field fails
|
|
375
|
+
* open (undefined) rather than misclassifying an attempt. */
|
|
376
|
+
function readStopReason(event) {
|
|
377
|
+
const message = isRecord(event.message) ? event.message : undefined;
|
|
378
|
+
const candidates = [
|
|
379
|
+
message?.stop_reason,
|
|
380
|
+
message?.stopReason,
|
|
381
|
+
event.stopReason,
|
|
382
|
+
event.stop_reason,
|
|
383
|
+
];
|
|
384
|
+
for (const candidate of candidates) {
|
|
385
|
+
if (typeof candidate === "string" &&
|
|
386
|
+
candidate.trim().length > 0) {
|
|
387
|
+
return candidate.trim();
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return undefined;
|
|
391
|
+
}
|
|
368
392
|
function aggregateSdkTokenUsage(samples) {
|
|
369
393
|
const identified = new Map();
|
|
370
394
|
let anonymousMaximum = 0;
|
|
@@ -415,6 +439,32 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
415
439
|
const stdoutPreview = new BoundedTextPreview("stdout");
|
|
416
440
|
const stdoutCollector = createPiJsonlStreamCollector();
|
|
417
441
|
const usageSamples = [];
|
|
442
|
+
// Writer-thinking-exhausted diagnosis evidence (AC-002). Collected in the
|
|
443
|
+
// subscribe callback regardless of shouldPersistSessionEvent filtering, so a
|
|
444
|
+
// length-stopped thinking-only attempt with no persisted write tool calls is
|
|
445
|
+
// still distinguishable from generic empty output.
|
|
446
|
+
let stopReason;
|
|
447
|
+
let thinkingObserved = false;
|
|
448
|
+
let writeToolCallCount = 0;
|
|
449
|
+
const observeWriterThinkingExhaustionEvidence = (event) => {
|
|
450
|
+
const type = typeof event.type === "string" ? event.type : "";
|
|
451
|
+
if (type === "thinking_delta") {
|
|
452
|
+
thinkingObserved = true;
|
|
453
|
+
}
|
|
454
|
+
if (type === "tool_execution_start" &&
|
|
455
|
+
typeof event.toolName === "string" &&
|
|
456
|
+
SDK_WRITE_TOOL_NAMES.has(event.toolName)) {
|
|
457
|
+
writeToolCallCount += 1;
|
|
458
|
+
}
|
|
459
|
+
if (type === "turn_end" ||
|
|
460
|
+
type === "message_end" ||
|
|
461
|
+
type === "agent_end" ||
|
|
462
|
+
type === "agent_settled") {
|
|
463
|
+
const candidate = readStopReason(event);
|
|
464
|
+
if (candidate)
|
|
465
|
+
stopReason = candidate;
|
|
466
|
+
}
|
|
467
|
+
};
|
|
418
468
|
let session;
|
|
419
469
|
let unsubscribe;
|
|
420
470
|
let timeoutHandle;
|
|
@@ -513,6 +563,11 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
513
563
|
const usageSample = extractSdkUsageSample(event);
|
|
514
564
|
if (usageSample)
|
|
515
565
|
usageSamples.push(usageSample);
|
|
566
|
+
// Capture writer-thinking-exhausted evidence BEFORE the
|
|
567
|
+
// shouldPersistSessionEvent filter: thinking_delta is intentionally
|
|
568
|
+
// excluded from persisted JSONL, but must still count toward the
|
|
569
|
+
// in-memory thinkingObserved/writeToolCallCount/stopReason evidence.
|
|
570
|
+
observeWriterThinkingExhaustionEvidence(event);
|
|
516
571
|
if (!shouldPersistSessionEvent(event))
|
|
517
572
|
return;
|
|
518
573
|
const line = serializeSessionEvent(event);
|
|
@@ -623,5 +678,11 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
623
678
|
timedOut,
|
|
624
679
|
tokensUsed,
|
|
625
680
|
subagentStats: collected.subagentStats,
|
|
681
|
+
// Writer-thinking-exhausted diagnosis evidence (AC-002). Carried through
|
|
682
|
+
// the object spread in executePiStep so executeDagPiNode can read them
|
|
683
|
+
// via a widened type without modifying PiStepExecutionResult's shape.
|
|
684
|
+
stopReason,
|
|
685
|
+
thinkingObserved,
|
|
686
|
+
writeToolCallCount,
|
|
626
687
|
};
|
|
627
688
|
}
|
|
@@ -32,7 +32,7 @@ import { analyzeBackendTestCaseCoverage, analyzeBackendTestMarkdownPytestCorresp
|
|
|
32
32
|
import { materializeBackendTestResultFromPytestHtml, materializeBackendTestResultFromRunDir, parsePytestHtmlReport, } from "../workflows/dag/backend-test-result-contract.js";
|
|
33
33
|
import { collectBackendTestHumanCaseCatalog, collectBackendTestMappedPytestScripts, resolveBackendTestMappedPytestScripts, collectJacocoCoverage, hasBlockingBackendMarkdownSafetyFindings, inspectBackendTestEnvironment, requiredBackendMarkdownCaseAcIds, renderBackendTestFacts, renderBackendTestHtml, renderBackendTestL5Dashboard, redactBackendTestOutput, validateBackendMarkdownCases, validateBackendMarkdownTraceability, writeRunReport, } from "../workflows/dag/backend-test-markdown-workflow.js";
|
|
34
34
|
import { applyDeterministicScenarioParamRepairs, assessBackendScenarioParamConsistency, classifyBackendTestFailureWithScenarioParam, readBackendScenarioParamFacts, renderBackendTestFailureAnalysis, writeBackendScenarioParamArtifacts, writeScenarioParamRepairAudit, } from "../workflows/dag/backend-test-scenario-param.js";
|
|
35
|
-
import { assessBackendPytestCollection, assessMissingBackendPytestScripts,
|
|
35
|
+
import { assessBackendPytestCollection, assessMissingBackendPytestScripts, assertBackendTestExecutionReadinessFresh, buildBackendPytestAssetInventory, materializeBackendTestExecutionReadiness, materializeEffectiveBackendPytestCollection, readBackendPytestCollectionFacts, readBackendTestExecutionReadiness, writeBackendPytestCollectionArtifacts, } from "../workflows/dag/backend-test-pytest-collection.js";
|
|
36
36
|
import { computeL5ReportMetrics } from "../workflows/dag/l5-report-metrics.js";
|
|
37
37
|
import { buildBackendTestCanonicalResultFromInitialShellSnippet, materializeBackendTestClassification, } from "../workflows/dag/backend-test-classification-contract.js";
|
|
38
38
|
import { backendTestSemanticReviewSchema, materializeBackendTestSemanticReview, } from "../workflows/dag/backend-test-semantic-review-contract.js";
|
|
@@ -43,6 +43,57 @@ import { readRunState } from "../workflows/dag/run-store.js";
|
|
|
43
43
|
import { resolveDagTaskSourcePath } from "../task/dag-source-paths.js";
|
|
44
44
|
import { readProjectGovernanceContext } from "../workflows/dag/project-governance-context.js";
|
|
45
45
|
import { assertMavenPlanFresh, MavenPlanStaleError, } from "../verification/maven/index.js";
|
|
46
|
+
async function runBackendScenarioParamPreflight(input) {
|
|
47
|
+
const outputs = [];
|
|
48
|
+
const initial = await assessBackendScenarioParamConsistency({
|
|
49
|
+
workspaceRoot: input.workspaceRoot,
|
|
50
|
+
phase: "initial",
|
|
51
|
+
repairAttempt: 0,
|
|
52
|
+
strictScenarioParamGate: input.strictScenarioParamGate,
|
|
53
|
+
});
|
|
54
|
+
const initialArtifacts = await writeBackendScenarioParamArtifacts({
|
|
55
|
+
runDir: input.runDir,
|
|
56
|
+
facts: initial.facts,
|
|
57
|
+
markdown: initial.markdown,
|
|
58
|
+
});
|
|
59
|
+
outputs.push(`scenarioParamInitial=${initialArtifacts.reportPath}`);
|
|
60
|
+
let finalFacts;
|
|
61
|
+
let finalMarkdown;
|
|
62
|
+
if (initial.facts.repairEligible) {
|
|
63
|
+
const repair = await applyDeterministicScenarioParamRepairs({
|
|
64
|
+
workspaceRoot: input.workspaceRoot,
|
|
65
|
+
facts: initial.facts,
|
|
66
|
+
});
|
|
67
|
+
const auditPath = await writeScenarioParamRepairAudit({
|
|
68
|
+
runDir: input.runDir,
|
|
69
|
+
audit: repair.audit,
|
|
70
|
+
changedFiles: repair.changedFiles,
|
|
71
|
+
});
|
|
72
|
+
outputs.push(`scenarioParamRepairAudit=${auditPath}`, `scenarioParamRepaired=${repair.repaired.join(",") || "(none)"}`);
|
|
73
|
+
const reassessed = await assessBackendScenarioParamConsistency({
|
|
74
|
+
workspaceRoot: input.workspaceRoot,
|
|
75
|
+
phase: "final",
|
|
76
|
+
repairAttempt: 1,
|
|
77
|
+
strictScenarioParamGate: input.strictScenarioParamGate,
|
|
78
|
+
});
|
|
79
|
+
finalFacts = reassessed.facts;
|
|
80
|
+
finalMarkdown = reassessed.markdown;
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
finalFacts = { ...initial.facts, phase: "final", repairAttempt: 0, repairEligible: false };
|
|
84
|
+
finalMarkdown = initial.markdown.replace("Phase: initial", "Phase: final");
|
|
85
|
+
}
|
|
86
|
+
const finalArtifacts = await writeBackendScenarioParamArtifacts({
|
|
87
|
+
runDir: input.runDir,
|
|
88
|
+
facts: finalFacts,
|
|
89
|
+
markdown: finalMarkdown,
|
|
90
|
+
});
|
|
91
|
+
outputs.push(`scenarioParamFinal=${finalArtifacts.reportPath}`, `scenarioParamStatus=${finalFacts.overallStatus}`);
|
|
92
|
+
if (input.strictScenarioParamGate && ["FAIL", "UNAVAILABLE"].includes(finalFacts.overallStatus)) {
|
|
93
|
+
throw new Error(`backend-test strict scenario-param gate blocked ${finalFacts.overallStatus} before collection`);
|
|
94
|
+
}
|
|
95
|
+
return { facts: finalFacts, outputs };
|
|
96
|
+
}
|
|
46
97
|
/** In-memory same-run success cache. Never shared across runIds. */
|
|
47
98
|
const sameRunShellCommandCaches = new Map();
|
|
48
99
|
function shellCommandCacheKey(runId, workspaceFingerprint, contractKey) {
|
|
@@ -612,6 +663,12 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
612
663
|
}
|
|
613
664
|
else {
|
|
614
665
|
const mappedScripts = resolution.existingScripts;
|
|
666
|
+
const scenario = await runBackendScenarioParamPreflight({
|
|
667
|
+
workspaceRoot: input.cwd,
|
|
668
|
+
runDir: meta.runDir,
|
|
669
|
+
strictScenarioParamGate: Boolean((meta.spec.globalConstraints ?? []).some((item) => /strictScenarioParamGate\s*=\s*true/i.test(item))),
|
|
670
|
+
});
|
|
671
|
+
outputs.push(...scenario.outputs);
|
|
615
672
|
const inventory = await buildBackendPytestAssetInventory(input.cwd, mappedScripts);
|
|
616
673
|
const shellQuote = (value) => `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
617
674
|
const targets = mappedScripts.map(shellQuote).join(" ");
|
|
@@ -628,6 +685,23 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
628
685
|
durationMs: Date.now() - started,
|
|
629
686
|
};
|
|
630
687
|
}
|
|
688
|
+
let fixtureResolution;
|
|
689
|
+
if ((result.exitCode ?? 2) === 0) {
|
|
690
|
+
const fixtureCommand = `PYTHONDONTWRITEBYTECODE=1 python -m pytest --setup-plan -q -p no:cacheprovider ${targets}`;
|
|
691
|
+
const [fixtureResult] = await executePipelineCommands(input, meta, [fixtureCommand]);
|
|
692
|
+
if (!fixtureResult)
|
|
693
|
+
throw new Error("backend pytest fixture-resolution command did not produce a result");
|
|
694
|
+
if (["spawn-error", "timeout", "termination-unconfirmed"].includes(fixtureResult.failureCategory ?? "")) {
|
|
695
|
+
return {
|
|
696
|
+
ok: false,
|
|
697
|
+
stdout: fixtureResult.stdout,
|
|
698
|
+
stderr: fixtureResult.stderr || "backend pytest fixture resolution could not start",
|
|
699
|
+
failureCategory: fixtureResult.failureCategory,
|
|
700
|
+
durationMs: Date.now() - started,
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
fixtureResolution = { exitCode: fixtureResult.exitCode ?? 2, stdout: fixtureResult.stdout, stderr: fixtureResult.stderr };
|
|
704
|
+
}
|
|
631
705
|
facts = assessBackendPytestCollection({
|
|
632
706
|
phase: "initial",
|
|
633
707
|
mappedScripts,
|
|
@@ -635,6 +709,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
635
709
|
exitCode: result.exitCode ?? 2,
|
|
636
710
|
stdout: result.stdout,
|
|
637
711
|
stderr: result.stderr,
|
|
712
|
+
fixtureResolution,
|
|
638
713
|
});
|
|
639
714
|
}
|
|
640
715
|
const artifacts = await writeBackendPytestCollectionArtifacts({
|
|
@@ -650,8 +725,10 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
650
725
|
repairEligible: facts.repairEligible,
|
|
651
726
|
collectionAttempted: facts.collectionAttempted,
|
|
652
727
|
pytestExitCode: facts.pytestExitCode,
|
|
728
|
+
fixtureResolutionStatus: facts.fixtureResolutionStatus,
|
|
653
729
|
collectedItemCount: facts.collectedItemCount,
|
|
654
730
|
missingMappedScripts: facts.missingMappedScripts,
|
|
731
|
+
repairPaths: facts.repairPaths,
|
|
655
732
|
factsPath: "contracts/backend-test-pytest-collection-initial.json",
|
|
656
733
|
}),
|
|
657
734
|
stderr: "",
|
|
@@ -669,6 +746,12 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
669
746
|
}
|
|
670
747
|
else if (initial.status === "REPAIRABLE") {
|
|
671
748
|
const mappedScripts = await collectBackendTestMappedPytestScripts(input.cwd);
|
|
749
|
+
const scenario = await runBackendScenarioParamPreflight({
|
|
750
|
+
workspaceRoot: input.cwd,
|
|
751
|
+
runDir: meta.runDir,
|
|
752
|
+
strictScenarioParamGate: Boolean((meta.spec.globalConstraints ?? []).some((item) => /strictScenarioParamGate\s*=\s*true/i.test(item))),
|
|
753
|
+
});
|
|
754
|
+
outputs.push(...scenario.outputs);
|
|
672
755
|
const inventory = await buildBackendPytestAssetInventory(input.cwd, mappedScripts);
|
|
673
756
|
const shellQuote = (value) => `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
674
757
|
const targets = mappedScripts.map(shellQuote).join(" ");
|
|
@@ -686,6 +769,23 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
686
769
|
durationMs: Date.now() - started,
|
|
687
770
|
};
|
|
688
771
|
}
|
|
772
|
+
let fixtureResolution;
|
|
773
|
+
if ((result.exitCode ?? 2) === 0) {
|
|
774
|
+
const fixtureCommand = `PYTHONDONTWRITEBYTECODE=1 python -m pytest --setup-plan -q -p no:cacheprovider ${targets}`;
|
|
775
|
+
const [fixtureResult] = await executePipelineCommands(input, meta, [fixtureCommand]);
|
|
776
|
+
if (!fixtureResult)
|
|
777
|
+
throw new Error("backend pytest final fixture-resolution command did not produce a result");
|
|
778
|
+
if (["spawn-error", "timeout", "termination-unconfirmed"].includes(fixtureResult.failureCategory ?? "")) {
|
|
779
|
+
return {
|
|
780
|
+
ok: false,
|
|
781
|
+
stdout: fixtureResult.stdout,
|
|
782
|
+
stderr: fixtureResult.stderr || "backend pytest final fixture resolution could not start",
|
|
783
|
+
failureCategory: fixtureResult.failureCategory,
|
|
784
|
+
durationMs: Date.now() - started,
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
fixtureResolution = { exitCode: fixtureResult.exitCode ?? 2, stdout: fixtureResult.stdout, stderr: fixtureResult.stderr };
|
|
788
|
+
}
|
|
689
789
|
const finalFacts = assessBackendPytestCollection({
|
|
690
790
|
phase: "final",
|
|
691
791
|
mappedScripts,
|
|
@@ -693,6 +793,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
693
793
|
exitCode: result.exitCode ?? 2,
|
|
694
794
|
stdout: result.stdout,
|
|
695
795
|
stderr: result.stderr,
|
|
796
|
+
fixtureResolution,
|
|
696
797
|
});
|
|
697
798
|
effective = await materializeEffectiveBackendPytestCollection({
|
|
698
799
|
workspaceRoot: input.cwd,
|
|
@@ -708,6 +809,14 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
708
809
|
stem: "effective",
|
|
709
810
|
facts: effective,
|
|
710
811
|
});
|
|
812
|
+
const scenarioFacts = await readBackendScenarioParamFacts(path.join(meta.runDir, "contracts", "backend-test-scenario-param-consistency-facts.json"));
|
|
813
|
+
const readiness = await materializeBackendTestExecutionReadiness({
|
|
814
|
+
runDir: meta.runDir,
|
|
815
|
+
workspaceRoot: input.cwd,
|
|
816
|
+
effective,
|
|
817
|
+
scenarioParamStatus: scenarioFacts.overallStatus,
|
|
818
|
+
scenarioParamRepairAttempt: scenarioFacts.repairAttempt,
|
|
819
|
+
});
|
|
711
820
|
return {
|
|
712
821
|
ok: true,
|
|
713
822
|
stdout: JSON.stringify({
|
|
@@ -715,7 +824,10 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
715
824
|
collectionSource: effective.collectionSource,
|
|
716
825
|
repairAttempt: effective.repairAttempt,
|
|
717
826
|
collectedItemCount: effective.collectedItemCount,
|
|
827
|
+
fixtureResolutionStatus: effective.fixtureResolutionStatus,
|
|
828
|
+
executionReadinessStatus: readiness.status,
|
|
718
829
|
factsPath: "contracts/backend-test-pytest-collection-effective.json",
|
|
830
|
+
readinessPath: "contracts/backend-test-execution-readiness.json",
|
|
719
831
|
reportPath: artifacts.reportPath,
|
|
720
832
|
}),
|
|
721
833
|
stderr: "",
|
|
@@ -770,76 +882,18 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
770
882
|
durationMs: Date.now() - started,
|
|
771
883
|
};
|
|
772
884
|
}
|
|
773
|
-
// Scenario-param
|
|
885
|
+
// Scenario-param assessment and deterministic repair run before collection,
|
|
886
|
+
// so effective collection/fixture facts bind the final executable assets.
|
|
774
887
|
try {
|
|
775
|
-
const
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
});
|
|
781
|
-
const initialArtifacts = await writeBackendScenarioParamArtifacts({
|
|
782
|
-
runDir: meta.runDir,
|
|
783
|
-
facts: initialAssessment.facts,
|
|
784
|
-
markdown: initialAssessment.markdown,
|
|
785
|
-
});
|
|
786
|
-
outputs.push(`scenarioParamInitial=${initialArtifacts.reportPath}`, `scenarioParamInitialFacts=${initialArtifacts.factsPath}`);
|
|
787
|
-
let finalFacts = initialAssessment.facts;
|
|
788
|
-
let finalMarkdown = initialAssessment.markdown;
|
|
789
|
-
if (initialAssessment.facts.repairEligible) {
|
|
790
|
-
const repair = await applyDeterministicScenarioParamRepairs({
|
|
791
|
-
workspaceRoot: input.cwd,
|
|
792
|
-
facts: initialAssessment.facts,
|
|
793
|
-
});
|
|
794
|
-
const auditPath = await writeScenarioParamRepairAudit({
|
|
795
|
-
runDir: meta.runDir,
|
|
796
|
-
audit: repair.audit,
|
|
797
|
-
changedFiles: repair.changedFiles,
|
|
798
|
-
});
|
|
799
|
-
outputs.push(`scenarioParamRepairAudit=${auditPath}`, `scenarioParamRepaired=${repair.repaired.join(",") || "(none)"}`);
|
|
800
|
-
const reassessment = await assessBackendScenarioParamConsistency({
|
|
801
|
-
workspaceRoot: input.cwd,
|
|
802
|
-
phase: "final",
|
|
803
|
-
repairAttempt: 1,
|
|
804
|
-
strictScenarioParamGate: initialAssessment.facts.strictScenarioParamGate,
|
|
805
|
-
});
|
|
806
|
-
finalFacts = reassessment.facts;
|
|
807
|
-
finalMarkdown = reassessment.markdown;
|
|
808
|
-
}
|
|
809
|
-
else {
|
|
810
|
-
finalFacts = {
|
|
811
|
-
...initialAssessment.facts,
|
|
812
|
-
phase: "final",
|
|
813
|
-
repairAttempt: 0,
|
|
814
|
-
repairEligible: false,
|
|
815
|
-
};
|
|
816
|
-
finalMarkdown = initialAssessment.markdown.replace("Phase: initial", "Phase: final");
|
|
817
|
-
}
|
|
818
|
-
const finalArtifacts = await writeBackendScenarioParamArtifacts({
|
|
819
|
-
runDir: meta.runDir,
|
|
820
|
-
facts: finalFacts,
|
|
821
|
-
markdown: finalMarkdown,
|
|
822
|
-
});
|
|
823
|
-
outputs.push(`scenarioParamFinal=${finalArtifacts.reportPath}`, `scenarioParamFinalFacts=${finalArtifacts.factsPath}`, `scenarioParamMismatch=${finalFacts.summary.mismatchCount}`, finalMarkdown);
|
|
824
|
-
if (finalFacts.strictScenarioParamGate &&
|
|
825
|
-
finalFacts.summary.mismatchCount > 0) {
|
|
826
|
-
return {
|
|
827
|
-
ok: false,
|
|
828
|
-
stdout: outputs.join("\n\n"),
|
|
829
|
-
stderr: "backend-test strict scenario-param gate blocked residual MISMATCH before execute",
|
|
830
|
-
failureCategory: "invalid-output",
|
|
831
|
-
durationMs: Date.now() - started,
|
|
832
|
-
};
|
|
833
|
-
}
|
|
888
|
+
const scenarioFactsPath = path.join(meta.runDir, "contracts", "backend-test-scenario-param-consistency-facts.json");
|
|
889
|
+
const scenarioReportPath = path.join(meta.runDir, "reports", "backend-test-scenario-param-consistency.md");
|
|
890
|
+
const scenarioFacts = await readBackendScenarioParamFacts(scenarioFactsPath);
|
|
891
|
+
const scenarioReport = await readFile(scenarioReportPath, "utf8");
|
|
892
|
+
outputs.push(`scenarioParamFinal=${scenarioReportPath}`, `scenarioParamFinalFacts=${scenarioFactsPath}`, `scenarioParamStatus=${scenarioFacts.overallStatus}`, scenarioReport);
|
|
834
893
|
}
|
|
835
894
|
catch (error) {
|
|
836
895
|
const message = error instanceof Error ? error.message : String(error);
|
|
837
|
-
|
|
838
|
-
message +
|
|
839
|
-
"\n";
|
|
840
|
-
const reportPath = await writeRunReport(meta.runDir, "backend-test-scenario-param-consistency.md", report);
|
|
841
|
-
outputs.push(`scenarioParam=${reportPath}`, report);
|
|
842
|
-
// Advisory by default: do not fail the node on scenario-param infrastructure errors.
|
|
896
|
+
outputs.push("# Backend Test Scenario-Param Consistency\n\n## Status\n\nUNAVAILABLE\n\n## Findings\n\n- Pre-collection scenario-param evidence missing: " + message + "\n");
|
|
843
897
|
}
|
|
844
898
|
}
|
|
845
899
|
else if (pipeline === "markdown-manifest") {
|
|
@@ -866,8 +920,8 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
866
920
|
outputs.push(`manifest=${manifestPath}`, `materializationStatus=${manifest.materializationStatus ?? "available"}`, `coverageSummary.explicitAcCount=${summary?.explicitAcCount ?? "unavailable"}`, `coverageSummary.coveredAcCount=${summary?.coveredAcCount ?? "unavailable"}`, `coverageSummary.caseCount=${summary?.caseCount ?? "unavailable"}`, `coverageSummary.generatedCount=${summary?.generatedCount ?? "unavailable"}`, `ruleCoverageSummary.ruleCount=${manifest.ruleCoverageSummary?.ruleCount ?? "unavailable"}`, `correspondenceSummary.exactCorrespondenceCount=${manifest.correspondenceSummary?.exactCorrespondenceCount ?? "unavailable"}`, `correspondenceSummary.primarySymbolCount=${manifest.correspondenceSummary?.primarySymbolCount ?? "unavailable"}`, `correspondenceSummary.testPoints=${manifest.correspondenceSummary?.mappedTestPointCount ?? "unavailable"}/${manifest.correspondenceSummary?.testPointCount ?? "unavailable"}`, `correspondenceSummary.variantTestPointCount=${manifest.correspondenceSummary?.variantTestPointCount ?? "unavailable"}`, `correspondenceSummary.assertionTestPointCount=${manifest.correspondenceSummary?.assertionTestPointCount ?? "unavailable"}`, `correspondenceSummary.crossCuttingTestPointCount=${manifest.correspondenceSummary?.crossCuttingTestPointCount ?? "unavailable"}`, `correspondenceSummary.unclassifiedTestPointCount=${manifest.correspondenceSummary?.unclassifiedTestPointCount ?? "unavailable"}`, `correspondenceSummary.duplicateBindingTestPointCount=${manifest.correspondenceSummary?.duplicateBindingTestPointCount ?? "unavailable"}`);
|
|
867
921
|
}
|
|
868
922
|
else if (pipeline === "markdown-execute-html") {
|
|
869
|
-
const
|
|
870
|
-
await
|
|
923
|
+
const executionReadiness = await readBackendTestExecutionReadiness(path.join(meta.runDir, "contracts", "backend-test-execution-readiness.json"));
|
|
924
|
+
await assertBackendTestExecutionReadinessFresh(input.cwd, executionReadiness);
|
|
871
925
|
const mappedScripts = await collectBackendTestMappedPytestScripts(input.cwd);
|
|
872
926
|
const shellQuote = (value) => `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
873
927
|
const pytestTargets = mappedScripts.map(shellQuote).join(" ");
|
|
@@ -1068,8 +1122,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
1068
1122
|
const scenarioParamStatusByToken = new Map();
|
|
1069
1123
|
try {
|
|
1070
1124
|
const scenarioFacts = await readBackendScenarioParamFacts(path.join(meta.runDir, "contracts", "backend-test-scenario-param-consistency-facts.json"));
|
|
1071
|
-
scenarioParamFinalStatus =
|
|
1072
|
-
scenarioFacts.summary.mismatchCount > 0 ? "FAIL" : "PASS";
|
|
1125
|
+
scenarioParamFinalStatus = scenarioFacts.overallStatus;
|
|
1073
1126
|
scenarioParamRepairAttempt = scenarioFacts.repairAttempt;
|
|
1074
1127
|
for (const entry of scenarioFacts.entries) {
|
|
1075
1128
|
scenarioParamStatusByToken.set(entry.caseId, entry.status);
|
|
@@ -1085,16 +1138,20 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
1085
1138
|
htmlReportPath: "reports/backend-test.html",
|
|
1086
1139
|
total: parsed.tests,
|
|
1087
1140
|
passed: parsed.passed,
|
|
1088
|
-
failed: parsed.failed
|
|
1089
|
-
|
|
1141
|
+
failed: parsed.failed,
|
|
1142
|
+
errors: parsed.errors,
|
|
1143
|
+
durationLabel: `${parsed.durationMs ?? failureCases.reduce((sum, item) => sum + (item.durationMs ?? 0), 0)}ms`,
|
|
1090
1144
|
environmentSummary: await readFile(path.join(reportsDir, "backend-test-environment.md"), "utf8").catch(() => "unavailable"),
|
|
1091
1145
|
scenarioParamFinalStatus,
|
|
1092
1146
|
repairAttempt: scenarioParamRepairAttempt,
|
|
1093
1147
|
failures: failureCases.map((result) => {
|
|
1094
|
-
const
|
|
1095
|
-
const
|
|
1096
|
-
const caseId =
|
|
1097
|
-
|
|
1148
|
+
const identityText = `${result.nodeId ?? ""} ${result.name}`;
|
|
1149
|
+
const caseToken = identityText.match(/BE[-_][A-Z0-9_-]+[-_]\d{2,3}/i)?.[0];
|
|
1150
|
+
const caseId = caseToken
|
|
1151
|
+
? caseToken.replace(/^BE_/i, "BE-").replaceAll("_", "-").toUpperCase()
|
|
1152
|
+
: result.name;
|
|
1153
|
+
const tpToken = identityText.match(/TP-[A-Z0-9-]+/i)?.[0]?.toUpperCase();
|
|
1154
|
+
const scenarioParamStatus = (tpToken && scenarioParamStatusByToken.get(tpToken)) ||
|
|
1098
1155
|
scenarioParamStatusByToken.get(caseId);
|
|
1099
1156
|
const evidence = [result.message, result.details]
|
|
1100
1157
|
.filter(Boolean)
|
|
@@ -1102,7 +1159,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
1102
1159
|
const statusMatch = /assert\s+(\d{3})\s*==\s*(\d{3})/i.exec(evidence) ||
|
|
1103
1160
|
/expected[^\d]*(\d{3})[\s\S]{0,40}actual[^\d]*(\d{3})/i.exec(evidence);
|
|
1104
1161
|
return {
|
|
1105
|
-
name: result.name,
|
|
1162
|
+
name: tpToken ?? result.name,
|
|
1106
1163
|
caseId,
|
|
1107
1164
|
scenario: cases.find((item) => item.id === caseId)?.scenario ??
|
|
1108
1165
|
cases.find((item) => item.id === caseId)?.title ??
|
|
@@ -1110,7 +1167,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
1110
1167
|
expectedCode: statusMatch?.[2] ?? statusMatch?.[1],
|
|
1111
1168
|
actualCode: statusMatch?.[1] ?? statusMatch?.[2],
|
|
1112
1169
|
message: result.message || result.status,
|
|
1113
|
-
scriptPath: result.classname,
|
|
1170
|
+
scriptPath: result.nodeId ?? result.filePath ?? result.classname,
|
|
1114
1171
|
durationLabel: result.durationMs !== undefined
|
|
1115
1172
|
? `${result.durationMs}ms`
|
|
1116
1173
|
: undefined,
|