share.test.ts 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. import { describe, expect, test } from "bun:test"
  2. import { Share } from "../../src/core/share"
  3. import { Storage } from "../../src/core/storage"
  4. import { Identifier } from "@opencode-ai/core/util/identifier"
  5. describe.concurrent("core.share", () => {
  6. test("should create a share", async () => {
  7. const sessionID = Identifier.descending()
  8. const share = await Share.create({ sessionID })
  9. expect(share.sessionID).toBe(sessionID)
  10. expect(share.secret).toBeDefined()
  11. await Share.remove({ id: share.id, secret: share.secret })
  12. })
  13. test("should remove a share as admin", async () => {
  14. const share = await Share.create({ sessionID: Identifier.descending() })
  15. await Share.removeAdmin({ id: share.id })
  16. expect(await Share.get(share.id)).toBeUndefined()
  17. })
  18. test("should sync data to a share", async () => {
  19. const sessionID = Identifier.descending()
  20. const share = await Share.create({ sessionID })
  21. const data: Share.Data[] = [
  22. {
  23. type: "part",
  24. data: { id: "part1", sessionID, messageID: "msg1", type: "text", text: "Hello" },
  25. },
  26. ]
  27. await Share.sync({
  28. share: { id: share.id, secret: share.secret },
  29. data,
  30. })
  31. const snapshot = await Storage.read<{ data: Share.Data[] }>(["share_snapshot", share.id])
  32. expect(snapshot?.data).toHaveLength(1)
  33. await Share.remove({ id: share.id, secret: share.secret })
  34. })
  35. test("should sync multiple batches of data", async () => {
  36. const sessionID = Identifier.descending()
  37. const share = await Share.create({ sessionID })
  38. const data1: Share.Data[] = [
  39. {
  40. type: "part",
  41. data: { id: "part1", sessionID, messageID: "msg1", type: "text", text: "Hello" },
  42. },
  43. ]
  44. const data2: Share.Data[] = [
  45. {
  46. type: "part",
  47. data: { id: "part2", sessionID, messageID: "msg1", type: "text", text: "World" },
  48. },
  49. ]
  50. await Share.sync({
  51. share: { id: share.id, secret: share.secret },
  52. data: data1,
  53. })
  54. await Share.sync({
  55. share: { id: share.id, secret: share.secret },
  56. data: data2,
  57. })
  58. const snapshot = await Storage.read<{ data: Share.Data[] }>(["share_snapshot", share.id])
  59. expect(snapshot?.data).toHaveLength(2)
  60. await Share.remove({ id: share.id, secret: share.secret })
  61. })
  62. test("should retrieve synced data", async () => {
  63. const sessionID = Identifier.descending()
  64. const share = await Share.create({ sessionID })
  65. const data: Share.Data[] = [
  66. {
  67. type: "part",
  68. data: { id: "part1", sessionID, messageID: "msg1", type: "text", text: "Hello" },
  69. },
  70. {
  71. type: "part",
  72. data: { id: "part2", sessionID, messageID: "msg1", type: "text", text: "World" },
  73. },
  74. ]
  75. await Share.sync({
  76. share: { id: share.id, secret: share.secret },
  77. data,
  78. })
  79. const result = await Share.data(share.id)
  80. expect(result.length).toBe(2)
  81. expect(result[0].type).toBe("part")
  82. expect(result[1].type).toBe("part")
  83. await Share.remove({ id: share.id, secret: share.secret })
  84. })
  85. test("should retrieve data from multiple syncs", async () => {
  86. const sessionID = Identifier.descending()
  87. const share = await Share.create({ sessionID })
  88. const data1: Share.Data[] = [
  89. {
  90. type: "part",
  91. data: { id: "part1", sessionID, messageID: "msg1", type: "text", text: "Hello" },
  92. },
  93. ]
  94. const data2: Share.Data[] = [
  95. {
  96. type: "part",
  97. data: { id: "part2", sessionID, messageID: "msg2", type: "text", text: "World" },
  98. },
  99. ]
  100. const data3: Share.Data[] = [
  101. { type: "part", data: { id: "part3", sessionID, messageID: "msg3", type: "text", text: "!" } },
  102. ]
  103. await Share.sync({
  104. share: { id: share.id, secret: share.secret },
  105. data: data1,
  106. })
  107. await Share.sync({
  108. share: { id: share.id, secret: share.secret },
  109. data: data2,
  110. })
  111. await Share.sync({
  112. share: { id: share.id, secret: share.secret },
  113. data: data3,
  114. })
  115. const result = await Share.data(share.id)
  116. expect(result.length).toBe(3)
  117. const parts = result.filter((d) => d.type === "part")
  118. expect(parts.length).toBe(3)
  119. await Share.remove({ id: share.id, secret: share.secret })
  120. })
  121. test("should return latest data when syncing duplicate parts", async () => {
  122. const sessionID = Identifier.descending()
  123. const share = await Share.create({ sessionID })
  124. const data1: Share.Data[] = [
  125. {
  126. type: "part",
  127. data: { id: "part1", sessionID, messageID: "msg1", type: "text", text: "Hello" },
  128. },
  129. ]
  130. const data2: Share.Data[] = [
  131. {
  132. type: "part",
  133. data: { id: "part1", sessionID, messageID: "msg1", type: "text", text: "Hello Updated" },
  134. },
  135. ]
  136. await Share.sync({
  137. share: { id: share.id, secret: share.secret },
  138. data: data1,
  139. })
  140. await Share.sync({
  141. share: { id: share.id, secret: share.secret },
  142. data: data2,
  143. })
  144. const result = await Share.data(share.id)
  145. expect(result.length).toBe(1)
  146. const [first] = result
  147. expect(first.type).toBe("part")
  148. expect(first.type === "part" && first.data.type === "text" && first.data.text).toBe("Hello Updated")
  149. await Share.remove({ id: share.id, secret: share.secret })
  150. })
  151. test("should return empty array for share with no data", async () => {
  152. const sessionID = Identifier.descending()
  153. const share = await Share.create({ sessionID })
  154. const result = await Share.data(share.id)
  155. expect(result).toEqual([])
  156. await Share.remove({ id: share.id, secret: share.secret })
  157. })
  158. test("should migrate legacy event data into the snapshot", async () => {
  159. const sessionID = Identifier.descending()
  160. const share = await Share.create({ sessionID })
  161. const data: Share.Data[] = [
  162. {
  163. type: "part",
  164. data: { id: "part1", sessionID, messageID: "msg1", type: "text", text: "Hello" },
  165. },
  166. ]
  167. await Storage.remove(["share_snapshot", share.id])
  168. await Storage.write(["share_event", share.id, Identifier.descending()], data)
  169. const result = await Share.data(share.id)
  170. const snapshot = await Storage.read<{ data: Share.Data[] }>(["share_snapshot", share.id])
  171. expect(result).toHaveLength(1)
  172. expect(snapshot?.data).toHaveLength(1)
  173. await Share.remove({ id: share.id, secret: share.secret })
  174. })
  175. test("should throw error for invalid secret", async () => {
  176. const sessionID = Identifier.descending()
  177. const share = await Share.create({ sessionID })
  178. const data: Share.Data[] = [
  179. {
  180. type: "part",
  181. data: { id: "part1", sessionID, messageID: "msg1", type: "text", text: "Test" },
  182. },
  183. ]
  184. expect(async () => {
  185. await Share.sync({
  186. share: { id: share.id, secret: "invalid-secret" },
  187. data,
  188. })
  189. }).toThrow()
  190. await Share.remove({ id: share.id, secret: share.secret })
  191. })
  192. test("should throw error for non-existent share", async () => {
  193. const sessionID = Identifier.descending()
  194. const data: Share.Data[] = [
  195. {
  196. type: "part",
  197. data: { id: "part1", sessionID, messageID: "msg1", type: "text", text: "Test" },
  198. },
  199. ]
  200. expect(async () => {
  201. await Share.sync({
  202. share: { id: "non-existent-id", secret: "some-secret" },
  203. data,
  204. })
  205. }).toThrow()
  206. })
  207. test("should handle different data types", async () => {
  208. const sessionID = Identifier.descending()
  209. const share = await Share.create({ sessionID })
  210. const data: Share.Data[] = [
  211. { type: "session", data: { id: sessionID, status: "running" } as any },
  212. { type: "message", data: { id: "msg1", sessionID } as any },
  213. {
  214. type: "part",
  215. data: { id: "part1", sessionID, messageID: "msg1", type: "text", text: "Hello" },
  216. },
  217. ]
  218. await Share.sync({
  219. share: { id: share.id, secret: share.secret },
  220. data,
  221. })
  222. const result = await Share.data(share.id)
  223. expect(result.length).toBe(3)
  224. expect(result.some((d) => d.type === "session")).toBe(true)
  225. expect(result.some((d) => d.type === "message")).toBe(true)
  226. expect(result.some((d) => d.type === "part")).toBe(true)
  227. await Share.remove({ id: share.id, secret: share.secret })
  228. })
  229. })