index.test.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. import { describe, expect } from "bun:test"
  2. import { Cause, Effect, Exit, Schema } from "effect"
  3. import { SystemContext } from "@opencode-ai/core/system-context"
  4. import { it } from "../lib/effect"
  5. const key = SystemContext.Key.make
  6. const stringContext = (input: {
  7. key: string
  8. value: string | SystemContext.Unavailable
  9. baseline?: (value: string) => string
  10. update?: (previous: string, current: string) => string
  11. removed?: (value: string) => string
  12. }) =>
  13. SystemContext.make({
  14. key: key(input.key),
  15. codec: Schema.toCodecJson(Schema.String),
  16. load: Effect.succeed(input.value),
  17. baseline: input.baseline ?? String,
  18. update: input.update ?? ((_previous, current) => current),
  19. removed: input.removed,
  20. })
  21. describe("SystemContext", () => {
  22. it.effect("stores the canonical JSON encoding of the loaded value", () =>
  23. Effect.gen(function* () {
  24. const context = SystemContext.make({
  25. key: key("core/date"),
  26. codec: Schema.toCodecJson(Schema.DateFromString),
  27. load: Effect.succeed(new Date("2026-06-03T12:00:00.000Z")),
  28. baseline: (date) => date.toISOString(),
  29. update: (_previous, date) => date.toISOString(),
  30. removed: () => "Date removed",
  31. })
  32. expect((yield* SystemContext.initialize(context)).snapshot["core/date"].value).toBe("2026-06-03T12:00:00.000Z")
  33. }),
  34. )
  35. it.effect("loads once and initializes a baseline with a structured snapshot", () =>
  36. Effect.gen(function* () {
  37. let loads = 0
  38. const context = SystemContext.combine([
  39. SystemContext.make({
  40. key: key("core/date"),
  41. codec: Schema.toCodecJson(Schema.String),
  42. load: Effect.sync(() => {
  43. loads++
  44. return "2026-06-03"
  45. }),
  46. baseline: (date) => `Today's date is ${date}.`,
  47. update: (previous, current) => `The date changed from ${previous} to ${current}.`,
  48. removed: () => "The date was removed.",
  49. }),
  50. stringContext({ key: "core/location", value: "/repo", baseline: (value) => `Directory: ${value}` }),
  51. ])
  52. expect(yield* SystemContext.initialize(context)).toEqual({
  53. baseline: "Today's date is 2026-06-03.\n\nDirectory: /repo",
  54. snapshot: {
  55. "core/date": { value: "2026-06-03", removed: "The date was removed." },
  56. "core/location": { value: "/repo" },
  57. },
  58. })
  59. expect(loads).toBe(1)
  60. }),
  61. )
  62. it.effect("renders updates only after a structured value changes", () =>
  63. Effect.gen(function* () {
  64. const previous = {
  65. "core/date": { value: "2026-06-03", removed: "The date was removed." },
  66. "core/location": { value: "/repo", removed: "Removed: /repo" },
  67. }
  68. const changed = SystemContext.combine([
  69. stringContext({
  70. key: "core/date",
  71. value: "2026-06-04",
  72. update: (before, current) => `The date changed from ${before} to ${current}.`,
  73. removed: () => "The date was removed.",
  74. }),
  75. stringContext({ key: "core/location", value: "/repo" }),
  76. ])
  77. expect(yield* SystemContext.reconcile(changed, previous)).toEqual({
  78. _tag: "Updated",
  79. text: "The date changed from 2026-06-03 to 2026-06-04.",
  80. snapshot: {
  81. "core/date": { value: "2026-06-04", removed: "The date was removed." },
  82. "core/location": { value: "/repo", removed: "Removed: /repo" },
  83. },
  84. })
  85. expect(
  86. yield* SystemContext.reconcile(
  87. SystemContext.combine([
  88. stringContext({ key: "core/date", value: "2026-06-03", removed: () => "The date was removed." }),
  89. stringContext({ key: "core/location", value: "/repo" }),
  90. ]),
  91. previous,
  92. ),
  93. ).toEqual({ _tag: "Unchanged" })
  94. }),
  95. )
  96. it.effect("uses the baseline for a newly added source", () =>
  97. Effect.gen(function* () {
  98. const context = stringContext({
  99. key: "core/skills",
  100. value: "effect",
  101. baseline: (skill) => `Available skill: ${skill}`,
  102. })
  103. expect(yield* SystemContext.reconcile(context, {})).toEqual({
  104. _tag: "Updated",
  105. text: "Available skill: effect",
  106. snapshot: { "core/skills": { value: "effect" } },
  107. })
  108. }),
  109. )
  110. it.effect("retains admitted snapshots while a source is temporarily unavailable", () =>
  111. Effect.gen(function* () {
  112. const previous = { "core/remote": { value: "instructions", removed: "Instructions removed" } }
  113. const context = stringContext({ key: "core/remote", value: SystemContext.unavailable })
  114. expect(yield* SystemContext.reconcile(context, previous)).toEqual({ _tag: "Unchanged" })
  115. expect(yield* SystemContext.replace(context, previous)).toEqual({ _tag: "ReplacementBlocked" })
  116. expect(yield* SystemContext.replace(context, {})).toMatchObject({ _tag: "ReplacementReady" })
  117. }),
  118. )
  119. it.effect("blocks initialization while a source is unavailable", () =>
  120. Effect.gen(function* () {
  121. const exit = yield* SystemContext.initialize(
  122. stringContext({ key: "core/remote", value: SystemContext.unavailable }),
  123. ).pipe(Effect.exit)
  124. expect(Exit.isFailure(exit)).toBe(true)
  125. if (Exit.isFailure(exit))
  126. expect(Cause.squash(exit.cause)).toEqual(
  127. new SystemContext.InitializationBlocked({ keys: [key("core/remote")] }),
  128. )
  129. }),
  130. )
  131. it.effect("emits the previously stored removal message", () =>
  132. Effect.gen(function* () {
  133. expect(
  134. yield* SystemContext.reconcile(SystemContext.empty, {
  135. "core/instructions": { value: "contents", removed: "Instructions removed; stop applying them." },
  136. }),
  137. ).toEqual({
  138. _tag: "Updated",
  139. text: "Instructions removed; stop applying them.",
  140. snapshot: {},
  141. })
  142. }),
  143. )
  144. it.effect("requests replacement when a source without removal text disappears", () =>
  145. Effect.gen(function* () {
  146. expect(
  147. yield* SystemContext.reconcile(SystemContext.empty, { "core/date": { value: "2026-06-04" } }),
  148. ).toMatchObject({
  149. _tag: "ReplacementReady",
  150. })
  151. }),
  152. )
  153. it.effect("renders multiple removals in stable key order", () =>
  154. Effect.gen(function* () {
  155. expect(
  156. yield* SystemContext.reconcile(SystemContext.empty, {
  157. "core/z": { value: "z", removed: "Removed z" },
  158. "core/a": { value: "a", removed: "Removed a" },
  159. }),
  160. ).toMatchObject({ _tag: "Updated", text: "Removed a\n\nRemoved z" })
  161. }),
  162. )
  163. it.effect("rejects empty model-visible renderings", () =>
  164. Effect.gen(function* () {
  165. const exit = yield* SystemContext.initialize(
  166. stringContext({ key: "core/empty", value: "value", baseline: () => "" }),
  167. ).pipe(Effect.exit)
  168. expect(Exit.isFailure(exit)).toBe(true)
  169. if (Exit.isFailure(exit)) expect(Cause.pretty(exit.cause)).toContain("rendered an empty baseline")
  170. }),
  171. )
  172. it.effect("requests replacement when a stored value no longer decodes", () =>
  173. Effect.gen(function* () {
  174. expect(
  175. yield* SystemContext.reconcile(stringContext({ key: "core/date", value: "2026-06-04" }), {
  176. "core/date": { value: 42, removed: "Date removed" },
  177. }),
  178. ).toMatchObject({ _tag: "ReplacementReady" })
  179. }),
  180. )
  181. it.effect("replaces from one coherent source observation", () =>
  182. Effect.gen(function* () {
  183. let loads = 0
  184. const context = SystemContext.make({
  185. key: key("core/date"),
  186. codec: Schema.toCodecJson(Schema.String),
  187. load: Effect.sync(() => {
  188. loads++
  189. return "2026-06-04"
  190. }),
  191. baseline: String,
  192. update: (_previous, current) => current,
  193. })
  194. expect(yield* SystemContext.reconcile(context, { "core/date": { value: 42 } })).toMatchObject({
  195. _tag: "ReplacementReady",
  196. generation: { baseline: "2026-06-04" },
  197. })
  198. expect(loads).toBe(1)
  199. }),
  200. )
  201. it.effect("does not render discarded updates while replacing", () =>
  202. Effect.gen(function* () {
  203. let updates = 0
  204. const context = SystemContext.combine([
  205. stringContext({
  206. key: "core/date",
  207. value: "2026-06-04",
  208. update: () => {
  209. updates++
  210. return "updated"
  211. },
  212. }),
  213. stringContext({ key: "core/location", value: "/repo" }),
  214. ])
  215. expect(
  216. yield* SystemContext.reconcile(context, {
  217. "core/date": { value: "2026-06-03" },
  218. "core/location": { value: 42 },
  219. }),
  220. ).toMatchObject({ _tag: "ReplacementReady" })
  221. expect(updates).toBe(0)
  222. }),
  223. )
  224. it.effect("blocks an incompatible replacement while another admitted source is unavailable", () =>
  225. Effect.gen(function* () {
  226. const previous = {
  227. "core/date": { value: 42, removed: "Date removed" },
  228. "core/remote": { value: "instructions", removed: "Instructions removed" },
  229. }
  230. const context = SystemContext.combine([
  231. stringContext({ key: "core/date", value: "2026-06-04" }),
  232. stringContext({ key: "core/remote", value: SystemContext.unavailable }),
  233. ])
  234. expect(yield* SystemContext.reconcile(context, previous)).toEqual({ _tag: "ReplacementBlocked" })
  235. expect(yield* SystemContext.replace(context, previous)).toEqual({ _tag: "ReplacementBlocked" })
  236. }),
  237. )
  238. it.effect("rejects duplicate source keys", () =>
  239. Effect.sync(() => {
  240. expect(() =>
  241. SystemContext.combine([
  242. stringContext({ key: "core/date", value: "one" }),
  243. stringContext({ key: "core/date", value: "two" }),
  244. ]),
  245. ).toThrow(new SystemContext.DuplicateKeyError({ key: key("core/date") }))
  246. }),
  247. )
  248. it.effect("combines contexts in order", () =>
  249. Effect.gen(function* () {
  250. expect(
  251. (yield* SystemContext.initialize(
  252. SystemContext.combine([
  253. stringContext({ key: "core/date", value: "date" }),
  254. stringContext({ key: "core/location", value: "location" }),
  255. ]),
  256. )).baseline,
  257. ).toBe("date\n\nlocation")
  258. }),
  259. )
  260. it.effect("requires namespaced source keys", () =>
  261. Effect.sync(() => {
  262. const decodeKey = Schema.decodeUnknownSync(SystemContext.Key)
  263. expect(decodeKey("core/date")).toBe(key("core/date"))
  264. expect(() => decodeKey("date")).toThrow()
  265. }),
  266. )
  267. it.effect("requires namespaced durable snapshot keys", () =>
  268. Effect.sync(() => {
  269. const decodeSnapshot = Schema.decodeUnknownSync(SystemContext.Snapshot)
  270. expect(Object.keys(decodeSnapshot({ "core/date": { value: "date" } }))).toEqual(["core/date"])
  271. expect(() => decodeSnapshot({ date: { value: "date" } })).toThrow()
  272. expect(() => decodeSnapshot({ "core/date": { value: "date", removed: "" } })).toThrow()
  273. }),
  274. )
  275. })