Merge pull request #498 from pikasTech/fix/code-agent-timeout-control-20260527004132
修复 Code Agent 空闲超时恢复和可配置超时
This commit is contained in:
@@ -4,13 +4,13 @@ import { createCloudApiServer } from "../../internal/cloud/server.mjs";
|
||||
const host = process.env.HWLAB_CLOUD_API_HOST || "0.0.0.0";
|
||||
const port = parsePort(process.env.HWLAB_CLOUD_API_PORT, parsePort(process.env.PORT, 6667));
|
||||
const commitId = process.env.HWLAB_COMMIT_ID || process.env.HWLAB_GIT_SHA;
|
||||
const codeAgentTimeoutMs = parseTimeout(process.env.HWLAB_CODE_AGENT_TIMEOUT_MS, 600000, {
|
||||
const codeAgentTimeoutMs = parseTimeout(process.env.HWLAB_CODE_AGENT_TIMEOUT_MS, 1200000, {
|
||||
min: 1000,
|
||||
max: 900000
|
||||
max: 2400000
|
||||
});
|
||||
const codeAgentHardTimeoutMs = parseTimeout(process.env.HWLAB_CODE_AGENT_HARD_TIMEOUT_MS, Math.max(600000, codeAgentTimeoutMs * 4), {
|
||||
const codeAgentHardTimeoutMs = parseTimeout(process.env.HWLAB_CODE_AGENT_HARD_TIMEOUT_MS, Math.max(1200000, codeAgentTimeoutMs * 4), {
|
||||
min: codeAgentTimeoutMs,
|
||||
max: 1800000
|
||||
max: 3600000
|
||||
});
|
||||
|
||||
if (!process.env.HWLAB_IMAGE && commitId) {
|
||||
|
||||
@@ -10,9 +10,9 @@ import { createServer } from "node:http";
|
||||
|
||||
const serviceId = "hwlab-edge-proxy";
|
||||
const upstream = process.env.HWLAB_EDGE_UPSTREAM || "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667";
|
||||
const proxyTimeoutMs = parseTimeout(process.env.HWLAB_EDGE_PROXY_TIMEOUT_MS, 660000, {
|
||||
const proxyTimeoutMs = parseTimeout(process.env.HWLAB_EDGE_PROXY_TIMEOUT_MS, 1260000, {
|
||||
min: 1000,
|
||||
max: 900000
|
||||
max: 2400000
|
||||
});
|
||||
const { host, port } = resolveHostPort({
|
||||
listenEnv: "HWLAB_EDGE_LISTEN",
|
||||
|
||||
+3
-3
@@ -205,7 +205,7 @@
|
||||
"HWLAB_M3_PATCH_PANEL_URL": "http://hwlab-patch-panel.hwlab-dev.svc.cluster.local:7301",
|
||||
"HWLAB_CODE_AGENT_PROVIDER": "codex-stdio",
|
||||
"HWLAB_CODE_AGENT_MODEL": "gpt-5.5",
|
||||
"HWLAB_CODE_AGENT_TIMEOUT_MS": "600000",
|
||||
"HWLAB_CODE_AGENT_TIMEOUT_MS": "1200000",
|
||||
"HWLAB_CODE_AGENT_OPENAI_BASE_URL": "http://127.0.0.1:49280/responses",
|
||||
"HWLAB_CODE_AGENT_HWLAB_API_BASE_URL": "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667",
|
||||
"HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED": "1",
|
||||
@@ -238,7 +238,7 @@
|
||||
"env": {
|
||||
"HWLAB_ENVIRONMENT": "dev",
|
||||
"HWLAB_API_BASE_URL": "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667",
|
||||
"HWLAB_CLOUD_WEB_PROXY_TIMEOUT_MS": "660000",
|
||||
"HWLAB_CLOUD_WEB_PROXY_TIMEOUT_MS": "1260000",
|
||||
"HWLAB_COMMIT_ID": "2be9f52",
|
||||
"HWLAB_IMAGE": "127.0.0.1:5000/hwlab/hwlab-cloud-web:2be9f52",
|
||||
"HWLAB_IMAGE_TAG": "2be9f52"
|
||||
@@ -376,7 +376,7 @@
|
||||
"env": {
|
||||
"HWLAB_EDGE_LISTEN": "0.0.0.0:6667",
|
||||
"HWLAB_EDGE_UPSTREAM": "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667",
|
||||
"HWLAB_EDGE_PROXY_TIMEOUT_MS": "660000"
|
||||
"HWLAB_EDGE_PROXY_TIMEOUT_MS": "1260000"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
},
|
||||
{
|
||||
"name": "HWLAB_CODE_AGENT_TIMEOUT_MS",
|
||||
"value": "600000"
|
||||
"value": "1200000"
|
||||
},
|
||||
{
|
||||
"name": "HWLAB_CODE_AGENT_OPENAI_BASE_URL",
|
||||
@@ -381,7 +381,7 @@
|
||||
},
|
||||
{
|
||||
"name": "HWLAB_CLOUD_WEB_PROXY_TIMEOUT_MS",
|
||||
"value": "660000"
|
||||
"value": "1260000"
|
||||
},
|
||||
{
|
||||
"name": "HWLAB_COMMIT_ID",
|
||||
@@ -958,7 +958,7 @@
|
||||
},
|
||||
{
|
||||
"name": "HWLAB_EDGE_PROXY_TIMEOUT_MS",
|
||||
"value": "660000"
|
||||
"value": "1260000"
|
||||
}
|
||||
],
|
||||
"readinessProbe": {
|
||||
|
||||
@@ -490,9 +490,9 @@ const runtimeKind = process.env.HWLAB_ARTIFACT_KIND || "health-placeholder";
|
||||
const entrypoint = process.env.HWLAB_SERVICE_ENTRYPOINT || "";
|
||||
const port = Number.parseInt(process.env.PORT || process.env.HWLAB_PORT || "8080", 10);
|
||||
const cloudApiBaseUrl = process.env.HWLAB_API_BASE_URL || "";
|
||||
const cloudApiProxyTimeoutMs = parseTimeout(process.env.HWLAB_CLOUD_WEB_PROXY_TIMEOUT_MS, 660000, {
|
||||
const cloudApiProxyTimeoutMs = parseTimeout(process.env.HWLAB_CLOUD_WEB_PROXY_TIMEOUT_MS, 1260000, {
|
||||
min: 1000,
|
||||
max: 900000
|
||||
max: 2400000
|
||||
});
|
||||
const authUsername = nonEmptyEnv("HWLAB_CLOUD_WEB_AUTH_USERNAME", "admin");
|
||||
const authPassword = nonEmptyEnv("HWLAB_CLOUD_WEB_AUTH_PASSWORD", "hwlab2026");
|
||||
|
||||
@@ -258,10 +258,10 @@ test("compact commander summary exposes canPublish/canApply and dry-run devLiveC
|
||||
cloudWebDistFreshness: { status: "pass", mismatches: [] },
|
||||
codeAgentTimeoutReadiness: {
|
||||
status: "pass",
|
||||
frontendTimeoutMs: 660000,
|
||||
backendTimeoutMs: 600000,
|
||||
frontendTimeoutMs: 1800000,
|
||||
backendTimeoutMs: 1200000,
|
||||
requiredFrontendTimeoutMs: 35000,
|
||||
requiredBackendTimeoutMs: 600000,
|
||||
requiredBackendTimeoutMs: 1200000,
|
||||
transportWaitsForBackend: true,
|
||||
summary: "ready"
|
||||
},
|
||||
@@ -292,10 +292,10 @@ test("compact summary blocks publish on stale Cloud Web source dist", () => {
|
||||
cloudWebDistFreshness: { status: "blocked", mismatches: ["index.html"] },
|
||||
codeAgentTimeoutReadiness: {
|
||||
status: "pass",
|
||||
frontendTimeoutMs: 660000,
|
||||
backendTimeoutMs: 600000,
|
||||
frontendTimeoutMs: 1800000,
|
||||
backendTimeoutMs: 1200000,
|
||||
requiredFrontendTimeoutMs: 35000,
|
||||
requiredBackendTimeoutMs: 600000,
|
||||
requiredBackendTimeoutMs: 1200000,
|
||||
transportWaitsForBackend: true,
|
||||
summary: "ready"
|
||||
},
|
||||
@@ -326,9 +326,9 @@ test("compact summary separates Code Agent timeout and durable runtime blockers
|
||||
codeAgentTimeoutReadiness: {
|
||||
status: "blocked",
|
||||
frontendTimeoutMs: 4500,
|
||||
backendTimeoutMs: 600000,
|
||||
backendTimeoutMs: 1200000,
|
||||
requiredFrontendTimeoutMs: 35000,
|
||||
requiredBackendTimeoutMs: 600000,
|
||||
requiredBackendTimeoutMs: 1200000,
|
||||
transportWaitsForBackend: false,
|
||||
summary: "Code Agent browser timeout is too short."
|
||||
},
|
||||
@@ -373,10 +373,10 @@ test("compact summary blocks publish when Code Agent transport timeout does not
|
||||
cloudWebDistFreshness: { status: "pass", mismatches: [] },
|
||||
codeAgentTimeoutReadiness: {
|
||||
status: "blocked",
|
||||
frontendTimeoutMs: 600000,
|
||||
backendTimeoutMs: 600000,
|
||||
frontendTimeoutMs: 1200000,
|
||||
backendTimeoutMs: 1200000,
|
||||
requiredFrontendTimeoutMs: 35000,
|
||||
requiredBackendTimeoutMs: 600000,
|
||||
requiredBackendTimeoutMs: 1200000,
|
||||
transportWaitsForBackend: false,
|
||||
summary: "Code Agent timeout layering is unsafe."
|
||||
},
|
||||
@@ -405,10 +405,10 @@ test("compact summary keeps provider-secret blockers out of publish but blocks a
|
||||
cloudWebDistFreshness: { status: "pass", mismatches: [] },
|
||||
codeAgentTimeoutReadiness: {
|
||||
status: "pass",
|
||||
frontendTimeoutMs: 660000,
|
||||
backendTimeoutMs: 600000,
|
||||
frontendTimeoutMs: 1800000,
|
||||
backendTimeoutMs: 1200000,
|
||||
requiredFrontendTimeoutMs: 35000,
|
||||
requiredBackendTimeoutMs: 600000,
|
||||
requiredBackendTimeoutMs: 1200000,
|
||||
transportWaitsForBackend: true,
|
||||
summary: "ready"
|
||||
},
|
||||
|
||||
@@ -22,8 +22,8 @@ const expectedBlockedGuardCommand = `${guardCommand} --expect-blocked`;
|
||||
const digestPattern = /^sha256:[a-f0-9]{64}$/u;
|
||||
const commitPattern = /^[a-f0-9]{7,40}$/u;
|
||||
const minCodeAgentFrontendTimeoutMs = 35000;
|
||||
const defaultCodeAgentBackendTimeoutMs = 600000;
|
||||
const minCodeAgentBackendTimeoutMs = 600000;
|
||||
const defaultCodeAgentBackendTimeoutMs = 1200000;
|
||||
const minCodeAgentBackendTimeoutMs = 1200000;
|
||||
|
||||
export function normalizeCommit(value) {
|
||||
if (typeof value !== "string") return "unknown";
|
||||
@@ -915,12 +915,12 @@ function summarizeCloudWebBuildFreshness(distFreshness, artifactCloudWeb) {
|
||||
}
|
||||
|
||||
async function inspectCodeAgentTimeoutReadiness(repoRoot) {
|
||||
const [appSource, chatSource] = await Promise.all([
|
||||
const [appSource, cloudApiEntrySource] = await Promise.all([
|
||||
readFile(path.join(repoRoot, "web/hwlab-cloud-web/app.mjs"), "utf8"),
|
||||
readFile(path.join(repoRoot, "internal/cloud/code-agent-chat.mjs"), "utf8")
|
||||
readFile(path.join(repoRoot, "cmd/hwlab-cloud-api/main.mjs"), "utf8")
|
||||
]);
|
||||
const frontendTimeoutMs = numericSourceConstant(appSource, "DEFAULT_CODE_AGENT_TIMEOUT_MS");
|
||||
const backendTimeoutMs = numericSourceConstant(chatSource, "DEFAULT_CODE_AGENT_TIMEOUT_MS");
|
||||
const backendTimeoutMs = parseTimeoutFallbackMs(cloudApiEntrySource, "HWLAB_CODE_AGENT_TIMEOUT_MS");
|
||||
const frontendReady = Number.isInteger(frontendTimeoutMs) && frontendTimeoutMs >= minCodeAgentFrontendTimeoutMs;
|
||||
const backendReady = Number.isInteger(backendTimeoutMs) && backendTimeoutMs >= minCodeAgentBackendTimeoutMs;
|
||||
const transportWaitsForBackend = Number.isInteger(frontendTimeoutMs) && Number.isInteger(backendTimeoutMs) && frontendTimeoutMs > backendTimeoutMs;
|
||||
@@ -943,6 +943,13 @@ function numericSourceConstant(source, name) {
|
||||
return Number.parseInt(match[1].replace(/_/gu, ""), 10);
|
||||
}
|
||||
|
||||
function parseTimeoutFallbackMs(source, envName) {
|
||||
const escaped = envName.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
||||
const match = source.match(new RegExp(`parseTimeout\\(process\\.env\\.${escaped},\\s*([0-9_]+)`, "u"));
|
||||
if (!match) return null;
|
||||
return Number.parseInt(match[1].replace(/_/gu, ""), 10);
|
||||
}
|
||||
|
||||
async function inspectM3IoSourceReadiness(repoRoot) {
|
||||
const files = await listTrackedFiles(repoRoot);
|
||||
const candidateFiles = files.filter((file) =>
|
||||
|
||||
@@ -13,8 +13,8 @@ import {
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const expectedPorts = Object.freeze({ frontend: 16666, api: 16667 });
|
||||
const codeAgentBackendTimeoutMs = "600000";
|
||||
const codeAgentTransportTimeoutMs = "660000";
|
||||
const codeAgentBackendTimeoutMs = "1200000";
|
||||
const codeAgentTransportTimeoutMs = "1260000";
|
||||
const legacyPublicPorts = new Set([6666, 6667]);
|
||||
const duplicateMappingAllowed = new Set(["hwlab-box-simu", "hwlab-gateway-simu"]);
|
||||
const tomlBareKeyPattern = /^[A-Za-z0-9_-]+$/u;
|
||||
|
||||
@@ -23,7 +23,7 @@ const webRoot = path.join(repoRoot, "web/hwlab-cloud-web");
|
||||
const defaultLiveUrl = "http://74.48.78.17:16666/";
|
||||
const helpOwner = "codex_1779444232735_1";
|
||||
const legacyFailureWindowMs = 4500;
|
||||
const codeAgentLongTimeoutMs = 600000;
|
||||
const codeAgentLongTimeoutMs = 1800000;
|
||||
const localAgentFixtureDelayMs = 5200;
|
||||
const codeAgentE2ePrompts = Object.freeze([
|
||||
{
|
||||
@@ -2657,7 +2657,8 @@ function hasCodeAgentLongTimeoutContract(files) {
|
||||
const app = files.app;
|
||||
return (
|
||||
/DEFAULT_API_TIMEOUT_MS\s*=\s*4500/u.test(app) &&
|
||||
/DEFAULT_CODE_AGENT_TIMEOUT_MS\s*=\s*600000/u.test(app) &&
|
||||
/DEFAULT_CODE_AGENT_TIMEOUT_MS\s*=\s*1800000/u.test(app) &&
|
||||
/MAX_CODE_AGENT_TIMEOUT_MS\s*=\s*2400000/u.test(app) &&
|
||||
/DEFAULT_CODE_AGENT_SUBMIT_TIMEOUT_MS\s*=\s*60000/u.test(app) &&
|
||||
/async function sendAgentMessage[\s\S]*?fetchJson\("\/v1\/agent\/chat"[\s\S]*?timeoutMs:\s*CODE_AGENT_SUBMIT_TIMEOUT_MS/u.test(app) &&
|
||||
/async function sendAgentMessage[\s\S]*?shortConnection:\s*true/u.test(app) &&
|
||||
@@ -2782,7 +2783,8 @@ function hasCodeAgentConversationUxStates({ app, styles }) {
|
||||
!/function\s+messageAttributionPanel\s*\(/u.test(app) &&
|
||||
!/codeAgentAttributionFromMessage/u.test(app) &&
|
||||
/DEFAULT_API_TIMEOUT_MS\s*=\s*4500/u.test(app) &&
|
||||
/DEFAULT_CODE_AGENT_TIMEOUT_MS\s*=\s*600000/u.test(app) &&
|
||||
/DEFAULT_CODE_AGENT_TIMEOUT_MS\s*=\s*1800000/u.test(app) &&
|
||||
/MAX_CODE_AGENT_TIMEOUT_MS\s*=\s*2400000/u.test(app) &&
|
||||
/timeoutMs:\s*CODE_AGENT_SUBMIT_TIMEOUT_MS/u.test(app) &&
|
||||
/HWLAB_CLOUD_WEB_CONFIG/u.test(app) &&
|
||||
/正在处理这次 Code Agent 请求/u.test(app) &&
|
||||
|
||||
@@ -20,7 +20,8 @@ import {
|
||||
|
||||
const DEFAULT_API_TIMEOUT_MS = 4500;
|
||||
const DEFAULT_LIVE_SURFACE_TIMEOUT_MS = 12000;
|
||||
const DEFAULT_CODE_AGENT_TIMEOUT_MS = 600000;
|
||||
const DEFAULT_CODE_AGENT_TIMEOUT_MS = 1800000;
|
||||
const MAX_CODE_AGENT_TIMEOUT_MS = 2400000;
|
||||
const DEFAULT_CODE_AGENT_SUBMIT_TIMEOUT_MS = 60000;
|
||||
const DEFAULT_CODE_AGENT_CANCEL_TIMEOUT_MS = 30000;
|
||||
const DEFAULT_GATEWAY_SHELL_TIMEOUT_MS = 120000;
|
||||
@@ -307,8 +308,8 @@ function resolveUserCodeAgentTimeoutMs() {
|
||||
} catch {
|
||||
stored = null;
|
||||
}
|
||||
return clampTimeoutMs(stored, { min, max: 1200000 })
|
||||
?? resolveTimeoutMs("codeAgentTimeoutMs", DEFAULT_CODE_AGENT_TIMEOUT_MS, { min, max: 1200000 });
|
||||
return clampTimeoutMs(stored, { min, max: MAX_CODE_AGENT_TIMEOUT_MS })
|
||||
?? resolveTimeoutMs("codeAgentTimeoutMs", DEFAULT_CODE_AGENT_TIMEOUT_MS, { min, max: MAX_CODE_AGENT_TIMEOUT_MS });
|
||||
}
|
||||
|
||||
function resolveUserGatewayShellTimeoutMs() {
|
||||
@@ -350,7 +351,7 @@ function initCodeAgentTimeoutControl() {
|
||||
renderCodeAgentSummary();
|
||||
});
|
||||
el.codeAgentTimeout.addEventListener("change", () => {
|
||||
const next = clampTimeoutMs(el.codeAgentTimeout.value, { min: 30000, max: 1200000 }) ?? DEFAULT_CODE_AGENT_TIMEOUT_MS;
|
||||
const next = clampTimeoutMs(el.codeAgentTimeout.value, { min: 30000, max: MAX_CODE_AGENT_TIMEOUT_MS }) ?? DEFAULT_CODE_AGENT_TIMEOUT_MS;
|
||||
CODE_AGENT_TIMEOUT_MS = next;
|
||||
try {
|
||||
window.localStorage?.setItem(CODE_AGENT_TIMEOUT_STORAGE_KEY, String(next));
|
||||
@@ -514,7 +515,7 @@ function refreshRestoredCodeAgentTraces() {
|
||||
function reconcileRestoredCodeAgentResults() {
|
||||
for (const message of state.chatMessages) {
|
||||
if (!message.traceId || message.role !== "agent") continue;
|
||||
if (["completed", "failed", "timeout", "canceled", "error"].includes(String(message.status ?? "").toLowerCase())) continue;
|
||||
if (!shouldReconcileRestoredCodeAgentResult(message)) continue;
|
||||
reconcileCodeAgentResult(message.id, { quiet: true });
|
||||
}
|
||||
}
|
||||
@@ -1816,10 +1817,33 @@ async function refreshTraceAfterResultPollError(traceId) {
|
||||
function shouldReconcileCodeAgentResult(message, runnerTrace = message?.runnerTrace) {
|
||||
if (!message?.traceId || message.role !== "agent") return false;
|
||||
if (state.resultReconciliationInFlight.has(message.traceId)) return false;
|
||||
if (["completed", "failed", "timeout", "canceled", "error"].includes(String(message.status ?? "").toLowerCase())) return false;
|
||||
const status = String(message.status ?? "").toLowerCase();
|
||||
if (["completed", "canceled"].includes(status)) return false;
|
||||
if (["failed", "timeout", "error"].includes(status)) return traceHasTerminalResultEvent(runnerTrace) || isRecoverableCodeAgentTerminalMessage(message);
|
||||
return traceHasTerminalResultEvent(runnerTrace);
|
||||
}
|
||||
|
||||
function shouldReconcileRestoredCodeAgentResult(message) {
|
||||
if (!message?.traceId || message.role !== "agent") return false;
|
||||
if (state.resultReconciliationInFlight.has(message.traceId)) return false;
|
||||
const status = String(message.status ?? "").toLowerCase();
|
||||
if (["completed", "canceled"].includes(status)) return false;
|
||||
if (["failed", "timeout", "error"].includes(status)) return isRecoverableCodeAgentTerminalMessage(message) || traceHasTerminalResultEvent(message.runnerTrace);
|
||||
return true;
|
||||
}
|
||||
|
||||
function isRecoverableCodeAgentTerminalMessage(message) {
|
||||
const code = normalizeErrorCode(message?.error?.code);
|
||||
return [
|
||||
"client_trace_idle_timeout",
|
||||
"client_timeout",
|
||||
"proxy_timeout",
|
||||
"cloud_api_proxy_timeout",
|
||||
"code_agent_background_failed",
|
||||
"code_agent_session_evidence_missing"
|
||||
].includes(code) || isCodeAgentTimeoutError(message?.error, message?.text);
|
||||
}
|
||||
|
||||
function traceHasTerminalResultEvent(runnerTrace) {
|
||||
const events = Array.isArray(runnerTrace?.events) ? runnerTrace.events : [];
|
||||
const terminalLabel = runnerTrace?.lastEvent?.label ?? events.at(-1)?.label;
|
||||
|
||||
@@ -213,9 +213,11 @@
|
||||
<select id="code-agent-timeout" aria-label="Code Agent 无新事件超时">
|
||||
<option value="180000">3 分钟</option>
|
||||
<option value="300000">5 分钟</option>
|
||||
<option value="600000" selected>10 分钟</option>
|
||||
<option value="600000">10 分钟</option>
|
||||
<option value="900000">15 分钟</option>
|
||||
<option value="1200000">20 分钟</option>
|
||||
<option value="1800000" selected>30 分钟</option>
|
||||
<option value="2400000">40 分钟</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="agent-timeout-control" for="gateway-shell-timeout">
|
||||
|
||||
@@ -421,7 +421,8 @@ assert.match(app, /error\.traceId = (?:traceId|response\.data\?\.traceId \|\| tr
|
||||
assert.match(app, /DEFAULT_API_TIMEOUT_MS\s*=\s*4500/);
|
||||
assert.match(app, /DEFAULT_LIVE_SURFACE_TIMEOUT_MS\s*=\s*12000/);
|
||||
assert.match(app, /LIVE_SURFACE_TIMEOUT_MS\s*=\s*resolveTimeoutMs\("liveSurfaceTimeoutMs"/);
|
||||
assert.match(app, /DEFAULT_CODE_AGENT_TIMEOUT_MS\s*=\s*600000/);
|
||||
assert.match(app, /DEFAULT_CODE_AGENT_TIMEOUT_MS\s*=\s*1800000/);
|
||||
assert.match(app, /MAX_CODE_AGENT_TIMEOUT_MS\s*=\s*2400000/);
|
||||
assert.match(app, /DEFAULT_CODE_AGENT_SUBMIT_TIMEOUT_MS\s*=\s*60000/);
|
||||
assert.match(app, /CODE_AGENT_TIMEOUT_STORAGE_KEY/);
|
||||
assert.match(functionBody(app, "clampTimeoutMs"), /value === null \|\| value === undefined/);
|
||||
@@ -1070,7 +1071,8 @@ assert.match(app, /error\?\.message/);
|
||||
assert.match(app, /DEFAULT_API_TIMEOUT_MS\s*=\s*4500/);
|
||||
assert.match(app, /DEFAULT_LIVE_SURFACE_TIMEOUT_MS\s*=\s*12000/);
|
||||
assert.match(app, /LIVE_SURFACE_TIMEOUT_MS\s*=\s*resolveTimeoutMs\("liveSurfaceTimeoutMs"/);
|
||||
assert.match(app, /DEFAULT_CODE_AGENT_TIMEOUT_MS\s*=\s*600000/);
|
||||
assert.match(app, /DEFAULT_CODE_AGENT_TIMEOUT_MS\s*=\s*1800000/);
|
||||
assert.match(app, /MAX_CODE_AGENT_TIMEOUT_MS\s*=\s*2400000/);
|
||||
assert.match(app, /CODE_AGENT_SUBMIT_TIMEOUT_MS/);
|
||||
assert.match(app, /CODE_AGENT_TIMEOUT_MS/);
|
||||
assert.match(functionBody(app, "sendAgentMessage"), /timeoutMs:\s*CODE_AGENT_SUBMIT_TIMEOUT_MS/);
|
||||
|
||||
@@ -372,6 +372,94 @@ test("restored pending trace polls result endpoint without token or live provide
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
test("restored client idle timeout is corrected by completed result polling", async () => {
|
||||
const traceId = "trc_restore_client_idle_timeout_completed";
|
||||
const server = await startStaticServer(webRoot, {
|
||||
[`/v1/agent/chat/result/${traceId}`]: completedResultFixture(traceId, "超时后端补齐的最终回复。"),
|
||||
[`/v1/agent/chat/trace/${traceId}`]: {
|
||||
traceId,
|
||||
status: "completed",
|
||||
eventCount: 1,
|
||||
events: [{
|
||||
seq: 1,
|
||||
traceId,
|
||||
label: "result:completed",
|
||||
type: "result",
|
||||
status: "completed",
|
||||
terminal: true,
|
||||
message: "Code Agent result is ready for short-connection polling."
|
||||
}],
|
||||
lastEvent: {
|
||||
label: "result:completed",
|
||||
type: "result",
|
||||
status: "completed",
|
||||
terminal: true
|
||||
}
|
||||
}
|
||||
});
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
try {
|
||||
const page = await browser.newPage({
|
||||
viewport: { width: 1366, height: 768 },
|
||||
deviceScaleFactor: 1
|
||||
});
|
||||
await page.addInitScript(({ traceId: injectedTraceId }) => {
|
||||
window.localStorage.setItem("hwlab.workbench.codeAgentSession.v1", JSON.stringify({
|
||||
version: 1,
|
||||
updatedAtMs: Date.now(),
|
||||
conversationId: "cnv_restore_client_idle_timeout_completed",
|
||||
sessionId: "ses_restore_client_idle_timeout_completed",
|
||||
threadId: "thread_restore_client_idle_timeout_completed",
|
||||
sessionStatus: "timeout",
|
||||
chatMessages: [{
|
||||
id: "msg_restore_client_idle_timeout_completed",
|
||||
role: "agent",
|
||||
title: "Code Agent 无新事件超时",
|
||||
text: "Code Agent 超过 600000ms 无新事件;输入已保留,可稍后重试。",
|
||||
status: "timeout",
|
||||
traceId: injectedTraceId,
|
||||
conversationId: "cnv_restore_client_idle_timeout_completed",
|
||||
sessionId: "ses_restore_client_idle_timeout_completed",
|
||||
threadId: "thread_restore_client_idle_timeout_completed",
|
||||
retryInput: "restore timeout fixture",
|
||||
error: {
|
||||
code: "client_trace_idle_timeout",
|
||||
category: "timeout",
|
||||
timeoutMs: 600000,
|
||||
message: "Code Agent 超过 600000ms 无新事件"
|
||||
},
|
||||
runnerTrace: {
|
||||
traceId: injectedTraceId,
|
||||
eventCount: 1,
|
||||
events: [{
|
||||
label: "result:completed",
|
||||
type: "result",
|
||||
status: "completed",
|
||||
terminal: true
|
||||
}],
|
||||
lastEvent: { label: "result:completed", status: "completed" }
|
||||
}
|
||||
}]
|
||||
}));
|
||||
}, { traceId });
|
||||
await page.goto(`${server.url}?hwlab-test-hooks=1`, { waitUntil: "domcontentloaded", timeout: 15000 });
|
||||
await login(page);
|
||||
await page.waitForFunction(() => window.__hwlabWorkbenchTestHooks, null, { timeout: 12000 });
|
||||
await page.waitForFunction(() => window.__hwlabWorkbenchTestHooks.latestAgentMessageText()?.status === "completed", null, { timeout: 12000 });
|
||||
const restored = await page.evaluate(() => window.__hwlabWorkbenchTestHooks.latestAgentMessageText());
|
||||
assert.equal(restored.status, "completed", JSON.stringify(restored));
|
||||
assert.match(restored.text, /超时后端补齐的最终回复/);
|
||||
assert.equal(restored.providerTrace.terminalStatus, "completed");
|
||||
assert.equal(restored.error, null);
|
||||
|
||||
await page.close();
|
||||
} finally {
|
||||
await browser.close();
|
||||
await server.close();
|
||||
}
|
||||
});
|
||||
|
||||
async function traceMetrics(page) {
|
||||
return page.evaluate(() => window.__hwlabWorkbenchTestHooks.traceScrollMetrics());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user