@gjermundgaraba/effect-actions 0.19.0 → 0.21.0

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 (326) hide show
  1. package/README.md +19 -41
  2. package/dist/Action.d.mts +140 -48
  3. package/dist/Action.mjs +88 -44
  4. package/dist/ActionCli.d.mts +70 -35
  5. package/dist/ActionCli.mjs +53 -34
  6. package/dist/ActionHttp.d.mts +70 -38
  7. package/dist/ActionHttp.mjs +76 -25
  8. package/dist/ActionMcp.d.mts +56 -24
  9. package/dist/ActionMcp.mjs +104 -61
  10. package/dist/ActionRpc.d.mts +222 -57
  11. package/dist/ActionRpc.mjs +301 -49
  12. package/dist/ActionToolkit.d.mts +22 -7
  13. package/dist/ActionToolkit.mjs +37 -14
  14. package/dist/Authentication.d.mts +42 -12
  15. package/dist/Authentication.mjs +38 -32
  16. package/dist/Event.d.mts +370 -0
  17. package/dist/Event.mjs +103 -0
  18. package/dist/Testing.d.mts +1 -3
  19. package/dist/Testing.mjs +2 -3
  20. package/dist/Webhook.d.mts +146 -0
  21. package/dist/Webhook.mjs +72 -0
  22. package/dist/authentication/provider.d.mts +35 -12
  23. package/dist/authentication/provider.mjs +2 -3
  24. package/dist/authentication/refusal.mjs +3 -4
  25. package/dist/cli/commands.d.mts +9 -7
  26. package/dist/cli/commands.mjs +85 -35
  27. package/dist/contract/ast.mjs +5 -3
  28. package/dist/contract/authorizer.d.mts +8 -0
  29. package/dist/contract/authorizer.mjs +20 -0
  30. package/dist/contract/call.d.mts +9 -8
  31. package/dist/contract/call.mjs +49 -8
  32. package/dist/contract/callback.d.mts +7 -0
  33. package/dist/contract/callback.mjs +27 -0
  34. package/dist/contract/caller.d.mts +12 -3
  35. package/dist/contract/caller.mjs +23 -5
  36. package/dist/contract/canonical.mjs +10 -0
  37. package/dist/contract/codec.d.mts +9 -0
  38. package/dist/contract/codec.mjs +9 -0
  39. package/dist/contract/codes.mjs +81 -0
  40. package/dist/contract/declared.d.mts +0 -2
  41. package/dist/contract/declared.mjs +26 -11
  42. package/dist/contract/digest.mjs +6 -0
  43. package/dist/contract/errors.d.mts +9 -5
  44. package/dist/contract/errors.mjs +2 -3
  45. package/dist/contract/feed.d.mts +127 -0
  46. package/dist/contract/feed.mjs +249 -0
  47. package/dist/contract/hints.mjs +1 -2
  48. package/dist/contract/hub.d.mts +94 -0
  49. package/dist/contract/hub.mjs +133 -0
  50. package/dist/contract/idempotency.d.mts +47 -0
  51. package/dist/contract/idempotency.mjs +99 -0
  52. package/dist/contract/implementation.d.mts +73 -51
  53. package/dist/contract/implementation.mjs +94 -60
  54. package/dist/contract/input-check.d.mts +15 -0
  55. package/dist/contract/input-check.mjs +16 -0
  56. package/dist/contract/lease.mjs +28 -0
  57. package/dist/contract/listed.d.mts +4 -0
  58. package/dist/contract/listed.mjs +22 -0
  59. package/dist/contract/log.d.mts +41 -0
  60. package/dist/contract/log.mjs +144 -0
  61. package/dist/contract/media.d.mts +1 -3
  62. package/dist/contract/media.mjs +1 -2
  63. package/dist/contract/names.mjs +15 -6
  64. package/dist/contract/occurrence.d.mts +29 -0
  65. package/dist/contract/occurrence.mjs +45 -0
  66. package/dist/contract/pages.d.mts +13 -0
  67. package/dist/contract/pages.mjs +39 -0
  68. package/dist/contract/progress.d.mts +26 -0
  69. package/dist/contract/progress.mjs +72 -0
  70. package/dist/contract/push.d.mts +20 -0
  71. package/dist/contract/push.mjs +96 -0
  72. package/dist/contract/receiving.d.mts +29 -0
  73. package/dist/contract/receiving.mjs +42 -0
  74. package/dist/contract/recent.mjs +12 -0
  75. package/dist/contract/reported.mjs +16 -0
  76. package/dist/contract/serving.mjs +63 -0
  77. package/dist/contract/status.mjs +15 -7
  78. package/dist/contract/stream.d.mts +34 -0
  79. package/dist/contract/stream.mjs +31 -0
  80. package/dist/contract/subscribe.d.mts +45 -0
  81. package/dist/contract/subscribe.mjs +50 -0
  82. package/dist/contract/transient.mjs +28 -0
  83. package/dist/contract/watch.d.mts +18 -0
  84. package/dist/contract/watch.mjs +105 -0
  85. package/dist/contract/webhooks.d.mts +42 -0
  86. package/dist/contract/webhooks.mjs +53 -0
  87. package/dist/http/client.d.mts +37 -14
  88. package/dist/http/client.mjs +52 -14
  89. package/dist/http/events.d.mts +56 -0
  90. package/dist/http/events.mjs +102 -0
  91. package/dist/http/placed.d.mts +36 -0
  92. package/dist/http/placed.mjs +71 -0
  93. package/dist/http/schema-errors.mjs +2 -3
  94. package/dist/http/streamed.d.mts +21 -0
  95. package/dist/http/streamed.mjs +94 -0
  96. package/dist/http/watch.mjs +50 -0
  97. package/dist/mcp/bridge.mjs +283 -0
  98. package/dist/mcp/client.d.mts +30 -99
  99. package/dist/mcp/client.mjs +60 -134
  100. package/dist/mcp/events.mjs +142 -0
  101. package/dist/mcp/progress.mjs +34 -0
  102. package/dist/mcp/protocol.mjs +40 -15
  103. package/dist/mcp/reply.d.mts +79 -0
  104. package/dist/mcp/reply.mjs +99 -0
  105. package/dist/mcp/request.d.mts +40 -0
  106. package/dist/mcp/request.mjs +51 -0
  107. package/dist/mcp/watch.mjs +115 -0
  108. package/dist/rpc/watch.mjs +39 -0
  109. package/dist/stdio/console.mjs +14 -7
  110. package/dist/testing/memory.d.mts +1 -3
  111. package/dist/testing/memory.mjs +4 -5
  112. package/dist/webhook/address.mjs +47 -0
  113. package/dist/webhook/attempt.mjs +65 -0
  114. package/dist/webhook/receiver.mjs +67 -0
  115. package/dist/webhook/signing.mjs +28 -0
  116. package/dist/webhook/stored.mjs +57 -0
  117. package/dist/webhook/subscriptions.mjs +342 -0
  118. package/docs/Action-failures.md +2 -2
  119. package/docs/Action-streams.md +115 -0
  120. package/docs/Action.md +58 -124
  121. package/docs/ActionCli-failures.md +1 -2
  122. package/docs/ActionCli.md +83 -88
  123. package/docs/ActionHttp-failures.md +3 -3
  124. package/docs/ActionHttp.md +90 -70
  125. package/docs/ActionMcp-failures.md +2 -1
  126. package/docs/ActionMcp.md +120 -92
  127. package/docs/ActionRpc-events.md +126 -0
  128. package/docs/ActionRpc-failures.md +1 -2
  129. package/docs/ActionRpc.md +108 -79
  130. package/docs/ActionToolkit-failures.md +1 -2
  131. package/docs/ActionToolkit.md +33 -90
  132. package/docs/Authentication-advanced.md +57 -61
  133. package/docs/Authentication-failures.md +5 -4
  134. package/docs/Authentication.md +23 -58
  135. package/docs/Event-failures.md +15 -0
  136. package/docs/Event.md +281 -0
  137. package/docs/README.md +51 -82
  138. package/docs/Testing-failures.md +1 -5
  139. package/docs/Testing.md +126 -86
  140. package/docs/Webhook-failures.md +8 -0
  141. package/docs/Webhook.md +319 -0
  142. package/docs/setup-failures.md +2 -3
  143. package/docs/setup.md +32 -53
  144. package/examples/README.md +12 -37
  145. package/examples/assistant/README.md +21 -26
  146. package/examples/assistant/src/agent.ts +66 -24
  147. package/examples/assistant/src/cli.ts +7 -4
  148. package/examples/assistant/src/commands.ts +9 -4
  149. package/examples/assistant/src/identity/Identity.ts +5 -4
  150. package/examples/assistant/src/identity/authorize.ts +13 -5
  151. package/examples/assistant/src/identity/handlers.ts +3 -2
  152. package/examples/assistant/src/identity/permissions.ts +9 -3
  153. package/examples/assistant/src/platform/tasks-file.ts +15 -11
  154. package/examples/assistant/src/scripted.ts +7 -4
  155. package/examples/assistant/src/tasks/Tasks.ts +20 -6
  156. package/examples/assistant/src/tasks/handlers.ts +9 -5
  157. package/examples/assistant/src/tasks/store.ts +38 -11
  158. package/examples/assistant/src/toolkit.ts +11 -7
  159. package/examples/assistant/test/commands.test.ts +53 -27
  160. package/examples/assistant/test/identity/identity.test.ts +27 -11
  161. package/examples/assistant/test/platform/tasks-file.test.ts +39 -23
  162. package/examples/assistant/test/tasks/tasks.test.ts +62 -34
  163. package/examples/assistant/test/toolkit.test.ts +104 -75
  164. package/examples/build-events/README.md +83 -0
  165. package/examples/build-events/src/Api.ts +27 -0
  166. package/examples/build-events/src/Rpc.ts +26 -0
  167. package/examples/build-events/src/builds/Builds.ts +108 -0
  168. package/examples/build-events/src/builds/events.ts +54 -0
  169. package/examples/build-events/src/builds/handlers.ts +58 -0
  170. package/examples/build-events/src/builds/log.ts +30 -0
  171. package/examples/build-events/src/builds/status.ts +28 -0
  172. package/examples/build-events/src/builds/store.ts +130 -0
  173. package/examples/build-events/src/cli.ts +18 -0
  174. package/examples/build-events/src/commands.ts +32 -0
  175. package/examples/build-events/src/deploys/Deploys.ts +44 -0
  176. package/examples/build-events/src/deploys/approvers.ts +55 -0
  177. package/examples/build-events/src/deploys/events.ts +32 -0
  178. package/examples/build-events/src/deploys/handlers.ts +38 -0
  179. package/examples/build-events/src/identity/Identity.ts +29 -0
  180. package/examples/build-events/src/identity/members.ts +7 -0
  181. package/examples/build-events/src/identity/verifier.ts +22 -0
  182. package/examples/build-events/src/notifier/Notices.ts +26 -0
  183. package/examples/build-events/src/notifier/routes.ts +71 -0
  184. package/examples/build-events/src/notifier.ts +48 -0
  185. package/examples/build-events/src/routes.ts +71 -0
  186. package/examples/build-events/src/server.ts +62 -0
  187. package/examples/build-events/test/builds/events.test.ts +68 -0
  188. package/examples/build-events/test/builds/log.test.ts +71 -0
  189. package/examples/build-events/test/builds/progress.test.ts +57 -0
  190. package/examples/build-events/test/builds/status.test.ts +42 -0
  191. package/examples/build-events/test/commands.test.ts +103 -0
  192. package/examples/build-events/test/deploys/events.test.ts +52 -0
  193. package/examples/build-events/test/http.test.ts +95 -0
  194. package/examples/build-events/test/notifier.test.ts +80 -0
  195. package/examples/build-events/test/routes.test.ts +96 -0
  196. package/examples/build-events/test/rpc.test.ts +84 -0
  197. package/examples/build-events/test/support/as.ts +8 -0
  198. package/examples/build-events/test/support/memory.ts +43 -0
  199. package/examples/build-events/test/webhooks.test.ts +143 -0
  200. package/examples/kitchen-sink/README.md +178 -166
  201. package/examples/kitchen-sink/src/Actions.ts +11 -3
  202. package/examples/kitchen-sink/src/Api.ts +12 -6
  203. package/examples/kitchen-sink/src/Rpc.ts +7 -5
  204. package/examples/kitchen-sink/src/admin/Admin.ts +21 -10
  205. package/examples/kitchen-sink/src/admin/authenticate.ts +8 -4
  206. package/examples/kitchen-sink/src/admin/authorize.ts +13 -5
  207. package/examples/kitchen-sink/src/admin/grants.ts +5 -3
  208. package/examples/kitchen-sink/src/admin/handlers.ts +9 -5
  209. package/examples/kitchen-sink/src/agent.ts +14 -6
  210. package/examples/kitchen-sink/src/assistant/Assistant.ts +21 -15
  211. package/examples/kitchen-sink/src/assistant/conversations.ts +14 -9
  212. package/examples/kitchen-sink/src/assistant/demo-model.ts +2 -2
  213. package/examples/kitchen-sink/src/assistant/handlers.ts +42 -22
  214. package/examples/kitchen-sink/src/attachments/Attachments.ts +6 -6
  215. package/examples/kitchen-sink/src/attachments/download.ts +7 -6
  216. package/examples/kitchen-sink/src/attachments/handlers.ts +9 -4
  217. package/examples/kitchen-sink/src/attachments/store.ts +25 -18
  218. package/examples/kitchen-sink/src/browser.ts +10 -4
  219. package/examples/kitchen-sink/src/cli.ts +28 -12
  220. package/examples/kitchen-sink/src/config.ts +1 -1
  221. package/examples/kitchen-sink/src/identity/Identity.ts +24 -10
  222. package/examples/kitchen-sink/src/identity/authenticate.ts +19 -11
  223. package/examples/kitchen-sink/src/identity/authorize.ts +14 -4
  224. package/examples/kitchen-sink/src/identity/directory.ts +16 -7
  225. package/examples/kitchen-sink/src/identity/handlers.ts +1 -0
  226. package/examples/kitchen-sink/src/identity/members.ts +7 -2
  227. package/examples/kitchen-sink/src/identity/quota.ts +23 -20
  228. package/examples/kitchen-sink/src/issues/Issues.ts +91 -15
  229. package/examples/kitchen-sink/src/issues/assistant.ts +4 -2
  230. package/examples/kitchen-sink/src/issues/events.ts +46 -0
  231. package/examples/kitchen-sink/src/issues/export.ts +35 -0
  232. package/examples/kitchen-sink/src/issues/guide.ts +4 -3
  233. package/examples/kitchen-sink/src/issues/handlers.ts +20 -7
  234. package/examples/kitchen-sink/src/issues/store.ts +156 -67
  235. package/examples/kitchen-sink/src/issues/write-limit.ts +44 -18
  236. package/examples/kitchen-sink/src/memory.ts +11 -2
  237. package/examples/kitchen-sink/src/ops.ts +10 -5
  238. package/examples/kitchen-sink/src/request-policy.ts +25 -9
  239. package/examples/kitchen-sink/src/routes.ts +67 -27
  240. package/examples/kitchen-sink/src/sdk.ts +22 -12
  241. package/examples/kitchen-sink/src/seed.ts +15 -9
  242. package/examples/kitchen-sink/src/server.ts +24 -10
  243. package/examples/kitchen-sink/src/stdio.ts +9 -3
  244. package/examples/kitchen-sink/src/workspaces/Workspaces.ts +10 -7
  245. package/examples/kitchen-sink/src/workspaces/handlers.ts +3 -2
  246. package/examples/kitchen-sink/src/workspaces/registry.ts +16 -10
  247. package/examples/kitchen-sink/src/workspaces/tenant.ts +12 -5
  248. package/examples/kitchen-sink/test/admin/admin.test.ts +38 -23
  249. package/examples/kitchen-sink/test/assistant/assistant.test.ts +471 -299
  250. package/examples/kitchen-sink/test/attachments/attachments.test.ts +135 -94
  251. package/examples/kitchen-sink/test/identity/identity.test.ts +110 -82
  252. package/examples/kitchen-sink/test/identity/session.test.ts +65 -43
  253. package/examples/kitchen-sink/test/issues/assistant.test.ts +50 -31
  254. package/examples/kitchen-sink/test/issues/events.test.ts +273 -0
  255. package/examples/kitchen-sink/test/issues/export.test.ts +120 -0
  256. package/examples/kitchen-sink/test/issues/issues.test.ts +105 -75
  257. package/examples/kitchen-sink/test/request-policy.test.ts +130 -33
  258. package/examples/kitchen-sink/test/routes.test.ts +102 -57
  259. package/examples/kitchen-sink/test/support/as.ts +9 -0
  260. package/examples/kitchen-sink/test/support/scripted.ts +6 -5
  261. package/examples/kitchen-sink/test/support/services.ts +17 -0
  262. package/examples/live-rpc/README.md +33 -35
  263. package/examples/live-rpc/src/Rpc.ts +9 -5
  264. package/examples/live-rpc/src/browser.ts +39 -11
  265. package/examples/live-rpc/src/chat/Chat.ts +30 -16
  266. package/examples/live-rpc/src/chat/authorize.ts +2 -1
  267. package/examples/live-rpc/src/chat/events.ts +74 -0
  268. package/examples/live-rpc/src/chat/handlers.ts +13 -5
  269. package/examples/live-rpc/src/chat/room.ts +9 -5
  270. package/examples/live-rpc/src/chat/store.ts +51 -14
  271. package/examples/live-rpc/src/client.ts +22 -5
  272. package/examples/live-rpc/src/identity/Identity.ts +3 -3
  273. package/examples/live-rpc/src/identity/verifier.ts +8 -4
  274. package/examples/live-rpc/src/page.ts +58 -12
  275. package/examples/live-rpc/src/routes.ts +14 -10
  276. package/examples/live-rpc/src/server.ts +15 -5
  277. package/examples/live-rpc/test/browser.test.ts +127 -50
  278. package/examples/live-rpc/test/chat/chat.test.ts +215 -23
  279. package/examples/live-rpc/test/routes.test.ts +130 -46
  280. package/examples/live-rpc/test/server.test.ts +94 -33
  281. package/examples/mcp-tools/README.md +24 -38
  282. package/examples/mcp-tools/src/platform/fake-farm.ts +63 -24
  283. package/examples/mcp-tools/src/platform/print-farm.ts +59 -29
  284. package/examples/mcp-tools/src/printers/Printers.ts +18 -7
  285. package/examples/mcp-tools/src/printers/farm.ts +15 -5
  286. package/examples/mcp-tools/src/printers/handlers.ts +12 -6
  287. package/examples/mcp-tools/src/routes.ts +8 -6
  288. package/examples/mcp-tools/src/server.ts +12 -5
  289. package/examples/mcp-tools/src/stdio.ts +3 -2
  290. package/examples/mcp-tools/test/printers/printers.test.ts +132 -89
  291. package/examples/mcp-tools/test/routes.test.ts +140 -81
  292. package/examples/notes-service/README.md +30 -45
  293. package/examples/notes-service/src/Api.ts +1 -0
  294. package/examples/notes-service/src/browser.ts +15 -14
  295. package/examples/notes-service/src/cli.ts +8 -3
  296. package/examples/notes-service/src/commands.ts +46 -20
  297. package/examples/notes-service/src/identity/Identity.ts +2 -2
  298. package/examples/notes-service/src/identity/authorize.ts +6 -2
  299. package/examples/notes-service/src/identity/handlers.ts +3 -2
  300. package/examples/notes-service/src/identity/users.ts +2 -1
  301. package/examples/notes-service/src/identity/verifier.ts +12 -6
  302. package/examples/notes-service/src/notes/Notes.ts +46 -22
  303. package/examples/notes-service/src/notes/handlers.ts +18 -9
  304. package/examples/notes-service/src/notes/store.ts +70 -43
  305. package/examples/notes-service/src/routes.ts +7 -5
  306. package/examples/notes-service/src/server.ts +12 -5
  307. package/examples/notes-service/src/status/Status.ts +1 -1
  308. package/examples/notes-service/src/status/handlers.ts +5 -3
  309. package/examples/notes-service/src/stdio.ts +7 -3
  310. package/examples/notes-service/src/worker.ts +6 -3
  311. package/examples/notes-service/test/commands.test.ts +164 -91
  312. package/examples/notes-service/test/notes/as.ts +3 -1
  313. package/examples/notes-service/test/notes/idempotency.test.ts +97 -67
  314. package/examples/notes-service/test/notes/in-process.test.ts +26 -15
  315. package/examples/notes-service/test/notes/notes.test.ts +49 -31
  316. package/examples/notes-service/test/routes.test.ts +106 -71
  317. package/examples/notes-service/test/worker.test.ts +21 -13
  318. package/examples/quickstart/README.md +2 -6
  319. package/examples/quickstart/src/Api.ts +1 -0
  320. package/examples/quickstart/src/client.ts +6 -3
  321. package/examples/quickstart/src/greeting/Greeting.ts +1 -1
  322. package/examples/quickstart/src/greeting/handlers.ts +5 -2
  323. package/examples/quickstart/src/routes.ts +3 -2
  324. package/examples/quickstart/src/server.ts +6 -2
  325. package/examples/quickstart/test/routes.test.ts +3 -2
  326. package/package.json +19 -17
@@ -1,13 +1,10 @@
1
1
  import { describe, expect, it } from "@effect/vitest";
2
+ import * as Action from "@gjermundgaraba/effect-actions/Action";
2
3
  import { ConfigProvider, Effect, Fiber, Layer } from "effect";
3
- import {
4
- HttpClient,
5
- HttpClientError,
6
- type HttpClientRequest,
7
- HttpClientResponse,
8
- } from "effect/http";
4
+ import { HttpClient, HttpClientError, HttpClientResponse } from "effect/http";
5
+ import type { HttpClientRequest } from "effect/http";
9
6
  import { TestClock } from "effect/testing";
10
- import * as Action from "@gjermundgaraba/effect-actions/Action";
7
+
11
8
  import { fakeFarm } from "../../src/platform/fake-farm.js";
12
9
  import { printFarm } from "../../src/platform/print-farm.js";
13
10
  import { printers } from "../../src/printers/handlers.js";
@@ -27,63 +24,83 @@ const farmOver = (upstream: Layer.Layer<HttpClient.HttpClient>) =>
27
24
  FARM_URL: "https://farm.example.test",
28
25
  FARM_USER: "agent",
29
26
  FARM_PASSWORD: "secret",
30
- }),
31
- ),
32
- ),
27
+ })
28
+ )
29
+ )
33
30
  );
34
31
 
35
32
  // The fake farm, noting each request the service sends it. A request is noted as the service
36
33
  // wrote it, before its client prepends FARM_URL, so the paths are relative.
37
- const noting = (sent: Array<string>) =>
34
+ const noting = (sent: string[]) =>
38
35
  Layer.effect(
39
36
  HttpClient.HttpClient,
40
37
  Effect.map(
41
38
  HttpClient.HttpClient,
42
39
  HttpClient.tapRequest((request) =>
43
- Effect.sync(() => sent.push(`${request.method} ${request.url}`)),
44
- ),
45
- ),
40
+ Effect.sync(() => sent.push(`${request.method} ${request.url}`))
41
+ )
42
+ )
46
43
  ).pipe(Layer.provide(fakeFarm));
47
44
 
48
45
  // A farm that lets the service sign in, then answers every other request with `answer`.
49
46
  const farmThat = (
50
47
  answer: (
51
- request: HttpClientRequest.HttpClientRequest,
52
- ) => Effect.Effect<HttpClientResponse.HttpClientResponse, HttpClientError.HttpClientError>,
48
+ request: HttpClientRequest.HttpClientRequest
49
+ ) => Effect.Effect<
50
+ HttpClientResponse.HttpClientResponse,
51
+ HttpClientError.HttpClientError
52
+ >
53
53
  ) =>
54
54
  Layer.succeed(
55
55
  HttpClient.HttpClient,
56
56
  HttpClient.make((request, url) =>
57
57
  url.pathname === "/api/login"
58
- ? Effect.succeed(HttpClientResponse.fromWeb(request, Response.json({ token: "session" })))
59
- : answer(request),
60
- ),
58
+ ? Effect.succeed(
59
+ HttpClientResponse.fromWeb(
60
+ request,
61
+ Response.json({ token: "session" })
62
+ )
63
+ )
64
+ : answer(request)
65
+ )
61
66
  );
62
67
 
63
68
  const door = { id: "mk4-1", name: "MK4 by the door" };
64
69
 
65
70
  describe("printers, in process", () => {
66
- it.effect("pauses, resumes and cancels a job, pause and cancel safe to repeat", () =>
67
- Effect.gen(function* () {
68
- const client = yield* Action.client(printers);
69
- const pause = client.set_paused({ printer_id: "mk4-1", paused: true });
70
- const cancel = client.cancel_print({ printer_id: "mk4-1" });
71
- const job = { file: "bracket.gcode", progress: 62 };
72
-
73
- expect(yield* pause).toEqual({ ...door, state: "paused", job });
74
- expect(yield* pause).toEqual({ ...door, state: "paused", job });
75
- expect(yield* client.set_paused({ printer_id: "mk4-1", paused: false })).toEqual({
76
- ...door,
77
- state: "printing",
78
- job,
79
- });
80
- expect(yield* cancel).toEqual({ ...door, state: "idle", job: null });
81
- expect(yield* cancel).toEqual({ ...door, state: "idle", job: null });
82
- }).pipe(Effect.provide(farmOver(fakeFarm))),
71
+ it.effect(
72
+ "pauses, resumes and cancels a job, pause and cancel safe to repeat",
73
+ () =>
74
+ Effect.gen(function* () {
75
+ const client = yield* Action.client(printers);
76
+ const pause = client.set_paused({ printer_id: "mk4-1", paused: true });
77
+ const cancel = client.cancel_print({ printer_id: "mk4-1" });
78
+ const job = { file: "bracket.gcode", progress: 62 };
79
+
80
+ expect(yield* pause).toStrictEqual({ ...door, state: "paused", job });
81
+ expect(yield* pause).toStrictEqual({ ...door, state: "paused", job });
82
+ expect(
83
+ yield* client.set_paused({ printer_id: "mk4-1", paused: false })
84
+ ).toStrictEqual({
85
+ ...door,
86
+ state: "printing",
87
+ job,
88
+ });
89
+ expect(yield* cancel).toStrictEqual({
90
+ ...door,
91
+ state: "idle",
92
+ job: null,
93
+ });
94
+ expect(yield* cancel).toStrictEqual({
95
+ ...door,
96
+ state: "idle",
97
+ job: null,
98
+ });
99
+ }).pipe(Effect.provide(farmOver(fakeFarm)))
83
100
  );
84
101
 
85
102
  it.effect("signs in once at startup, however many tools are called", () => {
86
- const sent: Array<string> = [];
103
+ const sent: string[] = [];
87
104
 
88
105
  return Effect.gen(function* () {
89
106
  const client = yield* Action.client(printers);
@@ -91,7 +108,7 @@ describe("printers, in process", () => {
91
108
  yield* client.list_printers();
92
109
  yield* client.set_paused({ printer_id: "mk4-1", paused: true });
93
110
 
94
- expect(sent).toEqual([
111
+ expect(sent).toStrictEqual([
95
112
  "POST /api/login",
96
113
  "GET /api/printers",
97
114
  "POST /api/printers/mk4-1/pause",
@@ -99,36 +116,56 @@ describe("printers, in process", () => {
99
116
  }).pipe(Effect.provide(farmOver(noting(sent))));
100
117
  });
101
118
 
102
- it.effect("refuses an id that could leave its place in the farm's URL, sending nothing", () => {
103
- const sent: Array<string> = [];
104
-
105
- return Effect.gen(function* () {
106
- const client = yield* Action.client(printers);
107
-
108
- for (const printer_id of ["..", "mk4-1/../..", "mk4-1?force=true", ""]) {
109
- const refused = yield* Effect.flip(client.cancel_print({ printer_id }));
110
-
111
- expect(refused).toBeInstanceOf(Action.InvalidInput);
112
- }
113
-
114
- expect(sent).toEqual(["POST /api/login"]);
115
- }).pipe(Effect.provide(farmOver(noting(sent))));
116
- });
119
+ it.effect(
120
+ "refuses an id that could leave its place in the farm's URL, sending nothing",
121
+ () => {
122
+ const sent: string[] = [];
123
+
124
+ return Effect.gen(function* () {
125
+ const client = yield* Action.client(printers);
126
+
127
+ for (const printer_id of [
128
+ "..",
129
+ "mk4-1/../..",
130
+ "mk4-1?force=true",
131
+ "",
132
+ ]) {
133
+ const refused = yield* Effect.flip(
134
+ client.cancel_print({ printer_id })
135
+ );
136
+
137
+ expect(refused).toBeInstanceOf(Action.InvalidInput);
138
+ }
139
+
140
+ expect(sent).toStrictEqual(["POST /api/login"]);
141
+ }).pipe(Effect.provide(farmOver(noting(sent))));
142
+ }
143
+ );
117
144
 
118
145
  // The message is all a model gets of a failure, so each test asserts it.
119
- it.effect("fails with the declared error, carrying the farm's own reason", () =>
120
- Effect.gen(function* () {
121
- const client = yield* Action.client(printers);
122
- const busy = yield* Effect.flip(client.start_print({ printer_id: "mk4-1", file: "a.gcode" }));
123
- const unknown = yield* Effect.flip(client.get_camera_snapshot({ printer_id: "mk9" }));
124
-
125
- expect(busy).toBeInstanceOf(PrintFarmError);
126
- expect(busy.message).toBe(
127
- "The print farm refused: MK4 by the door is busy with bracket.gcode.",
128
- );
129
- expect(unknown).toBeInstanceOf(PrintFarmError);
130
- expect(unknown.message).toBe("The print farm refused: No printer has the id mk9.");
131
- }).pipe(Effect.provide(farmOver(fakeFarm))),
146
+ it.effect(
147
+ "fails with the declared error, carrying the farm's own reason",
148
+ () =>
149
+ Effect.gen(function* () {
150
+ const client = yield* Action.client(printers);
151
+
152
+ const busy = yield* Effect.flip(
153
+ client.start_print({ printer_id: "mk4-1", file: "a.gcode" })
154
+ );
155
+
156
+ const unknown = yield* Effect.flip(
157
+ client.get_camera_snapshot({ printer_id: "mk9" })
158
+ );
159
+
160
+ expect(busy).toBeInstanceOf(PrintFarmError);
161
+ expect(busy.message).toBe(
162
+ "The print farm refused: MK4 by the door is busy with bracket.gcode."
163
+ );
164
+ expect(unknown).toBeInstanceOf(PrintFarmError);
165
+ expect(unknown.message).toBe(
166
+ "The print farm refused: No printer has the id mk9."
167
+ );
168
+ }).pipe(Effect.provide(farmOver(fakeFarm)))
132
169
  );
133
170
 
134
171
  it.effect("says so when the farm cannot be reached", () =>
@@ -145,30 +182,36 @@ describe("printers, in process", () => {
145
182
  Effect.fail(
146
183
  new HttpClientError.HttpClientError({
147
184
  reason: new HttpClientError.TransportError({ request }),
148
- }),
149
- ),
150
- ),
151
- ),
152
- ),
153
- ),
185
+ })
186
+ )
187
+ )
188
+ )
189
+ )
190
+ )
154
191
  );
155
192
 
156
- it.effect("says so when the farm answers in a shape the service does not know", () =>
157
- Effect.gen(function* () {
158
- const client = yield* Action.client(printers);
159
- const error = yield* Effect.flip(client.list_printers());
160
-
161
- expect(error).toBeInstanceOf(PrintFarmError);
162
- expect(error.message).toBe("The print farm sent an answer this server could not read.");
163
- }).pipe(
164
- Effect.provide(
165
- farmOver(
166
- farmThat((request) =>
167
- Effect.succeed(HttpClientResponse.fromWeb(request, Response.json({ rows: [] }))),
168
- ),
169
- ),
170
- ),
171
- ),
193
+ it.effect(
194
+ "says so when the farm answers in a shape the service does not know",
195
+ () =>
196
+ Effect.gen(function* () {
197
+ const client = yield* Action.client(printers);
198
+ const error = yield* Effect.flip(client.list_printers());
199
+
200
+ expect(error).toBeInstanceOf(PrintFarmError);
201
+ expect(error.message).toBe(
202
+ "The print farm sent an answer this server could not read."
203
+ );
204
+ }).pipe(
205
+ Effect.provide(
206
+ farmOver(
207
+ farmThat((request) =>
208
+ Effect.succeed(
209
+ HttpClientResponse.fromWeb(request, Response.json({ rows: [] }))
210
+ )
211
+ )
212
+ )
213
+ )
214
+ )
172
215
  );
173
216
 
174
217
  // `it.effect` runs on a test clock, so the thirty seconds pass when the test says so.
@@ -183,6 +226,6 @@ describe("printers, in process", () => {
183
226
 
184
227
  expect(error).toBeInstanceOf(PrintFarmError);
185
228
  expect(error.message).toBe("The print farm did not answer in time.");
186
- }).pipe(Effect.provide(farmOver(farmThat(() => Effect.never)))),
229
+ }).pipe(Effect.provide(farmOver(farmThat(() => Effect.never))))
187
230
  );
188
231
  });
@@ -1,8 +1,9 @@
1
1
  import { describe, expect, it } from "@effect/vitest";
2
- import { ConfigProvider, Effect, Layer, Schema } from "effect";
3
- import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http";
4
2
  import * as ActionMcp from "@gjermundgaraba/effect-actions/ActionMcp";
5
3
  import * as Testing from "@gjermundgaraba/effect-actions/Testing";
4
+ import { ConfigProvider, Effect, Layer, Schema } from "effect";
5
+ import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http";
6
+
6
7
  import { fakeFarm, picture } from "../src/platform/fake-farm.js";
7
8
  import { printFarm } from "../src/platform/print-farm.js";
8
9
  import { PrintFarmError, Printers } from "../src/printers/Printers.js";
@@ -12,13 +13,19 @@ import { routes } from "../src/routes.js";
12
13
  // The real routes answer in memory, and no port is opened.
13
14
 
14
15
  // What the entrypoint reads from the environment, given to the test as a Layer.
15
- const farm = { FARM_URL: "https://farm.example.test", FARM_USER: "agent", FARM_PASSWORD: "secret" };
16
+ const farm = {
17
+ FARM_URL: "https://farm.example.test",
18
+ FARM_USER: "agent",
19
+ FARM_PASSWORD: "secret",
20
+ };
16
21
 
17
22
  // The fake farm goes to `printFarm` alone, so the test's own requests still reach the routes.
18
23
  const servedWith = (environment: Record<string, string>) =>
19
24
  Testing.layer(routes).pipe(
20
25
  Layer.provide(printFarm.pipe(Layer.provide(fakeFarm))),
21
- Layer.provide(ConfigProvider.layer(ConfigProvider.fromEnvRecord(environment))),
26
+ Layer.provide(
27
+ ConfigProvider.layer(ConfigProvider.fromEnvRecord(environment))
28
+ )
22
29
  );
23
30
 
24
31
  const served = servedWith({
@@ -35,7 +42,7 @@ const preflight = (origin: string) =>
35
42
  "access-control-request-method": "POST",
36
43
  "access-control-request-headers": "content-type, mcp-protocol-version",
37
44
  },
38
- }),
45
+ })
39
46
  );
40
47
 
41
48
  // One tool of a `tools/list` answer, as far as this test reads it.
@@ -50,32 +57,48 @@ const Listed = Schema.Struct({
50
57
  destructiveHint: Schema.Boolean,
51
58
  idempotentHint: Schema.Boolean,
52
59
  }),
53
- }),
60
+ })
54
61
  ),
55
62
  }),
56
63
  });
57
64
 
65
+ const hints = (
66
+ readOnlyHint: boolean,
67
+ destructiveHint: boolean,
68
+ idempotentHint: boolean
69
+ ) => ({
70
+ readOnlyHint,
71
+ destructiveHint,
72
+ idempotentHint,
73
+ });
74
+
75
+ const from = (origin: string) =>
76
+ HttpClient.execute(
77
+ ActionMcp.request("tools/list", {}, { headers: { origin } })
78
+ );
79
+
58
80
  describe("the tools, over MCP", () => {
59
81
  it.effect("lists every tool with its title and hints as sent", () =>
60
82
  Effect.gen(function* () {
61
83
  // A raw request, for what no typed client shows: here, the listing itself.
62
- const response = yield* HttpClient.execute(ActionMcp.request("tools/list"));
63
- const { result } = yield* HttpClientResponse.schemaBodyJson(Listed)(response);
84
+ const response = yield* HttpClient.execute(
85
+ ActionMcp.request("tools/list")
86
+ );
64
87
 
65
- const hints = (readOnlyHint: boolean, destructiveHint: boolean, idempotentHint: boolean) => ({
66
- readOnlyHint,
67
- destructiveHint,
68
- idempotentHint,
69
- });
88
+ const { result } =
89
+ yield* HttpClientResponse.schemaBodyJson(Listed)(response);
70
90
 
71
- expect(result.tools).toEqual([
91
+ expect(result.tools).toStrictEqual([
72
92
  {
73
93
  name: "list_printers",
74
94
  annotations: { title: "List printers", ...hints(true, false, false) },
75
95
  },
76
96
  {
77
97
  name: "get_camera_snapshot",
78
- annotations: { title: "Camera snapshot", ...hints(true, false, false) },
98
+ annotations: {
99
+ title: "Camera snapshot",
100
+ ...hints(true, false, false),
101
+ },
79
102
  },
80
103
  {
81
104
  name: "start_print",
@@ -83,14 +106,17 @@ describe("the tools, over MCP", () => {
83
106
  },
84
107
  {
85
108
  name: "set_paused",
86
- annotations: { title: "Pause or resume a print", ...hints(false, true, true) },
109
+ annotations: {
110
+ title: "Pause or resume a print",
111
+ ...hints(false, true, true),
112
+ },
87
113
  },
88
114
  {
89
115
  name: "cancel_print",
90
116
  annotations: { title: "Cancel a print", ...hints(false, true, true) },
91
117
  },
92
118
  ]);
93
- }).pipe(Effect.provide(served)),
119
+ }).pipe(Effect.provide(served))
94
120
  );
95
121
 
96
122
  it.effect("answers a camera's picture as an image beside the printer", () =>
@@ -99,7 +125,9 @@ describe("the tools, over MCP", () => {
99
125
  // from its image block, as the handler returned it.
100
126
  const mcp = yield* ActionMcp.client(Printers);
101
127
 
102
- expect(yield* mcp.get_camera_snapshot({ printer_id: "mk4-1" })).toEqual({
128
+ expect(
129
+ yield* mcp.get_camera_snapshot({ printer_id: "mk4-1" })
130
+ ).toStrictEqual({
103
131
  printer: {
104
132
  id: "mk4-1",
105
133
  name: "MK4 by the door",
@@ -108,80 +136,111 @@ describe("the tools, over MCP", () => {
108
136
  },
109
137
  image: { data: picture, mimeType: "image/png" },
110
138
  });
111
- }).pipe(Effect.provide(served)),
139
+ }).pipe(Effect.provide(served))
112
140
  );
113
141
 
114
- it.effect("answers the farm's refusal as the tool's declared error, message and all", () =>
115
- Effect.gen(function* () {
116
- const mcp = yield* ActionMcp.client(Printers);
117
- const error = yield* Effect.flip(mcp.get_camera_snapshot({ printer_id: "mini-1" }));
142
+ it.effect(
143
+ "answers the farm's refusal as the tool's declared error, message and all",
144
+ () =>
145
+ Effect.gen(function* () {
146
+ const mcp = yield* ActionMcp.client(Printers);
118
147
 
119
- expect(error).toBeInstanceOf(PrintFarmError);
120
- expect(error.message).toBe("The print farm refused: Mini on the shelf is offline.");
121
- }).pipe(Effect.provide(served)),
122
- );
148
+ const error = yield* Effect.flip(
149
+ mcp.get_camera_snapshot({ printer_id: "mini-1" })
150
+ );
123
151
 
124
- it.effect("accepts the browser origins the configuration lists, and no other", () =>
125
- Effect.gen(function* () {
126
- const from = (origin: string) =>
127
- HttpClient.execute(ActionMcp.request("tools/list", {}, { headers: { origin } }));
152
+ expect(error).toBeInstanceOf(PrintFarmError);
153
+ expect(error.message).toBe(
154
+ "The print farm refused: Mini on the shelf is offline."
155
+ );
156
+ }).pipe(Effect.provide(served))
157
+ );
128
158
 
129
- expect((yield* from("https://console.example.test")).status).toBe(200);
130
- expect((yield* from("https://wall.example.test")).status).toBe(200);
131
- expect((yield* from("https://elsewhere.example.test")).status).toBe(403);
132
- }).pipe(Effect.provide(served)),
159
+ it.effect(
160
+ "accepts the browser origins the configuration lists, and no other",
161
+ () =>
162
+ Effect.gen(function* () {
163
+ expect((yield* from("https://console.example.test")).status).toBe(200);
164
+ expect((yield* from("https://wall.example.test")).status).toBe(200);
165
+ expect((yield* from("https://elsewhere.example.test")).status).toBe(
166
+ 403
167
+ );
168
+ }).pipe(Effect.provide(served))
133
169
  );
134
170
 
135
- it.effect("answers a listed origin's CORS preflight, and its headers on a tool's answer", () =>
136
- Effect.gen(function* () {
137
- const asked = yield* preflight("https://console.example.test");
171
+ it.effect(
172
+ "answers a listed origin's CORS preflight, and its headers on a tool's answer",
173
+ () =>
174
+ Effect.gen(function* () {
175
+ const asked = yield* preflight("https://console.example.test");
138
176
 
139
- expect(asked.status).toBe(204);
140
- expect(asked.headers["access-control-allow-origin"]).toBe("https://console.example.test");
141
- expect(asked.headers["access-control-allow-methods"]).toBe("POST, DELETE");
142
- expect(asked.headers["access-control-allow-headers"]).toContain("MCP-Protocol-Version");
143
-
144
- const listed = yield* HttpClient.execute(
145
- ActionMcp.request(
146
- "tools/list",
147
- {},
148
- { headers: { origin: "https://console.example.test" } },
149
- ),
150
- );
177
+ expect(asked.status).toBe(204);
178
+ expect(asked.headers["access-control-allow-origin"]).toBe(
179
+ "https://console.example.test"
180
+ );
181
+ expect(asked.headers["access-control-allow-methods"]).toBe(
182
+ "POST, DELETE"
183
+ );
184
+ expect(asked.headers["access-control-allow-headers"]).toContain(
185
+ "MCP-Protocol-Version"
186
+ );
151
187
 
152
- expect(listed.headers["access-control-allow-origin"]).toBe("https://console.example.test");
153
- expect(listed.headers["access-control-expose-headers"]).toContain("Mcp-Session-Id");
188
+ const listed = yield* HttpClient.execute(
189
+ ActionMcp.request(
190
+ "tools/list",
191
+ {},
192
+ { headers: { origin: "https://console.example.test" } }
193
+ )
194
+ );
154
195
 
155
- // Two origins are configured, so each request's own is compared with the list.
156
- expect((yield* preflight("https://elsewhere.example.test")).headers).not.toHaveProperty(
157
- "access-control-allow-origin",
158
- );
159
- }).pipe(Effect.provide(served)),
196
+ expect(listed.headers["access-control-allow-origin"]).toBe(
197
+ "https://console.example.test"
198
+ );
199
+ expect(listed.headers["access-control-expose-headers"]).toContain(
200
+ "Mcp-Session-Id"
201
+ );
202
+
203
+ // Two origins are configured, so each request's own is compared with the list.
204
+ expect(
205
+ (yield* preflight("https://elsewhere.example.test")).headers
206
+ ).not.toHaveProperty("access-control-allow-origin");
207
+ }).pipe(Effect.provide(served))
160
208
  );
161
209
 
162
- it.effect("names its one configured origin to every preflight, and still refuses any other", () =>
163
- Effect.gen(function* () {
164
- // With exactly one origin, Effect's CORS compares nothing: the header is that origin
165
- // whoever asks. A page elsewhere is still refused, twice: its browser reads an origin
166
- // that is not its own, and the endpoint answers its request 403.
167
- const asked = yield* preflight("https://elsewhere.example.test");
168
-
169
- expect(asked.status).toBe(204);
170
- expect(asked.headers["access-control-allow-origin"]).toBe("https://console.example.test");
171
-
172
- const called = yield* HttpClient.execute(
173
- ActionMcp.request(
174
- "tools/list",
175
- {},
176
- { headers: { origin: "https://elsewhere.example.test" } },
177
- ),
178
- );
210
+ it.effect(
211
+ "names its one configured origin to every preflight, and still refuses any other",
212
+ () =>
213
+ Effect.gen(function* () {
214
+ // With exactly one origin, Effect's CORS compares nothing: the header is that origin
215
+ // whoever asks. A page elsewhere is still refused, twice: its browser reads an origin
216
+ // that is not its own, and the endpoint answers its request 403.
217
+ const asked = yield* preflight("https://elsewhere.example.test");
179
218
 
180
- expect(called.status).toBe(403);
181
- expect(called.headers["access-control-allow-origin"]).toBe("https://console.example.test");
182
- }).pipe(
183
- Effect.provide(servedWith({ ...farm, ALLOWED_ORIGINS: "https://console.example.test" })),
184
- ),
219
+ expect(asked.status).toBe(204);
220
+ expect(asked.headers["access-control-allow-origin"]).toBe(
221
+ "https://console.example.test"
222
+ );
223
+
224
+ const called = yield* HttpClient.execute(
225
+ ActionMcp.request(
226
+ "tools/list",
227
+ {},
228
+ { headers: { origin: "https://elsewhere.example.test" } }
229
+ )
230
+ );
231
+
232
+ expect(called.status).toBe(403);
233
+ expect(called.headers["access-control-allow-origin"]).toBe(
234
+ "https://console.example.test"
235
+ );
236
+ }).pipe(
237
+ Effect.provide(
238
+ servedWith({
239
+ ...farm,
240
+ ALLOWED_ORIGINS: "https://console.example.test",
241
+ })
242
+ )
243
+ )
185
244
  );
186
245
 
187
246
  it.effect("allows no origin where none is configured", () =>
@@ -189,7 +248,7 @@ describe("the tools, over MCP", () => {
189
248
  const asked = yield* preflight("https://console.example.test");
190
249
 
191
250
  expect(asked.headers).not.toHaveProperty("access-control-allow-origin");
192
- }).pipe(Effect.provide(servedWith(farm))),
251
+ }).pipe(Effect.provide(servedWith(farm)))
193
252
  );
194
253
 
195
254
  it.effect("serves the host's own route beside the tools", () =>
@@ -197,6 +256,6 @@ describe("the tools, over MCP", () => {
197
256
  const health = yield* HttpClient.get("/healthz");
198
257
 
199
258
  expect(yield* health.text).toBe("ok");
200
- }).pipe(Effect.provide(served)),
259
+ }).pipe(Effect.provide(served))
201
260
  );
202
261
  });