@smthrs/kernel 0.0.0-stage → 1.0.0-rc.4

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 (267) hide show
  1. package/CHANGELOG.md +206 -0
  2. package/LICENSE +21 -0
  3. package/README.md +154 -2
  4. package/dist/cjs/CapabilitySet.d.ts +6 -0
  5. package/dist/cjs/CapabilitySet.d.ts.map +1 -0
  6. package/dist/cjs/CapabilitySet.js +19 -0
  7. package/dist/cjs/CapabilitySet.js.map +7 -0
  8. package/dist/cjs/ChildProcessEnvironment.d.ts +60 -0
  9. package/dist/cjs/ChildProcessEnvironment.d.ts.map +1 -0
  10. package/dist/cjs/ChildProcessEnvironment.js +61 -0
  11. package/dist/cjs/ChildProcessEnvironment.js.map +7 -0
  12. package/dist/cjs/ChildProcessSpawner.d.ts +100 -0
  13. package/dist/cjs/ChildProcessSpawner.d.ts.map +1 -0
  14. package/dist/cjs/ChildProcessSpawner.js +291 -0
  15. package/dist/cjs/ChildProcessSpawner.js.map +7 -0
  16. package/dist/cjs/CommandLine.d.ts +173 -0
  17. package/dist/cjs/CommandLine.d.ts.map +1 -0
  18. package/dist/cjs/CommandLine.js +54 -0
  19. package/dist/cjs/CommandLine.js.map +7 -0
  20. package/dist/cjs/ContainedSpawner.d.ts +135 -0
  21. package/dist/cjs/ContainedSpawner.d.ts.map +1 -0
  22. package/dist/cjs/ContainedSpawner.js +141 -0
  23. package/dist/cjs/ContainedSpawner.js.map +7 -0
  24. package/dist/cjs/FileSystem.d.ts +452 -0
  25. package/dist/cjs/FileSystem.d.ts.map +1 -0
  26. package/dist/cjs/FileSystem.js +890 -0
  27. package/dist/cjs/FileSystem.js.map +7 -0
  28. package/dist/cjs/FileSystemBatch.d.ts +94 -0
  29. package/dist/cjs/FileSystemBatch.d.ts.map +1 -0
  30. package/dist/cjs/FileSystemBatch.js +31 -0
  31. package/dist/cjs/FileSystemBatch.js.map +7 -0
  32. package/dist/cjs/GrantEvent.d.ts +240 -0
  33. package/dist/cjs/GrantEvent.d.ts.map +1 -0
  34. package/dist/cjs/GrantEvent.js +115 -0
  35. package/dist/cjs/GrantEvent.js.map +7 -0
  36. package/dist/cjs/GrantStore.d.ts +270 -0
  37. package/dist/cjs/GrantStore.d.ts.map +1 -0
  38. package/dist/cjs/GrantStore.js +857 -0
  39. package/dist/cjs/GrantStore.js.map +7 -0
  40. package/dist/cjs/HostError.d.ts +36 -0
  41. package/dist/cjs/HostError.d.ts.map +1 -0
  42. package/dist/cjs/HostError.js +17 -0
  43. package/dist/cjs/HostError.js.map +7 -0
  44. package/dist/cjs/HostServices.d.ts +101 -0
  45. package/dist/cjs/HostServices.d.ts.map +1 -0
  46. package/dist/cjs/HostServices.js +66 -0
  47. package/dist/cjs/HostServices.js.map +7 -0
  48. package/dist/cjs/HttpClient.d.ts +177 -0
  49. package/dist/cjs/HttpClient.d.ts.map +1 -0
  50. package/dist/cjs/HttpClient.js +217 -0
  51. package/dist/cjs/HttpClient.js.map +7 -0
  52. package/dist/cjs/Jj.d.ts +67 -0
  53. package/dist/cjs/Jj.d.ts.map +1 -0
  54. package/dist/cjs/Jj.js +118 -0
  55. package/dist/cjs/Jj.js.map +7 -0
  56. package/dist/cjs/JournalGrantStore.d.ts +65 -0
  57. package/dist/cjs/JournalGrantStore.d.ts.map +1 -0
  58. package/dist/cjs/JournalGrantStore.js +354 -0
  59. package/dist/cjs/JournalGrantStore.js.map +7 -0
  60. package/dist/cjs/Path.d.ts +37 -0
  61. package/dist/cjs/Path.d.ts.map +1 -0
  62. package/dist/cjs/Path.js +46 -0
  63. package/dist/cjs/Path.js.map +7 -0
  64. package/dist/cjs/ProcessConfinement.d.ts +55 -0
  65. package/dist/cjs/ProcessConfinement.d.ts.map +1 -0
  66. package/dist/cjs/ProcessConfinement.js +94 -0
  67. package/dist/cjs/ProcessConfinement.js.map +7 -0
  68. package/dist/cjs/ProcessLedger.d.ts +260 -0
  69. package/dist/cjs/ProcessLedger.d.ts.map +1 -0
  70. package/dist/cjs/ProcessLedger.js +190 -0
  71. package/dist/cjs/ProcessLedger.js.map +7 -0
  72. package/dist/cjs/Rooted.d.ts +53 -0
  73. package/dist/cjs/Rooted.d.ts.map +1 -0
  74. package/dist/cjs/Rooted.js +101 -0
  75. package/dist/cjs/Rooted.js.map +7 -0
  76. package/dist/cjs/Unreachable.d.ts +27 -0
  77. package/dist/cjs/Unreachable.d.ts.map +1 -0
  78. package/dist/cjs/Unreachable.js +43 -0
  79. package/dist/cjs/Unreachable.js.map +7 -0
  80. package/dist/cjs/Workspace.d.ts +62 -0
  81. package/dist/cjs/Workspace.d.ts.map +1 -0
  82. package/dist/cjs/Workspace.js +35 -0
  83. package/dist/cjs/Workspace.js.map +7 -0
  84. package/dist/cjs/index.d.ts +161 -0
  85. package/dist/cjs/index.d.ts.map +1 -0
  86. package/dist/cjs/index.js +73 -0
  87. package/dist/cjs/index.js.map +7 -0
  88. package/dist/cjs/internal/Containment.d.ts +24 -0
  89. package/dist/cjs/internal/Containment.d.ts.map +1 -0
  90. package/dist/cjs/internal/Containment.js +30 -0
  91. package/dist/cjs/internal/Containment.js.map +7 -0
  92. package/dist/cjs/internal/Pipeline.d.ts +18 -0
  93. package/dist/cjs/internal/Pipeline.d.ts.map +1 -0
  94. package/dist/cjs/internal/Pipeline.js +90 -0
  95. package/dist/cjs/internal/Pipeline.js.map +7 -0
  96. package/dist/cjs/internal/makeCapability.d.ts +16 -0
  97. package/dist/cjs/internal/makeCapability.d.ts.map +1 -0
  98. package/dist/cjs/internal/makeCapability.js +44 -0
  99. package/dist/cjs/internal/makeCapability.js.map +7 -0
  100. package/dist/cjs/package.json +1 -0
  101. package/dist/cjs/test/FileSystemBatchContract.d.ts +15 -0
  102. package/dist/cjs/test/FileSystemBatchContract.d.ts.map +1 -0
  103. package/dist/cjs/test/FileSystemBatchContract.js +76 -0
  104. package/dist/cjs/test/FileSystemBatchContract.js.map +7 -0
  105. package/dist/cjs/test/HostContract.d.ts +225 -0
  106. package/dist/cjs/test/HostContract.d.ts.map +1 -0
  107. package/dist/cjs/test/HostContract.js +489 -0
  108. package/dist/cjs/test/HostContract.js.map +7 -0
  109. package/dist/cjs/test/TestGrantStore.d.ts +31 -0
  110. package/dist/cjs/test/TestGrantStore.d.ts.map +1 -0
  111. package/dist/cjs/test/TestGrantStore.js +65 -0
  112. package/dist/cjs/test/TestGrantStore.js.map +7 -0
  113. package/dist/esm/CapabilitySet.d.ts +6 -0
  114. package/dist/esm/CapabilitySet.d.ts.map +1 -0
  115. package/dist/esm/CapabilitySet.js +6 -0
  116. package/dist/esm/CapabilitySet.js.map +1 -0
  117. package/dist/esm/ChildProcessEnvironment.d.ts +60 -0
  118. package/dist/esm/ChildProcessEnvironment.d.ts.map +1 -0
  119. package/dist/esm/ChildProcessEnvironment.js +91 -0
  120. package/dist/esm/ChildProcessEnvironment.js.map +1 -0
  121. package/dist/esm/ChildProcessSpawner.d.ts +100 -0
  122. package/dist/esm/ChildProcessSpawner.d.ts.map +1 -0
  123. package/dist/esm/ChildProcessSpawner.js +342 -0
  124. package/dist/esm/ChildProcessSpawner.js.map +1 -0
  125. package/dist/esm/CommandLine.d.ts +173 -0
  126. package/dist/esm/CommandLine.d.ts.map +1 -0
  127. package/dist/esm/CommandLine.js +195 -0
  128. package/dist/esm/CommandLine.js.map +1 -0
  129. package/dist/esm/ContainedSpawner.d.ts +135 -0
  130. package/dist/esm/ContainedSpawner.d.ts.map +1 -0
  131. package/dist/esm/ContainedSpawner.js +180 -0
  132. package/dist/esm/ContainedSpawner.js.map +1 -0
  133. package/dist/esm/FileSystem.d.ts +452 -0
  134. package/dist/esm/FileSystem.d.ts.map +1 -0
  135. package/dist/esm/FileSystem.js +989 -0
  136. package/dist/esm/FileSystem.js.map +1 -0
  137. package/dist/esm/FileSystemBatch.d.ts +94 -0
  138. package/dist/esm/FileSystemBatch.d.ts.map +1 -0
  139. package/dist/esm/FileSystemBatch.js +26 -0
  140. package/dist/esm/FileSystemBatch.js.map +1 -0
  141. package/dist/esm/GrantEvent.d.ts +240 -0
  142. package/dist/esm/GrantEvent.d.ts.map +1 -0
  143. package/dist/esm/GrantEvent.js +155 -0
  144. package/dist/esm/GrantEvent.js.map +1 -0
  145. package/dist/esm/GrantStore.d.ts +270 -0
  146. package/dist/esm/GrantStore.d.ts.map +1 -0
  147. package/dist/esm/GrantStore.js +951 -0
  148. package/dist/esm/GrantStore.js.map +1 -0
  149. package/dist/esm/HostError.d.ts +36 -0
  150. package/dist/esm/HostError.d.ts.map +1 -0
  151. package/dist/esm/HostError.js +20 -0
  152. package/dist/esm/HostError.js.map +1 -0
  153. package/dist/esm/HostServices.d.ts +101 -0
  154. package/dist/esm/HostServices.d.ts.map +1 -0
  155. package/dist/esm/HostServices.js +76 -0
  156. package/dist/esm/HostServices.js.map +1 -0
  157. package/dist/esm/HttpClient.d.ts +177 -0
  158. package/dist/esm/HttpClient.d.ts.map +1 -0
  159. package/dist/esm/HttpClient.js +299 -0
  160. package/dist/esm/HttpClient.js.map +1 -0
  161. package/dist/esm/Jj.d.ts +67 -0
  162. package/dist/esm/Jj.d.ts.map +1 -0
  163. package/dist/esm/Jj.js +111 -0
  164. package/dist/esm/Jj.js.map +1 -0
  165. package/dist/esm/JournalGrantStore.d.ts +65 -0
  166. package/dist/esm/JournalGrantStore.d.ts.map +1 -0
  167. package/dist/esm/JournalGrantStore.js +350 -0
  168. package/dist/esm/JournalGrantStore.js.map +1 -0
  169. package/dist/esm/Path.d.ts +37 -0
  170. package/dist/esm/Path.d.ts.map +1 -0
  171. package/dist/esm/Path.js +34 -0
  172. package/dist/esm/Path.js.map +1 -0
  173. package/dist/esm/ProcessConfinement.d.ts +55 -0
  174. package/dist/esm/ProcessConfinement.d.ts.map +1 -0
  175. package/dist/esm/ProcessConfinement.js +106 -0
  176. package/dist/esm/ProcessConfinement.js.map +1 -0
  177. package/dist/esm/ProcessLedger.d.ts +260 -0
  178. package/dist/esm/ProcessLedger.d.ts.map +1 -0
  179. package/dist/esm/ProcessLedger.js +283 -0
  180. package/dist/esm/ProcessLedger.js.map +1 -0
  181. package/dist/esm/Rooted.d.ts +53 -0
  182. package/dist/esm/Rooted.d.ts.map +1 -0
  183. package/dist/esm/Rooted.js +104 -0
  184. package/dist/esm/Rooted.js.map +1 -0
  185. package/dist/esm/Unreachable.d.ts +27 -0
  186. package/dist/esm/Unreachable.d.ts.map +1 -0
  187. package/dist/esm/Unreachable.js +26 -0
  188. package/dist/esm/Unreachable.js.map +1 -0
  189. package/dist/esm/Workspace.d.ts +62 -0
  190. package/dist/esm/Workspace.d.ts.map +1 -0
  191. package/dist/esm/Workspace.js +51 -0
  192. package/dist/esm/Workspace.js.map +1 -0
  193. package/dist/esm/index.d.ts +161 -0
  194. package/dist/esm/index.d.ts.map +1 -0
  195. package/dist/esm/index.js +151 -0
  196. package/dist/esm/index.js.map +1 -0
  197. package/dist/esm/internal/Containment.d.ts +24 -0
  198. package/dist/esm/internal/Containment.d.ts.map +1 -0
  199. package/dist/esm/internal/Containment.js +27 -0
  200. package/dist/esm/internal/Containment.js.map +1 -0
  201. package/dist/esm/internal/Pipeline.d.ts +18 -0
  202. package/dist/esm/internal/Pipeline.d.ts.map +1 -0
  203. package/dist/esm/internal/Pipeline.js +68 -0
  204. package/dist/esm/internal/Pipeline.js.map +1 -0
  205. package/dist/esm/internal/makeCapability.d.ts +16 -0
  206. package/dist/esm/internal/makeCapability.d.ts.map +1 -0
  207. package/dist/esm/internal/makeCapability.js +22 -0
  208. package/dist/esm/internal/makeCapability.js.map +1 -0
  209. package/dist/esm/test/FileSystemBatchContract.d.ts +15 -0
  210. package/dist/esm/test/FileSystemBatchContract.d.ts.map +1 -0
  211. package/dist/esm/test/FileSystemBatchContract.js +57 -0
  212. package/dist/esm/test/FileSystemBatchContract.js.map +1 -0
  213. package/dist/esm/test/HostContract.d.ts +225 -0
  214. package/dist/esm/test/HostContract.d.ts.map +1 -0
  215. package/dist/esm/test/HostContract.js +460 -0
  216. package/dist/esm/test/HostContract.js.map +1 -0
  217. package/dist/esm/test/TestGrantStore.d.ts +31 -0
  218. package/dist/esm/test/TestGrantStore.d.ts.map +1 -0
  219. package/dist/esm/test/TestGrantStore.js +57 -0
  220. package/dist/esm/test/TestGrantStore.js.map +1 -0
  221. package/docs/README.md +162 -0
  222. package/docs/api.md +1532 -0
  223. package/docs/concepts/decoration-in-place.md +117 -0
  224. package/docs/concepts/filesystem-batches.md +58 -0
  225. package/docs/concepts/filesystem-confinement.md +168 -0
  226. package/docs/concepts/grant-decisions.md +181 -0
  227. package/docs/concepts/process-containment.md +155 -0
  228. package/docs/contract.md +14 -0
  229. package/docs/guides/adapt-a-new-host-platform.md +148 -0
  230. package/docs/guides/answer-permission-requests.md +151 -0
  231. package/docs/guides/authorize-network-and-model-calls.md +118 -0
  232. package/docs/guides/contain-spawned-processes.md +118 -0
  233. package/docs/guides/guard-a-host-bundle.md +109 -0
  234. package/docs/guides/persist-grants-across-restarts.md +135 -0
  235. package/docs/guides/write-a-capability-policy.md +159 -0
  236. package/docs/installation.md +115 -0
  237. package/docs/quickstart.md +182 -0
  238. package/docs/testing.md +108 -0
  239. package/docs/troubleshooting.md +281 -0
  240. package/package.json +344 -3
  241. package/src/CapabilitySet.ts +6 -0
  242. package/src/ChildProcessEnvironment.ts +98 -0
  243. package/src/ChildProcessSpawner.ts +414 -0
  244. package/src/CommandLine.ts +250 -0
  245. package/src/ContainedSpawner.ts +284 -0
  246. package/src/FileSystem.ts +1655 -0
  247. package/src/FileSystemBatch.ts +90 -0
  248. package/src/GrantEvent.ts +174 -0
  249. package/src/GrantStore.ts +1345 -0
  250. package/src/HostError.ts +37 -0
  251. package/src/HostServices.ts +133 -0
  252. package/src/HttpClient.ts +406 -0
  253. package/src/Jj.ts +159 -0
  254. package/src/JournalGrantStore.ts +461 -0
  255. package/src/Path.ts +47 -0
  256. package/src/ProcessConfinement.ts +156 -0
  257. package/src/ProcessLedger.ts +423 -0
  258. package/src/Rooted.ts +128 -0
  259. package/src/Unreachable.ts +30 -0
  260. package/src/Workspace.ts +65 -0
  261. package/src/index.ts +181 -0
  262. package/src/internal/Containment.ts +37 -0
  263. package/src/internal/Pipeline.ts +85 -0
  264. package/src/internal/makeCapability.ts +26 -0
  265. package/src/test/FileSystemBatchContract.ts +59 -0
  266. package/src/test/HostContract.ts +785 -0
  267. package/src/test/TestGrantStore.ts +73 -0
@@ -0,0 +1,785 @@
1
+ /**
2
+ * Shared contract suite for complete Host bundles.
3
+ *
4
+ * This module is emitted as ESM, CJS, and declarations and exported from
5
+ * `@smthrs/kernel/test/contract`. It intentionally has a Vitest peer because
6
+ * it registers a reusable behavioral contract for third-party Host bundles.
7
+ *
8
+ * @since 1.0.0-rc.0
9
+ */
10
+
11
+ // Every case here runs on real elapsed time — subprocess spawns, file locks,
12
+ // mtimes, and poll loops — so the suite uses `it.live`; `it.effect`'s
13
+ // TestClock never advances for them.
14
+
15
+ import { describe, expect, it } from "@effect/vitest"
16
+ import * as JjService from "@smthrs/jj"
17
+ import type { Jj, JjErrorCode } from "@smthrs/jj"
18
+ import { Deferred, Effect, Fiber, FileSystem, type Layer, Option, Path, Stream } from "effect"
19
+ import { HttpClient } from "effect/unstable/http/HttpClient"
20
+ import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"
21
+ import type * as HttpClientResponse from "effect/unstable/http/HttpClientResponse"
22
+ import * as ChildProcess from "effect/unstable/process/ChildProcess"
23
+ import { type ChildProcessHandle, ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
24
+ import { tmpdir } from "node:os"
25
+ import { join } from "node:path"
26
+ import * as FileSystemBatchContract from "./FileSystemBatchContract.ts"
27
+
28
+ export { check as checkFileSystemBatch } from "./FileSystemBatchContract.ts"
29
+
30
+ /**
31
+ * A capability that must fail with a stable typed code.
32
+ *
33
+ * @category models
34
+ * @since 1.0.0-rc.0
35
+ */
36
+ export interface FailureCapability<Code extends string> {
37
+ readonly expected: "failure"
38
+ readonly code: Code
39
+ }
40
+
41
+ /**
42
+ * Successful filesystem contract options.
43
+ *
44
+ * `scratchPath` is required only for hosts whose filesystem does not accept an
45
+ * OS temp path — an in-memory double, say. Omitting it takes
46
+ * {@link defaultScratchPath}, which is process-scoped and outside the working
47
+ * tree.
48
+ *
49
+ * @category models
50
+ * @since 1.0.0-rc.0
51
+ */
52
+ export interface FileSystemSuccess {
53
+ readonly expected: "success"
54
+ readonly scratchPath?: string | undefined
55
+ /** Operations this otherwise available host deliberately refuses. */
56
+ readonly unsupported?: Partial<Record<FileSystemOperation, string>> | undefined
57
+ }
58
+
59
+ /**
60
+ * Every method on Effect's closed filesystem service.
61
+ * @category constants
62
+ * @since 1.0.0-rc.0
63
+ */
64
+ export const FileSystemOperations = [
65
+ "access",
66
+ "copy",
67
+ "copyFile",
68
+ "chmod",
69
+ "chown",
70
+ "glob",
71
+ "exists",
72
+ "link",
73
+ "makeDirectory",
74
+ "makeTempDirectory",
75
+ "makeTempDirectoryScoped",
76
+ "makeTempFile",
77
+ "makeTempFileScoped",
78
+ "open",
79
+ "readDirectory",
80
+ "readFile",
81
+ "readFileString",
82
+ "readLink",
83
+ "realPath",
84
+ "remove",
85
+ "rename",
86
+ "sink",
87
+ "stat",
88
+ "stream",
89
+ "symlink",
90
+ "truncate",
91
+ "utimes",
92
+ "watch",
93
+ "writeFile",
94
+ "writeFileString"
95
+ ] as const
96
+
97
+ /**
98
+ * One method of Effect's closed filesystem service.
99
+ * @category models
100
+ * @since 1.0.0-rc.0
101
+ */
102
+ export type FileSystemOperation = typeof FileSystemOperations[number]
103
+
104
+ /**
105
+ * Successful Path contract expectation.
106
+ *
107
+ * @category models
108
+ * @since 1.0.0-rc.0
109
+ */
110
+ export interface PathSuccess {
111
+ readonly expected: "success"
112
+ }
113
+
114
+ /**
115
+ * Successful `ChildProcessSpawner` contract options.
116
+ *
117
+ * Defaults use the current Node or Bun executable. In-process browser
118
+ * doubles can provide their own scripted commands and expected output.
119
+ *
120
+ * There is no timeout case: a wall-clock budget is `Effect.timeout` around any
121
+ * effect, not a per-spawn option, so there is nothing host-specific left to
122
+ * assert.
123
+ *
124
+ * @category models
125
+ * @since 1.0.0-rc.0
126
+ */
127
+ export interface ChildProcessSuccess {
128
+ readonly expected: "success"
129
+ readonly execCommand?: ChildProcess.Command | undefined
130
+ readonly expectedStdout?: string | undefined
131
+ readonly streamCommand?: ChildProcess.Command | undefined
132
+ readonly expectedStreamText?: string | undefined
133
+ /** A command carrying `cwd` and `env` options through to the child. */
134
+ readonly optionsCommand?: ChildProcess.Command | undefined
135
+ readonly expectedOptionsStdout?: string | undefined
136
+ /**
137
+ * How the host handles a command fed from a `stdin` stream: the expected
138
+ * stdout when it supports one, or the typed code when it does not.
139
+ */
140
+ readonly stdin?:
141
+ | { readonly command: ChildProcess.Command; readonly expectedStdout: string }
142
+ | FailureCapability<string>
143
+ | undefined
144
+ /** A two-leg pipeline, or the exact typed refusal for hosts without one. */
145
+ readonly pipeline?:
146
+ | { readonly command: ChildProcess.Command; readonly expectedStdout: string }
147
+ | FailureCapability<string>
148
+ | undefined
149
+ readonly interruptCommand?: ChildProcess.Command | undefined
150
+ }
151
+
152
+ /**
153
+ * Successful Jj contract expectation.
154
+ *
155
+ * @category models
156
+ * @since 1.0.0-rc.0
157
+ */
158
+ export interface JjSuccess {
159
+ readonly expected: "success"
160
+ /** Makes the first and second snapshots observably different. */
161
+ readonly prepareChange?: ((phase: "first" | "second") => Effect.Effect<void, unknown>) | undefined
162
+ /** Nonexistent path at which the contract may add one temporary workspace. */
163
+ readonly workspacePath?: string | undefined
164
+ /** Path whose repository root the host must resolve. */
165
+ readonly rootFrom?: string | undefined
166
+ /** Optional methods this otherwise available backend explicitly refuses. */
167
+ readonly unsupported?: Partial<Record<"root" | "revert", JjErrorCode>> | undefined
168
+ }
169
+
170
+ /**
171
+ * Every method on the closed Jj host service.
172
+ * @category constants
173
+ * @since 1.0.0-rc.0
174
+ */
175
+ export const JjOperations = [
176
+ "snapshot",
177
+ "restore",
178
+ "diff",
179
+ "workspaceAdd",
180
+ "workspaceForget",
181
+ "status",
182
+ "root",
183
+ "revert"
184
+ ] as const
185
+
186
+ /**
187
+ * One method of the closed Jj host service.
188
+ * @category models
189
+ * @since 1.0.0-rc.0
190
+ */
191
+ export type JjOperation = typeof JjOperations[number]
192
+
193
+ /**
194
+ * Successful HTTP contract probe.
195
+ *
196
+ * A request is explicit so the shared suite never invents a live network call.
197
+ *
198
+ * @category models
199
+ * @since 1.0.0-rc.0
200
+ */
201
+ export interface HttpClientProbe {
202
+ /** Exact request the host must execute. */
203
+ readonly request: HttpClientRequest.HttpClientRequest
204
+ /** Adapter-specific response proof. */
205
+ readonly assertResponse: (response: HttpClientResponse.HttpClientResponse) => void
206
+ }
207
+
208
+ /**
209
+ * Successful read/write/redirect HTTP contract probes.
210
+ * @category models
211
+ * @since 1.0.0-rc.0
212
+ */
213
+ export interface HttpClientSuccess {
214
+ readonly expected: "success"
215
+ /** Safe read method. */
216
+ readonly read: HttpClientProbe
217
+ /** Body-carrying write method. */
218
+ readonly write: HttpClientProbe
219
+ /** Manual redirect response; the host must not follow it below the guard. */
220
+ readonly redirect: HttpClientProbe
221
+ }
222
+
223
+ /**
224
+ * Complete capability expectations for the closed five-tag Host surface.
225
+ * Unsupported capabilities are asserted with their code and are never skipped.
226
+ *
227
+ * @category models
228
+ * @since 1.0.0-rc.0
229
+ */
230
+ export interface HostContractCapabilities {
231
+ readonly fileSystem: FileSystemSuccess | FailureCapability<string>
232
+ readonly path: PathSuccess | FailureCapability<string>
233
+ readonly childProcess: ChildProcessSuccess | FailureCapability<string>
234
+ readonly jj: JjSuccess | FailureCapability<JjErrorCode>
235
+ readonly httpClient: HttpClientSuccess | FailureCapability<string>
236
+ }
237
+
238
+ /**
239
+ * The full layer output required by the contract.
240
+ *
241
+ * @category models
242
+ * @since 1.0.0-rc.0
243
+ */
244
+ export type HostContractLayer = Layer.Layer<
245
+ FileSystem.FileSystem | Path.Path | ChildProcessSpawner | Jj | HttpClient,
246
+ unknown
247
+ >
248
+
249
+ /**
250
+ * Normalizes the code a Host failure is identified by, across the three shapes
251
+ * the closed surface produces: a `code` field (`JjError`), a nested
252
+ * `reason._tag` (`PlatformError`, `HttpClientError`), and a bare `_tag`.
253
+ * Anything else is uncoded.
254
+ *
255
+ * @category testing
256
+ * @since 1.0.0-rc.0
257
+ */
258
+ export const errorCode = (error: unknown): string | undefined => {
259
+ if (typeof error !== "object" || error === null) return undefined
260
+ if ("code" in error && typeof error.code === "string") return error.code
261
+ if (
262
+ "reason" in error &&
263
+ typeof error.reason === "object" &&
264
+ error.reason !== null &&
265
+ "_tag" in error.reason &&
266
+ typeof error.reason._tag === "string"
267
+ ) {
268
+ return error.reason._tag
269
+ }
270
+ if ("_tag" in error && typeof error._tag === "string") return error._tag
271
+ return undefined
272
+ }
273
+
274
+ /**
275
+ * Asserts that `effect` fails with `code`. Succeeding is itself a contract
276
+ * violation: a capability declared unsupported must never quietly work.
277
+ *
278
+ * @category testing
279
+ * @since 1.0.0-rc.0
280
+ */
281
+ export const assertFailure = <A, E, R>(
282
+ effect: Effect.Effect<A, E, R>,
283
+ code: string
284
+ ): Effect.Effect<void, never, R> =>
285
+ effect.pipe(
286
+ Effect.match({
287
+ onFailure: (error) => {
288
+ expect(errorCode(error)).toBe(code)
289
+ },
290
+ onSuccess: () => {
291
+ throw new Error(`expected typed failure ${code}`)
292
+ }
293
+ })
294
+ )
295
+
296
+ let scratchSeq = 0
297
+
298
+ /**
299
+ * Allocates the scratch file path a contract bundle writes through when the
300
+ * caller declares no `scratchPath` of its own.
301
+ *
302
+ * The path is absolute under the OS temp directory and scoped by both process
303
+ * id and an allocation counter. A repo-relative default would put the file in
304
+ * the working tree, where it is neither ignored by git nor private to one test
305
+ * process: concurrent `vitest run` invocations would then race the same path,
306
+ * and the suite's `ensuring` remove could truncate a sibling's file between its
307
+ * write and its read.
308
+ *
309
+ * @category testing
310
+ * @since 1.0.0-rc.0
311
+ */
312
+ export const defaultScratchPath = (suite: string): string => {
313
+ const slug = suite.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "host"
314
+ // Ambient `process.pid` on purpose: this is a Node-only test harness that
315
+ // must name a path unique to THIS operating-system process before any layer
316
+ // exists to ask. Injecting it would only move the same read one frame out.
317
+ return join(tmpdir(), `flows-host-contract-${process.pid}-${++scratchSeq}-${slug}`)
318
+ }
319
+
320
+ /** Everything a {@link HostContractLayer} provides; a case may use nothing else. */
321
+ type HostServices = Layer.Success<HostContractLayer>
322
+
323
+ const provide = <A, E>(
324
+ effect: Effect.Effect<A, E, HostServices>,
325
+ layer: HostContractLayer
326
+ ): Effect.Effect<A, E | unknown> => effect.pipe(Effect.provide(layer))
327
+
328
+ const run = <A, E>(effect: Effect.Effect<A, E, HostServices>, layer: HostContractLayer) => provide(effect, layer)
329
+
330
+ const unsupported = ChildProcess.make("host-contract-unsupported")
331
+
332
+ const unsupportedChildProcess = (operation: "string" | "stream", code: string) =>
333
+ Effect.gen(function*() {
334
+ const spawner = yield* ChildProcessSpawner
335
+ yield* operation === "stream"
336
+ ? assertFailure(Stream.runDrain(spawner.streamString(unsupported)), code)
337
+ : assertFailure(spawner.string(unsupported), code)
338
+ })
339
+
340
+ const fileSystemProbe = (
341
+ fs: FileSystem.FileSystem,
342
+ operation: FileSystemOperation,
343
+ root: string
344
+ ): Effect.Effect<unknown, unknown> => {
345
+ const at = (name: string): string => `${root}/${operation}-${name}`
346
+ const source = `${root}/source.txt`
347
+ const bytes = new TextEncoder().encode("host-contract")
348
+ switch (operation) {
349
+ case "access":
350
+ return fs.access(source)
351
+ case "copy":
352
+ return fs.copy(source, at("copy.txt"))
353
+ case "copyFile":
354
+ return fs.copyFile(source, at("copy-file.txt"))
355
+ case "chmod":
356
+ return fs.chmod(source, 0o600)
357
+ case "chown":
358
+ return Effect.flatMap(fs.stat(source), (info) =>
359
+ fs.chown(
360
+ source,
361
+ Option.getOrElse(info.uid, () => 0),
362
+ Option.getOrElse(info.gid, () => 0)
363
+ ))
364
+ case "glob":
365
+ return fs.glob("**/*.txt", { root })
366
+ case "exists":
367
+ return Effect.tap(fs.exists(source), (exists) => Effect.sync(() => expect(exists).toBe(true)))
368
+ case "link":
369
+ return fs.link(source, at("hard-link.txt"))
370
+ case "makeDirectory":
371
+ return fs.makeDirectory(at("directory"), { recursive: true })
372
+ case "makeTempDirectory":
373
+ return fs.makeTempDirectory({ directory: root })
374
+ case "makeTempDirectoryScoped":
375
+ return Effect.scoped(fs.makeTempDirectoryScoped({ directory: root }))
376
+ case "makeTempFile":
377
+ return fs.makeTempFile({ directory: root })
378
+ case "makeTempFileScoped":
379
+ return Effect.scoped(fs.makeTempFileScoped({ directory: root }))
380
+ case "open":
381
+ return Effect.scoped(fs.open(source, { flag: "r" }))
382
+ case "readDirectory":
383
+ return Effect.tap(fs.readDirectory(root), (entries) =>
384
+ Effect.sync(() => {
385
+ expect(Array.isArray(entries)).toBe(true)
386
+ expect(entries.every((entry) => typeof entry === "string")).toBe(true)
387
+ }))
388
+ case "readFile":
389
+ return Effect.gen(function*() {
390
+ const first = yield* fs.readFile(source)
391
+ expect(first).toBeInstanceOf(Uint8Array)
392
+ const original = first[0]
393
+ expect(original).toBeDefined()
394
+ first[0] = original! ^ 0xff
395
+ const second = yield* fs.readFile(source)
396
+ expect(second[0]).toBe(original)
397
+ })
398
+ case "readFileString":
399
+ return fs.readFileString(source)
400
+ case "readLink": {
401
+ const link = at("symbolic-link.txt")
402
+ return fs.symlink(source, link).pipe(Effect.andThen(fs.readLink(link)))
403
+ }
404
+ case "realPath":
405
+ return fs.realPath(source)
406
+ case "remove": {
407
+ const path = at("remove.txt")
408
+ return fs.writeFile(path, bytes).pipe(Effect.andThen(fs.remove(path)))
409
+ }
410
+ case "rename": {
411
+ const from = at("rename-from.txt")
412
+ return fs.writeFile(from, bytes).pipe(Effect.andThen(fs.rename(from, at("rename-to.txt"))))
413
+ }
414
+ case "sink":
415
+ return Stream.run(Stream.succeed(bytes), fs.sink(at("sink.txt")))
416
+ case "stat":
417
+ return Effect.tap(fs.stat(source), (info) =>
418
+ Effect.sync(() => {
419
+ expect(typeof info.size).toBe("bigint")
420
+ expect(typeof info.type).toBe("string")
421
+ }))
422
+ case "stream":
423
+ return Stream.runForEach(
424
+ fs.stream(source),
425
+ (chunk) => Effect.sync(() => expect(chunk).toBeInstanceOf(Uint8Array))
426
+ )
427
+ case "symlink":
428
+ return fs.symlink(source, at("symlink.txt"))
429
+ case "truncate": {
430
+ const path = at("truncate.txt")
431
+ return fs.writeFile(path, bytes).pipe(Effect.andThen(fs.truncate(path, 1)))
432
+ }
433
+ case "utimes":
434
+ return fs.utimes(source, new Date(0), new Date(0))
435
+ case "watch":
436
+ return Effect.gen(function*() {
437
+ const watched = yield* Stream.runHead(fs.watch(root)).pipe(
438
+ Effect.timeout("5 seconds"),
439
+ Effect.forkChild({ startImmediately: true })
440
+ )
441
+ // Keep producing events until the watcher observes one: subscribing
442
+ // can take longer than any fixed initial delay. The foreign Promise
443
+ // also works when the supplied host provides a frozen TestClock.
444
+ yield* Effect.raceFirst(
445
+ Fiber.join(watched),
446
+ fs.writeFile(at("watched.txt"), bytes).pipe(
447
+ Effect.andThen(Effect.promise(() => new Promise((resolve) => setTimeout(resolve, 10)))),
448
+ Effect.forever
449
+ )
450
+ )
451
+ })
452
+ case "writeFile":
453
+ return Effect.gen(function*() {
454
+ const input = bytes.slice()
455
+ const expected = input[0]
456
+ expect(expected).toBeDefined()
457
+ yield* fs.writeFile(at("write.txt"), input)
458
+ input[0] = expected! ^ 0xff
459
+ expect((yield* fs.readFile(at("write.txt")))[0]).toBe(expected)
460
+ })
461
+ case "writeFileString":
462
+ return fs.writeFileString(at("write-string.txt"), "host-contract")
463
+ }
464
+ }
465
+
466
+ /** The default stdin probe: echo back one line read from the child's stdin. */
467
+ const defaultStdinCommand = ChildProcess.make(
468
+ process.execPath,
469
+ [
470
+ "-e",
471
+ `let input = ""; process.stdin.setEncoding("utf8"); process.stdin.on("data", chunk => input += chunk); process.stdin.on("end", () => process.stdout.write(input.replace(/\\r?\\n$/, "")));`
472
+ ],
473
+ { stdin: Stream.fromArray([new TextEncoder().encode("stdin\n")]) }
474
+ )
475
+
476
+ /** The default two-leg process probe. */
477
+ const defaultPipelineCommand = ChildProcess.pipeTo(
478
+ ChildProcess.make(process.execPath, ["-e", `process.stdout.write("host-contract-pipeline")`]),
479
+ ChildProcess.make(process.execPath, ["-e", "process.stdin.pipe(process.stdout)"])
480
+ )
481
+
482
+ /** The default multi-leg cancellation probe. */
483
+ const defaultInterruptCommand = ChildProcess.pipeTo(
484
+ ChildProcess.make(process.execPath, ["-e", "setTimeout(() => {}, 10_000)"]),
485
+ ChildProcess.make(process.execPath, ["-e", "process.stdin.pipe(process.stdout)"])
486
+ )
487
+
488
+ /**
489
+ * Registers the shared Host contract with Vitest.
490
+ *
491
+ * Every invocation creates ten cases: complete service presence,
492
+ * FileSystem, Path, five child-process lifecycle cases, Jj, and
493
+ * HttpClient.
494
+ *
495
+ * @category testing
496
+ * @since 1.0.0-rc.0
497
+ */
498
+ export const runHostContract = (
499
+ name: string,
500
+ layer: HostContractLayer,
501
+ caps: HostContractCapabilities
502
+ ): void => {
503
+ const fileSystemCap = caps.fileSystem
504
+ const pathCap = caps.path
505
+ const childProcessCap = caps.childProcess
506
+ const jjCap = caps.jj
507
+ const httpClientCap = caps.httpClient
508
+ const scratchPath = fileSystemCap.expected === "success"
509
+ ? fileSystemCap.scratchPath ?? defaultScratchPath(name)
510
+ : ""
511
+
512
+ describe(`${name} Host contract`, () => {
513
+ it.live("provides every tag in the closed Host service list", () =>
514
+ run(
515
+ Effect.gen(function*() {
516
+ yield* FileSystem.FileSystem
517
+ yield* Path.Path
518
+ yield* ChildProcessSpawner
519
+ yield* JjService.Jj
520
+ yield* HttpClient
521
+ }),
522
+ layer
523
+ ))
524
+
525
+ it.live("declares every FileSystem operation", () =>
526
+ run(
527
+ fileSystemCap.expected === "failure"
528
+ ? Effect.gen(function*() {
529
+ const fs = yield* FileSystem.FileSystem
530
+ yield* Effect.forEach(
531
+ FileSystemOperations,
532
+ (operation) =>
533
+ assertFailure(
534
+ fileSystemProbe(fs, operation, "/host-contract/unsupported"),
535
+ fileSystemCap.code
536
+ ),
537
+ { discard: true }
538
+ )
539
+ })
540
+ : Effect.gen(function*() {
541
+ const fs = yield* FileSystem.FileSystem
542
+ const root = scratchPath
543
+ const bytes = new TextEncoder().encode("host-contract")
544
+ yield* Effect.gen(function*() {
545
+ yield* fs.makeDirectory(root, { recursive: true })
546
+ yield* fs.writeFile(`${root}/source.txt`, bytes)
547
+ yield* FileSystemBatchContract.check(fs, root)
548
+ yield* Effect.forEach(
549
+ FileSystemOperations,
550
+ (operation) => {
551
+ const probe = fileSystemProbe(fs, operation, root)
552
+ const code = fileSystemCap.unsupported?.[operation]
553
+ return code === undefined ? probe : assertFailure(probe, code)
554
+ },
555
+ { discard: true }
556
+ )
557
+ }).pipe(
558
+ Effect.ensuring(fs.remove(root, { recursive: true, force: true }).pipe(Effect.ignore))
559
+ )
560
+ }),
561
+ layer
562
+ ))
563
+
564
+ it.live("declares Path behavior", () =>
565
+ run(
566
+ pathCap.expected === "failure"
567
+ ? Effect.gen(function*() {
568
+ const path = yield* Path.Path
569
+ yield* assertFailure(
570
+ Effect.try({
571
+ try: () => path.fromFileUrl(new URL("file:///host-contract/value")),
572
+ catch: (error) => error
573
+ }),
574
+ pathCap.code
575
+ )
576
+ })
577
+ : Effect.gen(function*() {
578
+ const path = yield* Path.Path
579
+ expect(path.normalize("/host-contract/./nested/../value")).toBe(`${path.sep}host-contract${path.sep}value`)
580
+ }),
581
+ layer
582
+ ))
583
+
584
+ it.live("declares buffered child-process behavior", () =>
585
+ run(
586
+ childProcessCap.expected === "failure"
587
+ ? unsupportedChildProcess("string", childProcessCap.code)
588
+ : Effect.gen(function*() {
589
+ const spawner = yield* ChildProcessSpawner
590
+ const command = childProcessCap.execCommand ??
591
+ ChildProcess.make(process.execPath, ["-e", `process.stdout.write("host-contract")`])
592
+ expect(yield* spawner.string(command)).toBe(childProcessCap.expectedStdout ?? "host-contract")
593
+ expect(yield* spawner.exitCode(command)).toBe(0)
594
+ }),
595
+ layer
596
+ ))
597
+
598
+ it.live("declares child-process streaming behavior", () =>
599
+ run(
600
+ childProcessCap.expected === "failure"
601
+ ? unsupportedChildProcess("stream", childProcessCap.code)
602
+ : Effect.gen(function*() {
603
+ const spawner = yield* ChildProcessSpawner
604
+ const output = yield* Stream.mkString(
605
+ spawner.streamString(
606
+ childProcessCap.streamCommand ??
607
+ ChildProcess.make(process.execPath, ["-e", `process.stdout.write("host-contract-stream")`])
608
+ )
609
+ )
610
+ expect(output).toContain(childProcessCap.expectedStreamText ?? "host-contract-stream")
611
+ }),
612
+ layer
613
+ ))
614
+
615
+ it.live("declares child-process cwd and env option behavior", () =>
616
+ run(
617
+ childProcessCap.expected === "failure"
618
+ ? unsupportedChildProcess("string", childProcessCap.code)
619
+ : Effect.gen(function*() {
620
+ const spawner = yield* ChildProcessSpawner
621
+ const output = yield* spawner.string(
622
+ childProcessCap.optionsCommand ??
623
+ ChildProcess.make(process.execPath, ["-e", "process.stdout.write(process.env.HOST_CONTRACT_ENV)"], {
624
+ cwd: tmpdir(),
625
+ env: { HOST_CONTRACT_ENV: "env" },
626
+ extendEnv: true
627
+ })
628
+ )
629
+ expect(output).toBe(childProcessCap.expectedOptionsStdout ?? "env")
630
+ }),
631
+ layer
632
+ ))
633
+
634
+ it.live("declares child-process stdin behavior", () =>
635
+ run(
636
+ Effect.gen(function*() {
637
+ const spawner = yield* ChildProcessSpawner
638
+ const stdinCap = childProcessCap.expected === "failure"
639
+ ? childProcessCap
640
+ : childProcessCap.stdin ?? { command: defaultStdinCommand, expectedStdout: "stdin" }
641
+ if ("expected" in stdinCap) {
642
+ yield* assertFailure(spawner.string(defaultStdinCommand), stdinCap.code)
643
+ return
644
+ }
645
+ expect(yield* spawner.string(stdinCap.command)).toBe(stdinCap.expectedStdout)
646
+ }),
647
+ layer
648
+ ))
649
+
650
+ it.live("declares child-process pipeline behavior", () =>
651
+ run(
652
+ Effect.gen(function*() {
653
+ const spawner = yield* ChildProcessSpawner
654
+ const pipelineCap = childProcessCap.expected === "failure"
655
+ ? childProcessCap
656
+ : childProcessCap.pipeline ?? {
657
+ command: defaultPipelineCommand,
658
+ expectedStdout: "host-contract-pipeline"
659
+ }
660
+ if ("expected" in pipelineCap) {
661
+ yield* assertFailure(spawner.string(defaultPipelineCommand), pipelineCap.code)
662
+ return
663
+ }
664
+ expect(yield* spawner.string(pipelineCap.command)).toBe(pipelineCap.expectedStdout)
665
+ }),
666
+ layer
667
+ ))
668
+
669
+ it.live("declares child-process interruption behavior", () =>
670
+ run(
671
+ childProcessCap.expected === "failure"
672
+ ? unsupportedChildProcess("stream", childProcessCap.code)
673
+ : Effect.gen(function*() {
674
+ const spawner = yield* ChildProcessSpawner
675
+ const ready = yield* Deferred.make<ChildProcessHandle>()
676
+ const fiber = yield* Effect.scoped(
677
+ Effect.gen(function*() {
678
+ const handle = yield* spawner.spawn(
679
+ childProcessCap.interruptCommand ?? defaultInterruptCommand
680
+ )
681
+ yield* Deferred.succeed(ready, handle)
682
+ yield* handle.exitCode
683
+ })
684
+ ).pipe(
685
+ Effect.forkChild({ startImmediately: true })
686
+ )
687
+ const handle = yield* Deferred.await(ready)
688
+ expect(yield* handle.isRunning).toBe(true)
689
+ yield* Fiber.interrupt(fiber)
690
+ expect(yield* handle.isRunning).toBe(false)
691
+ }),
692
+ layer
693
+ ))
694
+
695
+ it.live("declares every Jj operation", () =>
696
+ run(
697
+ jjCap.expected === "failure"
698
+ ? Effect.gen(function*() {
699
+ const jj = yield* JjService.Jj
700
+ const root = jj.root
701
+ const revert = jj.revert
702
+ expect(root).toBeTypeOf("function")
703
+ expect(revert).toBeTypeOf("function")
704
+ yield* Effect.forEach([
705
+ jj.snapshot("host contract"),
706
+ jj.restore("host-contract-change"),
707
+ jj.diff("host-contract-from", "host-contract-to"),
708
+ jj.workspaceAdd("host-contract", "/host-contract/workspace", "host-contract-change"),
709
+ jj.workspaceForget("host-contract"),
710
+ jj.status(),
711
+ root!("/host-contract"),
712
+ revert!("host-contract-change")
713
+ ], (probe) =>
714
+ assertFailure(probe, jjCap.code), { discard: true })
715
+ })
716
+ : Effect.gen(function*() {
717
+ const jj = yield* JjService.Jj
718
+ const root = jj.root
719
+ const revert = jj.revert
720
+ expect(root).toBeTypeOf("function")
721
+ expect(revert).toBeTypeOf("function")
722
+ yield* (jjCap.prepareChange?.("first") ?? Effect.void)
723
+ const first = yield* jj.snapshot("host contract first")
724
+ expect(first.changeId.length).toBeGreaterThan(0)
725
+ yield* (jjCap.prepareChange?.("second") ?? Effect.void)
726
+ const second = yield* jj.snapshot("host contract second")
727
+ expect(second.changeId.length).toBeGreaterThan(0)
728
+ expect(typeof (yield* jj.diff(first.changeId, second.changeId))).toBe("string")
729
+ const revertCode = jjCap.unsupported?.revert
730
+ if (revertCode === undefined) {
731
+ const reverted = yield* revert!(second.changeId)
732
+ expect(Array.isArray(reverted.reverted)).toBe(true)
733
+ expect(reverted.reverted.every((path) =>
734
+ typeof path === "string"
735
+ )).toBe(true)
736
+ } else {
737
+ yield* assertFailure(revert!(second.changeId), revertCode)
738
+ }
739
+ yield* jj.restore(first.changeId)
740
+ const workspaceName = `host-contract-${process.pid}-${++scratchSeq}`
741
+ yield* jj.workspaceAdd(
742
+ workspaceName,
743
+ jjCap.workspacePath ?? defaultScratchPath(`${name}-jj-workspace`),
744
+ first.changeId
745
+ )
746
+ yield* jj.workspaceForget(workspaceName)
747
+ expect(typeof (yield* jj.status())).toBe("string")
748
+ const rootCode = jjCap.unsupported?.root
749
+ const rootFrom = jjCap.rootFrom ?? "."
750
+ if (rootCode === undefined) {
751
+ expect((yield* root!(rootFrom)).length).toBeGreaterThan(0)
752
+ } else {
753
+ yield* assertFailure(root!(rootFrom), rootCode)
754
+ }
755
+ }),
756
+ layer
757
+ ))
758
+
759
+ it.live("declares HTTP read, write, and redirect behavior", () =>
760
+ run(
761
+ httpClientCap.expected === "failure"
762
+ ? Effect.gen(function*() {
763
+ const client = yield* HttpClient
764
+ yield* Effect.forEach([
765
+ HttpClientRequest.get("http://127.0.0.1:1/host-contract/read"),
766
+ HttpClientRequest.post("http://127.0.0.1:1/host-contract/write").pipe(
767
+ HttpClientRequest.bodyText("host-contract")
768
+ ),
769
+ HttpClientRequest.get("http://127.0.0.1:1/host-contract/redirect")
770
+ ], (request) =>
771
+ assertFailure(client.execute(request), httpClientCap.code), { discard: true })
772
+ })
773
+ : Effect.gen(function*() {
774
+ const client = yield* HttpClient
775
+ for (const probe of [httpClientCap.read, httpClientCap.write, httpClientCap.redirect]) {
776
+ const response = yield* client.execute(probe.request)
777
+ expect(typeof response.status).toBe("number")
778
+ expect(typeof response.headers).toBe("object")
779
+ probe.assertResponse(response)
780
+ }
781
+ }),
782
+ layer
783
+ ))
784
+ })
785
+ }