Merge pull request #1417 from pikasTech/codex/1412-1414-workbench
修复 Workbench 新会话与移动端恢复问题
This commit is contained in:
@@ -717,7 +717,8 @@ test("workbench workspace read preserves a newer selected conversation without t
|
||||
assert.equal(restored.body.workspace.selectedConversation.sessionId, "ses_issue808_new");
|
||||
assert.equal(restored.body.workspace.workspace.selectedConversationId, "cnv_issue808_new");
|
||||
assert.equal(restored.body.workspace.workspace.selectedAgentSessionId, "ses_issue808_new");
|
||||
assert.equal(restored.body.workspace.workspace.lastTraceId, "trc_issue808_old_done");
|
||||
assert.equal(restored.body.workspace.workspace.lastTraceId, undefined);
|
||||
assert.notEqual(restored.body.workspace.workspace.sessionStatus, "running");
|
||||
assert.equal(agentRunCalls.length, 0);
|
||||
|
||||
const selectedOld = await postJson(port, `/v1/workbench/workspace/${workspace.body.workspace.workspaceId}/select-conversation`, {
|
||||
|
||||
@@ -2101,7 +2101,7 @@ function publicWorkbenchWorkspace(workspace, { conversation = null } = {}) {
|
||||
activeTraceId: workspace.activeTraceId,
|
||||
providerProfile: workspace.providerProfile,
|
||||
selectedConversation: conversation,
|
||||
workspace: redactedWorkspaceJson(workspace.workspace),
|
||||
workspace: redactedWorkspaceJson(workspaceJsonForSelectedConversation(workspace, conversation)),
|
||||
updatedBySessionId: workspace.updatedBySessionId,
|
||||
updatedByClient: workspace.updatedByClient,
|
||||
createdAt: workspace.createdAt,
|
||||
@@ -2110,6 +2110,20 @@ function publicWorkbenchWorkspace(workspace, { conversation = null } = {}) {
|
||||
secretMaterialStored: false
|
||||
};
|
||||
}
|
||||
function workspaceJsonForSelectedConversation(workspace, conversation = null) {
|
||||
const snapshot = normalizeObject(workspace?.workspace);
|
||||
const selectedConversationId = textOr(workspace?.selectedConversationId, "");
|
||||
if (!conversation || textOr(conversation.conversationId, "") !== selectedConversationId) return snapshot;
|
||||
return {
|
||||
...snapshot,
|
||||
selectedConversationId,
|
||||
selectedAgentSessionId: textOr(workspace.selectedAgentSessionId ?? conversation.sessionId, ""),
|
||||
activeTraceId: textOr(workspace.activeTraceId, ""),
|
||||
providerProfile: textOr(workspace.providerProfile, snapshot.providerProfile),
|
||||
sessionStatus: textOr(conversation.status, ""),
|
||||
lastTraceId: textOr(conversation.lastTraceId, "")
|
||||
};
|
||||
}
|
||||
function redactedWorkspaceJson(value = {}) {
|
||||
const workspace = normalizeObject(value);
|
||||
return pruneEmpty({
|
||||
|
||||
@@ -26,9 +26,11 @@ interface ScenarioState {
|
||||
conversations: ConversationRecord[];
|
||||
traces: Record<string, JsonRecord>;
|
||||
selectRequests: JsonRecord[];
|
||||
chatRequests: JsonRecord[];
|
||||
listOmitSelected: boolean;
|
||||
conversationDelayMs: number;
|
||||
terminalScript: boolean;
|
||||
staleNestedTraceId: string | null;
|
||||
}
|
||||
|
||||
const cwd = process.cwd();
|
||||
@@ -85,10 +87,17 @@ async function handleRequest(request: IncomingMessage, response: ServerResponse)
|
||||
const body = await readJson(request);
|
||||
state.selectRequests.push(redactRequestBody(body));
|
||||
const conversationId = String(body.conversationId ?? "");
|
||||
if (conversationId && !conversationById(conversationId) && body.create === true) state.conversations.unshift(createConversationFromSelect(body, conversationId));
|
||||
if (conversationId && conversationById(conversationId)) state.selectedConversationId = conversationId;
|
||||
return json(response, 200, { workspace: workspacePayload() });
|
||||
}
|
||||
|
||||
if (path === "/v1/agent/chat" && method === "POST") {
|
||||
const body = await readJson(request);
|
||||
state.chatRequests.push(redactRequestBody(body));
|
||||
return json(response, 200, acceptChatTurn(body));
|
||||
}
|
||||
|
||||
if (path === "/v1/agent/conversations" && method === "GET") {
|
||||
await delay(state.conversationDelayMs);
|
||||
const include = url.searchParams.get("includeConversationId");
|
||||
@@ -106,10 +115,18 @@ async function handleRequest(request: IncomingMessage, response: ServerResponse)
|
||||
if (sessionMatch && method === "GET") return json(response, 200, { session: sessionPayload(decodeURIComponent(sessionMatch[1] ?? "")) });
|
||||
|
||||
const turnMatch = path.match(/^\/v1\/agent\/turns\/([^/]+)$/u);
|
||||
if (turnMatch && method === "GET") return json(response, 200, turnPayload(decodeURIComponent(turnMatch[1] ?? "")));
|
||||
if (turnMatch && method === "GET") {
|
||||
const traceId = decodeURIComponent(turnMatch[1] ?? "");
|
||||
if (traceId === state.staleNestedTraceId) return json(response, 502, { ok: false, status: 502, error: { code: "upstream_unavailable", message: "stale trace is unavailable" } });
|
||||
return json(response, 200, turnPayload(traceId));
|
||||
}
|
||||
|
||||
const traceMatch = path.match(/^\/v1\/agent\/traces\/([^/]+)$/u);
|
||||
if (traceMatch && method === "GET") return json(response, 200, tracePayload(decodeURIComponent(traceMatch[1] ?? ""), url));
|
||||
if (traceMatch && method === "GET") {
|
||||
const traceId = decodeURIComponent(traceMatch[1] ?? "");
|
||||
if (traceId === state.staleNestedTraceId) return json(response, 502, { ok: false, status: 502, error: { code: "upstream_unavailable", message: "stale trace is unavailable" } });
|
||||
return json(response, 200, tracePayload(traceId, url));
|
||||
}
|
||||
|
||||
if (path === "/v1/provider-profiles") return json(response, 200, { profiles: [{ profile: "codex-api", name: "Codex API", configured: true }, { profile: "deepseek", name: "DeepSeek", configured: true }] });
|
||||
if (path === "/health/live" || path === "/health" || path === "/v1") return json(response, 200, { status: "ok", serviceId: "workbench-e2e", codeAgent: { ready: true, status: "ready" } });
|
||||
@@ -124,7 +141,7 @@ async function handleRequest(request: IncomingMessage, response: ServerResponse)
|
||||
function createScenarioState(scenarioId: string): ScenarioState {
|
||||
const base = structuredClone(capture.scenario);
|
||||
const id = scenarioId || "baseline";
|
||||
const selectedConversationId = id === "deep-link" ? "cnv_failed" : base.selectedConversationId;
|
||||
const selectedConversationId = id === "deep-link" || id === "stale-nested-trace" ? "cnv_failed" : base.selectedConversationId;
|
||||
return {
|
||||
scenarioId: id,
|
||||
projectId: base.projectId,
|
||||
@@ -134,14 +151,17 @@ function createScenarioState(scenarioId: string): ScenarioState {
|
||||
conversations: base.conversations,
|
||||
traces: base.traces,
|
||||
selectRequests: [],
|
||||
chatRequests: [],
|
||||
listOmitSelected: id === "selected-missing-from-list",
|
||||
conversationDelayMs: id === "loading" ? 2_500 : 0,
|
||||
terminalScript: id === "event-replay" || id === "running-to-terminal"
|
||||
terminalScript: id === "event-replay" || id === "running-to-terminal",
|
||||
staleNestedTraceId: id === "stale-nested-trace" ? "trc_stale_502" : null
|
||||
};
|
||||
}
|
||||
|
||||
function workspacePayload(): JsonRecord {
|
||||
const selected = conversationById(state.selectedConversationId);
|
||||
const nestedLastTraceId = state.staleNestedTraceId ?? selected?.lastTraceId ?? null;
|
||||
return {
|
||||
workspaceId: state.workspaceId,
|
||||
projectId: state.projectId,
|
||||
@@ -151,6 +171,7 @@ function workspacePayload(): JsonRecord {
|
||||
selectedAgentSessionId: selected?.sessionId ?? null,
|
||||
activeTraceId: selected?.status === "running" ? selected.lastTraceId ?? null : null,
|
||||
providerProfile: state.providerProfile,
|
||||
selectedConversation: selected ?? null,
|
||||
workspace: {
|
||||
projectId: state.projectId,
|
||||
selectedConversationId: state.selectedConversationId,
|
||||
@@ -158,12 +179,56 @@ function workspacePayload(): JsonRecord {
|
||||
threadId: selected?.threadId ?? null,
|
||||
sessionStatus: selected?.status ?? null,
|
||||
providerProfile: state.providerProfile,
|
||||
lastTraceId: selected?.lastTraceId ?? null,
|
||||
lastTraceId: nestedLastTraceId,
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createConversationFromSelect(body: JsonRecord, conversationId: string): ConversationRecord {
|
||||
const token = conversationId.slice(4);
|
||||
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;
|
||||
const now = new Date().toISOString();
|
||||
return {
|
||||
conversationId,
|
||||
projectId: state.projectId,
|
||||
sessionId,
|
||||
threadId,
|
||||
status: "active",
|
||||
startedAt: now,
|
||||
updatedAt: now,
|
||||
messageCount: 0,
|
||||
firstUserMessagePreview: null,
|
||||
session: { sessionId, threadId, status: "active" },
|
||||
messages: []
|
||||
};
|
||||
}
|
||||
|
||||
function acceptChatTurn(body: JsonRecord): JsonRecord {
|
||||
const conversationId = typeof body.conversationId === "string" ? body.conversationId : state.selectedConversationId;
|
||||
const sessionId = typeof body.sessionId === "string" ? body.sessionId : conversationById(conversationId)?.sessionId ?? `ses_${conversationId.slice(4)}`;
|
||||
const threadId = typeof body.threadId === "string" && body.threadId.trim() ? body.threadId : null;
|
||||
const traceId = typeof body.traceId === "string" && body.traceId.trim() ? body.traceId : `trc_${Date.now().toString(16)}`;
|
||||
const message = typeof body.message === "string" ? body.message : "";
|
||||
const conversation = conversationById(conversationId);
|
||||
const now = new Date().toISOString();
|
||||
if (conversation) {
|
||||
conversation.status = "running";
|
||||
conversation.lastTraceId = traceId;
|
||||
conversation.updatedAt = now;
|
||||
conversation.firstUserMessagePreview = conversation.firstUserMessagePreview ?? message;
|
||||
conversation.messageCount = (conversation.messages?.length ?? 0) + 2;
|
||||
conversation.messages = [
|
||||
...(conversation.messages ?? []),
|
||||
{ id: `msg_${traceId}_user`, role: "user", title: "用户", text: message, status: "sent", createdAt: now, conversationId, sessionId, threadId, traceId },
|
||||
{ id: `msg_${traceId}_agent`, role: "agent", title: "Code Agent", text: "", status: "running", createdAt: now, conversationId, sessionId, threadId, traceId, runnerTrace: { traceId, status: "running", sessionId, threadId, events: [], eventCount: 0, fullTraceLoaded: false, hasMore: false } }
|
||||
];
|
||||
}
|
||||
state.traces[traceId] = { traceId, status: "running", conversationId, sessionId, threadId, events: [], eventCount: 0, fullTraceLoaded: false, hasMore: false };
|
||||
return { status: "running", accepted: true, running: true, terminal: false, traceId, conversationId, sessionId, threadId, turnId: traceId };
|
||||
}
|
||||
|
||||
function conversationSummary(conversation: ConversationRecord): ConversationRecord {
|
||||
const { messages: _messages, ...rest } = conversation;
|
||||
return rest as ConversationRecord;
|
||||
@@ -245,7 +310,7 @@ function clearActiveTraceForSelection(): void {
|
||||
}
|
||||
|
||||
function stateSummary(): JsonRecord {
|
||||
return { scenarioId: state.scenarioId, selectedConversationId: state.selectedConversationId, selectRequests: state.selectRequests, conversations: state.conversations.map((item) => ({ conversationId: item.conversationId, sessionId: item.sessionId, status: item.status, lastTraceId: item.lastTraceId })) };
|
||||
return { scenarioId: state.scenarioId, selectedConversationId: state.selectedConversationId, selectRequests: state.selectRequests, chatRequests: state.chatRequests, staleNestedTraceId: state.staleNestedTraceId, workspace: workspacePayload(), conversations: state.conversations.map((item) => ({ conversationId: item.conversationId, sessionId: item.sessionId, threadId: item.threadId, status: item.status, lastTraceId: item.lastTraceId })) };
|
||||
}
|
||||
|
||||
function authPayload(): JsonRecord {
|
||||
|
||||
@@ -197,7 +197,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
rememberWorkspaceSnapshot(tabProjectId, workspace.value);
|
||||
void refreshSessionStatusById(sessionIdFromConversation(conversation));
|
||||
loading.value = true;
|
||||
const persistPromise = api.workbench.selectConversation(current.workspaceId, { projectId: tabProjectId, conversationId: conversation.conversationId, sessionId: conversation.sessionId, threadId: conversation.threadId, updatedByClient: "cloud-web-vue" });
|
||||
const persistPromise = api.workbench.selectConversation(current.workspaceId, { projectId: tabProjectId, conversationId: conversation.conversationId, sessionId: conversation.sessionId, threadId: providerThreadIdForRequest(conversation.threadId), updatedByClient: "cloud-web-vue" });
|
||||
const detailResponse = await api.workbench.conversation(conversation.conversationId, { projectId: tabProjectId });
|
||||
const response = await persistPromise;
|
||||
loading.value = false;
|
||||
@@ -315,7 +315,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
create: true,
|
||||
conversationId: conversation.conversationId,
|
||||
sessionId: conversation.sessionId,
|
||||
threadId: conversation.threadId,
|
||||
threadId: providerThreadIdForRequest(conversation.threadId),
|
||||
updatedByClient: "cloud-web-vue-fast-create"
|
||||
});
|
||||
if (!response.ok || !response.data?.workspace) {
|
||||
@@ -478,6 +478,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
setActiveConversationSelection(conversationId);
|
||||
const sessionId = composer.value.sessionId;
|
||||
const threadId = composer.value.threadId;
|
||||
const providerThreadId = providerThreadIdForRequest(threadId);
|
||||
const user = makeMessage("user", value, "sent", { traceId: steerTraceId ?? traceId, conversationId, sessionId, threadId, title: steerMode ? "用户引导" : "用户" });
|
||||
const pending = makeMessage("agent", "", "running", { traceId, conversationId, sessionId, threadId, title: "Code Agent", retryInput: value, traceAutoLifecycle: "running" });
|
||||
messages.value.push(user, pending);
|
||||
@@ -495,7 +496,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
currentRequest.value = null;
|
||||
return;
|
||||
}
|
||||
const payload = { projectId: activeProjectId.value, message: value, prompt: value, conversationId, sessionId, threadId, traceId, providerProfile: providerProfile.value, gatewayShellTimeoutMs: gatewayShellTimeoutMs.value, workspaceId: workspace.value?.workspaceId, workspaceRevision: workspace.value?.revision, targetTraceId: composer.value.targetTraceId, steerTraceId };
|
||||
const payload = { projectId: activeProjectId.value, message: value, prompt: value, conversationId, sessionId, threadId: providerThreadId, traceId, providerProfile: providerProfile.value, gatewayShellTimeoutMs: gatewayShellTimeoutMs.value, workspaceId: workspace.value?.workspaceId, workspaceRevision: workspace.value?.revision, targetTraceId: composer.value.targetTraceId, steerTraceId };
|
||||
const route = steerMode ? api.agent.steerAgentMessage : api.agent.sendAgentMessage;
|
||||
const response = await route(payload, codeAgentTimeoutMs.value, () => activityRef.value);
|
||||
if (!response.ok || !response.data) {
|
||||
@@ -1028,13 +1029,13 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
if (!response) {
|
||||
const workspaceId = workspace.value?.workspaceId;
|
||||
if (!workspaceId) return;
|
||||
response = await api.workbench.selectConversation(workspaceId, { projectId: selectedProjectId, conversationId, sessionId: conversation.sessionId, threadId: conversation.threadId, updatedByClient: "cloud-web-vue" });
|
||||
response = await api.workbench.selectConversation(workspaceId, { projectId: selectedProjectId, conversationId, sessionId: conversation.sessionId, threadId: providerThreadIdForRequest(conversation.threadId), updatedByClient: "cloud-web-vue" });
|
||||
}
|
||||
if (!response.ok && response.status === 404) {
|
||||
const fresh = await api.workbench.workspace(selectedProjectId);
|
||||
const freshWorkspace = fresh.data?.workspace;
|
||||
if (fresh.ok && freshWorkspace?.workspaceId && activeConversationId.value === conversationId) {
|
||||
response = await api.workbench.selectConversation(freshWorkspace.workspaceId, { projectId: selectedProjectId, conversationId, sessionId: conversation.sessionId, threadId: conversation.threadId, updatedByClient: "cloud-web-vue-retry" });
|
||||
response = await api.workbench.selectConversation(freshWorkspace.workspaceId, { projectId: selectedProjectId, conversationId, sessionId: conversation.sessionId, threadId: providerThreadIdForRequest(conversation.threadId), updatedByClient: "cloud-web-vue-retry" });
|
||||
}
|
||||
}
|
||||
if (!response.ok || !response.data?.workspace || activeConversationId.value !== conversationId) return;
|
||||
@@ -1057,7 +1058,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
rememberWorkspaceSnapshot(tabProjectId, workspace.value);
|
||||
rememberConversationDetail(conversation);
|
||||
const [retried, detailResponse] = await Promise.all([
|
||||
api.workbench.selectConversation(freshWorkspace.workspaceId, { projectId: tabProjectId, conversationId: conversation.conversationId, sessionId: conversation.sessionId, threadId: conversation.threadId, updatedByClient: "cloud-web-vue-retry" }),
|
||||
api.workbench.selectConversation(freshWorkspace.workspaceId, { projectId: tabProjectId, conversationId: conversation.conversationId, sessionId: conversation.sessionId, threadId: providerThreadIdForRequest(conversation.threadId), updatedByClient: "cloud-web-vue-retry" }),
|
||||
api.workbench.conversation(conversation.conversationId, { projectId: tabProjectId })
|
||||
]);
|
||||
if (!isCurrentWorkspaceSelection(requestEpoch, conversation.conversationId)) {
|
||||
@@ -1109,24 +1110,29 @@ function sessionIdFromConversation(conversation: ConversationRecord | null | und
|
||||
return firstNonEmptyString(conversation?.sessionId, conversation?.session?.sessionId) ?? null;
|
||||
}
|
||||
|
||||
function providerThreadIdForRequest(threadId: string | null | undefined): string | null {
|
||||
const value = firstNonEmptyString(threadId) ?? null;
|
||||
if (!value) return null;
|
||||
return /^thr_[A-Za-z0-9]{12,32}$/u.test(value) ? null : value;
|
||||
}
|
||||
|
||||
function makeOptimisticConversation(projectId: string, now = new Date()): ConversationRecord {
|
||||
const token = nextProtocolId("cnv").slice(4);
|
||||
const title = `新会话 ${shortSessionTimestamp(now)}`;
|
||||
const createdAt = now.toISOString();
|
||||
const sessionId = `ses_${token}`;
|
||||
const threadId = `thr_${token}`;
|
||||
return {
|
||||
conversationId: `cnv_${token}`,
|
||||
projectId,
|
||||
sessionId,
|
||||
threadId,
|
||||
threadId: null,
|
||||
status: "active",
|
||||
title,
|
||||
name: title,
|
||||
startedAt: createdAt,
|
||||
updatedAt: createdAt,
|
||||
messageCount: 0,
|
||||
session: { sessionId, threadId, status: "active" },
|
||||
session: { sessionId, status: "active" },
|
||||
messages: []
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2452,14 +2452,47 @@
|
||||
|
||||
.workbench-grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
grid-template-rows: minmax(0, 1fr);
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.session-rail,
|
||||
.hwpod-panel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.session-rail {
|
||||
display: grid;
|
||||
width: 100% !important;
|
||||
max-height: 168px;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
align-content: stretch;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.session-rail .panel-header.compact {
|
||||
min-height: 34px;
|
||||
}
|
||||
|
||||
.session-rail .panel-header.compact .page-eyebrow,
|
||||
.session-rail .session-model-channel,
|
||||
.session-rail .session-active-metadata,
|
||||
.session-rail .session-rail-actions,
|
||||
.session-rail .session-resize-handle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.session-rail .panel-header.compact h2 {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#session-create {
|
||||
min-width: 56px;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.session-list {
|
||||
max-height: 104px;
|
||||
}
|
||||
|
||||
.command-composer {
|
||||
gap: 8px;
|
||||
padding: 10px;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
export const selectors = {
|
||||
workspace: "#workspace",
|
||||
sessionCreate: "#session-create",
|
||||
sessionTabs: "#session-tabs",
|
||||
sessionStatus: "#session-status",
|
||||
commandInput: "#command-input",
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { expect, gotoWorkbench, test } from "../fixtures/test";
|
||||
import { selectors } from "../fixtures/selectors";
|
||||
|
||||
test.use({ viewport: { width: 390, height: 844 } });
|
||||
|
||||
test("mobile Workbench exposes a visible create session entry", async ({ page }) => {
|
||||
await gotoWorkbench(page);
|
||||
const create = page.locator(selectors.sessionCreate);
|
||||
await expect(create).toBeVisible();
|
||||
const box = await create.boundingBox();
|
||||
expect(box?.width ?? 0).toBeGreaterThan(32);
|
||||
expect(box?.height ?? 0).toBeGreaterThan(24);
|
||||
await create.click();
|
||||
await expect(page.locator('.session-tab[data-active="true"]')).toHaveAttribute("data-conversation-id", /^cnv_/u);
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { expect, fakeServerState, gotoWorkbench, test } from "../fixtures/test";
|
||||
import { selectors } from "../fixtures/selectors";
|
||||
|
||||
test("new optimistic session first turn does not submit local thr_* as upstream thread", async ({ page }) => {
|
||||
await gotoWorkbench(page);
|
||||
await page.locator(selectors.sessionCreate).click();
|
||||
const activeTab = page.locator('.session-tab[data-active="true"]');
|
||||
await expect(activeTab).toHaveAttribute("data-conversation-id", /^cnv_/u);
|
||||
const conversationId = await activeTab.getAttribute("data-conversation-id");
|
||||
expect(conversationId).toBeTruthy();
|
||||
|
||||
await page.locator(selectors.commandInput).fill("web-probe D601 v0.3 smoke: reply with exactly OK.");
|
||||
await expect(page.locator(selectors.commandSend)).toBeEnabled();
|
||||
await page.locator(selectors.commandSend).click();
|
||||
|
||||
await expect.poll(async () => ((await fakeServerState(page)).chatRequests as unknown[]).length).toBe(1);
|
||||
const state = await fakeServerState(page);
|
||||
const request = (state.chatRequests as Record<string, unknown>[])[0];
|
||||
if (!request) throw new Error("expected one chat request");
|
||||
expect(request.conversationId).toBe(conversationId);
|
||||
expect(String(request.sessionId)).toMatch(/^ses_/u);
|
||||
expect(request.threadId ?? null).toBeNull();
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { expect, gotoWorkbench, test } from "../fixtures/test";
|
||||
import { selectors, sessionTab } from "../fixtures/selectors";
|
||||
|
||||
test.use({ scenarioId: "stale-nested-trace" });
|
||||
|
||||
test("session restore ignores stale nested workspace lastTraceId", async ({ page }) => {
|
||||
const staleTraceRequests: string[] = [];
|
||||
page.on("request", (request) => {
|
||||
if (/\/v1\/agent\/(?:turns|traces)\/trc_stale_502/u.test(request.url())) staleTraceRequests.push(request.url());
|
||||
});
|
||||
|
||||
await gotoWorkbench(page, "/workbench/sessions/cnv_failed?projectId=prj_hwpod_workbench");
|
||||
await expect(page.locator(sessionTab("cnv_failed"))).toHaveAttribute("data-active", "true");
|
||||
await expect(page.locator(`${selectors.messageCard}[data-role="agent"][data-status="failed"]`)).toContainText("缺少受控依赖");
|
||||
await expect(page.locator(`${selectors.traceTimeline}[data-status="failed"]`)).toBeVisible();
|
||||
await page.waitForTimeout(500);
|
||||
expect(staleTraceRequests).toEqual([]);
|
||||
});
|
||||
Reference in New Issue
Block a user