network.test.ts 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. // kilocode_change - new file
  2. import { describe, expect, test } from "bun:test"
  3. import { Bus } from "../../src/bus"
  4. import { Instance } from "../../src/project/instance"
  5. import { tmpdir } from "../fixture/fixture"
  6. import { SessionNetwork } from "../../src/session/network"
  7. describe("session.network", () => {
  8. test("detects common network disconnect codes", () => {
  9. expect(SessionNetwork.disconnected({ code: "ECONNREFUSED" })).toBe(true)
  10. expect(SessionNetwork.disconnected({ code: "ENOTFOUND" })).toBe(true)
  11. expect(SessionNetwork.disconnected({ code: "EAI_AGAIN" })).toBe(true)
  12. expect(SessionNetwork.disconnected({ code: "ENOENT" })).toBe(false)
  13. })
  14. test("detects provider unable to connect message", () => {
  15. const err = new Error("Unable to connect. Is the computer able to access the url?")
  16. expect(SessionNetwork.disconnected(err)).toBe(true)
  17. expect(SessionNetwork.message(err)).toBe("Unable to connect. Is the computer able to access the url?")
  18. })
  19. test("detects wrapped network cause", () => {
  20. const err = new Error("top") as Error & { cause?: unknown }
  21. err.cause = { code: "ETIMEDOUT" }
  22. expect(SessionNetwork.disconnected(err)).toBe(true)
  23. expect(SessionNetwork.message(err)).toBe("Connection timed out")
  24. })
  25. test("detects TimeoutError as disconnected", () => {
  26. const err = new DOMException("The operation was aborted due to timeout", "TimeoutError")
  27. expect(SessionNetwork.disconnected(err)).toBe(true)
  28. expect(SessionNetwork.message(err)).toBe("Request timed out")
  29. })
  30. test("detects wrapped TimeoutError in cause chain", () => {
  31. const timeout = new DOMException("signal timed out", "TimeoutError")
  32. const err = new Error("request failed", { cause: timeout })
  33. expect(SessionNetwork.disconnected(err)).toBe(true)
  34. expect(SessionNetwork.message(err)).toBe("Request timed out")
  35. })
  36. test("reply resolves pending request", async () => {
  37. await using tmp = await tmpdir({ git: true })
  38. await Instance.provide({
  39. directory: tmp.path,
  40. fn: async () => {
  41. const { promise } = await SessionNetwork.ask({
  42. sessionID: "ses_test",
  43. message: "Connection refused",
  44. abort: new AbortController().signal,
  45. })
  46. const pending = await SessionNetwork.list()
  47. expect(pending).toHaveLength(1)
  48. const req = pending[0]!
  49. await SessionNetwork.reply({ requestID: req.id })
  50. await expect(promise).resolves.toBeUndefined()
  51. },
  52. })
  53. })
  54. test("reject rejects pending request", async () => {
  55. await using tmp = await tmpdir({ git: true })
  56. await Instance.provide({
  57. directory: tmp.path,
  58. fn: async () => {
  59. const { promise } = await SessionNetwork.ask({
  60. sessionID: "ses_test",
  61. message: "Connection timed out",
  62. abort: new AbortController().signal,
  63. })
  64. const pending = await SessionNetwork.list()
  65. expect(pending).toHaveLength(1)
  66. const req = pending[0]!
  67. await SessionNetwork.reject({ requestID: req.id })
  68. await expect(promise).rejects.toBeInstanceOf(SessionNetwork.RejectedError)
  69. },
  70. })
  71. })
  72. test("aborted signal rejects without publishing asked", async () => {
  73. await using tmp = await tmpdir({ git: true })
  74. await Instance.provide({
  75. directory: tmp.path,
  76. fn: async () => {
  77. const abort = new AbortController()
  78. const seen: string[] = []
  79. const offAsked = Bus.subscribe(SessionNetwork.Event.Asked, () => seen.push("asked"))
  80. const offRejected = Bus.subscribe(SessionNetwork.Event.Rejected, () => seen.push("rejected"))
  81. abort.abort()
  82. try {
  83. const { promise } = await SessionNetwork.ask({
  84. sessionID: "ses_test",
  85. message: "Connection timed out",
  86. abort: abort.signal,
  87. })
  88. const err = await promise.catch((err) => err)
  89. expect(err).toBeInstanceOf(DOMException)
  90. expect(err.name).toBe("AbortError")
  91. expect(await SessionNetwork.list()).toHaveLength(0)
  92. expect(seen).toStrictEqual(["rejected"])
  93. } finally {
  94. offAsked()
  95. offRejected()
  96. }
  97. },
  98. })
  99. })
  100. test("abort during pending ask rejects with AbortError and cleans up", async () => {
  101. await using tmp = await tmpdir({ git: true })
  102. await Instance.provide({
  103. directory: tmp.path,
  104. fn: async () => {
  105. const abort = new AbortController()
  106. const { promise } = await SessionNetwork.ask({
  107. sessionID: "ses_test",
  108. message: "Connection refused",
  109. abort: abort.signal,
  110. })
  111. // wait for the ask to register
  112. const list = await SessionNetwork.list()
  113. expect(list).toHaveLength(1)
  114. // abort while waiting
  115. abort.abort()
  116. const err = await promise.catch((e: unknown) => e)
  117. expect(err).toBeInstanceOf(DOMException)
  118. expect((err as DOMException).name).toBe("AbortError")
  119. // pending entry should be cleaned up
  120. expect(await SessionNetwork.list()).toHaveLength(0)
  121. },
  122. })
  123. })
  124. })