state.test.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  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("runs transforms during every reload", () =>
  36. Effect.gen(function* () {
  37. let value = "first"
  38. const state = State.create({
  39. initial: () => ({ values: [] as string[] }),
  40. draft: (draft) => ({ add: (item: string) => draft.values.push(item) }),
  41. })
  42. yield* state.transform((editor) => {
  43. editor.add(value)
  44. })
  45. expect(state.get().values).toEqual(["first"])
  46. value = "second"
  47. const reload = yield* state.reload().pipe(Effect.forkChild({ startImmediately: true }))
  48. yield* TestClock.adjust("500 millis")
  49. yield* Fiber.join(reload)
  50. expect(state.get().values).toEqual(["second"])
  51. }),
  52. )
  53. it.effect("disposes a transform once and rebuilds remaining state", () =>
  54. Effect.gen(function* () {
  55. const state = State.create({
  56. initial: () => ({ values: [] as string[] }),
  57. draft: (draft) => ({ add: (item: string) => draft.values.push(item) }),
  58. })
  59. yield* state.transform((editor) => {
  60. editor.add("first")
  61. })
  62. const registration = yield* state.transform((editor) => {
  63. editor.add("second")
  64. })
  65. expect(state.get().values).toEqual(["first", "second"])
  66. yield* registration.dispose
  67. expect(state.get().values).toEqual(["first"])
  68. yield* registration.dispose
  69. expect(state.get().values).toEqual(["first"])
  70. }),
  71. )
  72. it.effect("batches automatic rebuilds", () =>
  73. Effect.gen(function* () {
  74. let finalized = 0
  75. const first = State.create({
  76. initial: () => ({ values: [] as string[] }),
  77. draft: (draft) => ({ add: (item: string) => draft.values.push(item) }),
  78. finalize: () => Effect.sync(() => finalized++),
  79. })
  80. const second = State.create({
  81. initial: () => ({ values: [] as string[] }),
  82. draft: (draft) => ({ add: (item: string) => draft.values.push(item) }),
  83. finalize: () => Effect.sync(() => finalized++),
  84. })
  85. yield* State.batch(
  86. Effect.gen(function* () {
  87. yield* first.transform((draft) => {
  88. draft.add("first")
  89. })
  90. yield* first.transform((draft) => {
  91. draft.add("second")
  92. })
  93. yield* second.transform((draft) => {
  94. draft.add("third")
  95. })
  96. expect(finalized).toBe(0)
  97. }),
  98. )
  99. expect(first.get().values).toEqual(["first", "second"])
  100. expect(second.get().values).toEqual(["third"])
  101. expect(finalized).toBe(2)
  102. }),
  103. )
  104. it.effect("debounces reload bursts", () =>
  105. Effect.gen(function* () {
  106. let finalized = 0
  107. const state = State.create({
  108. initial: () => ({ values: [] as string[] }),
  109. draft: (draft) => ({ add: (item: string) => draft.values.push(item) }),
  110. finalize: () => Effect.sync(() => finalized++),
  111. })
  112. yield* state.transform((draft) => {
  113. draft.add("value")
  114. })
  115. finalized = 0
  116. const first = yield* state.reload().pipe(Effect.forkChild({ startImmediately: true }))
  117. yield* TestClock.adjust("250 millis")
  118. const second = yield* state.reload().pipe(Effect.forkChild({ startImmediately: true }))
  119. yield* TestClock.adjust("499 millis")
  120. expect(finalized).toBe(0)
  121. yield* TestClock.adjust("1 millis")
  122. yield* Fiber.join(first)
  123. yield* Fiber.join(second)
  124. expect(finalized).toBe(1)
  125. }),
  126. )
  127. })