Files
paseo/packages/app/src/hooks/use-agent-initialization.test.ts
T
Mohamed BoudraandGitHub b613bea9f6 Default client RPC waits to 60 seconds (#1789)
* fix(client): wait longer for session responses

* fix(client): default RPC waits to sixty seconds

* fix(app): preserve detached stream scroll on delayed history

Code drift: longer client RPC waits let delayed timeline responses arrive after a user scroll-away, so web stream anchoring must not reattach on transient scroll-top resets.

Restore the 15s connect deadline and leave app initialization slack above the default 60s session RPC wait.

* fix(client): keep helper waits within caller deadlines

Review fix: the 60s default session RPC wait leaked into wait previews and waitForAgentUpsert helper fetches. Bound those helper RPCs to the caller deadline or a short best-effort preview timeout, and allow small scroll ranges to reattach at bottom.

* fix(client): respect caller timeout budgets

* fix(cli): keep diagnostic probes responsive

* Refactor daemon client request options

* Preserve daemon client legacy overloads
2026-06-29 17:23:18 +02:00

161 lines
5.1 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from "vitest";
import { useSessionStore } from "@/stores/session-store";
import { TIMELINE_FETCH_PAGE_SIZE } from "@/timeline/timeline-fetch-policy";
import { getInitDeferred, getInitKey, resolveInitDeferred } from "@/utils/agent-initialization";
import {
createSetAgentInitializing,
ensureAgentIsInitialized,
refreshAgentInitializationTimeout,
refreshAgent,
} from "./use-agent-initialization";
const serverId = "server-1";
const agentId = "agent-1";
interface FakeDaemonClient {
fetchAgentTimeline: ReturnType<typeof vi.fn>;
refreshAgent: ReturnType<typeof vi.fn>;
}
function makeClient(): FakeDaemonClient {
return {
fetchAgentTimeline: vi.fn().mockResolvedValue(undefined),
refreshAgent: vi.fn().mockResolvedValue(undefined),
};
}
function bindSetAgentInitializing() {
return createSetAgentInitializing(serverId, useSessionStore.getState().setInitializingAgents);
}
afterEach(() => {
resolveInitDeferred(getInitKey(serverId, agentId));
useSessionStore.setState({ sessions: {}, agentLastActivity: new Map() });
vi.restoreAllMocks();
});
describe("ensureAgentIsInitialized", () => {
it("requests bounded projected catch-up after the current cursor when authoritative history is loaded", () => {
const client = makeClient();
useSessionStore.getState().initializeSession(serverId, client as never);
useSessionStore
.getState()
.setAgentTimelineCursor(
serverId,
new Map([[agentId, { epoch: "epoch-1", startSeq: 1, endSeq: 42 }]]),
);
useSessionStore.getState().setAgentAuthoritativeHistoryApplied(serverId, agentId, true);
void ensureAgentIsInitialized({
serverId,
agentId,
client: client as never,
setAgentInitializing: bindSetAgentInitializing(),
});
expect(client.fetchAgentTimeline).toHaveBeenCalledWith(agentId, {
direction: "after",
cursor: { epoch: "epoch-1", seq: 42 },
limit: TIMELINE_FETCH_PAGE_SIZE,
projection: "projected",
});
expect(getInitDeferred(getInitKey(serverId, agentId))?.requestDirection).toBe("after");
});
it("requests a bounded projected tail when no authoritative cursor is available", () => {
const client = makeClient();
useSessionStore.getState().initializeSession(serverId, client as never);
void ensureAgentIsInitialized({
serverId,
agentId,
client: client as never,
setAgentInitializing: bindSetAgentInitializing(),
});
expect(client.fetchAgentTimeline).toHaveBeenCalledWith(agentId, {
direction: "tail",
limit: TIMELINE_FETCH_PAGE_SIZE,
projection: "projected",
});
expect(getInitDeferred(getInitKey(serverId, agentId))?.requestDirection).toBe("tail");
});
it("times out initialization after 65 seconds", async () => {
vi.useFakeTimers();
const client = makeClient();
useSessionStore.getState().initializeSession(serverId, client as never);
const promise = ensureAgentIsInitialized({
serverId,
agentId,
client: client as never,
setAgentInitializing: bindSetAgentInitializing(),
});
vi.advanceTimersByTime(64_999);
expect(getInitDeferred(getInitKey(serverId, agentId))).toBeDefined();
vi.advanceTimersByTime(1);
await expect(promise).rejects.toThrow("History sync timed out after 65s");
expect(getInitDeferred(getInitKey(serverId, agentId))).toBeUndefined();
expect(useSessionStore.getState().sessions[serverId]?.initializingAgents.get(agentId)).toBe(
false,
);
vi.useRealTimers();
});
it("refreshes the initialization timeout after paged catch-up progress", async () => {
vi.useFakeTimers();
const client = makeClient();
useSessionStore.getState().initializeSession(serverId, client as never);
const setAgentInitializing = bindSetAgentInitializing();
const key = getInitKey(serverId, agentId);
const promise = ensureAgentIsInitialized({
serverId,
agentId,
client: client as never,
setAgentInitializing,
});
vi.advanceTimersByTime(64_999);
refreshAgentInitializationTimeout({ key, agentId, setAgentInitializing });
vi.advanceTimersByTime(1);
expect(getInitDeferred(key)).toBeDefined();
const rejection = expect(promise).rejects.toThrow("History sync timed out after 65s");
vi.advanceTimersByTime(64_998);
expect(getInitDeferred(key)).toBeDefined();
vi.advanceTimersByTime(1);
await rejection;
expect(getInitDeferred(key)).toBeUndefined();
vi.useRealTimers();
});
});
describe("refreshAgent", () => {
it("fetches a bounded projected tail after refreshing the agent", async () => {
const client = makeClient();
useSessionStore.getState().initializeSession(serverId, client as never);
await refreshAgent({
agentId,
client: client as never,
setAgentInitializing: bindSetAgentInitializing(),
});
expect(client.refreshAgent).toHaveBeenCalledWith(agentId);
expect(client.fetchAgentTimeline).toHaveBeenCalledWith(agentId, {
direction: "tail",
limit: TIMELINE_FETCH_PAGE_SIZE,
projection: "projected",
});
});
});