diff --git a/tools/hwlab-cli/client.test.ts b/tools/hwlab-cli/client.test.ts index 99a5435e..b11bfe59 100644 --- a/tools/hwlab-cli/client.test.ts +++ b/tools/hwlab-cli/client.test.ts @@ -244,7 +244,7 @@ test("hwlab-cli client gateway pressure reports byte/truncation visibility", asy assert.match(result.payload.results.find((item: any) => item.name === "large-stdout").stdoutSha256, /^[a-f0-9]{64}$/u); }); -test("hwlab-cli client agent send submits async and polls result", async () => { +test("hwlab-cli client agent send submits async and returns trace without waiting by default", async () => { const calls: any[] = []; const result = await runHwlabCli([ "client", @@ -259,11 +259,7 @@ test("hwlab-cli client agent send submits async and polls result", async () => { "--trace-id", "trc_test", "--conversation-id", - "cnv_test", - "--poll-interval-ms", - "1", - "--timeout-ms", - "1000" + "cnv_test" ], { fetchImpl: async (url, init) => { calls.push({ url: String(url), init, body: init?.body ? JSON.parse(String(init.body)) : null }); @@ -273,9 +269,6 @@ test("hwlab-cli client agent send submits async and polls result", async () => { if (String(url).endsWith("/v1/agent/chat")) { return new Response(JSON.stringify({ accepted: true, status: "running", traceId: "trc_test", resultUrl: "/v1/agent/chat/result/trc_test" }), { status: 202 }); } - if (String(url).endsWith("/v1/workbench/workspace/wsp_test")) { - return new Response(JSON.stringify({ ok: true, workspace: { workspaceId: "wsp_test", revision: 5, selectedConversationId: "cnv_test", selectedAgentSessionId: "ses_test", workspace: { threadId: "thread-test", sessionStatus: "completed" } } }), { status: 200 }); - } return new Response(JSON.stringify({ status: "completed", traceId: "trc_test", conversationId: "cnv_test", reply: { role: "assistant", content: "hi" } }), { status: 200 }); }, sleep: async () => {} @@ -290,11 +283,58 @@ test("hwlab-cli client agent send submits async and polls result", async () => { assert.equal(calls[1].body.expectedWorkspaceRevision, 3); assert.equal(calls[1].body.sessionId, "ses_test"); assert.equal(calls[1].body.threadId, "thread-test"); - assert.equal(calls[2].url, "http://web.test/v1/agent/chat/result/trc_test"); - assert.equal(calls[3].url, "http://web.test/v1/workbench/workspace/wsp_test"); + assert.equal(calls.length, 2); + assert.equal(result.payload.traceId, "trc_test"); + assert.equal(result.payload.waited, false); + assert.equal(result.payload.waitPolicy.defaultWait, false); + assert.equal(result.payload.waitPolicy.webEquivalent, true); + assert.ok(result.payload.waitPolicy.nextCommands.some((command: string) => command.includes("agent result trc_test"))); +}); + +test("hwlab-cli client agent send waits only when --wait is explicit", async () => { + const calls: any[] = []; + const result = await runHwlabCli([ + "client", + "agent", + "send", + "--base-url", + "http://web.test", + "--cookie", + "hwlab_session=session-a", + "--message", + "hello", + "--trace-id", + "trc_wait", + "--conversation-id", + "cnv_wait", + "--wait", + "--poll-interval-ms", + "1", + "--timeout-ms", + "1000" + ], { + 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_wait", revision: 3, selectedConversationId: "cnv_wait", selectedAgentSessionId: "ses_wait", workspace: { threadId: "thread-wait" } } }), { status: 200 }); + } + if (String(url).endsWith("/v1/agent/chat")) { + return new Response(JSON.stringify({ accepted: true, status: "running", traceId: "trc_wait", resultUrl: "/v1/agent/chat/result/trc_wait" }), { status: 202 }); + } + if (String(url).endsWith("/v1/workbench/workspace/wsp_wait")) { + return new Response(JSON.stringify({ ok: true, workspace: { workspaceId: "wsp_wait", revision: 5, selectedConversationId: "cnv_wait", selectedAgentSessionId: "ses_wait", workspace: { threadId: "thread-wait", sessionStatus: "completed" } } }), { status: 200 }); + } + return new Response(JSON.stringify({ status: "completed", traceId: "trc_wait", conversationId: "cnv_wait", reply: { role: "assistant", content: "hi" } }), { status: 200 }); + }, + sleep: async () => {} + }); + + assert.equal(result.exitCode, 0); + assert.equal(calls[2].url, "http://web.test/v1/agent/chat/result/trc_wait"); + assert.equal(calls[3].url, "http://web.test/v1/workbench/workspace/wsp_wait"); assert.equal(Object.hasOwn(calls[3].body, "activeTraceId"), true); assert.equal(calls[3].body.activeTraceId, null); - assert.equal(result.payload.traceId, "trc_test"); + assert.equal(result.payload.traceId, "trc_wait"); assert.equal(result.payload.result.body.status, "completed"); assert.equal(result.payload.result.body.assistantText, "hi"); assert.equal(result.payload.result.body.reply.content, "hi"); @@ -323,8 +363,7 @@ test("hwlab-cli client agent send preserves Web continuation fields", async () = "--thread-id", "019e8078-db67-7750-a5d9-1a99f3abd445", "--retry-of", - "trc_previous", - "--no-wait" + "trc_previous" ], { cwd, fetchImpl: async (url, init) => { @@ -367,8 +406,7 @@ test("hwlab-cli client agent send can replay continuation from an existing trace "--from-trace", "trc_source_web", "--trace-id", - "trc_replay_web", - "--no-wait" + "trc_replay_web" ], { fetchImpl: async (url, init) => { calls.push({ url: String(url), init, body: init?.body ? JSON.parse(String(init.body)) : null }); @@ -438,9 +476,9 @@ test("hwlab-cli client agent trace can render with the Web trace row path", asyn assert.equal(result.payload.body.render, "web"); assert.equal(result.payload.body.renderer, "web/hwlab-cloud-web/app-trace:traceDisplayRows"); assert.equal(result.payload.body.sourceEventCount, 3); - assert.ok(result.payload.body.renderedRowCount >= 2); - assert.ok(result.payload.body.rows.some((row: any) => /请求接受/u.test(row.header))); + assert.ok(result.payload.body.renderedRowCount >= 1); assert.ok(result.payload.body.rows.some((row: any) => /助手最后一条消息/u.test(row.header))); + assert.equal(JSON.stringify(result.payload.body.rows).includes("session:reused"), false); }); test("hwlab-cli Web trace render reports suppressed noise and command tool details", async () => { diff --git a/tools/src/hwlab-cli-lib.ts b/tools/src/hwlab-cli-lib.ts index 0915355c..2fcc1341 100644 --- a/tools/src/hwlab-cli-lib.ts +++ b/tools/src/hwlab-cli-lib.ts @@ -119,7 +119,8 @@ function help() { "hwlab-cli client workbench reset --profile NAME --confirm", "hwlab-cli client request GET /v1/access/status [--full]", "hwlab-cli client rpc system.health [--full]", - "hwlab-cli client agent send --message TEXT|--message-file PATH [--from-trace TRACE] [--conversation-id ID] [--session-id ID] [--thread-id ID] [--retry-of TRACE] --provider-profile deepseek --timeout-ms 120000", + "hwlab-cli client agent send --message TEXT|--message-file PATH [--from-trace TRACE] [--conversation-id ID] [--session-id ID] [--thread-id ID] [--retry-of TRACE] --provider-profile deepseek", + "hwlab-cli client agent send --message TEXT --wait --timeout-ms 50000", "hwlab-cli client agent result TRACE_ID", "hwlab-cli client agent trace TRACE_ID [--render web]", "hwlab-cli client agent inspect --trace-id TRACE_ID", @@ -614,7 +615,8 @@ function agentHelp() { serviceRuntime: false, imagePublished: false, commands: [ - "send --message TEXT|--message-file PATH [--from-trace TRACE] [--conversation-id ID] [--session-id ID] [--thread-id ID] [--retry-of TRACE] [--no-wait]", + "send --message TEXT|--message-file PATH [--from-trace TRACE] [--conversation-id ID] [--session-id ID] [--thread-id ID] [--retry-of TRACE] [default: short return]", + "send --message TEXT --wait [--timeout-ms N] [--poll-interval-ms N]", "result TRACE_ID [--full]", "trace TRACE_ID [--render web] [--limit N] [--full]", "inspect --trace-id TRACE_ID|--conversation-id ID|--session-id ID|--thread-id ID", @@ -923,8 +925,8 @@ async function agentSend(context: any) { return responsePayload("client.agent.send", accepted, context, { route: route("POST", "/v1/agent/chat"), traceId, conversationId, continuation, replay: replay.summary }); } await saveAcceptedAgentWorkspaceState(context, accepted.body, { traceId, conversationId, sessionId, threadId, providerProfile: requestBody.providerProfile }); - if (parsed.noWait === true) { - 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 }); + if (parsed.wait !== true) { + 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 }); @@ -963,6 +965,20 @@ async function pollAgentResult(context: any, traceId: string, acceptedBody: any) return { final: false, response: lastResponse, polls, timeoutMs, resultPath }; } +function agentSendWaitPolicy(traceId: string) { + return { + defaultWait: false, + webEquivalent: true, + reason: "Cloud Web submits /v1/agent/chat as a short request and observes progress by polling result/trace; CLI follows the same path by default to avoid UniDesk ssh/tran 60s runtime disconnects.", + waitCommand: `hwlab-cli client agent send --from-trace ${traceId} --message TEXT --wait`, + nextCommands: [ + `hwlab-cli client agent result ${traceId}`, + `hwlab-cli client agent trace ${traceId} --render web`, + `hwlab-cli client workbench status` + ] + }; +} + async function workbenchCommand(context: any) { const subcommand = context.rest[0] || "summary"; if (wantsHelp(context)) return workbenchHelp();