state.test.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. import { describe, expect } from "bun:test"
  2. import { State } from "@opencode-ai/core/state"
  3. import { Deferred, Effect, Exit, Fiber, Layer, Scope } from "effect"
  4. import { TestClock } from "effect/testing"
  5. import { testEffect } from "./lib/effect"
  6. const it = testEffect(Layer.empty)
  7. describe("State", () => {
  8. it.effect("commits a transform atomically when its updater is interrupted", () =>
  9. Effect.gen(function* () {
  10. const rebuilding = yield* Deferred.make<void>()
  11. const release = yield* Deferred.make<void>()
  12. let block = true
  13. const state = State.create({
  14. initial: () => ({ values: [] as string[] }),
  15. draft: (draft) => ({ add: (value: string) => draft.values.push(value) }),
  16. finalize: () =>
  17. block ? Deferred.succeed(rebuilding, undefined).pipe(Effect.andThen(Deferred.await(release))) : Effect.void,
  18. })
  19. const scope = yield* Scope.make()
  20. const fiber = yield* state
  21. .transform((editor) => {
  22. editor.add("registered")
  23. })
  24. .pipe(Scope.provide(scope), Effect.forkChild)
  25. yield* Deferred.await(rebuilding)
  26. const interruption = yield* Fiber.interrupt(fiber).pipe(Effect.forkChild)
  27. block = false
  28. yield* Deferred.succeed(release, undefined)
  29. yield* Fiber.join(interruption)
  30. expect(state.get().values).toEqual(["registered"])
  31. yield* Scope.close(scope, Exit.void)
  32. expect(state.get().values).toEqual([])
  33. }),
  34. )
  35. it.effect("commits rebuilt state before finalize runs", () =>
  36. Effect.gen(function* () {
  37. const observed: string[][] = []
  38. const state: State.Interface<{ values: string[] }, { add: (item: string) => void }> = State.create({
  39. initial: () => ({ values: [] as string[] }),
  40. draft: (draft) => ({ add: (item: string) => draft.values.push(item) }),
  41. finalize: () => Effect.sync(() => observed.push([...state.get().values])),
  42. })
  43. yield* state.transform((draft) => {
  44. draft.add("value")
  45. })
  46. // Update events publish from finalize, so consumers reading on the event
  47. // must observe the rebuilt state, not the previous one.
  48. expect(observed).toEqual([["value"]])
  49. }),
  50. )
  51. it.effect("runs transforms during every reload", () =>
  52. Effect.gen(function* () {
  53. let value = "first"
  54. const state = State.create({
  55. initial: () => ({ values: [] as string[] }),
  56. draft: (draft) => ({ add: (item: string) => draft.values.push(item) }),
  57. })
  58. yield* state.transform((editor) => {
  59. editor.add(value)
  60. })
  61. expect(state.get().values).toEqual(["first"])
  62. value = "second"
  63. const reload = yield* state.reload().pipe(Effect.forkChild({ startImmediately: true }))
  64. yield* TestClock.adjust("500 millis")
  65. yield* Fiber.join(reload)
  66. expect(state.get().values).toEqual(["second"])
  67. }),
  68. )
  69. it.effect("disposes a transform once and rebuilds remaining state", () =>
  70. Effect.gen(function* () {
  71. const state = State.create({
  72. initial: () => ({ values: [] as string[] }),
  73. draft: (draft) => ({ add: (item: string) => draft.values.push(item) }),
  74. })
  75. yield* state.transform((editor) => {
  76. editor.add("first")
  77. })
  78. const registration = yield* state.transform((editor) => {
  79. editor.add("second")
  80. })
  81. expect(state.get().values).toEqual(["first", "second"])
  82. yield* registration.dispose
  83. expect(state.get().values).toEqual(["first"])
  84. yield* registration.dispose
  85. expect(state.get().values).toEqual(["first"])
  86. }),
  87. )
  88. it.effect("batches automatic rebuilds", () =>
  89. Effect.gen(function* () {
  90. let finalized = 0
  91. const first = State.create({
  92. initial: () => ({ values: [] as string[] }),
  93. draft: (draft) => ({ add: (item: string) => draft.values.push(item) }),
  94. finalize: () => Effect.sync(() => finalized++),
  95. })
  96. const second = State.create({
  97. initial: () => ({ values: [] as string[] }),
  98. draft: (draft) => ({ add: (item: string) => draft.values.push(item) }),
  99. finalize: () => Effect.sync(() => finalized++),
  100. })
  101. yield* State.batch(
  102. Effect.gen(function* () {
  103. yield* first.transform((draft) => {
  104. draft.add("first")
  105. })
  106. yield* first.transform((draft) => {
  107. draft.add("second")
  108. })
  109. yield* second.transform((draft) => {
  110. draft.add("third")
  111. })
  112. expect(finalized).toBe(0)
  113. }),
  114. )
  115. expect(first.get().values).toEqual(["first", "second"])
  116. expect(second.get().values).toEqual(["third"])
  117. expect(finalized).toBe(2)
  118. }),
  119. )
  120. it.effect("debounces reload bursts", () =>
  121. Effect.gen(function* () {
  122. let finalized = 0
  123. const state = State.create({
  124. initial: () => ({ values: [] as string[] }),
  125. draft: (draft) => ({ add: (item: string) => draft.values.push(item) }),
  126. finalize: () => Effect.sync(() => finalized++),
  127. })
  128. yield* state.transform((draft) => {
  129. draft.add("value")
  130. })
  131. finalized = 0
  132. const first = yield* state.reload().pipe(Effect.forkChild({ startImmediately: true }))
  133. yield* TestClock.adjust("250 millis")
  134. const second = yield* state.reload().pipe(Effect.forkChild({ startImmediately: true }))
  135. yield* TestClock.adjust("499 millis")
  136. expect(finalized).toBe(0)
  137. yield* TestClock.adjust("1 millis")
  138. yield* Fiber.join(first)
  139. yield* Fiber.join(second)
  140. expect(finalized).toBe(1)
  141. }),
  142. )
  143. })