feat: integrate codex queue and pipeline oa flow

- add Codex Queue microservice/frontend integration and related deployment docs
- document 100% Pipeline OA event-flow requirements and E2E gates
- harden Pipeline frontend Gantt/timeline E2E assertions and rendering
This commit is contained in:
Codex
2026-05-07 18:11:43 +00:00
parent 2aaf0447a6
commit 79166574e8
27 changed files with 3209 additions and 123 deletions
+3 -2
View File
@@ -18,7 +18,7 @@ export interface ContainerStatus {
ports: string;
}
const rebuildableServices = ["backend-core", "frontend", "provider-gateway", "todo-note"] as const;
const rebuildableServices = ["backend-core", "frontend", "provider-gateway", "todo-note", "codex-queue"] as const;
export type RebuildableService = typeof rebuildableServices[number];
export function isRebuildableService(value: string | undefined): value is RebuildableService {
@@ -270,6 +270,7 @@ export async function stackStatus(config: UniDeskConfig): Promise<unknown> {
internalPorts: [
{ name: "backend-core", containerPort: config.network.core.containerPort, hostPort: null },
{ name: "database", containerPort: config.network.database.containerPort, hostPort: null },
{ name: "codex-queue", containerPort: 4222, hostPort: null },
],
containers: dockerContainers(config),
health: {
@@ -307,7 +308,7 @@ export function stackLogs(config: UniDeskConfig, tailBytes: number): unknown {
const currentFiles = allFiles.filter((path) => basename(path).startsWith(runtimeEnv.logPrefix));
const selectedFiles = (currentFiles.length > 0 ? currentFiles : allFiles.slice(-12)).slice(-12);
const files = selectedFiles.map((path) => ({ path, name: basename(path), tail: tailFile(path, tailBytes) }));
const containerNames = ["unidesk-database", "unidesk-backend-core", "unidesk-frontend", "unidesk-provider-gateway-main", "todo-note-backend"];
const containerNames = ["unidesk-database", "unidesk-backend-core", "unidesk-frontend", "unidesk-provider-gateway-main", "todo-note-backend", "codex-queue-backend"];
const docker = containerNames.map((name) => {
const result = runCommand(["docker", "logs", "--tail", "40", name], repoRoot);
return { name, exitCode: result.exitCode, stdoutTail: result.stdout.slice(-tailBytes), stderrTail: result.stderr.slice(-tailBytes) };
+326 -15
View File
@@ -35,6 +35,7 @@ const NETWORK_CHECK_NAMES = [
"network:findjob-public-blocked",
"network:met-nonlinear-public-blocked",
"network:todo-note-public-blocked",
"network:codex-queue-public-blocked",
] as const;
const SERVICE_CHECK_NAMES = [
@@ -51,6 +52,7 @@ const SERVICE_CHECK_NAMES = [
"microservice:catalog-pipeline",
"microservice:catalog-met-nonlinear",
"microservice:catalog-todo-note",
"microservice:catalog-codex-queue",
"microservice:findjob-status",
"microservice:findjob-health",
"microservice:findjob-summary",
@@ -58,6 +60,7 @@ const SERVICE_CHECK_NAMES = [
"microservice:pipeline-status",
"microservice:pipeline-health",
"microservice:pipeline-snapshot",
"microservice:pipeline-oa-event-flow",
"microservice:met-nonlinear-status",
"microservice:met-nonlinear-health",
"microservice:met-nonlinear-queue",
@@ -67,6 +70,9 @@ const SERVICE_CHECK_NAMES = [
"microservice:todo-note-health",
"microservice:todo-note-migrated-data",
"microservice:todo-note-write-path",
"microservice:codex-queue-status",
"microservice:codex-queue-health",
"microservice:codex-queue-tasks",
] as const;
const DATABASE_CHECK_NAMES = [
@@ -96,11 +102,15 @@ const FRONTEND_CHECK_NAMES = [
"frontend:microservice-catalog-visible",
"frontend:todo-note-integrated-visible",
"frontend:findjob-integrated-visible",
"frontend:codex-queue-integrated-visible",
"frontend:url-route-deeplink",
"frontend:pipeline-integrated-visible",
"frontend:pipeline-react-flow-visible",
"frontend:pipeline-gantt-defaults",
"frontend:pipeline-gantt-export",
"frontend:pipeline-gantt-observation-live-running",
"frontend:pipeline-step-timeline-visible",
"frontend:pipeline-oa-event-flow-visible",
"frontend:met-nonlinear-integrated-visible",
"frontend:met-nonlinear-project-tree-detail",
"frontend:met-nonlinear-queue-detail-speed",
@@ -157,17 +167,18 @@ function pipelineSnapshotNodeIds(value: any): string[] {
function pipelineSnapshotEdges(pipeline: any): Array<{ source: string; target: string; edgeType: string }> {
const config = pipelineSnapshotConfigObject(pipeline);
const rawEdges = Array.isArray(config.edges) ? config.edges : Array.isArray(pipeline?.edges) ? pipeline.edges : [];
return rawEdges.map((edge: any) => ({
const edges: Array<{ source: string; target: string; edgeType: string }> = rawEdges.map((edge: any) => ({
source: String(edge?.from || edge?.source || ""),
target: String(edge?.to || edge?.target || ""),
edgeType: String(edge?.edgeType || ""),
})).filter((edge) => edge.source && edge.target);
}));
return edges.filter((edge) => edge.source.length > 0 && edge.target.length > 0);
}
function pipelineSnapshotNodes(pipeline: any): string[] {
const config = pipelineSnapshotConfigObject(pipeline);
const rawNodes = Array.isArray(config.nodes) ? config.nodes : Array.isArray(pipeline?.nodes) ? pipeline.nodes : [];
const nodeIds = new Set(rawNodes.map((node: any) => String(node?.id || node?.nodeId || "")).filter(Boolean));
const nodeIds = new Set<string>(rawNodes.map((node: any) => String(node?.id || node?.nodeId || "")).filter((nodeId: string) => nodeId.length > 0));
const rawBatches = Array.isArray(config.topologicalBatches) ? config.topologicalBatches : Array.isArray(pipeline?.topologicalBatches) ? pipeline.topologicalBatches : [];
for (const batch of rawBatches) pipelineSnapshotNodeIds(batch).forEach((nodeId) => nodeIds.add(nodeId));
for (const edge of pipelineSnapshotEdges(pipeline)) {
@@ -177,11 +188,33 @@ function pipelineSnapshotNodes(pipeline: any): string[] {
return Array.from(nodeIds);
}
function pipelineSnapshotMonitorNodeIds(pipeline: any): string[] {
const config = pipelineSnapshotConfigObject(pipeline);
const rawNodes = Array.isArray(config.nodes) ? config.nodes : Array.isArray(pipeline?.nodes) ? pipeline.nodes : [];
return rawNodes.map((node: any) => {
const nodeId = String(node?.id || node?.nodeId || "");
const kind = String(node?.kind || "").toLowerCase();
const monitor = node?.instanceInputs?.monitor;
const componentRef = node?.componentRef || {};
const componentId = String(componentRef?.id || componentRef?.componentClass || "");
const isMonitor = kind === "procedure"
&& (node?.instanceInputs?.monitorMode === true || monitor?.enabled === true || componentId.toLowerCase().includes("monitor"));
return isMonitor ? nodeId : "";
}).filter(Boolean);
}
function pipelineOrderWithLeadingMonitors(nodeIds: string[], monitorNodeIds: string[]): string[] {
const monitorSet = new Set(monitorNodeIds);
const leading = monitorNodeIds.filter((nodeId) => nodeIds.includes(nodeId));
return leading.length === 0 ? nodeIds : [...leading, ...nodeIds.filter((nodeId) => !monitorSet.has(nodeId))];
}
function pipelineSnapshotNodeOrder(pipeline: any): string[] {
const config = pipelineSnapshotConfigObject(pipeline);
const rawBatches = Array.isArray(config.topologicalBatches) ? config.topologicalBatches : Array.isArray(pipeline?.topologicalBatches) ? pipeline.topologicalBatches : [];
const explicit = rawBatches.map((batch: any) => pipelineSnapshotNodeIds(batch)).filter((batch: string[]) => batch.length > 0);
if (explicit.length > 0) return explicit.flatMap((batch: string[]) => batch);
const monitorNodeIds = pipelineSnapshotMonitorNodeIds(pipeline);
if (explicit.length > 0) return pipelineOrderWithLeadingMonitors(explicit.flatMap((batch: string[]) => batch), monitorNodeIds);
const nodeIds = pipelineSnapshotNodes(pipeline);
const idSet = new Set(nodeIds);
@@ -206,7 +239,7 @@ function pipelineSnapshotNodeOrder(pipeline: any): string[] {
}
nodeIds.forEach((nodeId) => { if (!levels.has(nodeId)) levels.set(nodeId, 0); });
const maxLevel = Math.max(0, ...Array.from(levels.values()));
return Array.from({ length: maxLevel + 1 }, (_item, level) => nodeIds.filter((nodeId) => levels.get(nodeId) === level)).flatMap((batch) => batch);
return pipelineOrderWithLeadingMonitors(Array.from({ length: maxLevel + 1 }, (_item, level) => nodeIds.filter((nodeId) => levels.get(nodeId) === level)).flatMap((batch) => batch), monitorNodeIds);
}
function escapedPatternRegex(value: string): string {
@@ -363,6 +396,78 @@ function dockerCoreJson(path: string, init?: { method?: string; body?: unknown }
}
}
function shellQuote(value: string): string {
return `'${value.replace(/'/g, "'\\''")}'`;
}
function pipelineDetailHasLiveObservation(detail: unknown): boolean {
const body = (detail as { body?: { gantt?: { markers?: unknown[]; intervals?: unknown[] } } }).body;
const markers = Array.isArray(body?.gantt?.markers) ? body.gantt.markers : [];
const intervals = Array.isArray(body?.gantt?.intervals) ? body.gantt.intervals : [];
const hasObservation = markers.some((marker: any) =>
String(marker?.promptEvent || marker?.event || "").toLowerCase() === "node-long-running-observation"
&& String(marker?.sourceNodeId || "").length > 0
&& String(marker?.nodeId || "").length > 0
&& String(marker?.sourceNodeId || "") !== String(marker?.nodeId || ""));
const hasRunning = intervals.some((interval: any) => String(interval?.status || "").toLowerCase() === "running");
return hasObservation && hasRunning;
}
function findPipelineLiveObservationCandidate(): { ok: boolean; candidate: { pipelineId: string; runId: string } | null; latest?: unknown } {
const snapshot = dockerCoreJson("/api/microservices/pipeline/proxy/api/snapshot?__unideskArrayLimit=registry.components:2,runs:30");
const runs = (snapshot as { body?: { runs?: Array<{ runId?: string; pipelineId?: string }> } }).body?.runs ?? [];
let latest: unknown = snapshot;
for (const run of runs) {
const runId = String(run?.runId || "");
if (!runId) continue;
const detail = dockerCoreJson(`/api/microservices/pipeline/proxy/api/node-control/runs/${encodeURIComponent(runId)}?tail=180&view=gantt&scale=77&_=${Date.now()}`);
latest = detail;
if (pipelineDetailHasLiveObservation(detail)) {
return { ok: true, candidate: { pipelineId: String(run?.pipelineId || ""), runId }, latest: detail };
}
}
return { ok: false, candidate: null, latest };
}
function startPipelineLiveObservationRun(): { ok: boolean; runId: string; command: string[]; exitCode: number | null; stdout: string; stderr: string } {
const runId = `e2e_observe_live_${Date.now()}`;
const task = "UniDesk E2E live Gantt verification: keep a real node running long enough to receive OA long-running observation events.";
const startCommand = [
"npx tsx scripts/pipeline-cli.ts pipeline start",
"--id monitor-management-behavior-test",
`--run-id ${shellQuote(runId)}`,
"--timeout-ms 1200000",
`--task ${shellQuote(task)}`,
].join(" ");
const remoteCommand = `cd /home/ubuntu/pipeline && ${startCommand}`;
const command = ["bun", "scripts/cli.ts", "ssh", "D601", remoteCommand];
const result = runCommand(command, repoRoot);
return {
ok: result.exitCode === 0,
runId,
command,
exitCode: result.exitCode,
stdout: result.stdout.slice(-4000),
stderr: result.stderr.slice(-4000),
};
}
async function ensurePipelineLiveObservationCandidate(): Promise<unknown> {
const existing = findPipelineLiveObservationCandidate();
if (existing.candidate) return { ...existing, ok: true, reused: true };
const started = startPipelineLiveObservationRun();
if (!started.ok) return { ok: false, stage: "start", started, existing };
const startedAt = Date.now();
let latest: unknown = null;
while (Date.now() - startedAt < 240_000) {
const candidate = findPipelineLiveObservationCandidate();
latest = candidate.latest;
if (candidate.candidate) return { ...candidate, ok: true, reused: false, started };
await Bun.sleep(10_000);
}
return { ok: false, stage: "wait-live-observation", timeoutMs: 240_000, started, latest };
}
async function waitForTaskStatus(taskId: string, expected: string, timeoutMs = 10_000): Promise<unknown> {
const started = Date.now();
let latest: unknown = null;
@@ -455,12 +560,14 @@ async function exposureChecks(config: UniDeskConfig, urls: PublicUrls, checks: E
const findjobPublic = await fetchProbe(`http://${config.network.publicHost}:3254/api/health`, 2500);
const metNonlinearPublic = await fetchProbe(`http://${config.network.publicHost}:3288/health`, 2500);
const todoNotePublic = await fetchProbe(`http://${config.network.publicHost}:4211/api/health`, 2500);
addSelectedCheck(checks, options, "network:only-frontend-provider-ports", !portsText.includes(`:${config.network.core.port}->`) && !portsText.includes(`:${config.network.database.port}->`), portSummary);
const codexQueuePublic = await fetchProbe(`http://${config.network.publicHost}:14222/health`, 2500);
addSelectedCheck(checks, options, "network:only-frontend-provider-ports", !portsText.includes(`:${config.network.core.port}->`) && !portsText.includes(`:${config.network.database.port}->`) && !portsText.includes(":14222->"), portSummary);
addSelectedCheck(checks, options, "network:core-public-blocked", (corePublic as { reachable?: boolean }).reachable === false, corePublic);
addSelectedCheck(checks, options, "network:database-public-blocked", (databasePublic as { reachable?: boolean }).reachable === false, databasePublic);
addSelectedCheck(checks, options, "network:findjob-public-blocked", (findjobPublic as { reachable?: boolean }).reachable === false, findjobPublic);
addSelectedCheck(checks, options, "network:met-nonlinear-public-blocked", (metNonlinearPublic as { reachable?: boolean }).reachable === false, metNonlinearPublic);
addSelectedCheck(checks, options, "network:todo-note-public-blocked", (todoNotePublic as { reachable?: boolean }).reachable === false, todoNotePublic);
addSelectedCheck(checks, options, "network:codex-queue-public-blocked", (codexQueuePublic as { reachable?: boolean }).reachable === false, codexQueuePublic);
}
async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2ECheck[], options: E2ERunOptions): Promise<void> {
@@ -476,6 +583,7 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2
const pipelineStatus = dockerCoreJson("/api/microservices/pipeline/status");
const pipelineHealth = dockerCoreJson("/api/microservices/pipeline/health");
const pipelineSnapshot = dockerCoreJson("/api/microservices/pipeline/proxy/api/snapshot?__unideskArrayLimit=registry.components:8,runs:3");
const pipelineOaEventFlow = dockerCoreJson("/api/microservices/pipeline/proxy/api/oa-event-flow/diagnostics");
const metNonlinearStatus = dockerCoreJson("/api/microservices/met-nonlinear/status");
const metNonlinearHealth = dockerCoreJson("/api/microservices/met-nonlinear/health");
const metNonlinearQueue = dockerCoreJson("/api/microservices/met-nonlinear/proxy/api/queue?__unideskArrayLimit=jobs:10");
@@ -484,6 +592,9 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2
const todoNoteStatus = dockerCoreJson("/api/microservices/todo-note/status");
const todoNoteHealth = dockerCoreJson("/api/microservices/todo-note/health");
const todoNoteInstances = dockerCoreJson("/api/microservices/todo-note/proxy/api/instances");
const codexQueueStatus = dockerCoreJson("/api/microservices/codex-queue/status");
const codexQueueHealth = dockerCoreJson("/api/microservices/codex-queue/health");
const codexQueueTasks = dockerCoreJson("/api/microservices/codex-queue/proxy/api/tasks?limit=5");
const todoE2eName = `E2E Todo ${Date.now()}`;
const todoNoteCreate = dockerCoreJson("/api/microservices/todo-note/proxy/api/instances", { method: "POST", body: { name: todoE2eName } });
const todoCreatedId = (todoNoteCreate as { body?: { id?: string } }).body?.id ?? "";
@@ -524,15 +635,19 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2
const pipeline = microserviceList.find((service) => service.id === "pipeline");
const metNonlinear = microserviceList.find((service) => service.id === "met-nonlinear");
const todoNote = microserviceList.find((service) => service.id === "todo-note");
const codexQueue = microserviceList.find((service) => service.id === "codex-queue");
const findjobSummaryBody = (findjobSummary as { body?: { totalJobs?: number; prioritizedJobs?: number } }).body;
const findjobJobs = (findjobJobsPreview as { body?: { jobs?: unknown[]; _unidesk?: { arrayLimits?: { jobs?: { returnedLength?: number; originalLength?: number } } } } }).body;
const todoNoteRows = (todoNoteInstances as { body?: { instances?: Array<{ id?: string; name?: string; todoCount?: number; completedCount?: number }> } }).body?.instances ?? [];
const todoNoteNames = todoNoteRows.map((row) => row.name);
const pipelineSnapshotBody = (pipelineSnapshot as { body?: { ok?: boolean; registry?: { ok?: boolean; components?: unknown[] }; pipelines?: unknown[]; runs?: unknown[]; _unidesk?: { arrayLimits?: { "registry.components"?: { returnedLength?: number; originalLength?: number }; runs?: { returnedLength?: number; originalLength?: number } } } } }).body;
const pipelineOaBody = (pipelineOaEventFlow as { body?: { ok?: boolean; mode?: string; forbiddenResiduals?: unknown[]; runs?: Array<{ runId?: string; nodeFinishedCount?: number; nodeFinishedWithPolicyCount?: number; monitorAuditNodeFinishedCount?: number; noAuditPolicyCount?: number; controlQueuedCount?: number; controlAppliedCount?: number }>; hasNeutralNodeFinishedEvidence?: boolean; hasNoAuditPolicyEvidence?: boolean; hasAuditPolicyEvidence?: boolean } }).body;
const metNonlinearHealthBody = (metNonlinearHealth as { body?: { ok?: boolean; targetGpu?: { name?: string; freeRatio?: number } | null; image?: { present?: boolean; image?: string } } }).body;
const metNonlinearQueueBody = (metNonlinearQueue as { body?: { ok?: boolean; queue?: { counts?: Record<string, number>; maxConcurrency?: number; targetGpuName?: string }; jobs?: unknown[] } }).body;
const metNonlinearProjectsBody = (metNonlinearProjects as { body?: { ok?: boolean; projects?: unknown[] } }).body;
const metNonlinearImagesBody = (metNonlinearImages as { body?: { ok?: boolean; mlImage?: { present?: boolean; image?: string } } }).body;
const codexQueueHealthBody = (codexQueueHealth as { body?: { ok?: boolean; queue?: { defaultModel?: string; judgeConfigured?: boolean } } }).body;
const codexQueueTasksBody = (codexQueueTasks as { body?: { ok?: boolean; queue?: { defaultModel?: string }; tasks?: unknown[] } }).body;
const firstPipelineRun = Array.isArray(pipelineSnapshotBody?.runs)
? pipelineSnapshotBody.runs[0] as { runId?: string; pipelineId?: string; status?: string; updatedAt?: string } | undefined
: undefined;
@@ -555,9 +670,10 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2
},
};
addSelectedCheck(checks, options, "microservice:catalog-findjob", (microservices as { ok?: boolean }).ok === true && findjob?.providerId === "D601" && findjob.backend?.public === false, { microservices });
addSelectedCheck(checks, options, "microservice:catalog-pipeline", (microservices as { ok?: boolean }).ok === true && pipeline?.providerId === "D601" && pipeline.backend?.public === false && pipeline.runtime?.container?.name === "pipeline-v2-webui", { microservices });
addSelectedCheck(checks, options, "microservice:catalog-pipeline", (microservices as { ok?: boolean }).ok === true && pipeline?.providerId === "D601" && pipeline.backend?.public === false && pipeline.runtime?.container?.name === "pipeline-v2-control", { microservices });
addSelectedCheck(checks, options, "microservice:catalog-met-nonlinear", (microservices as { ok?: boolean }).ok === true && metNonlinear?.providerId === "D601" && metNonlinear.backend?.public === false && metNonlinear.runtime?.container?.name === "met-nonlinear-ts", { microservices });
addSelectedCheck(checks, options, "microservice:catalog-todo-note", (microservices as { ok?: boolean }).ok === true && todoNote?.providerId === config.providerGateway.id && todoNote.backend?.public === false && todoNote.runtime?.container?.name === "todo-note-backend", { microservices });
addSelectedCheck(checks, options, "microservice:catalog-codex-queue", (microservices as { ok?: boolean }).ok === true && codexQueue?.providerId === config.providerGateway.id && codexQueue.backend?.public === false && codexQueue.runtime?.container?.name === "codex-queue-backend", { microservices });
addSelectedCheck(checks, options, "microservice:findjob-status", (findjobStatus as { ok?: boolean }).ok === true && (findjobStatus as { body?: { microservice?: { id?: string; providerId?: string } } }).body?.microservice?.providerId === "D601", findjobStatus);
addSelectedCheck(checks, options, "microservice:findjob-health", (findjobHealth as { ok?: boolean; body?: { ok?: boolean } }).ok === true && (findjobHealth as { body?: { ok?: boolean } }).body?.ok === true, findjobHealth);
addSelectedCheck(checks, options, "microservice:findjob-summary", (findjobSummary as { ok?: boolean }).ok === true && Number.isFinite(findjobSummaryBody?.totalJobs) && Number.isFinite(findjobSummaryBody?.prioritizedJobs), findjobSummary);
@@ -565,6 +681,27 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2
addSelectedCheck(checks, options, "microservice:pipeline-status", (pipelineStatus as { ok?: boolean }).ok === true && (pipelineStatus as { body?: { microservice?: { id?: string; providerId?: string } } }).body?.microservice?.providerId === "D601", pipelineStatus);
addSelectedCheck(checks, options, "microservice:pipeline-health", (pipelineHealth as { ok?: boolean; body?: { ok?: boolean; service?: string } }).ok === true && (pipelineHealth as { body?: { ok?: boolean } }).body?.ok === true, pipelineHealth);
addSelectedCheck(checks, options, "microservice:pipeline-snapshot", (pipelineSnapshot as { ok?: boolean }).ok === true && pipelineSnapshotBody?.ok === true && pipelineSnapshotBody.registry?.ok === true && Array.isArray(pipelineSnapshotBody.registry.components) && pipelineSnapshotBody.registry.components.length > 0 && Array.isArray(pipelineSnapshotBody.pipelines) && pipelineSnapshotBody.pipelines.length > 0 && Array.isArray(pipelineSnapshotBody.runs) && pipelineSnapshotBody.runs.length > 0 && (pipelineSnapshotBody._unidesk?.arrayLimits?.["registry.components"]?.returnedLength ?? 999) <= 8 && (pipelineSnapshotBody._unidesk?.arrayLimits?.runs?.returnedLength ?? 999) <= 3, pipelineSnapshotDetail);
addSelectedCheck(checks, options, "microservice:pipeline-oa-event-flow",
(pipelineOaEventFlow as { ok?: boolean }).ok === true
&& pipelineOaBody?.ok === true
&& pipelineOaBody.mode === "oa-event-flow-100"
&& Array.isArray(pipelineOaBody.forbiddenResiduals)
&& pipelineOaBody.forbiddenResiduals.length === 0
&& pipelineOaBody.hasNeutralNodeFinishedEvidence === true
&& pipelineOaBody.hasNoAuditPolicyEvidence === true
&& pipelineOaBody.hasAuditPolicyEvidence === true
&& (pipelineOaBody.runs ?? []).some((run) => Number(run.noAuditPolicyCount || 0) > 0)
&& (pipelineOaBody.runs ?? []).some((run) => Number(run.monitorAuditNodeFinishedCount || 0) > 0 && Number(run.controlQueuedCount || 0) > 0 && Number(run.controlAppliedCount || 0) > 0)
&& (pipelineOaBody.runs ?? []).every((run) => Number(run.nodeFinishedWithPolicyCount || 0) === 0),
{
ok: (pipelineOaEventFlow as { ok?: boolean }).ok,
mode: pipelineOaBody?.mode,
forbiddenResiduals: pipelineOaBody?.forbiddenResiduals,
hasNeutralNodeFinishedEvidence: pipelineOaBody?.hasNeutralNodeFinishedEvidence,
hasNoAuditPolicyEvidence: pipelineOaBody?.hasNoAuditPolicyEvidence,
hasAuditPolicyEvidence: pipelineOaBody?.hasAuditPolicyEvidence,
runs: (pipelineOaBody?.runs ?? []).slice(0, 8),
});
addSelectedCheck(checks, options, "microservice:met-nonlinear-status", (metNonlinearStatus as { ok?: boolean }).ok === true && (metNonlinearStatus as { body?: { microservice?: { id?: string; providerId?: string } } }).body?.microservice?.providerId === "D601", metNonlinearStatus);
addSelectedCheck(checks, options, "microservice:met-nonlinear-health", (metNonlinearHealth as { ok?: boolean }).ok === true && metNonlinearHealthBody?.ok === true, metNonlinearHealth);
addSelectedCheck(checks, options, "microservice:met-nonlinear-queue", (metNonlinearQueue as { ok?: boolean }).ok === true && metNonlinearQueueBody?.ok === true && typeof metNonlinearQueueBody.queue?.counts === "object" && metNonlinearQueueBody.queue?.targetGpuName === "2080 Ti", metNonlinearQueue);
@@ -574,6 +711,9 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2
addSelectedCheck(checks, options, "microservice:todo-note-health", (todoNoteHealth as { ok?: boolean; body?: { ok?: boolean; storage?: string } }).ok === true && (todoNoteHealth as { body?: { ok?: boolean; storage?: string } }).body?.ok === true && (todoNoteHealth as { body?: { storage?: string } }).body?.storage === "postgres", todoNoteHealth);
addSelectedCheck(checks, options, "microservice:todo-note-migrated-data", (todoNoteInstances as { ok?: boolean }).ok === true && todoNoteRows.length >= 5 && ["CONSTAR", "大论文", "找工作", "小论文", "事务"].every((name) => todoNoteNames.includes(name)) && todoNoteRows.reduce((sum, row) => sum + Number(row.todoCount ?? 0), 0) >= 100, { todoNoteInstances });
addSelectedCheck(checks, options, "microservice:todo-note-write-path", (todoNoteCreate as { ok?: boolean }).ok === true && (todoNoteAdd as { ok?: boolean }).ok === true && (todoNoteToggle as { ok?: boolean }).ok === true && (todoNoteUndo as { ok?: boolean }).ok === true && (todoNoteDelete as { ok?: boolean }).ok === true, { todoNoteCreate, todoNoteAdd, todoNoteToggle, todoNoteUndo, todoNoteDelete });
addSelectedCheck(checks, options, "microservice:codex-queue-status", (codexQueueStatus as { ok?: boolean }).ok === true && (codexQueueStatus as { body?: { microservice?: { id?: string; providerId?: string } } }).body?.microservice?.providerId === config.providerGateway.id, codexQueueStatus);
addSelectedCheck(checks, options, "microservice:codex-queue-health", (codexQueueHealth as { ok?: boolean }).ok === true && codexQueueHealthBody?.ok === true && codexQueueHealthBody.queue?.defaultModel === "gpt-5.4-mini", codexQueueHealth);
addSelectedCheck(checks, options, "microservice:codex-queue-tasks", (codexQueueTasks as { ok?: boolean }).ok === true && codexQueueTasksBody?.ok === true && Array.isArray(codexQueueTasksBody.tasks) && codexQueueTasksBody.queue?.defaultModel === "gpt-5.4-mini", codexQueueTasks);
const upgradeDispatch = dockerCoreJson("/api/dispatch", {
method: "POST",
body: { providerId: config.providerGateway.id, command: "provider.upgrade", payload: { source: "cli-e2e", mode: "plan" } },
@@ -670,12 +810,16 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
const needMicroserviceCatalog = wants("frontend:microservice-catalog-visible");
const needTodoNote = wants("frontend:todo-note-integrated-visible");
const needFindJob = wants("frontend:findjob-integrated-visible");
const needCodexQueue = wants("frontend:codex-queue-integrated-visible");
const needRouteDeepLink = wants("frontend:url-route-deeplink");
const needPipeline = wantsAny([
"frontend:pipeline-integrated-visible",
"frontend:pipeline-react-flow-visible",
"frontend:pipeline-gantt-defaults",
"frontend:pipeline-gantt-export",
"frontend:pipeline-gantt-observation-live-running",
"frontend:pipeline-step-timeline-visible",
"frontend:pipeline-oa-event-flow-visible",
]);
const needMetNonlinear = wantsAny([
"frontend:met-nonlinear-integrated-visible",
@@ -734,6 +878,7 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
let microserviceCatalogText = "";
let todoNoteText = "";
let findjobText = "";
let codexQueueText = "";
let routeDeepLinkText = "";
let routeInitialPath = "";
let routeDockerPath = "";
@@ -742,16 +887,21 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
let routeOverviewPath = "";
let routeOverviewText = "";
let pipelineText = "";
let pipelineOaPanelText = "";
let pipelineFlowNodeCount = 0;
let pipelineFlowEdgeCount = 0;
let pipelineSelectedId = "";
let pipelineGanttScaleLabel = "";
let pipelineGanttAutoHideIdleChecked = true;
let pipelineGanttHeaderNodeOrder: string[] = [];
let pipelineGanttExportInfo: any = { downloaded: false, suggestedFilename: "", savePath: "", bytes: 0 };
let pipelineSnapshotForFrontend: any = null;
let pipelineObservationSetup: any = null;
let pipelineObservationGanttMetrics: any = { candidate: null, observationArrowCount: 0, observationSourceMarkerCount: 0, observationArrowTargetInsetsPx: [], liveRunningBarCount: 0, liveRunningHeights: [], runningAnimationNames: [], arrowPairs: [], hasLiveSweep: false };
let pipelineStepTimelineText = "";
let pipelineSessionHeadText = "";
let firstPipelineStepSummaryText = "";
let pipelineTimelineMetrics = { clientWidth: 0, scrollWidth: 0, clientHeight: 0, scrollHeight: 0, hasHorizontalScroll: false };
let pipelineTimelineMetrics = { clientWidth: 0, scrollWidth: 0, clientHeight: 0, scrollHeight: 0, hasHorizontalScroll: false, flowConnectorVisible: false, maxStepIdleGapMs: 0, idleGapCount: 0 };
let firstPipelineStepSummaryMetrics = { clientWidth: 0, scrollWidth: 0, clientHeight: 0, scrollHeight: 0, hasHorizontalScroll: false };
let firstPipelineStepExpandedText = "";
let metNonlinearInitialText = "";
@@ -769,6 +919,10 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
await page.waitForTimeout(80);
}
if (wants("frontend:pipeline-gantt-observation-live-running")) {
pipelineObservationSetup = await ensurePipelineLiveObservationCandidate();
}
if (needMobileRail || needMobileContent) {
await page.setViewportSize({ width: 390, height: 860 });
if (needMobileRail) {
@@ -885,9 +1039,9 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
}
}
if (needMicroserviceCatalog || needTodoNote || needFindJob || needRouteDeepLink || needPipeline || needMetNonlinear) {
if (needMicroserviceCatalog || needTodoNote || needFindJob || needCodexQueue || needRouteDeepLink || needPipeline || needMetNonlinear) {
await page.getByRole("button", { name: /微服务/ }).click();
if (needMicroserviceCatalog || needTodoNote || needFindJob || needRouteDeepLink || needPipeline || needMetNonlinear) {
if (needMicroserviceCatalog || needTodoNote || needFindJob || needCodexQueue || needRouteDeepLink || needPipeline || needMetNonlinear) {
await page.waitForSelector('[data-testid="microservice-catalog-page"]', { timeout: 10000 });
}
if (needMicroserviceCatalog) {
@@ -895,6 +1049,7 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
await page.waitForSelector('[data-testid="microservice-row-pipeline"]', { timeout: 10000 });
await page.waitForSelector('[data-testid="microservice-row-met-nonlinear"]', { timeout: 10000 });
await page.waitForSelector('[data-testid="microservice-row-todo-note"]', { timeout: 10000 });
await page.waitForSelector('[data-testid="microservice-row-codex-queue"]', { timeout: 10000 });
microserviceCatalogText = await page.locator('[data-testid="microservice-catalog-page"]').innerText({ timeout: 5000 });
}
if (needTodoNote) {
@@ -950,6 +1105,22 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
}, undefined, { timeout: 30000 });
findjobText = await page.locator('[data-testid="findjob-page"]').innerText({ timeout: 5000 });
}
if (needCodexQueue) {
await page.getByRole("button", { name: /Codex Queue/ }).click();
await page.waitForSelector('[data-testid="codex-queue-page"]', { timeout: 10000 });
await page.waitForFunction(() => {
const text = document.body.innerText;
const lower = text.toLowerCase();
return lower.includes("codex queue 控制台")
&& text.includes("gpt-5.4-mini")
&& text.includes("仅 UniDesk frontend 代理访问")
&& text.includes("提交任务")
&& text.includes("追加 prompt")
&& text.includes("打断")
&& lower.includes("attempts");
}, undefined, { timeout: 30000 });
codexQueueText = await page.locator('[data-testid="codex-queue-page"]').innerText({ timeout: 5000 });
}
if (needRouteDeepLink) {
await page.goto(`${urls.frontendUrl}/app/pipeline/`, { waitUntil: "domcontentloaded", timeout: 15000 });
await page.waitForSelector('[data-testid="app-shell"]', { timeout: 10000 });
@@ -990,16 +1161,49 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
&& /运行记录\s+[1-9]\d*/.test(text);
}, undefined, { timeout: 30000 });
pipelineFlowNodeCount = await page.locator('[data-testid="pipeline-react-flow"] .react-flow__node').count();
await page.waitForFunction(() => document.querySelectorAll('[data-testid="pipeline-react-flow"] .react-flow__edge').length > 0, undefined, { timeout: 10000 }).catch(() => undefined);
pipelineFlowEdgeCount = await page.locator('[data-testid="pipeline-react-flow"] .react-flow__edge').count();
pipelineSelectedId = await page.locator('[data-testid="pipeline-select"]').evaluate((element) => (element as HTMLSelectElement).value);
await page.waitForFunction(() => {
const panel = document.querySelector('[data-testid="pipeline-oa-event-flow-panel"]') as HTMLElement | null;
const text = panel?.innerText || "";
const lower = text.toLowerCase();
return lower.includes("oa flow")
&& text.includes("100%")
&& lower.includes("no-audit")
&& lower.includes("monitor 审核")
&& text.includes("禁止残留")
&& text.includes("policy-in-detail 0");
}, undefined, { timeout: 30000 });
pipelineText = await page.locator('[data-testid="pipeline-page"]').innerText({ timeout: 5000 });
pipelineOaPanelText = await page.locator('[data-testid="pipeline-oa-event-flow-panel"]').innerText({ timeout: 5000 });
pipelineGanttScaleLabel = await page.locator('[data-testid="pipeline-gantt-scale-label"]').innerText({ timeout: 5000 });
pipelineGanttAutoHideIdleChecked = await page.locator('[data-testid="pipeline-gantt-auto-hide-idle"]').isChecked();
await page.waitForFunction(() => document.querySelectorAll('[data-testid="pipeline-gantt-head-node"]').length > 0, undefined, { timeout: 15000 });
pipelineGanttHeaderNodeOrder = await page.locator('[data-testid="pipeline-gantt-head-node"]').evaluateAll((elements) =>
elements.map((element) => element.getAttribute("data-node-id") || "").filter(Boolean));
pipelineSnapshotForFrontend = await page.evaluate(async () => {
const response = await fetch("/api/microservices/pipeline/proxy/api/snapshot?__unideskArrayLimit=registry.components:8,runs:3", { credentials: "same-origin" });
return response.ok ? response.json() : null;
});
if (wants("frontend:pipeline-gantt-export")) {
const exportButton = page.getByTestId("pipeline-export-gantt");
await exportButton.waitFor({ state: "visible", timeout: 10000 });
await page.waitForFunction(() => {
const button = document.querySelector('[data-testid="pipeline-export-gantt"]') as HTMLButtonElement | null;
return Boolean(button && !button.disabled);
}, undefined, { timeout: 10000 });
const [download] = await Promise.all([
page.waitForEvent("download", { timeout: 15000 }),
exportButton.click(),
]);
const suggestedFilename = download.suggestedFilename();
const safeFilename = suggestedFilename.replace(/[^a-zA-Z0-9_.-]+/g, "-") || "pipeline-gantt.png";
const savePath = join(e2eDir, `${Date.now()}_${safeFilename}`);
await download.saveAs(savePath);
const bytes = readFileSync(savePath).byteLength;
pipelineGanttExportInfo = { downloaded: true, suggestedFilename, savePath, bytes };
}
if (wants("frontend:pipeline-step-timeline-visible")) {
const firstGanttLine = page.locator('[data-testid="pipeline-epoch-gantt"] [data-testid="pipeline-gantt-line"]').first();
await firstGanttLine.scrollIntoViewIfNeeded({ timeout: 10000 });
@@ -1012,12 +1216,24 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
firstPipelineStepSummaryText = await firstPipelineStepSummary.innerText({ timeout: 5000 });
pipelineTimelineMetrics = await page.locator('[data-testid="pipeline-step-timeline"]').evaluate((element) => {
const target = element as HTMLElement;
const flow = target.querySelector(".pipeline-opencode-flow") as HTMLElement | null;
const flowBefore = flow ? window.getComputedStyle(flow, "::before") : null;
const steps = Array.from(target.querySelectorAll('[data-testid="pipeline-opencode-step"]')) as HTMLElement[];
const gaps = steps.slice(1).map((step, index) => {
const previous = steps[index];
const previousEndMs = Number(previous.dataset.stepEndMs || "0");
const nextStartMs = Number(step.dataset.stepStartMs || "0");
return Number.isFinite(previousEndMs) && Number.isFinite(nextStartMs) && previousEndMs > 0 && nextStartMs > 0 ? nextStartMs - previousEndMs : 0;
}).filter((gap) => gap > 0);
return {
clientWidth: target.clientWidth,
scrollWidth: target.scrollWidth,
clientHeight: target.clientHeight,
scrollHeight: target.scrollHeight,
hasHorizontalScroll: target.scrollWidth > target.clientWidth + 1,
flowConnectorVisible: Boolean(flowBefore && flowBefore.display !== "none" && flowBefore.content !== "none" && flowBefore.content !== "normal" && Number.parseFloat(flowBefore.width || "0") > 0),
maxStepIdleGapMs: gaps.length > 0 ? Math.max(...gaps) : 0,
idleGapCount: gaps.length,
};
});
firstPipelineStepSummaryMetrics = await firstPipelineStepSummary.evaluate((element) => {
@@ -1034,6 +1250,72 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
await firstPipelineStep.locator('[data-testid="pipeline-opencode-step-body"]').waitFor({ state: "visible", timeout: 10000 });
firstPipelineStepExpandedText = await firstPipelineStep.innerText({ timeout: 5000 });
}
if (wants("frontend:pipeline-gantt-observation-live-running")) {
const candidate = await page.evaluate(async () => {
const snapshotResponse = await fetch("/api/microservices/pipeline/proxy/api/snapshot?__unideskArrayLimit=registry.components:2,runs:30", { credentials: "same-origin" });
if (!snapshotResponse.ok) return null;
const snapshot = await snapshotResponse.json();
const runs = Array.isArray(snapshot?.runs) ? snapshot.runs : [];
for (const run of runs) {
const runId = String(run?.runId || "");
if (!runId) continue;
const detailResponse = await fetch(`/api/microservices/pipeline/proxy/api/node-control/runs/${encodeURIComponent(runId)}?tail=160&view=gantt&scale=77&_=${Date.now()}`, { credentials: "same-origin" });
if (!detailResponse.ok) continue;
const detail = await detailResponse.json();
const markers = Array.isArray(detail?.gantt?.markers) ? detail.gantt.markers : [];
const intervals = Array.isArray(detail?.gantt?.intervals) ? detail.gantt.intervals : [];
const hasObservation = markers.some((marker: any) =>
String(marker?.promptEvent || marker?.event || "").toLowerCase() === "node-long-running-observation"
&& String(marker?.sourceNodeId || "")
&& String(marker?.nodeId || "")
&& String(marker?.sourceNodeId || "") !== String(marker?.nodeId || ""));
const hasRunning = intervals.some((interval: any) => String(interval?.status || "").toLowerCase() === "running");
if (hasObservation && hasRunning) return { pipelineId: String(run?.pipelineId || detail?.request?.pipelineId || ""), runId };
}
return null;
});
if (candidate?.pipelineId && candidate?.runId) {
await page.locator('[data-testid="pipeline-select"]').selectOption(candidate.pipelineId);
await page.waitForFunction((runId) => Array.from(document.querySelectorAll('[data-testid="pipeline-run-select"] option')).some((option) => (option as HTMLOptionElement).value === runId), candidate.runId, { timeout: 15000 });
await page.locator('[data-testid="pipeline-run-select"]').selectOption(candidate.runId);
await page.waitForFunction((runId) => document.querySelector('[data-testid="pipeline-epoch-gantt"]')?.getAttribute("data-run-id") === runId, candidate.runId, { timeout: 15000 });
await page.waitForFunction(() => document.querySelectorAll('[data-testid="pipeline-gantt-observation-arrow"]').length > 0, undefined, { timeout: 15000 });
await page.waitForFunction(() => document.querySelectorAll('[data-testid="pipeline-gantt-line"][data-status="running"][data-live="true"]').length > 0, undefined, { timeout: 15000 });
await page.waitForTimeout(1200);
pipelineObservationGanttMetrics = await page.locator('[data-testid="pipeline-epoch-gantt"]').evaluate((element, selectedCandidate) => {
const root = element as HTMLElement;
const arrows = Array.from(root.querySelectorAll('[data-testid="pipeline-gantt-observation-arrow"]'));
const liveBars = Array.from(root.querySelectorAll('[data-testid="pipeline-gantt-line"][data-status="running"][data-live="true"]')) as HTMLElement[];
const svg = root.querySelector(".pipeline-gantt-arrow-layer") as SVGSVGElement | null;
const svgRect = svg?.getBoundingClientRect();
const markerElements = Array.from(root.querySelectorAll("[data-marker-id]")) as HTMLElement[];
const markerById = new Map(markerElements.map((marker) => [marker.getAttribute("data-marker-id") || "", marker]));
return {
candidate: selectedCandidate,
observationArrowCount: arrows.length,
observationSourceMarkerCount: root.querySelectorAll('[data-marker-id^="observation-source:"]').length,
arrowPairs: arrows.map((arrow) => `${arrow.getAttribute("data-source-node-id") || ""}->${arrow.getAttribute("data-target-node-id") || ""}`),
observationArrowTargetInsetsPx: arrows.map((arrow) => {
const path = arrow as SVGPathElement;
const targetMarker = markerById.get(path.getAttribute("data-target-marker-id") || "");
if (!svgRect || !targetMarker || typeof path.getTotalLength !== "function") return null;
const end = path.getPointAtLength(path.getTotalLength());
const markerRect = targetMarker.getBoundingClientRect();
const markerCenterX = markerRect.left + markerRect.width / 2 - svgRect.left;
const markerCenterY = markerRect.top + markerRect.height / 2 - svgRect.top;
return Math.round(Math.hypot(markerCenterX - end.x, markerCenterY - end.y));
}).filter((value) => typeof value === "number"),
liveRunningBarCount: liveBars.length,
liveRunningHeights: liveBars.map((bar) => Math.round(bar.getBoundingClientRect().height)),
runningAnimationNames: liveBars.map((bar) => window.getComputedStyle(bar).animationName),
hasLiveSweep: liveBars.some((bar) => window.getComputedStyle(bar, "::after").animationName.includes("ganttLiveSweep")),
};
}, candidate);
pipelineObservationGanttMetrics = { ...pipelineObservationGanttMetrics, setup: pipelineObservationSetup };
} else {
pipelineObservationGanttMetrics = { ...pipelineObservationGanttMetrics, candidate, setup: pipelineObservationSetup };
}
}
}
if (needMetNonlinear) {
await page.getByRole("button", { name: /MET Nonlinear/ }).click();
@@ -1095,8 +1377,11 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
const microserviceCatalogTextLower = microserviceCatalogText.toLowerCase();
const todoNoteTextLower = todoNoteText.toLowerCase();
const findjobTextLower = findjobText.toLowerCase();
const codexQueueTextLower = codexQueueText.toLowerCase();
const pipelineTextLower = pipelineText.toLowerCase();
const activePipeline = Array.isArray(pipelineSnapshotForFrontend?.pipelines) ? pipelineSnapshotForFrontend.pipelines[0] : null;
const activePipeline = Array.isArray(pipelineSnapshotForFrontend?.pipelines)
? pipelineSnapshotForFrontend.pipelines.find((pipeline: any) => String(pipeline?.id || "") === pipelineSelectedId) || pipelineSnapshotForFrontend.pipelines[0]
: null;
const expectedGanttNodeOrder = activePipeline ? pipelineSnapshotNodeOrder(activePipeline) : [];
const headerIndexByNodeId = new Map(pipelineGanttHeaderNodeOrder.map((nodeId, index) => [nodeId, index]));
const downstreamViolations = activePipeline
@@ -1112,7 +1397,7 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
addSelectedCheck(checks, options, "frontend:sidebar-collapse", railWidthBefore >= 160 && railWidthCollapsed <= 70, { railWidthBefore, railWidthCollapsed });
addSelectedCheck(checks, options, "frontend:mobile-nav-fixed-height", mobileRailHeights.length > 0 && mobileRailMax - mobileRailMin <= 1 && mobileRailMax <= 44, { mobileRailHeights });
addSelectedCheck(checks, options, "frontend:mobile-content-top-aligned", mobileContentMetrics.pageTop <= 190 && mobileContentMetrics.emptyTextOffset <= 14, { mobileContentMetrics });
addSelectedCheck(checks, options, "frontend:pending-task-drilldown", pendingTaskText.includes("待处理任务") && (pendingTaskText.includes("当前无待处理任务") || (pendingTaskText.includes("Provider") && pendingTaskText.includes("已等待"))), { pendingTaskPreview: pendingTaskText.slice(0, 600) });
addSelectedCheck(checks, options, "frontend:pending-task-drilldown", pendingTaskText.includes("待处理任务") && (pendingTaskText.includes("当前无待处理任务") || (pendingTaskText.toLowerCase().includes("provider") && pendingTaskText.includes("已等待"))), { pendingTaskPreview: pendingTaskText.slice(0, 600) });
addSelectedCheck(checks, options, "frontend:task-history-diagnostics", taskHistoryText.includes("任务耗时") && taskHistoryText.includes("诊断信息") && taskHistoryText.includes("失败原因") && taskHistoryText.includes("e2e forced failure for diagnostics"), { taskHistoryPreview: taskHistoryText.slice(0, 900) });
addSelectedCheck(checks, options, "frontend:no-naked-json-before-click", rawBlocksBefore === 0 && !nakedJsonText, { rawBlocksBefore, nakedJsonText });
addSelectedCheck(checks, options, "frontend:raw-json-explicit-button", rawText.includes('"providerId"') && rawText.includes(config.providerGateway.id), { rawTextPreview: rawText.slice(0, 400) });
@@ -1124,12 +1409,22 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
addSelectedCheck(checks, options, "frontend:gateway-duration-subsecond-visible", gatewayHasSubsecondDuration && !gatewayHasRoundedZeroDuration, { gatewayHasSubsecondDuration, gatewayHasRoundedZeroDuration, gatewayTextPreview: gatewayText.slice(0, 900) });
addSelectedCheck(checks, options, "frontend:provider-operation-availability-visible", sshAvailabilityTexts.length >= 1 && upgradeAvailabilityTexts.length >= 1 && sshAvailabilityTexts.every((text) => text.includes("SSH 透传")) && upgradeAvailabilityTexts.every((text) => text.includes("远程更新")) && upgradeAvailabilityTexts.some((text) => text.includes("always-enabled")), { sshAvailabilityTexts, upgradeAvailabilityTexts });
addSelectedCheck(checks, options, "frontend:overview-pgdata-visible", bodyText.includes("PGDATA") && bodyText.includes(config.database.volume), { bodyPreview: bodyText.slice(0, 800) });
addSelectedCheck(checks, options, "frontend:microservice-catalog-visible", microserviceCatalogTextLower.includes("findjob") && microserviceCatalogTextLower.includes("pipeline") && microserviceCatalogTextLower.includes("todo note") && microserviceCatalogTextLower.includes("met nonlinear") && microserviceCatalogText.includes("D601") && microserviceCatalogText.includes(config.providerGateway.id) && microserviceCatalogTextLower.includes("private") && microserviceCatalogText.includes("https://gitee.com/Lyon1998/findjob") && microserviceCatalogText.includes("https://github.com/pikasTech/pipeline") && microserviceCatalogText.includes("https://github.com/pikasTech/met_nonlinear") && microserviceCatalogText.includes("https://gitee.com/Lyon1998/todo_note"), { microserviceCatalogPreview: microserviceCatalogText.slice(0, 1600) });
addSelectedCheck(checks, options, "frontend:microservice-catalog-visible", microserviceCatalogTextLower.includes("findjob") && microserviceCatalogTextLower.includes("pipeline") && microserviceCatalogTextLower.includes("todo note") && microserviceCatalogTextLower.includes("met nonlinear") && microserviceCatalogTextLower.includes("codex queue") && microserviceCatalogText.includes("D601") && microserviceCatalogText.includes(config.providerGateway.id) && microserviceCatalogTextLower.includes("private") && microserviceCatalogText.includes("https://gitee.com/Lyon1998/findjob") && microserviceCatalogText.includes("https://github.com/pikasTech/pipeline") && microserviceCatalogText.includes("https://github.com/pikasTech/met_nonlinear") && microserviceCatalogText.includes("https://gitee.com/Lyon1998/todo_note") && microserviceCatalogText.includes("https://github.com/pikasTech/unidesk"), { microserviceCatalogPreview: microserviceCatalogText.slice(0, 1800) });
addSelectedCheck(checks, options, "frontend:todo-note-integrated-visible", todoNoteTextLower.includes("todo note 工作台") && todoNoteText.includes("CONSTAR") && todoNoteText.includes("大论文") && todoNoteText.includes("UI E2E smoke task") && todoNoteText.includes("撤销") && todoNoteText.includes("重做") && todoNoteText.includes("全部展开") && todoNoteText.includes("仅 UniDesk frontend 代理访问"), { todoNoteTextPreview: todoNoteText.slice(0, 1400) });
addSelectedCheck(checks, options, "frontend:findjob-integrated-visible", findjobTextLower.includes("findjob 工作台".toLowerCase()) && findjobText.includes("岗位总量") && findjobText.includes("D601") && findjobText.includes("近期岗位") && findjobText.includes("仅 UniDesk frontend 代理访问") && /岗位总量\s+\d+/.test(findjobText) && /health\s+ok/i.test(findjobText) && /[1-9]\d*\/[1-9]\d*\s+preview/i.test(findjobText), { findjobTextPreview: findjobText.slice(0, 1200) });
addSelectedCheck(checks, options, "frontend:codex-queue-integrated-visible", codexQueueTextLower.includes("codex queue 控制台".toLowerCase()) && codexQueueText.includes("gpt-5.4-mini") && codexQueueText.includes("提交任务") && codexQueueText.includes("追加 prompt") && codexQueueText.includes("打断") && codexQueueTextLower.includes("attempts") && codexQueueText.includes("仅 UniDesk frontend 代理访问"), { codexQueueTextPreview: codexQueueText.slice(0, 1400) });
addSelectedCheck(checks, options, "frontend:url-route-deeplink", routeInitialPath === "/app/pipeline/" && routeDockerPath === "/nodes/docker/" && routeBackPath === "/app/pipeline/" && routeOverviewPath === "/ops/status/" && routeDeepLinkText.toLowerCase().includes("pipeline v2 工作台".toLowerCase()) && routeOverviewText.includes("核心指标"), { routeInitialPath, routeDockerPath, routeBackIntermediatePath, routeBackPath, routeOverviewPath, routeDeepLinkPreview: routeDeepLinkText.slice(0, 1200), routeOverviewPreview: routeOverviewText.slice(0, 800) });
addSelectedCheck(checks, options, "frontend:pipeline-integrated-visible", pipelineTextLower.includes("pipeline v2 工作台".toLowerCase()) && pipelineText.includes("D601") && pipelineText.includes("控制图") && /epoch\s+甘特图/i.test(pipelineText) && pipelineText.includes("运行材料索引") && pipelineText.includes("仅 UniDesk frontend 代理访问") && /Health\s+OK/i.test(pipelineText) && /组件\s+\d+/.test(pipelineText) && /运行记录\s+[1-9]\d*/.test(pipelineText), { pipelineTextPreview: pipelineText.slice(0, 1200) });
addSelectedCheck(checks, options, "frontend:pipeline-integrated-visible", pipelineTextLower.includes("pipeline v2 工作台".toLowerCase()) && pipelineText.includes("D601") && pipelineText.includes("控制图") && pipelineText.includes("评分器") && /epoch\s+甘特图/i.test(pipelineText) && pipelineText.includes("运行材料索引") && pipelineText.includes("仅 UniDesk frontend 代理访问") && /Health\s+OK/i.test(pipelineText) && /组件\s+\d+/.test(pipelineText) && /运行记录\s+[1-9]\d*/.test(pipelineText), { pipelineTextPreview: pipelineText.slice(0, 1200) });
addSelectedCheck(checks, options, "frontend:pipeline-react-flow-visible", pipelineFlowNodeCount > 0 && pipelineFlowEdgeCount > 0, { pipelineFlowNodeCount, pipelineFlowEdgeCount });
addSelectedCheck(checks, options, "frontend:pipeline-oa-event-flow-visible",
pipelineOaPanelText.toLowerCase().includes("oa flow")
&& pipelineOaPanelText.includes("100%")
&& pipelineOaPanelText.toLowerCase().includes("no-audit")
&& pipelineOaPanelText.toLowerCase().includes("monitor 审核")
&& pipelineOaPanelText.includes("禁止残留")
&& pipelineOaPanelText.includes("policy-in-detail 0")
&& !pipelineOaPanelText.includes("{"),
{ pipelineOaPanelPreview: pipelineOaPanelText.slice(0, 1400) });
addSelectedCheck(checks, options, "frontend:pipeline-gantt-defaults", pipelineGanttScaleLabel.includes("100 px/min") && pipelineGanttAutoHideIdleChecked === false && pipelineGanttHeaderNodeOrder.length > 0 && downstreamViolations.length === 0, {
pipelineGanttScaleLabel,
pipelineGanttAutoHideIdleChecked,
@@ -1137,17 +1432,33 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
expectedGanttNodeOrder,
downstreamViolations,
});
addSelectedCheck(checks, options, "frontend:pipeline-gantt-export",
pipelineGanttExportInfo.downloaded === true
&& Number(pipelineGanttExportInfo.bytes || 0) > 2048
&& /\.(png|svg)$/i.test(String(pipelineGanttExportInfo.suggestedFilename || "")),
{ pipelineGanttExportInfo });
addSelectedCheck(checks, options, "frontend:pipeline-gantt-observation-live-running",
Boolean(pipelineObservationGanttMetrics?.candidate)
&& Number(pipelineObservationGanttMetrics?.observationArrowCount || 0) > 0
&& Number(pipelineObservationGanttMetrics?.observationSourceMarkerCount || 0) === 0
&& (pipelineObservationGanttMetrics?.observationArrowTargetInsetsPx || []).some((value: number) => value >= 8)
&& Number(pipelineObservationGanttMetrics?.liveRunningBarCount || 0) > 0
&& (pipelineObservationGanttMetrics?.liveRunningHeights || []).some((height: number) => height >= 24)
&& (pipelineObservationGanttMetrics?.runningAnimationNames || []).some((name: string) => String(name || "").includes("ganttPulse"))
&& pipelineObservationGanttMetrics?.hasLiveSweep === true,
{ pipelineObservationGanttMetrics });
addSelectedCheck(checks, options, "frontend:pipeline-step-timeline-visible",
pipelineStepTimelineText.includes("OpenCode Step Timeline")
&& pipelineStepTimelineText.includes("时间")
&& pipelineStepTimelineText.includes("工具调用")
&& !pipelineStepTimelineText.includes("{\n")
&& !firstPipelineStepSummaryText.includes("{\n")
&& pipelineSessionHeadText.includes("agent")
&& pipelineSessionHeadText.toLowerCase().includes("model")
&& !firstPipelineStepSummaryText.toLowerCase().includes("tokens")
&& !firstPipelineStepSummaryText.includes("MiniMax-M2.7")
&& !firstPipelineStepSummaryText.includes("\nbuild\n")
&& !pipelineTimelineMetrics.hasHorizontalScroll
&& !pipelineTimelineMetrics.flowConnectorVisible
&& !firstPipelineStepSummaryMetrics.hasHorizontalScroll
&& firstPipelineStepSummaryMetrics.clientHeight <= 190
&& firstPipelineStepExpandedText.toLowerCase().includes("tokens")