|
8 | 8 | resolveFailoverStatus, |
9 | 9 | } from "./failover-error.js"; |
10 | 10 | import { classifyFailoverSignal } from "./pi-embedded-helpers/errors.js"; |
| 11 | +import { SessionWriteLockTimeoutError } from "./session-write-lock-error.js"; |
11 | 12 |
|
12 | 13 | // OpenAI 429 example shape: https://help.openai.com/en/articles/5955604-how-can-i-solve-429-too-many-requests-errors |
13 | 14 | const OPENAI_RATE_LIMIT_MESSAGE = |
@@ -359,6 +360,87 @@ describe("failover-error", () => { |
359 | 360 | ).toBe("overloaded"); |
360 | 361 | }); |
361 | 362 |
|
| 363 | + it("does not classify session lock wait errors as model timeout failover", () => { |
| 364 | + const sessionLockError = new SessionWriteLockTimeoutError({ |
| 365 | + timeoutMs: 10_000, |
| 366 | + owner: "pid=37121", |
| 367 | + lockPath: "/tmp/openclaw/session.jsonl.lock", |
| 368 | + }); |
| 369 | + expect(resolveFailoverReasonFromError(sessionLockError)).toBeNull(); |
| 370 | + expect(isTimeoutError(sessionLockError)).toBe(false); |
| 371 | + |
| 372 | + const wrappedLockError = Object.assign(new Error("operation timed out"), { |
| 373 | + name: "AbortError", |
| 374 | + cause: sessionLockError, |
| 375 | + }); |
| 376 | + expect(resolveFailoverReasonFromError(wrappedLockError)).toBeNull(); |
| 377 | + expect(isTimeoutError(wrappedLockError)).toBe(false); |
| 378 | + |
| 379 | + const abortWrappedLockError = Object.assign(new Error("request was aborted"), { |
| 380 | + name: "AbortError", |
| 381 | + cause: sessionLockError, |
| 382 | + }); |
| 383 | + expect(resolveFailoverReasonFromError(abortWrappedLockError)).toBeNull(); |
| 384 | + expect(isTimeoutError(abortWrappedLockError)).toBe(false); |
| 385 | + }); |
| 386 | + |
| 387 | + it("keeps explicit provider failover metadata authoritative over nested session lock text", () => { |
| 388 | + expect( |
| 389 | + resolveFailoverReasonFromError({ |
| 390 | + status: 429, |
| 391 | + code: "RESOURCE_EXHAUSTED", |
| 392 | + message: "upstream quota pressure", |
| 393 | + cause: new SessionWriteLockTimeoutError({ |
| 394 | + timeoutMs: 10_000, |
| 395 | + owner: "pid=37121", |
| 396 | + lockPath: "/tmp/openclaw/session.jsonl.lock", |
| 397 | + }), |
| 398 | + }), |
| 399 | + ).toBe("rate_limit"); |
| 400 | + }); |
| 401 | + |
| 402 | + it("keeps inferred HTTP failover metadata authoritative over nested session lock text", () => { |
| 403 | + expect( |
| 404 | + resolveFailoverReasonFromError({ |
| 405 | + message: "HTTP 429: upstream quota pressure", |
| 406 | + cause: new SessionWriteLockTimeoutError({ |
| 407 | + timeoutMs: 10_000, |
| 408 | + owner: "pid=37121", |
| 409 | + lockPath: "/tmp/openclaw/session.jsonl.lock", |
| 410 | + }), |
| 411 | + }), |
| 412 | + ).toBe("rate_limit"); |
| 413 | + }); |
| 414 | + |
| 415 | + it("does not treat generic abort codes as explicit failover metadata over nested session lock text", () => { |
| 416 | + expect( |
| 417 | + resolveFailoverReasonFromError({ |
| 418 | + name: "AbortError", |
| 419 | + code: "ABORT_ERR", |
| 420 | + message: "The operation was aborted", |
| 421 | + cause: new SessionWriteLockTimeoutError({ |
| 422 | + timeoutMs: 10_000, |
| 423 | + owner: "pid=37121", |
| 424 | + lockPath: "/tmp/openclaw/session.jsonl.lock", |
| 425 | + }), |
| 426 | + }), |
| 427 | + ).toBeNull(); |
| 428 | + }); |
| 429 | + |
| 430 | + it("does not let cause-based failover classification bypass wrapper session lock suppression", () => { |
| 431 | + expect( |
| 432 | + resolveFailoverReasonFromError({ |
| 433 | + message: "wrapper", |
| 434 | + reason: new SessionWriteLockTimeoutError({ |
| 435 | + timeoutMs: 10_000, |
| 436 | + owner: "pid=37121", |
| 437 | + lockPath: "/tmp/openclaw/session.jsonl.lock", |
| 438 | + }), |
| 439 | + cause: new Error("operation timed out"), |
| 440 | + }), |
| 441 | + ).toBeNull(); |
| 442 | + }); |
| 443 | + |
362 | 444 | it("classifies provider-scoped generic upstream errors for failover", () => { |
363 | 445 | expect( |
364 | 446 | resolveFailoverReasonFromError({ |
|
0 commit comments