test(reply): merge duplicate runReplyAgent streaming and fallback cases

This commit is contained in:
Peter Steinberger
2026-02-22 08:59:46 +00:00
parent 15657dd48d
commit ee3abb2278

View File

@@ -337,66 +337,62 @@ describe("runReplyAgent typing (heartbeat)", () => {
expect(typing.startTypingLoop).not.toHaveBeenCalled(); expect(typing.startTypingLoop).not.toHaveBeenCalled();
}); });
it("suppresses partial streaming for NO_REPLY", async () => { it("suppresses NO_REPLY partials but allows normal No-prefix partials", async () => {
const onPartialReply = vi.fn(); const cases = [
state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: AgentRunParams) => { {
await params.onPartialReply?.({ text: "NO_REPLY" }); partials: ["NO_REPLY"],
return { payloads: [{ text: "NO_REPLY" }], meta: {} }; finalText: "NO_REPLY",
}); expectedForwarded: [] as string[],
shouldType: false,
},
{
partials: ["NO_", "NO_RE", "NO_REPLY"],
finalText: "NO_REPLY",
expectedForwarded: [] as string[],
shouldType: false,
},
{
partials: ["No", "No, that is valid"],
finalText: "No, that is valid",
expectedForwarded: ["No", "No, that is valid"],
shouldType: true,
},
] as const;
const { run, typing } = createMinimalRun({ for (const testCase of cases) {
opts: { isHeartbeat: false, onPartialReply }, const onPartialReply = vi.fn();
typingMode: "message", state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: AgentRunParams) => {
}); for (const text of testCase.partials) {
await run(); await params.onPartialReply?.({ text });
}
return { payloads: [{ text: testCase.finalText }], meta: {} };
});
expect(onPartialReply).not.toHaveBeenCalled(); const { run, typing } = createMinimalRun({
expect(typing.startTypingOnText).not.toHaveBeenCalled(); opts: { isHeartbeat: false, onPartialReply },
expect(typing.startTypingLoop).not.toHaveBeenCalled(); typingMode: "message",
}); });
await run();
it("suppresses partial streaming for NO_REPLY prefixes", async () => { if (testCase.expectedForwarded.length === 0) {
const onPartialReply = vi.fn(); expect(onPartialReply).not.toHaveBeenCalled();
state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: AgentRunParams) => { } else {
await params.onPartialReply?.({ text: "NO_" }); expect(onPartialReply).toHaveBeenCalledTimes(testCase.expectedForwarded.length);
await params.onPartialReply?.({ text: "NO_RE" }); testCase.expectedForwarded.forEach((text, index) => {
await params.onPartialReply?.({ text: "NO_REPLY" }); expect(onPartialReply).toHaveBeenNthCalledWith(index + 1, {
return { payloads: [{ text: "NO_REPLY" }], meta: {} }; text,
}); mediaUrls: undefined,
});
});
}
const { run, typing } = createMinimalRun({ if (testCase.shouldType) {
opts: { isHeartbeat: false, onPartialReply }, expect(typing.startTypingOnText).toHaveBeenCalled();
typingMode: "message", } else {
}); expect(typing.startTypingOnText).not.toHaveBeenCalled();
await run(); }
expect(typing.startTypingLoop).not.toHaveBeenCalled();
expect(onPartialReply).not.toHaveBeenCalled(); }
expect(typing.startTypingOnText).not.toHaveBeenCalled();
expect(typing.startTypingLoop).not.toHaveBeenCalled();
});
it("does not suppress partial streaming for normal 'No' prefixes", async () => {
const onPartialReply = vi.fn();
state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: AgentRunParams) => {
await params.onPartialReply?.({ text: "No" });
await params.onPartialReply?.({ text: "No, that is valid" });
return { payloads: [{ text: "No, that is valid" }], meta: {} };
});
const { run, typing } = createMinimalRun({
opts: { isHeartbeat: false, onPartialReply },
typingMode: "message",
});
await run();
expect(onPartialReply).toHaveBeenCalledTimes(2);
expect(onPartialReply).toHaveBeenNthCalledWith(1, { text: "No", mediaUrls: undefined });
expect(onPartialReply).toHaveBeenNthCalledWith(2, {
text: "No, that is valid",
mediaUrls: undefined,
});
expect(typing.startTypingOnText).toHaveBeenCalled();
expect(typing.startTypingLoop).not.toHaveBeenCalled();
}); });
it("does not start typing on assistant message start without prior text in message mode", async () => { it("does not start typing on assistant message start without prior text in message mode", async () => {
@@ -488,41 +484,48 @@ describe("runReplyAgent typing (heartbeat)", () => {
}); });
}); });
it("signals typing on tool results", async () => { it("handles typing for normal and silent tool results", async () => {
const onToolResult = vi.fn(); const cases = [
state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: AgentRunParams) => { {
await params.onToolResult?.({ text: "tooling", mediaUrls: [] }); toolText: "tooling",
return { payloads: [{ text: "final" }], meta: {} }; shouldType: true,
}); shouldForward: true,
},
{
toolText: "NO_REPLY",
shouldType: false,
shouldForward: false,
},
] as const;
const { run, typing } = createMinimalRun({ for (const testCase of cases) {
typingMode: "message", const onToolResult = vi.fn();
opts: { onToolResult }, state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: AgentRunParams) => {
}); await params.onToolResult?.({ text: testCase.toolText, mediaUrls: [] });
await run(); return { payloads: [{ text: "final" }], meta: {} };
});
expect(typing.startTypingOnText).toHaveBeenCalledWith("tooling"); const { run, typing } = createMinimalRun({
expect(onToolResult).toHaveBeenCalledWith({ typingMode: "message",
text: "tooling", opts: { onToolResult },
mediaUrls: [], });
}); await run();
});
it("skips typing for silent tool results", async () => { if (testCase.shouldType) {
const onToolResult = vi.fn(); expect(typing.startTypingOnText).toHaveBeenCalledWith(testCase.toolText);
state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: AgentRunParams) => { } else {
await params.onToolResult?.({ text: "NO_REPLY", mediaUrls: [] }); expect(typing.startTypingOnText).not.toHaveBeenCalled();
return { payloads: [{ text: "final" }], meta: {} }; }
});
const { run, typing } = createMinimalRun({ if (testCase.shouldForward) {
typingMode: "message", expect(onToolResult).toHaveBeenCalledWith({
opts: { onToolResult }, text: testCase.toolText,
}); mediaUrls: [],
await run(); });
} else {
expect(typing.startTypingOnText).not.toHaveBeenCalled(); expect(onToolResult).not.toHaveBeenCalled();
expect(onToolResult).not.toHaveBeenCalled(); }
}
}); });
it("retries transient HTTP failures once with timer-driven backoff", async () => { it("retries transient HTTP failures once with timer-driven backoff", async () => {
@@ -979,100 +982,67 @@ describe("runReplyAgent typing (heartbeat)", () => {
} }
}); });
it("backfills fallback reason when fallback is already active", async () => { it("updates fallback reason summary while fallback stays active", async () => {
const sessionEntry: SessionEntry = { const cases = [
sessionId: "session", {
updatedAt: Date.now(), existingReason: undefined,
fallbackNoticeSelectedModel: "anthropic/claude", reportedReason: "rate_limit",
fallbackNoticeActiveModel: "deepinfra/moonshotai/Kimi-K2.5", expectedReason: "rate limit",
modelProvider: "deepinfra", },
model: "moonshotai/Kimi-K2.5", {
}; existingReason: "rate limit",
const sessionStore = { main: sessionEntry }; reportedReason: "timeout",
expectedReason: "timeout",
},
] as const;
state.runEmbeddedPiAgentMock.mockResolvedValue({ for (const testCase of cases) {
payloads: [{ text: "final" }], const sessionEntry: SessionEntry = {
meta: {}, sessionId: "session",
}); updatedAt: Date.now(),
const fallbackSpy = vi fallbackNoticeSelectedModel: "anthropic/claude",
.spyOn(modelFallbackModule, "runWithModelFallback") fallbackNoticeActiveModel: "deepinfra/moonshotai/Kimi-K2.5",
.mockImplementation( ...(testCase.existingReason ? { fallbackNoticeReason: testCase.existingReason } : {}),
async ({ run }: { run: (provider: string, model: string) => Promise<unknown> }) => ({ modelProvider: "deepinfra",
result: await run("deepinfra", "moonshotai/Kimi-K2.5"), model: "moonshotai/Kimi-K2.5",
provider: "deepinfra", };
model: "moonshotai/Kimi-K2.5", const sessionStore = { main: sessionEntry };
attempts: [
{ state.runEmbeddedPiAgentMock.mockResolvedValue({
provider: "anthropic", payloads: [{ text: "final" }],
model: "claude", meta: {},
error: "Provider anthropic is in cooldown (all profiles unavailable)",
reason: "rate_limit",
},
],
}),
);
try {
const { run } = createMinimalRun({
resolvedVerboseLevel: "on",
sessionEntry,
sessionStore,
sessionKey: "main",
}); });
const res = await run(); const fallbackSpy = vi
const firstText = Array.isArray(res) ? res[0]?.text : res?.text; .spyOn(modelFallbackModule, "runWithModelFallback")
expect(firstText).not.toContain("Model Fallback:"); .mockImplementation(
expect(sessionEntry.fallbackNoticeReason).toBe("rate limit"); async ({ run }: { run: (provider: string, model: string) => Promise<unknown> }) => ({
} finally { result: await run("deepinfra", "moonshotai/Kimi-K2.5"),
fallbackSpy.mockRestore(); provider: "deepinfra",
} model: "moonshotai/Kimi-K2.5",
}); attempts: [
{
it("refreshes fallback reason summary while fallback stays active", async () => { provider: "anthropic",
const sessionEntry: SessionEntry = { model: "claude",
sessionId: "session", error: "Provider anthropic is in cooldown (all profiles unavailable)",
updatedAt: Date.now(), reason: testCase.reportedReason,
fallbackNoticeSelectedModel: "anthropic/claude", },
fallbackNoticeActiveModel: "deepinfra/moonshotai/Kimi-K2.5", ],
fallbackNoticeReason: "rate limit", }),
modelProvider: "deepinfra", );
model: "moonshotai/Kimi-K2.5", try {
}; const { run } = createMinimalRun({
const sessionStore = { main: sessionEntry }; resolvedVerboseLevel: "on",
sessionEntry,
state.runEmbeddedPiAgentMock.mockResolvedValue({ sessionStore,
payloads: [{ text: "final" }], sessionKey: "main",
meta: {}, });
}); const res = await run();
const fallbackSpy = vi const firstText = Array.isArray(res) ? res[0]?.text : res?.text;
.spyOn(modelFallbackModule, "runWithModelFallback") expect(firstText).not.toContain("Model Fallback:");
.mockImplementation( expect(sessionEntry.fallbackNoticeReason).toBe(testCase.expectedReason);
async ({ run }: { run: (provider: string, model: string) => Promise<unknown> }) => ({ } finally {
result: await run("deepinfra", "moonshotai/Kimi-K2.5"), fallbackSpy.mockRestore();
provider: "deepinfra", }
model: "moonshotai/Kimi-K2.5",
attempts: [
{
provider: "anthropic",
model: "claude",
error: "Provider anthropic is in cooldown (all profiles unavailable)",
reason: "timeout",
},
],
}),
);
try {
const { run } = createMinimalRun({
resolvedVerboseLevel: "on",
sessionEntry,
sessionStore,
sessionKey: "main",
});
const res = await run();
const firstText = Array.isArray(res) ? res[0]?.text : res?.text;
expect(firstText).not.toContain("Model Fallback:");
expect(sessionEntry.fallbackNoticeReason).toBe("timeout");
} finally {
fallbackSpy.mockRestore();
} }
}); });