From 05c5c046bc06488c27b49a7dbe316888955c7f4b Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 3 Jun 2026 01:28:30 +0800 Subject: [PATCH] fix: keep hwlab cli workspace output current --- tools/hwlab-cli/client.test.ts | 75 ++++++++++++++++++++++++++++++++++ tools/src/hwlab-cli-lib.ts | 45 ++++++++++++++++---- 2 files changed, 113 insertions(+), 7 deletions(-) diff --git a/tools/hwlab-cli/client.test.ts b/tools/hwlab-cli/client.test.ts index 972938c0..4ad522f8 100644 --- a/tools/hwlab-cli/client.test.ts +++ b/tools/hwlab-cli/client.test.ts @@ -645,6 +645,81 @@ test("hwlab-cli client agent send restores workspace when completed patch omits assert.equal(session.workspace.threadId, "thread-restore-workspace"); }); +test("hwlab-cli client agent send output uses completed result when restore is stale", async () => { + const cwd = await mkdtemp(path.join(os.tmpdir(), "hwlab-cli-client-agent-stale-restore-")); + const calls: any[] = []; + const result = await runHwlabCli([ + "client", + "agent", + "send", + "--base-url", + "http://web.test", + "--cookie", + "hwlab_session=session-a", + "--message", + "start a fresh conversation", + "--trace-id", + "trc_stale_restore", + "--conversation-id", + "cnv_stale_restore", + "--wait", + "--poll-interval-ms", + "1", + "--timeout-ms", + "1000" + ], { + cwd, + fetchImpl: async (url, init) => { + calls.push({ url: String(url), init, body: init?.body ? JSON.parse(String(init.body)) : null }); + if (String(url).endsWith("/v1/workbench/workspace?projectId=prj_device_pod_workbench")) { + return new Response(JSON.stringify({ + ok: true, + workspace: { + workspaceId: "wsp_stale_restore", + revision: 21, + selectedConversationId: "cnv_previous_restore", + selectedAgentSessionId: "ses_previous_restore", + selectedConversation: { + conversationId: "cnv_previous_restore", + sessionId: "ses_previous_restore", + threadId: "thread-previous-restore" + }, + workspace: { + sessionStatus: "idle", + threadId: "thread-previous-restore" + } + } + }), { status: 200 }); + } + if (String(url).endsWith("/v1/agent/chat")) { + return new Response(JSON.stringify({ accepted: true, status: "running", traceId: "trc_stale_restore", resultUrl: "/v1/agent/chat/result/trc_stale_restore" }), { status: 202 }); + } + if (String(url).endsWith("/v1/agent/chat/result/trc_stale_restore")) { + return new Response(JSON.stringify({ status: "completed", traceId: "trc_stale_restore", conversationId: "cnv_stale_restore", sessionId: "ses_stale_restore", reply: { role: "assistant", content: "stale restore ready" } }), { status: 200 }); + } + if (String(url).endsWith("/v1/workbench/workspace/wsp_stale_restore")) { + return new Response(JSON.stringify({ ok: true }), { status: 200 }); + } + return new Response(JSON.stringify({ ok: false, error: { code: "unexpected_test_route" } }), { status: 404 }); + }, + sleep: async () => {} + }); + + assert.equal(result.exitCode, 0); + assert.equal(calls[4].url, "http://web.test/v1/workbench/workspace?projectId=prj_device_pod_workbench"); + assert.equal(result.payload.workspace.selectedConversationId, "cnv_stale_restore"); + assert.equal(result.payload.workspace.selectedAgentSessionId, "ses_stale_restore"); + assert.equal(Object.hasOwn(result.payload.workspace, "threadId"), false); + assert.equal(result.payload.workspace.selectedConversation.conversationId, "cnv_stale_restore"); + assert.equal(result.payload.workspace.selectedConversation.sessionId, "ses_stale_restore"); + assert.equal(Object.hasOwn(result.payload.workspace.selectedConversation, "threadId"), false); + + const session = JSON.parse(await readFile(path.join(cwd, ".state/hwlab-cli/session.json"), "utf8")); + assert.equal(session.workspace.selectedConversationId, "cnv_stale_restore"); + assert.equal(session.workspace.selectedAgentSessionId, "ses_stale_restore"); + assert.equal(Object.hasOwn(session.workspace, "threadId"), false); +}); + test("hwlab-cli client agent send preserves Web continuation fields", async () => { const cwd = await mkdtemp(path.join(os.tmpdir(), "hwlab-cli-client-agent-continuation-")); await writeFile(path.join(cwd, "prompt.txt"), "看看有什么device-pod能用?", "utf8"); diff --git a/tools/src/hwlab-cli-lib.ts b/tools/src/hwlab-cli-lib.ts index 08c5cf11..397e0956 100644 --- a/tools/src/hwlab-cli-lib.ts +++ b/tools/src/hwlab-cli-lib.ts @@ -1088,7 +1088,7 @@ async function agentSend(context: any) { return responsePayload("client.agent.send", accepted, context, { route: route("POST", "/v1/agent/chat"), traceId, conversationId, continuation, replay: replay.summary, workspace: workspaceSummaryFromSession(await loadStoredState({ parsed, env: context.env, cwd: context.cwd ?? process.cwd() })), waited: false, waitPolicy: agentSendWaitPolicy(traceId) }); } const result = await pollAgentResult(context, traceId, accepted.body); - await saveCompletedAgentWorkspaceState(context, result.response?.body, { traceId, conversationId, sessionId, threadId, providerProfile: requestBody.providerProfile }); + const completedWorkspace = await saveCompletedAgentWorkspaceState(context, result.response?.body, { traceId, conversationId, sessionId, threadId, providerProfile: requestBody.providerProfile }); return ok("client.agent.send", { status: result.final ? "succeeded" : "timeout", route: route("POST", "/v1/agent/chat"), @@ -1098,7 +1098,7 @@ async function agentSend(context: any) { replay: replay.summary, accepted: compactBody(accepted.body), result: result.response ? compactResponse(result.response) : null, - workspace: workspaceSummaryFromSession(await loadStoredState({ parsed, env: context.env, cwd: context.cwd ?? process.cwd() })), + workspace: completedWorkspace ?? workspaceSummaryFromSession(await loadStoredState({ parsed, env: context.env, cwd: context.cwd ?? process.cwd() })), polls: result.polls, timeoutMs: result.timeoutMs, resultUrl: result.resultPath @@ -1781,8 +1781,12 @@ async function saveAcceptedAgentWorkspaceState(context: any, acceptedBody: any, async function saveCompletedAgentWorkspaceState(context: any, resultBody: any, fallback: any) { const session = await loadStoredState({ parsed: context.parsed, env: context.env, cwd: context.cwd ?? process.cwd() }) ?? {}; const existing = session.workspace && typeof session.workspace === "object" ? session.workspace : {}; + const resultWorkspace = resultBody ? workspaceWithAgentResult(existing, resultBody, fallback) : null; + if (resultWorkspace) { + await saveSession(context, { ...session, baseUrl: baseUrl(context.parsed, context.env), workspace: resultWorkspace, updatedAt: context.now() }); + } const workspaceId = text(existing.workspaceId); - if (!workspaceId) return; + if (!workspaceId) return resultWorkspace; const status = text(resultBody?.status); const terminal = status && status !== "running"; const pathName = `/v1/workbench/workspace/${encodeURIComponent(workspaceId)}`; @@ -1807,12 +1811,39 @@ async function saveCompletedAgentWorkspaceState(context: any, resultBody: any, f body, timeoutMs: DEFAULT_TIMEOUT_MS }); - if (!responseSucceeded(response)) return; + if (!responseSucceeded(response)) return resultWorkspace; if (response.body?.workspace) { - await saveWorkspaceState(context, response.body.workspace); - return; + const workspace = workspaceWithAgentResult(response.body.workspace, resultBody, fallback); + await saveWorkspaceState(context, workspace); + return workspace; } - await restoreWorkbenchWorkspace(context, { quiet: true }); + const restored = await restoreWorkbenchWorkspace(context, { quiet: true }); + const workspace = workspaceWithAgentResult(restored, resultBody, fallback); + if (workspace) await saveWorkspaceState(context, workspace); + return workspace ?? resultWorkspace; +} + +function workspaceWithAgentResult(workspace: any, resultBody: any, fallback: any) { + const base = normalizeWorkbenchWorkspace(workspace) ?? {}; + const conversationId = text(resultBody?.conversationId) || text(fallback.conversationId) || text(base.selectedConversationId); + const sessionId = text(resultBody?.sessionId) || text(fallback.sessionId) || text(base.selectedAgentSessionId); + const resultThreadId = text(resultBody?.threadId ?? resultBody?.session?.threadId ?? resultBody?.providerTrace?.threadId ?? fallback.threadId); + const baseConversationMatches = !conversationId || text(base.selectedConversationId) === conversationId; + const baseSessionMatches = !sessionId || text(base.selectedAgentSessionId) === sessionId; + const baseThreadId = baseConversationMatches && baseSessionMatches ? text(base.threadId) : ""; + const selectedConversation = baseConversationMatches && baseSessionMatches && base.selectedConversation + ? base.selectedConversation + : clean({ conversationId, sessionId, threadId: resultThreadId || baseThreadId }); + return clean({ + ...base, + selectedConversationId: conversationId, + selectedAgentSessionId: sessionId, + providerProfile: text(fallback.providerProfile) || text(base.providerProfile), + sessionStatus: text(resultBody?.status) || text(base.sessionStatus), + threadId: resultThreadId || baseThreadId, + selectedConversation, + updatedByClient: "hwlab-cli" + }); } async function saveWorkspaceState(context: any, workspaceBody: any) {