* 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
161 lines
5.1 KiB
TypeScript
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",
|
|
});
|
|
});
|
|
});
|