@tea-agent/loop-agent 0.24.4 → 0.24.6
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 +47 -0
- package/README.md +5 -2
- package/dist/application/dag/generate-task-dag.js +5 -8
- package/dist/commands/init.js +19 -5
- package/dist/executors/shell-executor.js +10 -2
- package/dist/task/task-demand-routing.js +27 -14
- package/dist/worker/cli.js +59 -14
- package/dist/worker/observe/dag-run-artifacts.js +90 -0
- package/dist/worker/observe/node-input.js +444 -0
- package/dist/worker/observe/routes.js +17 -0
- package/dist/worker/observe/static/api.js +9 -0
- package/dist/worker/observe/static/constants.js +9 -0
- package/dist/worker/observe/static/state.js +14 -0
- package/dist/worker/observe/static/styles.css +74 -0
- package/dist/worker/observe/static/views/dag-inspector.js +371 -15
- package/dist/workflows/dag/backend-test-markdown-workflow.js +75 -9
- package/dist/workflows/dag/backend-test-result-contract.js +103 -0
- package/dist/workflows/dag/init-hybrid.js +9 -8
- package/dist/workflows/dag/node-execution.js +3 -2
- package/docs/templates/backend-test-dag.generate-pytest.prompt.md +3 -2
- package/docs/templates/backend-test-dag.json +7 -7
- package/docs/templates/backend-test-dag.review-cases.prompt.md +1 -1
- package/docs/templates/evaluation/agents-map-slim-v1.md +1 -1
- package/docs/templates/evaluation/agents-map-verbose-v0.md +2 -2
- package/harness.json +1 -1
- package/package.json +1 -1
- package/skills/agent-worker/SKILL.md +1 -1
- package/skills/loop-agent/SKILL.md +1 -1
- package/skills/loop-agent/references/command-reference.md +3 -2
- package/skills/loop-agent/references/hybrid-dag.md +2 -2
|
@@ -10,12 +10,13 @@ import {
|
|
|
10
10
|
UI_TEXT,
|
|
11
11
|
} from "../constants.js";
|
|
12
12
|
import { clearNode, el } from "../dom.js";
|
|
13
|
-
import { fetchJson, fetchSpecEvidenceFile } from "../api.js";
|
|
13
|
+
import { fetchJson, fetchSpecEvidenceFile, fetchDagNodeInput } from "../api.js";
|
|
14
14
|
import { badge } from "../format.js";
|
|
15
15
|
import { renderMarkdown } from "../markdown-render.js";
|
|
16
16
|
import {
|
|
17
17
|
uiState,
|
|
18
18
|
makeSessionEventIdentity,
|
|
19
|
+
makeDagNodeInputIdentity,
|
|
19
20
|
openSpecEvidenceDetail,
|
|
20
21
|
closeSpecEvidenceDetail,
|
|
21
22
|
updateDagTimelineViewportState,
|
|
@@ -50,9 +51,13 @@ export function selectDagNode(dagRunId, nodeId) {
|
|
|
50
51
|
// into node B's spec-evidence panel (AC-007 regression guard).
|
|
51
52
|
uiState.specEvidenceDetail = null;
|
|
52
53
|
uiState.specEvidenceListEvidence = null;
|
|
54
|
+
uiState.dagNodeInputData = null;
|
|
55
|
+
uiState.dagNodeInputError = null;
|
|
56
|
+
uiState.dagNodeInputLoading = false;
|
|
53
57
|
// Bump the session-event identity token so any in-flight response for the
|
|
54
58
|
// previous node is rejected before it can mutate the current node cache.
|
|
55
59
|
uiState.sessionEventIdentity = makeSessionEventIdentity(dagRunId, nodeId);
|
|
60
|
+
uiState.dagNodeInputIdentity = makeDagNodeInputIdentity(dagRunId, nodeId);
|
|
56
61
|
}
|
|
57
62
|
void import("./dag.js").then((m) => m.renderDagDetail(dagRunId, false));
|
|
58
63
|
}
|
|
@@ -530,16 +535,337 @@ function renderSpecEvidenceDetail(content, dagRunId, nodeId) {
|
|
|
530
535
|
content.appendChild(wrapper);
|
|
531
536
|
}
|
|
532
537
|
|
|
538
|
+
function sourceLabel(source) {
|
|
539
|
+
if (source === "defaults") return "继承默认值";
|
|
540
|
+
if (source === "schema-default") return "schema 默认";
|
|
541
|
+
if (source === "merged") return "合并";
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function appendMetaRow(dl, label, value, source) {
|
|
546
|
+
if (value == null || value === "") return;
|
|
547
|
+
const dt = el("dt", null, label);
|
|
548
|
+
const dd = el("dd", null, String(value));
|
|
549
|
+
const inherited = sourceLabel(source);
|
|
550
|
+
if (inherited) {
|
|
551
|
+
dd.appendChild(document.createTextNode(" "));
|
|
552
|
+
dd.appendChild(el("span", "node-input-inherited", inherited));
|
|
553
|
+
}
|
|
554
|
+
dl.appendChild(dt);
|
|
555
|
+
dl.appendChild(dd);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function appendCodeList(parent, title, values) {
|
|
559
|
+
if (!values || values.length === 0) return;
|
|
560
|
+
const section = el("section", "node-input-section");
|
|
561
|
+
section.appendChild(el("h4", null, title));
|
|
562
|
+
const list = el("ul", "node-input-code-list");
|
|
563
|
+
for (const value of values) {
|
|
564
|
+
const item = document.createElement("li");
|
|
565
|
+
const code = document.createElement("code");
|
|
566
|
+
code.textContent = String(value);
|
|
567
|
+
item.appendChild(code);
|
|
568
|
+
list.appendChild(item);
|
|
569
|
+
}
|
|
570
|
+
section.appendChild(list);
|
|
571
|
+
parent.appendChild(section);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function renderDagNodeInputDetail(content, data) {
|
|
575
|
+
const wrap = el("div", "node-input-root");
|
|
576
|
+
wrap.appendChild(
|
|
577
|
+
el("p", "node-input-semantics", UI_TEXT.inputFrozenLabel),
|
|
578
|
+
);
|
|
579
|
+
|
|
580
|
+
if (data.summary) {
|
|
581
|
+
const section = el("section", "node-input-section");
|
|
582
|
+
section.appendChild(el("h4", null, "摘要"));
|
|
583
|
+
const dl = el("dl", "node-input-summary");
|
|
584
|
+
appendMetaRow(
|
|
585
|
+
dl,
|
|
586
|
+
"executor",
|
|
587
|
+
data.summary.executor,
|
|
588
|
+
data.summary.executorSource,
|
|
589
|
+
);
|
|
590
|
+
appendMetaRow(dl, "role", data.summary.role, null);
|
|
591
|
+
appendMetaRow(dl, "complexity", data.summary.complexity, null);
|
|
592
|
+
appendMetaRow(
|
|
593
|
+
dl,
|
|
594
|
+
"writePolicy",
|
|
595
|
+
data.summary.writePolicy,
|
|
596
|
+
data.summary.writePolicySource,
|
|
597
|
+
);
|
|
598
|
+
appendMetaRow(dl, "toolProfile", data.summary.toolProfile, null);
|
|
599
|
+
section.appendChild(dl);
|
|
600
|
+
wrap.appendChild(section);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (data.dependency) {
|
|
604
|
+
const section = el("section", "node-input-section");
|
|
605
|
+
section.appendChild(el("h4", null, "依赖"));
|
|
606
|
+
const dl = el("dl", "node-input-summary");
|
|
607
|
+
appendMetaRow(
|
|
608
|
+
dl,
|
|
609
|
+
"depends_on",
|
|
610
|
+
(data.dependency.dependsOn ?? []).join(", ") || "(none)",
|
|
611
|
+
null,
|
|
612
|
+
);
|
|
613
|
+
if (data.dependency.dependsPolicy) {
|
|
614
|
+
appendMetaRow(dl, "dependsPolicy", data.dependency.dependsPolicy, null);
|
|
615
|
+
}
|
|
616
|
+
if ((data.dependency.failureAwareDependsOn ?? []).length > 0) {
|
|
617
|
+
appendMetaRow(
|
|
618
|
+
dl,
|
|
619
|
+
"failureAwareDependsOn",
|
|
620
|
+
data.dependency.failureAwareDependsOn.join(", "),
|
|
621
|
+
null,
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
if (data.dependency.runIf) {
|
|
625
|
+
appendMetaRow(dl, "runIf", data.dependency.runIf, null);
|
|
626
|
+
}
|
|
627
|
+
section.appendChild(dl);
|
|
628
|
+
wrap.appendChild(section);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (data.prompt) {
|
|
632
|
+
const section = el("section", "node-input-section");
|
|
633
|
+
section.appendChild(el("h4", null, "任务正文"));
|
|
634
|
+
if (data.prompt.source?.path) {
|
|
635
|
+
const meta = el("p", "muted");
|
|
636
|
+
meta.textContent = `source: ${data.prompt.source.path}${
|
|
637
|
+
data.prompt.source.sha256Prefix
|
|
638
|
+
? ` (sha256 ${data.prompt.source.sha256Prefix}…)`
|
|
639
|
+
: ""
|
|
640
|
+
}`;
|
|
641
|
+
section.appendChild(meta);
|
|
642
|
+
}
|
|
643
|
+
section.appendChild(renderMarkdown(data.prompt.text ?? ""));
|
|
644
|
+
if (data.prompt.truncated) {
|
|
645
|
+
section.appendChild(
|
|
646
|
+
el(
|
|
647
|
+
"p",
|
|
648
|
+
"node-input-truncated",
|
|
649
|
+
`已截断,仅显示前 ${data.prompt.maxBytes ?? 0} 字节(UTF-8)。`,
|
|
650
|
+
),
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
wrap.appendChild(section);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (data.executorInput) {
|
|
657
|
+
const section = el("section", "node-input-section");
|
|
658
|
+
section.appendChild(el("h4", null, "执行器输入摘要"));
|
|
659
|
+
const dl = el("dl", "node-input-summary");
|
|
660
|
+
appendMetaRow(dl, "kind", data.executorInput.kind, null);
|
|
661
|
+
if (data.executorInput.modelHint) {
|
|
662
|
+
appendMetaRow(dl, "modelHint", data.executorInput.modelHint, null);
|
|
663
|
+
}
|
|
664
|
+
section.appendChild(dl);
|
|
665
|
+
appendCodeList(section, "skills", data.executorInput.skills);
|
|
666
|
+
if (data.executorInput.shell) {
|
|
667
|
+
const shell = data.executorInput.shell;
|
|
668
|
+
const shellDl = el("dl", "node-input-summary");
|
|
669
|
+
appendMetaRow(shellDl, "preset", shell.preset, null);
|
|
670
|
+
appendMetaRow(shellDl, "cwd", shell.cwd, null);
|
|
671
|
+
appendMetaRow(shellDl, "timeoutMs", shell.timeoutMs, null);
|
|
672
|
+
appendMetaRow(
|
|
673
|
+
shellDl,
|
|
674
|
+
"nonZeroExitPolicy",
|
|
675
|
+
shell.nonZeroExitPolicy,
|
|
676
|
+
null,
|
|
677
|
+
);
|
|
678
|
+
appendMetaRow(
|
|
679
|
+
shellDl,
|
|
680
|
+
"gates",
|
|
681
|
+
(shell.gates ?? []).join(", ") || "(none)",
|
|
682
|
+
null,
|
|
683
|
+
);
|
|
684
|
+
section.appendChild(shellDl);
|
|
685
|
+
if ((shell.commands ?? []).length > 0) {
|
|
686
|
+
const list = el("ol", "node-input-command-list");
|
|
687
|
+
for (const command of shell.commands) {
|
|
688
|
+
const item = document.createElement("li");
|
|
689
|
+
const pre = document.createElement("pre");
|
|
690
|
+
const code = document.createElement("code");
|
|
691
|
+
code.textContent = String(command);
|
|
692
|
+
pre.appendChild(code);
|
|
693
|
+
item.appendChild(pre);
|
|
694
|
+
list.appendChild(item);
|
|
695
|
+
}
|
|
696
|
+
section.appendChild(list);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
if (data.executorInput.static) {
|
|
700
|
+
const staticBlock = data.executorInput.static;
|
|
701
|
+
const staticDl = el("dl", "node-input-summary");
|
|
702
|
+
appendMetaRow(staticDl, "status", staticBlock.status, null);
|
|
703
|
+
section.appendChild(staticDl);
|
|
704
|
+
section.appendChild(renderMarkdown(staticBlock.resultPreview ?? ""));
|
|
705
|
+
if (staticBlock.truncated) {
|
|
706
|
+
section.appendChild(
|
|
707
|
+
el(
|
|
708
|
+
"p",
|
|
709
|
+
"node-input-truncated",
|
|
710
|
+
`已截断,仅显示前 ${staticBlock.maxBytes ?? 0} 字节(UTF-8)。`,
|
|
711
|
+
),
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
wrap.appendChild(section);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
if (data.boundaries) {
|
|
719
|
+
const section = el("section", "node-input-section");
|
|
720
|
+
section.appendChild(el("h4", null, "边界与输出契约"));
|
|
721
|
+
const dl = el("dl", "node-input-summary");
|
|
722
|
+
appendMetaRow(dl, "outputMode", data.boundaries.outputMode, null);
|
|
723
|
+
appendMetaRow(
|
|
724
|
+
dl,
|
|
725
|
+
"firstProtocolLine",
|
|
726
|
+
data.boundaries.firstProtocolLine,
|
|
727
|
+
null,
|
|
728
|
+
);
|
|
729
|
+
section.appendChild(dl);
|
|
730
|
+
if (data.boundaries.outputContract) {
|
|
731
|
+
section.appendChild(el("h5", null, "outputContract"));
|
|
732
|
+
section.appendChild(renderMarkdown(data.boundaries.outputContract));
|
|
733
|
+
}
|
|
734
|
+
appendCodeList(section, "allowedPaths", data.boundaries.allowedPaths);
|
|
735
|
+
appendCodeList(section, "forbiddenPaths", data.boundaries.forbiddenPaths);
|
|
736
|
+
appendCodeList(section, "writeSet", data.boundaries.writeSet);
|
|
737
|
+
wrap.appendChild(section);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
if (data.promptFingerprint?.exists) {
|
|
741
|
+
const section = el("section", "node-input-section");
|
|
742
|
+
section.appendChild(el("h4", null, "Assembled prompt 指纹"));
|
|
743
|
+
const dl = el("dl", "node-input-summary");
|
|
744
|
+
appendMetaRow(
|
|
745
|
+
dl,
|
|
746
|
+
"sha256",
|
|
747
|
+
data.promptFingerprint.sha256Prefix ?? "(parse failed)",
|
|
748
|
+
null,
|
|
749
|
+
);
|
|
750
|
+
appendMetaRow(
|
|
751
|
+
dl,
|
|
752
|
+
"lengthChars",
|
|
753
|
+
data.promptFingerprint.lengthChars,
|
|
754
|
+
null,
|
|
755
|
+
);
|
|
756
|
+
appendMetaRow(dl, "artifact", data.promptFingerprint.artifact, null);
|
|
757
|
+
section.appendChild(dl);
|
|
758
|
+
section.appendChild(
|
|
759
|
+
el("p", "muted", UI_TEXT.inputFingerprintNote),
|
|
760
|
+
);
|
|
761
|
+
wrap.appendChild(section);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
if ((data.warnings ?? []).length > 0) {
|
|
765
|
+
const section = el("section", "node-input-section");
|
|
766
|
+
section.appendChild(el("h4", null, "警告"));
|
|
767
|
+
for (const warning of data.warnings) {
|
|
768
|
+
section.appendChild(el("p", "node-input-warning", String(warning)));
|
|
769
|
+
}
|
|
770
|
+
wrap.appendChild(section);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
content.appendChild(wrap);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function unavailableMessage(reason) {
|
|
777
|
+
if (reason === "unsupported-dynamic-node") {
|
|
778
|
+
return UI_TEXT.inputDynamicUnsupported;
|
|
779
|
+
}
|
|
780
|
+
if (reason === "run-spec-unavailable") {
|
|
781
|
+
return UI_TEXT.inputRunSpecUnavailable;
|
|
782
|
+
}
|
|
783
|
+
if (reason === "task-not-found") {
|
|
784
|
+
return UI_TEXT.inputUnavailable;
|
|
785
|
+
}
|
|
786
|
+
return UI_TEXT.noInput;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
async function renderDagNodeInput(content, dagRunId, node) {
|
|
790
|
+
const nodeId = node.nodeId;
|
|
791
|
+
const identity = makeDagNodeInputIdentity(dagRunId, nodeId);
|
|
792
|
+
uiState.dagNodeInputIdentity = identity;
|
|
793
|
+
uiState.dagNodeInputLoading = true;
|
|
794
|
+
uiState.dagNodeInputError = null;
|
|
795
|
+
|
|
796
|
+
clearNode(content);
|
|
797
|
+
const loading = el("p", "muted", UI_TEXT.inputLoading);
|
|
798
|
+
loading.setAttribute("role", "status");
|
|
799
|
+
loading.setAttribute("aria-live", "polite");
|
|
800
|
+
content.appendChild(loading);
|
|
801
|
+
|
|
802
|
+
const result = await fetchDagNodeInput(dagRunId, nodeId);
|
|
803
|
+
if (
|
|
804
|
+
uiState.dagNodeInputIdentity !== identity ||
|
|
805
|
+
uiState.selectedDagNodeId !== nodeId ||
|
|
806
|
+
uiState.currentDagRunId !== dagRunId ||
|
|
807
|
+
uiState.dagInspectorTab !== "input" ||
|
|
808
|
+
!content.isConnected ||
|
|
809
|
+
content.dataset.dagNodeId !== String(nodeId) ||
|
|
810
|
+
content.dataset.dagRunId !== String(dagRunId)
|
|
811
|
+
) {
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
uiState.dagNodeInputLoading = false;
|
|
816
|
+
clearNode(content);
|
|
817
|
+
|
|
818
|
+
if (!result.ok) {
|
|
819
|
+
const message =
|
|
820
|
+
result.body?.error ||
|
|
821
|
+
(result.status === 0
|
|
822
|
+
? UI_TEXT.inputError
|
|
823
|
+
: `${UI_TEXT.inputError}(HTTP ${result.status})`);
|
|
824
|
+
uiState.dagNodeInputError = message;
|
|
825
|
+
uiState.dagNodeInputData = null;
|
|
826
|
+
const error = el("p", "node-input-error", message);
|
|
827
|
+
error.setAttribute("role", "alert");
|
|
828
|
+
content.appendChild(error);
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
const data = result.body;
|
|
833
|
+
uiState.dagNodeInputData = data;
|
|
834
|
+
uiState.dagNodeInputError = null;
|
|
835
|
+
if (!data?.available) {
|
|
836
|
+
const empty = el(
|
|
837
|
+
"p",
|
|
838
|
+
"empty",
|
|
839
|
+
unavailableMessage(data?.availabilityReason),
|
|
840
|
+
);
|
|
841
|
+
empty.setAttribute("role", "status");
|
|
842
|
+
content.appendChild(empty);
|
|
843
|
+
if ((data?.warnings ?? []).length > 0) {
|
|
844
|
+
for (const warning of data.warnings) {
|
|
845
|
+
content.appendChild(el("p", "node-input-warning", String(warning)));
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
renderDagNodeInputDetail(content, data);
|
|
852
|
+
}
|
|
853
|
+
|
|
533
854
|
function fillDagInspectorContent(content, dagRunId, node) {
|
|
534
855
|
const previousRenderedNode = content.dataset.dagNodeId ?? "";
|
|
535
|
-
const
|
|
856
|
+
const previousRenderedRun = content.dataset.dagRunId ?? "";
|
|
857
|
+
const nodeChanged =
|
|
858
|
+
previousRenderedNode !== String(node.nodeId ?? "") ||
|
|
859
|
+
previousRenderedRun !== String(dagRunId ?? "");
|
|
536
860
|
if (nodeChanged) {
|
|
537
861
|
// Drop any open spec-evidence preview + cached list so a stale A detail
|
|
538
862
|
// cannot survive into node B (AC-007 regression guard).
|
|
539
863
|
uiState.specEvidenceDetail = null;
|
|
540
864
|
uiState.specEvidenceListEvidence = null;
|
|
865
|
+
uiState.dagNodeInputData = null;
|
|
866
|
+
uiState.dagNodeInputError = null;
|
|
541
867
|
}
|
|
542
|
-
const activeTab = ["timeline", "spec-evidence"].includes(
|
|
868
|
+
const activeTab = ["input", "timeline", "spec-evidence"].includes(
|
|
543
869
|
uiState.dagInspectorTab,
|
|
544
870
|
)
|
|
545
871
|
? uiState.dagInspectorTab
|
|
@@ -548,12 +874,15 @@ function renderSpecEvidenceDetail(content, dagRunId, nodeId) {
|
|
|
548
874
|
content.setAttribute("role", "tabpanel");
|
|
549
875
|
content.setAttribute("aria-labelledby", `dag-inspector-tab-${activeTab}`);
|
|
550
876
|
content.dataset.dagNodeId = String(node.nodeId ?? "");
|
|
877
|
+
content.dataset.dagRunId = String(dagRunId ?? "");
|
|
551
878
|
// Drop timeline-only classes when switching tabs so output layout stays clean.
|
|
552
879
|
content.className = "dag-inspector-content";
|
|
553
880
|
if (activeTab === "spec-evidence") {
|
|
554
881
|
void renderSpecEvidence(content, dagRunId, node.nodeId);
|
|
555
882
|
} else if (activeTab === "timeline") {
|
|
556
883
|
renderSessionTimeline(content, node.nodeId);
|
|
884
|
+
} else if (activeTab === "input") {
|
|
885
|
+
void renderDagNodeInput(content, dagRunId, node);
|
|
557
886
|
} else if (!node.outputPreview && !node.errorPreview) {
|
|
558
887
|
content.appendChild(el("p", "empty", UI_TEXT.noOutput));
|
|
559
888
|
} else {
|
|
@@ -592,6 +921,7 @@ function buildDagInspectorHeader(dagRunId, node) {
|
|
|
592
921
|
header.appendChild(controls);
|
|
593
922
|
|
|
594
923
|
const tabDefs = [
|
|
924
|
+
["input", "节点输入", "ri-login-box-line"],
|
|
595
925
|
["output", "节点输出", "ri-file-text-line"],
|
|
596
926
|
["timeline", "执行过程", "ri-route-line"],
|
|
597
927
|
["spec-evidence", "规范证据", "ri-book-open-line"],
|
|
@@ -660,6 +990,17 @@ function syncDagInspectorHeader(panel, dagRunId, node) {
|
|
|
660
990
|
}
|
|
661
991
|
|
|
662
992
|
function dagInspectorSignature(dagRunId, node) {
|
|
993
|
+
const inputRevision =
|
|
994
|
+
uiState.dagInspectorTab === "input"
|
|
995
|
+
? [
|
|
996
|
+
uiState.dagNodeInputIdentity,
|
|
997
|
+
uiState.dagNodeInputLoading,
|
|
998
|
+
uiState.dagNodeInputError ?? "",
|
|
999
|
+
uiState.dagNodeInputData?.availabilityReason ?? "",
|
|
1000
|
+
uiState.dagNodeInputData?.promptFingerprint?.sha256Prefix ?? "",
|
|
1001
|
+
node.status ?? "",
|
|
1002
|
+
]
|
|
1003
|
+
: null;
|
|
663
1004
|
return JSON.stringify([
|
|
664
1005
|
dagRunId,
|
|
665
1006
|
node.nodeId,
|
|
@@ -667,6 +1008,7 @@ function dagInspectorSignature(dagRunId, node) {
|
|
|
667
1008
|
node.outputPreview ?? "",
|
|
668
1009
|
node.errorPreview ?? "",
|
|
669
1010
|
uiState.dagInspectorTab === "timeline" ? uiState.sessionEventOffset : null,
|
|
1011
|
+
inputRevision,
|
|
670
1012
|
]);
|
|
671
1013
|
}
|
|
672
1014
|
|
|
@@ -702,17 +1044,30 @@ export function renderDagInspector(dagRunId, node, initial = true) {
|
|
|
702
1044
|
syncDagInspectorHeader(panel, dagRunId, node);
|
|
703
1045
|
if (panel.dataset.contentSignature === nextContentSignature) return;
|
|
704
1046
|
const previousNode = existingContent.dataset.dagNodeId ?? "";
|
|
705
|
-
const
|
|
706
|
-
const
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
1047
|
+
const previousRun = existingContent.dataset.dagRunId ?? "";
|
|
1048
|
+
const nodeChanged =
|
|
1049
|
+
previousNode !== String(node.nodeId ?? "") ||
|
|
1050
|
+
previousRun !== String(dagRunId ?? "");
|
|
1051
|
+
const activeTab = ["input", "timeline", "spec-evidence"].includes(
|
|
1052
|
+
uiState.dagInspectorTab,
|
|
1053
|
+
)
|
|
1054
|
+
? uiState.dagInspectorTab
|
|
1055
|
+
: "output";
|
|
1056
|
+
const isInputTab = activeTab === "input";
|
|
1057
|
+
const followLatest = isInputTab
|
|
1058
|
+
? false
|
|
1059
|
+
: nodeChanged ||
|
|
1060
|
+
computeFollowLatest(
|
|
1061
|
+
existingContent.scrollTop,
|
|
1062
|
+
existingContent.scrollHeight,
|
|
1063
|
+
existingContent.clientHeight,
|
|
1064
|
+
);
|
|
1065
|
+
const regionKey = `dag:${dagRunId}:${node.nodeId}:${isInputTab ? "input" : "output"}`;
|
|
1066
|
+
const viewportUpdater = isInputTab
|
|
1067
|
+
? "dagNodeInputViewportState"
|
|
1068
|
+
: "dagNodeOutputViewportState";
|
|
1069
|
+
uiState[viewportUpdater] = updateDagOutputViewportState(
|
|
1070
|
+
uiState[viewportUpdater],
|
|
716
1071
|
regionKey,
|
|
717
1072
|
existingContent.scrollLeft,
|
|
718
1073
|
existingContent.scrollTop,
|
|
@@ -729,7 +1084,8 @@ export function renderDagInspector(dagRunId, node, initial = true) {
|
|
|
729
1084
|
if (followLatest) {
|
|
730
1085
|
existingContent.scrollTop = existingContent.scrollHeight;
|
|
731
1086
|
} else {
|
|
732
|
-
|
|
1087
|
+
const saved = uiState[viewportUpdater];
|
|
1088
|
+
existingContent.scrollTop = nodeChanged ? 0 : (saved?.scrollTop ?? 0);
|
|
733
1089
|
}
|
|
734
1090
|
for (const [index, pre] of [
|
|
735
1091
|
...existingContent.querySelectorAll("pre"),
|
|
@@ -413,6 +413,18 @@ export async function validateBackendMarkdownCases(input) {
|
|
|
413
413
|
if (!hasAssertableExpectedResult(expected)) {
|
|
414
414
|
findings.push(`${testCase.id} has no structured expected result`);
|
|
415
415
|
}
|
|
416
|
+
const expectedScript = expectedBackendTestPytestScriptForMarkdownModule(relativeFile);
|
|
417
|
+
const mappedScripts = extractMappedPytestScripts(testCase.body);
|
|
418
|
+
if (mappedScripts.length === 0) {
|
|
419
|
+
findings.push(`${testCase.id} has no mapped pytest script in Automation Notes/自动化映射 (expected ${expectedScript} from module ${path.basename(relativeFile)})`);
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
for (const script of mappedScripts) {
|
|
423
|
+
if (script !== expectedScript) {
|
|
424
|
+
findings.push(`${testCase.id} automation mapping ${script} must equal module one-to-one path ${expectedScript} (from ${path.basename(relativeFile)})`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
416
428
|
}
|
|
417
429
|
}
|
|
418
430
|
const missing = input.requiredRequirementIds.filter((id) => !coveredAc.has(id));
|
|
@@ -454,6 +466,27 @@ export async function validateBackendMarkdownCases(input) {
|
|
|
454
466
|
function collectMarkdownCaseIds(markdown) {
|
|
455
467
|
return splitCases(markdown).map((testCase) => testCase.id);
|
|
456
468
|
}
|
|
469
|
+
/**
|
|
470
|
+
* Derive the stable module stem used for Markdown ↔ pytest one-to-one mapping.
|
|
471
|
+
* `testcase/md/<module>.md` → stem → `testcase/test_<stem>.py`.
|
|
472
|
+
* Example: `BE-HEALTH.md` / `be-health.md` / `order-api.md` → `be_health` / `order_api`.
|
|
473
|
+
*/
|
|
474
|
+
export function normalizeBackendTestModuleStem(moduleFileName) {
|
|
475
|
+
const base = path.basename(moduleFileName).replace(/\.md$/i, "");
|
|
476
|
+
const stem = base
|
|
477
|
+
.toLowerCase()
|
|
478
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
479
|
+
.replace(/^_+|_+$/g, "")
|
|
480
|
+
.replace(/_+/g, "_");
|
|
481
|
+
if (!stem) {
|
|
482
|
+
throw new Error(`cannot derive backend-test module stem from: ${moduleFileName}`);
|
|
483
|
+
}
|
|
484
|
+
return stem;
|
|
485
|
+
}
|
|
486
|
+
/** Expected pytest script path for a Markdown module file (basename or relative path). */
|
|
487
|
+
export function expectedBackendTestPytestScriptForMarkdownModule(moduleFileName) {
|
|
488
|
+
return `testcase/test_${normalizeBackendTestModuleStem(moduleFileName)}.py`;
|
|
489
|
+
}
|
|
457
490
|
function extractMappedPytestScripts(testCaseBody) {
|
|
458
491
|
const automation = sectionBody(testCaseBody, CASE_SECTION_ALIASES.automationNotes);
|
|
459
492
|
if (!automation.trim())
|
|
@@ -462,6 +495,15 @@ function extractMappedPytestScripts(testCaseBody) {
|
|
|
462
495
|
.map((match) => match[1].replaceAll("\\", "/"))
|
|
463
496
|
.filter((value) => !value.includes("..")));
|
|
464
497
|
}
|
|
498
|
+
function isSafeBackendPytestScript(script, workspaceRoot) {
|
|
499
|
+
const absolute = path.resolve(workspaceRoot, script);
|
|
500
|
+
const relative = path.relative(workspaceRoot, absolute).replaceAll(path.sep, "/");
|
|
501
|
+
return (!relative.startsWith("..") &&
|
|
502
|
+
!path.isAbsolute(relative) &&
|
|
503
|
+
script.startsWith("testcase/") &&
|
|
504
|
+
!script.includes("..") &&
|
|
505
|
+
/^test_.*\.py$/i.test(path.basename(script)));
|
|
506
|
+
}
|
|
465
507
|
function testFunctionRegion(input) {
|
|
466
508
|
const functionLineStart = input.source.lastIndexOf("\n", input.functionIndex - 1) + 1;
|
|
467
509
|
const functionLine = input.source.slice(functionLineStart, input.functionHeaderEnd);
|
|
@@ -610,26 +652,50 @@ export async function collectBackendTestMappedPytestScripts(workspaceRoot) {
|
|
|
610
652
|
for (const file of files) {
|
|
611
653
|
if (path.basename(file).toLowerCase() === "readme.md")
|
|
612
654
|
continue;
|
|
655
|
+
const relativeFile = path.relative(workspaceRoot, file).replaceAll(path.sep, "/");
|
|
656
|
+
const expectedScript = expectedBackendTestPytestScriptForMarkdownModule(relativeFile);
|
|
613
657
|
const markdown = await readFile(file, "utf8");
|
|
658
|
+
const mappedFromModule = new Set();
|
|
614
659
|
for (const testCase of splitCases(markdown)) {
|
|
615
|
-
for (const script of extractMappedPytestScripts(testCase.body))
|
|
660
|
+
for (const script of extractMappedPytestScripts(testCase.body)) {
|
|
661
|
+
mappedFromModule.add(script);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
// Prefer explicit mappings, but recover to the deterministic module stem path
|
|
665
|
+
// when the model wrote the correct one-to-one file while Markdown still names
|
|
666
|
+
// a drifted script (common: health.md maps test_health.py but writer emitted
|
|
667
|
+
// test_be_health.py for BE-HEALTH.md, or the reverse).
|
|
668
|
+
if (mappedFromModule.size === 0) {
|
|
669
|
+
if (await exists(path.resolve(workspaceRoot, expectedScript))) {
|
|
670
|
+
scripts.add(expectedScript);
|
|
671
|
+
}
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
for (const script of mappedFromModule) {
|
|
675
|
+
if (!isSafeBackendPytestScript(script, workspaceRoot)) {
|
|
676
|
+
throw new Error(`unsafe mapped pytest script: ${script}`);
|
|
677
|
+
}
|
|
678
|
+
if (await exists(path.resolve(workspaceRoot, script))) {
|
|
616
679
|
scripts.add(script);
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
if (isSafeBackendPytestScript(expectedScript, workspaceRoot) &&
|
|
683
|
+
(await exists(path.resolve(workspaceRoot, expectedScript)))) {
|
|
684
|
+
scripts.add(expectedScript);
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
throw new Error(`mapped pytest script is missing: ${script} (module one-to-one path ${expectedScript} also missing)`);
|
|
617
688
|
}
|
|
618
689
|
}
|
|
619
690
|
if (scripts.size === 0) {
|
|
620
691
|
throw new Error("no pytest scripts are mapped by final Markdown cases");
|
|
621
692
|
}
|
|
622
693
|
const safeScripts = [];
|
|
623
|
-
for (const script of unique(scripts)) {
|
|
624
|
-
|
|
625
|
-
const relative = path.relative(workspaceRoot, absolute);
|
|
626
|
-
if (relative.startsWith("..") ||
|
|
627
|
-
path.isAbsolute(relative) ||
|
|
628
|
-
!script.startsWith("testcase/") ||
|
|
629
|
-
!/^test_.*\.py$/i.test(path.basename(script))) {
|
|
694
|
+
for (const script of unique([...scripts]).sort()) {
|
|
695
|
+
if (!isSafeBackendPytestScript(script, workspaceRoot)) {
|
|
630
696
|
throw new Error(`unsafe mapped pytest script: ${script}`);
|
|
631
697
|
}
|
|
632
|
-
if (!(await exists(
|
|
698
|
+
if (!(await exists(path.resolve(workspaceRoot, script)))) {
|
|
633
699
|
throw new Error(`mapped pytest script is missing: ${script}`);
|
|
634
700
|
}
|
|
635
701
|
safeScripts.push(script);
|