Merge pull request #2336 from pikasTech/fix/agentrun-inspector-web-probe
Pipelines as Code CI / hwlab-web-probe-sentinel-nc01- Success
Pipelines as Code CI / platform-infra-gitea-nc01- Success
Pipelines as Code CI / unidesk-host- Success

增加 AgentRun 树图与 Inspector 聚焦验收
This commit is contained in:
Lyon
2026-07-16 19:49:29 +08:00
committed by GitHub
5 changed files with 97 additions and 9 deletions
@@ -12,7 +12,12 @@
- 默认 profile 由同一 YAML 的 `defaultConsoleVerificationProfile` 选择,CLI `--profile` 只做显式 profile 选择;
- 该命令用于已部署页面人工 closeout,不作为 CI/CD gate,不替代 Workbench Kafka、Trace 和性能专项 validator
- 成功或失败均以有界 `issueEvidence`、report SHA、screenshot SHA 和失败摘要收口,不再重复创建 `.state/probes/*.mjs`
- AgentRun 思维导图的聚焦验收使用 `--profile agentrun-tree`,直接恢复 `/agents/runs?view=tree`、等待 `.agent-mind-map` 并采集 `1920x1080` 截图。
- AgentRun 思维导图的聚焦验收使用 `--profile agentrun-tree`
- 直接恢复 `/agents/runs?view=tree` 并等待 `.agent-mind-map`
- 先采集 `1920x1080` 纯树图截图;
- 再按 YAML `selection` 合同用键盘选择首个 Run
- 校验 SVG 连线、Inspector 与工作区边界、标题/页签/正文几何;
- 最后采集 Inspector 打开状态截图。
- WebProbe 远程命令错误可见性:
- 默认文本输出必须在独立 `COMMAND_OUTPUT` 段完整展示已经脱敏并限长的 `stderrTail``stdoutTail`
- 摘要表可以继续提供单行预览,但不得用表格宽度裁剪替代完整的有界错误证据;
+19 -3
View File
@@ -858,7 +858,7 @@ templates:
failures: 20
consoleErrors: 20
responseErrors: 20
screenshots: 1
screenshots: 2
textPreviewChars: 160
viewports:
- id: desktop
@@ -868,8 +868,24 @@ templates:
- id: agent-runs-tree
path: /agents/runs?view=tree
readySelector: .agent-mind-map
screenshot: true
workflows: {}
screenshot: false
workflows:
agentObserver:
routeId: agent-runs-tree
eventSourcePath: /v1/agent-observer/events
phaseSelector: .agent-observer-stream strong
blockedPhases:
- 未连接
- 传输错误
streamSettleMs: 1200
views:
- buttonName:
queryValue: tree
screenshotName: agentrun-tree.png
selection:
selector: ".agent-mind-map-node[data-kind='run']"
inspectorSelector: .agent-observer-inspector
screenshotName: agentrun-tree-inspector.png
realtimeFanoutProfiles:
pure-kafka-live:
subscriberCount: 2
@@ -55,6 +55,12 @@ export interface HwlabRuntimeWebProbeConsoleAgentViewSpec {
readonly screenshotName: string;
}
export interface HwlabRuntimeWebProbeConsoleAgentSelectionSpec {
readonly selector: string;
readonly inspectorSelector: string;
readonly screenshotName: string;
}
export interface HwlabRuntimeWebProbeConsoleAgentObserverWorkflowSpec {
readonly routeId: string;
readonly eventSourcePath: string;
@@ -62,6 +68,7 @@ export interface HwlabRuntimeWebProbeConsoleAgentObserverWorkflowSpec {
readonly blockedPhases: readonly string[];
readonly streamSettleMs: number;
readonly views: readonly HwlabRuntimeWebProbeConsoleAgentViewSpec[];
readonly selection?: HwlabRuntimeWebProbeConsoleAgentSelectionSpec;
}
export interface HwlabRuntimeWebProbeConsoleVerificationProfileSpec {
@@ -124,7 +131,7 @@ function consoleProfile(value: unknown, path: string): HwlabRuntimeWebProbeConso
onlyKeys(output, ["failures", "consoleErrors", "responseErrors", "screenshots", "textPreviewChars"], `${path}.outputLimits`);
const screenshotBudget = integer(output.screenshots, `${path}.outputLimits.screenshots`, 1, 1000);
const matrixScreenshots = routes.filter((item) => item.screenshot).length * viewports.length;
const workflowScreenshots = workflows.agentObserver?.views.length ?? 0;
const workflowScreenshots = (workflows.agentObserver?.views.length ?? 0) + (workflows.agentObserver?.selection === undefined ? 0 : 1);
if (matrixScreenshots + workflowScreenshots > screenshotBudget) {
throw new Error(`${path}.outputLimits.screenshots=${screenshotBudget} is below the declared matrix/workflow screenshot count ${matrixScreenshots + workflowScreenshots}`);
}
@@ -243,10 +250,11 @@ function mdtodoWorkflow(value: unknown, path: string, routeIds: ReadonlySet<stri
function agentObserverWorkflow(value: unknown, path: string, routeIds: ReadonlySet<string>): HwlabRuntimeWebProbeConsoleAgentObserverWorkflowSpec {
const raw = record(value, path);
onlyKeys(raw, ["routeId", "eventSourcePath", "phaseSelector", "blockedPhases", "streamSettleMs", "views"], path);
onlyKeys(raw, ["routeId", "eventSourcePath", "phaseSelector", "blockedPhases", "streamSettleMs", "views", "selection"], path);
const views = array(raw.views, `${path}.views`, 1, 10).map((item, index) => agentView(item, `${path}.views[${index}]`));
const selection = raw.selection === undefined ? undefined : agentSelection(raw.selection, `${path}.selection`);
unique(views.map((item) => item.queryValue), `${path}.views[].queryValue`);
unique(views.map((item) => item.screenshotName), `${path}.views[].screenshotName`);
unique([...views.map((item) => item.screenshotName), ...(selection === undefined ? [] : [selection.screenshotName])], `${path}.screenshots`);
return {
routeId: routeReference(raw.routeId, `${path}.routeId`, routeIds),
eventSourcePath: absolutePath(text(raw.eventSourcePath, `${path}.eventSourcePath`), `${path}.eventSourcePath`),
@@ -254,6 +262,19 @@ function agentObserverWorkflow(value: unknown, path: string, routeIds: ReadonlyS
blockedPhases: textArray(raw.blockedPhases, `${path}.blockedPhases`, 1, 20, 100),
streamSettleMs: integer(raw.streamSettleMs, `${path}.streamSettleMs`, 0, 120_000),
views,
...(selection === undefined ? {} : { selection }),
};
}
function agentSelection(value: unknown, path: string): HwlabRuntimeWebProbeConsoleAgentSelectionSpec {
const raw = record(value, path);
onlyKeys(raw, ["selector", "inspectorSelector", "screenshotName"], path);
const screenshotName = boundedText(raw.screenshotName, `${path}.screenshotName`, 120);
if (!/^[A-Za-z0-9._-]+\.png$/u.test(screenshotName)) throw new Error(`${path}.screenshotName must be a safe PNG filename`);
return {
selector: boundedText(raw.selector, `${path}.selector`, 500),
inspectorSelector: boundedText(raw.inspectorSelector, `${path}.inspectorSelector`, 500),
screenshotName,
};
}
@@ -45,9 +45,15 @@ test("Cloud Console verification profile is owned by lane YAML", () => {
id: "agent-runs-tree",
path: "/agents/runs?view=tree",
readySelector: ".agent-mind-map",
screenshot: true,
screenshot: false,
}]);
assert.deepEqual(agentrunTree.viewports.map((item) => [item.width, item.height]), [[1920, 1080]]);
assert.deepEqual(agentrunTree.workflows.agentObserver?.selection, {
selector: ".agent-mind-map-node[data-kind='run']",
inspectorSelector: ".agent-observer-inspector",
screenshotName: "agentrun-tree-inspector.png",
});
assert.equal(agentrunTree.outputLimits.screenshots, 2);
});
test("Cloud Console profile parser rejects unknown fields and duplicate route ids", () => {
@@ -72,6 +78,8 @@ test("generated Cloud Console runner is valid JavaScript and contains no node or
assert.doesNotMatch(generatorSource, /NC01|JD01|D601|\/dashboard|lab-dev\.hwpod\.com/u);
assert.match(script, /config\/example\.yaml#profiles\.fixture/u);
assert.match(script, /requestedRoute/u);
assert.match(script, /noInspectorChromeOverlap/u);
assert.match(script, /page\.keyboard\.press\("Enter"\)/u);
});
test("console-verify parses into the existing managed script action", () => {
@@ -320,12 +320,50 @@ export default async function consoleVerify({ page, context, goto, reloadStable,
const artifact = await captureScreenshot(view.screenshotName);
viewResults.push({ buttonName: view.buttonName, queryValue, screenshotSha256: artifact?.sha256 || null });
}
let selection = null;
if (workflow.selection) {
const candidate = page.locator(workflow.selection.selector).first();
if (await candidate.count() < 1) {
addFailure("AgentRun: selection target missing " + workflow.selection.selector);
} else {
await candidate.focus();
await page.keyboard.press("Enter");
await page.locator(workflow.selection.inspectorSelector).waitFor({ state: "visible", timeout: config.navigationTimeoutMs });
await wait(config.settleMs);
const layout = await page.evaluate(({ inspectorSelector }) => {
const bounds = (selector) => {
const rect = document.querySelector(selector)?.getBoundingClientRect();
return rect ? { left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom, width: rect.width, height: rect.height } : null;
};
const edgeCount = Array.from(document.querySelectorAll(".agent-mind-map-flow .vue-flow__edge-path"))
.filter((edge) => typeof edge.getTotalLength === "function" && edge.getTotalLength() > 8).length;
const inspector = bounds(inspectorSelector);
const content = bounds(".agent-observer-content");
const header = bounds(inspectorSelector + " .operations-inspector-header");
const tabs = bounds(inspectorSelector + " .operations-inspector-tabs");
const body = bounds(inspectorSelector + " .operations-inspector-body");
return {
edgeCount,
inspector,
noWorkspaceOverlap: Boolean(content && inspector && content.right <= inspector.left + 1),
noInspectorChromeOverlap: Boolean(header && tabs && body && header.bottom <= tabs.top + 1 && tabs.bottom <= body.top + 1),
inspectorInsideViewport: Boolean(inspector && inspector.left >= 0 && inspector.right <= innerWidth + 1 && inspector.top >= 0 && inspector.bottom <= innerHeight + 1),
};
}, workflow.selection);
if (layout.edgeCount < 1) addFailure("AgentRun: mind map has no connected SVG edges");
if (!layout.noWorkspaceOverlap) addFailure("AgentRun: Inspector overlaps the workspace");
if (!layout.noInspectorChromeOverlap) addFailure("AgentRun: Inspector header, tabs and body overlap");
if (!layout.inspectorInsideViewport) addFailure("AgentRun: Inspector exceeds the viewport");
const artifact = await captureScreenshot(workflow.selection.screenshotName);
selection = { layout, screenshotSha256: artifact?.sha256 || null };
}
}
const after = await agentObserverState(workflow);
if (after.sourceCount !== before.sourceCount) {
addFailure("AgentRun: view switch changed EventSource count " + before.sourceCount + " -> " + after.sourceCount);
}
if (workflow.blockedPhases.includes(after.phase)) addFailure("AgentRun: transport phase " + after.phase);
return { before, after, views: viewResults };
return { before, after, views: viewResults, selection };
}
async function agentObserverState(workflow) {