@yolk-sdk/emulators 0.1.0-canary.96

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (333) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2135 -0
  3. package/dist/anthropic.d.mts +269 -0
  4. package/dist/anthropic.d.mts.map +1 -0
  5. package/dist/anthropic.mjs +177 -0
  6. package/dist/anthropic.mjs.map +1 -0
  7. package/dist/chat-completions.d.mts +288 -0
  8. package/dist/chat-completions.d.mts.map +1 -0
  9. package/dist/chat-completions.mjs +451 -0
  10. package/dist/chat-completions.mjs.map +1 -0
  11. package/dist/codex.d.mts +265 -0
  12. package/dist/codex.d.mts.map +1 -0
  13. package/dist/codex.mjs +199 -0
  14. package/dist/codex.mjs.map +1 -0
  15. package/dist/dropbox/api.d.mts +63 -0
  16. package/dist/dropbox/api.d.mts.map +1 -0
  17. package/dist/dropbox/api.mjs +565 -0
  18. package/dist/dropbox/api.mjs.map +1 -0
  19. package/dist/dropbox/state.d.mts +136 -0
  20. package/dist/dropbox/state.d.mts.map +1 -0
  21. package/dist/dropbox/state.mjs +209 -0
  22. package/dist/dropbox/state.mjs.map +1 -0
  23. package/dist/dropbox.d.mts +79 -0
  24. package/dist/dropbox.d.mts.map +1 -0
  25. package/dist/dropbox.mjs +124 -0
  26. package/dist/dropbox.mjs.map +1 -0
  27. package/dist/email-fixtures.d.mts +36 -0
  28. package/dist/email-fixtures.d.mts.map +1 -0
  29. package/dist/email-fixtures.mjs +1080 -0
  30. package/dist/email-fixtures.mjs.map +1 -0
  31. package/dist/email.d.mts +160 -0
  32. package/dist/email.d.mts.map +1 -0
  33. package/dist/email.mjs +608 -0
  34. package/dist/email.mjs.map +1 -0
  35. package/dist/emulator-compose.d.mts +40 -0
  36. package/dist/emulator-compose.d.mts.map +1 -0
  37. package/dist/emulator-compose.mjs +91 -0
  38. package/dist/emulator-compose.mjs.map +1 -0
  39. package/dist/emulator-http.d.mts +53 -0
  40. package/dist/emulator-http.d.mts.map +1 -0
  41. package/dist/emulator-http.mjs +117 -0
  42. package/dist/emulator-http.mjs.map +1 -0
  43. package/dist/emulator-kernel.d.mts +176 -0
  44. package/dist/emulator-kernel.d.mts.map +1 -0
  45. package/dist/emulator-kernel.mjs +413 -0
  46. package/dist/emulator-kernel.mjs.map +1 -0
  47. package/dist/fixture-route.d.mts +129 -0
  48. package/dist/fixture-route.d.mts.map +1 -0
  49. package/dist/fixture-route.mjs +257 -0
  50. package/dist/fixture-route.mjs.map +1 -0
  51. package/dist/fortnox/api.d.mts +92 -0
  52. package/dist/fortnox/api.d.mts.map +1 -0
  53. package/dist/fortnox/api.mjs +750 -0
  54. package/dist/fortnox/api.mjs.map +1 -0
  55. package/dist/fortnox/state.d.mts +533 -0
  56. package/dist/fortnox/state.d.mts.map +1 -0
  57. package/dist/fortnox/state.mjs +612 -0
  58. package/dist/fortnox/state.mjs.map +1 -0
  59. package/dist/fortnox.d.mts +128 -0
  60. package/dist/fortnox.d.mts.map +1 -0
  61. package/dist/fortnox.mjs +403 -0
  62. package/dist/fortnox.mjs.map +1 -0
  63. package/dist/gateway-evaluate-recordings.d.mts +12 -0
  64. package/dist/gateway-evaluate-recordings.d.mts.map +1 -0
  65. package/dist/gateway-evaluate-recordings.mjs +128 -0
  66. package/dist/gateway-evaluate-recordings.mjs.map +1 -0
  67. package/dist/gateway.d.mts +272 -0
  68. package/dist/gateway.d.mts.map +1 -0
  69. package/dist/gateway.mjs +400 -0
  70. package/dist/gateway.mjs.map +1 -0
  71. package/dist/github/api.d.mts +63 -0
  72. package/dist/github/api.d.mts.map +1 -0
  73. package/dist/github/api.mjs +527 -0
  74. package/dist/github/api.mjs.map +1 -0
  75. package/dist/github/state.d.mts +272 -0
  76. package/dist/github/state.d.mts.map +1 -0
  77. package/dist/github/state.mjs +303 -0
  78. package/dist/github/state.mjs.map +1 -0
  79. package/dist/github.d.mts +77 -0
  80. package/dist/github.d.mts.map +1 -0
  81. package/dist/github.mjs +180 -0
  82. package/dist/github.mjs.map +1 -0
  83. package/dist/google/calendar.d.mts +10 -0
  84. package/dist/google/calendar.d.mts.map +1 -0
  85. package/dist/google/calendar.mjs +252 -0
  86. package/dist/google/calendar.mjs.map +1 -0
  87. package/dist/google/drive.d.mts +8 -0
  88. package/dist/google/drive.d.mts.map +1 -0
  89. package/dist/google/drive.mjs +252 -0
  90. package/dist/google/drive.mjs.map +1 -0
  91. package/dist/google/gmail.d.mts +10 -0
  92. package/dist/google/gmail.d.mts.map +1 -0
  93. package/dist/google/gmail.mjs +591 -0
  94. package/dist/google/gmail.mjs.map +1 -0
  95. package/dist/google/shared.d.mts +107 -0
  96. package/dist/google/shared.d.mts.map +1 -0
  97. package/dist/google/shared.mjs +146 -0
  98. package/dist/google/shared.mjs.map +1 -0
  99. package/dist/google/state.d.mts +385 -0
  100. package/dist/google/state.d.mts.map +1 -0
  101. package/dist/google/state.mjs +496 -0
  102. package/dist/google/state.mjs.map +1 -0
  103. package/dist/google.d.mts +75 -0
  104. package/dist/google.d.mts.map +1 -0
  105. package/dist/google.mjs +210 -0
  106. package/dist/google.mjs.map +1 -0
  107. package/dist/linkedin-search/api.d.mts +45 -0
  108. package/dist/linkedin-search/api.d.mts.map +1 -0
  109. package/dist/linkedin-search/api.mjs +181 -0
  110. package/dist/linkedin-search/api.mjs.map +1 -0
  111. package/dist/linkedin-search/state.d.mts +123 -0
  112. package/dist/linkedin-search/state.d.mts.map +1 -0
  113. package/dist/linkedin-search/state.mjs +261 -0
  114. package/dist/linkedin-search/state.mjs.map +1 -0
  115. package/dist/linkedin-search.d.mts +69 -0
  116. package/dist/linkedin-search.d.mts.map +1 -0
  117. package/dist/linkedin-search.mjs +158 -0
  118. package/dist/linkedin-search.mjs.map +1 -0
  119. package/dist/mcp/api.d.mts +44 -0
  120. package/dist/mcp/api.d.mts.map +1 -0
  121. package/dist/mcp/api.mjs +557 -0
  122. package/dist/mcp/api.mjs.map +1 -0
  123. package/dist/mcp/recordings.d.mts +40 -0
  124. package/dist/mcp/recordings.d.mts.map +1 -0
  125. package/dist/mcp/recordings.mjs +2390 -0
  126. package/dist/mcp/recordings.mjs.map +1 -0
  127. package/dist/mcp/state.d.mts +71 -0
  128. package/dist/mcp/state.d.mts.map +1 -0
  129. package/dist/mcp/state.mjs +83 -0
  130. package/dist/mcp/state.mjs.map +1 -0
  131. package/dist/mcp.d.mts +84 -0
  132. package/dist/mcp.d.mts.map +1 -0
  133. package/dist/mcp.mjs +241 -0
  134. package/dist/mcp.mjs.map +1 -0
  135. package/dist/messages.d.mts +183 -0
  136. package/dist/messages.d.mts.map +1 -0
  137. package/dist/messages.mjs +532 -0
  138. package/dist/messages.mjs.map +1 -0
  139. package/dist/microsoft/api.d.mts +47 -0
  140. package/dist/microsoft/api.d.mts.map +1 -0
  141. package/dist/microsoft/api.mjs +178 -0
  142. package/dist/microsoft/api.mjs.map +1 -0
  143. package/dist/microsoft/calendar.d.mts +30 -0
  144. package/dist/microsoft/calendar.d.mts.map +1 -0
  145. package/dist/microsoft/calendar.mjs +271 -0
  146. package/dist/microsoft/calendar.mjs.map +1 -0
  147. package/dist/microsoft/drive.d.mts +36 -0
  148. package/dist/microsoft/drive.d.mts.map +1 -0
  149. package/dist/microsoft/drive.mjs +298 -0
  150. package/dist/microsoft/drive.mjs.map +1 -0
  151. package/dist/microsoft/graph.d.mts +198 -0
  152. package/dist/microsoft/graph.d.mts.map +1 -0
  153. package/dist/microsoft/graph.mjs +264 -0
  154. package/dist/microsoft/graph.mjs.map +1 -0
  155. package/dist/microsoft/mail.d.mts +48 -0
  156. package/dist/microsoft/mail.d.mts.map +1 -0
  157. package/dist/microsoft/mail.mjs +488 -0
  158. package/dist/microsoft/mail.mjs.map +1 -0
  159. package/dist/microsoft/state.d.mts +480 -0
  160. package/dist/microsoft/state.d.mts.map +1 -0
  161. package/dist/microsoft/state.mjs +625 -0
  162. package/dist/microsoft/state.mjs.map +1 -0
  163. package/dist/microsoft.d.mts +168 -0
  164. package/dist/microsoft.d.mts.map +1 -0
  165. package/dist/microsoft.mjs +483 -0
  166. package/dist/microsoft.mjs.map +1 -0
  167. package/dist/node.d.mts +47 -0
  168. package/dist/node.d.mts.map +1 -0
  169. package/dist/node.mjs +178 -0
  170. package/dist/node.mjs.map +1 -0
  171. package/dist/notion/api.d.mts +51 -0
  172. package/dist/notion/api.d.mts.map +1 -0
  173. package/dist/notion/api.mjs +507 -0
  174. package/dist/notion/api.mjs.map +1 -0
  175. package/dist/notion/state.d.mts +329 -0
  176. package/dist/notion/state.d.mts.map +1 -0
  177. package/dist/notion/state.mjs +432 -0
  178. package/dist/notion/state.mjs.map +1 -0
  179. package/dist/notion.d.mts +72 -0
  180. package/dist/notion.d.mts.map +1 -0
  181. package/dist/notion.mjs +127 -0
  182. package/dist/notion.mjs.map +1 -0
  183. package/dist/openai.d.mts +165 -0
  184. package/dist/openai.d.mts.map +1 -0
  185. package/dist/openai.mjs +141 -0
  186. package/dist/openai.mjs.map +1 -0
  187. package/dist/opencode-recordings.d.mts +7 -0
  188. package/dist/opencode-recordings.d.mts.map +1 -0
  189. package/dist/opencode-recordings.mjs +222 -0
  190. package/dist/opencode-recordings.mjs.map +1 -0
  191. package/dist/opencode.d.mts +70 -0
  192. package/dist/opencode.d.mts.map +1 -0
  193. package/dist/opencode.mjs +217 -0
  194. package/dist/opencode.mjs.map +1 -0
  195. package/dist/r2-fixtures.d.mts +36 -0
  196. package/dist/r2-fixtures.d.mts.map +1 -0
  197. package/dist/r2-fixtures.mjs +245 -0
  198. package/dist/r2-fixtures.mjs.map +1 -0
  199. package/dist/r2-guard.d.mts +43 -0
  200. package/dist/r2-guard.d.mts.map +1 -0
  201. package/dist/r2-guard.mjs +215 -0
  202. package/dist/r2-guard.mjs.map +1 -0
  203. package/dist/r2.d.mts +166 -0
  204. package/dist/r2.d.mts.map +1 -0
  205. package/dist/r2.mjs +517 -0
  206. package/dist/r2.mjs.map +1 -0
  207. package/dist/responses.d.mts +231 -0
  208. package/dist/responses.d.mts.map +1 -0
  209. package/dist/responses.mjs +556 -0
  210. package/dist/responses.mjs.map +1 -0
  211. package/dist/route-evidence.d.mts +45 -0
  212. package/dist/route-evidence.d.mts.map +1 -0
  213. package/dist/route-evidence.mjs +56 -0
  214. package/dist/route-evidence.mjs.map +1 -0
  215. package/dist/router.d.mts +93 -0
  216. package/dist/router.d.mts.map +1 -0
  217. package/dist/router.mjs +259 -0
  218. package/dist/router.mjs.map +1 -0
  219. package/dist/stateful-core.d.mts +17 -0
  220. package/dist/stateful-core.d.mts.map +1 -0
  221. package/dist/stateful-core.mjs +41 -0
  222. package/dist/stateful-core.mjs.map +1 -0
  223. package/dist/stateful-emulator.d.mts +565 -0
  224. package/dist/stateful-emulator.d.mts.map +1 -0
  225. package/dist/stateful-emulator.mjs +1228 -0
  226. package/dist/stateful-emulator.mjs.map +1 -0
  227. package/dist/stateful-secrets.d.mts +84 -0
  228. package/dist/stateful-secrets.d.mts.map +1 -0
  229. package/dist/stateful-secrets.mjs +216 -0
  230. package/dist/stateful-secrets.mjs.map +1 -0
  231. package/dist/subscription-usage-recordings.d.mts +9 -0
  232. package/dist/subscription-usage-recordings.d.mts.map +1 -0
  233. package/dist/subscription-usage-recordings.mjs +53 -0
  234. package/dist/subscription-usage-recordings.mjs.map +1 -0
  235. package/dist/subscription-usage.d.mts +53 -0
  236. package/dist/subscription-usage.d.mts.map +1 -0
  237. package/dist/subscription-usage.mjs +21 -0
  238. package/dist/subscription-usage.mjs.map +1 -0
  239. package/dist/telegram/api.d.mts +34 -0
  240. package/dist/telegram/api.d.mts.map +1 -0
  241. package/dist/telegram/api.mjs +257 -0
  242. package/dist/telegram/api.mjs.map +1 -0
  243. package/dist/telegram/state.d.mts +128 -0
  244. package/dist/telegram/state.d.mts.map +1 -0
  245. package/dist/telegram/state.mjs +154 -0
  246. package/dist/telegram/state.mjs.map +1 -0
  247. package/dist/telegram.d.mts +62 -0
  248. package/dist/telegram.d.mts.map +1 -0
  249. package/dist/telegram.mjs +127 -0
  250. package/dist/telegram.mjs.map +1 -0
  251. package/dist/todoist/api.d.mts +55 -0
  252. package/dist/todoist/api.d.mts.map +1 -0
  253. package/dist/todoist/api.mjs +415 -0
  254. package/dist/todoist/api.mjs.map +1 -0
  255. package/dist/todoist/state.d.mts +275 -0
  256. package/dist/todoist/state.d.mts.map +1 -0
  257. package/dist/todoist/state.mjs +345 -0
  258. package/dist/todoist/state.mjs.map +1 -0
  259. package/dist/todoist.d.mts +68 -0
  260. package/dist/todoist.d.mts.map +1 -0
  261. package/dist/todoist.mjs +181 -0
  262. package/dist/todoist.mjs.map +1 -0
  263. package/dist/xai.d.mts +267 -0
  264. package/dist/xai.d.mts.map +1 -0
  265. package/dist/xai.mjs +252 -0
  266. package/dist/xai.mjs.map +1 -0
  267. package/package.json +157 -0
  268. package/src/anthropic.ts +289 -0
  269. package/src/chat-completions.ts +924 -0
  270. package/src/codex.ts +296 -0
  271. package/src/dropbox/api.ts +1084 -0
  272. package/src/dropbox/state.ts +364 -0
  273. package/src/dropbox.ts +203 -0
  274. package/src/email-fixtures.ts +1297 -0
  275. package/src/email.ts +1108 -0
  276. package/src/emulator-compose.ts +147 -0
  277. package/src/emulator-http.ts +184 -0
  278. package/src/emulator-kernel.ts +844 -0
  279. package/src/fixture-route.ts +561 -0
  280. package/src/fortnox/api.ts +1352 -0
  281. package/src/fortnox/state.ts +798 -0
  282. package/src/fortnox.ts +801 -0
  283. package/src/gateway-evaluate-recordings.ts +153 -0
  284. package/src/gateway.ts +530 -0
  285. package/src/github/api.ts +986 -0
  286. package/src/github/state.ts +439 -0
  287. package/src/github.ts +271 -0
  288. package/src/google/calendar.ts +486 -0
  289. package/src/google/drive.ts +471 -0
  290. package/src/google/gmail.ts +1011 -0
  291. package/src/google/shared.ts +321 -0
  292. package/src/google/state.ts +686 -0
  293. package/src/google.ts +298 -0
  294. package/src/linkedin-search/api.ts +363 -0
  295. package/src/linkedin-search/state.ts +373 -0
  296. package/src/linkedin-search.ts +241 -0
  297. package/src/mcp/api.ts +995 -0
  298. package/src/mcp/recordings.ts +2665 -0
  299. package/src/mcp/state.ts +125 -0
  300. package/src/mcp.ts +319 -0
  301. package/src/messages.ts +844 -0
  302. package/src/microsoft/api.ts +426 -0
  303. package/src/microsoft/calendar.ts +491 -0
  304. package/src/microsoft/drive.ts +541 -0
  305. package/src/microsoft/graph.ts +550 -0
  306. package/src/microsoft/mail.ts +833 -0
  307. package/src/microsoft/state.ts +822 -0
  308. package/src/microsoft.ts +982 -0
  309. package/src/node.ts +293 -0
  310. package/src/notion/api.ts +976 -0
  311. package/src/notion/state.ts +512 -0
  312. package/src/notion.ts +210 -0
  313. package/src/openai.ts +198 -0
  314. package/src/opencode-recordings.ts +257 -0
  315. package/src/opencode.ts +271 -0
  316. package/src/r2-fixtures.ts +295 -0
  317. package/src/r2-guard.ts +323 -0
  318. package/src/r2.ts +890 -0
  319. package/src/responses.ts +901 -0
  320. package/src/route-evidence.ts +90 -0
  321. package/src/router.ts +490 -0
  322. package/src/stateful-core.ts +70 -0
  323. package/src/stateful-emulator.ts +2571 -0
  324. package/src/stateful-secrets.ts +299 -0
  325. package/src/subscription-usage-recordings.ts +77 -0
  326. package/src/subscription-usage.ts +68 -0
  327. package/src/telegram/api.ts +437 -0
  328. package/src/telegram/state.ts +229 -0
  329. package/src/telegram.ts +227 -0
  330. package/src/todoist/api.ts +736 -0
  331. package/src/todoist/state.ts +456 -0
  332. package/src/todoist.ts +316 -0
  333. package/src/xai.ts +341 -0
package/src/notion.ts ADDED
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Stateful Notion emulator (the `/v1` routes on `https://api.notion.com` at API version
3
+ * 2025-09-03), built on the upstream `@emulators/core` custom runtime, with a request ledger,
4
+ * status faults, and an `/_emulate/*` control plane.
5
+ *
6
+ * It never imports SDK code: its wire shapes, error envelopes, and default seed are copied as data
7
+ * from the synthetic Notion conformance fixtures, and every route names the conformance cases it
8
+ * follows in `notionEmulatorRoutes`. Only the routes those eight cases (with their cleanup) send
9
+ * are emulated. Response behaviour comes only from the fixtures: anything they do not show answers
10
+ * one ledgered 400 not-emulated, writes nothing, and uses up no fault. That includes a search
11
+ * without matches or matching a trashed page, so the read-only leftover lookup fails on a clean
12
+ * workspace and after the write case (the live runner prints its lookup-failed WARN). Search
13
+ * considers only the pages whose content the state holds (an implied page never matches) and
14
+ * refuses a match shown only as a query row. Every route answers only on the recorded origin
15
+ * `https://api.notion.com` (`fetchOn(origin)` serves it behind a loopback rewrite).
16
+ *
17
+ * Request-shape latitude (`/notion`, the only accepted deviations): any bearer value (never
18
+ * checked or stored); extra request headers; JSON key order; `content-type` media-type
19
+ * parameters; the order of query parameters; Notion ids with or without dashes, in any case; any
20
+ * search `query` (looked up in the state); and any `page_size` from 1 to 100 whose page shows
21
+ * only recorded results (the data source query: 1). `Notion-Version` must be `2025-09-03`.
22
+ * Everything else (other keys, filters, booleans, sorts, query parameters, another origin,
23
+ * titles, repeated or missing `page_size`, and cursors not issued for the same list since the
24
+ * last reset or whose list changed) is not emulated.
25
+ *
26
+ * Node-only: `@emulators/core` imports Node builtins, so the core is loaded lazily by
27
+ * `makeNotionEmulator` (importing this module has no side effects).
28
+ *
29
+ * @experimental
30
+ */
31
+ import { Data, Predicate } from 'effect'
32
+ import {
33
+ notionApiRoutes,
34
+ notionEmulatorDrillKnobs,
35
+ notionEmulatorVersion,
36
+ type NotionApiEnv,
37
+ type NotionEmulatorDrills
38
+ } from './notion/api.ts'
39
+ import {
40
+ buildSeedState,
41
+ decodeState,
42
+ type NotionEmulatorSeed,
43
+ type NotionEmulatorState
44
+ } from './notion/state.ts'
45
+ import type { EmulatorRouteEvidence } from './route-evidence.ts'
46
+ import {
47
+ StatefulFault,
48
+ StatefulFaultMatch,
49
+ checkBooleanDrills,
50
+ makeStatefulEmulator,
51
+ routeEvidence,
52
+ type StatefulCoverage,
53
+ type StatefulEmulatorApi,
54
+ type StatefulFaultState,
55
+ type StatefulInputKind,
56
+ type StatefulLedgerEntry
57
+ } from './stateful-emulator.ts'
58
+ import { statefulCoreRuntime } from './stateful-core.ts'
59
+
60
+ export type { EmulatorEvidence, EmulatorRouteEvidence } from './route-evidence.ts'
61
+
62
+ export { emulatorEvidenceHeader } from './route-evidence.ts'
63
+
64
+ export {
65
+ notionEmulatorBasePath,
66
+ notionEmulatorOrigin,
67
+ notionEmulatorVersion,
68
+ type NotionEmulatorDrills
69
+ } from './notion/api.ts'
70
+
71
+ export {
72
+ NotionEmulatorBlock,
73
+ NotionEmulatorBotUser,
74
+ NotionEmulatorDataSource,
75
+ NotionEmulatorDatabase,
76
+ NotionEmulatorImpliedPage,
77
+ NotionEmulatorPage,
78
+ NotionEmulatorParent,
79
+ NotionEmulatorProfile,
80
+ NotionEmulatorPropertyItems,
81
+ NotionEmulatorSeed,
82
+ NotionEmulatorStateSchema,
83
+ type NotionEmulatorState
84
+ } from './notion/state.ts'
85
+
86
+ /**
87
+ * Route evidence manifest: every emulated route, whether it writes, and the conformance cases
88
+ * whose (currently synthetic, unverified) wire claims it follows. Kept in sync with the handlers
89
+ * by construction (both come from one route table).
90
+ */
91
+ export const notionEmulatorRoutes: ReadonlyArray<EmulatorRouteEvidence> =
92
+ notionApiRoutes.map(routeEvidence)
93
+
94
+ /**
95
+ * Optional fault filter; an omitted field matches every request. `method` is the HTTP method;
96
+ * `path` is the raw request path (ending in `*`, a prefix); `route` is a route template of the
97
+ * manifest (another value is rejected when the fault is added).
98
+ */
99
+ export const NotionFaultMatch = StatefulFaultMatch
100
+
101
+ export type NotionFaultMatch = typeof StatefulFaultMatch.Type
102
+
103
+ /**
104
+ * A status fault (400-599): answer matching requests with this status, headers, and body instead
105
+ * of the route's write, so nothing is written. The body defaults to
106
+ * `{ error: { type: 'emulator_fault', message } }`; `count` limits how many requests it answers.
107
+ * Only a request the emulator would answer reaches a fault: a request that is not emulated, by
108
+ * its shape or by the state, never uses one up.
109
+ */
110
+ export const NotionFault = StatefulFault
111
+
112
+ export type NotionFault = StatefulFault
113
+
114
+ export type NotionFaultState = StatefulFaultState
115
+
116
+ export type NotionLedgerEntry = StatefulLedgerEntry
117
+
118
+ export type NotionCoverage = StatefulCoverage
119
+
120
+ /** Invalid emulator input from the JS API: a seed, a fault, or an option. A programmer error. */
121
+ export class NotionEmulatorInputInvalid extends Data.TaggedError('NotionEmulatorInputInvalid')<{
122
+ readonly input: StatefulInputKind
123
+ readonly reason: string
124
+ }> {
125
+ override get message(): string {
126
+ return `Invalid Notion emulator ${this.input}: ${this.reason}`
127
+ }
128
+ }
129
+
130
+ export type NotionEmulatorOptions = {
131
+ /** Typed seed; defaults to the fixture entities (`profile: 'default'`). */
132
+ readonly seed?: NotionEmulatorSeed
133
+ /** Clock in epoch milliseconds (created page timestamps). Defaults to `Date.now`. */
134
+ readonly now?: () => number
135
+ /** Drill knobs (tests only): make the emulator disagree with one conformance claim. */
136
+ readonly drills?: NotionEmulatorDrills
137
+ }
138
+
139
+ export type NotionEmulator = StatefulEmulatorApi<NotionEmulatorState, NotionEmulatorSeed>
140
+
141
+ const inputInvalid = (input: StatefulInputKind, reason: string) =>
142
+ new NotionEmulatorInputInvalid({ input, reason })
143
+
144
+ /**
145
+ * Create a stateful Notion emulator on the `@emulators/core` custom runtime. Each call has its
146
+ * own state, ledger, and faults. Rejects with `NotionEmulatorInputInvalid` for an invalid seed or
147
+ * option. Every request needs `Notion-Version: 2025-09-03` (else not emulated). See
148
+ * `src/stateful-emulator.ts` for the request precedence.
149
+ */
150
+ export const makeNotionEmulator = async (
151
+ options: NotionEmulatorOptions = {}
152
+ ): Promise<NotionEmulator> => {
153
+ const initial = buildSeedState(options.seed ?? {})
154
+
155
+ if (Predicate.isString(initial)) {
156
+ throw inputInvalid('seed', initial)
157
+ }
158
+
159
+ checkBooleanDrills(options.drills, notionEmulatorDrillKnobs, inputInvalid)
160
+
161
+ const drills = options.drills ?? {}
162
+
163
+ const env: NotionApiEnv = {
164
+ now: options.now ?? (() => Date.now()),
165
+ drills: {
166
+ searchRepeatsResults: drills.searchRepeatsResults === true,
167
+ botUserAsPerson: drills.botUserAsPerson === true,
168
+ envelopeStatusMismatch: drills.envelopeStatusMismatch === true,
169
+ omitTitlePlainText: drills.omitTitlePlainText === true,
170
+ blockCursorRepeats: drills.blockCursorRepeats === true,
171
+ rejectDoubleEncodedPropertyId: drills.rejectDoubleEncodedPropertyId === true,
172
+ rowParentAsDatabase: drills.rowParentAsDatabase === true,
173
+ trashedPageNotFound: drills.trashedPageNotFound === true
174
+ },
175
+ cursors: new Map(),
176
+ propertyCursorCounter: { next: 1 },
177
+ generation: { current: 0 },
178
+ firstIssued: new Map()
179
+ }
180
+
181
+ const api = await makeStatefulEmulator<NotionEmulatorState, NotionApiEnv, NotionEmulatorSeed>(
182
+ {
183
+ routes: notionApiRoutes,
184
+ env,
185
+ initial,
186
+ buildSeed: buildSeedState,
187
+ requestProblem: header =>
188
+ header('notion-version') === notionEmulatorVersion
189
+ ? undefined
190
+ : `Notion-Version other than ${notionEmulatorVersion} (the version every fixture sends) is not emulated`,
191
+ recordHeaders: [{ name: 'notion-version', json: false }],
192
+ // Cursor values never cross a reset or seed: property cursor numbers keep advancing, and the
193
+ // generation makes later search and block cursors distinct from earlier ones.
194
+ clearRuntime: () => {
195
+ env.cursors.clear()
196
+ env.generation.current += 1
197
+ },
198
+ runtimeState: () => ({}),
199
+ seedSummary: state => ({
200
+ pages: state.pages.length,
201
+ blocks: state.blocks.length,
202
+ dataSources: state.dataSources.length
203
+ }),
204
+ inputInvalid
205
+ },
206
+ statefulCoreRuntime({ name: 'notion', initial, decodeState, inputInvalid })
207
+ )
208
+
209
+ return api
210
+ }
package/src/openai.ts ADDED
@@ -0,0 +1,198 @@
1
+ /**
2
+ * OpenAI Chat Completions emulator: a plain fetch handler for
3
+ * `POST /v1/chat/completions` (origin `https://api.openai.com`), with scripted
4
+ * turns, wire faults, a request ledger, and an `/_emulate/*` control plane.
5
+ *
6
+ * It never imports SDK code: its wire shapes follow the synthetic OpenAI chat
7
+ * conformance fixtures (`chat.completion.chunk` SSE, usage chunk,
8
+ * `data: [DONE]`, the `chat.completion` JSON body, and the
9
+ * `{ error: { message, type, param, code } }` envelope) and are linked to
10
+ * those conformance case ids in `openAiEmulatorRoutes`. The Chat Completions
11
+ * machinery is shared with the Gateway emulator (`chat-completions.ts`).
12
+ *
13
+ * Reasoning models are not emulated yet: no default model streams reasoning,
14
+ * and scripted turns cannot carry reasoning.
15
+ *
16
+ * Runtime-portable Web APIs only (`Request`, `Response`, `ReadableStream`,
17
+ * `TextEncoder`, `URL`); no Effect runtime is required to use it.
18
+ *
19
+ * @experimental
20
+ */
21
+ import { Data } from 'effect'
22
+ import * as Schema from 'effect/Schema'
23
+ import {
24
+ ChatFault,
25
+ ChatFaultMatch,
26
+ ChatScriptedError,
27
+ ChatScriptedToolCall,
28
+ ChatScriptedUsage,
29
+ chatScriptedCompletionFields,
30
+ makeChatCompletionsEmulator,
31
+ type ChatCompletionsEmulator,
32
+ type ChatCoverage,
33
+ type ChatFaultKind,
34
+ type ChatFaultState,
35
+ type ChatLedgerEntry,
36
+ type ChatRouteCoverage,
37
+ type ChatWireError
38
+ } from './chat-completions.ts'
39
+ import type { EmulatorRouteEvidence } from './route-evidence.ts'
40
+
41
+ export type { EmulatorEvidence, EmulatorRouteEvidence } from './route-evidence.ts'
42
+
43
+ export { emulatorEvidenceHeader } from './route-evidence.ts'
44
+
45
+ export const openAiChatCompletionsPath = '/v1/chat/completions'
46
+
47
+ /** Synthetic-safe default model ids, including the OpenAI chat conformance defaults. */
48
+ export const openAiEmulatorDefaultModels: ReadonlyArray<string> = ['gpt-4.1-nano', 'gpt-4.1-mini']
49
+
50
+ /**
51
+ * Route evidence manifest: every emulated OpenAI route and the conformance
52
+ * cases whose (currently synthetic, unverified) wire shapes it follows.
53
+ */
54
+ export const openAiEmulatorRoutes: ReadonlyArray<EmulatorRouteEvidence> = [
55
+ {
56
+ method: 'POST',
57
+ path: openAiChatCompletionsPath,
58
+ kind: 'provider',
59
+ write: false,
60
+ caseIds: [
61
+ 'openai.chat.stream.plain-text',
62
+ 'openai.chat.stream.tool-call-deltas',
63
+ 'openai.chat.stream.error-envelope',
64
+ 'openai.chat.json.plain-text'
65
+ ],
66
+ evidence: 'unverified',
67
+ observedAt: undefined
68
+ }
69
+ ]
70
+
71
+ /** Optional fault filter; an omitted field matches every request. `path` ending in `*` is a prefix. */
72
+ export const OpenAiFaultMatch = ChatFaultMatch
73
+
74
+ export type OpenAiFaultMatch = ChatFaultMatch
75
+
76
+ /**
77
+ * Wire faults for emulated routes, the same kinds as the Gateway emulator:
78
+ * `status` (the body defaults to an OpenAI error envelope),
79
+ * `error-after-chunks`, and `truncate-after-chunks`. Statuses that cannot
80
+ * carry a body and redirects are rejected, as are invalid headers and
81
+ * `location`. A chunk fault that cannot take effect answers 500 and is not
82
+ * consumed.
83
+ */
84
+ export const OpenAiFault = ChatFault
85
+
86
+ export type OpenAiFault = ChatFault
87
+
88
+ export type OpenAiFaultKind = ChatFaultKind
89
+
90
+ /** Usage for a scripted turn (sent as the wire `usage` object). */
91
+ export const OpenAiScriptedUsage = ChatScriptedUsage
92
+
93
+ export type OpenAiScriptedUsage = ChatScriptedUsage
94
+
95
+ /** One scripted tool call; its JSON arguments stream as these fragments. */
96
+ export const OpenAiScriptedToolCall = ChatScriptedToolCall
97
+
98
+ export type OpenAiScriptedToolCall = ChatScriptedToolCall
99
+
100
+ /**
101
+ * A scripted completion. Every field is exact (nothing is filled in) except:
102
+ * `usage` omitted is synthesized when the request asks for usage, and `null`
103
+ * drops it; `finishReason` omitted is `tool_calls` with tool calls, else
104
+ * `stop`. Reasoning is not emulated: reasoning fields are rejected.
105
+ */
106
+ export const OpenAiScriptedCompletion = Schema.Struct(chatScriptedCompletionFields)
107
+
108
+ export type OpenAiScriptedCompletion = typeof OpenAiScriptedCompletion.Type
109
+
110
+ /** A scripted error response: status, body (a string is sent as is), and optional headers. */
111
+ export const OpenAiScriptedError = ChatScriptedError
112
+
113
+ export type OpenAiScriptedError = ChatScriptedError
114
+
115
+ /** A turn queued for the next chat completion request. */
116
+ export const OpenAiScriptedTurn = Schema.Union([OpenAiScriptedError, OpenAiScriptedCompletion])
117
+
118
+ export type OpenAiScriptedTurn = typeof OpenAiScriptedTurn.Type
119
+
120
+ /** Thrown by the JS API (`faults.add`, `script.enqueue`) for invalid input; a programmer error. */
121
+ export class OpenAiEmulatorInputInvalid extends Data.TaggedError('OpenAiEmulatorInputInvalid')<{
122
+ readonly input: 'fault' | 'turn'
123
+ readonly reason: string
124
+ }> {
125
+ override get message(): string {
126
+ return `Invalid OpenAI emulator ${this.input}: ${this.reason}`
127
+ }
128
+ }
129
+
130
+ export type OpenAiLedgerEntry = ChatLedgerEntry
131
+
132
+ export type OpenAiFaultState = ChatFaultState
133
+
134
+ export type OpenAiRouteCoverage = ChatRouteCoverage
135
+
136
+ export type OpenAiCoverage = ChatCoverage
137
+
138
+ export type OpenAiEmulatorOptions = {
139
+ /** Model ids that exist. Defaults to `openAiEmulatorDefaultModels`. */
140
+ readonly knownModels?: ReadonlyArray<string>
141
+ }
142
+
143
+ export type OpenAiEmulator = ChatCompletionsEmulator<OpenAiScriptedTurn>
144
+
145
+ const openAiErrorEnvelope = (error: ChatWireError): Schema.Json => ({
146
+ error: { message: error.message, type: error.type, param: null, code: error.code }
147
+ })
148
+
149
+ /**
150
+ * Create an OpenAI Chat Completions emulator. Each call has independent
151
+ * ledger, fault, and script state.
152
+ *
153
+ * Without a script, `POST /v1/chat/completions` answers a known model with
154
+ * synthetic text deltas (`stream: true`: `chat.completion.chunk` SSE, a finish
155
+ * chunk, a usage chunk when `stream_options.include_usage` is set, and
156
+ * `data: [DONE]`; `stream: false`: one `chat.completion` JSON body). A request
157
+ * with `tools` gets one tool call whose arguments are synthesized from the
158
+ * tool's JSON Schema and streamed in fragments, finishing with `tool_calls`.
159
+ * Unknown models get the OpenAI error envelope (404, `model_not_found`); a
160
+ * missing bearer credential gets a 401 envelope (`invalid_api_key`); unknown
161
+ * routes get a 404 JSON error. The ledger records the
162
+ * `max_completion_tokens` limit as `maxCompletionTokens`.
163
+ *
164
+ * Precedence per chat request matches the Gateway emulator: authentication and
165
+ * JSON validation, then the first matching `status` fault, then the next
166
+ * scripted turn, then model validation and defaults; a first matching chunk
167
+ * fault then shapes the body. Credential headers are never recorded, and the
168
+ * bearer value is never checked or stored.
169
+ */
170
+ export const makeOpenAiEmulator = (options: OpenAiEmulatorOptions = {}): OpenAiEmulator =>
171
+ makeChatCompletionsEmulator({
172
+ path: openAiChatCompletionsPath,
173
+ routes: openAiEmulatorRoutes,
174
+ knownModels: options.knownModels ?? openAiEmulatorDefaultModels,
175
+ reasoningModels: undefined,
176
+ errorEnvelope: openAiErrorEnvelope,
177
+ // Copied data shape of the synthetic OpenAI error-envelope fixture (unknown model id).
178
+ unknownModel: {
179
+ status: 404,
180
+ error: {
181
+ message: 'Synthetic placeholder: the requested model does not exist.',
182
+ type: 'invalid_request_error',
183
+ code: 'model_not_found'
184
+ }
185
+ },
186
+ auth: {
187
+ unauthorized: {
188
+ message: 'Synthetic: missing or invalid API key.',
189
+ type: 'invalid_request_error',
190
+ code: 'invalid_api_key'
191
+ }
192
+ },
193
+ completionTokenField: 'max_completion_tokens',
194
+ responseIdPrefix: 'chatcmpl-synthetic',
195
+ defaultText: ['Hello', ' from the', ' synthetic OpenAI emulator.'],
196
+ turnSchema: OpenAiScriptedTurn,
197
+ inputInvalid: (input, reason) => new OpenAiEmulatorInputInvalid({ input, reason })
198
+ })
@@ -0,0 +1,257 @@
1
+ /**
2
+ * OpenCode Go recordings the `/opencode` emulator answers from: one per Go conformance fixture.
3
+ *
4
+ * Copied as data from the committed (synthetic, unverified) conformance fixtures; never imported.
5
+ * `test/fixture-recordings.test.ts` fails when a fixture changes and this copy does not. Internal;
6
+ * not a package export.
7
+ */
8
+ import type { FixtureRecording } from './fixture-route.ts'
9
+
10
+ export const openCodeGoRecordings: ReadonlyArray<FixtureRecording> = [
11
+ {
12
+ fixtureId: 'opencode.go.chat.stream.plain-text.synthetic',
13
+ caseId: 'opencode.go.chat.stream.plain-text',
14
+ request: {
15
+ method: 'POST',
16
+ path: '/zen/go/v1/chat/completions',
17
+ query: '',
18
+ headers: {
19
+ accept: 'text/event-stream',
20
+ 'content-type': 'application/json'
21
+ },
22
+ body: {
23
+ model: 'synthetic-go-chat',
24
+ messages: [
25
+ {
26
+ role: 'system',
27
+ content: 'Reply in one short sentence.'
28
+ },
29
+ {
30
+ role: 'user',
31
+ content: 'Say hello.'
32
+ }
33
+ ],
34
+ max_tokens: 64,
35
+ stream: true,
36
+ stream_options: {
37
+ include_usage: true
38
+ }
39
+ }
40
+ },
41
+ response: {
42
+ status: 200,
43
+ headers: {
44
+ 'content-type': 'text/event-stream'
45
+ },
46
+ streamed: true,
47
+ chunks: [
48
+ 'data: {"id":"chatcmpl-synthetic-go-1","object":"chat.completion.chunk","created":1790000000,"model":"synthetic-go-chat","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}\n\n',
49
+ 'data: {"id":"chatcmpl-synthetic-go-1","object":"chat.completion.chunk","created":1790000000,"model":"synthetic-go-chat","choices":[{"index":0,"delta":{"content":"Hello"},"finish_reason":null}]}\n\n',
50
+ 'data: {"id":"chatcmpl-synthetic-go-1","object":"chat.completion.chunk","created":1790000000,"model":"synthetic-go-chat","choices":[{"index":0,',
51
+ '"delta":{"content":" from the"},"finish_reason":null}]}\n\n',
52
+ 'data: {"id":"chatcmpl-synthetic-go-1","object":"chat.completion.chunk","created":1790000000,"model":"synthetic-go-chat","choices":[{"index":0,"delta":{"content":" synthetic model."},"finish_reason":null}]}\n\n',
53
+ 'data: {"id":"chatcmpl-synthetic-go-1","object":"chat.completion.chunk","created":1790000000,"model":"synthetic-go-chat","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}\n\n',
54
+ 'data: {"id":"chatcmpl-synthetic-go-1","object":"chat.completion.chunk","created":1790000000,"model":"synthetic-go-chat","choices":[],"usage":{"prompt_tokens":53,"completion_tokens":12,"total_tokens":65}}\n\n',
55
+ 'data: [DONE]\n\n'
56
+ ]
57
+ }
58
+ },
59
+ {
60
+ fixtureId: 'opencode.go.messages.stream.plain-text.synthetic',
61
+ caseId: 'opencode.go.messages.stream.plain-text',
62
+ request: {
63
+ method: 'POST',
64
+ path: '/zen/go/v1/messages',
65
+ query: '',
66
+ headers: {
67
+ accept: 'text/event-stream',
68
+ 'content-type': 'application/json'
69
+ },
70
+ body: {
71
+ model: 'synthetic-go-messages',
72
+ system: [
73
+ {
74
+ type: 'text',
75
+ text: 'Reply in one short sentence.'
76
+ }
77
+ ],
78
+ messages: [
79
+ {
80
+ role: 'user',
81
+ content: 'Say hello.'
82
+ }
83
+ ],
84
+ max_tokens: 64,
85
+ stream: true
86
+ }
87
+ },
88
+ response: {
89
+ status: 200,
90
+ headers: {
91
+ 'content-type': 'text/event-stream'
92
+ },
93
+ streamed: true,
94
+ chunks: [
95
+ 'event: message_start\ndata: {"type":"message_start","message":{"id":"msg_synthetic_1","type":"message","role":"assistant","model":"synthetic-go-messages","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":45,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":1}}}\n\n',
96
+ 'event: content_block_start\ndata: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}\n\n',
97
+ 'event: ping\ndata: {"type":"ping"}\n\n',
98
+ 'event: content_block_delta\ndata: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}\n\n',
99
+ 'event: content_block_delta\ndata: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta",',
100
+ '"text":" from the"}}\n\n',
101
+ 'event: content_block_delta\ndata: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" synthetic Anthropic emulator."}}\n\n',
102
+ 'event: content_block_stop\ndata: {"type":"content_block_stop","index":0}\n\n',
103
+ 'event: message_delta\ndata: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":45,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":11}}\n\n',
104
+ 'event: message_stop\ndata: {"type":"message_stop"}\n\n'
105
+ ]
106
+ }
107
+ },
108
+ {
109
+ fixtureId: 'opencode.go.responses.stream.plain-text.synthetic',
110
+ caseId: 'opencode.go.responses.stream.plain-text',
111
+ request: {
112
+ method: 'POST',
113
+ path: '/zen/go/v1/responses',
114
+ query: '',
115
+ headers: {
116
+ accept: 'text/event-stream',
117
+ 'content-type': 'application/json'
118
+ },
119
+ body: {
120
+ model: 'synthetic-go-responses',
121
+ instructions: 'Reply in one short sentence.',
122
+ input: [
123
+ {
124
+ role: 'user',
125
+ content: 'Say hello.'
126
+ }
127
+ ],
128
+ store: false,
129
+ stream: true,
130
+ max_output_tokens: 64
131
+ }
132
+ },
133
+ response: {
134
+ status: 200,
135
+ headers: {
136
+ 'content-type': 'text/event-stream'
137
+ },
138
+ streamed: true,
139
+ chunks: [
140
+ 'event: response.created\ndata: {"type":"response.created","sequence_number":0,"response":{"id":"resp_synthetic_1","object":"response","created_at":1790000000,"status":"in_progress","error":null,"incomplete_details":null,"model":"synthetic-go-responses","output":[],"usage":null}}\n\n',
141
+ 'event: response.in_progress\ndata: {"type":"response.in_progress","sequence_number":1,"response":{"id":"resp_synthetic_1","object":"response","created_at":1790000000,"status":"in_progress","error":null,"incomplete_details":null,"model":"synthetic-go-responses","output":[],"usage":null}}\n\n',
142
+ 'event: response.output_item.added\ndata: {"type":"response.output_item.added","sequence_number":2,"output_index":0,"item":{"id":"msg_synthetic_1_0","type":"message","status":"in_progress","role":"assistant","content":[]}}\n\n',
143
+ 'event: response.content_part.added\ndata: {"type":"response.content_part.added","sequence_number":3,"item_id":"msg_synthetic_1_0","output_index":0,"content_index":0,"part":{"type":"output_text","text":"","annotations":[]}}\n\n',
144
+ 'event: response.output_text.delta\ndata: {"type":"response.output_text.delta","sequence_number":4,"item_id":"msg_synthetic_1_0","output_index":0,"content_index":0,"delta":"Hello"}\n\n',
145
+ 'event: response.output_text.delta\ndata: {"type":"response.output_text.delta","sequence_number":5,"item_id":"msg_synthetic_1_0","output_index":0,"content_index":0,',
146
+ '"delta":" from the"}\n\n',
147
+ 'event: response.output_text.delta\ndata: {"type":"response.output_text.delta","sequence_number":6,"item_id":"msg_synthetic_1_0","output_index":0,"content_index":0,"delta":" synthetic model."}\n\n',
148
+ 'event: response.output_text.done\ndata: {"type":"response.output_text.done","sequence_number":7,"item_id":"msg_synthetic_1_0","output_index":0,"content_index":0,"text":"Hello from the synthetic model."}\n\n',
149
+ 'event: response.content_part.done\ndata: {"type":"response.content_part.done","sequence_number":8,"item_id":"msg_synthetic_1_0","output_index":0,"content_index":0,"part":{"type":"output_text","text":"Hello from the synthetic model.","annotations":[]}}\n\n',
150
+ 'event: response.output_item.done\ndata: {"type":"response.output_item.done","sequence_number":9,"output_index":0,"item":{"id":"msg_synthetic_1_0","type":"message","status":"completed","role":"assistant","content":[{"type":"output_text","text":"Hello from the synthetic model.","annotations":[]}]}}\n\n',
151
+ 'event: response.completed\ndata: {"type":"response.completed","sequence_number":10,"response":{"id":"resp_synthetic_1","object":"response","created_at":1790000000,"status":"completed","error":null,"incomplete_details":null,"model":"synthetic-go-responses","output":[{"id":"msg_synthetic_1_0","type":"message","status":"completed","role":"assistant","content":[{"type":"output_text","text":"Hello from the synthetic model.","annotations":[]}]}],"usage":{"input_tokens":45,"input_tokens_details":{"cached_tokens":0},"output_tokens":12,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":57}}}\n\n'
152
+ ]
153
+ }
154
+ },
155
+ {
156
+ fixtureId: 'opencode.go.responses.stream.commentary-replay.synthetic',
157
+ caseId: 'opencode.go.responses.stream.commentary-replay',
158
+ request: {
159
+ method: 'POST',
160
+ path: '/zen/go/v1/responses',
161
+ query: '',
162
+ headers: {
163
+ accept: 'text/event-stream',
164
+ 'content-type': 'application/json'
165
+ },
166
+ body: {
167
+ model: 'synthetic-go-responses',
168
+ instructions: 'Reply in one short sentence.',
169
+ input: [
170
+ {
171
+ role: 'user',
172
+ content: 'What is the weather in Springfield?'
173
+ },
174
+ {
175
+ role: 'assistant',
176
+ content: 'I will look up the weather first.',
177
+ phase: 'commentary'
178
+ },
179
+ {
180
+ type: 'function_call',
181
+ call_id: 'call_synthetic_weather',
182
+ name: 'lookup_weather',
183
+ arguments: '{"city":"Springfield"}'
184
+ },
185
+ {
186
+ type: 'function_call_output',
187
+ call_id: 'call_synthetic_weather',
188
+ output: 'Sunny and mild.'
189
+ }
190
+ ],
191
+ store: false,
192
+ stream: true,
193
+ max_output_tokens: 64,
194
+ tools: [
195
+ {
196
+ type: 'function',
197
+ name: 'lookup_weather',
198
+ description: 'Look up the current weather for a city.',
199
+ parameters: {
200
+ type: 'object',
201
+ properties: {
202
+ city: {
203
+ type: 'string'
204
+ }
205
+ },
206
+ required: ['city'],
207
+ additionalProperties: false
208
+ }
209
+ }
210
+ ],
211
+ parallel_tool_calls: true
212
+ }
213
+ },
214
+ response: {
215
+ status: 200,
216
+ headers: {
217
+ 'content-type': 'text/event-stream'
218
+ },
219
+ streamed: true,
220
+ chunks: [
221
+ 'event: response.created\ndata: {"type":"response.created","sequence_number":0,"response":{"id":"resp_synthetic_1","object":"response","created_at":1790000000,"status":"in_progress","error":null,"incomplete_details":null,"model":"synthetic-go-responses","output":[],"usage":null}}\n\n',
222
+ 'event: response.in_progress\ndata: {"type":"response.in_progress","sequence_number":1,"response":{"id":"resp_synthetic_1","object":"response","created_at":1790000000,"status":"in_progress","error":null,"incomplete_details":null,"model":"synthetic-go-responses","output":[],"usage":null}}\n\n',
223
+ 'event: response.output_item.added\ndata: {"type":"response.output_item.added","sequence_number":2,"output_index":0,"item":{"id":"msg_synthetic_1_0","type":"message","status":"in_progress","role":"assistant","content":[]}}\n\n',
224
+ 'event: response.content_part.added\ndata: {"type":"response.content_part.added","sequence_number":3,"item_id":"msg_synthetic_1_0","output_index":0,"content_index":0,"part":{"type":"output_text","text":"","annotations":[]}}\n\n',
225
+ 'event: response.output_text.delta\ndata: {"type":"response.output_text.delta","sequence_number":4,"item_id":"msg_synthetic_1_0","output_index":0,"content_index":0,"delta":"It is sunny"}\n\n',
226
+ 'event: response.output_text.delta\ndata: {"type":"response.output_text.delta","sequence_number":5,"item_id":"msg_synthetic_1_0","output_index":0,"content_index":0,',
227
+ '"delta":" and mild in Springfield."}\n\n',
228
+ 'event: response.output_text.done\ndata: {"type":"response.output_text.done","sequence_number":6,"item_id":"msg_synthetic_1_0","output_index":0,"content_index":0,"text":"It is sunny and mild in Springfield."}\n\n',
229
+ 'event: response.content_part.done\ndata: {"type":"response.content_part.done","sequence_number":7,"item_id":"msg_synthetic_1_0","output_index":0,"content_index":0,"part":{"type":"output_text","text":"It is sunny and mild in Springfield.","annotations":[]}}\n\n',
230
+ 'event: response.output_item.done\ndata: {"type":"response.output_item.done","sequence_number":8,"output_index":0,"item":{"id":"msg_synthetic_1_0","type":"message","status":"completed","role":"assistant","content":[{"type":"output_text","text":"It is sunny and mild in Springfield.","annotations":[]}]}}\n\n',
231
+ 'event: response.completed\ndata: {"type":"response.completed","sequence_number":9,"response":{"id":"resp_synthetic_1","object":"response","created_at":1790000000,"status":"completed","error":null,"incomplete_details":null,"model":"synthetic-go-responses","output":[{"id":"msg_synthetic_1_0","type":"message","status":"completed","role":"assistant","content":[{"type":"output_text","text":"It is sunny and mild in Springfield.","annotations":[]}]}],"usage":{"input_tokens":193,"input_tokens_details":{"cached_tokens":0},"output_tokens":9,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":202}}}\n\n'
232
+ ]
233
+ }
234
+ },
235
+ {
236
+ fixtureId: 'opencode.go.usage.snapshot.synthetic',
237
+ caseId: 'opencode.go.usage.snapshot',
238
+ request: {
239
+ method: 'GET',
240
+ path: '/zen/go/v1/usage',
241
+ query: '',
242
+ headers: {
243
+ accept: 'application/json'
244
+ }
245
+ },
246
+ response: {
247
+ status: 200,
248
+ headers: {
249
+ 'content-type': 'application/json'
250
+ },
251
+ streamed: false,
252
+ chunks: [
253
+ '{"usage":{"rolling":{"percent":12.5,"resetsAt":"2026-10-01T03:00:00.000Z"},"weekly":{"percent":40,"resetsAt":"2026-10-05T00:00:00.000Z"},"monthly":{"percent":55,"resetsAt":"2026-10-31T00:00:00.000Z"}}}'
254
+ ]
255
+ }
256
+ }
257
+ ]