| ... | @@ -1,13 +1,20 @@ | ... | @@ -1,13 +1,20 @@ |
| 1 | import { describe, expect, it } from "@effect/vitest"; | 1 | import { describe, expect, it } from "@effect/vitest"; |
| | 2 | import { EnvironmentNotRegisteredError } from "@t3tools/client-runtime/connection"; |
| | 3 | import { isTransportConnectionErrorMessage } from "@t3tools/client-runtime/errors"; |
| | 4 | import { EnvironmentRpcUnavailableError } from "@t3tools/client-runtime/rpc"; |
| 2 | import { | 5 | import { |
| 3 | CommandId, | 6 | CommandId, |
| | 7 | EnvironmentAuthorizationError, |
| 4 | EnvironmentId, | 8 | EnvironmentId, |
| 5 | MessageId, | 9 | MessageId, |
| | 10 | OrchestrationDispatchCommandError, |
| 6 | ProjectId, | 11 | ProjectId, |
| 7 | ProviderInstanceId, | 12 | ProviderInstanceId, |
| 8 | ThreadId, | 13 | ThreadId, |
| 9 | } from "@t3tools/contracts"; | 14 | } from "@t3tools/contracts"; |
| 10 | import { AtomRegistry } from "effect/unstable/reactivity"; | 15 | import { AtomRegistry } from "effect/unstable/reactivity"; |
| | 16 | import * as RpcClientError from "effect/unstable/rpc/RpcClientError"; |
| | 17 | import * as Socket from "effect/unstable/socket/Socket"; |
| 11 | import { onTestFinished, vi } from "vite-plus/test"; | 18 | import { onTestFinished, vi } from "vite-plus/test"; |
| 12 | | 19 | |
| 13 | const outboxFiles = vi.hoisted(() => new Map<string, string | Error>()); | 20 | const outboxFiles = vi.hoisted(() => new Map<string, string | Error>()); |
| ... | @@ -1399,6 +1406,62 @@ describe("thread outbox", () => { | ... | @@ -1399,6 +1406,62 @@ describe("thread outbox", () => { |
| 1399 | }), | 1406 | }), |
| 1400 | ).toBe(true); | 1407 | ).toBe(true); |
| 1401 | expect(shouldRetryThreadOutboxDelivery(new Error("Thread no longer exists"))).toBe(false); | 1408 | expect(shouldRetryThreadOutboxDelivery(new Error("Thread no longer exists"))).toBe(false); |
| | 1409 | expect( |
| | 1410 | shouldRetryThreadOutboxDelivery( |
| | 1411 | new OrchestrationDispatchCommandError({ message: "Thread no longer exists" }), |
| | 1412 | ), |
| | 1413 | ).toBe(false); |
| | 1414 | expect( |
| | 1415 | shouldRetryThreadOutboxDelivery( |
| | 1416 | new EnvironmentAuthorizationError({ |
| | 1417 | message: "Missing scope", |
| | 1418 | requiredScope: "orchestration:operate", |
| | 1419 | }), |
| | 1420 | ), |
| | 1421 | ).toBe(false); |
| | 1422 | }); |
| | 1423 | |
| | 1424 | // A pending task created offline drains the moment the phone reconnects, |
| | 1425 | // which is exactly when the socket is most likely to drop again. Every way a |
| | 1426 | // request can fail in flight must retry; a restore turns the pending task |
| | 1427 | // into a draft and it disappears from the list. |
| | 1428 | it("retries every in-flight transport failure by tag, not by message text", () => { |
| | 1429 | const socketReasons = [ |
| | 1430 | new Socket.SocketReadError({ cause: new Error("The network connection was lost.") }), |
| | 1431 | new Socket.SocketWriteError({ cause: new Error("Broken pipe") }), |
| | 1432 | new Socket.SocketCloseError({ code: 1006 }), |
| | 1433 | new Socket.SocketOpenError({ kind: "Timeout", cause: new Error("timeout") }), |
| | 1434 | ]; |
| | 1435 | for (const reason of socketReasons) { |
| | 1436 | const error = new RpcClientError.RpcClientError({ reason }); |
| | 1437 | expect(isTransportConnectionErrorMessage(error.message)).toBe( |
| | 1438 | reason._tag === "SocketCloseError" || reason._tag === "SocketOpenError", |
| | 1439 | ); |
| | 1440 | expect(shouldRetryThreadOutboxDelivery(error)).toBe(true); |
| | 1441 | } |
| | 1442 | expect( |
| | 1443 | shouldRetryThreadOutboxDelivery( |
| | 1444 | new RpcClientError.RpcClientError({ |
| | 1445 | reason: new RpcClientError.RpcClientDefect({ |
| | 1446 | message: "Error decoding message", |
| | 1447 | cause: new Error("Unexpected end of JSON input"), |
| | 1448 | }), |
| | 1449 | }), |
| | 1450 | ), |
| | 1451 | ).toBe(true); |
| | 1452 | expect( |
| | 1453 | shouldRetryThreadOutboxDelivery( |
| | 1454 | new EnvironmentRpcUnavailableError({ |
| | 1455 | environmentId: "environment-1", |
| | 1456 | message: "Home is not connected.", |
| | 1457 | }), |
| | 1458 | ), |
| | 1459 | ).toBe(true); |
| | 1460 | expect( |
| | 1461 | shouldRetryThreadOutboxDelivery( |
| | 1462 | new EnvironmentNotRegisteredError({ environmentId: EnvironmentId.make("environment-1") }), |
| | 1463 | ), |
| | 1464 | ).toBe(true); |
| 1402 | }); | 1465 | }); |
| 1403 | | 1466 | |
| 1404 | it("retains queued messages when settings synchronization fails before startTurn", () => { | 1467 | it("retains queued messages when settings synchronization fails before startTurn", () => { |