openclaw/ui/src/ui/app-tool-stream.node.test.ts

143 lines
4.0 KiB
TypeScript

import { beforeAll, describe, expect, it, vi } from "vitest";
import { handleAgentEvent, type FallbackStatus, type ToolStreamEntry } from "./app-tool-stream.ts";
type ToolStreamHost = Parameters<typeof handleAgentEvent>[0];
type MutableHost = ToolStreamHost & {
compactionStatus?: unknown;
compactionClearTimer?: number | null;
fallbackStatus?: FallbackStatus | null;
fallbackClearTimer?: number | null;
};
function createHost(overrides?: Partial<MutableHost>): MutableHost {
return {
sessionKey: "main",
chatRunId: null,
chatStream: null,
chatStreamStartedAt: null,
chatStreamSegments: [],
toolStreamById: new Map<string, ToolStreamEntry>(),
toolStreamOrder: [],
chatToolMessages: [],
toolStreamSyncTimer: null,
compactionStatus: null,
compactionClearTimer: null,
fallbackStatus: null,
fallbackClearTimer: null,
...overrides,
};
}
describe("app-tool-stream fallback lifecycle handling", () => {
beforeAll(() => {
const globalWithWindow = globalThis as typeof globalThis & {
window?: Window & typeof globalThis;
};
if (!globalWithWindow.window) {
globalWithWindow.window = globalThis as unknown as Window & typeof globalThis;
}
});
it("accepts session-scoped fallback lifecycle events when no run is active", () => {
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",
reasonSummary: "rate limit",
},
});
expect(host.fallbackStatus?.selected).toBe("fireworks/minimax-m2p5");
expect(host.fallbackStatus?.active).toBe("deepinfra/moonshotai/Kimi-K2.5");
expect(host.fallbackStatus?.reason).toBe("rate limit");
vi.useRealTimers();
});
it("rejects idle fallback lifecycle events for other sessions", () => {
vi.useFakeTimers();
const host = createHost();
handleAgentEvent(host, {
runId: "run-1",
seq: 1,
stream: "lifecycle",
ts: Date.now(),
sessionKey: "agent:other:main",
data: {
phase: "fallback",
selectedProvider: "fireworks",
selectedModel: "fireworks/minimax-m2p5",
activeProvider: "deepinfra",
activeModel: "moonshotai/Kimi-K2.5",
},
});
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();
});
});