fix: extend code agent heavy request timeout

This commit is contained in:
Code Queue Review
2026-05-23 04:34:23 +00:00
parent 71152d4d7f
commit 78675f2de1
10 changed files with 511 additions and 63 deletions
+132 -50
View File
@@ -176,9 +176,19 @@ export function runDevCloudWorkbenchStaticSmoke() {
return runStaticSmoke();
}
export async function runDevCloudWorkbenchTimeoutFixtureSmoke(options = {}) {
return runLocalAgentFixtureSmoke({
mode: "local-agent-timeout-fixture-browser",
responseDelayMs: options.responseDelayMs ?? 120,
timeoutConfigMs: options.timeoutConfigMs ?? 50,
expectTimeout: true
});
}
export async function runDevCloudWorkbenchSmoke(argv = []) {
const args = parseSmokeArgs(argv);
if (args.mode === "live") return runLiveSmoke(args);
if (args.mode === "local-agent-timeout-fixture") return runDevCloudWorkbenchTimeoutFixtureSmoke();
if (args.mode === "local-agent-fixture") return runDevCloudWorkbenchLocalAgentFixtureSmoke();
if (args.mode === "mobile") return runDevCloudWorkbenchMobileSmoke();
return runStaticSmoke();
@@ -197,6 +207,8 @@ export function parseSmokeArgs(argv) {
args.mobile = true;
} else if (arg === "--local-agent-fixture") {
args.mode = "local-agent-fixture";
} else if (arg === "--local-agent-timeout-fixture") {
args.mode = "local-agent-timeout-fixture";
} else if (arg === "--url") {
index += 1;
if (!argv[index]) throw new Error("--url requires a value");
@@ -1269,6 +1281,12 @@ function hasCodeAgentConversationUxStates({ app, styles }) {
/function\s+messageEvidencePanel\s*\(/u.test(app) &&
/function\s+messageEvidenceSummary\s*\(/u.test(app) &&
/function\s+boundedEvidenceField\s*\(/u.test(app) &&
/DEFAULT_API_TIMEOUT_MS\s*=\s*4500/u.test(app) &&
/DEFAULT_CODE_AGENT_TIMEOUT_MS\s*=\s*150000/u.test(app) &&
/timeoutMs:\s*CODE_AGENT_TIMEOUT_MS/u.test(app) &&
/HWLAB_CLOUD_WEB_CONFIG/u.test(app) &&
/失败时会保留输入供重试/u.test(app) &&
/Code Agent 请求超过/u.test(app) &&
/Code Agent SOURCE 回复/u.test(app) &&
/SOURCE fixture 只可显示为 SOURCE 回复,不能冒充 DEV-LIVE/u.test(app) &&
/sourceKind:\s*completion\.sourceKind/u.test(submitBody) &&
@@ -1949,26 +1967,35 @@ async function inspectJourneyControls(page) {
}
async function inspectJourneyUi(page) {
return page.evaluate(() => ({
title: document.title,
workspaceHidden: document.querySelector('[data-view="workspace"]')?.hidden ?? null,
gateHidden: document.querySelector('[data-view="gate"]')?.hidden ?? null,
helpHidden: document.querySelector('[data-view="help"]')?.hidden ?? null,
agentChatStatus: document.querySelector("#agent-chat-status")?.textContent?.trim() ?? null,
inputCleared: document.querySelector("#command-input")?.value === "",
completedMessageVisible: Boolean(document.querySelector(".message-card.status-completed")),
sourceMessageVisible: Boolean(document.querySelector(".message-card.status-source")),
failedMessageVisible: Boolean(document.querySelector(".message-card.status-failed")),
messageCount: document.querySelectorAll(".message-card").length,
completedMessageHasNonSensitiveMeta: Boolean(
[...document.querySelectorAll(".message-card.status-completed .message-meta")]
.some((element) => /openai-responses|gpt-5\.5|trc_/u.test(element.textContent ?? ""))
),
sourceMessageHasNonSensitiveMeta: Boolean(
[...document.querySelectorAll(".message-card.status-source .message-meta")]
.some((element) => /SOURCE|source-fixture|trc_/u.test(element.textContent ?? ""))
)
}));
return page.evaluate(() => {
const failedText = [...document.querySelectorAll(".message-card.status-failed")]
.map((element) => element.textContent ?? "")
.join("\n");
const commandValue = document.querySelector("#command-input")?.value ?? "";
return {
title: document.title,
workspaceHidden: document.querySelector('[data-view="workspace"]')?.hidden ?? null,
gateHidden: document.querySelector('[data-view="gate"]')?.hidden ?? null,
helpHidden: document.querySelector('[data-view="help"]')?.hidden ?? null,
agentChatStatus: document.querySelector("#agent-chat-status")?.textContent?.trim() ?? null,
inputCleared: commandValue === "",
retryInputPreserved: commandValue === "请用一句话说明当前 HWLAB 工作台可以做什么。",
completedMessageVisible: Boolean(document.querySelector(".message-card.status-completed")),
sourceMessageVisible: Boolean(document.querySelector(".message-card.status-source")),
failedMessageVisible: Boolean(document.querySelector(".message-card.status-failed")),
failedMessageHasTrace: /trace=trc_/u.test(failedText),
failedMessageHasChineseTimeout: /请求超过|请求超时|保留输入|重试/u.test(failedText),
messageCount: document.querySelectorAll(".message-card").length,
completedMessageHasNonSensitiveMeta: Boolean(
[...document.querySelectorAll(".message-card.status-completed .message-meta")]
.some((element) => /openai-responses|gpt-5\.5|trc_/u.test(element.textContent ?? ""))
),
sourceMessageHasNonSensitiveMeta: Boolean(
[...document.querySelectorAll(".message-card.status-source .message-meta")]
.some((element) => /SOURCE|source-fixture|trc_/u.test(element.textContent ?? ""))
)
};
});
}
async function inspectDefaultApiStatus(page) {
@@ -2012,6 +2039,19 @@ function blockedAgentChatResponse(code) {
}
export async function runDevCloudWorkbenchLocalAgentFixtureSmoke() {
return runLocalAgentFixtureSmoke({
mode: "local-agent-fixture-browser",
responseDelayMs: 0,
expectTimeout: false
});
}
async function runLocalAgentFixtureSmoke({
mode,
responseDelayMs = 0,
timeoutConfigMs = null,
expectTimeout = false
} = {}) {
let chromium;
try {
({ chromium } = await import("playwright"));
@@ -2019,7 +2059,7 @@ export async function runDevCloudWorkbenchLocalAgentFixtureSmoke() {
return {
status: "skip",
task: "DC-DCSN-P0-2026-003",
mode: "local-agent-fixture-browser",
mode,
evidenceLevel: "SOURCE",
devLive: false,
summary: `Local Code Agent browser fixture smoke skipped because Playwright is unavailable: ${error.message}`,
@@ -2029,11 +2069,22 @@ export async function runDevCloudWorkbenchLocalAgentFixtureSmoke() {
};
}
const server = await startStaticWebServer({ agentFixture: true });
const server = await startStaticWebServer({ agentFixture: true, agentDelayMs: responseDelayMs });
let browser;
try {
browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1366, height: 768 } });
if (timeoutConfigMs !== null) {
await page.addInitScript((timeoutMs) => {
globalThis.HWLAB_CLOUD_WEB_CONFIG = {
...(globalThis.HWLAB_CLOUD_WEB_CONFIG ?? {}),
timeouts: {
...(globalThis.HWLAB_CLOUD_WEB_CONFIG?.timeouts ?? {}),
codeAgentTimeoutMs: timeoutMs
}
};
}, timeoutConfigMs);
}
const agentResponses = [];
page.on("response", async (response) => {
if (!response.url().includes("/v1/agent/chat")) return;
@@ -2061,30 +2112,50 @@ export async function runDevCloudWorkbenchLocalAgentFixtureSmoke() {
const controls = await inspectJourneyControls(page);
const apiStatus = await inspectDefaultApiStatus(page);
await page.locator("#command-input").fill("请用一句话说明当前 HWLAB 工作台可以做什么。");
const responsePromise = page.waitForResponse(
const responsePromise = expectTimeout ? null : page.waitForResponse(
(response) => response.url().includes("/v1/agent/chat") && response.request().method() === "POST",
{ timeout: 15000 }
);
await page.locator("#command-send").click();
const response = await responsePromise;
const responseBody = await response.json().catch(() => null);
await page.waitForFunction(
() => document.querySelector("#agent-chat-status")?.textContent?.trim() === "SOURCE 回复",
null,
{ timeout: 8000 }
);
let response = null;
let responseBody = null;
if (expectTimeout) {
await page.waitForFunction(
() => document.querySelector("#agent-chat-status")?.textContent?.trim() === "发送失败",
null,
{ timeout: 8000 }
);
} else {
response = await responsePromise;
responseBody = await response.json().catch(() => null);
await page.waitForFunction(
() => document.querySelector("#agent-chat-status")?.textContent?.trim() === "SOURCE 回复",
null,
{ timeout: 8000 }
);
}
const ui = await inspectJourneyUi(page);
const responseSummary = sanitizeAgentChatBody(responseBody, { httpStatus: response.status() });
const pass =
response.ok() &&
responseSummary?.status === "completed" &&
responseSummary?.sourceKind === "SOURCE" &&
responseSummary?.hasReply === true &&
ui.agentChatStatus === "SOURCE 回复" &&
ui.sourceMessageVisible &&
ui.sourceMessageHasNonSensitiveMeta &&
!ui.completedMessageVisible &&
!ui.failedMessageVisible;
const responseSummary = response ? sanitizeAgentChatBody(responseBody, { httpStatus: response.status() }) : null;
const pass = expectTimeout
? (
ui.agentChatStatus === "发送失败" &&
ui.failedMessageVisible &&
ui.retryInputPreserved &&
ui.failedMessageHasTrace &&
ui.failedMessageHasChineseTimeout &&
!ui.completedMessageVisible
)
: (
response.ok() &&
responseSummary?.status === "completed" &&
responseSummary?.sourceKind === "SOURCE" &&
responseSummary?.hasReply === true &&
ui.agentChatStatus === "SOURCE 回复" &&
ui.sourceMessageVisible &&
ui.sourceMessageHasNonSensitiveMeta &&
!ui.completedMessageVisible &&
!ui.failedMessageVisible
);
const checks = [
{
id: "local-agent-fixture-controls",
@@ -2099,9 +2170,11 @@ export async function runDevCloudWorkbenchLocalAgentFixtureSmoke() {
observations: apiStatus
},
{
id: "local-agent-fixture-replied-state",
id: expectTimeout ? "local-agent-timeout-fixture-failed-state" : "local-agent-fixture-replied-state",
status: pass ? "pass" : "blocked",
summary: "Local SOURCE fixture sends input and reaches a user-visible replied state without marking the fixture as DEV-LIVE.",
summary: expectTimeout
? "Local SOURCE fixture proves a bounded Code Agent timeout stays failed/BLOCKED in the UI, preserves trace evidence, and keeps the user's input for retry."
: "Local SOURCE fixture sends input and reaches a user-visible replied state without marking the fixture as DEV-LIVE.",
observations: {
response: responseSummary,
ui,
@@ -2120,12 +2193,14 @@ export async function runDevCloudWorkbenchLocalAgentFixtureSmoke() {
return {
status: blockers.length === 0 ? "pass" : "blocked",
task: "DC-DCSN-P0-2026-003",
mode: "local-agent-fixture-browser",
mode,
url: server.url,
generatedAt: new Date().toISOString(),
evidenceLevel: "SOURCE",
devLive: false,
summary: "Local browser smoke uses a sanitized SOURCE fixture for /v1/agent/chat; it proves the UI conversation path only and does not claim DEV-LIVE.",
summary: expectTimeout
? "Local browser smoke uses a sanitized slow SOURCE fixture for /v1/agent/chat; it proves timeout UI classification only and does not claim DEV-LIVE."
: "Local browser smoke uses a sanitized SOURCE fixture for /v1/agent/chat; it proves the UI conversation path only and does not claim DEV-LIVE.",
checks,
blockers,
safety: {
@@ -2139,7 +2214,7 @@ export async function runDevCloudWorkbenchLocalAgentFixtureSmoke() {
return {
status: "blocked",
task: "DC-DCSN-P0-2026-003",
mode: "local-agent-fixture-browser",
mode,
url: server.url,
generatedAt: new Date().toISOString(),
evidenceLevel: "SOURCE",
@@ -2149,7 +2224,7 @@ export async function runDevCloudWorkbenchLocalAgentFixtureSmoke() {
blockers: [
{
type: "observability_blocker",
scope: "local-agent-fixture-browser",
scope: mode,
status: "open",
summary: error.message
}
@@ -2336,7 +2411,7 @@ export async function runDevCloudWorkbenchMobileSmoke() {
async function startStaticWebServer(options = {}) {
const server = http.createServer(async (request, response) => {
const url = new URL(request.url ?? "/", "http://127.0.0.1");
if (options.agentFixture && await handleLocalAgentFixtureApi({ request, response, url })) {
if (options.agentFixture && await handleLocalAgentFixtureApi({ request, response, url, options })) {
return;
}
let pathname = decodeURIComponent(url.pathname);
@@ -2369,7 +2444,7 @@ async function startStaticWebServer(options = {}) {
};
}
async function handleLocalAgentFixtureApi({ request, response, url }) {
async function handleLocalAgentFixtureApi({ request, response, url, options = {} }) {
if (request.method === "GET" && (url.pathname === "/health/live" || url.pathname === "/v1")) {
jsonResponse(response, 200, {
serviceId: "hwlab-cloud-api",
@@ -2418,6 +2493,7 @@ async function handleLocalAgentFixtureApi({ request, response, url }) {
}
if (request.method === "POST" && url.pathname === "/v1/agent/chat") {
const body = await readJsonBody(request);
await delay(options.agentDelayMs ?? 0);
const timestamp = "2026-05-22T00:00:00.000Z";
const conversationId = stringOrFallback(body?.conversationId, "cnv_source_fixture_browser");
jsonResponse(response, 200, {
@@ -2453,6 +2529,11 @@ async function handleLocalAgentFixtureApi({ request, response, url }) {
return false;
}
function delay(ms) {
const normalized = Number.isFinite(Number(ms)) && Number(ms) > 0 ? Number(ms) : 0;
return new Promise((resolve) => setTimeout(resolve, normalized));
}
function localSourceFixtureAvailability() {
return {
endpoint: "POST /v1/agent/chat",
@@ -2781,11 +2862,12 @@ function escapeRegExp(value) {
export function printSmokeHelp() {
return {
status: "usage",
command: "node scripts/dev-cloud-workbench-smoke.mjs --static | --mobile | --local-agent-fixture | --live --url http://74.48.78.17:16666/ [--report reports/dev-gate/dev-cloud-workbench-live.json]",
command: "node scripts/dev-cloud-workbench-smoke.mjs --static | --mobile | --local-agent-fixture | --local-agent-timeout-fixture | --live --url http://74.48.78.17:16666/ [--report reports/dev-gate/dev-cloud-workbench-live.json]",
notes: [
"Static mode reads repository files and emits SOURCE-level evidence only.",
"--mobile runs a local static 390x844 browser hit-test and still emits SOURCE-level evidence only.",
"--local-agent-fixture runs a local browser send/reply path with a sanitized SOURCE fixture and does not claim DEV-LIVE.",
"--local-agent-timeout-fixture runs a local slow SOURCE fixture to verify bounded timeout UI classification and retry preservation without live acceptance.",
"Live mode opens the deployed workbench in a browser, verifies core controls, sends one Code Agent message, and retains only non-sensitive response fields."
]
};