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:
@@ -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
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user