fix(auto-reply): harden fallback lifecycle formatting

This commit is contained in:
Gustavo Madeira Santana
2026-02-19 03:27:36 -05:00
committed by joshavant
parent 9baae38704
commit 241d39daea
5 changed files with 95 additions and 3 deletions

View File

@@ -66,6 +66,19 @@ describe("fallback-state", () => {
expect(resolved.nextState.activeModel).toBe("deepinfra/moonshotai/Kimi-K2.5");
});
it("normalizes fallback reason whitespace for summaries", () => {
const resolved = resolveFallbackTransition({
selectedProvider: "fireworks",
selectedModel: "fireworks/minimax-m2p5",
activeProvider: "deepinfra",
activeModel: "moonshotai/Kimi-K2.5",
attempts: [{ ...baseAttempt, reason: "rate_limit\n\tburst" }],
state: {},
});
expect(resolved.reasonSummary).toBe("rate limit burst");
});
it("refreshes reason when fallback remains active with same model pair", () => {
const resolved = resolveFallbackTransition({
selectedProvider: "fireworks",

View File

@@ -15,7 +15,9 @@ export function normalizeFallbackModelRef(value?: string): string | undefined {
}
function truncateFallbackReasonPart(value: string, max = FALLBACK_REASON_PART_MAX): string {
const text = String(value ?? "").trim();
const text = String(value ?? "")
.replace(/\s+/g, " ")
.trim();
if (text.length <= max) {
return text;
}

View File

@@ -509,6 +509,30 @@ describe("runReplyAgent typing (heartbeat)", () => {
expect(onToolResult).not.toHaveBeenCalled();
});
it("retries transient HTTP failures once with timer-driven backoff", async () => {
vi.useFakeTimers();
let calls = 0;
state.runEmbeddedPiAgentMock.mockImplementation(async () => {
calls += 1;
if (calls === 1) {
throw new Error("502 Bad Gateway");
}
return { payloads: [{ text: "final" }], meta: {} };
});
const { run } = createMinimalRun({
typingMode: "message",
});
const runPromise = run();
await vi.advanceTimersByTimeAsync(2_499);
expect(calls).toBe(1);
await vi.advanceTimersByTimeAsync(1);
await runPromise;
expect(calls).toBe(2);
vi.useRealTimers();
});
it("announces auto-compaction in verbose mode and tracks count", async () => {
await withTempStateDir(async (stateDir) => {
const storePath = path.join(stateDir, "sessions", "sessions.json");

View File

@@ -83,4 +83,57 @@ describe("app-tool-stream fallback lifecycle handling", () => {
expect(host.fallbackStatus).toBeNull();
vi.useRealTimers();
});
it("auto-clears fallback status after toast duration", () => {
vi.useFakeTimers();
const host = createHost();
handleAgentEvent(host, {
runId: "run-1",
seq: 1,
stream: "lifecycle",
ts: Date.now(),
sessionKey: "main",
data: {
phase: "fallback",
selectedProvider: "fireworks",
selectedModel: "fireworks/minimax-m2p5",
activeProvider: "deepinfra",
activeModel: "moonshotai/Kimi-K2.5",
},
});
expect(host.fallbackStatus).not.toBeNull();
vi.advanceTimersByTime(7_999);
expect(host.fallbackStatus).not.toBeNull();
vi.advanceTimersByTime(1);
expect(host.fallbackStatus).toBeNull();
vi.useRealTimers();
});
it("builds previous fallback label from provider + model on fallback_cleared", () => {
vi.useFakeTimers();
const host = createHost();
handleAgentEvent(host, {
runId: "run-1",
seq: 1,
stream: "lifecycle",
ts: Date.now(),
sessionKey: "main",
data: {
phase: "fallback_cleared",
selectedProvider: "fireworks",
selectedModel: "fireworks/minimax-m2p5",
activeProvider: "fireworks",
activeModel: "fireworks/minimax-m2p5",
previousActiveProvider: "deepinfra",
previousActiveModel: "moonshotai/Kimi-K2.5",
},
});
expect(host.fallbackStatus?.phase).toBe("cleared");
expect(host.fallbackStatus?.previous).toBe("deepinfra/moonshotai/Kimi-K2.5");
vi.useRealTimers();
});
});

View File

@@ -339,8 +339,8 @@ function handleLifecycleFallbackEvent(host: CompactionHost, payload: AgentEventP
resolveModelLabel(data.activeProvider, data.activeModel) ??
resolveModelLabel(data.toProvider, data.toModel);
const previous =
toTrimmedString(data.previousActiveModel) ??
resolveModelLabel(data.previousActiveProvider, data.previousActiveModel);
resolveModelLabel(data.previousActiveProvider, data.previousActiveModel) ??
toTrimmedString(data.previousActiveModel);
if (!selected || !active) {
return;
}