@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
@@ -0,0 +1,527 @@
1
+ import { exactBodyKeys, exactQuery, isJsonObject, isNotEmulated, notEmulated, statefulRoute } from "../stateful-emulator.mjs";
2
+ import { githubFilePathSource, githubLabelNamePattern, githubMaxCommentId, githubOwnerPattern, githubRepoPattern, githubTimestampPattern, mintedCommentNodeId, mintedIssueIdBase, mintedIssueNodeId } from "./state.mjs";
3
+ import { Predicate } from "effect";
4
+ //#region src/github/api.ts
5
+ /**
6
+ * GitHub emulator API: the route table (evidence, raw parameter patterns, request-shape checks, and
7
+ * stateful handlers), the fixture error bodies, object rendering, and `Link` paging (internal;
8
+ * re-exported by `src/github.ts`).
9
+ *
10
+ * Only the REST routes the seven GitHub conformance cases send are emulated, on
11
+ * `https://api.github.com`, with the wire shapes of their synthetic fixtures: the label listing
12
+ * (with the paging fixture's `Link` header), issue read, create, rename, and close, the issue
13
+ * search the validation fixture refuses, file contents (folded base64), comment create, listing
14
+ * `since`, and delete, and issue label add and remove. Every route plans first (reading the state,
15
+ * writing nothing) and commits only an eligible request: anything no fixture shows is not emulated
16
+ * (400, nothing written, no fault used up).
17
+ *
18
+ * @experimental
19
+ */
20
+ /** The origin every GitHub fixture records; another origin is not emulated. */
21
+ const githubEmulatorOrigin = "https://api.github.com";
22
+ /** The only `X-GitHub-Api-Version` the emulator answers (every fixture sends it). */
23
+ const githubEmulatorApiVersion = "2026-03-10";
24
+ /** The only `Accept` the emulator answers (every fixture sends it). */
25
+ const githubEmulatorAccept = "application/vnd.github+json";
26
+ /** Content type of every GitHub JSON response, as the fixtures record it. */
27
+ const githubContentType = "application/json; charset=utf-8";
28
+ /** The origin of the raw file URLs the contents fixture records (`download_url`). */
29
+ const rawOrigin = "https://raw.githubusercontent.com";
30
+ const githubEmulatorDrillKnobs = [
31
+ "linkOmitsNext",
32
+ "notFoundOmitsDocumentationUrl",
33
+ "validationWithoutErrors",
34
+ "contentUnfolded",
35
+ "sinceExcludesEqual",
36
+ "addAnswerOmitsLabel",
37
+ "closeWithoutClosedAt"
38
+ ];
39
+ /**
40
+ * The fixtures' error bodies, byte for byte (`JSON.stringify` keeps this key order): the issue
41
+ * and comment 404s, the label-not-on-issue 404, and the overlong search's 422.
42
+ */
43
+ const githubEmulatorErrorBodies = {
44
+ issueNotFound: {
45
+ message: "Not Found",
46
+ documentation_url: "https://docs.github.com/rest/issues/issues#get-an-issue",
47
+ status: "404"
48
+ },
49
+ commentNotFound: {
50
+ message: "Not Found",
51
+ documentation_url: "https://docs.github.com/rest/issues/comments#delete-an-issue-comment",
52
+ status: "404"
53
+ },
54
+ labelNotOnIssue: {
55
+ message: "Label does not exist",
56
+ documentation_url: "https://docs.github.com/rest/issues/labels#remove-a-label-from-an-issue",
57
+ status: "404"
58
+ },
59
+ searchTooLong: {
60
+ message: "Validation Failed",
61
+ errors: [{
62
+ message: "The search is longer than 256 characters.",
63
+ resource: "Search",
64
+ field: "q",
65
+ code: "invalid"
66
+ }],
67
+ documentation_url: "https://docs.github.com/v3/search/",
68
+ status: "422"
69
+ }
70
+ };
71
+ const pagingCase = "github.labels.list-link-paging";
72
+ const notFoundCase = "github.errors.not-found-envelope";
73
+ const validationCase = "github.errors.validation-envelope";
74
+ const contentsCase = "github.contents.base64-file";
75
+ const commentCase = "github.comments.create-delete";
76
+ const labelCase = "github.labels.add-remove";
77
+ const lifecycleCase = "github.issues.lifecycle-close";
78
+ const repoParams = {
79
+ owner: githubOwnerPattern,
80
+ repo: githubRepoPattern
81
+ };
82
+ const issueNumberPattern = /^[1-9][0-9]{0,9}$/;
83
+ /** Fifteen digits at most: every id up to `githubMaxCommentId`. */
84
+ const commentIdPattern = new RegExp(`^[1-9][0-9]{0,${String(githubMaxCommentId).length - 1}}$`);
85
+ /** A plain file path of at most 200 characters (the seed's path pattern and length bound). */
86
+ const filePathParam = new RegExp(`^(?=.{1,200}$)${githubFilePathSource}$`);
87
+ const evidence = (method, path, write, caseIds, params = {}) => ({
88
+ method,
89
+ path,
90
+ kind: "connector",
91
+ write,
92
+ caseIds,
93
+ evidence: "unverified",
94
+ origin: githubEmulatorOrigin,
95
+ params
96
+ });
97
+ const repoEvidence = (method, path, write, caseIds, params = {}) => evidence(method, `/repos/{owner}/{repo}${path}`, write, caseIds, {
98
+ ...repoParams,
99
+ ...params
100
+ });
101
+ const json = (status, body, headers = {}) => new Response(JSON.stringify(body), {
102
+ status,
103
+ headers: {
104
+ "content-type": githubContentType,
105
+ ...headers
106
+ }
107
+ });
108
+ /** A read-only commit. */
109
+ const answer = (response) => () => response();
110
+ /** The current instant as GitHub writes it (whole seconds); throws for an unusable clock. */
111
+ const timestamp = (env) => {
112
+ const text = new Date(env.now()).toISOString().replace(/\.\d{3}Z$/, "Z");
113
+ if (!githubTimestampPattern.test(text)) throw new Error("the clock is out of range");
114
+ return text;
115
+ };
116
+ const repoOf = (request) => ({
117
+ owner: request.params.owner ?? "",
118
+ repo: request.params.repo ?? ""
119
+ });
120
+ /** Not emulated unless the request names the seeded repository (exactly, as stored). */
121
+ const otherRepository = (state, input) => state.repository.owner === input.owner && state.repository.repo === input.repo ? void 0 : notEmulated("a repository other than the seeded one is not emulated");
122
+ const withoutQuery = (request) => {
123
+ const query = exactQuery(request, []);
124
+ return isNotEmulated(query) ? query : void 0;
125
+ };
126
+ const issueNumberOf = (request) => Number(request.params.issueNumber);
127
+ const nonEmptyString = (value, label) => Predicate.isString(value) && value.length > 0 ? value : notEmulated(`${label} must be a non-empty string`);
128
+ const repoApi = (state) => `${githubEmulatorOrigin}/repos/${state.repository.owner}/${state.repository.repo}`;
129
+ const repoHtml = (state) => `https://github.com/${state.repository.owner}/${state.repository.repo}`;
130
+ const renderUser = (user) => ({
131
+ login: user.login,
132
+ id: user.id,
133
+ type: user.type,
134
+ site_admin: user.siteAdmin
135
+ });
136
+ const renderLabel = (state, label) => ({
137
+ id: label.id,
138
+ node_id: label.nodeId,
139
+ url: `${repoApi(state)}/labels/${encodeURIComponent(label.name)}`,
140
+ name: label.name,
141
+ color: label.color,
142
+ default: label.default,
143
+ description: label.description
144
+ });
145
+ const labelNamed = (state, name) => state.labels.find((label) => label.name === name);
146
+ /** Labels by name, rendered (names the seed validated, or the add route checked). */
147
+ const renderLabels = (state, names) => names.flatMap((name) => {
148
+ const label = labelNamed(state, name);
149
+ return label === void 0 ? [] : [renderLabel(state, label)];
150
+ });
151
+ const renderIssue = (state, issue) => ({
152
+ url: `${repoApi(state)}/issues/${issue.number}`,
153
+ repository_url: repoApi(state),
154
+ html_url: `${repoHtml(state)}/issues/${issue.number}`,
155
+ id: issue.id,
156
+ node_id: issue.nodeId,
157
+ number: issue.number,
158
+ title: issue.title,
159
+ user: renderUser(issue.user),
160
+ labels: [...renderLabels(state, issue.labels)],
161
+ state: issue.state,
162
+ locked: false,
163
+ assignees: [],
164
+ milestone: null,
165
+ comments: 0,
166
+ created_at: issue.createdAt,
167
+ updated_at: issue.updatedAt,
168
+ closed_at: issue.closedAt,
169
+ author_association: issue.authorAssociation,
170
+ active_lock_reason: null,
171
+ body: issue.body,
172
+ state_reason: issue.stateReason
173
+ });
174
+ /** Every fixture's issue answers `comments: 0`: an issue holding comments is not rendered. */
175
+ const commentsRefusal = (state, issue) => state.comments.some((comment) => comment.issueNumber === issue.number) ? notEmulated("an issue answer while the issue holds comments is not emulated (no fixture records one)") : void 0;
176
+ const renderComment = (state, comment) => ({
177
+ url: `${repoApi(state)}/issues/comments/${comment.id}`,
178
+ html_url: `${repoHtml(state)}/issues/${comment.issueNumber}#issuecomment-${comment.id}`,
179
+ issue_url: `${repoApi(state)}/issues/${comment.issueNumber}`,
180
+ id: comment.id,
181
+ node_id: mintedCommentNodeId(comment.id),
182
+ user: renderUser(comment.user),
183
+ created_at: comment.createdAt,
184
+ updated_at: comment.updatedAt,
185
+ author_association: comment.authorAssociation,
186
+ body: comment.body
187
+ });
188
+ /** Base64 of the UTF-8 text, folded every 60 characters with a trailing line break. */
189
+ const foldedBase64 = (text, folded) => {
190
+ let binary = "";
191
+ for (const byte of new TextEncoder().encode(text)) binary += String.fromCharCode(byte);
192
+ const encoded = btoa(binary);
193
+ return folded ? `${(encoded.match(/.{1,60}/g) ?? []).join("\n")}\n` : encoded;
194
+ };
195
+ const issueHeld = (state, number) => state.issues.find((issue) => issue.number === number);
196
+ const missingIssue = () => notEmulated("an issue the state does not hold is not emulated on this route");
197
+ const listLabels = statefulRoute(repoEvidence("GET", "/labels", false, [pagingCase, labelCase]), "none", (request) => {
198
+ const query = exactQuery(request, ["per_page"], ["page"]);
199
+ if (isNotEmulated(query)) return query;
200
+ const perPage = query.per_page ?? "";
201
+ if (!/^[1-9][0-9]{0,2}$/.test(perPage) || Number(perPage) > 100) return notEmulated("per_page must be an integer from 1 to 100");
202
+ const page = query.page;
203
+ if (page !== void 0 && (!/^[1-9][0-9]{0,5}$/.test(page) || Number(page) < 2)) return notEmulated("page must be an integer of 2 or more (the first page omits it)");
204
+ return {
205
+ ...repoOf(request),
206
+ perPage: Number(perPage),
207
+ page: page === void 0 ? 1 : Number(page)
208
+ };
209
+ }, (state, input, { env }) => {
210
+ const other = otherRepository(state, input);
211
+ if (other !== void 0) return other;
212
+ const count = state.labels.length;
213
+ if (count === 0) return notEmulated("a label listing without labels is not emulated (no fixture records one)");
214
+ const last = Math.ceil(count / input.perPage);
215
+ if (input.page > last + 1) return notEmulated("a label page beyond the one after the last is not emulated");
216
+ return answer(() => {
217
+ const target = (page) => `<${githubEmulatorOrigin}/repositories/${state.repository.id}/labels?per_page=${input.perPage}&page=${page}>`;
218
+ const relations = [
219
+ input.page > 1 ? `${target(input.page - 1)}; rel="prev"` : void 0,
220
+ input.page < last && !(env.drills.linkOmitsNext && input.page === 1) ? `${target(input.page + 1)}; rel="next"` : void 0,
221
+ input.page === last ? void 0 : `${target(last)}; rel="last"`,
222
+ input.page > 1 ? `${target(1)}; rel="first"` : void 0
223
+ ].filter(Predicate.isString);
224
+ const start = (input.page - 1) * input.perPage;
225
+ return json(200, state.labels.slice(start, start + input.perPage).map((label) => renderLabel(state, label)), relations.length === 0 ? {} : { link: relations.join(", ") });
226
+ });
227
+ });
228
+ const issueInput = (request) => withoutQuery(request) ?? {
229
+ ...repoOf(request),
230
+ number: issueNumberOf(request)
231
+ };
232
+ const getIssue = statefulRoute(repoEvidence("GET", "/issues/{issueNumber}", false, [
233
+ notFoundCase,
234
+ labelCase,
235
+ lifecycleCase
236
+ ], { issueNumber: issueNumberPattern }), "none", issueInput, (state, input, { env }) => {
237
+ const other = otherRepository(state, input);
238
+ if (other !== void 0) return other;
239
+ const issue = issueHeld(state, input.number);
240
+ if (issue !== void 0) return commentsRefusal(state, issue) ?? answer(() => json(200, renderIssue(state, issue)));
241
+ if (input.number >= state.counters.nextIssueNumber) return answer(() => {
242
+ const { documentation_url: _url, ...withoutUrl } = githubEmulatorErrorBodies.issueNotFound;
243
+ return json(404, env.drills.notFoundOmitsDocumentationUrl ? withoutUrl : githubEmulatorErrorBodies.issueNotFound);
244
+ });
245
+ return notEmulated("an issue below nextIssueNumber that the state does not hold is implied");
246
+ });
247
+ /** The connector's repository qualifier, then the case's query. */
248
+ const scopedQueryPattern = /^repo:([^/\s]+)\/([^/\s]+) ([\s\S]*)$/;
249
+ /** GitHub's documented search length limit, which the validation fixture refuses past. */
250
+ const searchLimit = 256;
251
+ const searchIssues = statefulRoute(evidence("GET", "/search/issues", false, [validationCase]), "none", (request) => {
252
+ const query = exactQuery(request, ["q"]);
253
+ if (isNotEmulated(query)) return query;
254
+ const scoped = scopedQueryPattern.exec(query.q ?? "");
255
+ if (scoped === null) return notEmulated("a search without the leading repo:<owner>/<repo> qualifier is not emulated");
256
+ return (scoped[3] ?? "").length > searchLimit ? {
257
+ owner: scoped[1] ?? "",
258
+ repo: scoped[2] ?? ""
259
+ } : notEmulated("a search of at most 256 characters is not emulated (no fixture records results)");
260
+ }, (state, input, { env }) => otherRepository(state, input) ?? answer(() => json(422, env.drills.validationWithoutErrors ? {
261
+ ...githubEmulatorErrorBodies.searchTooLong,
262
+ errors: []
263
+ } : githubEmulatorErrorBodies.searchTooLong)));
264
+ const getContents = statefulRoute(repoEvidence("GET", "/contents/{path+}", false, [contentsCase], { path: filePathParam }), "none", (request) => withoutQuery(request) ?? {
265
+ ...repoOf(request),
266
+ path: request.params.path ?? ""
267
+ }, (state, input, { env }) => {
268
+ const other = otherRepository(state, input);
269
+ if (other !== void 0) return other;
270
+ const file = state.files.find((candidate) => candidate.path === input.path);
271
+ if (file === void 0) return notEmulated("a path naming no seeded file is not emulated (no fixture records one)");
272
+ return answer(() => {
273
+ const { owner, repo, defaultBranch } = state.repository;
274
+ return json(200, {
275
+ name: file.path.split("/").at(-1) ?? file.path,
276
+ path: file.path,
277
+ sha: file.sha,
278
+ size: new TextEncoder().encode(file.text).byteLength,
279
+ url: `${repoApi(state)}/contents/${file.path}?ref=${defaultBranch}`,
280
+ html_url: `${repoHtml(state)}/blob/${defaultBranch}/${file.path}`,
281
+ git_url: `${repoApi(state)}/git/blobs/${file.sha}`,
282
+ download_url: `${rawOrigin}/${owner}/${repo}/${defaultBranch}/${file.path}`,
283
+ type: "file",
284
+ content: foldedBase64(file.text, !env.drills.contentUnfolded),
285
+ encoding: "base64"
286
+ });
287
+ });
288
+ });
289
+ const createComment = statefulRoute(repoEvidence("POST", "/issues/{issueNumber}/comments", true, [commentCase], { issueNumber: issueNumberPattern }), "json", (request) => {
290
+ const issue = issueInput(request);
291
+ if (isNotEmulated(issue)) return issue;
292
+ const body = exactBodyKeys(request.json, "the comment body", ["body"]);
293
+ if (isNotEmulated(body)) return body;
294
+ const text = nonEmptyString(body.body, "body");
295
+ return isNotEmulated(text) ? text : {
296
+ ...issue,
297
+ body: text
298
+ };
299
+ }, (state, input, { env }) => {
300
+ const other = otherRepository(state, input);
301
+ if (other !== void 0) return other;
302
+ const issue = issueHeld(state, input.number);
303
+ if (issue === void 0) return missingIssue();
304
+ if (issue.state !== "open") return notEmulated("commenting on a closed issue is not emulated (no fixture records one)");
305
+ if (state.counters.nextCommentId > 999999999999999) return notEmulated("the emulator has run out of comment ids");
306
+ return () => {
307
+ const at = timestamp(env);
308
+ const id = state.counters.nextCommentId;
309
+ const comment = {
310
+ id,
311
+ issueNumber: issue.number,
312
+ body: input.body,
313
+ user: state.viewer,
314
+ authorAssociation: "OWNER",
315
+ createdAt: at,
316
+ updatedAt: at
317
+ };
318
+ state.counters = {
319
+ ...state.counters,
320
+ nextCommentId: id + 1
321
+ };
322
+ state.comments = [...state.comments, comment];
323
+ return json(201, renderComment(state, comment));
324
+ };
325
+ });
326
+ const listComments = statefulRoute(repoEvidence("GET", "/issues/{issueNumber}/comments", false, [commentCase], { issueNumber: issueNumberPattern }), "none", (request) => {
327
+ const query = exactQuery(request, ["per_page", "since"]);
328
+ if (isNotEmulated(query)) return query;
329
+ if (query.per_page !== "100") return notEmulated("a comment listing per_page other than 100 is not emulated");
330
+ const since = query.since ?? "";
331
+ const instant = githubTimestampPattern.test(since) ? Date.parse(since) : NaN;
332
+ if (!Number.isFinite(instant)) return notEmulated("since must be a timestamp of the form YYYY-MM-DDTHH:MM:SSZ");
333
+ return {
334
+ ...repoOf(request),
335
+ number: issueNumberOf(request),
336
+ since: instant
337
+ };
338
+ }, (state, input, { env }) => {
339
+ const other = otherRepository(state, input);
340
+ if (other !== void 0) return other;
341
+ if (issueHeld(state, input.number) === void 0) return missingIssue();
342
+ const listed = state.comments.filter((comment) => {
343
+ if (comment.issueNumber !== input.number) return false;
344
+ const updated = Date.parse(comment.updatedAt);
345
+ return env.drills.sinceExcludesEqual ? updated > input.since : updated >= input.since;
346
+ });
347
+ if (listed.length > 1) return notEmulated("a comment listing of more than one comment is not emulated");
348
+ return answer(() => json(200, listed.map((comment) => renderComment(state, comment))));
349
+ });
350
+ const deleteComment = statefulRoute(repoEvidence("DELETE", "/issues/comments/{commentId}", true, [commentCase], { commentId: commentIdPattern }), "none", (request) => withoutQuery(request) ?? {
351
+ ...repoOf(request),
352
+ id: Number(request.params.commentId)
353
+ }, (state, input) => {
354
+ const other = otherRepository(state, input);
355
+ if (other !== void 0) return other;
356
+ if (state.comments.find((candidate) => candidate.id === input.id) !== void 0) return () => {
357
+ state.comments = state.comments.filter((candidate) => candidate.id !== input.id);
358
+ state.deletedComments = [...state.deletedComments, input.id];
359
+ return new Response(null, { status: 204 });
360
+ };
361
+ return state.deletedComments.includes(input.id) ? answer(() => json(404, githubEmulatorErrorBodies.commentNotFound)) : notEmulated("a comment this emulator never held is not emulated");
362
+ });
363
+ /** GitHub orders label names case-insensitively; the fixture's answer is in that order. */
364
+ const sortsAfter = (name, other) => name.toLowerCase() > other.toLowerCase();
365
+ /** The route table: evidence plus handlers. `githubEmulatorRoutes` is its evidence part. */
366
+ const githubApiRoutes = [
367
+ listLabels,
368
+ getIssue,
369
+ searchIssues,
370
+ getContents,
371
+ createComment,
372
+ listComments,
373
+ deleteComment,
374
+ statefulRoute(repoEvidence("POST", "/issues/{issueNumber}/labels", true, [labelCase], { issueNumber: issueNumberPattern }), "json", (request) => {
375
+ const issue = issueInput(request);
376
+ if (isNotEmulated(issue)) return issue;
377
+ const body = exactBodyKeys(request.json, "the labels body", ["labels"]);
378
+ if (isNotEmulated(body)) return body;
379
+ const labels = body.labels;
380
+ if (!Array.isArray(labels) || labels.length !== 1) return notEmulated("adding other than exactly one label is not emulated");
381
+ const [label] = labels;
382
+ return Predicate.isString(label) && githubLabelNamePattern.test(label) ? {
383
+ ...issue,
384
+ label
385
+ } : notEmulated("the label must be a plain label name");
386
+ }, (state, input, { env }) => {
387
+ const other = otherRepository(state, input);
388
+ if (other !== void 0) return other;
389
+ const issue = issueHeld(state, input.number);
390
+ if (issue === void 0) return missingIssue();
391
+ if (issue.state !== "open") return notEmulated("labelling a closed issue is not emulated (no fixture records one)");
392
+ if (labelNamed(state, input.label) === void 0) return notEmulated("adding a label the repository does not have is not emulated");
393
+ if (issue.labels.includes(input.label)) return notEmulated("adding a label the issue already has is not emulated");
394
+ if (!issue.labels.every((name) => sortsAfter(input.label, name))) return notEmulated("adding a label that does not sort after the issue labels is not emulated");
395
+ return () => {
396
+ const labels = [...issue.labels, input.label];
397
+ state.issues = state.issues.map((candidate) => candidate.number === issue.number ? {
398
+ ...candidate,
399
+ labels
400
+ } : candidate);
401
+ return json(200, [...renderLabels(state, env.drills.addAnswerOmitsLabel ? issue.labels : labels)]);
402
+ };
403
+ }),
404
+ statefulRoute(repoEvidence("DELETE", "/issues/{issueNumber}/labels/{name}", true, [labelCase], {
405
+ issueNumber: issueNumberPattern,
406
+ name: githubLabelNamePattern
407
+ }), "none", (request) => {
408
+ const issue = issueInput(request);
409
+ return isNotEmulated(issue) ? issue : {
410
+ ...issue,
411
+ label: request.params.name ?? ""
412
+ };
413
+ }, (state, input) => {
414
+ const other = otherRepository(state, input);
415
+ if (other !== void 0) return other;
416
+ const issue = issueHeld(state, input.number);
417
+ if (issue === void 0) return missingIssue();
418
+ if (issue.state !== "open") return notEmulated("unlabelling a closed issue is not emulated (no fixture records one)");
419
+ if (labelNamed(state, input.label) === void 0) return notEmulated("removing a label the repository does not have is not emulated");
420
+ if (!issue.labels.includes(input.label)) return answer(() => json(404, githubEmulatorErrorBodies.labelNotOnIssue));
421
+ const labels = issue.labels.filter((name) => name !== input.label);
422
+ if (labels.length === 0) return notEmulated("removing the last label of an issue is not emulated (no fixture records it)");
423
+ return () => {
424
+ state.issues = state.issues.map((candidate) => candidate.number === issue.number ? {
425
+ ...candidate,
426
+ labels
427
+ } : candidate);
428
+ return json(200, [...renderLabels(state, labels)]);
429
+ };
430
+ }),
431
+ statefulRoute(repoEvidence("POST", "/issues", true, [lifecycleCase]), "json", (request) => {
432
+ const query = withoutQuery(request);
433
+ if (query !== void 0) return query;
434
+ const body = exactBodyKeys(request.json, "the issue body", ["title", "body"]);
435
+ if (isNotEmulated(body)) return body;
436
+ const title = nonEmptyString(body.title, "title");
437
+ if (isNotEmulated(title)) return title;
438
+ return Predicate.isString(body.body) ? {
439
+ ...repoOf(request),
440
+ title,
441
+ body: body.body
442
+ } : notEmulated("body must be a string");
443
+ }, (state, input, { env }) => {
444
+ const other = otherRepository(state, input);
445
+ if (other !== void 0) return other;
446
+ const number = state.counters.nextIssueNumber;
447
+ if (number > 9999999999) return notEmulated("the repository has run out of emulated issue numbers");
448
+ return () => {
449
+ const at = timestamp(env);
450
+ const issue = {
451
+ number,
452
+ id: mintedIssueIdBase + number,
453
+ nodeId: mintedIssueNodeId(number),
454
+ title: input.title,
455
+ body: input.body,
456
+ state: "open",
457
+ stateReason: null,
458
+ labels: [],
459
+ user: state.viewer,
460
+ authorAssociation: "OWNER",
461
+ createdAt: at,
462
+ updatedAt: at,
463
+ closedAt: null,
464
+ createdHere: true
465
+ };
466
+ state.counters = {
467
+ ...state.counters,
468
+ nextIssueNumber: number + 1
469
+ };
470
+ state.issues = [...state.issues, issue];
471
+ return json(201, renderIssue(state, issue));
472
+ };
473
+ }),
474
+ statefulRoute(repoEvidence("PATCH", "/issues/{issueNumber}", true, [lifecycleCase], { issueNumber: issueNumberPattern }), "json", (request) => {
475
+ const issue = issueInput(request);
476
+ if (isNotEmulated(issue)) return issue;
477
+ const body = request.json;
478
+ if (!isJsonObject(body)) return notEmulated("the update body must be a JSON object");
479
+ const keys = Object.keys(body).sort().join(",");
480
+ if (keys === "title") {
481
+ const title = nonEmptyString(body.title, "title");
482
+ return isNotEmulated(title) ? title : {
483
+ ...issue,
484
+ change: "rename",
485
+ title
486
+ };
487
+ }
488
+ if (keys === "state,state_reason") return body.state === "closed" && body.state_reason === "completed" ? {
489
+ ...issue,
490
+ change: "close"
491
+ } : notEmulated("state changes other than closed as completed are not emulated");
492
+ return notEmulated("issue updates other than { title } or { state, state_reason } are not emulated");
493
+ }, (state, input, { env }) => {
494
+ const other = otherRepository(state, input);
495
+ if (other !== void 0) return other;
496
+ const issue = issueHeld(state, input.number);
497
+ if (issue === void 0 || !issue.createdHere) return notEmulated("updating an issue not created here is not emulated");
498
+ if (issue.state !== "open") return notEmulated("updating a closed issue is not emulated (no fixture records one)");
499
+ const refusal = commentsRefusal(state, issue);
500
+ if (refusal !== void 0) return refusal;
501
+ return () => {
502
+ const updated = input.change === "rename" ? {
503
+ ...issue,
504
+ title: input.title
505
+ } : (() => {
506
+ const at = timestamp(env);
507
+ return {
508
+ ...issue,
509
+ state: "closed",
510
+ stateReason: "completed",
511
+ closedAt: at,
512
+ updatedAt: at
513
+ };
514
+ })();
515
+ state.issues = state.issues.map((candidate) => candidate.number === issue.number ? updated : candidate);
516
+ const rendered = renderIssue(state, updated);
517
+ return json(200, input.change === "close" && env.drills.closeWithoutClosedAt ? {
518
+ ...rendered,
519
+ closed_at: null
520
+ } : rendered);
521
+ };
522
+ })
523
+ ];
524
+ //#endregion
525
+ export { githubApiRoutes, githubEmulatorAccept, githubEmulatorApiVersion, githubEmulatorDrillKnobs, githubEmulatorErrorBodies, githubEmulatorOrigin };
526
+
527
+ //# sourceMappingURL=api.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.mjs","names":[],"sources":["../../src/github/api.ts"],"sourcesContent":["/**\n * GitHub emulator API: the route table (evidence, raw parameter patterns, request-shape checks, and\n * stateful handlers), the fixture error bodies, object rendering, and `Link` paging (internal;\n * re-exported by `src/github.ts`).\n *\n * Only the REST routes the seven GitHub conformance cases send are emulated, on\n * `https://api.github.com`, with the wire shapes of their synthetic fixtures: the label listing\n * (with the paging fixture's `Link` header), issue read, create, rename, and close, the issue\n * search the validation fixture refuses, file contents (folded base64), comment create, listing\n * `since`, and delete, and issue label add and remove. Every route plans first (reading the state,\n * writing nothing) and commits only an eligible request: anything no fixture shows is not emulated\n * (400, nothing written, no fault used up).\n *\n * @experimental\n */\nimport { Predicate } from 'effect'\nimport type * as Schema from 'effect/Schema'\nimport {\n exactBodyKeys,\n exactQuery,\n isJsonObject,\n isNotEmulated,\n notEmulated,\n statefulRoute,\n type Commit,\n type EmulatedRequest,\n type NotEmulated,\n type StatefulRoute\n} from '../stateful-emulator.ts'\nimport {\n githubFilePathSource,\n githubLabelNamePattern,\n githubMaxCommentId,\n githubMaxIssueNumber,\n githubOwnerPattern,\n githubRepoPattern,\n githubTimestampPattern,\n mintedCommentNodeId,\n mintedIssueIdBase,\n mintedIssueNodeId,\n type GithubEmulatorComment,\n type GithubEmulatorIssue,\n type GithubEmulatorLabel,\n type GithubEmulatorState,\n type GithubEmulatorUser\n} from './state.ts'\n\n/** The origin every GitHub fixture records; another origin is not emulated. */\nexport const githubEmulatorOrigin = 'https://api.github.com'\n\n/** The only `X-GitHub-Api-Version` the emulator answers (every fixture sends it). */\nexport const githubEmulatorApiVersion = '2026-03-10'\n\n/** The only `Accept` the emulator answers (every fixture sends it). */\nexport const githubEmulatorAccept = 'application/vnd.github+json'\n\n/** Content type of every GitHub JSON response, as the fixtures record it. */\nconst githubContentType = 'application/json; charset=utf-8'\n\n/** The origin of the raw file URLs the contents fixture records (`download_url`). */\nconst rawOrigin = 'https://raw.githubusercontent.com'\n\n/** Drill knobs (tests only): each makes the emulator disagree with one conformance claim. */\nexport type GithubEmulatorDrills = {\n /** The first label page's `Link` lists no `rel=\"next\"` while labels remain. */\n readonly linkOmitsNext?: boolean\n /** The not-found body of an unreached issue number lacks `documentation_url`. */\n readonly notFoundOmitsDocumentationUrl?: boolean\n /** The overlong search's validation body carries an empty `errors` array. */\n readonly validationWithoutErrors?: boolean\n /** File contents answer base64 on one line (not folded). */\n readonly contentUnfolded?: boolean\n /** A comment listing `since` excludes a comment updated at exactly that instant. */\n readonly sinceExcludesEqual?: boolean\n /** The label add answers the issue labels without the added label. */\n readonly addAnswerOmitsLabel?: boolean\n /** Closing an issue answers `closed_at: null`. */\n readonly closeWithoutClosedAt?: boolean\n}\n\nexport const githubEmulatorDrillKnobs: ReadonlyArray<keyof GithubEmulatorDrills> = [\n 'linkOmitsNext',\n 'notFoundOmitsDocumentationUrl',\n 'validationWithoutErrors',\n 'contentUnfolded',\n 'sinceExcludesEqual',\n 'addAnswerOmitsLabel',\n 'closeWithoutClosedAt'\n]\n\nexport type GithubApiEnv = {\n /** Clock in epoch milliseconds (created issue and comment timestamps, close times). */\n readonly now: () => number\n readonly drills: Readonly<Record<keyof GithubEmulatorDrills, boolean>>\n}\n\n/**\n * The fixtures' error bodies, byte for byte (`JSON.stringify` keeps this key order): the issue\n * and comment 404s, the label-not-on-issue 404, and the overlong search's 422.\n */\nexport const githubEmulatorErrorBodies = {\n issueNotFound: {\n message: 'Not Found',\n documentation_url: 'https://docs.github.com/rest/issues/issues#get-an-issue',\n status: '404'\n },\n commentNotFound: {\n message: 'Not Found',\n documentation_url: 'https://docs.github.com/rest/issues/comments#delete-an-issue-comment',\n status: '404'\n },\n labelNotOnIssue: {\n message: 'Label does not exist',\n documentation_url: 'https://docs.github.com/rest/issues/labels#remove-a-label-from-an-issue',\n status: '404'\n },\n searchTooLong: {\n message: 'Validation Failed',\n errors: [\n {\n message: 'The search is longer than 256 characters.',\n resource: 'Search',\n field: 'q',\n code: 'invalid'\n }\n ],\n documentation_url: 'https://docs.github.com/v3/search/',\n status: '422'\n }\n} as const satisfies Readonly<Record<string, Schema.Json>>\n\ntype Route = StatefulRoute<GithubEmulatorState, GithubApiEnv>\n\nconst pagingCase = 'github.labels.list-link-paging'\n\nconst notFoundCase = 'github.errors.not-found-envelope'\n\nconst validationCase = 'github.errors.validation-envelope'\n\nconst contentsCase = 'github.contents.base64-file'\n\nconst commentCase = 'github.comments.create-delete'\n\nconst labelCase = 'github.labels.add-remove'\n\nconst lifecycleCase = 'github.issues.lifecycle-close'\n\n// Raw (still percent-encoded) path parameter patterns: a request whose raw path is not exactly one\n// of these shapes is unrecognised (ledgered with constant text only).\n\nconst repoParams = { owner: githubOwnerPattern, repo: githubRepoPattern }\n\nconst issueNumberPattern = /^[1-9][0-9]{0,9}$/\n\n/** Fifteen digits at most: every id up to `githubMaxCommentId`. */\nconst commentIdPattern = new RegExp(`^[1-9][0-9]{0,${String(githubMaxCommentId).length - 1}}$`)\n\n/** A plain file path of at most 200 characters (the seed's path pattern and length bound). */\nconst filePathParam = new RegExp(`^(?=.{1,200}$)${githubFilePathSource}$`)\n\nconst evidence = (\n method: string,\n path: string,\n write: boolean,\n caseIds: ReadonlyArray<string>,\n params: Readonly<Record<string, RegExp>> = {}\n) => ({\n method,\n path,\n kind: 'connector' as const,\n write,\n caseIds,\n evidence: 'unverified' as const,\n origin: githubEmulatorOrigin,\n params\n})\n\nconst repoEvidence = (\n method: string,\n path: string,\n write: boolean,\n caseIds: ReadonlyArray<string>,\n params: Readonly<Record<string, RegExp>> = {}\n) => evidence(method, `/repos/{owner}/{repo}${path}`, write, caseIds, { ...repoParams, ...params })\n\n// Responses.\n\nconst json = (status: number, body: Schema.Json, headers: Record<string, string> = {}): Response =>\n new Response(JSON.stringify(body), {\n status,\n headers: { 'content-type': githubContentType, ...headers }\n })\n\n/** A read-only commit. */\nconst answer =\n (response: () => Response): Commit =>\n () =>\n response()\n\n/** The current instant as GitHub writes it (whole seconds); throws for an unusable clock. */\nconst timestamp = (env: GithubApiEnv): string => {\n const text = new Date(env.now()).toISOString().replace(/\\.\\d{3}Z$/, 'Z')\n\n if (!githubTimestampPattern.test(text)) throw new Error('the clock is out of range')\n\n return text\n}\n\n// Requests.\n\ntype RepoInput = { readonly owner: string; readonly repo: string }\n\nconst repoOf = (request: EmulatedRequest): RepoInput => ({\n owner: request.params.owner ?? '',\n repo: request.params.repo ?? ''\n})\n\n/** Not emulated unless the request names the seeded repository (exactly, as stored). */\nconst otherRepository = (state: GithubEmulatorState, input: RepoInput): NotEmulated | undefined =>\n state.repository.owner === input.owner && state.repository.repo === input.repo\n ? undefined\n : notEmulated('a repository other than the seeded one is not emulated')\n\nconst withoutQuery = (request: EmulatedRequest): NotEmulated | undefined => {\n const query = exactQuery(request, [])\n\n return isNotEmulated(query) ? query : undefined\n}\n\nconst issueNumberOf = (request: EmulatedRequest): number => Number(request.params.issueNumber)\n\nconst nonEmptyString = (value: Schema.Json | undefined, label: string): string | NotEmulated =>\n Predicate.isString(value) && value.length > 0\n ? value\n : notEmulated(`${label} must be a non-empty string`)\n\n// Rendering, in the fixture key order.\n\nconst repoApi = (state: GithubEmulatorState): string =>\n `${githubEmulatorOrigin}/repos/${state.repository.owner}/${state.repository.repo}`\n\nconst repoHtml = (state: GithubEmulatorState): string =>\n `https://github.com/${state.repository.owner}/${state.repository.repo}`\n\nconst renderUser = (user: GithubEmulatorUser): Schema.JsonObject => ({\n login: user.login,\n id: user.id,\n type: user.type,\n site_admin: user.siteAdmin\n})\n\nconst renderLabel = (\n state: GithubEmulatorState,\n label: GithubEmulatorLabel\n): Schema.JsonObject => ({\n id: label.id,\n node_id: label.nodeId,\n url: `${repoApi(state)}/labels/${encodeURIComponent(label.name)}`,\n name: label.name,\n color: label.color,\n default: label.default,\n description: label.description\n})\n\nconst labelNamed = (state: GithubEmulatorState, name: string): GithubEmulatorLabel | undefined =>\n state.labels.find(label => label.name === name)\n\n/** Labels by name, rendered (names the seed validated, or the add route checked). */\nconst renderLabels = (\n state: GithubEmulatorState,\n names: ReadonlyArray<string>\n): ReadonlyArray<Schema.JsonObject> =>\n names.flatMap(name => {\n const label = labelNamed(state, name)\n\n return label === undefined ? [] : [renderLabel(state, label)]\n })\n\nconst renderIssue = (\n state: GithubEmulatorState,\n issue: GithubEmulatorIssue\n): Schema.JsonObject => ({\n url: `${repoApi(state)}/issues/${issue.number}`,\n repository_url: repoApi(state),\n html_url: `${repoHtml(state)}/issues/${issue.number}`,\n id: issue.id,\n node_id: issue.nodeId,\n number: issue.number,\n title: issue.title,\n user: renderUser(issue.user),\n labels: [...renderLabels(state, issue.labels)],\n state: issue.state,\n locked: false,\n assignees: [],\n milestone: null,\n comments: 0,\n created_at: issue.createdAt,\n updated_at: issue.updatedAt,\n closed_at: issue.closedAt,\n author_association: issue.authorAssociation,\n active_lock_reason: null,\n body: issue.body,\n state_reason: issue.stateReason\n})\n\n/** Every fixture's issue answers `comments: 0`: an issue holding comments is not rendered. */\nconst commentsRefusal = (\n state: GithubEmulatorState,\n issue: GithubEmulatorIssue\n): NotEmulated | undefined =>\n state.comments.some(comment => comment.issueNumber === issue.number)\n ? notEmulated(\n 'an issue answer while the issue holds comments is not emulated (no fixture records one)'\n )\n : undefined\n\nconst renderComment = (\n state: GithubEmulatorState,\n comment: GithubEmulatorComment\n): Schema.JsonObject => ({\n url: `${repoApi(state)}/issues/comments/${comment.id}`,\n html_url: `${repoHtml(state)}/issues/${comment.issueNumber}#issuecomment-${comment.id}`,\n issue_url: `${repoApi(state)}/issues/${comment.issueNumber}`,\n id: comment.id,\n node_id: mintedCommentNodeId(comment.id),\n user: renderUser(comment.user),\n created_at: comment.createdAt,\n updated_at: comment.updatedAt,\n author_association: comment.authorAssociation,\n body: comment.body\n})\n\n/** Base64 of the UTF-8 text, folded every 60 characters with a trailing line break. */\nconst foldedBase64 = (text: string, folded: boolean): string => {\n let binary = ''\n\n for (const byte of new TextEncoder().encode(text)) binary += String.fromCharCode(byte)\n\n const encoded = btoa(binary)\n\n return folded ? `${(encoded.match(/.{1,60}/g) ?? []).join('\\n')}\\n` : encoded\n}\n\nconst issueHeld = (state: GithubEmulatorState, number: number): GithubEmulatorIssue | undefined =>\n state.issues.find(issue => issue.number === number)\n\nconst missingIssue = (): NotEmulated =>\n notEmulated('an issue the state does not hold is not emulated on this route')\n\n// Routes.\n\ntype LabelsInput = RepoInput & { readonly perPage: number; readonly page: number }\n\nconst listLabels: Route = statefulRoute(\n repoEvidence('GET', '/labels', false, [pagingCase, labelCase]),\n 'none',\n (request): LabelsInput | NotEmulated => {\n const query = exactQuery(request, ['per_page'], ['page'])\n\n if (isNotEmulated(query)) return query\n\n const perPage = query.per_page ?? ''\n\n if (!/^[1-9][0-9]{0,2}$/.test(perPage) || Number(perPage) > 100) {\n return notEmulated('per_page must be an integer from 1 to 100')\n }\n\n const page = query.page\n\n // The fixtures ask for the first page without `page`.\n if (page !== undefined && (!/^[1-9][0-9]{0,5}$/.test(page) || Number(page) < 2)) {\n return notEmulated('page must be an integer of 2 or more (the first page omits it)')\n }\n\n return {\n ...repoOf(request),\n perPage: Number(perPage),\n page: page === undefined ? 1 : Number(page)\n }\n },\n (state, input, { env }) => {\n const other = otherRepository(state, input)\n\n if (other !== undefined) return other\n\n const count = state.labels.length\n\n if (count === 0) {\n return notEmulated('a label listing without labels is not emulated (no fixture records one)')\n }\n\n const last = Math.ceil(count / input.perPage)\n\n // The paging fixture records the page after the last (empty), never one further.\n if (input.page > last + 1) {\n return notEmulated('a label page beyond the one after the last is not emulated')\n }\n\n return answer(() => {\n const target = (page: number) =>\n `<${githubEmulatorOrigin}/repositories/${state.repository.id}/labels` +\n `?per_page=${input.perPage}&page=${page}>`\n\n // The paging fixture's relations and order: prev, next, last, first.\n const relations = [\n input.page > 1 ? `${target(input.page - 1)}; rel=\"prev\"` : undefined,\n input.page < last && !(env.drills.linkOmitsNext && input.page === 1)\n ? `${target(input.page + 1)}; rel=\"next\"`\n : undefined,\n input.page === last ? undefined : `${target(last)}; rel=\"last\"`,\n input.page > 1 ? `${target(1)}; rel=\"first\"` : undefined\n ].filter(Predicate.isString)\n\n const start = (input.page - 1) * input.perPage\n\n return json(\n 200,\n state.labels.slice(start, start + input.perPage).map(label => renderLabel(state, label)),\n // A listing that fits one page carries no Link, as the label fixture records.\n relations.length === 0 ? {} : { link: relations.join(', ') }\n )\n })\n }\n)\n\ntype IssueInput = RepoInput & { readonly number: number }\n\nconst issueInput = (request: EmulatedRequest): IssueInput | NotEmulated =>\n withoutQuery(request) ?? { ...repoOf(request), number: issueNumberOf(request) }\n\nconst getIssue: Route = statefulRoute(\n repoEvidence('GET', '/issues/{issueNumber}', false, [notFoundCase, labelCase, lifecycleCase], {\n issueNumber: issueNumberPattern\n }),\n 'none',\n issueInput,\n (state, input, { env }) => {\n const other = otherRepository(state, input)\n\n if (other !== undefined) return other\n\n const issue = issueHeld(state, input.number)\n\n if (issue !== undefined) {\n return commentsRefusal(state, issue) ?? answer(() => json(200, renderIssue(state, issue)))\n }\n\n // A number the repository has not reached answers the not-found fixture's 404.\n if (input.number >= state.counters.nextIssueNumber) {\n return answer(() => {\n const { documentation_url: _url, ...withoutUrl } = githubEmulatorErrorBodies.issueNotFound\n\n return json(\n 404,\n env.drills.notFoundOmitsDocumentationUrl\n ? withoutUrl\n : githubEmulatorErrorBodies.issueNotFound\n )\n })\n }\n\n return notEmulated('an issue below nextIssueNumber that the state does not hold is implied')\n }\n)\n\n/** The connector's repository qualifier, then the case's query. */\nconst scopedQueryPattern = /^repo:([^/\\s]+)\\/([^/\\s]+) ([\\s\\S]*)$/\n\n/** GitHub's documented search length limit, which the validation fixture refuses past. */\nconst searchLimit = 256\n\nconst searchIssues: Route = statefulRoute(\n evidence('GET', '/search/issues', false, [validationCase]),\n 'none',\n (request): RepoInput | NotEmulated => {\n const query = exactQuery(request, ['q'])\n\n if (isNotEmulated(query)) return query\n\n const scoped = scopedQueryPattern.exec(query.q ?? '')\n\n if (scoped === null) {\n return notEmulated(\n 'a search without the leading repo:<owner>/<repo> qualifier is not emulated'\n )\n }\n\n // Only a refusal is recorded: a query after the qualifier of at most 256 characters would\n // answer results, which no fixture shows.\n return (scoped[3] ?? '').length > searchLimit\n ? { owner: scoped[1] ?? '', repo: scoped[2] ?? '' }\n : notEmulated(\n 'a search of at most 256 characters is not emulated (no fixture records results)'\n )\n },\n (state, input, { env }) =>\n otherRepository(state, input) ??\n answer(() =>\n json(\n 422,\n env.drills.validationWithoutErrors\n ? { ...githubEmulatorErrorBodies.searchTooLong, errors: [] }\n : githubEmulatorErrorBodies.searchTooLong\n )\n )\n)\n\ntype FileInput = RepoInput & { readonly path: string }\n\nconst getContents: Route = statefulRoute(\n repoEvidence('GET', '/contents/{path+}', false, [contentsCase], { path: filePathParam }),\n 'none',\n (request): FileInput | NotEmulated =>\n withoutQuery(request) ?? { ...repoOf(request), path: request.params.path ?? '' },\n (state, input, { env }) => {\n const other = otherRepository(state, input)\n\n if (other !== undefined) return other\n\n const file = state.files.find(candidate => candidate.path === input.path)\n\n if (file === undefined) {\n return notEmulated('a path naming no seeded file is not emulated (no fixture records one)')\n }\n\n return answer(() => {\n const { owner, repo, defaultBranch } = state.repository\n const name = file.path.split('/').at(-1) ?? file.path\n\n return json(200, {\n name,\n path: file.path,\n sha: file.sha,\n size: new TextEncoder().encode(file.text).byteLength,\n url: `${repoApi(state)}/contents/${file.path}?ref=${defaultBranch}`,\n html_url: `${repoHtml(state)}/blob/${defaultBranch}/${file.path}`,\n git_url: `${repoApi(state)}/git/blobs/${file.sha}`,\n download_url: `${rawOrigin}/${owner}/${repo}/${defaultBranch}/${file.path}`,\n type: 'file',\n content: foldedBase64(file.text, !env.drills.contentUnfolded),\n encoding: 'base64'\n })\n })\n }\n)\n\ntype CommentInput = IssueInput & { readonly body: string }\n\nconst createComment: Route = statefulRoute(\n repoEvidence('POST', '/issues/{issueNumber}/comments', true, [commentCase], {\n issueNumber: issueNumberPattern\n }),\n 'json',\n (request): CommentInput | NotEmulated => {\n const issue = issueInput(request)\n\n if (isNotEmulated(issue)) return issue\n\n const body = exactBodyKeys(request.json, 'the comment body', ['body'])\n\n if (isNotEmulated(body)) return body\n\n const text = nonEmptyString(body.body, 'body')\n\n return isNotEmulated(text) ? text : { ...issue, body: text }\n },\n (state, input, { env }) => {\n const other = otherRepository(state, input)\n\n if (other !== undefined) return other\n\n const issue = issueHeld(state, input.number)\n\n if (issue === undefined) return missingIssue()\n\n if (issue.state !== 'open') {\n return notEmulated('commenting on a closed issue is not emulated (no fixture records one)')\n }\n\n // Refused before any fault: a comment past the maximum could never be deleted again.\n if (state.counters.nextCommentId > githubMaxCommentId) {\n return notEmulated('the emulator has run out of comment ids')\n }\n\n return () => {\n // Read the clock before any write: a failing clock writes nothing.\n const at = timestamp(env)\n const id = state.counters.nextCommentId\n\n const comment: GithubEmulatorComment = {\n id,\n issueNumber: issue.number,\n body: input.body,\n user: state.viewer,\n authorAssociation: 'OWNER',\n createdAt: at,\n updatedAt: at\n }\n\n state.counters = { ...state.counters, nextCommentId: id + 1 }\n state.comments = [...state.comments, comment]\n\n return json(201, renderComment(state, comment))\n }\n }\n)\n\ntype CommentsInput = IssueInput & { readonly since: number }\n\nconst listComments: Route = statefulRoute(\n repoEvidence('GET', '/issues/{issueNumber}/comments', false, [commentCase], {\n issueNumber: issueNumberPattern\n }),\n 'none',\n (request): CommentsInput | NotEmulated => {\n const query = exactQuery(request, ['per_page', 'since'])\n\n if (isNotEmulated(query)) return query\n\n // The fixture records one page of 100, without `page`.\n if (query.per_page !== '100') {\n return notEmulated('a comment listing per_page other than 100 is not emulated')\n }\n\n const since = query.since ?? ''\n const instant = githubTimestampPattern.test(since) ? Date.parse(since) : Number.NaN\n\n if (!Number.isFinite(instant)) {\n return notEmulated('since must be a timestamp of the form YYYY-MM-DDTHH:MM:SSZ')\n }\n\n return { ...repoOf(request), number: issueNumberOf(request), since: instant }\n },\n (state, input, { env }) => {\n const other = otherRepository(state, input)\n\n if (other !== undefined) return other\n\n if (issueHeld(state, input.number) === undefined) return missingIssue()\n\n const listed = state.comments.filter(comment => {\n if (comment.issueNumber !== input.number) return false\n\n const updated = Date.parse(comment.updatedAt)\n\n return env.drills.sinceExcludesEqual ? updated > input.since : updated >= input.since\n })\n\n // The fixture records a listing of one comment and an empty one; the order of several is not\n // recorded.\n if (listed.length > 1) {\n return notEmulated('a comment listing of more than one comment is not emulated')\n }\n\n return answer(() =>\n json(\n 200,\n listed.map(comment => renderComment(state, comment))\n )\n )\n }\n)\n\ntype CommentIdInput = RepoInput & { readonly id: number }\n\nconst deleteComment: Route = statefulRoute(\n repoEvidence('DELETE', '/issues/comments/{commentId}', true, [commentCase], {\n commentId: commentIdPattern\n }),\n 'none',\n (request): CommentIdInput | NotEmulated =>\n withoutQuery(request) ?? { ...repoOf(request), id: Number(request.params.commentId) },\n (state, input) => {\n const other = otherRepository(state, input)\n\n if (other !== undefined) return other\n\n const comment = state.comments.find(candidate => candidate.id === input.id)\n\n if (comment !== undefined) {\n return () => {\n state.comments = state.comments.filter(candidate => candidate.id !== input.id)\n state.deletedComments = [...state.deletedComments, input.id]\n\n // The fixture's bodiless 204.\n return new Response(null, { status: 204 })\n }\n }\n\n // A comment deleted here answers the fixture's second-delete 404.\n return state.deletedComments.includes(input.id)\n ? answer(() => json(404, githubEmulatorErrorBodies.commentNotFound))\n : notEmulated('a comment this emulator never held is not emulated')\n }\n)\n\ntype AddLabelInput = IssueInput & { readonly label: string }\n\n/** GitHub orders label names case-insensitively; the fixture's answer is in that order. */\nconst sortsAfter = (name: string, other: string): boolean =>\n name.toLowerCase() > other.toLowerCase()\n\nconst addLabels: Route = statefulRoute(\n repoEvidence('POST', '/issues/{issueNumber}/labels', true, [labelCase], {\n issueNumber: issueNumberPattern\n }),\n 'json',\n (request): AddLabelInput | NotEmulated => {\n const issue = issueInput(request)\n\n if (isNotEmulated(issue)) return issue\n\n const body = exactBodyKeys(request.json, 'the labels body', ['labels'])\n\n if (isNotEmulated(body)) return body\n\n const labels = body.labels\n\n if (!Array.isArray(labels) || labels.length !== 1) {\n return notEmulated('adding other than exactly one label is not emulated')\n }\n\n const [label] = labels\n\n return Predicate.isString(label) && githubLabelNamePattern.test(label)\n ? { ...issue, label }\n : notEmulated('the label must be a plain label name')\n },\n (state, input, { env }) => {\n const other = otherRepository(state, input)\n\n if (other !== undefined) return other\n\n const issue = issueHeld(state, input.number)\n\n if (issue === undefined) return missingIssue()\n\n if (issue.state !== 'open') {\n return notEmulated('labelling a closed issue is not emulated (no fixture records one)')\n }\n\n // Adding a label the repository lacks creates it, which no fixture records.\n if (labelNamed(state, input.label) === undefined) {\n return notEmulated('adding a label the repository does not have is not emulated')\n }\n\n if (issue.labels.includes(input.label)) {\n return notEmulated('adding a label the issue already has is not emulated')\n }\n\n // The fixture's answer is both the issue's labels with the new one appended and name order.\n if (!issue.labels.every(name => sortsAfter(input.label, name))) {\n return notEmulated('adding a label that does not sort after the issue labels is not emulated')\n }\n\n return () => {\n const labels = [...issue.labels, input.label]\n\n state.issues = state.issues.map(candidate =>\n candidate.number === issue.number ? { ...candidate, labels } : candidate\n )\n\n return json(200, [\n ...renderLabels(state, env.drills.addAnswerOmitsLabel ? issue.labels : labels)\n ])\n }\n }\n)\n\nconst removeLabel: Route = statefulRoute(\n repoEvidence('DELETE', '/issues/{issueNumber}/labels/{name}', true, [labelCase], {\n issueNumber: issueNumberPattern,\n name: githubLabelNamePattern\n }),\n 'none',\n (request): AddLabelInput | NotEmulated => {\n const issue = issueInput(request)\n\n return isNotEmulated(issue) ? issue : { ...issue, label: request.params.name ?? '' }\n },\n (state, input) => {\n const other = otherRepository(state, input)\n\n if (other !== undefined) return other\n\n const issue = issueHeld(state, input.number)\n\n if (issue === undefined) return missingIssue()\n\n if (issue.state !== 'open') {\n return notEmulated('unlabelling a closed issue is not emulated (no fixture records one)')\n }\n\n if (labelNamed(state, input.label) === undefined) {\n return notEmulated('removing a label the repository does not have is not emulated')\n }\n\n // A repository label that is not on the issue answers the fixture's 404.\n if (!issue.labels.includes(input.label)) {\n return answer(() => json(404, githubEmulatorErrorBodies.labelNotOnIssue))\n }\n\n const labels = issue.labels.filter(name => name !== input.label)\n\n // The fixture's removal answers the remaining labels; no fixture records an empty answer.\n if (labels.length === 0) {\n return notEmulated(\n 'removing the last label of an issue is not emulated (no fixture records it)'\n )\n }\n\n return () => {\n state.issues = state.issues.map(candidate =>\n candidate.number === issue.number ? { ...candidate, labels } : candidate\n )\n\n return json(200, [...renderLabels(state, labels)])\n }\n }\n)\n\ntype CreateIssueInput = RepoInput & { readonly title: string; readonly body: string }\n\nconst createIssue: Route = statefulRoute(\n repoEvidence('POST', '/issues', true, [lifecycleCase]),\n 'json',\n (request): CreateIssueInput | NotEmulated => {\n const query = withoutQuery(request)\n\n if (query !== undefined) return query\n\n const body = exactBodyKeys(request.json, 'the issue body', ['title', 'body'])\n\n if (isNotEmulated(body)) return body\n\n const title = nonEmptyString(body.title, 'title')\n\n if (isNotEmulated(title)) return title\n\n return Predicate.isString(body.body)\n ? { ...repoOf(request), title, body: body.body }\n : notEmulated('body must be a string')\n },\n (state, input, { env }) => {\n const other = otherRepository(state, input)\n\n if (other !== undefined) return other\n\n const number = state.counters.nextIssueNumber\n\n if (number > githubMaxIssueNumber) {\n return notEmulated('the repository has run out of emulated issue numbers')\n }\n\n return () => {\n // Read the clock before any write: a failing clock writes nothing.\n const at = timestamp(env)\n\n const issue: GithubEmulatorIssue = {\n number,\n id: mintedIssueIdBase + number,\n nodeId: mintedIssueNodeId(number),\n title: input.title,\n body: input.body,\n state: 'open',\n stateReason: null,\n labels: [],\n user: state.viewer,\n authorAssociation: 'OWNER',\n createdAt: at,\n updatedAt: at,\n closedAt: null,\n createdHere: true\n }\n\n state.counters = { ...state.counters, nextIssueNumber: number + 1 }\n state.issues = [...state.issues, issue]\n\n return json(201, renderIssue(state, issue))\n }\n }\n)\n\ntype UpdateIssueInput = IssueInput &\n ({ readonly change: 'rename'; readonly title: string } | { readonly change: 'close' })\n\nconst updateIssue: Route = statefulRoute(\n repoEvidence('PATCH', '/issues/{issueNumber}', true, [lifecycleCase], {\n issueNumber: issueNumberPattern\n }),\n 'json',\n (request): UpdateIssueInput | NotEmulated => {\n const issue = issueInput(request)\n\n if (isNotEmulated(issue)) return issue\n\n const body = request.json\n\n if (!isJsonObject(body)) return notEmulated('the update body must be a JSON object')\n\n const keys = Object.keys(body).sort().join(',')\n\n // The two recorded updates: a rename, and closing as completed.\n if (keys === 'title') {\n const title = nonEmptyString(body.title, 'title')\n\n return isNotEmulated(title) ? title : { ...issue, change: 'rename', title }\n }\n\n if (keys === 'state,state_reason') {\n return body.state === 'closed' && body.state_reason === 'completed'\n ? { ...issue, change: 'close' }\n : notEmulated('state changes other than closed as completed are not emulated')\n }\n\n return notEmulated(\n 'issue updates other than { title } or { state, state_reason } are not emulated'\n )\n },\n (state, input, { env }) => {\n const other = otherRepository(state, input)\n\n if (other !== undefined) return other\n\n const issue = issueHeld(state, input.number)\n\n if (issue === undefined || !issue.createdHere) {\n return notEmulated('updating an issue not created here is not emulated')\n }\n\n if (issue.state !== 'open') {\n return notEmulated('updating a closed issue is not emulated (no fixture records one)')\n }\n\n const refusal = commentsRefusal(state, issue)\n\n if (refusal !== undefined) return refusal\n\n return () => {\n // Read the clock before any write: a failing clock writes nothing. A rename keeps\n // `updated_at`, as the lifecycle fixture records.\n const updated: GithubEmulatorIssue =\n input.change === 'rename'\n ? { ...issue, title: input.title }\n : (() => {\n const at = timestamp(env)\n\n return {\n ...issue,\n state: 'closed',\n stateReason: 'completed',\n closedAt: at,\n updatedAt: at\n }\n })()\n\n state.issues = state.issues.map(candidate =>\n candidate.number === issue.number ? updated : candidate\n )\n\n const rendered = renderIssue(state, updated)\n\n return json(\n 200,\n input.change === 'close' && env.drills.closeWithoutClosedAt\n ? { ...rendered, closed_at: null }\n : rendered\n )\n }\n }\n)\n\n/** The route table: evidence plus handlers. `githubEmulatorRoutes` is its evidence part. */\nexport const githubApiRoutes: ReadonlyArray<Route> = [\n listLabels,\n getIssue,\n searchIssues,\n getContents,\n createComment,\n listComments,\n deleteComment,\n addLabels,\n removeLabel,\n createIssue,\n updateIssue\n]\n"],"mappings":"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"}