源码快照 @ 2026-09-02 (source-only, 剔除非源码素材)
This commit is contained in:
@@ -0,0 +1,640 @@
|
||||
import { afterEach, describe, expect, jest, setSystemTime, test } from "bun:test";
|
||||
import type { SessionStatus } from "@opencode-ai/sdk/v2/client";
|
||||
|
||||
import type { OpenworkSessionSnapshot } from "../src/app/lib/openwork-server";
|
||||
import { useSessionActivityStore } from "../src/react-app/domains/session/status/session-activity-store";
|
||||
import {
|
||||
__applySessionSyncEventForTest,
|
||||
__createWorkspaceSessionSyncForTest,
|
||||
__disposeWorkspaceSessionSyncForTest,
|
||||
__setWorkspaceSessionSyncStatusFetcherForTest,
|
||||
__setWorkspaceSessionSyncSubscriptionFactoryForTest,
|
||||
ensureWorkspaceSessionSync,
|
||||
markSessionSnapshotFetchStart,
|
||||
seedSessionState,
|
||||
snapshotKey,
|
||||
statusKey,
|
||||
trackWorkspaceSessionSync,
|
||||
transcriptKey,
|
||||
} from "../src/react-app/domains/session/sync/session-sync";
|
||||
import { getReactQueryClient } from "../src/react-app/infra/query-client";
|
||||
|
||||
type SyncInput = {
|
||||
workspaceId: string;
|
||||
baseUrl: string;
|
||||
openworkToken: string;
|
||||
};
|
||||
|
||||
type Subscription = {
|
||||
signal: AbortSignal;
|
||||
end: () => void;
|
||||
};
|
||||
|
||||
const workspaceId = "workspace-run-status";
|
||||
const sessionId = "session-run-status";
|
||||
const syncInputs: SyncInput[] = [];
|
||||
const subscriptions: Subscription[] = [];
|
||||
|
||||
function createSnapshot(status: SessionStatus): OpenworkSessionSnapshot {
|
||||
return {
|
||||
session: {
|
||||
id: sessionId,
|
||||
slug: sessionId,
|
||||
projectID: "project-run-status",
|
||||
directory: "/tmp/project-run-status",
|
||||
title: "Run status test",
|
||||
version: "1",
|
||||
time: { created: 1, updated: 1 },
|
||||
},
|
||||
messages: [],
|
||||
todos: [],
|
||||
status,
|
||||
};
|
||||
}
|
||||
|
||||
function createSyncInput(): SyncInput {
|
||||
const input = {
|
||||
workspaceId,
|
||||
baseUrl: "https://run-status.example/opencode",
|
||||
openworkToken: "token",
|
||||
};
|
||||
syncInputs.push(input);
|
||||
return input;
|
||||
}
|
||||
|
||||
function createTestSync() {
|
||||
const input = createSyncInput();
|
||||
const cleanup = __createWorkspaceSessionSyncForTest(input);
|
||||
const releaseSession = trackWorkspaceSessionSync(input, sessionId);
|
||||
return { input, cleanup, releaseSession };
|
||||
}
|
||||
|
||||
async function createSubscription(_baseUrl: string, _token: string, signal: AbortSignal) {
|
||||
let end = () => {};
|
||||
const ended = new Promise<void>((resolve) => {
|
||||
end = resolve;
|
||||
});
|
||||
signal.addEventListener("abort", end, { once: true });
|
||||
async function* stream() {
|
||||
await ended;
|
||||
}
|
||||
subscriptions.push({ signal, end });
|
||||
return stream();
|
||||
}
|
||||
|
||||
async function waitForSubscriptions(count: number) {
|
||||
const deadline = Date.now() + 2_000;
|
||||
while (subscriptions.length < count && Date.now() < deadline) {
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 5));
|
||||
}
|
||||
expect(subscriptions).toHaveLength(count);
|
||||
}
|
||||
|
||||
async function flushMicrotasks() {
|
||||
for (let index = 0; index < 10; index += 1) await Promise.resolve();
|
||||
}
|
||||
|
||||
function applyStatus(input: SyncInput, status: SessionStatus) {
|
||||
__applySessionSyncEventForTest(input, {
|
||||
type: "session.status",
|
||||
properties: { sessionID: sessionId, status },
|
||||
});
|
||||
}
|
||||
|
||||
function applyCompletedToolAndFinalAnswer(input: SyncInput) {
|
||||
__applySessionSyncEventForTest(input, {
|
||||
type: "message.updated",
|
||||
properties: {
|
||||
info: {
|
||||
id: "assistant-tool",
|
||||
role: "assistant",
|
||||
sessionID: sessionId,
|
||||
time: { created: 1, completed: 2 },
|
||||
},
|
||||
},
|
||||
} as any);
|
||||
__applySessionSyncEventForTest(input, {
|
||||
type: "message.part.updated",
|
||||
properties: {
|
||||
part: {
|
||||
id: "part-tool",
|
||||
sessionID: sessionId,
|
||||
messageID: "assistant-tool",
|
||||
type: "tool",
|
||||
callID: "call-tool",
|
||||
tool: "lookup",
|
||||
state: {
|
||||
status: "completed",
|
||||
input: { query: "fixture" },
|
||||
output: "fixture result",
|
||||
title: "Lookup",
|
||||
metadata: {},
|
||||
time: { start: 1, end: 2 },
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any);
|
||||
__applySessionSyncEventForTest(input, {
|
||||
type: "message.updated",
|
||||
properties: {
|
||||
info: {
|
||||
id: "assistant-final",
|
||||
role: "assistant",
|
||||
sessionID: sessionId,
|
||||
time: { created: 3, completed: 4 },
|
||||
},
|
||||
},
|
||||
} as any);
|
||||
__applySessionSyncEventForTest(input, {
|
||||
type: "message.part.updated",
|
||||
properties: {
|
||||
part: {
|
||||
id: "part-final",
|
||||
sessionID: sessionId,
|
||||
messageID: "assistant-final",
|
||||
type: "text",
|
||||
text: "The final answer is visible.",
|
||||
},
|
||||
},
|
||||
} as any);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
for (const input of syncInputs) __disposeWorkspaceSessionSyncForTest(input);
|
||||
syncInputs.length = 0;
|
||||
subscriptions.length = 0;
|
||||
__setWorkspaceSessionSyncSubscriptionFactoryForTest(null);
|
||||
__setWorkspaceSessionSyncStatusFetcherForTest(null);
|
||||
useSessionActivityStore.setState({ recordsByWorkspaceId: {}, statusesByWorkspaceId: {} });
|
||||
getReactQueryClient().clear();
|
||||
setSystemTime();
|
||||
});
|
||||
|
||||
describe("session run status ordering", () => {
|
||||
test("preserves the activity snapshot when workspace seeds are unchanged", () => {
|
||||
const store = useSessionActivityStore.getState();
|
||||
store.seedWorkspaceSessions(workspaceId, [{ id: sessionId, status: { type: "idle" } }]);
|
||||
const before = useSessionActivityStore.getState();
|
||||
let notifications = 0;
|
||||
const unsubscribe = useSessionActivityStore.subscribe(() => {
|
||||
notifications += 1;
|
||||
});
|
||||
|
||||
for (let index = 0; index < 60; index += 1) {
|
||||
useSessionActivityStore.getState().seedWorkspaceSessions(
|
||||
workspaceId,
|
||||
[{ id: sessionId, status: { type: "idle" } }],
|
||||
);
|
||||
}
|
||||
|
||||
expect(useSessionActivityStore.getState()).toBe(before);
|
||||
expect(notifications).toBe(0);
|
||||
|
||||
useSessionActivityStore.getState().seedWorkspaceSessions(
|
||||
workspaceId,
|
||||
[{ id: sessionId, status: { type: "busy" } }],
|
||||
);
|
||||
unsubscribe();
|
||||
|
||||
expect(useSessionActivityStore.getState()).not.toBe(before);
|
||||
expect(useSessionActivityStore.getState().getStatus(workspaceId, sessionId)).toBe("thinking");
|
||||
expect(notifications).toBe(1);
|
||||
});
|
||||
|
||||
test("does not publish duplicate activity observations", () => {
|
||||
useSessionActivityStore.getState().setRunStatus(workspaceId, sessionId, { type: "busy" });
|
||||
useSessionActivityStore.getState().markMessageRole(workspaceId, sessionId, "assistant-1", "assistant");
|
||||
useSessionActivityStore.getState().markAssistantOutput(workspaceId, sessionId, "assistant-1");
|
||||
const before = useSessionActivityStore.getState().recordsByWorkspaceId[workspaceId]?.[sessionId];
|
||||
let notifications = 0;
|
||||
const unsubscribe = useSessionActivityStore.subscribe(() => {
|
||||
notifications += 1;
|
||||
});
|
||||
|
||||
useSessionActivityStore.getState().markMessageRole(workspaceId, sessionId, "assistant-1", "assistant");
|
||||
useSessionActivityStore.getState().markAssistantOutput(workspaceId, sessionId, "assistant-1");
|
||||
useSessionActivityStore.getState().replaceWaitingRequests(workspaceId, sessionId, "permission", []);
|
||||
useSessionActivityStore.getState().clearError(workspaceId, sessionId);
|
||||
useSessionActivityStore.getState().setCompacting(workspaceId, sessionId, false);
|
||||
|
||||
unsubscribe();
|
||||
expect(useSessionActivityStore.getState().recordsByWorkspaceId[workspaceId]?.[sessionId]).toBe(before);
|
||||
expect(notifications).toBe(0);
|
||||
});
|
||||
|
||||
test("invalidates the durable snapshot when a tracked run becomes idle", () => {
|
||||
const { input, cleanup, releaseSession } = createTestSync();
|
||||
const queryClient = getReactQueryClient();
|
||||
queryClient.setQueryData(snapshotKey(workspaceId, sessionId), createSnapshot({ type: "busy" }));
|
||||
|
||||
__applySessionSyncEventForTest(input, {
|
||||
type: "session.idle",
|
||||
properties: { sessionID: sessionId },
|
||||
});
|
||||
|
||||
expect(queryClient.getQueryState(snapshotKey(workspaceId, sessionId))?.isInvalidated).toBe(true);
|
||||
|
||||
releaseSession();
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("clears the busy status cache when a run errors without a following idle", () => {
|
||||
const statusUpdates: SessionStatus[] = [];
|
||||
const input = {
|
||||
workspaceId,
|
||||
baseUrl: "https://run-status.example/opencode",
|
||||
openworkToken: "token",
|
||||
onSessionStatus: (update: { sessionId: string; status: SessionStatus }) => {
|
||||
statusUpdates.push(update.status);
|
||||
},
|
||||
};
|
||||
syncInputs.push(input);
|
||||
const cleanup = __createWorkspaceSessionSyncForTest(input);
|
||||
const releaseSession = trackWorkspaceSessionSync(input, sessionId);
|
||||
|
||||
applyStatus(input, { type: "busy" });
|
||||
expect(getReactQueryClient().getQueryData(statusKey(workspaceId, sessionId))).toEqual({ type: "busy" });
|
||||
|
||||
__applySessionSyncEventForTest(input, {
|
||||
type: "session.error",
|
||||
properties: {
|
||||
sessionID: sessionId,
|
||||
error: { name: "UnknownError", data: { message: "provider exploded" } },
|
||||
},
|
||||
});
|
||||
|
||||
// The chat surface derives its thread status from this cache: an errored
|
||||
// run must stop reading busy, or the "Working…" row ticks forever beside
|
||||
// the error card when the engine never sends a follow-up idle event.
|
||||
expect(getReactQueryClient().getQueryData(statusKey(workspaceId, sessionId))).toEqual({ type: "idle" });
|
||||
expect(useSessionActivityStore.getState().recordsByWorkspaceId[workspaceId]?.[sessionId]?.status).toBe("error");
|
||||
// Status listeners (like the queued-send drainer) get the same idle edge
|
||||
// session.idle would have delivered, so queued sends are not wedged.
|
||||
expect(statusUpdates.at(-1)).toEqual({ type: "idle" });
|
||||
|
||||
releaseSession();
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("does not resurrect a finished run from a stale busy snapshot", () => {
|
||||
const { input, cleanup, releaseSession } = createTestSync();
|
||||
const snapshot = createSnapshot({ type: "busy" });
|
||||
markSessionSnapshotFetchStart(snapshot, 100);
|
||||
|
||||
setSystemTime(200);
|
||||
applyStatus(input, { type: "busy" });
|
||||
setSystemTime(300);
|
||||
__applySessionSyncEventForTest(input, {
|
||||
type: "session.idle",
|
||||
properties: { sessionID: sessionId },
|
||||
});
|
||||
seedSessionState(workspaceId, snapshot);
|
||||
|
||||
expect(useSessionActivityStore.getState().recordsByWorkspaceId[workspaceId]?.[sessionId]?.status).toBe("idle");
|
||||
expect(getReactQueryClient().getQueryData(statusKey(workspaceId, sessionId))).toEqual({ type: "idle" });
|
||||
|
||||
releaseSession();
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("does not let an older idle snapshot stop a live run", () => {
|
||||
setSystemTime(200);
|
||||
useSessionActivityStore.getState().setRunStatus(workspaceId, sessionId, { type: "busy" });
|
||||
|
||||
useSessionActivityStore.getState().seedSessionRun(
|
||||
workspaceId,
|
||||
sessionId,
|
||||
{ type: "idle" },
|
||||
false,
|
||||
{ snapshotStartedAt: 100 },
|
||||
);
|
||||
|
||||
expect(useSessionActivityStore.getState().recordsByWorkspaceId[workspaceId]?.[sessionId]?.runActive).toBe(true);
|
||||
});
|
||||
|
||||
test("lets a newer idle snapshot heal a stale active record", () => {
|
||||
setSystemTime(100);
|
||||
useSessionActivityStore.getState().setRunStatus(workspaceId, sessionId, { type: "busy" });
|
||||
|
||||
useSessionActivityStore.getState().seedSessionRun(
|
||||
workspaceId,
|
||||
sessionId,
|
||||
{ type: "idle" },
|
||||
false,
|
||||
{ snapshotStartedAt: 200 },
|
||||
);
|
||||
|
||||
expect(useSessionActivityStore.getState().recordsByWorkspaceId[workspaceId]?.[sessionId]?.status).toBe("idle");
|
||||
});
|
||||
|
||||
test("ignores run state from unmarked snapshot objects", () => {
|
||||
setSystemTime(100);
|
||||
useSessionActivityStore.getState().setRunStatus(workspaceId, sessionId, { type: "idle" });
|
||||
getReactQueryClient().setQueryData(statusKey(workspaceId, sessionId), { type: "idle" });
|
||||
|
||||
seedSessionState(workspaceId, createSnapshot({ type: "busy" }));
|
||||
|
||||
expect(useSessionActivityStore.getState().recordsByWorkspaceId[workspaceId]?.[sessionId]?.status).toBe("idle");
|
||||
expect(getReactQueryClient().getQueryData(statusKey(workspaceId, sessionId))).toEqual({ type: "idle" });
|
||||
});
|
||||
|
||||
test("preserves assistant output when an active seed omits it", () => {
|
||||
setSystemTime(100);
|
||||
useSessionActivityStore.getState().setRunStatus(workspaceId, sessionId, { type: "busy" });
|
||||
useSessionActivityStore.getState().markAssistantOutput(workspaceId, sessionId);
|
||||
|
||||
useSessionActivityStore.getState().seedSessionRun(
|
||||
workspaceId,
|
||||
sessionId,
|
||||
{ type: "busy" },
|
||||
undefined,
|
||||
{ snapshotStartedAt: 200 },
|
||||
);
|
||||
|
||||
const record = useSessionActivityStore.getState().recordsByWorkspaceId[workspaceId]?.[sessionId];
|
||||
expect(record?.assistantOutput).toBe(true);
|
||||
expect(record?.status).toBe("responding");
|
||||
});
|
||||
});
|
||||
|
||||
describe("session run status reconnect reconciliation", () => {
|
||||
test("seeds a missed busy edge into the status cache on connect", async () => {
|
||||
__setWorkspaceSessionSyncSubscriptionFactoryForTest(createSubscription);
|
||||
__setWorkspaceSessionSyncStatusFetcherForTest(async () => ({ [sessionId]: { type: "busy" } }));
|
||||
setSystemTime(100);
|
||||
const input = createSyncInput();
|
||||
ensureWorkspaceSessionSync(input);
|
||||
const releaseSession = trackWorkspaceSessionSync(input, sessionId);
|
||||
await waitForSubscriptions(1);
|
||||
await flushMicrotasks();
|
||||
|
||||
expect(useSessionActivityStore.getState().recordsByWorkspaceId[workspaceId]?.[sessionId]?.status).toBe("thinking");
|
||||
expect(getReactQueryClient().getQueryData(statusKey(workspaceId, sessionId))).toEqual({ type: "busy" });
|
||||
|
||||
releaseSession();
|
||||
});
|
||||
|
||||
test("does not clobber a newer live status with a stale reconnect fetch", async () => {
|
||||
__setWorkspaceSessionSyncSubscriptionFactoryForTest(createSubscription);
|
||||
let resolveStatuses: (statuses: Record<string, SessionStatus>) => void = () => {};
|
||||
__setWorkspaceSessionSyncStatusFetcherForTest(() => new Promise((resolve) => {
|
||||
resolveStatuses = resolve;
|
||||
}));
|
||||
setSystemTime(100);
|
||||
const input = createSyncInput();
|
||||
ensureWorkspaceSessionSync(input);
|
||||
const releaseSession = trackWorkspaceSessionSync(input, sessionId);
|
||||
await waitForSubscriptions(1);
|
||||
await flushMicrotasks();
|
||||
|
||||
setSystemTime(200);
|
||||
applyStatus(input, { type: "busy" });
|
||||
resolveStatuses({});
|
||||
await flushMicrotasks();
|
||||
|
||||
expect(useSessionActivityStore.getState().recordsByWorkspaceId[workspaceId]?.[sessionId]?.runActive).toBe(true);
|
||||
expect(getReactQueryClient().getQueryData(statusKey(workspaceId, sessionId))).toEqual({ type: "busy" });
|
||||
|
||||
releaseSession();
|
||||
});
|
||||
|
||||
test("does not resurrect an idle run from an older reconnect snapshot", async () => {
|
||||
__setWorkspaceSessionSyncSubscriptionFactoryForTest(createSubscription);
|
||||
let resolveStatuses: (statuses: Record<string, SessionStatus>) => void = () => {};
|
||||
__setWorkspaceSessionSyncStatusFetcherForTest(() => new Promise((resolve) => {
|
||||
resolveStatuses = resolve;
|
||||
}));
|
||||
setSystemTime(100);
|
||||
const input = createSyncInput();
|
||||
ensureWorkspaceSessionSync(input);
|
||||
const releaseSession = trackWorkspaceSessionSync(input, sessionId);
|
||||
await waitForSubscriptions(1);
|
||||
await flushMicrotasks();
|
||||
|
||||
setSystemTime(200);
|
||||
applyStatus(input, { type: "busy" });
|
||||
setSystemTime(300);
|
||||
applyStatus(input, { type: "idle" });
|
||||
resolveStatuses({ [sessionId]: { type: "busy" } });
|
||||
await flushMicrotasks();
|
||||
|
||||
expect(useSessionActivityStore.getState().recordsByWorkspaceId[workspaceId]?.[sessionId]?.runActive).toBe(false);
|
||||
expect(getReactQueryClient().getQueryData(statusKey(workspaceId, sessionId))).toEqual({ type: "idle" });
|
||||
|
||||
releaseSession();
|
||||
});
|
||||
|
||||
test("heals an active record when a reconnect reports no active run", async () => {
|
||||
__setWorkspaceSessionSyncSubscriptionFactoryForTest(createSubscription);
|
||||
__setWorkspaceSessionSyncStatusFetcherForTest(async () => ({}));
|
||||
setSystemTime(100);
|
||||
const input = createSyncInput();
|
||||
ensureWorkspaceSessionSync(input);
|
||||
await waitForSubscriptions(1);
|
||||
await flushMicrotasks();
|
||||
|
||||
setSystemTime(200);
|
||||
applyStatus(input, { type: "busy" });
|
||||
jest.useFakeTimers();
|
||||
subscriptions[0]?.end();
|
||||
await flushMicrotasks();
|
||||
jest.advanceTimersByTime(1_000);
|
||||
await flushMicrotasks();
|
||||
|
||||
expect(subscriptions).toHaveLength(2);
|
||||
expect(useSessionActivityStore.getState().recordsByWorkspaceId[workspaceId]?.[sessionId]?.status).toBe("idle");
|
||||
});
|
||||
|
||||
test("keeps the stream and run state intact when reconciliation fails", async () => {
|
||||
__setWorkspaceSessionSyncSubscriptionFactoryForTest(createSubscription);
|
||||
__setWorkspaceSessionSyncStatusFetcherForTest(async () => {
|
||||
throw new Error("status unavailable");
|
||||
});
|
||||
setSystemTime(100);
|
||||
const input = createSyncInput();
|
||||
ensureWorkspaceSessionSync(input);
|
||||
await waitForSubscriptions(1);
|
||||
await flushMicrotasks();
|
||||
|
||||
setSystemTime(200);
|
||||
applyStatus(input, { type: "busy" });
|
||||
jest.useFakeTimers();
|
||||
subscriptions[0]?.end();
|
||||
await flushMicrotasks();
|
||||
jest.advanceTimersByTime(1_000);
|
||||
await flushMicrotasks();
|
||||
|
||||
expect(subscriptions).toHaveLength(2);
|
||||
expect(subscriptions[1]?.signal.aborted).toBe(false);
|
||||
expect(useSessionActivityStore.getState().recordsByWorkspaceId[workspaceId]?.[sessionId]?.runActive).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("active session status reconciliation", () => {
|
||||
test("converges a missed terminal edge to idle without losing the completed tool or final answer", async () => {
|
||||
jest.useFakeTimers();
|
||||
setSystemTime(100);
|
||||
let statusFetches = 0;
|
||||
__setWorkspaceSessionSyncStatusFetcherForTest(async () => {
|
||||
statusFetches += 1;
|
||||
return {};
|
||||
});
|
||||
const { input, cleanup, releaseSession } = createTestSync();
|
||||
const queryClient = getReactQueryClient();
|
||||
queryClient.setQueryData(snapshotKey(workspaceId, sessionId), createSnapshot({ type: "busy" }));
|
||||
|
||||
applyStatus(input, { type: "busy" });
|
||||
applyCompletedToolAndFinalAnswer(input);
|
||||
|
||||
const before = queryClient.getQueryData<any[]>(transcriptKey(workspaceId, sessionId));
|
||||
expect(before?.[0]?.parts[0]).toMatchObject({
|
||||
type: "dynamic-tool",
|
||||
state: "output-available",
|
||||
output: "fixture result",
|
||||
});
|
||||
expect(before?.[1]?.parts[0]).toMatchObject({
|
||||
type: "text",
|
||||
text: "The final answer is visible.",
|
||||
});
|
||||
expect(useSessionActivityStore.getState().getStatus(workspaceId, sessionId)).toBe("responding");
|
||||
|
||||
// No session.status idle or session.idle event arrives. The long-lived
|
||||
// stream remains open; only the authoritative status level reports that
|
||||
// the run has ended.
|
||||
setSystemTime(350);
|
||||
jest.advanceTimersByTime(250);
|
||||
await flushMicrotasks();
|
||||
|
||||
expect(statusFetches).toBe(1);
|
||||
expect(useSessionActivityStore.getState().getStatus(workspaceId, sessionId)).toBe("idle");
|
||||
expect(queryClient.getQueryData(statusKey(workspaceId, sessionId))).toEqual({ type: "idle" });
|
||||
expect(queryClient.getQueryState(snapshotKey(workspaceId, sessionId))?.isInvalidated).toBe(true);
|
||||
const after = queryClient.getQueryData<any[]>(transcriptKey(workspaceId, sessionId));
|
||||
expect(after?.[1]?.parts[0]).toMatchObject({
|
||||
type: "text",
|
||||
text: "The final answer is visible.",
|
||||
});
|
||||
|
||||
releaseSession();
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("keeps genuine tool, retry, waiting, and compaction work active until status is authoritatively idle", async () => {
|
||||
jest.useFakeTimers();
|
||||
setSystemTime(100);
|
||||
let status: Record<string, SessionStatus> = { [sessionId]: { type: "busy" } };
|
||||
__setWorkspaceSessionSyncStatusFetcherForTest(async () => status);
|
||||
const { input, cleanup, releaseSession } = createTestSync();
|
||||
|
||||
applyStatus(input, { type: "busy" });
|
||||
useSessionActivityStore.getState().setWaitingRequest(
|
||||
workspaceId,
|
||||
sessionId,
|
||||
"permission",
|
||||
"permission-1",
|
||||
true,
|
||||
);
|
||||
jest.advanceTimersByTime(250);
|
||||
await flushMicrotasks();
|
||||
expect(useSessionActivityStore.getState().getStatus(workspaceId, sessionId)).toBe("waiting");
|
||||
|
||||
useSessionActivityStore.getState().setWaitingRequest(
|
||||
workspaceId,
|
||||
sessionId,
|
||||
"permission",
|
||||
"permission-1",
|
||||
false,
|
||||
);
|
||||
useSessionActivityStore.getState().setCompacting(workspaceId, sessionId, true);
|
||||
status = {
|
||||
[sessionId]: {
|
||||
type: "retry",
|
||||
attempt: 1,
|
||||
message: "retrying",
|
||||
next: 1_000,
|
||||
},
|
||||
};
|
||||
jest.advanceTimersByTime(250);
|
||||
await flushMicrotasks();
|
||||
expect(useSessionActivityStore.getState().getStatus(workspaceId, sessionId)).toBe("compacting");
|
||||
expect(useSessionActivityStore.getState().recordsByWorkspaceId[workspaceId]?.[sessionId]?.runActive).toBe(true);
|
||||
|
||||
status = {};
|
||||
jest.advanceTimersByTime(250);
|
||||
await flushMicrotasks();
|
||||
expect(useSessionActivityStore.getState().getStatus(workspaceId, sessionId)).toBe("idle");
|
||||
|
||||
releaseSession();
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("keeps a newer busy edge when an older idle poll resolves after a queued follow-up starts", async () => {
|
||||
jest.useFakeTimers();
|
||||
setSystemTime(100);
|
||||
let resolveStatuses: (statuses: Record<string, SessionStatus>) => void = () => {};
|
||||
__setWorkspaceSessionSyncStatusFetcherForTest(() => new Promise((resolve) => {
|
||||
resolveStatuses = resolve;
|
||||
}));
|
||||
const { input, cleanup, releaseSession } = createTestSync();
|
||||
|
||||
applyStatus(input, { type: "busy" });
|
||||
setSystemTime(350);
|
||||
jest.advanceTimersByTime(250);
|
||||
await flushMicrotasks();
|
||||
|
||||
// A queued follow-up starts while the older status request is in flight.
|
||||
// Its live edge must win over the stale empty snapshot.
|
||||
setSystemTime(Date.now() + 100);
|
||||
applyStatus(input, { type: "busy" });
|
||||
resolveStatuses({});
|
||||
await flushMicrotasks();
|
||||
|
||||
expect(useSessionActivityStore.getState().recordsByWorkspaceId[workspaceId]?.[sessionId]?.runActive).toBe(true);
|
||||
expect(getReactQueryClient().getQueryData(statusKey(workspaceId, sessionId))).toEqual({ type: "busy" });
|
||||
|
||||
releaseSession();
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("accepts terminal-before-close, reordered final content, and duplicate terminal events", async () => {
|
||||
__setWorkspaceSessionSyncSubscriptionFactoryForTest(createSubscription);
|
||||
__setWorkspaceSessionSyncStatusFetcherForTest(async () => ({}));
|
||||
setSystemTime(100);
|
||||
const input = createSyncInput();
|
||||
ensureWorkspaceSessionSync(input);
|
||||
const releaseSession = trackWorkspaceSessionSync(input, sessionId);
|
||||
await waitForSubscriptions(1);
|
||||
await flushMicrotasks();
|
||||
|
||||
applyStatus(input, { type: "busy" });
|
||||
setSystemTime(200);
|
||||
__applySessionSyncEventForTest(input, {
|
||||
type: "session.idle",
|
||||
properties: { sessionID: sessionId },
|
||||
});
|
||||
|
||||
// The event transport is intentionally still open when terminal status
|
||||
// arrives, and final content is delivered after it.
|
||||
expect(subscriptions[0]?.signal.aborted).toBe(false);
|
||||
expect(useSessionActivityStore.getState().getStatus(workspaceId, sessionId)).toBe("idle");
|
||||
applyCompletedToolAndFinalAnswer(input);
|
||||
|
||||
// Duplicate modern and deprecated terminal events remain idempotent.
|
||||
applyStatus(input, { type: "idle" });
|
||||
__applySessionSyncEventForTest(input, {
|
||||
type: "session.idle",
|
||||
properties: { sessionID: sessionId },
|
||||
});
|
||||
|
||||
const transcript = getReactQueryClient().getQueryData<any[]>(transcriptKey(workspaceId, sessionId));
|
||||
expect(transcript?.[1]?.parts[0]).toMatchObject({
|
||||
type: "text",
|
||||
text: "The final answer is visible.",
|
||||
});
|
||||
expect(useSessionActivityStore.getState().getStatus(workspaceId, sessionId)).toBe("idle");
|
||||
expect(subscriptions[0]?.signal.aborted).toBe(false);
|
||||
|
||||
releaseSession();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user