diff --git a/scripts/web-live-dom-probe.mjs b/scripts/web-live-dom-probe.mjs index d9239d5f..1de6a7fa 100644 --- a/scripts/web-live-dom-probe.mjs +++ b/scripts/web-live-dom-probe.mjs @@ -111,9 +111,10 @@ export async function runProbe(args) { await waitForMessageCards(page, args, actions); await page.screenshot({ path: screenshotPath, fullPage: true }); const dom = await collectDomEvidence(page); + const promptValidation = validatePromptFinalResponse(dom, args); result = { ok: true, - status: dom.requiredSelectors.workspace && dom.requiredSelectors.commandInput && dom.requiredSelectors.conversationList ? "pass" : "blocked", + status: dom.requiredSelectors.workspace && dom.requiredSelectors.commandInput && dom.requiredSelectors.conversationList && promptValidation.ok ? "pass" : "blocked", command: "web-live-dom-probe run", generatedAt: new Date().toISOString(), startedAt, @@ -123,6 +124,7 @@ export async function runProbe(args) { browser: browser.hwlabLaunchPlan ?? null, actions, dom, + promptValidation, artifacts: { reportPath, screenshotPath }, safety: { repoOwnedLauncher: true, @@ -155,6 +157,32 @@ export async function runProbe(args) { return result; } +function validatePromptFinalResponse(dom, args) { + if (!args.message) return { ok: true, applicable: false, reason: "no-prompt-submitted" }; + const messages = Array.isArray(dom.messages) ? dom.messages : []; + const latestAgent = [...messages].reverse().find((message) => message.role === "agent"); + const finalResponse = latestAgent?.finalResponse ?? null; + const markdown = finalResponse?.markdown ?? null; + const failures = []; + if (!latestAgent) failures.push("agent-message-missing"); + if (latestAgent?.status !== "completed") failures.push("agent-not-completed"); + if (finalResponse?.present !== true) failures.push("final-response-missing"); + if (!finalResponse || Number(finalResponse.textChars ?? 0) <= 0) failures.push("final-response-empty"); + if (Number(markdown?.headings ?? 0) <= 0) failures.push("markdown-heading-missing"); + if (Number(markdown?.lists ?? 0) <= 0 || Number(markdown?.listItems ?? 0) <= 0) failures.push("markdown-list-missing"); + if (Number(markdown?.codeBlocks ?? 0) <= 0) failures.push("markdown-code-block-missing"); + return { + ok: failures.length === 0, + applicable: true, + failures, + finalResponse: finalResponse ? { + textChars: finalResponse.textChars ?? null, + textPreview: finalResponse.textPreview ?? null, + markdown: finalResponse.markdown ?? null + } : null + }; +} + async function startProbe(args) { await mkdir(stateRoot, { recursive: true }); const jobId = args.jobId || `web-live-dom-probe-${Date.now()}-${process.pid}`; diff --git a/web/hwlab-cloud-web/src/state/runner-trace.test.ts b/web/hwlab-cloud-web/src/state/runner-trace.test.ts index 4843337f..7611f26c 100644 --- a/web/hwlab-cloud-web/src/state/runner-trace.test.ts +++ b/web/hwlab-cloud-web/src/state/runner-trace.test.ts @@ -72,6 +72,28 @@ test("mergeTraceResults uses final assistant authority instead of later progress assert.doesNotMatch(merged.assistantText ?? "", / --- ## hwpod-spec 机制说明 ###/u); }); +test("mergeTraceResults prefers terminal result markdown over non-final flattened trace snapshots", () => { + const resultMarkdown = "ISSUE959_MARKDOWN_OK\n\n## hwpod-spec 机制说明\n\n- 第一条\n- 第二条\n\n```bash\nhwpod inspect --dry-run\n```"; + const flattenedTrace = "I'll read context first.ISSUE959_MARKDOWN_OK ## hwpod-spec 机制说明 - 第一条 - 第二条 ```bash hwpod inspect --dry-run ```"; + const terminal: AgentChatResultResponse = { + status: "completed", + traceId: "trc_issue959_real_payload", + reply: { role: "assistant", content: resultMarkdown } + }; + const events: TraceEvent[] = [ + { seq: 31, label: "agentrun:assistant:message", type: "assistant", status: "running", message: flattenedTrace }, + { seq: 36, label: "agentrun:assistant:message", type: "assistant", status: "running", message: flattenedTrace }, + { seq: 42, label: "agentrun:result:completed", type: "result", status: "completed", terminal: true, message: "AgentRun result is ready for HWLAB short-connection polling." } + ]; + const trace: TraceSnapshot = { traceId: "trc_issue959_real_payload", status: "completed", events, eventCount: events.length, lastEventLabel: "agentrun:result:completed" }; + + const merged = mergeTraceResults(terminal, trace); + + assert.equal(merged.assistantText, resultMarkdown); + assert.match(merged.assistantText ?? "", /ISSUE959_MARKDOWN_OK\n\n## hwpod-spec/u); + assert.doesNotMatch(merged.assistantText ?? "", /^I'll read context first/u); +}); + test("mergeTraceResults prefers persisted finalResponse text for expired or compact natural traces", () => { const finalMarkdown = "**结论**\n\n- 自然结束和刷新后都应该显示这一段。\n- Markdown 换行必须保留。"; const terminal: AgentChatResultResponse = { diff --git a/web/hwlab-cloud-web/src/state/runner-trace.ts b/web/hwlab-cloud-web/src/state/runner-trace.ts index e45f1e2e..ea5e5d4b 100644 --- a/web/hwlab-cloud-web/src/state/runner-trace.ts +++ b/web/hwlab-cloud-web/src/state/runner-trace.ts @@ -97,7 +97,10 @@ export function mergeTraceResults(terminal: AgentChatResultResponse, trace: Trac const terminalRunnerEvents = Array.isArray(terminalRunnerTrace?.events) ? terminalRunnerTrace.events : []; const events = longestTraceEvents(traceEvents, terminalEvents, terminalRunnerEvents); const mergedTrace = { ...trace, events, eventCount: trace.eventCount ?? events.length }; - const assistantText = finalResponseText(trace.finalResponse) ?? assistantTextFromTraceEvents(events) ?? firstNonEmptyResultText(terminal); + const assistantText = finalResponseText(trace.finalResponse) + ?? terminalAssistantTextFromTraceEvents(events) + ?? firstNonEmptyResultText(terminal) + ?? assistantTextFromTraceEvents(events); return { ...terminal, traceId: trace.traceId ?? terminal.traceId,