
























@@ -0,0 +1,172 @@
1+import { describe, expect, it } from "vitest";
2+import {
3+NON_DELIVERABLE_TERMINAL_TURN_REASON,
4+resolveAttemptTrajectoryTerminal,
5+resolveTerminalAssistantTexts,
6+type ResolveAttemptTrajectoryTerminalParams,
7+} from "./attempt-trajectory-status.js";
8+9+function baseParams(
10+overrides: Partial<ResolveAttemptTrajectoryTerminalParams> = {},
11+): ResolveAttemptTrajectoryTerminalParams {
12+return {
13+aborted: false,
14+timedOut: false,
15+assistantTexts: [],
16+toolMetas: [],
17+didSendViaMessagingTool: false,
18+didSendDeterministicApprovalPrompt: false,
19+messagingToolSentTexts: [],
20+messagingToolSentMediaUrls: [],
21+messagingToolSentTargets: [],
22+successfulCronAdds: 0,
23+synthesizedPayloadCount: 0,
24+ ...overrides,
25+};
26+}
27+28+describe("attempt trajectory status", () => {
29+it("marks a terminal turn without visible text, tools, or delivery as an error", () => {
30+expect(resolveAttemptTrajectoryTerminal(baseParams())).toEqual({
31+status: "error",
32+terminalError: NON_DELIVERABLE_TERMINAL_TURN_REASON,
33+});
34+});
35+36+it("keeps visible assistant text as success", () => {
37+expect(
38+resolveAttemptTrajectoryTerminal(baseParams({ assistantTexts: ["Visible answer."] })),
39+).toEqual({ status: "success" });
40+});
41+42+it("keeps committed messaging tool delivery as success even without assistant text", () => {
43+expect(
44+resolveAttemptTrajectoryTerminal(
45+baseParams({
46+didSendViaMessagingTool: true,
47+messagingToolSentTargets: [{ channel: "telegram" }],
48+}),
49+),
50+).toEqual({ status: "success" });
51+});
52+53+it("does not treat an uncommitted messaging tool attempt as delivery", () => {
54+expect(
55+resolveAttemptTrajectoryTerminal(
56+baseParams({
57+didSendViaMessagingTool: true,
58+messagingToolSentTexts: [" "],
59+messagingToolSentMediaUrls: [" "],
60+}),
61+),
62+).toEqual({
63+status: "error",
64+terminalError: NON_DELIVERABLE_TERMINAL_TURN_REASON,
65+});
66+});
67+68+it("does not treat tool metadata alone as terminal progress", () => {
69+expect(
70+resolveAttemptTrajectoryTerminal(
71+baseParams({
72+toolMetas: [{ toolName: "read" }],
73+}),
74+),
75+).toEqual({
76+status: "error",
77+terminalError: NON_DELIVERABLE_TERMINAL_TURN_REASON,
78+});
79+});
80+81+it("keeps synthesized terminal payloads as success", () => {
82+expect(resolveAttemptTrajectoryTerminal(baseParams({ synthesizedPayloadCount: 1 }))).toEqual({
83+status: "success",
84+});
85+});
86+87+it("keeps heartbeat responses as success", () => {
88+expect(
89+resolveAttemptTrajectoryTerminal(
90+baseParams({
91+heartbeatToolResponse: { notify: false, summary: "ok" },
92+}),
93+),
94+).toEqual({
95+status: "success",
96+});
97+});
98+99+it("does not treat expected silent turns as non-deliverable failures", () => {
100+expect(resolveAttemptTrajectoryTerminal(baseParams({ silentExpected: true }))).toEqual({
101+status: "success",
102+});
103+});
104+105+it("does not treat eligible empty silent replies as non-deliverable failures", () => {
106+expect(
107+resolveAttemptTrajectoryTerminal(baseParams({ emptyAssistantReplyIsSilent: true })),
108+).toEqual({
109+status: "success",
110+});
111+});
112+113+it("does not let the raw silent policy hide ineligible empty failures", () => {
114+expect(
115+resolveAttemptTrajectoryTerminal(baseParams({ emptyAssistantReplyIsSilent: false })),
116+).toEqual({
117+status: "error",
118+terminalError: NON_DELIVERABLE_TERMINAL_TURN_REASON,
119+});
120+});
121+122+it("uses safe last-assistant fallback text for terminal delivery status", () => {
123+expect(
124+resolveTerminalAssistantTexts({
125+assistantTexts: [],
126+lastAssistantStopReason: "stop",
127+lastAssistantVisibleText: "Fallback answer.",
128+}),
129+).toEqual(["Fallback answer."]);
130+expect(
131+resolveTerminalAssistantTexts({
132+assistantTexts: [],
133+lastAssistantStopReason: "error",
134+lastAssistantVisibleText: "Raw provider error",
135+}),
136+).toEqual([]);
137+});
138+139+it("marks terminal tool-use attempts as non-deliverable without explicit delivery", () => {
140+expect(
141+resolveAttemptTrajectoryTerminal(
142+baseParams({
143+assistantTexts: ["I will update that file."],
144+toolMetas: [{ toolName: "write" }],
145+lastAssistantStopReason: "toolUse",
146+}),
147+),
148+).toEqual({
149+status: "error",
150+terminalError: NON_DELIVERABLE_TERMINAL_TURN_REASON,
151+});
152+expect(
153+resolveAttemptTrajectoryTerminal(
154+baseParams({
155+assistantTexts: ["I sent the reply."],
156+didSendViaMessagingTool: true,
157+messagingToolSentTexts: ["sent"],
158+lastAssistantStopReason: "toolUse",
159+}),
160+),
161+).toEqual({ status: "success" });
162+});
163+164+it("preserves prompt errors and interrupts", () => {
165+expect(
166+resolveAttemptTrajectoryTerminal(baseParams({ promptError: new Error("boom") })),
167+).toEqual({ status: "error" });
168+expect(resolveAttemptTrajectoryTerminal(baseParams({ timedOut: true }))).toEqual({
169+status: "interrupted",
170+});
171+});
172+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。