Merge pull request #728 from pikasTech/fix/issue723-cli-output-workspace

fix: keep hwlab cli workspace output current
This commit is contained in:
Lyon
2026-06-03 01:29:25 +08:00
committed by GitHub
2 changed files with 113 additions and 7 deletions
+75
View File
@@ -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");
+38 -7
View File
@@ -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) {