@homeflare/alchemy 0.19.1 → 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 (198) hide show
  1. package/README.md +25 -0
  2. package/dist/caddy/access-forward-auth.d.ts +56 -0
  3. package/dist/caddy/access-forward-auth.d.ts.map +1 -0
  4. package/dist/caddy/index.d.ts +6 -0
  5. package/dist/caddy/index.d.ts.map +1 -1
  6. package/dist/caddy/index.js +169 -75
  7. package/dist/caddy/index.js.map +10 -7
  8. package/dist/cloudflare/mcp-server-api.d.ts +135 -0
  9. package/dist/cloudflare/mcp-server-api.d.ts.map +1 -0
  10. package/dist/cloudflare/mcp-server-form.d.ts +116 -0
  11. package/dist/cloudflare/mcp-server-form.d.ts.map +1 -0
  12. package/dist/index.d.ts +1 -0
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/launchd/file-converge.d.ts +70 -0
  15. package/dist/launchd/file-converge.d.ts.map +1 -0
  16. package/dist/launchd/file-identity.d.ts +32 -0
  17. package/dist/launchd/file-identity.d.ts.map +1 -0
  18. package/dist/launchd/host-file-form.d.ts +2 -0
  19. package/dist/launchd/host-file-form.d.ts.map +1 -1
  20. package/dist/launchd/host-file-lifecycle.d.ts +11 -24
  21. package/dist/launchd/host-file-lifecycle.d.ts.map +1 -1
  22. package/dist/launchd/host-file.d.ts.map +1 -1
  23. package/dist/launchd/index.js +173 -120
  24. package/dist/launchd/index.js.map +12 -10
  25. package/dist/launchd/local-runner.d.ts.map +1 -1
  26. package/dist/launchd/runner.d.ts +17 -2
  27. package/dist/launchd/runner.d.ts.map +1 -1
  28. package/dist/launchd/sudo-runner.d.ts +11 -0
  29. package/dist/launchd/sudo-runner.d.ts.map +1 -1
  30. package/dist/linux/index.js +37 -11
  31. package/dist/linux/index.js.map +10 -10
  32. package/dist/linux/region.d.ts +11 -0
  33. package/dist/linux/region.d.ts.map +1 -1
  34. package/dist/linux/remote-file-lifecycle.d.ts.map +1 -1
  35. package/dist/linux/remote-file-plan.d.ts +22 -0
  36. package/dist/linux/remote-file-plan.d.ts.map +1 -1
  37. package/dist/linux/remote-file.d.ts +4 -1
  38. package/dist/linux/remote-file.d.ts.map +1 -1
  39. package/dist/linux/sudo-listing.d.ts +47 -0
  40. package/dist/linux/sudo-listing.d.ts.map +1 -0
  41. package/dist/linux/unit-handlers.d.ts.map +1 -1
  42. package/dist/linux/unit-preflight.d.ts +10 -1
  43. package/dist/linux/unit-preflight.d.ts.map +1 -1
  44. package/dist/proxmox/credential-literals.d.ts +1 -1
  45. package/dist/proxmox/credential-literals.d.ts.map +1 -1
  46. package/dist/proxmox/generated/pbs/nodes-node.d.ts +1 -1
  47. package/dist/proxmox/generated/pbs/nodes-node.d.ts.map +1 -1
  48. package/dist/proxmox/generated/pbs.d.ts +7 -0
  49. package/dist/proxmox/generated/pbs.d.ts.map +1 -1
  50. package/dist/proxmox/generated/pve.d.ts +7 -0
  51. package/dist/proxmox/generated/pve.d.ts.map +1 -1
  52. package/dist/proxmox/index.d.ts +16 -4
  53. package/dist/proxmox/index.d.ts.map +1 -1
  54. package/dist/proxmox/index.js +5 -2
  55. package/dist/proxmox/index.js.map +9 -9
  56. package/dist/proxmox/pbs-notification-target-form.d.ts +1 -1
  57. package/dist/proxmox/pbs-notification-target-form.d.ts.map +1 -1
  58. package/dist/proxmox/pbs-notification-target.d.ts +1 -1
  59. package/dist/proxmox/pbs-notification-target.d.ts.map +1 -1
  60. package/dist/release/archive.d.ts +11 -0
  61. package/dist/release/archive.d.ts.map +1 -0
  62. package/dist/release/binary-claim.d.ts +53 -0
  63. package/dist/release/binary-claim.d.ts.map +1 -0
  64. package/dist/release/binary-diff.d.ts +38 -0
  65. package/dist/release/binary-diff.d.ts.map +1 -0
  66. package/dist/release/binary-form.d.ts +118 -0
  67. package/dist/release/binary-form.d.ts.map +1 -0
  68. package/dist/release/binary-lifecycle.d.ts +55 -0
  69. package/dist/release/binary-lifecycle.d.ts.map +1 -0
  70. package/dist/release/binary-read.d.ts +11 -0
  71. package/dist/release/binary-read.d.ts.map +1 -0
  72. package/dist/release/binary.d.ts +68 -0
  73. package/dist/release/binary.d.ts.map +1 -0
  74. package/dist/release/catalog.d.ts +76 -0
  75. package/dist/release/catalog.d.ts.map +1 -0
  76. package/dist/release/checksums.d.ts +22 -0
  77. package/dist/release/checksums.d.ts.map +1 -0
  78. package/dist/release/declared-pins.d.ts +4 -0
  79. package/dist/release/declared-pins.d.ts.map +1 -0
  80. package/dist/release/download.d.ts +44 -0
  81. package/dist/release/download.d.ts.map +1 -0
  82. package/dist/release/fake-deploy.d.ts +48 -0
  83. package/dist/release/fake-deploy.d.ts.map +1 -0
  84. package/dist/release/fake-engine.d.ts +8 -0
  85. package/dist/release/fake-engine.d.ts.map +1 -0
  86. package/dist/release/fake-install.d.ts +39 -0
  87. package/dist/release/fake-install.d.ts.map +1 -0
  88. package/dist/release/fake-provider.d.ts +58 -0
  89. package/dist/release/fake-provider.d.ts.map +1 -0
  90. package/dist/release/fake-release.d.ts +95 -0
  91. package/dist/release/fake-release.d.ts.map +1 -0
  92. package/dist/release/index.d.ts +24 -0
  93. package/dist/release/index.d.ts.map +1 -0
  94. package/dist/release/index.js +1365 -0
  95. package/dist/release/index.js.map +35 -0
  96. package/dist/release/providers.d.ts +19 -0
  97. package/dist/release/providers.d.ts.map +1 -0
  98. package/dist/release/refused.d.ts +39 -0
  99. package/dist/release/refused.d.ts.map +1 -0
  100. package/dist/release/tar.d.ts +14 -0
  101. package/dist/release/tar.d.ts.map +1 -0
  102. package/dist/release/victoria.d.ts +36 -0
  103. package/dist/release/victoria.d.ts.map +1 -0
  104. package/dist/{proxmox → secrets}/write-only.d.ts.map +1 -1
  105. package/docs/adopted-deploys.md +13 -0
  106. package/docs/linux-host.md +5 -1
  107. package/docs/provider-standard.md +138 -0
  108. package/docs/release-binary-catalogs.md +113 -0
  109. package/docs/release-binary-measured.md +163 -0
  110. package/docs/release-binary-refusals.md +57 -0
  111. package/docs/release-binary-upstream.md +163 -0
  112. package/docs/release-binary.md +154 -0
  113. package/docs/upstream-conformance.md +164 -0
  114. package/package.json +5 -1
  115. package/src/caddy/access-forward-auth.test.ts +63 -0
  116. package/src/caddy/access-forward-auth.ts +120 -0
  117. package/src/caddy/index.ts +10 -0
  118. package/src/caddy/providers.test.ts +4 -0
  119. package/src/cloudflare/mcp-server-api.ts +294 -0
  120. package/src/cloudflare/mcp-server-form.ts +219 -0
  121. package/src/index.ts +1 -0
  122. package/src/launchd/file-converge.ts +233 -0
  123. package/src/launchd/file-identity.ts +69 -0
  124. package/src/launchd/host-file-alias.test.ts +65 -0
  125. package/src/launchd/host-file-form.ts +2 -1
  126. package/src/launchd/host-file-lifecycle.ts +25 -144
  127. package/src/launchd/host-file.ts +8 -2
  128. package/src/launchd/local-runner.test.ts +12 -1
  129. package/src/launchd/local-runner.ts +2 -0
  130. package/src/launchd/runner.ts +17 -2
  131. package/src/launchd/sudo-runner.ts +11 -0
  132. package/src/linux/region.ts +15 -0
  133. package/src/linux/remote-file-lifecycle.test.ts +0 -88
  134. package/src/linux/remote-file-lifecycle.ts +25 -7
  135. package/src/linux/remote-file-plan.ts +37 -1
  136. package/src/linux/remote-file-region.test.ts +181 -0
  137. package/src/linux/remote-file.ts +4 -1
  138. package/src/linux/sudo-listing.ts +122 -0
  139. package/src/linux/unit-handlers.ts +12 -5
  140. package/src/linux/unit-preflight.ts +20 -4
  141. package/src/linux/unit-rename.test.ts +76 -0
  142. package/src/proxmox/ceph-adopt.test.ts +210 -0
  143. package/src/proxmox/credential-literals.ts +1 -1
  144. package/src/proxmox/generated/pbs/nodes-node.ts +1 -1
  145. package/src/proxmox/generated/pbs.ts +7 -0
  146. package/src/proxmox/generated/pve.ts +7 -0
  147. package/src/proxmox/index.test.ts +7 -4
  148. package/src/proxmox/index.ts +16 -4
  149. package/src/proxmox/pbs-notification-target-form.ts +1 -1
  150. package/src/proxmox/pbs-notification-target-lifecycle.ts +1 -1
  151. package/src/proxmox/pbs-notification-target-wire.ts +1 -1
  152. package/src/proxmox/pbs-notification-target.ts +1 -1
  153. package/src/release/adopt-parity.test.ts +106 -0
  154. package/src/release/apply-pins.test.ts +128 -0
  155. package/src/release/archive.ts +94 -0
  156. package/src/release/binary-alias.test.ts +109 -0
  157. package/src/release/binary-claim.ts +80 -0
  158. package/src/release/binary-claims.test.ts +47 -0
  159. package/src/release/binary-diff.ts +73 -0
  160. package/src/release/binary-form.ts +229 -0
  161. package/src/release/binary-lifecycle.test.ts +90 -0
  162. package/src/release/binary-lifecycle.ts +202 -0
  163. package/src/release/binary-read.ts +55 -0
  164. package/src/release/binary-refusals.test.ts +192 -0
  165. package/src/release/binary-sudo.test.ts +93 -0
  166. package/src/release/binary.ts +181 -0
  167. package/src/release/catalog.test.ts +96 -0
  168. package/src/release/catalog.ts +175 -0
  169. package/src/release/checksums.test.ts +35 -0
  170. package/src/release/checksums.ts +43 -0
  171. package/src/release/declared-pins.ts +39 -0
  172. package/src/release/download.test.ts +138 -0
  173. package/src/release/download.ts +110 -0
  174. package/src/release/error-channel.test.ts +69 -0
  175. package/src/release/fake-deploy.ts +87 -0
  176. package/src/release/fake-engine.ts +50 -0
  177. package/src/release/fake-install.ts +59 -0
  178. package/src/release/fake-provider.ts +68 -0
  179. package/src/release/fake-release.ts +185 -0
  180. package/src/release/fixtures/victoria/darwin-arm64-assets.txt +22 -0
  181. package/src/release/fixtures/victoria/victoria-logs-darwin-arm64-v1.52.0_checksums.txt +2 -0
  182. package/src/release/fixtures/victoria/victoria-metrics-darwin-arm64-v1.151.0_checksums.txt +2 -0
  183. package/src/release/fixtures/victoria/victoria-traces-darwin-arm64-v0.10.0_checksums.txt +2 -0
  184. package/src/release/fixtures/victoria/vmutils-darwin-arm64-v1.151.0_checksums.txt +8 -0
  185. package/src/release/index.ts +41 -0
  186. package/src/release/plan.test.ts +189 -0
  187. package/src/release/provider-refusals.test.ts +48 -0
  188. package/src/release/providers.test.ts +174 -0
  189. package/src/release/providers.ts +24 -0
  190. package/src/release/recovery-read.test.ts +44 -0
  191. package/src/release/refused.ts +40 -0
  192. package/src/release/tar.test.ts +142 -0
  193. package/src/release/tar.ts +195 -0
  194. package/src/release/victoria.test.ts +108 -0
  195. package/src/release/victoria.ts +154 -0
  196. package/src/{proxmox → secrets}/write-only.ts +6 -0
  197. /package/dist/{proxmox → secrets}/write-only.d.ts +0 -0
  198. /package/src/{proxmox → secrets}/write-only.test.ts +0 -0
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Release.Binary through Alchemy's OWN Plan and Apply (openbao/fake-stack.ts, in-memory state), with
3
+ * `HostDirectory` declared in front of it, over a fake host that models mkdir/rmdir
4
+ * (linux/fake-linux-host.ts) and a fake HttpClient. No network, no real filesystem.
5
+ *
6
+ * ★ WHAT ONLY THE ENGINE CAN SAY. A handler test shows what `diff` answers; this shows what the
7
+ * engine then DOES with it, in which order: that `directory: dir.path` orders the directory
8
+ * first; that a version bump writes the new binary before the old one goes; that a version
9
+ * outside the catalog fails the plan with nothing fetched and nothing touched.
10
+ */
11
+ import { describe, expect, test } from 'bun:test';
12
+ import * as Effect from 'effect/Effect';
13
+ import * as Layer from 'effect/Layer';
14
+ import * as HttpClient from 'effect/unstable/http/HttpClient';
15
+ import * as HttpClientResponse from 'effect/unstable/http/HttpClientResponse';
16
+ import { sha256Hex } from '../launchd/job-form.ts';
17
+ import { type HostRunner, hostRunnerLayer } from '../launchd/runner.ts';
18
+ import { HostDirectory, HostDirectoryProvider } from '../linux/directory.ts';
19
+ import { fakeLinuxHost } from '../linux/fake-linux-host.ts';
20
+ import { fakeStack } from '../openbao/fake-stack.ts';
21
+ import { releaseUrl } from './binary-form.ts';
22
+ import { ReleaseBinary, makeReleaseBinaryProvider } from './binary.ts';
23
+ import { type PinnedBinary, catalogBinary, catalogDirectory } from './catalog.ts';
24
+ import {
25
+ BINARY,
26
+ VMALERT_REQUEST,
27
+ VMUTILS_URL,
28
+ bytesOf,
29
+ gzip,
30
+ syntheticRelease,
31
+ tarOf,
32
+ } from './fake-release.ts';
33
+ import { VICTORIA_RELEASES } from './victoria.ts';
34
+
35
+ const ROOT = '/opt/example/bin';
36
+ const NEXT = bytesOf('#!fake vmalert 1.152.0\n');
37
+
38
+ /** vmutils 1.151.0 (synthetic, at its real URL) and a synthetic 1.152.0 beside it. */
39
+ const releases = () => {
40
+ const current = syntheticRelease();
41
+ const next = gzip(tarOf([{ bytes: NEXT, name: 'vmalert-prod' }]));
42
+ const nextPins: PinnedBinary = {
43
+ archive: {
44
+ asset: 'vmutils-darwin-arm64-v1.152.0.tar.gz',
45
+ repo: 'VictoriaMetrics/VictoriaMetrics',
46
+ sha256: sha256Hex(next),
47
+ size: next.length,
48
+ tag: 'v1.152.0',
49
+ },
50
+ member: 'vmalert-prod',
51
+ name: 'vmalert',
52
+ sha256: sha256Hex(NEXT),
53
+ };
54
+ const nextUrl = releaseUrl(nextPins.archive.repo, nextPins.archive.tag, nextPins.archive.asset);
55
+ const served: Record<string, Uint8Array> = { [VMUTILS_URL]: current.vmutils, [nextUrl]: next };
56
+ return { current: catalogBinary(current.catalog, VMALERT_REQUEST), next: nextPins, served };
57
+ };
58
+
59
+ /** One fake host and one fake server; `events` is every host change and request, in order. */
60
+ const harness = () => {
61
+ const { current, next, served } = releases();
62
+ const fake = fakeLinuxHost({ dirs: { [ROOT]: 0 }, euid: 0 });
63
+ const events: string[] = [];
64
+ const base = fake.runner;
65
+ const runner: HostRunner = {
66
+ ...base,
67
+ exec: (argv) => {
68
+ events.push(argv.filter((arg) => arg !== '--').join(' '));
69
+ return base.exec(argv);
70
+ },
71
+ removeFile: (path) => {
72
+ events.push(`remove ${path}`);
73
+ return base.removeFile(path);
74
+ },
75
+ writeFileAtomic: (path, bytes, options) => {
76
+ events.push(`write ${path}`);
77
+ return base.writeFileAtomic(path, bytes, options);
78
+ },
79
+ };
80
+ const client = HttpClient.make((request, url) => {
81
+ events.push(`GET ${url.toString()}`);
82
+ const bytes = served[url.toString()];
83
+ const body = bytes === undefined ? null : new Blob([bytes.slice()]);
84
+ return Effect.succeed(
85
+ HttpClientResponse.fromWeb(request, new Response(body, { status: body ? 200 : 404 })),
86
+ );
87
+ });
88
+ const providers = Layer.mergeAll(makeReleaseBinaryProvider(), HostDirectoryProvider()).pipe(
89
+ Layer.provide(
90
+ Layer.mergeAll(hostRunnerLayer(runner), Layer.succeed(HttpClient.HttpClient, client)),
91
+ ),
92
+ );
93
+ const stack = fakeStack(providers, {}, 'ReleaseStack');
94
+ /** The stack body: one versioned directory and the binary in it, ordered by `dir.path`. */
95
+ const declare = (pins: PinnedBinary, version: string) =>
96
+ Effect.gen(function* () {
97
+ const path = catalogDirectory(ROOT, { package: 'vmutils', version });
98
+ const dir = yield* HostDirectory(`vmutils-${version}`, { mode: 0o755, path });
99
+ yield* ReleaseBinary('vmalert', { ...pins, directory: dir.path });
100
+ });
101
+ return { current, declare, events, fake, next, stack };
102
+ };
103
+
104
+ const OLD = `${ROOT}/vmutils-1.151.0`;
105
+ const NEW = `${ROOT}/vmutils-1.152.0`;
106
+
107
+ describe('through the engine', () => {
108
+ test('a version outside the catalog fails the plan: nothing fetched, nothing touched', async () => {
109
+ const h = harness();
110
+ const body = Effect.gen(function* () {
111
+ const pins = catalogBinary(VICTORIA_RELEASES, { ...VMALERT_REQUEST, version: '1.152.0' });
112
+ yield* ReleaseBinary('vmalert', { ...pins, directory: OLD });
113
+ });
114
+ await expect(h.stack.deploy(body)).rejects.toThrow('vmutils 1.152.0 is not in the catalog');
115
+ expect(h.events).toEqual([]);
116
+ });
117
+
118
+ test('the directory first, then one download, then the one write', async () => {
119
+ const h = harness();
120
+ const planned = await h.stack.deploy(h.declare(h.current, '1.151.0'));
121
+ expect(planned).toEqual({ vmalert: 'create', 'vmutils-1.151.0': 'create' });
122
+ expect(h.events).toEqual([`mkdir -m 755 ${OLD}`, `GET ${VMUTILS_URL}`, `write ${OLD}/vmalert`]);
123
+ expect(h.fake.files.get(`${OLD}/vmalert`)?.bytes).toEqual(BINARY.vmalert);
124
+ });
125
+
126
+ test('a second deploy of the same declaration changes nothing and fetches nothing', async () => {
127
+ const h = harness();
128
+ await h.stack.deploy(h.declare(h.current, '1.151.0'));
129
+ h.events.length = 0;
130
+ await h.stack.deploy(h.declare(h.current, '1.151.0'));
131
+ expect(h.events).toEqual([]);
132
+ });
133
+
134
+ test('a version bump: the new binary is written BEFORE the old one goes, then the old directory', async () => {
135
+ const h = harness();
136
+ await h.stack.deploy(h.declare(h.current, '1.151.0'));
137
+ h.events.length = 0;
138
+ const planned = await h.stack.deploy(h.declare(h.next, '1.152.0'));
139
+ expect(planned).toEqual({
140
+ vmalert: 'replace',
141
+ 'vmutils-1.151.0': 'delete',
142
+ 'vmutils-1.152.0': 'create',
143
+ });
144
+ // ★ MEASURED HERE, not reasoned from Apply.ts: create-before-delete, and the old directory
145
+ // only once it is empty — a job whose argv holds `vmalert.path` moves before its file goes.
146
+ expect(h.events).toEqual([
147
+ `mkdir -m 755 ${NEW}`,
148
+ `GET ${releaseUrl(h.next.archive.repo, h.next.archive.tag, h.next.archive.asset)}`,
149
+ `write ${NEW}/vmalert`,
150
+ `remove ${OLD}/vmalert`,
151
+ `rmdir ${OLD}`,
152
+ ]);
153
+ expect(h.fake.files.get(`${NEW}/vmalert`)?.bytes).toEqual(NEXT);
154
+ expect([...h.fake.files.keys()]).toEqual([`${NEW}/vmalert`]);
155
+ });
156
+
157
+ test('a new pin in the SAME directory fails the plan; the installed binary is untouched', async () => {
158
+ const h = harness();
159
+ await h.stack.deploy(h.declare(h.current, '1.151.0'));
160
+ h.events.length = 0;
161
+ await expect(h.stack.deploy(h.declare(h.next, '1.151.0'))).rejects.toThrow(
162
+ 'would overwrite it in place',
163
+ );
164
+ // ★ Refused by diff, at plan: the directory was already created, so its path is resolved.
165
+ expect(h.events).toEqual([]);
166
+ expect(h.fake.files.get(`${OLD}/vmalert`)?.bytes).toEqual(BINARY.vmalert);
167
+ });
168
+
169
+ test('the same bytes re-pinned while the directory is updated: refused at apply, binary kept', async () => {
170
+ const h = harness();
171
+ const body = (mode: number, tag: string) =>
172
+ Effect.gen(function* () {
173
+ const dir = yield* HostDirectory('vmutils-1.151.0', { mode, path: OLD });
174
+ const archive = { ...h.current.archive, tag };
175
+ yield* ReleaseBinary('vmalert', { ...h.current, archive, directory: dir.path });
176
+ });
177
+ await h.stack.deploy(body(0o755, 'v1.151.0'));
178
+ h.events.length = 0;
179
+ // The directory's update leaves its path unresolved at plan, so diff cannot see the path.
180
+ await expect(h.stack.deploy(body(0o750, 'v1.151.1'))).rejects.toThrow(
181
+ 'would overwrite it in place',
182
+ );
183
+ // 🔴 MEASURED 2026-09-22 with diff answering `replace` here instead of `update`: the deploy
184
+ // SUCCEEDED and the binary was gone — the new generation took the identical file as its own,
185
+ // then Phase 2 deleted the old generation at the same path (binary-diff.ts).
186
+ expect(h.events).toEqual([`chmod 750 ${OLD}`]);
187
+ expect(h.fake.files.get(`${OLD}/vmalert`)?.bytes).toEqual(BINARY.vmalert);
188
+ });
189
+ });
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The provider's plan-time refusals, through the engine-facing handlers (fake-provider.ts): each
3
+ * with zero requests and zero writes. (A version outside a catalog never reaches a handler: it
4
+ * fails in the stack program — catalog.test.ts, plan.test.ts.)
5
+ */
6
+ import { describe, expect, test } from 'bun:test';
7
+ import * as Effect from 'effect/Effect';
8
+ import type { ReleaseBinaryProps } from './binary-form.ts';
9
+ import { VMUTILS_DIR } from './fake-release.ts';
10
+ import { call, harness, ids, must } from './fake-provider.ts';
11
+
12
+ describe('plan-time refusals: zero requests, zero writes', () => {
13
+ test('the adoption probe refuses a malformed pin before touching the host', async () => {
14
+ const h = harness();
15
+ const next = { ...h.VMALERT, sha256: 'v1.151.0' };
16
+ const probe = h.run((p) => must(p.read)({ ...ids, olds: next as never, output: undefined }));
17
+ await expect(probe).rejects.toThrow('sha256 must be 64 lower-case hex digits');
18
+ expect(h.requests).toEqual([]);
19
+ expect(h.fake.calls).toEqual([]);
20
+ });
21
+
22
+ test('diff refuses a pin that is not a plain value, even while the directory is an Output', async () => {
23
+ const h = harness();
24
+ const output = await h.run((p) => h.reconcile(p, h.VMALERT));
25
+ h.requests.length = 0;
26
+ // ⛔ A pin computed during the deploy is a fetch by another name.
27
+ const pending = {
28
+ ...h.VMALERT,
29
+ directory: Effect.succeed(VMUTILS_DIR),
30
+ sha256: Effect.succeed(h.VMALERT.sha256),
31
+ };
32
+ const diff = h.run((p) =>
33
+ must(p.diff)({ ...ids, ...call, news: pending as never, olds: h.VMALERT as never, output }),
34
+ );
35
+ await expect(diff).rejects.toThrow('sha256 is required, as a plain string');
36
+ expect(h.requests).toEqual([]);
37
+ });
38
+
39
+ test('diff refuses a new pin at the same path', async () => {
40
+ const h = harness();
41
+ const output = await h.run((p) => h.reconcile(p, h.VMALERT));
42
+ const bumped: ReleaseBinaryProps = { ...h.VMALERT, sha256: 'e'.repeat(64) };
43
+ const diff = h.run((p) =>
44
+ must(p.diff)({ ...ids, ...call, news: bumped as never, olds: h.VMALERT as never, output }),
45
+ );
46
+ await expect(diff).rejects.toThrow('would overwrite it in place');
47
+ });
48
+ });
@@ -0,0 +1,174 @@
1
+ /**
2
+ * The Alchemy wiring: the provider built as a Layer over a fake host and a fake `HttpClient`, its
3
+ * handlers called the way the engine calls them — plus the barrel, the subpath's public API.
4
+ *
5
+ * ★ `read` with no prior state is the adoption probe. A binary already at the path comes back
6
+ * `Unowned`, recognised by its digest; after `--adopt` the forced update finds it matching and
7
+ * neither downloads nor writes. The plan-time refusals: provider-refusals.test.ts.
8
+ */
9
+ import { describe, expect, test } from 'bun:test';
10
+ import { Unowned, stripUnowned } from 'alchemy/AdoptPolicy';
11
+ import * as Effect from 'effect/Effect';
12
+ import { ReleaseBinary } from './binary.ts';
13
+ import {
14
+ BINARY,
15
+ VMUTILS_DIR,
16
+ VMUTILS_URL,
17
+ pathsOn,
18
+ releaseHost,
19
+ syntheticRelease,
20
+ vmalertProps,
21
+ } from './fake-release.ts';
22
+ import { PATH, call, harness, ids, must } from './fake-provider.ts';
23
+ import * as barrel from './index.ts';
24
+ import { releaseProviders } from './providers.ts';
25
+
26
+ describe('the lifecycle through the engine-facing handlers', () => {
27
+ test('reconcile installs, noting progress; diff is then a noop, a mode change an update', async () => {
28
+ const h = harness();
29
+ const [output, same, mode] = await h.run((p) =>
30
+ Effect.gen(function* () {
31
+ const output = yield* h.reconcile(p, h.VMALERT);
32
+ const diff = must(p.diff);
33
+ const base = { ...ids, ...call, olds: h.VMALERT as never, output };
34
+ return [
35
+ output,
36
+ yield* diff({ ...base, news: h.VMALERT as never }),
37
+ yield* diff({ ...base, news: { ...h.VMALERT, mode: 0o555 } as never }),
38
+ ] as const;
39
+ }),
40
+ );
41
+ expect(output.sha256).toBe(new Bun.CryptoHasher('sha256').update(BINARY.vmalert).digest('hex'));
42
+ expect([same, mode]).toEqual([{ action: 'noop' }, { action: 'update' }]);
43
+ expect(h.notes.some((n) => n.startsWith('downloading '))).toBe(true);
44
+ });
45
+
46
+ test('while the directory is an Output: other bytes are a replace, the same bytes an update', async () => {
47
+ const h = harness();
48
+ const diffs = await h.run((p) =>
49
+ Effect.gen(function* () {
50
+ const output = yield* h.reconcile(p, h.VMALERT);
51
+ const base = { ...ids, ...call, olds: h.VMALERT as never, output };
52
+ const pending = { ...h.VMALERT, directory: Effect.succeed(`${VMUTILS_DIR}-next`) };
53
+ const archive = { ...h.VMALERT.archive, tag: 'v1.152.0' };
54
+ const diff = (news: object) => must(p.diff)({ ...base, news: news as never });
55
+ return [
56
+ yield* diff({ ...pending, archive, sha256: 'e'.repeat(64) }),
57
+ yield* diff({ ...pending, archive }),
58
+ yield* diff(pending),
59
+ ];
60
+ }),
61
+ );
62
+ // ★ Never `replace` for the same bytes: onto an unmoved path, the new generation would accept
63
+ // the old file as its own and Phase 2 would then delete it (binary-diff.ts).
64
+ expect(diffs).toEqual([{ action: 'replace' }, { action: 'update' }, undefined]);
65
+ });
66
+
67
+ test('that update, if the path did not move after all, is refused at apply', async () => {
68
+ const h = harness();
69
+ const output = await h.run((p) => h.reconcile(p, h.VMALERT));
70
+ h.requests.length = 0;
71
+ const repinned = { ...h.VMALERT, archive: { ...h.VMALERT.archive, tag: 'v1.152.0' } };
72
+ const applying = h.run((p) => h.reconcile(p, repinned, output, h.VMALERT));
73
+ await expect(applying).rejects.toThrow('would overwrite it in place');
74
+ expect(h.requests).toEqual([]);
75
+ expect(h.fake.files.get(PATH)?.bytes).toEqual(BINARY.vmalert);
76
+ });
77
+
78
+ test('a binary someone already placed: Unowned by digest, then adopted with no download', async () => {
79
+ const h = harness();
80
+ h.fake.files.set(PATH, { bytes: BINARY.vmalert, gid: 0, kind: 'file', mode: 0o755, uid: 0 });
81
+ const before = pathsOn(h.fake);
82
+ const [probe, adopted] = await h.run((p) =>
83
+ Effect.gen(function* () {
84
+ const probe = yield* must(p.read)({ ...ids, olds: h.VMALERT as never, output: undefined });
85
+ if (probe === undefined) throw new Error('nothing found');
86
+ // What the engine does after `--adopt`: strip the brand, then force an update.
87
+ const adopted = yield* h.reconcile(p, h.VMALERT, stripUnowned(probe));
88
+ return [probe, adopted] as const;
89
+ }),
90
+ );
91
+ expect(Unowned.is(probe)).toBe(true);
92
+ // ★ Recognised by its digest alone: the probe's sha256 IS the pin; nothing was executed.
93
+ expect(probe.sha256).toBe(h.VMALERT.sha256);
94
+ expect(adopted).toMatchObject({
95
+ member: 'vmalert-prod',
96
+ path: PATH,
97
+ sha256: h.VMALERT.sha256,
98
+ url: VMUTILS_URL,
99
+ });
100
+ expect(h.requests).toEqual([]);
101
+ expect(h.fake.calls.filter((c) => c[0] === 'write')).toEqual([]);
102
+ expect(pathsOn(h.fake)).toEqual(before);
103
+ });
104
+
105
+ test('vmagent and vmalert at once: ONE download of the shared archive', async () => {
106
+ const h = harness();
107
+ const vmagent = {
108
+ ...h.VMALERT,
109
+ member: 'vmagent-prod',
110
+ name: 'vmagent',
111
+ sha256: new Bun.CryptoHasher('sha256').update(BINARY.vmagent).digest('hex'),
112
+ };
113
+ await h.run((p) =>
114
+ Effect.all([h.reconcile(p, h.VMALERT), h.reconcile(p, vmagent)], {
115
+ concurrency: 'unbounded',
116
+ }),
117
+ );
118
+ expect(h.requests).toEqual([VMUTILS_URL]);
119
+ expect(pathsOn(h.fake)).toEqual([`${VMUTILS_DIR}/vmagent`, PATH]);
120
+ });
121
+
122
+ test('delete removes the binary and never the directory', async () => {
123
+ const h = harness();
124
+ await h.run((p) =>
125
+ Effect.gen(function* () {
126
+ const output = yield* h.reconcile(p, h.VMALERT);
127
+ yield* p.delete({
128
+ ...ids,
129
+ bindings: [] as never,
130
+ olds: h.VMALERT as never,
131
+ output,
132
+ session: undefined as never,
133
+ });
134
+ }),
135
+ );
136
+ expect(h.fake.files.has(PATH)).toBe(false);
137
+ expect(h.fake.dirs.has(VMUTILS_DIR)).toBe(true);
138
+ });
139
+ });
140
+
141
+ describe('the subpath', () => {
142
+ test('releaseProviders builds with its own HTTP client, and a probe of nothing is nothing', async () => {
143
+ const fake = releaseHost();
144
+ const VMALERT = vmalertProps(syntheticRelease().catalog);
145
+ const found = await Effect.runPromise(
146
+ Effect.gen(function* () {
147
+ const p = yield* ReleaseBinary.Provider;
148
+ return yield* must(p.read)({ ...ids, olds: VMALERT as never, output: undefined });
149
+ }).pipe(Effect.provide(releaseProviders(fake.runner))),
150
+ );
151
+ expect(found).toBeUndefined();
152
+ });
153
+
154
+ test('the barrel is exactly the public API', () => {
155
+ expect(Object.keys(barrel).sort()).toEqual([
156
+ 'ArchiveRefused',
157
+ 'BinaryRefused',
158
+ 'ChecksumMismatch',
159
+ 'DownloadFailed',
160
+ 'HostRunnerService',
161
+ 'ReleaseBinary',
162
+ 'ReleaseBinaryProvider',
163
+ 'VICTORIA_RELEASES',
164
+ 'catalogBinary',
165
+ 'catalogDirectory',
166
+ 'catalogProblems',
167
+ 'hostRunnerLayer',
168
+ 'identifyBinary',
169
+ 'releaseBinaryPath',
170
+ 'releaseProviders',
171
+ 'releaseUrl',
172
+ ]);
173
+ });
174
+ });
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The release-binary provider with ONE HostRunner and its own HTTP client, as a single layer for a
3
+ * host stack:
4
+ *
5
+ * const runner = localRunner();
6
+ * Layer.mergeAll(launchdProviders(runner), releaseProviders(runner), …the stack's others)
7
+ *
8
+ * ★ PASS THE SAME RUNNER THE JOBS USE, so the binary and the daemon that runs it are written by one
9
+ * identity on one host — a stack cannot install as one user and bootstrap as another by accident.
10
+ * ⛔ THERE IS NO DEFAULT RUNNER. A binary is written wherever the runner reaches; a silent default to
11
+ * "this machine" would install a Mac's daemons onto whichever laptop ran the deploy (the same
12
+ * reasoning as linuxProviders()).
13
+ * ★ `FetchHttpClient.layer` IS PROVIDED HERE, not left to the stack: the Mac host stack this exists
14
+ * for provides no HTTP layer, and a missing one fails only at the first install.
15
+ */
16
+ import * as Layer from 'effect/Layer';
17
+ import * as FetchHttpClient from 'effect/unstable/http/FetchHttpClient';
18
+ import { type HostRunner, hostRunnerLayer } from '../launchd/runner.ts';
19
+ import { ReleaseBinaryProvider } from './binary.ts';
20
+
21
+ export const releaseProviders = (runner: HostRunner) =>
22
+ ReleaseBinaryProvider().pipe(
23
+ Layer.provide(Layer.mergeAll(hostRunnerLayer(runner), FetchHttpClient.layer)),
24
+ );
@@ -0,0 +1,44 @@
1
+ /**
2
+ * A FAILED FIRST INSTALL MUST NOT BRICK THE STAGE (binary-read.ts), through Alchemy's own Plan and
3
+ * Apply. With `directory: dir.path`, the documented shape, Apply commits the `creating` row while
4
+ * the directory is still an Output, so the row carries no `directory`; the next plan's recovery
5
+ * read — and, once the declaration is gone, Apply's orphan delete — is asked with that row.
6
+ *
7
+ * 🔴 MEASURED 2026-09-22 against the code before binary-read.ts, every case below: the next deploy
8
+ * died with `TypeError: undefined is not an object (evaluating 'path.startsWith')`, the fixed
9
+ * declaration included, and removing the declaration failed with `DestroyError`.
10
+ */
11
+ import { describe, expect, test } from 'bun:test';
12
+ import { BINARY, VMUTILS_URL } from './fake-release.ts';
13
+ import { DIR, PATH, deployHarness } from './fake-deploy.ts';
14
+
15
+ describe('after a failed first install', () => {
16
+ test('a download that failed: the next deploy installs, then is a noop', async () => {
17
+ const h = deployHarness(false);
18
+ h.server.serving = false;
19
+ await expect(h.stack.deploy(h.wired())).rejects.toThrow('HTTP 404');
20
+ h.server.serving = true;
21
+ h.events.length = 0;
22
+ expect(await h.stack.deploy(h.wired())).toEqual({ dir: 'noop', vmalert: 'create' });
23
+ expect(h.events).toEqual([`GET ${VMUTILS_URL}`, `write ${PATH}`]);
24
+ expect(h.fake.files.get(PATH)?.bytes).toEqual(BINARY.vmalert);
25
+ expect(await h.stack.deploy(h.wired())).toEqual({ dir: 'noop', vmalert: 'noop' });
26
+ });
27
+
28
+ test('a declaration refused at apply (a group-writable mode), then fixed: it installs', async () => {
29
+ const h = deployHarness(false);
30
+ await expect(
31
+ h.stack.deploy(h.wired('vmalert', 0o755, undefined, { mode: 0o775 })),
32
+ ).rejects.toThrow('mode must not be group- or world-writable');
33
+ expect(await h.stack.deploy(h.wired())).toEqual({ dir: 'noop', vmalert: 'create' });
34
+ expect(h.fake.files.get(PATH)?.mode).toBe(0o755);
35
+ });
36
+
37
+ test('the failed declaration removed instead: its row is dropped, the directory kept', async () => {
38
+ const h = deployHarness(false);
39
+ h.server.serving = false;
40
+ await expect(h.stack.deploy(h.wired())).rejects.toThrow('HTTP 404');
41
+ expect(await h.stack.deploy(h.dirOnly)).toEqual({ dir: 'noop', vmalert: 'delete' });
42
+ expect(h.fake.dirs.has(DIR)).toBe(true);
43
+ });
44
+ });
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Release.Binary's failures as tagged errors — each carrying the house refusal sentence
3
+ * (`Release.Binary <path>: …`) as its `message`.
4
+ *
5
+ * ★ TAGGED BECAUSE ALCHEMY UPSTREAM REQUIRES IT (its AGENTS.md at v2.0.0-beta.79: Data.TaggedError,
6
+ * `catchTag`, never a cast on `_tag`), so the classes need no rewrite on the way there. The
7
+ * MESSAGE stays the kit's wording, so a deploy log reads the same as every other house family's.
8
+ * ⚠️ TAGGED IS NOT YET TYPED END TO END — measured in error-channel.test.ts. catalogBinary() throws,
9
+ * so in a stack program its BinaryRefused is a DEFECT that `catchTag` never sees; and the provider
10
+ * runs every lifecycle call through the house `lift()`, which keeps the instance at runtime but
11
+ * types the channel as `Error`. No caller can name one of these tags today without a cast.
12
+ * ★ THE TAGS NAME NO VENDOR. Downloading a pinned archive, refusing an unsafe one and comparing
13
+ * digests are the same for every catalog (docs/release-binary-upstream.md).
14
+ */
15
+ import * as Data from 'effect/Data';
16
+
17
+ /** The declaration itself: a version no catalog pins, a malformed pin, a bad path or mode. */
18
+ export class BinaryRefused extends Data.TaggedError('BinaryRefused')<{
19
+ readonly message: string;
20
+ }> {}
21
+
22
+ /** The download: a status that is not 200, a body that is not the pinned size, a dead transport. */
23
+ export class DownloadFailed extends Data.TaggedError('DownloadFailed')<{
24
+ readonly message: string;
25
+ /** Worth another attempt: a transport failure or a 5xx, never a wrong size or a 404. */
26
+ readonly retryable: boolean;
27
+ }> {}
28
+
29
+ /** Bytes that do not hash to their pin — the archive, or a member extracted from it. */
30
+ export class ChecksumMismatch extends Data.TaggedError('ChecksumMismatch')<{
31
+ readonly message: string;
32
+ readonly subject: string;
33
+ readonly expected: string;
34
+ readonly actual: string;
35
+ }> {}
36
+
37
+ /** An archive this reader will not trust: an unsafe or unexpected entry, a missing member. */
38
+ export class ArchiveRefused extends Data.TaggedError('ArchiveRefused')<{
39
+ readonly message: string;
40
+ }> {}
@@ -0,0 +1,142 @@
1
+ /**
2
+ * The tar reader against archives built here, byte by byte: it hands back only what was asked for,
3
+ * by exact name, and refuses the WHOLE archive for any entry that could make a name lie — whether
4
+ * or not that entry was the one asked for.
5
+ */
6
+ import { describe, expect, test } from 'bun:test';
7
+ import { extractMembers } from './archive.ts';
8
+ import { type TarEntry, bytesOf, gzip, tarHeader, tarOf } from './fake-release.ts';
9
+ import { ArchiveRefused } from './refused.ts';
10
+ import { tarReader } from './tar.ts';
11
+
12
+ const binary = { bytes: bytesOf('#!binary\n'), name: 'vmalert-prod' } as const;
13
+ const read = (tar: Uint8Array, wanted = ['vmalert-prod'], chunk = tar.length) => {
14
+ const reader = tarReader(new Set(wanted));
15
+ for (let at = 0; at < tar.length; at += chunk) reader.push(tar.subarray(at, at + chunk));
16
+ return reader.finish();
17
+ };
18
+ const refusal = (run: () => unknown): string => {
19
+ try {
20
+ run();
21
+ } catch (cause) {
22
+ expect(cause).toBeInstanceOf(ArchiveRefused);
23
+ return (cause as ArchiveRefused).message;
24
+ }
25
+ throw new Error('expected a refusal');
26
+ };
27
+
28
+ describe('reading', () => {
29
+ test('only the asked-for member comes back; every name is listed', () => {
30
+ const tar = tarOf([{ bytes: bytesOf('x'.repeat(700)), name: 'vmagent-prod' }, binary]);
31
+ const { members, names } = read(tar);
32
+ expect([...members.keys()]).toEqual(['vmalert-prod']);
33
+ expect(new TextDecoder().decode(members.get('vmalert-prod'))).toBe('#!binary\n');
34
+ expect(names).toEqual(['vmagent-prod', 'vmalert-prod']);
35
+ });
36
+
37
+ test('one byte at a time reads the same as all at once', () => {
38
+ const tar = tarOf([{ bytes: bytesOf('y'.repeat(1025)), name: 'vmagent-prod' }, binary]);
39
+ expect(read(tar, ['vmagent-prod', 'vmalert-prod'], 1).members).toEqual(
40
+ read(tar, ['vmagent-prod', 'vmalert-prod']).members,
41
+ );
42
+ });
43
+
44
+ test('matching is exact: no ./, no prefix, no suffix', () => {
45
+ const tar = tarOf([
46
+ { ...binary, name: './vmalert-prod' },
47
+ { ...binary, name: 'vmalert-prod-x' },
48
+ ]);
49
+ expect(read(tar).members.size).toBe(0);
50
+ });
51
+
52
+ test('a POSIX prefix is part of the name', () => {
53
+ const tar = tarOf([{ ...binary, format: 'posix', prefix: 'bin' }]);
54
+ expect(read(tar, ['bin/vmalert-prod']).members.size).toBe(1);
55
+ expect(read(tar).members.size).toBe(0);
56
+ });
57
+
58
+ test('an empty member and trailing zero padding are fine', () => {
59
+ const tar = tarOf([{ name: 'empty' }, binary], 10_240);
60
+ expect(read(tar, ['empty']).members.get('empty')?.length).toBe(0);
61
+ });
62
+
63
+ test('extractMembers gunzips as it reads', async () => {
64
+ const { members } = await extractMembers(gzip(tarOf([binary])), new Set(['vmalert-prod']));
65
+ expect(members.get('vmalert-prod')).toEqual(binary.bytes);
66
+ });
67
+ });
68
+
69
+ describe('tar-slip and every other entry that could lie is refused, asked for or not', () => {
70
+ const beside = (entry: TarEntry) => tarOf([binary, entry]);
71
+ test.each([
72
+ ['an absolute name', { name: '/etc/passwd' }, 'is absolute'],
73
+ ['a .. segment', { name: '../vmalert-prod' }, 'climbs out'],
74
+ ['a .. deeper in', { name: 'bin/../../x' }, 'climbs out'],
75
+ ['a symlink', { name: 'link', type: '2' }, 'a symlink'],
76
+ ['a hard link', { name: 'hard', type: '1' }, 'a hard link'],
77
+ ['a directory', { name: 'bin/', type: '5' }, 'a directory'],
78
+ ['a FIFO', { name: 'fifo', type: '6' }, 'a FIFO'],
79
+ ['a GNU long name', { name: '././@LongLink', type: 'L' }, 'GNU long-name'],
80
+ ['a PAX header', { name: 'PaxHeader', type: 'x' }, 'PAX extended'],
81
+ ['an unknown type', { name: 'odd', type: 'Z' }, 'unknown type'],
82
+ ['a repeated name', binary, 'appears twice'],
83
+ ['a failed header checksum', { corruptChecksum: true, name: 'x' }, 'fails its checksum'],
84
+ ['a base-256 size', { base256: true, bytes: bytesOf('z'), name: 'big' }, 'base-256'],
85
+ ['a character device', { name: 'tty', type: '3' }, 'a character device'],
86
+ ['a block device', { name: 'disk', type: '4' }, 'a block device'],
87
+ ['a contiguous file', { name: 'contig', type: '7' }, 'a contiguous file'],
88
+ ['a GNU long link', { name: '././@LongLink', type: 'K' }, 'GNU long-link'],
89
+ ['a PAX global header', { name: 'pax_global_header', type: 'g' }, 'PAX global'],
90
+ ['an empty name', { bytes: bytesOf('z'), name: '' }, 'empty name'],
91
+ ] as const)('%s', (_, entry, message) => {
92
+ expect(refusal(() => read(beside(entry)))).toContain(message);
93
+ });
94
+
95
+ test('a name that is not UTF-8 is refused, not decoded loosely into another name', () => {
96
+ const tar = tarOf([binary, { bytes: bytesOf('z'), name: 'x' }]);
97
+ const at = 1024; // the second header: one header block and one data block in
98
+ tar[at] = 0xff;
99
+ tar.fill(0x20, at + 148, at + 156);
100
+ const sum = tar.subarray(at, at + 512).reduce((total, byte) => total + byte, 0);
101
+ tar.set(bytesOf(`${sum.toString(8).padStart(6, '0')}\0 `), at + 148);
102
+ expect(refusal(() => read(tar))).toContain('not UTF-8');
103
+ });
104
+
105
+ test('a symlink under the asked-for name is refused, not followed', () => {
106
+ expect(refusal(() => read(tarOf([{ name: 'vmalert-prod', type: '2' }])))).toContain('symlink');
107
+ });
108
+
109
+ test('a header that is neither ustar nor GNU (v7 tar) is refused', () => {
110
+ const tar = tarOf([binary]);
111
+ tar.fill(0, 257, 265);
112
+ const header = tarHeader(binary);
113
+ header.fill(0, 257, 265);
114
+ header.fill(0x20, 148, 156);
115
+ const sum = header.reduce((total, byte) => total + byte, 0);
116
+ header.set(new TextEncoder().encode(`${sum.toString(8).padStart(6, '0')}\0 `), 148);
117
+ tar.set(header, 0);
118
+ expect(refusal(() => read(tar))).toContain('neither ustar nor GNU');
119
+ });
120
+
121
+ test('a truncated archive — no end marker — is refused', () => {
122
+ const tar = tarOf([binary]);
123
+ expect(refusal(() => read(tar.subarray(0, tar.length - 1024)))).toContain('stops before');
124
+ });
125
+
126
+ test('a lone zero block between entries is refused', () => {
127
+ const one = tarOf([binary], 512);
128
+ const two = tarOf([{ ...binary, name: 'other' }]);
129
+ const spliced = new Uint8Array([...one, ...two]);
130
+ expect(refusal(() => read(spliced))).toContain('zero block');
131
+ });
132
+
133
+ test('data after the end marker is refused', () => {
134
+ const tar = new Uint8Array([...tarOf([binary]), 1]);
135
+ expect(refusal(() => read(tar))).toContain('follows the end-of-archive');
136
+ });
137
+
138
+ test('bytes that are not gzip are refused as an archive, not thrown raw', async () => {
139
+ const promise = extractMembers(bytesOf('not gzip at all'), new Set(['x']));
140
+ await expect(promise).rejects.toBeInstanceOf(ArchiveRefused);
141
+ });
142
+ });