Merge pull request #1588 from pikasTech/fix/1582-no-destructive-projection
fix: Workbench 新建 session 读侧失败不再破坏 active 投影
This commit is contained in:
@@ -126,6 +126,7 @@ async function handleRequest(request: IncomingMessage, response: ServerResponse)
|
||||
if (sessionMessagesMatch && method === "GET") {
|
||||
const sessionId = canonicalSessionId(decodeURIComponent(sessionMessagesMatch[1] ?? ""));
|
||||
await delay(sessionDetailDelayMs(sessionId));
|
||||
if (state.scenarioId === "created-session-read-failure" && sessionId === "ses_created_read_failure") return json(response, 503, { ok: false, status: 503, error: { code: "session_messages_unavailable" } });
|
||||
const session = visibleSessionById(sessionId);
|
||||
return session ? json(response, 200, workbenchSessionMessagesPayload(session, url)) : json(response, 404, { ok: false, status: 404, error: { code: "session_not_found" } });
|
||||
}
|
||||
@@ -134,6 +135,7 @@ async function handleRequest(request: IncomingMessage, response: ServerResponse)
|
||||
if (workbenchSessionMatch && method === "GET") {
|
||||
const sessionId = canonicalSessionId(decodeURIComponent(workbenchSessionMatch[1] ?? ""));
|
||||
await delay(sessionDetailDelayMs(sessionId));
|
||||
if (state.scenarioId === "created-session-read-failure" && sessionId === "ses_created_read_failure") return json(response, 503, { ok: false, status: 503, error: { code: "session_detail_unavailable" } });
|
||||
if (state.scenarioId === "session-switch-detail-404-isolated" && sessionId === "ses_stale_404") return json(response, 404, { ok: false, status: 404, error: { code: "session_not_found" } });
|
||||
if (state.scenarioId === "completed-replay-detail-404" && sessionId === "ses_completed") return json(response, 404, { ok: false, status: 404, error: { code: "session_replay_unavailable" } });
|
||||
const session = visibleSessionById(sessionId, { includeArchived: true });
|
||||
@@ -517,6 +519,8 @@ function createManualSession(body: JsonRecord): SessionRecord {
|
||||
? "server_created"
|
||||
: state.scenarioId === "create-while-route-loading"
|
||||
? "created_while_running"
|
||||
: state.scenarioId === "created-session-read-failure"
|
||||
? "created_read_failure"
|
||||
: Date.now().toString(36);
|
||||
const sessionId = typeof body.sessionId === "string" && body.sessionId.trim() ? body.sessionId : `ses_${token}`;
|
||||
const threadId = typeof body.threadId === "string" && body.threadId.trim() ? body.threadId : null;
|
||||
|
||||
@@ -118,8 +118,9 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
const selectedIsCurrent = Boolean(selected && !isArchivedSession(selected) && isCurrentSessionSelection(requestEpoch, selected.sessionId));
|
||||
if (selectedIsCurrent && selected) applySelectedSessionDetail(selected, "route");
|
||||
if (selected && !isArchivedSession(selected) && routeRequestId && !isCurrentSessionSelection(requestEpoch, selected.sessionId) && selected.sessionId !== activeSessionId.value) rememberSessionDetail(selected);
|
||||
if (selected && isArchivedSession(selected)) isolateSessionLoadFailure(selected.sessionId, "session archived");
|
||||
if (options.invalidRouteId || (targetSessionId && !selected)) isolateSessionLoadFailure(targetSessionId ?? options.invalidRouteId ?? "invalid-session", "session URL not found");
|
||||
if (selected && isArchivedSession(selected)) applyCanonicalSessionArchive(selected.sessionId, "session archived");
|
||||
if (options.invalidRouteId) markSessionReadUnavailable(options.invalidRouteId, "invalid session URL");
|
||||
if (targetSessionId && !selected) markSessionReadUnavailable(targetSessionId, "session detail unavailable");
|
||||
const stableSelected = selectedIsCurrent && selected ? selected : activeSessionListSeed();
|
||||
const nextSessions = stableSessionList(sessions.value, listedSessions, stableSelected?.sessionId ?? selected?.sessionId ?? routeSessionId ?? includeSessionId, stableSelected);
|
||||
rememberSessionList(nextSessions);
|
||||
@@ -204,7 +205,8 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
loading.value = false;
|
||||
clearSessionDetailLoading(requestId);
|
||||
clearSwitchingSession(requestId);
|
||||
isolateSessionLoadFailure(requestId, isArchivedSession(selected) ? "session archived" : "session URL not found");
|
||||
if (selected && isArchivedSession(selected)) applyCanonicalSessionArchive(selected.sessionId, "session archived");
|
||||
else markSessionReadUnavailable(requestId, "session detail unavailable");
|
||||
failWorkbenchSessionSwitch(normalized ?? requestId);
|
||||
return false;
|
||||
}
|
||||
@@ -1020,13 +1022,20 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
restartRealtime("apply-selected-session");
|
||||
}
|
||||
|
||||
function isolateSessionLoadFailure(sessionId: string, message: string): void {
|
||||
function applyCanonicalSessionArchive(sessionId: string, message: string): void {
|
||||
forgetSession(sessionId);
|
||||
replaceActiveSessionSelection(null);
|
||||
currentRequest.value = null;
|
||||
if (activeSessionId.value === sessionId) replaceActiveSessionSelection(null);
|
||||
if (currentRequest.value?.sessionId === sessionId) currentRequest.value = null;
|
||||
error.value = message;
|
||||
sessionsReady.value = sessions.value.length > 0;
|
||||
restartRealtime("session-load-failed");
|
||||
restartRealtime("session-archived");
|
||||
}
|
||||
|
||||
function markSessionReadUnavailable(sessionId: string, message: string): void {
|
||||
void sessionId;
|
||||
error.value = message;
|
||||
sessionsReady.value = sessions.value.length > 0;
|
||||
restartRealtime("session-read-unavailable");
|
||||
}
|
||||
|
||||
function reattachRestoredActiveTrace(): void {
|
||||
|
||||
@@ -72,6 +72,36 @@ test.describe("session create while old route is still loading", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("session create read-side failure authority", () => {
|
||||
test.use({ scenarioId: "created-session-read-failure" });
|
||||
|
||||
test("created session keeps active projection when detail/messages read fails", async ({ page }) => {
|
||||
await gotoWorkbench(page, "/workbench/sessions/ses_running");
|
||||
|
||||
const created = page.waitForResponse((response) => response.request().method() === "POST" && new URL(response.url()).pathname === "/v1/agent/sessions");
|
||||
await page.locator(selectors.sessionCreate).click();
|
||||
expect((await created).status()).toBe(200);
|
||||
|
||||
const createdTab = page.locator(sessionTab("ses_created_read_failure"));
|
||||
await expect(page).toHaveURL(/\/workbench\/sessions\/ses_created_read_failure/u);
|
||||
await expect(createdTab).toHaveAttribute("data-active", "true");
|
||||
await expect(page.locator(sessionTab("ses_running"))).toHaveAttribute("data-active", "false");
|
||||
await expect(page.locator(sessionTab("ses_running"))).toHaveAttribute("data-running", "true");
|
||||
await expect(page.locator(".conversation-error-hint")).toContainText(/unavailable/u);
|
||||
await expect(page.locator(".composer-mode")).toContainText("turn / 新请求");
|
||||
await expect(page.locator(".composer-warning")).toHaveCount(0);
|
||||
|
||||
await page.locator(selectors.commandInput).fill("created session projection must survive read failure");
|
||||
await expect(page.locator(selectors.commandSend)).toBeEnabled();
|
||||
await page.waitForTimeout(800);
|
||||
await expect(createdTab).toHaveAttribute("data-active", "true");
|
||||
await expect(page.locator(".composer-mode")).toContainText("turn / 新请求");
|
||||
|
||||
const state = await fakeServerState(page);
|
||||
expect((state.legacyRequestLedger as unknown[]).length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("session create and delete authority", () => {
|
||||
test.use({ scenarioId: "server-authoritative-create" });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user