






















@@ -0,0 +1,126 @@
1+import { describe, expect, it, vi } from "vitest";
2+3+const LOGIN_HINT_SENTINEL = "<<login-hint-for-provider>>";
4+5+vi.mock("../provider-auth-recovery-hint.js", () => ({
6+buildProviderAuthRecoveryHint: (params: { provider: string }) =>
7+`${LOGIN_HINT_SENTINEL}:${params.provider}`,
8+}));
9+10+import type { FailoverReason } from "../embedded-agent-helpers/types.js";
11+import { formatAuthProfileFailureMessage } from "./failure-copy.js";
12+13+const PROVIDER = "openai-codex";
14+15+const REASONS_WITH_RECOVERY: readonly FailoverReason[] = [
16+"auth",
17+"session_expired",
18+"auth_permanent",
19+"billing",
20+];
21+const REASONS_TRANSIENT: readonly FailoverReason[] = [
22+"rate_limit",
23+"overloaded",
24+"timeout",
25+"server_error",
26+"model_not_found",
27+];
28+29+describe("formatAuthProfileFailureMessage", () => {
30+describe("recovery-hint dispatch", () => {
31+it("includes the login command for reasons the user can act on", () => {
32+for (const reason of REASONS_WITH_RECOVERY) {
33+const message = formatAuthProfileFailureMessage({
34+ reason,
35+provider: PROVIDER,
36+allInCooldown: true,
37+});
38+expect(message, `reason=${reason}`).toContain(`${LOGIN_HINT_SENTINEL}:${PROVIDER}`);
39+}
40+});
41+42+it("omits the login command for transient cooldown reasons", () => {
43+for (const reason of REASONS_TRANSIENT) {
44+const message = formatAuthProfileFailureMessage({
45+ reason,
46+provider: PROVIDER,
47+allInCooldown: true,
48+});
49+expect(message, `reason=${reason}`).not.toContain(LOGIN_HINT_SENTINEL);
50+}
51+});
52+});
53+54+describe("reason coverage", () => {
55+it("renders distinct copy across the major reason classes", () => {
56+const samples = (["auth", "billing", "rate_limit", "timeout"] as const).map((reason) =>
57+formatAuthProfileFailureMessage({ reason, provider: PROVIDER, allInCooldown: true }),
58+);
59+expect(new Set(samples).size).toBe(samples.length);
60+});
61+62+it("always mentions the provider name", () => {
63+for (const reason of [...REASONS_WITH_RECOVERY, ...REASONS_TRANSIENT, "unknown"] as const) {
64+const message = formatAuthProfileFailureMessage({
65+ reason,
66+provider: PROVIDER,
67+allInCooldown: true,
68+});
69+expect(message, `reason=${reason}`).toContain(PROVIDER);
70+}
71+});
72+});
73+74+describe("cause handling", () => {
75+it("returns the cause text verbatim when the reason has no actionable copy", () => {
76+const cause = new Error("upstream provider returned 502");
77+const message = formatAuthProfileFailureMessage({
78+reason: "unknown",
79+provider: PROVIDER,
80+allInCooldown: false,
81+ cause,
82+});
83+expect(message).toBe(cause.message);
84+});
85+86+it("appends a diagnostic suffix when the cause adds detail beyond the description", () => {
87+const message = formatAuthProfileFailureMessage({
88+reason: "auth",
89+provider: PROVIDER,
90+allInCooldown: false,
91+cause: new Error("invalid_grant"),
92+});
93+expect(message).toContain("(invalid_grant)");
94+});
95+96+it("does not append a diagnostic suffix when the cause text is already in the description", () => {
97+// Derive the description sentence by formatting once without a cause, then stripping
98+// the mocked recovery hint. Using that sentence as the cause should be deduped.
99+const withoutCause = formatAuthProfileFailureMessage({
100+reason: "auth",
101+provider: PROVIDER,
102+allInCooldown: false,
103+});
104+const description = withoutCause
105+.replace(new RegExp(`\\s*${LOGIN_HINT_SENTINEL}:[^\\s]+\\s*$`), "")
106+.trim();
107+const withDuplicateCause = formatAuthProfileFailureMessage({
108+reason: "auth",
109+provider: PROVIDER,
110+allInCooldown: false,
111+cause: new Error(description),
112+});
113+expect(withDuplicateCause).toBe(withoutCause);
114+});
115+116+it("produces non-empty copy for unknown reasons with no cause", () => {
117+const message = formatAuthProfileFailureMessage({
118+reason: "unknown",
119+provider: PROVIDER,
120+allInCooldown: false,
121+});
122+expect(message).toContain(PROVIDER);
123+expect(message.length).toBeGreaterThan(0);
124+});
125+});
126+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。