Merge pull request #2336 from pikasTech/fix/agentrun-inspector-web-probe
增加 AgentRun 树图与 Inspector 聚焦验收
This commit is contained in:
@@ -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`;
|
||||
- 摘要表可以继续提供单行预览,但不得用表格宽度裁剪替代完整的有界错误证据;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user