@tea-agent/loop-agent 0.24.5 → 0.24.7
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 +32 -0
- package/dist/executors/shell-executor.js +46 -3
- package/dist/worker/delivery/verification-bundle.js +26 -5
- 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/worker/outcomes/projector.js +5 -1
- package/dist/workflows/dag/backend-test-result-contract.js +103 -0
- package/dist/workflows/dag/frontend-test-result-contract.js +106 -41
- package/dist/workflows/dag/init-hybrid.js +4 -6
- package/dist/workflows/dag/node-execution.js +3 -2
- package/dist/workflows/dag/types.js +2 -0
- package/dist/workflows/dag/validate.js +3 -2
- package/package.json +1 -1
|
@@ -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"),
|
|
@@ -111,8 +111,12 @@ export async function projectOutcome(input) {
|
|
|
111
111
|
if (artifact.sha256 && artifact.sha256 !== recomputed) {
|
|
112
112
|
return contractError(`artifact sha256 mismatch for ${artifact.path}: declared=${artifact.sha256} actual=${recomputed}`);
|
|
113
113
|
}
|
|
114
|
+
// Persist portable repo-relative paths only (Delivery verifyArtifactRefs rejects abs paths).
|
|
115
|
+
const relativePath = path
|
|
116
|
+
.relative(path.resolve(input.repoRoot), resolved)
|
|
117
|
+
.replace(/\\/g, "/");
|
|
114
118
|
validatedArtifacts.push({
|
|
115
|
-
path:
|
|
119
|
+
path: relativePath,
|
|
116
120
|
sha256: recomputed,
|
|
117
121
|
...(artifact.kind ? { kind: artifact.kind } : {}),
|
|
118
122
|
...(artifact.schemaId ? { schemaId: artifact.schemaId } : {}),
|
|
@@ -773,6 +773,109 @@ async function readPytestExitCode(runDir, fromNodeId, exitRelativePath = "report
|
|
|
773
773
|
}
|
|
774
774
|
throw new Error("missing valid pytest exit evidence");
|
|
775
775
|
}
|
|
776
|
+
/**
|
|
777
|
+
* Materialize Result v1 from a pytest-html 4.x self-contained report (Markdown-first
|
|
778
|
+
* 8-node pipeline). `junit.relativePath` records the HTML path for integrity hashing
|
|
779
|
+
* (same field used historically for JUnit XML path).
|
|
780
|
+
*/
|
|
781
|
+
export async function materializeBackendTestResultFromPytestHtml(input) {
|
|
782
|
+
const htmlRelativePath = input.htmlRelativePath ?? "reports/backend-test.html";
|
|
783
|
+
const artifactName = input.artifactName ?? "backend-test-result.json";
|
|
784
|
+
const outputDir = input.outputDir ?? "contracts";
|
|
785
|
+
if (!/^[a-z0-9][a-z0-9._-]*\.json$/.test(artifactName) ||
|
|
786
|
+
!/^[a-z0-9][a-z0-9._-]*$/.test(outputDir)) {
|
|
787
|
+
throw new Error("unsafe structured artifact path");
|
|
788
|
+
}
|
|
789
|
+
let html = input.htmlContent;
|
|
790
|
+
if (html === undefined) {
|
|
791
|
+
const htmlAbs = path.join(input.runDir, ...htmlRelativePath.split("/"));
|
|
792
|
+
try {
|
|
793
|
+
html = await readFile(htmlAbs, "utf8");
|
|
794
|
+
}
|
|
795
|
+
catch {
|
|
796
|
+
throw new Error(`missing pytest-html report at ${htmlRelativePath} (fail-closed for Result v1)`);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
if (!html.trim()) {
|
|
800
|
+
throw new Error("invalid pytest-html report: empty report");
|
|
801
|
+
}
|
|
802
|
+
let parsed;
|
|
803
|
+
try {
|
|
804
|
+
parsed = parsePytestHtmlReport(html);
|
|
805
|
+
}
|
|
806
|
+
catch (error) {
|
|
807
|
+
throw new Error(`invalid pytest-html report: ${error instanceof Error ? error.message : String(error)}`);
|
|
808
|
+
}
|
|
809
|
+
const sha256 = createHash("sha256").update(html).digest("hex");
|
|
810
|
+
const exit = input.pytestExitCode;
|
|
811
|
+
let executionStatus = "completed";
|
|
812
|
+
let collectionStatus = "ok";
|
|
813
|
+
let outcome = "passed";
|
|
814
|
+
const looksLikeCollection = exit === 2 ||
|
|
815
|
+
(parsed.errors > 0 && parsed.passed + parsed.failed === 0) ||
|
|
816
|
+
parsed.failures.some((f) => f.kind === "error" &&
|
|
817
|
+
/collect|import|syntax/i.test(`${f.name} ${f.message}`));
|
|
818
|
+
if (looksLikeCollection && (parsed.errors > 0 || exit >= 2)) {
|
|
819
|
+
executionStatus = "collection-error";
|
|
820
|
+
collectionStatus = "error";
|
|
821
|
+
outcome = "collection-error";
|
|
822
|
+
}
|
|
823
|
+
else if (exit >= 2 && parsed.failed === 0 && parsed.errors === 0) {
|
|
824
|
+
executionStatus = "command-error";
|
|
825
|
+
collectionStatus = "unknown";
|
|
826
|
+
outcome = "command-error";
|
|
827
|
+
}
|
|
828
|
+
else if (parsed.failed > 0 || parsed.errors > 0 || exit === 1) {
|
|
829
|
+
executionStatus = "completed";
|
|
830
|
+
collectionStatus = "ok";
|
|
831
|
+
outcome = "completed-with-failures";
|
|
832
|
+
}
|
|
833
|
+
else if (exit === 0 && parsed.failed === 0 && parsed.errors === 0) {
|
|
834
|
+
executionStatus = "completed";
|
|
835
|
+
collectionStatus = "ok";
|
|
836
|
+
outcome = "passed";
|
|
837
|
+
}
|
|
838
|
+
else {
|
|
839
|
+
executionStatus = "command-error";
|
|
840
|
+
collectionStatus = "unknown";
|
|
841
|
+
outcome = "command-error";
|
|
842
|
+
}
|
|
843
|
+
const commandSummary = input.commandSummary ??
|
|
844
|
+
`PYTHONUTF8=1 PYTHONIOENCODING=utf-8 PYTHONDONTWRITEBYTECODE=1 python -m pytest -v -p no:cacheprovider --html=${htmlRelativePath} --self-contained-html`;
|
|
845
|
+
assertNoSecrets("commandSummary", commandSummary);
|
|
846
|
+
const result = backendTestResultContractSchema.parse({
|
|
847
|
+
schemaVersion: 1,
|
|
848
|
+
executionStatus,
|
|
849
|
+
pytestExitCode: exit,
|
|
850
|
+
collectionStatus,
|
|
851
|
+
tests: parsed.tests,
|
|
852
|
+
passed: parsed.passed,
|
|
853
|
+
failed: parsed.failed,
|
|
854
|
+
error: parsed.errors,
|
|
855
|
+
skipped: parsed.skipped,
|
|
856
|
+
durationMs: parsed.durationMs,
|
|
857
|
+
junit: {
|
|
858
|
+
relativePath: htmlRelativePath,
|
|
859
|
+
sha256,
|
|
860
|
+
},
|
|
861
|
+
commandSummary,
|
|
862
|
+
failures: parsed.failures.map((f) => ({
|
|
863
|
+
classname: f.classname || "unknown",
|
|
864
|
+
name: f.name || "unknown",
|
|
865
|
+
message: truncate(f.message || "failure"),
|
|
866
|
+
kind: f.kind,
|
|
867
|
+
})),
|
|
868
|
+
outcome,
|
|
869
|
+
});
|
|
870
|
+
const relativePath = path.posix.join(outputDir, artifactName);
|
|
871
|
+
const artifactPath = await writeDagRunJsonArtifact(input.runDir, relativePath, result);
|
|
872
|
+
const serialized = `${JSON.stringify(result, null, 2)}\n`;
|
|
873
|
+
return {
|
|
874
|
+
path: artifactPath,
|
|
875
|
+
sha256: createHash("sha256").update(serialized).digest("hex"),
|
|
876
|
+
schemaId: BACKEND_TEST_RESULT_SCHEMA_ID,
|
|
877
|
+
};
|
|
878
|
+
}
|
|
776
879
|
export async function materializeBackendTestResultFromRunDir(input) {
|
|
777
880
|
if (!/^[a-z0-9][a-z0-9._-]*\.json$/.test(input.artifactName) ||
|
|
778
881
|
!/^[a-z0-9][a-z0-9._-]*$/.test(input.outputDir)) {
|