prompt-persistence.test.ts 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. import { describe, expect, test } from "bun:test"
  2. import type { AsyncStorage } from "@solid-primitives/storage"
  3. import { createEffect, createRoot } from "solid-js"
  4. import type { Platform } from "@/context/platform"
  5. import { createPromptReady, createPromptSession } from "@/context/prompt-state"
  6. import { ServerScope } from "@/utils/server-scope"
  7. import { createDraftStore } from "@/utils/draft-store"
  8. let read: ((value: string | null) => void) | undefined
  9. const storage: AsyncStorage = {
  10. getItem: () => new Promise((resolve) => (read = resolve)),
  11. setItem: async () => undefined,
  12. removeItem: async () => undefined,
  13. clear: async () => undefined,
  14. key: async () => null,
  15. getLength: async () => 0,
  16. length: Promise.resolve(0),
  17. }
  18. const platform: Platform = {
  19. platform: "web",
  20. openExternal: () => undefined,
  21. restart: async () => undefined,
  22. notify: async () => undefined,
  23. draftStore: {
  24. ...storage,
  25. putBlob: async () => {
  26. throw new Error("putBlob is not used by this test")
  27. },
  28. },
  29. }
  30. describe("prompt persistence", () => {
  31. test("waits for an async draft to hydrate before reporting ready", async () => {
  32. await new Promise<void>((resolve, reject) => {
  33. createRoot((dispose) => {
  34. const session = createPromptSession(ServerScope.local, { draftID: "draft-async" }, undefined, platform)
  35. const ready = createPromptReady(() => session)
  36. expect(ready()).toBe(false)
  37. expect(session.current()[0]).toMatchObject({ type: "text", content: "" })
  38. read?.(
  39. JSON.stringify({
  40. prompt: [{ type: "text", content: "persisted draft", start: 0, end: 15 }],
  41. cursor: 15,
  42. context: { items: [] },
  43. }),
  44. )
  45. createEffect(() => {
  46. if (!ready()) return
  47. try {
  48. expect(session.current()[0]).toMatchObject({ type: "text", content: "persisted draft" })
  49. dispose()
  50. resolve()
  51. } catch (error) {
  52. dispose()
  53. reject(error)
  54. }
  55. })
  56. })
  57. })
  58. })
  59. })
  60. test("moves legacy image data URLs into blobs and hydrates object URLs", async () => {
  61. const documents = new Map<string, string>()
  62. const blobs = new Map<string, Blob>()
  63. const store = createDraftStore({
  64. get: async (key) => documents.get(key) ?? null,
  65. set: async (key, value) => void documents.set(key, value),
  66. remove: async (key) => void documents.delete(key),
  67. putBlob: async (blob) => {
  68. const id = String(blob.size)
  69. blobs.set(id, blob)
  70. return id
  71. },
  72. getBlob: async (id) => blobs.get(id) ?? null,
  73. })
  74. await store.setItem("prompt", JSON.stringify({ prompt: [{ type: "image", dataUrl: "data:image/png;base64,YQ==" }] }))
  75. expect(documents.get("prompt")).not.toContain("dataUrl")
  76. const value = JSON.parse((await store.getItem("prompt"))!)
  77. expect(value.prompt[0].blob.id).toBe("1")
  78. expect(value.prompt[0].blob.url).toStartWith("blob:")
  79. })
  80. test("does not let delayed blob migration overwrite a newer draft", async () => {
  81. const documents = new Map<string, string>()
  82. const migration = Promise.withResolvers<void>()
  83. const store = createDraftStore({
  84. get: async () => null,
  85. set: async (key, value) => void documents.set(key, value),
  86. remove: async () => undefined,
  87. putBlob: async () => {
  88. await migration.promise
  89. return "blob"
  90. },
  91. getBlob: async () => null,
  92. })
  93. const older = store.setItem(
  94. "prompt",
  95. JSON.stringify({ prompt: [{ type: "image", dataUrl: "data:image/png;base64,YQ==" }] }),
  96. )
  97. await Bun.sleep(0)
  98. await store.setItem("prompt", JSON.stringify({ prompt: [{ type: "text", content: "latest" }] }))
  99. migration.resolve()
  100. await older
  101. expect(documents.get("prompt")).toContain("latest")
  102. })