fix: preserve code queue prompt observation fields
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# UniDesk E2E Reference
|
||||
|
||||
UniDesk delivery is not complete until the public production frontend, public dev frontend proxy when deployed, public provider ingress, internal core API, PostgreSQL database, local provider-gateway self-connection, and frontend Playwright flow pass the relevant end-to-end checks. The canonical automated command is `bun scripts/cli.ts e2e run`.
|
||||
UniDesk delivery is not complete until the public production frontend, public dev frontend proxy when deployed, public provider ingress, internal core API, PostgreSQL database, local provider-gateway self-connection, and frontend Playwright flow pass the relevant end-to-end checks. The canonical automated command is `bun scripts/cli.ts e2e run`; set `UNIDESK_E2E_FRONTEND=dev` to target the public dev frontend proxy and `UNIDESK_E2E_AUTH_USERNAME` / `UNIDESK_E2E_AUTH_PASSWORD` when that environment uses credentials different from root `config.json`.
|
||||
|
||||
## Required Preconditions
|
||||
|
||||
@@ -35,7 +35,7 @@ Typical targeted commands:
|
||||
- Core API: `docker exec unidesk-backend-core` calls internal `GET /api/overview`, which must report `dbReady: true`, `pgdata.volumeName=unidesk_pgdata_10gb`, a positive PostgreSQL database byte count, and at least one online node; internal `GET /api/performance` must report component request statistics, internal operation statistics, PGDATA usage and Code Queue PostgreSQL storage metadata.
|
||||
- Provider self-connection: internal `GET /api/nodes` must contain `main-server` with `status: online`, `labels.providerGatewayVersion` equal to `src/components/provider-gateway/package.json`, `labels.providerGatewayUpgradePolicy: "always-enabled"`, `labels.providerGatewayRestartPolicyOk: true`, `labels.providerGatewayPidModeOk: true`, and `labels.providerGatewayRuntimeGuardOk: true`; internal `GET /api/nodes/system-status` must contain CPU/memory/disk samples plus a non-empty process resource list sorted by `memoryBytes` by default, where `memoryBytes` should use PSS when `/proc/[pid]/smaps_rollup` is available, otherwise `rssBytes - statm.shared` before raw RSS, and must retain `rssBytes` for diagnostics; internal `GET /api/nodes/docker-status` must contain a Docker snapshot for `main-server`; every running `provider-gateway` container visible in Docker snapshots must report `restartPolicy: "always"` and `pidMode: "host"`; public provider ingress `/health` must return ok.
|
||||
- Provider remote control: internal `/api/dispatch` must successfully complete a real `provider.upgrade` task in `mode: "plan"` so the upgrade path is validated without recreating the running gateway during E2E.
|
||||
- User services: internal `/api/microservices` must include `todo-note` and `oa-event-flow` on `main-server`, canonical `filebrowser` on `D518`, plus `k3sctl-adapter`, `code-queue`, `findjob`, `pipeline`, `met-nonlinear`, `claudeqq` and `filebrowser-d601` on `D601` with `public=false`; `/api/microservices/todo-note/health` must report `storage=postgres`, `/api/microservices/todo-note/proxy/api/instances` must expose the migrated Todo Note lists, and a temporary Todo Note list create/add/toggle/undo/delete cycle must succeed through the real provider-gateway proxy; `/api/microservices/oa-event-flow/health`, `/api/microservices/oa-event-flow/proxy/api/diagnostics`, `/api/microservices/oa-event-flow/proxy/api/events`, `/api/microservices/oa-event-flow/proxy/api/events?tags=service:pipeline` and `/api/microservices/oa-event-flow/proxy/api/stats/trace` must prove the independent OA event table、Pipeline bridge 和 stats center are reachable through UniDesk proxy; `/api/microservices/k3sctl-adapter/health` and `/api/microservices/k3sctl-adapter/proxy/api/control-plane` must expose the D601 `unidesk-k3s` control plane, `kubeApiProxy.mode=kubernetes-api-service-proxy`, D601 active Code Queue instance `servingHealthy=true`, `presentNodeIds` containing `D601`, `missingNodeIds=[]`, `status=healthy`, and `noFallback=true`; `/api/microservices/code-queue/health` must return the active Code Queue backend summary with default model `gpt-5.5`, `egressProxy.connected=true`, and `/api/microservices/code-queue/proxy/api/tasks/overview` must return queue state through backend-core -> k3sctl-adapter -> Kubernetes API service proxy -> k3s/k8s Service, not through a `serviceId=code-queue` provider-gateway direct task or `/api/code-queue-direct`; `/api/microservices/filebrowser/health`, `/api/microservices/filebrowser-d601/health` and `/api/microservices/filebrowser/proxy/` must prove File Browser health and WebUI access through UniDesk proxy; `/api/microservices/findjob/health` and `/api/microservices/findjob/proxy/api/summary` must succeed through the real provider-gateway proxy; `/api/microservices/findjob/proxy/api/jobs?__unideskArrayLimit=jobs:5` must return a bounded preview with `_unidesk.arrayLimits` metadata; `/api/microservices/pipeline/health`, `/api/microservices/pipeline/proxy/api/snapshot?__unideskArrayLimit=registry.components:8,runs:3` and `/api/microservices/pipeline/proxy/api/oa-event-flow/diagnostics` must return Pipeline health, registry/run previews and OA event-flow evidence; `/api/microservices/met-nonlinear/health`, `/api/microservices/met-nonlinear/proxy/api/queue`, `/api/microservices/met-nonlinear/proxy/api/projects?root=projects&limit=500`, `/api/microservices/met-nonlinear/proxy/api/projects?root=ex_projects&limit=500`, `/api/microservices/met-nonlinear/proxy/api/projects/config?path=<projectPath>` and `/api/microservices/met-nonlinear/proxy/api/images` must return the D601 TS backend health, queue/GPU policy, full project tree inputs, structured project detail and ready `met-nonlinear-ml:tf26` image status.
|
||||
- User services: internal `/api/microservices` must include `todo-note` and `oa-event-flow` on `main-server`, canonical `filebrowser` on `D518`, plus `k3sctl-adapter`, `code-queue`, `findjob`, `pipeline`, `met-nonlinear`, `claudeqq` and `filebrowser-d601` on `D601` with `public=false`; `/api/microservices/todo-note/health` must report `storage=postgres`, `/api/microservices/todo-note/proxy/api/instances` must expose the migrated Todo Note lists, and a temporary Todo Note list create/add/toggle/undo/delete cycle must succeed through the real provider-gateway proxy; `/api/microservices/oa-event-flow/health`, `/api/microservices/oa-event-flow/proxy/api/diagnostics`, `/api/microservices/oa-event-flow/proxy/api/events`, `/api/microservices/oa-event-flow/proxy/api/events?tags=service:pipeline` and `/api/microservices/oa-event-flow/proxy/api/stats/trace` must prove the independent OA event table、Pipeline bridge 和 stats center are reachable through UniDesk proxy; `/api/microservices/k3sctl-adapter/health` and `/api/microservices/k3sctl-adapter/proxy/api/control-plane` must expose the D601 `unidesk-k3s` control plane, `kubeApiProxy.mode=kubernetes-api-service-proxy`, D601 active Code Queue instance `servingHealthy=true`, `presentNodeIds` containing `D601`, `missingNodeIds=[]`, `status=healthy`, and `noFallback=true`; `/api/microservices/code-queue/health` must return the active Code Queue backend summary with default model `gpt-5.5`, `egressProxy.connected=true`, and `/api/microservices/code-queue/proxy/api/tasks/overview` must return queue state through backend-core -> k3sctl-adapter -> Kubernetes API service proxy -> k3s/k8s Service, not through a `serviceId=code-queue` provider-gateway direct task or `/api/code-queue-direct`; Code Queue raw prompt observation fields must preserve long prompt tails across create/list/detail/frontend paths, with any shortened text exposed only through explicit `*Preview` objects carrying `chars` and `truncated`; `/api/microservices/filebrowser/health`, `/api/microservices/filebrowser-d601/health` and `/api/microservices/filebrowser/proxy/` must prove File Browser health and WebUI access through UniDesk proxy; `/api/microservices/findjob/health` and `/api/microservices/findjob/proxy/api/summary` must succeed through the real provider-gateway proxy; `/api/microservices/findjob/proxy/api/jobs?__unideskArrayLimit=jobs:5` must return a bounded preview with `_unidesk.arrayLimits` metadata; `/api/microservices/pipeline/health`, `/api/microservices/pipeline/proxy/api/snapshot?__unideskArrayLimit=registry.components:8,runs:3` and `/api/microservices/pipeline/proxy/api/oa-event-flow/diagnostics` must return Pipeline health, registry/run previews and OA event-flow evidence; `/api/microservices/met-nonlinear/health`, `/api/microservices/met-nonlinear/proxy/api/queue`, `/api/microservices/met-nonlinear/proxy/api/projects?root=projects&limit=500`, `/api/microservices/met-nonlinear/proxy/api/projects?root=ex_projects&limit=500`, `/api/microservices/met-nonlinear/proxy/api/projects/config?path=<projectPath>` and `/api/microservices/met-nonlinear/proxy/api/images` must return the D601 TS backend health, queue/GPU policy, full project tree inputs, structured project detail and ready `met-nonlinear-ml:tf26` image status.
|
||||
- ClaudeQQ availability: `/api/microservices/claudeqq/health` must only pass when `ready=true`, NapCat HTTP and WebSocket are connected, and `napcat.loginState=logged_in`; `/api/microservices/claudeqq/proxy/api/napcat/login` must show the same logged-in account state and `/api/microservices/claudeqq/proxy/api/events/recent` must prove the backend can read the persistent event cache. A QR-code-only or not-logged-in NapCat state must be treated as unhealthy.
|
||||
- Database: the command writes an `unidesk_e2e_markers` row through `docker exec unidesk-database psql`, confirms provider state is stored in PostgreSQL, and checks Todo Note rows exist in `todo_note_instances` using the same named volume.
|
||||
- Pipeline OA event flow: `microservice:pipeline-oa-event-flow` must prove both no-audit and monitor-audit runs are driven by OA events end to end. The event stream must show `node-finished` as a neutral fact with `pipeline:{pipelineId}` and `epoch:{runId}` tags, OA policy as the source of downstream/audit decisions, monitor decisions as OA control events, and runner control-result evidence. E2E must fail if delivery still depends on a legacy detail audit policy flag as policy authority, independent legacy audit-request points, a legacy batch completion gate, direct monitor-to-runner calls, or frontend/CLI writes to Pipeline `.state`.
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import { codeQueuePromptResponseFields, type JsonRecord } from "../src/components/microservices/code-queue-mgr/src/prompt-observation";
|
||||
|
||||
function assertCondition(condition: unknown, message: string, detail: JsonRecord = {}): void {
|
||||
if (!condition) {
|
||||
throw new Error(`${message}: ${JSON.stringify(detail)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function previewText(value: unknown): string {
|
||||
if (typeof value === "string") return value;
|
||||
if (value && typeof value === "object" && !Array.isArray(value) && typeof (value as { text?: unknown }).text === "string") {
|
||||
return (value as { text: string }).text;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function taskDisplayPrompt(task: JsonRecord, preview = false): string {
|
||||
if (preview) {
|
||||
const previewValue = previewText(task.displayPromptPreview || task.basePromptPreview || task.promptPreview);
|
||||
if (previewValue.length > 0) return previewValue;
|
||||
}
|
||||
const displayPrompt = String(task.displayPrompt || "");
|
||||
if (displayPrompt.length > 0) return displayPrompt;
|
||||
return String(task.prompt || "");
|
||||
}
|
||||
|
||||
function taskBasePromptText(task: JsonRecord): string {
|
||||
const basePrompt = String(task.basePrompt || "");
|
||||
return basePrompt.length > 0 ? basePrompt : taskDisplayPrompt(task);
|
||||
}
|
||||
|
||||
function buildLongPrompt(marker: string): string {
|
||||
const filler = Array.from(
|
||||
{ length: 76 },
|
||||
(_, index) => `long-prompt-line-${String(index + 1).padStart(2, "0")} ${"abcdefghijklmnopqrstuvwxyz0123456789".repeat(2)}`,
|
||||
).join("\n");
|
||||
return [
|
||||
"Code Queue long prompt observation contract",
|
||||
"",
|
||||
filler,
|
||||
"",
|
||||
"验收标准:",
|
||||
`所有原始 prompt 观察层必须保留这个唯一 tail marker: ${marker}`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export function runCodeQueuePromptObservationContract(): JsonRecord {
|
||||
const marker = `CODE_QUEUE_PROMPT_OBSERVATION_TAIL_${Date.now()}`;
|
||||
const prompt = buildLongPrompt(marker);
|
||||
assertCondition(prompt.length > 2500, "long prompt fixture must exceed 2500 chars", { promptChars: prompt.length });
|
||||
|
||||
const task = { prompt, basePrompt: prompt };
|
||||
const createTask = codeQueuePromptResponseFields(task, { lite: false, userPromptForDisplay: (value) => value });
|
||||
const overviewTask = codeQueuePromptResponseFields(task, { lite: true, userPromptForDisplay: (value) => value });
|
||||
const metaTask: JsonRecord = {
|
||||
...codeQueuePromptResponseFields(task, { lite: false, userPromptForDisplay: (value) => value }),
|
||||
initialPrompt: prompt,
|
||||
};
|
||||
const frontendCardPrompt = taskDisplayPrompt(overviewTask, true);
|
||||
const frontendFullPrompt = taskBasePromptText(metaTask);
|
||||
const rawFields = [
|
||||
createTask.prompt,
|
||||
createTask.basePrompt,
|
||||
createTask.displayPrompt,
|
||||
overviewTask.prompt,
|
||||
overviewTask.basePrompt,
|
||||
overviewTask.displayPrompt,
|
||||
metaTask.prompt,
|
||||
metaTask.basePrompt,
|
||||
metaTask.displayPrompt,
|
||||
metaTask.initialPrompt,
|
||||
frontendFullPrompt,
|
||||
].map((value) => String(value || ""));
|
||||
const previews = [
|
||||
createTask.promptPreview,
|
||||
createTask.basePromptPreview,
|
||||
createTask.displayPromptPreview,
|
||||
overviewTask.promptPreview,
|
||||
overviewTask.basePromptPreview,
|
||||
overviewTask.displayPromptPreview,
|
||||
] as JsonRecord[];
|
||||
|
||||
assertCondition(rawFields.every((value) => value.includes(marker)), "raw prompt fields must include tail marker", {
|
||||
marker,
|
||||
lengths: rawFields.map((value) => value.length),
|
||||
});
|
||||
assertCondition(rawFields.every((value) => !value.includes("...<truncated>")), "raw prompt fields must not contain truncation marker");
|
||||
assertCondition(previews.every((value) => typeof value?.text === "string" && typeof value?.chars === "number" && typeof value?.truncated === "boolean"), "preview fields must be explicit metadata objects");
|
||||
assertCondition(previews.some((value) => value.truncated === true && String(value.text || "").includes("...<truncated>")), "preview fields may truncate explicitly");
|
||||
assertCondition(!frontendCardPrompt.includes(marker) && frontendCardPrompt.includes("...<truncated>"), "frontend card summary should use explicit preview text", { frontendCardChars: frontendCardPrompt.length });
|
||||
assertCondition(frontendFullPrompt.includes(marker), "frontend full prompt expansion source must include tail marker", { frontendFullChars: frontendFullPrompt.length });
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
marker,
|
||||
promptChars: prompt.length,
|
||||
createPromptChars: String(createTask.prompt || "").length,
|
||||
overviewPromptChars: String(overviewTask.prompt || "").length,
|
||||
metaDisplayPromptChars: String(metaTask.displayPrompt || "").length,
|
||||
frontendCardChars: frontendCardPrompt.length,
|
||||
frontendFullChars: frontendFullPrompt.length,
|
||||
previewTruncated: previews.some((value) => value.truncated === true),
|
||||
};
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
process.stdout.write(`${JSON.stringify(runCodeQueuePromptObservationContract(), null, 2)}\n`);
|
||||
}
|
||||
@@ -234,8 +234,10 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default
|
||||
fileItem("src/components/microservices/mdtodo/src/index.ts"),
|
||||
fileItem("src/components/microservices/decision-center/src/index.ts"),
|
||||
fileItem("src/components/microservices/code-queue-mgr/src/index.ts"),
|
||||
fileItem("src/components/microservices/code-queue-mgr/src/prompt-observation.ts"),
|
||||
fileItem("scripts/src/deploy.ts"),
|
||||
fileItem("scripts/src/e2e.ts"),
|
||||
fileItem("scripts/code-queue-prompt-observation-test.ts"),
|
||||
fileItem("scripts/src/artifact-registry.ts"),
|
||||
);
|
||||
} else {
|
||||
@@ -243,8 +245,10 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default
|
||||
}
|
||||
if (options.scriptsTypecheck) {
|
||||
items.push(commandItem("typescript:scripts", ["bunx", "tsc", "-p", "scripts/tsconfig.json", "--noEmit", "--pretty", "false"], 120_000));
|
||||
items.push(commandItem("code-queue:prompt-observation-contract", ["bun", "scripts/code-queue-prompt-observation-test.ts"], 30_000));
|
||||
} else {
|
||||
items.push(skippedItem("typescript:scripts", "scripts TypeScript typecheck is opt-in", "--scripts-typecheck or --full"));
|
||||
items.push(skippedItem("code-queue:prompt-observation-contract", "prompt observation contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
}
|
||||
if (options.logs) {
|
||||
items.push(unifiedLogRotationItem());
|
||||
|
||||
@@ -228,6 +228,8 @@ function compactSummary(summary: unknown, options: CodexTaskOptions, taskId: str
|
||||
const transcriptCount = asNumber(record.transcriptCount, 0);
|
||||
const transcriptMaxSeq = transcriptCount > 0 ? record.transcriptMaxSeq ?? null : null;
|
||||
const initialPrompt = asString(record.initialPrompt ?? record.prompt);
|
||||
const initialPromptView = textView(initialPrompt, options.full, 3000);
|
||||
const basePromptView = textView(asString(record.basePrompt), options.full, 2000);
|
||||
return {
|
||||
id: record.id ?? taskId,
|
||||
queueId: record.queueId ?? null,
|
||||
@@ -256,8 +258,9 @@ function compactSummary(summary: unknown, options: CodexTaskOptions, taskId: str
|
||||
startedAt: record.startedAt ?? null,
|
||||
updatedAt: record.updatedAt ?? null,
|
||||
finishedAt: record.finishedAt ?? null,
|
||||
initialPrompt: textView(initialPrompt, options.full, 3000),
|
||||
basePrompt: textView(asString(record.basePrompt), options.full, 2000),
|
||||
...(options.full
|
||||
? { initialPrompt: initialPromptView, basePrompt: basePromptView }
|
||||
: { initialPromptPreview: initialPromptView, basePromptPreview: basePromptView }),
|
||||
referenceTaskIds: record.referenceTaskIds ?? [],
|
||||
referenceInjection: record.referenceInjection ?? null,
|
||||
lastAssistantMessage: compactLastAssistant(record.lastAssistantMessage, options.full),
|
||||
@@ -825,7 +828,7 @@ function compactTaskMutationResponse(task: unknown, options: CompactTaskMutation
|
||||
startedAt: record.startedAt ?? null,
|
||||
updatedAt: record.updatedAt ?? null,
|
||||
finishedAt: record.finishedAt ?? null,
|
||||
prompt: textView(prompt, options.fullPrompt === true, 1200),
|
||||
...(options.fullPrompt === true ? { prompt: textView(prompt, true, 1200) } : { promptPreview: textView(prompt, false, 1200) }),
|
||||
commands: taskId.length === 0 ? null : {
|
||||
show: `bun scripts/cli.ts codex task ${taskId}`,
|
||||
trace: `bun scripts/cli.ts codex task ${taskId} --trace --tail --limit ${defaultTraceLimit}`,
|
||||
|
||||
+169
-3
@@ -139,6 +139,7 @@ const FRONTEND_CHECK_NAMES = [
|
||||
"frontend:code-queue-integrated-visible",
|
||||
"frontend:code-queue-enqueue-await-smoke",
|
||||
"frontend:code-queue-summary-mobile-wrap",
|
||||
"frontend:code-queue-long-prompt-observation",
|
||||
"frontend:code-queue-initial-prompt-full-expand",
|
||||
"frontend:code-queue-trace-full-load",
|
||||
"frontend:code-queue-judge-wrap",
|
||||
@@ -486,8 +487,10 @@ function wantsPrefix(options: E2ERunOptions, prefix: string): boolean {
|
||||
}
|
||||
|
||||
function publicUrls(config: UniDeskConfig): PublicUrls {
|
||||
const frontendTarget = String(process.env.UNIDESK_E2E_FRONTEND || "prod").trim().toLowerCase();
|
||||
const frontendPort = frontendTarget === "dev" ? config.network.devFrontend.port : config.network.frontend.port;
|
||||
return {
|
||||
frontendUrl: `http://${config.network.publicHost}:${config.network.frontend.port}`,
|
||||
frontendUrl: `http://${config.network.publicHost}:${frontendPort}`,
|
||||
providerIngressHealthUrl: `http://${config.network.publicHost}:${config.network.providerIngress.port}/health`,
|
||||
providerIngressWsUrl: `ws://${config.network.publicHost}:${config.network.providerIngress.port}/ws/provider`,
|
||||
blockedCoreUrl: `http://${config.network.publicHost}:${config.network.core.port}`,
|
||||
@@ -750,6 +753,148 @@ async function runCodeQueueEnqueueAwaitSmoke(page: Page): Promise<any> {
|
||||
}
|
||||
}
|
||||
|
||||
async function runCodeQueueLongPromptObservation(page: Page): Promise<any> {
|
||||
const marker = `E2E_LONG_PROMPT_TAIL_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`;
|
||||
const submitQueueId = "e2e-long-prompt";
|
||||
const filler = Array.from({ length: 76 }, (_, index) => `long-prompt-line-${String(index + 1).padStart(2, "0")} ${"abcdefghijklmnopqrstuvwxyz0123456789".repeat(2)}`).join("\n");
|
||||
const prompt = [
|
||||
"Code Queue long prompt observation regression",
|
||||
"",
|
||||
filler,
|
||||
"",
|
||||
"验收标准:",
|
||||
`必须在所有原始 prompt 观察层保留这个唯一 tail marker: ${marker}`,
|
||||
].join("\n");
|
||||
const promptChars = prompt.length;
|
||||
if (promptChars <= 2500) throw new Error(`long prompt fixture too short: ${promptChars}`);
|
||||
const apiFetch = async (path: string, init: RequestInit = {}): Promise<any> => {
|
||||
const response = await fetch(path, {
|
||||
credentials: "same-origin",
|
||||
headers: { "content-type": "application/json", ...(init.headers || {}) },
|
||||
...init,
|
||||
});
|
||||
const text = await response.text();
|
||||
let body: any = null;
|
||||
try { body = text.length > 0 ? JSON.parse(text) : null; } catch { body = { text }; }
|
||||
return { ok: response.ok, status: response.status, body };
|
||||
};
|
||||
|
||||
await page.getByTestId("code-queue-filter-select").selectOption("__all__").catch(() => undefined);
|
||||
await page.getByTestId("code-queue-id-select").selectOption(submitQueueId).catch(async () => {
|
||||
page.once("dialog", (dialog) => { void dialog.accept(submitQueueId); });
|
||||
await page.getByTestId("codex-create-queue-button").click();
|
||||
await page.waitForFunction((queueId) => {
|
||||
const select = document.querySelector('[data-testid="code-queue-id-select"]') as HTMLSelectElement | null;
|
||||
return Array.from(select?.options || []).some((option) => option.value === queueId);
|
||||
}, submitQueueId, { timeout: 10000 });
|
||||
await page.getByTestId("code-queue-id-select").selectOption(submitQueueId);
|
||||
});
|
||||
await page.getByTestId("codex-max-attempts-input").fill("1");
|
||||
await page.getByTestId("codex-repeat-count-input").fill("1");
|
||||
await page.locator('[data-testid="code-queue-task-form"] textarea').fill(prompt);
|
||||
await page.waitForFunction(() => {
|
||||
const button = document.querySelector('[data-testid="codex-enqueue-button"]') as HTMLButtonElement | null;
|
||||
return button !== null && !button.disabled;
|
||||
}, undefined, { timeout: 5000 });
|
||||
const responsePromise = page.waitForResponse((response) => isCodeQueueTaskEnqueueRequest(response.url(), response.request().method()), { timeout: 30000 });
|
||||
await page.getByTestId("codex-enqueue-button").click();
|
||||
const response = await responsePromise;
|
||||
const createBody = await response.json().catch((error: unknown) => ({ parseError: error instanceof Error ? error.message : String(error) }));
|
||||
const createdTask = Array.isArray(createBody?.tasks) ? createBody.tasks[0] : null;
|
||||
const taskId = String(createdTask?.id || "");
|
||||
if (!taskId) return { checked: true, marker, promptChars, createStatus: response.status(), taskId: "", error: "missing task id", createBody };
|
||||
await page.waitForSelector(`[data-testid="codex-task-${taskId}"]`, { timeout: 30000 }).catch(() => undefined);
|
||||
|
||||
const overview = await page.evaluate(async (id) => {
|
||||
const response = await fetch(`/api/microservices/code-queue/proxy/api/tasks/overview?limit=120&transcriptLimit=0&compact=1&selected=1&includeActive=1&stats=0&afterSeq=0&preferId=${encodeURIComponent(String(id))}`, { credentials: "same-origin" });
|
||||
const body = await response.json().catch(() => null);
|
||||
return { ok: response.ok, status: response.status, body };
|
||||
}, taskId);
|
||||
const meta = await page.evaluate(async (id) => {
|
||||
const response = await fetch(`/api/microservices/code-queue/proxy/api/tasks/${encodeURIComponent(String(id))}?meta=1`, { credentials: "same-origin" });
|
||||
const body = await response.json().catch(() => null);
|
||||
return { ok: response.ok, status: response.status, body };
|
||||
}, taskId);
|
||||
const initialPrompt = await page.evaluate(async (id) => {
|
||||
const response = await fetch(`/api/microservices/code-queue/proxy/api/tasks/${encodeURIComponent(String(id))}/prompt?part=initial`, { credentials: "same-origin" });
|
||||
const body = await response.json().catch(() => null);
|
||||
return { ok: response.ok, status: response.status, body };
|
||||
}, taskId);
|
||||
|
||||
await page.getByTestId(`codex-task-${taskId}`).click().catch(() => undefined);
|
||||
await page.waitForSelector('[data-testid="codex-initial-prompt-base"]', { timeout: 15000 }).catch(() => undefined);
|
||||
const ui = await page.evaluate(() => {
|
||||
const base = document.querySelector('[data-testid="codex-initial-prompt-base"]') as HTMLElement | null;
|
||||
return {
|
||||
baseText: base?.innerText || "",
|
||||
baseChars: (base?.innerText || "").length,
|
||||
};
|
||||
});
|
||||
|
||||
const overviewTasks = Array.isArray(overview.body?.tasks) ? overview.body.tasks : [];
|
||||
const overviewTask = overviewTasks.find((task: any) => String(task?.id || "") === taskId) || null;
|
||||
const selectedTask = overview.body?.selected?.task || null;
|
||||
const metaTask = meta.body?.task || null;
|
||||
const createPrompt = String(createdTask?.prompt || "");
|
||||
const createDisplayPrompt = String(createdTask?.displayPrompt || "");
|
||||
const createPreview = createdTask?.displayPromptPreview || createdTask?.promptPreview || null;
|
||||
const overviewPrompt = String(overviewTask?.prompt || "");
|
||||
const overviewDisplayPrompt = String(overviewTask?.displayPrompt || "");
|
||||
const overviewPreview = overviewTask?.displayPromptPreview || overviewTask?.promptPreview || null;
|
||||
const selectedDisplayPrompt = String(selectedTask?.displayPrompt || "");
|
||||
const selectedPreview = selectedTask?.displayPromptPreview || selectedTask?.promptPreview || null;
|
||||
const metaPrompt = String(metaTask?.prompt || "");
|
||||
const metaBasePrompt = String(metaTask?.basePrompt || "");
|
||||
const metaDisplayPrompt = String(metaTask?.displayPrompt || "");
|
||||
const promptText = String(initialPrompt.body?.text || "");
|
||||
const previewHasTruncationMarker = [createPreview?.text, overviewPreview?.text, selectedPreview?.text].some((value) => String(value || "").includes("...<truncated>"));
|
||||
const originalValues = [createPrompt, createDisplayPrompt, overviewPrompt, overviewDisplayPrompt, selectedDisplayPrompt, metaPrompt, metaBasePrompt, metaDisplayPrompt, promptText];
|
||||
await apiFetch(`/api/microservices/code-queue/proxy/api/tasks/${encodeURIComponent(taskId)}/interrupt`, { method: "POST", body: "{}" }).catch(() => null);
|
||||
return {
|
||||
checked: true,
|
||||
marker,
|
||||
taskId,
|
||||
promptChars,
|
||||
createStatus: response.status(),
|
||||
createOk: createBody?.ok === true,
|
||||
createPromptChars: createPrompt.length,
|
||||
createDisplayPromptChars: createDisplayPrompt.length,
|
||||
overviewStatus: overview.status,
|
||||
overviewTaskFound: overviewTask !== null,
|
||||
overviewPromptChars: overviewPrompt.length,
|
||||
overviewDisplayPromptChars: overviewDisplayPrompt.length,
|
||||
selectedTaskFound: selectedTask !== null && String(selectedTask?.id || "") === taskId,
|
||||
selectedDisplayPromptChars: selectedDisplayPrompt.length,
|
||||
metaStatus: meta.status,
|
||||
metaPromptChars: metaPrompt.length,
|
||||
metaBasePromptChars: metaBasePrompt.length,
|
||||
metaDisplayPromptChars: metaDisplayPrompt.length,
|
||||
initialPromptStatus: initialPrompt.status,
|
||||
initialPromptChars: Number(initialPrompt.body?.chars || promptText.length),
|
||||
uiBaseChars: ui.baseChars,
|
||||
preview: {
|
||||
create: createPreview,
|
||||
overview: overviewPreview,
|
||||
selected: selectedPreview,
|
||||
previewHasTruncationMarker,
|
||||
createPromptPreviewFieldPresent: createdTask?.promptPreview !== undefined,
|
||||
overviewPromptPreviewFieldPresent: overviewTask?.promptPreview !== undefined,
|
||||
selectedPromptPreviewFieldPresent: selectedTask?.promptPreview !== undefined,
|
||||
},
|
||||
checks: {
|
||||
createOriginalHasTail: createPrompt.includes(marker) && createDisplayPrompt.includes(marker),
|
||||
overviewOriginalHasTail: overviewPrompt.includes(marker) && overviewDisplayPrompt.includes(marker),
|
||||
selectedOriginalHasTail: selectedDisplayPrompt.includes(marker),
|
||||
metaOriginalHasTail: metaPrompt.includes(marker) && metaBasePrompt.includes(marker) && metaDisplayPrompt.includes(marker),
|
||||
initialPromptHasTail: promptText.includes(marker),
|
||||
uiFullPromptHasTail: String(ui.baseText || "").includes(marker),
|
||||
originalsHaveNoTruncationMarker: originalValues.every((value) => !value.includes("...<truncated>")),
|
||||
previewFieldsExplicit: createdTask?.promptPreview !== undefined && overviewTask?.promptPreview !== undefined && selectedTask?.promptPreview !== undefined,
|
||||
previewCanTruncate: Boolean(createPreview?.truncated || overviewPreview?.truncated || selectedPreview?.truncated),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function runPsql(config: UniDeskConfig, sql: string): { ok: boolean; stdout: string; stderr: string; exitCode: number | null } {
|
||||
const result = runCommand([
|
||||
"docker",
|
||||
@@ -1486,8 +1631,10 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
|
||||
page.on("dialog", (dialog) => dialog.accept());
|
||||
await page.goto(urls.frontendUrl, { waitUntil: "domcontentloaded", timeout: 15000 });
|
||||
await page.waitForSelector('[data-testid="login-screen"]', { timeout: 10000 });
|
||||
await page.fill('input[name="username"]', config.auth.username);
|
||||
await page.fill('input[name="password"]', config.auth.password);
|
||||
const frontendAuthUsername = process.env.UNIDESK_E2E_AUTH_USERNAME || config.auth.username;
|
||||
const frontendAuthPassword = process.env.UNIDESK_E2E_AUTH_PASSWORD || config.auth.password;
|
||||
await page.fill('input[name="username"]', frontendAuthUsername);
|
||||
await page.fill('input[name="password"]', frontendAuthPassword);
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForSelector('[data-testid="app-shell"]', { timeout: 10000 });
|
||||
await page.waitForFunction(() => document.querySelector('[data-testid="conn-text"]')?.textContent?.includes("核心在线"), undefined, { timeout: 15000 });
|
||||
@@ -1547,6 +1694,7 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
|
||||
let codeQueueSidebarUpdateMetrics: any = { cardCount: 0, labels: [], hasRecentUpdateLabel: false };
|
||||
let codeQueueHtmlGuard: any = { rootAttrMissing: false, sourceAttrMissing: false, sourceNoBasePrompt: false };
|
||||
let codeQueueSummaryMobileMetrics: any = { checked: false, summaryCount: 0, ok: false };
|
||||
let codeQueueLongPromptMetrics: any = { checked: false };
|
||||
let codeQueuePromptDefaultEmpty = false;
|
||||
let codeQueueSubmitGuard: any = { batchRowVisible: false, disabledBeforeConfirm: false, enabledAfterConfirm: false, waitElementMissingBeforeSubmit: false };
|
||||
let codeQueueEnqueueAwaitSmoke: any = { checked: false };
|
||||
@@ -1995,6 +2143,9 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
|
||||
if (wants("frontend:code-queue-enqueue-await-smoke")) {
|
||||
codeQueueEnqueueAwaitSmoke = await runCodeQueueEnqueueAwaitSmoke(page);
|
||||
}
|
||||
if (wants("frontend:code-queue-long-prompt-observation")) {
|
||||
codeQueueLongPromptMetrics = await runCodeQueueLongPromptObservation(page);
|
||||
}
|
||||
codeQueueOptions = await page.locator('[data-testid="code-queue-filter-select"] option').evaluateAll((options) => options.map((option) => (option as HTMLOptionElement).textContent || ""));
|
||||
codeQueueSwitchMetrics = await page.locator('[data-testid="code-queue-filter-select"] option').evaluateAll((options) => ({
|
||||
optionCount: options.length,
|
||||
@@ -3049,6 +3200,21 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
|
||||
&& (codeQueueEnqueueAwaitSmoke.interrupt?.ok === true || codeQueueEnqueueAwaitSmoke.interrupt?.status === 409),
|
||||
{ codeQueueEnqueueAwaitSmoke });
|
||||
addSelectedCheck(checks, options, "frontend:code-queue-summary-mobile-wrap", codeQueueSummaryMobileMetrics.checked === true && (codeQueueSummaryMobileMetrics.summaryCount === 0 || codeQueueSummaryMobileMetrics.ok === true), { codeQueueSummaryMobileMetrics });
|
||||
addSelectedCheck(checks, options, "frontend:code-queue-long-prompt-observation",
|
||||
codeQueueLongPromptMetrics.checked === true
|
||||
&& codeQueueLongPromptMetrics.createOk === true
|
||||
&& /^codex_\d+_[A-Za-z0-9_-]+$/u.test(String(codeQueueLongPromptMetrics.taskId || ""))
|
||||
&& Number(codeQueueLongPromptMetrics.promptChars || 0) > 2500
|
||||
&& codeQueueLongPromptMetrics.checks?.createOriginalHasTail === true
|
||||
&& codeQueueLongPromptMetrics.checks?.overviewOriginalHasTail === true
|
||||
&& codeQueueLongPromptMetrics.checks?.selectedOriginalHasTail === true
|
||||
&& codeQueueLongPromptMetrics.checks?.metaOriginalHasTail === true
|
||||
&& codeQueueLongPromptMetrics.checks?.initialPromptHasTail === true
|
||||
&& codeQueueLongPromptMetrics.checks?.uiFullPromptHasTail === true
|
||||
&& codeQueueLongPromptMetrics.checks?.originalsHaveNoTruncationMarker === true
|
||||
&& codeQueueLongPromptMetrics.checks?.previewFieldsExplicit === true
|
||||
&& codeQueueLongPromptMetrics.checks?.previewCanTruncate === true,
|
||||
{ codeQueueLongPromptMetrics });
|
||||
addSelectedCheck(checks, options, "frontend:code-queue-error-red-markers", codeQueueErrorHighlightMetrics.checked === true && codeQueueErrorHighlightMetrics.candidateFound === true && codeQueueErrorHighlightMetrics.ok === true, { codeQueueErrorHighlightMetrics });
|
||||
addSelectedCheck(checks, options, "frontend:code-queue-initial-prompt-full-expand",
|
||||
codexInitialPromptFullMetrics.candidateFound === false
|
||||
|
||||
@@ -535,7 +535,17 @@ function promptLineCount(text: string): number {
|
||||
return text.length > 0 ? text.split(/\r\n|\r|\n/u).length : 0;
|
||||
}
|
||||
|
||||
function taskDisplayPrompt(task: any): string {
|
||||
function previewTextField(value: any): string {
|
||||
if (typeof value === "string") return value;
|
||||
if (value && typeof value === "object" && !Array.isArray(value) && typeof value.text === "string") return value.text;
|
||||
return "";
|
||||
}
|
||||
|
||||
function taskDisplayPrompt(task: any, options: AnyRecord = {}): string {
|
||||
if (options.preview === true) {
|
||||
const preview = previewTextField(task?.displayPromptPreview || task?.basePromptPreview || task?.promptPreview);
|
||||
if (preview.length > 0) return preview;
|
||||
}
|
||||
const explicit = String(task?.displayPrompt || "");
|
||||
if (explicit.length > 0) return explicit;
|
||||
const prompt = String(task?.prompt || "");
|
||||
@@ -640,7 +650,9 @@ function traceSummaryIsCurrent(task: any): boolean {
|
||||
function taskBasePromptText(task: any): string {
|
||||
const summaryPrompt = taskPromptSummary(task);
|
||||
const basePrompt = String(summaryPrompt.basePrompt || "");
|
||||
return basePrompt.length > 0 ? basePrompt : taskDisplayPrompt(task);
|
||||
if (basePrompt.length > 0) return basePrompt;
|
||||
const direct = String(task?.basePrompt || "");
|
||||
return direct.length > 0 ? direct : taskDisplayPrompt(task);
|
||||
}
|
||||
|
||||
function taskFinalResponseText(task: any): string {
|
||||
@@ -1473,7 +1485,7 @@ function TaskCard({ task, selected, onSelect, onCopy, onReference, onMarkRead, c
|
||||
}, markingRead ? "标记中" : "标为已读") : null,
|
||||
),
|
||||
),
|
||||
h("strong", null, shortText(taskDisplayPrompt(task), 120) || "空任务"),
|
||||
h("strong", null, shortText(taskDisplayPrompt(task, { preview: true }), 120) || "空任务"),
|
||||
h("div", { className: "codex-task-meta" },
|
||||
h("span", null, `queue=${taskQueueLabel(task)}`),
|
||||
h("span", null, `provider=${task?.providerId || "D601"}`),
|
||||
@@ -2232,6 +2244,9 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi
|
||||
task?.displayPrompt,
|
||||
task?.basePrompt,
|
||||
task?.prompt,
|
||||
previewTextField(task?.displayPromptPreview),
|
||||
previewTextField(task?.basePromptPreview),
|
||||
previewTextField(task?.promptPreview),
|
||||
task?.finalResponse,
|
||||
task?.lastError?.message,
|
||||
].map((value) => String(value || "").toLowerCase()).join("\n");
|
||||
|
||||
@@ -497,14 +497,15 @@ fn preview(text: &str, max_chars: usize) -> String {
|
||||
result
|
||||
}
|
||||
|
||||
fn prefix_preview(text: &str, max_chars: usize) -> String {
|
||||
if text.chars().count() <= max_chars {
|
||||
return text.to_string();
|
||||
}
|
||||
let take = max_chars.saturating_sub(1);
|
||||
let mut result = text.chars().take(take).collect::<String>();
|
||||
result.push('…');
|
||||
result
|
||||
fn text_preview(value: &str, max_chars: usize) -> Value {
|
||||
let chars = value.chars().count();
|
||||
let truncated = chars > max_chars;
|
||||
json!({
|
||||
"text": if truncated { preview(value, max_chars) } else { value.to_string() },
|
||||
"chars": chars,
|
||||
"truncated": truncated,
|
||||
"omittedChars": if truncated { chars.saturating_sub(max_chars.saturating_sub(20)) } else { 0 }
|
||||
})
|
||||
}
|
||||
|
||||
fn strip_after_marker(text: &str, marker: &str) -> Option<String> {
|
||||
@@ -664,13 +665,17 @@ fn task_list_response(_state: &AppState, task: &TaskMeta, lite: bool) -> Value {
|
||||
};
|
||||
let final_text = final_response(task);
|
||||
let agent_port = code_agent_port(&task.model);
|
||||
let preview_limit = if lite { 360 } else { 2000 };
|
||||
json!({
|
||||
"id": task.id,
|
||||
"queueId": task.queue_id,
|
||||
"queueEnteredAt": task.task_json.as_ref().and_then(|value| value.get("queueEnteredAt")).and_then(Value::as_str).unwrap_or(&task.created_at),
|
||||
"prompt": if lite { prefix_preview(&display_prompt, 360) } else { preview(&task.prompt, 2000) },
|
||||
"basePrompt": if lite { prefix_preview(&task.base_prompt, 360) } else { preview(&task.base_prompt, 2000) },
|
||||
"displayPrompt": if lite { prefix_preview(&display_prompt, 360) } else { preview(&display_prompt, 2000) },
|
||||
"prompt": task.prompt,
|
||||
"basePrompt": task.base_prompt,
|
||||
"displayPrompt": display_prompt,
|
||||
"promptPreview": text_preview(&task.prompt, preview_limit),
|
||||
"basePromptPreview": text_preview(&task.base_prompt, preview_limit),
|
||||
"displayPromptPreview": text_preview(&display_prompt, preview_limit),
|
||||
"promptChars": task.prompt.chars().count(),
|
||||
"basePromptChars": task.base_prompt.chars().count(),
|
||||
"displayPromptChars": display_prompt.chars().count(),
|
||||
@@ -729,8 +734,15 @@ fn task_meta_response(state: &AppState, task: &TaskMeta) -> Value {
|
||||
let map = base.as_object_mut().expect("task response object");
|
||||
let output_count = json_array_len(task.task_json.as_ref(), "output", task.output_count);
|
||||
let prompt_history_count = json_array_len(task.task_json.as_ref(), "promptHistory", 0);
|
||||
let display_prompt = if task.base_prompt.is_empty() {
|
||||
user_prompt_for_display(&task.prompt)
|
||||
} else {
|
||||
task.base_prompt.clone()
|
||||
};
|
||||
map.insert("prompt".to_string(), json!(task.prompt));
|
||||
map.insert("basePrompt".to_string(), json!(task.base_prompt));
|
||||
map.insert("displayPrompt".to_string(), json!(display_prompt));
|
||||
map.insert("initialPrompt".to_string(), json!(task.prompt));
|
||||
map.insert("finalResponse".to_string(), json!(final_response(task)));
|
||||
map.insert(
|
||||
"promptHistory".to_string(),
|
||||
@@ -2200,3 +2212,136 @@ fn run_healthcheck(port: u16) -> bool {
|
||||
let mut body = String::new();
|
||||
stream.read_to_string(&mut body).is_ok() && body.contains("\"ok\": true")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn test_state() -> AppState {
|
||||
AppState {
|
||||
config: Config {
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 4278,
|
||||
database_url: "postgres://unused".to_string(),
|
||||
log_file: "/tmp/unidesk-code-queue-mgr-test.jsonl".to_string(),
|
||||
deploy_service_id: "code-queue-mgr".to_string(),
|
||||
deploy_repo: "".to_string(),
|
||||
deploy_commit: "".to_string(),
|
||||
deploy_requested_commit: "".to_string(),
|
||||
mgr_pool_max: 1,
|
||||
trace_pool_max: 1,
|
||||
default_provider_id: "D601".to_string(),
|
||||
default_workdir: "/workspace".to_string(),
|
||||
remote_default_workdir: "/home/ubuntu".to_string(),
|
||||
default_model: "gpt-5.5".to_string(),
|
||||
code_models: vec!["gpt-5.5".to_string()],
|
||||
default_max_attempts: 1,
|
||||
default_reasoning_effort: None,
|
||||
model_reasoning_efforts: json!({}),
|
||||
},
|
||||
started_at: now_iso(),
|
||||
logs: Arc::new(Mutex::new(VecDeque::new())),
|
||||
schema_ready: Arc::new(Mutex::new(false)),
|
||||
schema_last_error: Arc::new(Mutex::new(None)),
|
||||
last_database_error: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
fn long_prompt(marker: &str) -> String {
|
||||
let filler = (0..76)
|
||||
.map(|index| format!("long-prompt-line-{index:02} {}", "abcdefghijklmnopqrstuvwxyz0123456789".repeat(2)))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
[
|
||||
"Code Queue long prompt observation contract".to_string(),
|
||||
"".to_string(),
|
||||
filler,
|
||||
"".to_string(),
|
||||
"验收标准:".to_string(),
|
||||
format!("所有原始 prompt 观察层必须保留这个唯一 tail marker: {marker}"),
|
||||
]
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
fn test_task(prompt: String) -> TaskMeta {
|
||||
let at = now_iso();
|
||||
TaskMeta {
|
||||
id: "codex_1779000000000_longprompt".to_string(),
|
||||
queue_id: "e2e-long-prompt".to_string(),
|
||||
status: "queued".to_string(),
|
||||
provider_id: "D601".to_string(),
|
||||
execution_mode: "default".to_string(),
|
||||
model: "gpt-5.5".to_string(),
|
||||
cwd: "/home/ubuntu".to_string(),
|
||||
prompt: prompt.clone(),
|
||||
base_prompt: prompt,
|
||||
reference_task_ids: json!([]),
|
||||
reference_injection: None,
|
||||
reasoning_effort: None,
|
||||
max_attempts: 1,
|
||||
current_attempt: 0,
|
||||
current_mode: None,
|
||||
codex_thread_id: None,
|
||||
active_turn_id: None,
|
||||
created_at: at.clone(),
|
||||
updated_at: at,
|
||||
started_at: None,
|
||||
finished_at: None,
|
||||
read_at: None,
|
||||
last_error: None,
|
||||
last_judge: None,
|
||||
output_count: 1,
|
||||
event_count: 0,
|
||||
attempt_count: 0,
|
||||
last_output_seq: 1,
|
||||
task_json: Some(json!({
|
||||
"queueEnteredAt": now_iso(),
|
||||
"finalResponse": "",
|
||||
"stepCount": 0,
|
||||
"llmStepCount": 0,
|
||||
"judgeFailCount": 0,
|
||||
"cancelRequested": false,
|
||||
"attempts": [],
|
||||
"output": [],
|
||||
"events": [],
|
||||
"promptHistory": []
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
fn field_text<'a>(value: &'a Value, key: &str) -> &'a str {
|
||||
value.get(key).and_then(Value::as_str).unwrap_or("")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_observation_preserves_raw_fields_and_explicit_previews() {
|
||||
let state = test_state();
|
||||
let marker = "RUST_PROMPT_OBSERVATION_TAIL_MARKER";
|
||||
let prompt = long_prompt(marker);
|
||||
assert!(prompt.chars().count() > 2500);
|
||||
let task = test_task(prompt);
|
||||
let create = task_list_response(&state, &task, false);
|
||||
let overview = task_list_response(&state, &task, true);
|
||||
let meta = task_meta_response(&state, &task);
|
||||
let raw_fields = [
|
||||
field_text(&create, "prompt"),
|
||||
field_text(&create, "basePrompt"),
|
||||
field_text(&create, "displayPrompt"),
|
||||
field_text(&overview, "prompt"),
|
||||
field_text(&overview, "basePrompt"),
|
||||
field_text(&overview, "displayPrompt"),
|
||||
field_text(&meta, "prompt"),
|
||||
field_text(&meta, "basePrompt"),
|
||||
field_text(&meta, "displayPrompt"),
|
||||
field_text(&meta, "initialPrompt"),
|
||||
];
|
||||
assert!(raw_fields.iter().all(|value| value.contains(marker)), "{raw_fields:?}");
|
||||
assert!(raw_fields.iter().all(|value| !value.contains("...<truncated>")), "{raw_fields:?}");
|
||||
|
||||
let preview = overview.get("promptPreview").and_then(Value::as_object).expect("promptPreview metadata");
|
||||
assert_eq!(preview.get("truncated").and_then(Value::as_bool), Some(true));
|
||||
assert_eq!(preview.get("chars").and_then(Value::as_u64), Some(field_text(&overview, "prompt").chars().count() as u64));
|
||||
assert!(preview.get("text").and_then(Value::as_str).unwrap_or("").contains("...<truncated>"));
|
||||
assert!(!preview.get("text").and_then(Value::as_str).unwrap_or("").contains(marker));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import postgres from "postgres";
|
||||
import { createHourlyJsonlWriter, logRetentionBytesForService } from "../../../shared/src/rotating-jsonl";
|
||||
import { codeQueuePromptResponseFields, safePreview } from "./prompt-observation";
|
||||
|
||||
type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
|
||||
type JsonRecord = Record<string, JsonValue>;
|
||||
@@ -434,12 +435,6 @@ function timestampMs(value: string | Date | null | undefined): number | null {
|
||||
return Number.isFinite(ms) ? ms : null;
|
||||
}
|
||||
|
||||
function safePreview(value: unknown, maxChars: number): string {
|
||||
const text = String(value ?? "");
|
||||
if (text.length <= maxChars) return text;
|
||||
return `${text.slice(0, Math.max(0, maxChars - 20))}\n...<truncated>`;
|
||||
}
|
||||
|
||||
function prefixPreview(value: unknown, maxChars: number): string {
|
||||
const text = String(value ?? "");
|
||||
return text.length <= maxChars ? text : `${text.slice(0, Math.max(0, maxChars - 1))}…`;
|
||||
@@ -1635,17 +1630,12 @@ function taskStatisticsSummary(tasks: QueueTask[], days = 14): JsonRecord {
|
||||
}
|
||||
|
||||
function taskListResponse(task: QueueTask, lite = true): JsonRecord {
|
||||
const displayPrompt = task.basePrompt || userPromptForDisplay(task.prompt);
|
||||
const promptFields = codeQueuePromptResponseFields(task, { lite, userPromptForDisplay });
|
||||
return {
|
||||
id: task.id,
|
||||
queueId: queueIdOf(task),
|
||||
queueEnteredAt: task.queueEnteredAt,
|
||||
prompt: lite ? prefixPreview(displayPrompt, 360) : safePreview(displayPrompt, 2000),
|
||||
basePrompt: lite ? prefixPreview(task.basePrompt, 360) : safePreview(task.basePrompt, 2000),
|
||||
displayPrompt: lite ? prefixPreview(displayPrompt, 360) : safePreview(displayPrompt, 2000),
|
||||
promptChars: task.prompt.length,
|
||||
basePromptChars: task.basePrompt.length,
|
||||
displayPromptChars: displayPrompt.length,
|
||||
...promptFields,
|
||||
promptEditable: queuedTaskPromptEditable(task),
|
||||
finalResponseChars: task.finalResponse.length,
|
||||
stepCount: numberField(task.stepCount ?? task.llmStepCount, 0),
|
||||
@@ -1695,6 +1685,8 @@ function taskMetaResponse(task: QueueTask): JsonRecord {
|
||||
...taskListResponse(task, false),
|
||||
prompt: task.prompt,
|
||||
basePrompt: task.basePrompt,
|
||||
displayPrompt: task.basePrompt || userPromptForDisplay(task.prompt),
|
||||
initialPrompt: task.prompt,
|
||||
finalResponse: task.finalResponse,
|
||||
promptHistory: toJsonValue(task.promptHistory),
|
||||
attempts: toJsonValue(task.attempts),
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
|
||||
export type JsonRecord = Record<string, JsonValue>;
|
||||
|
||||
export interface PromptObservationTask {
|
||||
prompt: string;
|
||||
basePrompt: string;
|
||||
}
|
||||
|
||||
export interface PromptObservationOptions {
|
||||
lite?: boolean;
|
||||
userPromptForDisplay: (prompt: string) => string;
|
||||
}
|
||||
|
||||
export function safePreview(value: unknown, maxChars: number): string {
|
||||
const text = String(value ?? "");
|
||||
if (text.length <= maxChars) return text;
|
||||
return `${text.slice(0, Math.max(0, maxChars - 20))}\n...<truncated>`;
|
||||
}
|
||||
|
||||
export function textPreview(value: unknown, maxChars: number): JsonRecord {
|
||||
const text = String(value ?? "");
|
||||
const truncated = text.length > maxChars;
|
||||
return {
|
||||
text: truncated ? safePreview(text, maxChars) : text,
|
||||
chars: text.length,
|
||||
truncated,
|
||||
omittedChars: truncated ? text.length - Math.max(0, maxChars - 20) : 0,
|
||||
};
|
||||
}
|
||||
|
||||
export function codeQueuePromptResponseFields(task: PromptObservationTask, options: PromptObservationOptions): JsonRecord {
|
||||
const displayPrompt = task.basePrompt || options.userPromptForDisplay(task.prompt);
|
||||
const previewLimit = options.lite === true ? 360 : 2000;
|
||||
return {
|
||||
prompt: task.prompt,
|
||||
basePrompt: task.basePrompt,
|
||||
displayPrompt,
|
||||
promptPreview: textPreview(task.prompt, previewLimit),
|
||||
basePromptPreview: textPreview(task.basePrompt, previewLimit),
|
||||
displayPromptPreview: textPreview(displayPrompt, previewLimit),
|
||||
promptChars: task.prompt.length,
|
||||
basePromptChars: task.basePrompt.length,
|
||||
displayPromptChars: displayPrompt.length,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user