fix(web-probe): wait for mdtodo DOM before sentinel screenshots

This commit is contained in:
Codex
2026-06-27 06:18:47 +00:00
parent c0c392caa6
commit 379bb64d57
5 changed files with 45 additions and 2 deletions
@@ -23,16 +23,20 @@ sentinel:
path: /projects/mdtodo/sources/constart-71freq-mdtodo/files/file_0db4dc4e46adf188/tasks/R1
- type: screenshot
label: mdtodo-desktop-few-task-gap
waitProjectManagementReady: true
- type: goto
path: /projects/mdtodo/sources/constart-71freq-mdtodo/files/file_5f9645ffe8774b92/tasks/R14
- type: screenshot
label: mdtodo-r14-selected
waitProjectManagementReady: true
- type: openMdtodoReportPreview
task: R14
link: R14
- type: screenshot
label: mdtodo-r14-report-preview
waitProjectManagementReady: true
- type: toggleMdtodoReportFullscreen
text: toggle
- type: screenshot
label: mdtodo-r14-report-fullscreen
waitProjectManagementReady: true
@@ -390,7 +390,7 @@ async function processCommand(command) {
case "deleteMdtodoTask": return deleteMdtodoTask(command);
case "launchWorkbenchFromTask": return withObserverSync(await launchWorkbenchFromTask(command), "launchWorkbenchFromTask");
case "launchWorkbenchFromMdtodo": return withObserverSync(await launchWorkbenchFromMdtodo(command), "launchWorkbenchFromMdtodo");
case "screenshot": return captureScreenshot(command.reason || "manual", command.imageType || "png");
case "screenshot": return captureCommandScreenshot(command);
case "mark": return { mark: truncate(command.label || command.text || "mark", 200), currentUrl: currentPageUrl(), pageId };
case "stop": stopping = true; return { stopping: true, currentUrl: currentPageUrl(), pageId };
default: throw new Error("unsupported observer command type: " + command.type);
@@ -1251,6 +1251,28 @@ async function projectManagementReadinessSnapshot(targetPage) {
}, { selectors }).catch((error) => ({ error: errorSummary(error), valuesRedacted: true }));
}
async function waitForProjectManagementCommandReady(options = {}) {
const timeoutMs = Number.isFinite(Number(options.timeoutMs)) ? Math.max(1, Number(options.timeoutMs)) : 15000;
const started = Date.now();
const deadline = started + timeoutMs;
let last = null;
while (Date.now() <= deadline) {
last = await projectManagementCommandSnapshot();
const path = String(last?.path || safeUrlPath(currentPageUrl()) || "");
const baseReady = last?.pageKind === "project-management-mdtodo"
&& Number(last?.sourceCount || 0) > 0
&& Number(last?.fileCount || 0) > 0
&& Number(last?.taskCount || 0) > 0;
const needsTask = /\/tasks\//u.test(path);
const taskReady = !needsTask || Boolean(last?.selectedTaskId || last?.selectedTaskRef?.hash || last?.taskBodyVisible === true || last?.launchButtonVisible === true);
const needsReport = /\/reports\//u.test(path);
const reportReady = !needsReport || last?.reportPreviewVisible === true || last?.reportFullscreenVisible === true || Number(last?.reportLinkCount || 0) > 0;
if (baseReady && taskReady && reportReady) return { ok: true, reason: "project-management-command-ready", durationMs: Date.now() - started, snapshot: last, valuesRedacted: true };
await page.waitForTimeout(250).catch(() => {});
}
return { ok: false, reason: "project-management-command-not-ready", durationMs: Date.now() - started, snapshot: last, valuesRedacted: true };
}
function isWorkbenchPathname(value) {
const pathname = String(value || "");
return pathname === "/workbench" || pathname === "/workspace" || pathname.startsWith("/workbench/") || pathname.startsWith("/workspace/");
@@ -3793,6 +3815,18 @@ async function captureScreenshot(reason, imageType = "png") {
return artifact;
}
async function captureCommandScreenshot(command) {
const shouldWaitProject = command.waitProjectManagementReady === true;
const readiness = shouldWaitProject ? await waitForProjectManagementCommandReady({ timeoutMs: 15000 }) : null;
if (readiness && readiness.ok !== true) {
const error = new Error("screenshot project-management readiness wait failed: " + (readiness.reason || "not-ready"));
error.details = { readiness, currentUrl: currentPageUrl(), pageId, valuesRedacted: true };
throw error;
}
const artifact = await captureScreenshot(command.reason || command.label || "manual", command.imageType || "png");
return { ...artifact, readiness, valuesRedacted: true };
}
function eventRecord(type, data) {
const clean = sanitize(data) || {};
return { ts: new Date().toISOString(), type, jobId, pageId: clean.pageId ?? pageId, pageRole: clean.pageRole ?? "control", sampleSeq, commandId: activeCommandId, ...clean };
@@ -3838,6 +3872,7 @@ function commandInputSummary(command) {
expectedSentinelRange: command.expectedSentinelRange || null,
expectedActionWaitMs: command.expectedActionWaitMs === null || command.expectedActionWaitMs === undefined || command.expectedActionWaitMs === "" ? null : Number(command.expectedActionWaitMs),
requireComposerReady: command.requireComposerReady === true,
waitProjectManagementReady: command.waitProjectManagementReady === true,
findingId: command.findingId || null,
blocking: command.blocking === true ? true : command.blocking === false ? false : null,
sourceId: opaque(command.sourceId),
@@ -2574,6 +2574,7 @@ function appendScenarioObserveCommandArgs(args: string[], item: Record<string, u
const text = stringAtNullable(item, "text") ?? stringAtNullable(item, "value");
if (text !== null) args.push("--text", text);
}
if (item.waitProjectManagementReady === true && !args.includes("--wait-project-management-ready")) args.push("--wait-project-management-ready");
}
function finalizeQuickVerifyFailure(state: SentinelCicdState, input: {
+1
View File
@@ -204,6 +204,7 @@ export interface NodeWebProbeObserveOptions {
commandExpectedSentinelRange: string | null;
commandExpectedActionWaitMs: number | null;
commandRequireComposerReady: boolean;
commandWaitProjectManagementReady: boolean;
commandFindingId: string | null;
commandBlocking: boolean | null;
commandAccountId: string | null;
+3 -1
View File
@@ -278,7 +278,7 @@ export function parseNodeWebProbeObserveOptions(
"--workspace-root",
"--workspace-root-ref",
"--root",
]), new Set(["--force", "--full", "--raw", "--text-stdin", "--require-composer-ready", "--blocking", "--non-blocking"]));
]), new Set(["--force", "--full", "--raw", "--text-stdin", "--require-composer-ready", "--wait-project-management-ready", "--blocking", "--non-blocking"]));
const commandTypeRaw = optionValue(args, "--type") ?? null;
const commandType = commandTypeRaw === null ? null : parseNodeWebProbeObserveCommandType(commandTypeRaw);
const stateDir = optionValue(args, "--state-dir") ?? indexed?.stateDir ?? null;
@@ -431,6 +431,7 @@ export function parseNodeWebProbeObserveOptions(
commandExpectedSentinelRange,
commandExpectedActionWaitMs,
commandRequireComposerReady: args.includes("--require-composer-ready"),
commandWaitProjectManagementReady: args.includes("--wait-project-management-ready"),
commandFindingId,
commandBlocking,
commandAccountId,
@@ -1500,6 +1501,7 @@ export function runNodeWebProbeObserveCommand(options: NodeWebProbeObserveOption
expectedSentinelRange: options.commandExpectedSentinelRange,
expectedActionWaitMs: options.commandExpectedActionWaitMs,
requireComposerReady: options.commandRequireComposerReady,
waitProjectManagementReady: options.commandWaitProjectManagementReady,
findingId: options.commandFindingId,
blocking: options.commandBlocking,
accountId: options.commandAccountId,