test: expose live DOM probe message evidence
This commit is contained in:
@@ -45,6 +45,9 @@ export function parseProbeArgs(argv) {
|
||||
cancelRunning: true,
|
||||
waitAfterSubmitMs: 1500,
|
||||
timeoutMs: 30000,
|
||||
projectId: process.env.HWLAB_WEB_PROBE_PROJECT_ID ?? "prj_hwpod_workbench",
|
||||
conversationId: process.env.HWLAB_WEB_PROBE_CONVERSATION_ID ?? "",
|
||||
waitMessagesMs: 2500,
|
||||
reportPath: tempReportPath("web-live-dom-probe.json"),
|
||||
screenshotPath: tempReportPath("web-live-dom-probe.png"),
|
||||
jobId: ""
|
||||
@@ -65,10 +68,13 @@ export function parseProbeArgs(argv) {
|
||||
else if (arg === "--pass" || arg.startsWith("--pass=")) args.pass = readValue("--pass");
|
||||
else if (arg === "--viewport" || arg.startsWith("--viewport=")) args.viewport = readValue("--viewport");
|
||||
else if (arg === "--message" || arg.startsWith("--message=")) args.message = readValue("--message");
|
||||
else if (arg === "--project-id" || arg.startsWith("--project-id=")) args.projectId = readValue("--project-id");
|
||||
else if (arg === "--conversation-id" || arg.startsWith("--conversation-id=")) args.conversationId = readValue("--conversation-id");
|
||||
else if (arg === "--report" || arg.startsWith("--report=")) args.reportPath = readValue("--report");
|
||||
else if (arg === "--screenshot" || arg.startsWith("--screenshot=")) args.screenshotPath = readValue("--screenshot");
|
||||
else if (arg === "--job-id" || arg.startsWith("--job-id=")) args.jobId = readValue("--job-id");
|
||||
else if (arg === "--wait-after-submit-ms" || arg.startsWith("--wait-after-submit-ms=")) args.waitAfterSubmitMs = positiveInteger(readValue("--wait-after-submit-ms"), "--wait-after-submit-ms");
|
||||
else if (arg === "--wait-messages-ms" || arg.startsWith("--wait-messages-ms=")) args.waitMessagesMs = positiveInteger(readValue("--wait-messages-ms"), "--wait-messages-ms");
|
||||
else if (arg === "--timeout-ms" || arg.startsWith("--timeout-ms=")) args.timeoutMs = positiveInteger(readValue("--timeout-ms"), "--timeout-ms");
|
||||
else if (arg === "--fresh-session") args.freshSession = true;
|
||||
else if (arg === "--no-cancel-running") args.cancelRunning = false;
|
||||
@@ -92,11 +98,14 @@ export async function runProbe(args) {
|
||||
const context = await browser.newContext({ viewport: { width, height } });
|
||||
const page = await context.newPage();
|
||||
await page.goto(args.url, { waitUntil: "domcontentloaded", timeout: args.timeoutMs });
|
||||
await waitForAuthBootstrap(page, args, actions);
|
||||
await maybeLogin(page, args, actions);
|
||||
await page.locator("#workspace").waitFor({ state: "visible", timeout: args.timeoutMs });
|
||||
await page.locator("#command-input").waitFor({ state: "visible", timeout: args.timeoutMs });
|
||||
if (args.conversationId) await selectConversationInBrowserSession(page, args, actions);
|
||||
if (args.freshSession) await maybeClick(page, "#session-create", actions, "fresh-session");
|
||||
if (args.message) await submitPrompt(page, args, actions);
|
||||
await waitForMessageCards(page, args, actions);
|
||||
await page.screenshot({ path: screenshotPath, fullPage: true });
|
||||
const dom = await collectDomEvidence(page);
|
||||
result = {
|
||||
@@ -115,6 +124,8 @@ export async function runProbe(args) {
|
||||
safety: {
|
||||
repoOwnedLauncher: true,
|
||||
directChromiumLaunch: false,
|
||||
projectId: args.projectId,
|
||||
conversationSelected: Boolean(args.conversationId),
|
||||
freshSessionRequested: args.freshSession,
|
||||
promptSubmitted: Boolean(args.message),
|
||||
cancelRunningRequested: args.cancelRunning
|
||||
@@ -156,6 +167,7 @@ async function startProbe(args) {
|
||||
"--user", args.user,
|
||||
"--pass", args.pass,
|
||||
"--viewport", args.viewport,
|
||||
"--project-id", args.projectId,
|
||||
"--report", reportPath,
|
||||
"--screenshot", screenshotPath,
|
||||
"--timeout-ms", String(args.timeoutMs),
|
||||
@@ -163,6 +175,7 @@ async function startProbe(args) {
|
||||
];
|
||||
if (args.freshSession) childArgs.push("--fresh-session");
|
||||
if (!args.cancelRunning) childArgs.push("--no-cancel-running");
|
||||
if (args.conversationId) childArgs.push("--conversation-id", args.conversationId);
|
||||
if (args.message) childArgs.push("--message", args.message);
|
||||
const stdout = fs.openSync(stdoutPath, "a");
|
||||
const stderr = fs.openSync(stderrPath, "a");
|
||||
@@ -180,6 +193,8 @@ async function startProbe(args) {
|
||||
pid: child.pid,
|
||||
startedAt: new Date().toISOString(),
|
||||
url: args.url,
|
||||
projectId: args.projectId,
|
||||
conversationId: args.conversationId || null,
|
||||
reportPath,
|
||||
screenshotPath,
|
||||
stdoutPath,
|
||||
@@ -216,12 +231,34 @@ async function probeStatus(args) {
|
||||
|
||||
async function maybeLogin(page, args, actions) {
|
||||
const username = page.locator("#login-username");
|
||||
if (!(await username.isVisible({ timeout: 2500 }).catch(() => false))) return;
|
||||
if (!(await username.isVisible({ timeout: Math.min(args.timeoutMs, 12000) }).catch(() => false))) {
|
||||
actions.push({ action: "login", skipped: "login-form-not-visible", authState: await page.locator("body").getAttribute("data-auth-state").catch(() => null) });
|
||||
return;
|
||||
}
|
||||
await username.fill(args.user);
|
||||
await page.locator("#login-password").fill(args.pass);
|
||||
await page.locator("#login-submit").click();
|
||||
actions.push({ action: "login", user: args.user });
|
||||
await page.locator("#command-input").waitFor({ state: "visible", timeout: args.timeoutMs });
|
||||
const finalUrl = new URL(page.url());
|
||||
if (finalUrl.searchParams.has("username") || finalUrl.searchParams.has("password")) {
|
||||
throw new Error("login credentials leaked into URL query");
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForAuthBootstrap(page, args, actions) {
|
||||
const timeout = Math.min(args.timeoutMs, 12000);
|
||||
const settled = await page.waitForFunction(() => {
|
||||
const authState = document.body.getAttribute("data-auth-state");
|
||||
const loginVisible = Boolean(document.querySelector("#login-username"));
|
||||
const workspaceVisible = Boolean(document.querySelector("#workspace"));
|
||||
return authState !== "checking" || loginVisible || workspaceVisible;
|
||||
}, null, { timeout }).catch(() => null);
|
||||
actions.push({
|
||||
action: "auth-bootstrap",
|
||||
settled: Boolean(settled),
|
||||
authState: await page.locator("body").getAttribute("data-auth-state").catch(() => null)
|
||||
});
|
||||
}
|
||||
|
||||
async function maybeClick(page, selector, actions, action) {
|
||||
@@ -244,6 +281,77 @@ async function submitPrompt(page, args, actions) {
|
||||
if (args.cancelRunning) await maybeClick(page, ".message-action-cancel", actions, "cancel-running-message");
|
||||
}
|
||||
|
||||
async function selectConversationInBrowserSession(page, args, actions) {
|
||||
const result = await page.evaluate(async ({ projectId, conversationId }) => {
|
||||
const summarizeAuthSession = (body) => ({
|
||||
authenticated: body?.authenticated ?? null,
|
||||
userId: body?.user?.id ?? body?.actor?.id ?? null,
|
||||
username: body?.user?.username ?? body?.actor?.username ?? null,
|
||||
role: body?.user?.role ?? body?.actor?.role ?? null,
|
||||
status: body?.user?.status ?? body?.actor?.status ?? null
|
||||
});
|
||||
const summarizeWorkspace = (workspace) => workspace ? {
|
||||
workspaceId: workspace.workspaceId ?? null,
|
||||
ownerUserId: workspace.ownerUserId ?? null,
|
||||
projectId: workspace.projectId ?? null,
|
||||
selectedConversationId: workspace.selectedConversationId ?? null,
|
||||
selectedAgentSessionId: workspace.selectedAgentSessionId ?? null,
|
||||
selectedConversationStatus: workspace.selectedConversation?.status ?? null,
|
||||
selectedMessagesCount: workspace.selectedConversation?.messages?.length ?? workspace.selectedConversation?.messagesCount ?? null,
|
||||
revision: workspace.revision ?? null
|
||||
} : null;
|
||||
const sessionResponse = await fetch("/auth/session", { credentials: "same-origin" });
|
||||
const sessionBody = await sessionResponse.json().catch(() => null);
|
||||
const workspaceResponse = await fetch(`/v1/workbench/workspace?projectId=${encodeURIComponent(projectId)}`, { credentials: "same-origin" });
|
||||
const workspaceBody = await workspaceResponse.json().catch(() => null);
|
||||
const workspaceId = workspaceBody?.workspace?.workspaceId;
|
||||
if (!workspaceId) return {
|
||||
ok: false,
|
||||
phase: "workspace",
|
||||
status: workspaceResponse.status,
|
||||
workspaceId: null,
|
||||
authSession: summarizeAuthSession(sessionBody),
|
||||
workspace: summarizeWorkspace(workspaceBody?.workspace),
|
||||
bodyStatus: workspaceBody?.status ?? null,
|
||||
error: workspaceBody?.error ?? null
|
||||
};
|
||||
const selectResponse = await fetch(`/v1/workbench/workspace/${encodeURIComponent(workspaceId)}/select-conversation`, {
|
||||
method: "POST",
|
||||
credentials: "same-origin",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ projectId, conversationId, updatedByClient: "web-live-dom-probe" })
|
||||
});
|
||||
const selectBody = await selectResponse.json().catch(() => null);
|
||||
return {
|
||||
ok: selectResponse.ok,
|
||||
phase: "select-conversation",
|
||||
status: selectResponse.status,
|
||||
workspaceId,
|
||||
authSession: summarizeAuthSession(sessionBody),
|
||||
workspace: summarizeWorkspace(workspaceBody?.workspace),
|
||||
responseStatus: selectBody?.status ?? null,
|
||||
error: selectBody?.error ?? null,
|
||||
conversationId: selectBody?.workspace?.selectedConversationId ?? conversationId,
|
||||
selectedMessageCount: selectBody?.workspace?.selectedConversation?.messages?.length ?? selectBody?.workspace?.selectedConversation?.messagesCount ?? null,
|
||||
sessionStatus: selectBody?.workspace?.selectedConversation?.status ?? selectBody?.workspace?.sessionStatus ?? null
|
||||
};
|
||||
}, { projectId: args.projectId, conversationId: args.conversationId });
|
||||
actions.push({ action: "select-conversation", ...result });
|
||||
if (!result.ok) throw new Error(`browser session select-conversation failed: ${JSON.stringify(result)}`);
|
||||
await page.reload({ waitUntil: "domcontentloaded", timeout: args.timeoutMs });
|
||||
await waitForAuthBootstrap(page, args, actions);
|
||||
await page.locator("#workspace").waitFor({ state: "visible", timeout: args.timeoutMs });
|
||||
await page.locator("#command-input").waitFor({ state: "visible", timeout: args.timeoutMs });
|
||||
}
|
||||
|
||||
async function waitForMessageCards(page, args, actions) {
|
||||
if (!args.waitMessagesMs) return;
|
||||
const before = await page.locator(".message-card").count().catch(() => 0);
|
||||
const found = await page.waitForFunction(() => document.querySelectorAll(".message-card").length > 0, null, { timeout: args.waitMessagesMs }).catch(() => null);
|
||||
const after = await page.locator(".message-card").count().catch(() => 0);
|
||||
actions.push({ action: "wait-message-cards", before, after, found: Boolean(found), timeoutMs: args.waitMessagesMs });
|
||||
}
|
||||
|
||||
async function collectDomEvidence(page) {
|
||||
return page.evaluate(() => {
|
||||
const workspace = document.querySelector("#workspace");
|
||||
@@ -258,6 +366,7 @@ async function collectDomEvidence(page) {
|
||||
} : null;
|
||||
return {
|
||||
title: document.title,
|
||||
authState: document.body.getAttribute("data-auth-state"),
|
||||
requiredSelectors: {
|
||||
workspace: Boolean(workspace),
|
||||
commandInput: Boolean(document.querySelector("#command-input")),
|
||||
@@ -266,7 +375,38 @@ async function collectDomEvidence(page) {
|
||||
sessionCreate: Boolean(document.querySelector("#session-create"))
|
||||
},
|
||||
messageCount: cards.length,
|
||||
messageRoles: cards.map((card) => card.getAttribute("data-message-role") ?? [...card.classList].find((item) => item.startsWith("message-")) ?? "unknown"),
|
||||
messageRoles: cards.map((card) => card.getAttribute("data-message-role") ?? [...card.classList].find((item) => item === "message-user" || item === "message-agent" || item === "message-system") ?? "unknown"),
|
||||
messages: cards.map((card) => {
|
||||
const trace = card.querySelector(".message-trace");
|
||||
const traceEvents = card.querySelector(".message-trace-events");
|
||||
const finalResponse = card.querySelector(".message-final-response");
|
||||
const traceStyle = traceEvents ? getComputedStyle(traceEvents) : null;
|
||||
const finalStyle = finalResponse ? getComputedStyle(finalResponse) : null;
|
||||
const traceRect = traceEvents?.getBoundingClientRect();
|
||||
const finalRect = finalResponse?.getBoundingClientRect();
|
||||
return {
|
||||
status: card.getAttribute("data-message-status") ?? null,
|
||||
role: card.getAttribute("data-message-role") ?? [...card.classList].find((item) => item === "message-user" || item === "message-agent" || item === "message-system")?.replace(/^message-/, "") ?? null,
|
||||
className: card.className,
|
||||
trace: trace ? {
|
||||
present: true,
|
||||
open: trace instanceof HTMLDetailsElement ? trace.open : null,
|
||||
status: trace.getAttribute("data-trace-status"),
|
||||
mode: trace.getAttribute("data-trace-mode"),
|
||||
maxHeight: traceStyle?.maxHeight ?? null,
|
||||
overflowY: traceStyle?.overflowY ?? null,
|
||||
height: traceRect ? Math.round(traceRect.height) : null,
|
||||
scrollHeight: traceEvents ? Math.round(traceEvents.scrollHeight) : null
|
||||
} : { present: false },
|
||||
finalResponse: finalResponse ? {
|
||||
present: true,
|
||||
maxHeight: finalStyle?.maxHeight ?? null,
|
||||
overflowY: finalStyle?.overflowY ?? null,
|
||||
height: finalRect ? Math.round(finalRect.height) : null,
|
||||
scrollHeight: Math.round(finalResponse.scrollHeight)
|
||||
} : { present: false }
|
||||
};
|
||||
}),
|
||||
scroll,
|
||||
textPreview: (document.body.textContent ?? "").replace(/\s+/gu, " ").trim().slice(0, 800)
|
||||
};
|
||||
@@ -307,6 +447,7 @@ function helpPayload() {
|
||||
command: "web-live-dom-probe",
|
||||
usage: [
|
||||
"node scripts/web-live-dom-probe.mjs run --url http://74.48.78.17:19666/ --fresh-session --report /tmp/hwlab-dev-gate/web-live-dom-probe.json",
|
||||
"node scripts/web-live-dom-probe.mjs run --url http://74.48.78.17:19666/#/workspace --conversation-id cnv_...",
|
||||
"node scripts/web-live-dom-probe.mjs start --url http://74.48.78.17:19666/ --fresh-session",
|
||||
"node scripts/web-live-dom-probe.mjs status <jobId>"
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user