@homeflare/alchemy 0.37.0 → 0.37.2
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.
- package/dist/proxmox/ceph-daemon-wire.d.ts +5 -7
- package/dist/proxmox/ceph-daemon-wire.d.ts.map +1 -1
- package/dist/proxmox/ceph-daemon.d.ts +7 -8
- package/dist/proxmox/ceph-daemon.d.ts.map +1 -1
- package/dist/proxmox/ceph-fs-distilled.d.ts +31 -5
- package/dist/proxmox/ceph-fs-distilled.d.ts.map +1 -1
- package/dist/proxmox/ceph-fs-wire.d.ts +5 -22
- package/dist/proxmox/ceph-fs-wire.d.ts.map +1 -1
- package/dist/proxmox/ceph-fs.d.ts +5 -6
- package/dist/proxmox/ceph-fs.d.ts.map +1 -1
- package/dist/proxmox/ceph-pool-settle.d.ts +9 -8
- package/dist/proxmox/ceph-pool-settle.d.ts.map +1 -1
- package/dist/proxmox/ceph-pool-wire.d.ts +11 -20
- package/dist/proxmox/ceph-pool-wire.d.ts.map +1 -1
- package/dist/proxmox/group-wire.d.ts +8 -13
- package/dist/proxmox/group-wire.d.ts.map +1 -1
- package/dist/proxmox/index.js +296 -326
- package/dist/proxmox/index.js.map +14 -14
- package/dist/proxmox/node-network-wire.d.ts +1 -1
- package/dist/proxmox/node-network-wire.d.ts.map +1 -1
- package/dist/proxmox/storage-wire.d.ts +8 -11
- package/dist/proxmox/storage-wire.d.ts.map +1 -1
- package/dist/proxmox/unreadable-read.d.ts +7 -7
- package/dist/proxmox/user-wire.d.ts +8 -11
- package/dist/proxmox/user-wire.d.ts.map +1 -1
- package/dist/proxmox/user.d.ts +5 -5
- package/dist/proxmox/zfs-pool-wire.d.ts +1 -1
- package/dist/proxmox/zfs-pool-wire.d.ts.map +1 -1
- package/docs/changelog/README.md +22 -0
- package/docs/changelog/archive-01.md +176 -0
- package/docs/changelog/archive-02.md +180 -0
- package/docs/changelog/archive-03.md +179 -0
- package/docs/changelog/archive-04.md +178 -0
- package/docs/changelog/archive-05.md +177 -0
- package/docs/changelog/archive-06.md +180 -0
- package/docs/changelog/archive-07.md +180 -0
- package/docs/changelog/archive-08.md +178 -0
- package/docs/changelog/archive-09.md +181 -0
- package/docs/changelog/archive-10.md +182 -0
- package/docs/changelog/archive-11.md +182 -0
- package/docs/changelog/archive-12.md +183 -0
- package/docs/changelog/archive-13.md +180 -0
- package/docs/changelog/archive-14.md +178 -0
- package/docs/changelog/archive-15.md +181 -0
- package/docs/changelog/archive-16.md +182 -0
- package/docs/changelog/archive-17.md +183 -0
- package/docs/changelog/archive-18.md +55 -0
- package/docs/distilled-interim.md +15 -3
- package/docs/provider-standard.md +24 -6
- package/docs/proxmox.md +26 -0
- package/docs/upstream-conformance-bun.md +67 -0
- package/docs/upstream-conformance-conforms.md +55 -0
- package/docs/upstream-conformance-families.md +95 -0
- package/docs/upstream-conformance-grafana.md +106 -0
- package/docs/upstream-conformance.md +20 -298
- package/package.json +3 -3
- package/src/proxmox/ceph-daemon-wire.ts +4 -7
- package/src/proxmox/ceph-daemon.ts +7 -8
- package/src/proxmox/ceph-fs-delete.test.ts +5 -8
- package/src/proxmox/ceph-fs-destroy-sdk.test.ts +98 -0
- package/src/proxmox/ceph-fs-distilled.ts +45 -19
- package/src/proxmox/ceph-fs-wire.ts +5 -93
- package/src/proxmox/ceph-fs.ts +13 -15
- package/src/proxmox/ceph-pool-settle.ts +11 -10
- package/src/proxmox/ceph-pool-wire.ts +11 -20
- package/src/proxmox/ceph-pool-write.test.ts +9 -8
- package/src/proxmox/group-wire.ts +7 -12
- package/src/proxmox/read-failure-propagation.test.ts +5 -5
- package/src/proxmox/storage-wire.ts +9 -10
- package/src/proxmox/typed-absence.test.ts +146 -0
- package/src/proxmox/unreadable-read.ts +7 -7
- package/src/proxmox/user-wire.ts +7 -10
- package/src/proxmox/user.ts +5 -5
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# Upstream conformance, family by family
|
|
2
2
|
|
|
3
|
-
Status: open ledger.
|
|
3
|
+
Status: open ledger. Original audit: 2026-09-22, reading `origin/main` `925454b` against
|
|
4
4
|
[provider-standard.md](./provider-standard.md) (`alchemy@2.0.0-beta.79`). Nothing in any
|
|
5
5
|
family was changed by this audit. Each row names the rule it breaks and the upstream
|
|
6
6
|
replacement or fix. A row is crossed off in the PR that fixes it, with that PR's evidence.
|
|
7
|
-
A decision marked **maintainer** is not an agent's to make.
|
|
7
|
+
A decision marked **maintainer** is not an agent's to make. S20/S21 and decision-49
|
|
8
|
+
corrections below were checked on 2026-09-24; this is not a fresh audit of every row.
|
|
8
9
|
|
|
9
10
|
## Ranked findings
|
|
10
11
|
|
|
@@ -22,12 +23,12 @@ then tidiness.
|
|
|
22
23
|
`reconcileLock`/`deleteLock` (S19): distilled's operations are already Effects, so there
|
|
23
24
|
is nothing left to promise-wrap.
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
✅ **S20 and idempotent delete fixed 2026-09-24:** [kit PR #266](https://github.com/taslabs-net/homeflare-kit/pull/266)
|
|
27
|
+
removes lifecycle `Effect.orDie`, propagates distilled's typed SDK tags unchanged, and
|
|
28
|
+
folds `NoSuchBucket` to success on delete. This follows beta.79's `R2/BucketSippy.ts`;
|
|
29
|
+
no blanket refusal remap is needed. The existing read already folds that tag to absence.
|
|
30
|
+
|
|
31
|
+
Still open (outside that error-handling change):
|
|
31
32
|
- `reconcileLock` still skips the PUT whenever `output.rules` equals the declaration,
|
|
32
33
|
without reading the live lock (S10). Neither `--force` nor `drift --repair` can
|
|
33
34
|
restore lock rules removed out of band. ⚠️ Reasoned from the source, not measured live.
|
|
@@ -139,7 +140,7 @@ then tidiness.
|
|
|
139
140
|
6. **`openbao/*` has no upstream equivalent, and it conforms on the contract** (Effect
|
|
140
141
|
`HttpClient`, strict `Unowned`, retain). It diverges in three ways:
|
|
141
142
|
- 30 `Effect.die` sites in non-test source, several of them on `diff` and
|
|
142
|
-
`reconcile` paths (S20);
|
|
143
|
+
`reconcile` paths (defect risk; S20 no longer separately bans `Effect.die`);
|
|
143
144
|
- 42 test files on `node:test` (S43);
|
|
144
145
|
- `Bao.*` type strings, where the vendor name is `OpenBao` (H14).
|
|
145
146
|
|
|
@@ -147,300 +148,21 @@ then tidiness.
|
|
|
147
148
|
distilled's shape is the S23 path. **Decision:** maintainer, on whether the kit authors
|
|
148
149
|
a distilled package.
|
|
149
150
|
|
|
150
|
-
|
|
151
|
-
vendor schema, and a manifest records the version and sha256. It diverges in four ways:
|
|
152
|
-
- the generic factory's `read` returns plain attributes, so every family except `Lxc`
|
|
153
|
-
adopts silently, against the house's own H1;
|
|
154
|
-
- 27 `Effect.die` sites in non-test source (S20);
|
|
155
|
-
- 7 test files on `node:test`;
|
|
156
|
-
- `Pbs.*` type strings (H14).
|
|
157
|
-
8. **`talos/*`.** Its `talosctl` calls go through `ChildProcessSpawner`, as upstream's Docker
|
|
158
|
-
provider does, which conforms. Its file handling uses `Bun.file`, `Bun.write`,
|
|
159
|
-
`Bun.YAML` and `node:fs` (S42), and it uses `Effect.promise` in 3 files (S19). The
|
|
160
|
-
kubeconfig it writes is meant to be consumed through upstream's
|
|
161
|
-
`Kubernetes.KubeConfig({ path })`.
|
|
162
|
-
9. **`netbox/*` conforms mostly.** Its constraints are generated from NetBox 4.7.0's
|
|
163
|
-
OpenAPI. Adopting first is documented as deliberate.
|
|
164
|
-
- ✅ **S23 fixed 2026-09-23** (branch `claude2/distilled-netbox-family`, mirroring the
|
|
165
|
-
Forgejo migration above, decision 42). `Netbox.Prefix` now calls
|
|
166
|
-
`@distilled.cloud/netbox`'s typed `ipam` operations instead of a hand-rolled `Effect
|
|
167
|
-
HttpClient` client. The old status-carrying `NetboxError` and its `cause.status ===
|
|
168
|
-
404` check are gone entirely — `Netbox.Prefix` locates by a server-side list filter,
|
|
169
|
-
which never 404s (an empty page is a normal 200), so nothing here checks a status
|
|
170
|
-
code at all, a stronger form of the same rule `catchTag('NotFound', …)` enforces for
|
|
171
|
-
a family that reads by direct key. `client.ts` is deleted. Not published upstream
|
|
172
|
-
yet, so aliased onto `@homeflare/distilled-netbox@0.2.0` as a plain `dependencies`
|
|
173
|
-
entry, not a peer — [distilled-interim.md](./distilled-interim.md). State did not
|
|
174
|
-
move: props/attributes stay byte-identical, proven by the family's unchanged
|
|
175
|
-
existing tests plus new tests against a fake NetBox exercising the real distilled
|
|
176
|
-
protocol.
|
|
177
|
-
- ⛔ **Still diverges on credentials (S24):** `NETBOX_TOKEN` / `NETBOX_URL` are read at
|
|
178
|
-
call time, now through the SDK's `CredentialsFromEnv` rather than a hand-rolled
|
|
179
|
-
`token()` — same divergence, unchanged by the transport swap.
|
|
180
|
-
10. **`litellm/*` has no upstream equivalent** (LiteLLM has no upstream Alchemy family, the
|
|
181
|
-
same situation `openbao/*` is in above), and conforms on the transport contract.
|
|
182
|
-
- ✅ **S23 fixed 2026-09-24** (branch `claude2/litellm-distilled`, mirroring the NetBox
|
|
183
|
-
migration above, decision 43). `LiteLLM.PassThroughEndpoint` now calls
|
|
184
|
-
`@distilled.cloud/litellm`'s typed `misc` operations (`operations.ts`) instead of a
|
|
185
|
-
hand-rolled `Effect HttpClient` client. The old status-carrying `LitellmBadRequestError`
|
|
186
|
-
family is gone: every failure the four operations declare (`BadRequest`, `NotFound` on
|
|
187
|
-
update, `UnprocessableEntity`, plus the shared `Unauthorized`/`TooManyRequests`/server
|
|
188
|
-
errors) is the SDK's own typed error, `catchTag`'d — `deletePassThroughEndpoint`'s
|
|
189
|
-
re-list-on-ambiguous-`BadRequest` trick (S21: a status-and-a-real-read decision, never
|
|
190
|
-
body text) is unchanged, now keyed on the tag instead of the status code. The
|
|
191
|
-
per-base-URL write semaphore this vendor's whole-list storage forces (docs/litellm.md)
|
|
192
|
-
is unchanged too — the SDK has no opinion on it, so it stays one layer above the typed
|
|
193
|
-
calls, in `operations.ts`. `client.ts` and the kit's own hand-generated
|
|
194
|
-
`generated/pass-through.ts` are both deleted (the SDK's `misc.PassThroughGenericEndpoint`
|
|
195
|
-
is the same shape, generated from the same LiteLLM 1.100.0 OpenAPI document — see
|
|
196
|
-
`codegen/manifest.json`'s `litellm-openapi` entry, left in place and unconsumed rather
|
|
197
|
-
than deleted, the same pattern the two UniFi entries already establish). Not published
|
|
198
|
-
upstream yet, so aliased onto `@homeflare/distilled-litellm@0.2.0` as a plain
|
|
199
|
-
`dependencies` entry, not a peer — [distilled-interim.md](./distilled-interim.md). State
|
|
200
|
-
did not move: props/attributes stay byte-identical, proven by the family's unchanged
|
|
201
|
-
existing tests (now against a fake LiteLLM exercising the real distilled protocol
|
|
202
|
-
through `FetchHttpClient.Fetch`, not a loopback `Bun.serve`) plus the engine-level
|
|
203
|
-
replace test through `fake-stack.ts`. No stack in this estate currently imports
|
|
204
|
-
`litellmProviders`/`LiteLLM.PassThroughEndpoint` (measured 2026-09-24 across
|
|
205
|
-
homeflare-landscape's own repositories, including this one's `stacks/`), so there is no
|
|
206
|
-
live plan to re-run.
|
|
207
|
-
- ⛔ **Still diverges on credentials (S24):** `LITELLM_PROXY_URL` / `LITELLM_PROXY_API_KEY`
|
|
208
|
-
are read at call time, now through the SDK's `CredentialsFromEnv` rather than a
|
|
209
|
-
hand-rolled `resolveCreds` — same divergence, unchanged by the transport swap.
|
|
210
|
-
- ✅ **S20 fixed 2026-09-24** (same branch, PR review pass): `CredentialsFromEnv` ended in
|
|
211
|
-
`Effect.orDie`, so a missing/misspelled env var died as an engine-crashing defect
|
|
212
|
-
instead of the typed `ConfigError` `LitellmOpError` already declared — a regression
|
|
213
|
-
this migration made newly reachable (the retired hand-rolled `credentials.ts` failed
|
|
214
|
-
typed). Fixed at the source: `@distilled.cloud/litellm`'s own `credentials.ts`
|
|
215
|
-
(`homeflare/litellm@4ad19154`, commit local, not pushed — S22), copied forward here.
|
|
216
|
-
`netbox/*`'s `CredentialsFromEnv` has the identical `orDie` shape and is unfixed —
|
|
217
|
-
out of scope for this item; tracked as its own follow-up (task `task_3bbe1684`).
|
|
218
|
-
11. **Every family repeats `list: () => Effect.succeed([])`**, 30 times, and only
|
|
219
|
-
`R2BucketLock` and `MeshNode` declare `nuke`. The constructor already defaults `list`
|
|
220
|
-
(S12). **Fix:** write the reason where it differs, and declare `nuke: { skip: true }`
|
|
221
|
-
where nuke must never reach the object.
|
|
222
|
-
12. **Resource JSDoc is not in upstream's generator format.** Zero files use `@resource`,
|
|
223
|
-
`**Example:**` or `### Section` (S31, H10). **Decision:** maintainer, because it moves
|
|
224
|
-
the house's glyph rationale into `//` comments.
|
|
225
|
-
13. **Tests never use `alchemy/Test/Bun`.** Every lifecycle is proven against loopback
|
|
226
|
-
fakes (S28, H12). Live suites need a place to run, and that is a maintainer decision.
|
|
227
|
-
14. **`discord/*` is new (2026-09-24, task-authorized, kit PR TBD) and built directly on
|
|
228
|
-
`@distilled.cloud/discord` — no hand-rolled `client.ts` ever existed to retire.**
|
|
229
|
-
`Discord.ApplicationCommand` and `Discord.GuildApplicationCommand` call the SDK's
|
|
230
|
-
typed operations, `catchTag`'d through the shared `DiscordOpError` union (S21), with no
|
|
231
|
-
status sniffing anywhere in `resource.ts`. It follows upstream's `Snippet.ts` reference
|
|
232
|
-
exactly for a marker-less API (S7, S8): a cold `read` returns `Unowned(attrs)`, and both
|
|
233
|
-
convenience constructors pipe `adopt(true)` (H5) — a stricter posture than `netbox/*`'s
|
|
234
|
-
documented H1 gap, not a repeat of it. Rate-limit handling is the SDK's own default
|
|
235
|
-
`Retry` policy (bounded: `Schedule.recurs(8)`, S26) — this family adds nothing on top.
|
|
236
|
-
Full detail, the live Halibut census and its ownership-handover sequence, and every SDK
|
|
237
|
-
gap: [`discord.md`](./discord.md). ⛔ **Diverges on S24/S25 the same way every distilled
|
|
238
|
-
family in this ledger does:** credentials are read at call time through the SDK's own
|
|
239
|
-
`CredentialsFromEnv`, not a house `alchemy/Auth` provider — unchanged from `netbox/*`
|
|
240
|
-
and `litellm/*`'s entries above. **Gap, not yet fixed:** no vendor constraint table
|
|
241
|
-
(unlike NetBox/Paperless); `options` passed through opaquely rather than modeled from
|
|
242
|
-
the schema (`docs/discord.md#sdk-gaps`).
|
|
243
|
-
|
|
244
|
-
15. **`grafana/*` (added 2026-09-24) ships only `Datasource` — a real SDK gap, not scope-trimming.**
|
|
245
|
-
Measured against the published `@distilled.cloud/grafana@1.0.0-rc.12` tarball's
|
|
246
|
-
`lib/services/grafana.d.ts` (4,938 lines): the package has no create/read/update/delete
|
|
247
|
-
operations for folders (only `updateFolderPermissions` exists), plain dashboards (only
|
|
248
|
-
snapshot/public-dashboard routes exist — no `POST /dashboards/db`, no
|
|
249
|
-
`GET/DELETE /dashboards/uid/{uid}`), alert rules or contact points (both have only a
|
|
250
|
-
`routeGet*Export` read-only route under `/v1/provisioning/`, no create/update/delete). S1
|
|
251
|
-
found no upstream `Alchemy` family for any of these either. Full detail:
|
|
252
|
-
[grafana.md](./grafana.md#the-sdk-gap--why-only-datasource-ships).
|
|
253
|
-
|
|
254
|
-
✅ **S22 fixed 2026-09-24** (branch `claude2/distilled-grafana`): root cause measured to be
|
|
255
|
-
`skipDeprecated: true` dropping 64 operations Grafana's own spec marks `deprecated: true` in
|
|
256
|
-
favor of a separate, undocumented-here Kubernetes-apiserver route — not a spec omission. RFC-6902
|
|
257
|
-
patches un-deprecate exactly the 32 operations this finding named (folders, dashboards,
|
|
258
|
-
alerting-provisioning writes); the other 32 stay excluded. Shipped as
|
|
259
|
-
`@homeflare/distilled-grafana` (0.1.0 pre-release, 0.2.0 once this PR's `minor` changeset
|
|
260
|
-
releases it), aliased onto `@distilled.cloud/grafana` the same S22 route
|
|
261
|
-
`@homeflare/distilled-netbox` established. `Grafana.Datasource`'s props/attributes/generated code
|
|
262
|
-
are unchanged (diffed operation-by-operation against the prior commit).
|
|
263
|
-
|
|
264
|
-
✅ **`Grafana.Folder`/`Grafana.Dashboard` shipped** (PR 245, on top of the same
|
|
265
|
-
`@distilled.cloud/grafana` operations S22 unblocked) — the uid-required doctrine, the
|
|
266
|
-
provisioned-object refusal, the parentUid-is-create-only refusal and the volatile-field
|
|
267
|
-
normalization that makes an unchanged dashboard a true noop are all in
|
|
268
|
-
[grafana-folder-dashboard.md](./grafana-folder-dashboard.md), not repeated here.
|
|
269
|
-
|
|
270
|
-
✅ **`Grafana.ContactPoint`/`Grafana.MuteTiming`/`Grafana.MessageTemplate` shipped** (kit PR
|
|
271
|
-
250, first of three stacked PRs — `Grafana.AlertRuleGroup` then `Grafana.NotificationPolicy`
|
|
272
|
-
follow):
|
|
273
|
-
the foreign-provenance refusal (`alerting-provenance.ts`, an allowlist of Grafana's own
|
|
274
|
-
`ProvenanceNone`/`ProvenanceAPI` values, measured against `pkg/services/ngalert/models` and
|
|
275
|
-
Grafana's v13.1 docs) and the secret-settings-ref seam (`secret-refs.ts`, shared with
|
|
276
|
-
`Grafana.Datasource`) are both new shared building blocks — full detail:
|
|
277
|
-
[grafana-alerting.md](./grafana-alerting.md).
|
|
278
|
-
- 🔴 **New SDK gap found, not fixed here:** `MuteTimeInterval`'s generated type has no
|
|
279
|
-
`provenance`/`version` fields, and the SDK's own `TimeInterval` type is missing the real
|
|
280
|
-
Alertmanager time-interval fields (`weekdays`/`times`/`months`/etc.) entirely — Grafana's real
|
|
281
|
-
API returns and accepts all of them. NOT a data-loss gap: `@distilled.cloud/core`'s response
|
|
282
|
-
decoding never runs a strict schema decode (`JSON.parse` + a key-rename pass that leaves any
|
|
283
|
-
unmodeled key verbatim — `protocol-http.ts`'s `mapKeys`, confirmed by driving the real
|
|
284
|
-
operation through this family's fake-Grafana harness), so `mute-timing.ts` reads/writes both
|
|
285
|
-
through a locally-widened type instead. Same S22 fix route as the other named gaps
|
|
286
|
-
(regenerate the distilled clone's spec, copy forward) — not attempted here, since
|
|
287
|
-
`packages/distilled-grafana/src/` is vendored and never hand-edited.
|
|
288
|
-
- Measured, read-only, same census as PR 245's: `teslamate-grafana` has no contact points, mute
|
|
289
|
-
timings or templates configured either (consistent with the alerting-provisioning gap having
|
|
290
|
-
been real, not merely unexploited).
|
|
291
|
-
|
|
292
|
-
✅ **`Grafana.AlertRuleGroup` shipped** (kit PR 254, second of three, branched fresh off `main`
|
|
293
|
-
after PR 250 merged): group, not per-rule, is the unit — `PUT .../rule-groups/{Group}` owns
|
|
294
|
-
evaluation `interval` and the full ordered `rules[]` together, which per-rule operations
|
|
295
|
-
cannot express (no `interval` field, no reordering). Rule order is significant, proven by a
|
|
296
|
-
dedicated test. The per-rule provenance refusal (each `ProvisionedAlertRule.provenance`, since
|
|
297
|
-
the group type itself carries none) reuses `alerting-provenance.ts` unchanged, including the
|
|
298
|
-
missing-means-foreign fix from PR 250's review. A declared group whose folder does not exist
|
|
299
|
-
refuses as a typed `BadRequest` (`RoutePutAlertRuleGroupError` has no `NotFound` case) rather
|
|
300
|
-
than being auto-created — this resource never calls `Grafana.Folder`'s create path. A whole-group
|
|
301
|
-
write silently dropping a live rule not in the declaration (found by that PR's own adversarial
|
|
302
|
-
review) is now warned about loudly rather than silent, never a refusal. Full detail:
|
|
303
|
-
[grafana-alerting-rules.md](./grafana-alerting-rules.md).
|
|
151
|
+
## Remaining family findings
|
|
304
152
|
|
|
305
|
-
|
|
306
|
-
branched fresh off `main` after PR 254 merged): a SINGLETON — `fetchLive` never returns
|
|
307
|
-
`undefined` (`RouteGetPolicyTreeError` has no `NotFound` case), so this resource has no create
|
|
308
|
-
or delete in the sense every sibling resource does, only adopt-and-update. `destroy` never
|
|
309
|
-
calls `routeResetPolicyTree` without an explicit `allowReset: true` prop, a gate independent of
|
|
310
|
-
(and in addition to) `defaultRemovalPolicy: 'retain'` — two gates, since resetting wipes the
|
|
311
|
-
WHOLE instance's routing back to Grafana's bare default, not one object. The tree is opaque
|
|
312
|
-
`Record<string, unknown>` (same reasoning as `mute-timing.ts`'s `timeIntervals` — Grafana's
|
|
313
|
-
matcher-expression types are complex unions this family does not hand-model), compared via
|
|
314
|
-
`subset-match.ts` for Grafana-injected-default tolerance, with route order significant. The
|
|
315
|
-
same dropped-item warning kit PR 254 introduced for rule groups is mirrored here for dropped
|
|
316
|
-
routes. Full detail: [grafana-notification-policy.md](./grafana-notification-policy.md).
|
|
153
|
+
The remaining sequence is split into smaller documents to keep each below 200 lines.
|
|
317
154
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
`alchemy/Auth` provider (S24) — here the house's own `grafanaCredentials(target)`, not the
|
|
322
|
-
SDK's `CredentialsFromEnv`, because more than one Grafana instance exists on this estate
|
|
323
|
-
(grafana.md's own Credentials section).
|
|
324
|
-
- Measured, read-only, on `teslamate-grafana.service` (CT100, `teslamate/grafana:4.2.0`,
|
|
325
|
-
Grafana 13.1.3): one datasource (file-provisioned, not API-managed — a live example this
|
|
326
|
-
family COULD adopt), dashboards baked into the image (not API-managed either way), and no
|
|
327
|
-
folders, alert rules or contact points configured — consistent with the gap being real
|
|
328
|
-
rather than merely unexploited. No stack yet imports `@homeflare/alchemy/grafana`.
|
|
155
|
+
[Findings 7–14](./upstream-conformance-families.md) cover Proxmox, Talos, NetBox,
|
|
156
|
+
LiteLLM, list/JSDoc/test conventions and Discord. [Finding 15: Grafana](./upstream-conformance-grafana.md)
|
|
157
|
+
records the SDK migration, shipped resources and the scoped deprecated-API departure.
|
|
329
158
|
|
|
330
159
|
## Conforms
|
|
331
160
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
typed operations, `catchTag('NotFound', …)` replaced the status-carrying `PaperlessError`
|
|
336
|
-
union, and `client.ts`/`errors.ts`/`credentials.ts` are deleted. The shared
|
|
337
|
-
`matching.ts`/`matching-locate.ts` engine (locate-by-name before state, by `output.id` after —
|
|
338
|
-
PR 163) is unchanged in shape. Not published upstream yet, aliased onto
|
|
339
|
-
`@homeflare/distilled-paperless-ngx@0.3.0` — [distilled-interim.md](./distilled-interim.md).
|
|
340
|
-
State did not move: props/attributes stay byte-identical, proven by `tag.test.ts` and the full
|
|
341
|
-
PR 163 regression suite (`tag-identity.test.ts`) against a fake exercising the real distilled
|
|
342
|
-
protocol. ⛔ **Still diverges on credentials (S24):** `PAPERLESS_URL`/`PAPERLESS_TOKEN` are read
|
|
343
|
-
at call time, now through the SDK's `CredentialsFromEnv` — same divergence, unchanged by the
|
|
344
|
-
transport swap.
|
|
345
|
-
- **`github/RepositoryRuleset`** (added 2026-09-23) closes the hazard the row above names:
|
|
346
|
-
probes by name (`read`/`reconcile` both), answers `Unowned` on a cold name match (H1),
|
|
347
|
-
normalizes before comparing so a matching live ruleset is a true noop, and refuses
|
|
348
|
-
(rather than silently drops) a live rule type it does not model. Two recorded
|
|
349
|
-
divergences: the type string sits in upstream's own `GitHub` namespace (H14), and the
|
|
350
|
-
client is Octokit rather than distilled because distilled's `S.Struct` request schemas
|
|
351
|
-
have no field for `require_extra_approval_for_unattributed_changes` (H15) — see
|
|
352
|
-
[repository-ruleset.md](./repository-ruleset.md). `declareRepoPolicy` is rewired onto
|
|
353
|
-
it; the row below is now describing upstream `GitHub.Ruleset` itself, which this family
|
|
354
|
-
does not touch and remains usable directly.
|
|
355
|
-
- **`github/declareRepoPolicy`** composes upstream `GitHub.Repository` and
|
|
356
|
-
`GitHub.Ruleset`. Its one hazard is upstream's own: `Ruleset.read` returns `undefined`
|
|
357
|
-
without prior output, so a first deploy creates a duplicate
|
|
358
|
-
([repo-policy.md](./repo-policy.md)). The fix belongs upstream: read by name and answer
|
|
359
|
-
`Unowned`. Since 2026-09-23 `declareRepoPolicy` no longer takes this path (see the
|
|
360
|
-
`RepositoryRuleset` row above); this remains accurate for upstream `Ruleset` on its own.
|
|
361
|
-
- **`cloudflare/website.ts`** sets house defaults over upstream `Website.Astro` and
|
|
362
|
-
`Website.Vite`.
|
|
363
|
-
- **`ownership/*`** is built only on `AdoptPolicy`, `Stack`, `State` and `Artifacts`. Its
|
|
364
|
-
apply-time re-check (H2) exists because beta.79 skips the adoption probe while props hold
|
|
365
|
-
an Output. That fix also belongs in the engine upstream.
|
|
366
|
-
- **`verify/*`** (`hf-adopt-verify`) runs Alchemy's own planner. It exists because beta.79
|
|
367
|
-
labels every cold adoption as an update (H6).
|
|
368
|
-
- **`google-workspace/*`** (new 2026-09-24, `Group`, `GroupMember`, `DomainAlias`, `OrgUnit`)
|
|
369
|
-
is built directly on `@distilled.cloud/google-workspace@1.0.0-rc.12`'s typed
|
|
370
|
-
`unstable/admin_directory_v1` operations from the first commit — no hand-rolled client ever
|
|
371
|
-
existed for it to migrate off (S23), so there is no `client.ts` deletion to record. Every
|
|
372
|
-
`fetchLive` is get-by-key with its own `catchTag('NotFound', …)`, the same split forgejo's and
|
|
373
|
-
netbox's engines use (resource.ts). Get-by-key rather than NetBox's list-then-disambiguate,
|
|
374
|
-
since Directory addresses every object this family models by a stable key. ⛔ **Diverges on
|
|
375
|
-
credentials, same shape as the rest of this ledger (S24):** `GOOGLE_ACCESS_TOKEN` is read at
|
|
376
|
-
call time through the SDK's own `CredentialsFromEnv` — but here the SDK provides no way to
|
|
377
|
-
MINT that token at all (no service-account/DWD support), so a Bun wrapper outside this
|
|
378
|
-
package (H8, unwritten by this PR) is load-bearing in a way NetBox's/Forgejo's simple
|
|
379
|
-
API-token env vars are not. SDK gaps (the `unstable/` service tree, no per-operation error
|
|
380
|
-
beyond the shared 4xx set, no group-alias update) and the full credential/scope setup:
|
|
381
|
-
[google-workspace.md](./google-workspace.md).
|
|
161
|
+
[Conforming building blocks](./upstream-conformance-conforms.md) records the measured
|
|
162
|
+
conformance and remaining exceptions for Paperless, GitHub, ownership, verify and
|
|
163
|
+
Google Workspace.
|
|
382
164
|
|
|
383
165
|
## The Bun line
|
|
384
166
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
files under `src/` that call `Bun.*` or import `node:*`/`bun:*`. Six of them are outside
|
|
388
|
-
S42's scope:
|
|
389
|
-
|
|
390
|
-
- four loopback fakes (`caddy/fake-caddy.ts`, `openbao/fake-bao.ts`, `proxmox/fake-pve.ts`,
|
|
391
|
-
`proxmox/fake-pve-lxc.ts`), which are test-side under S44;
|
|
392
|
-
- `proxmox/provision-cli-fake.ts`, which is used only by tests;
|
|
393
|
-
- `verify/args.ts`, which belongs to the CLI and so is tooling under S43.
|
|
394
|
-
|
|
395
|
-
That leaves 17 files. The table lists them, plus `provision-cli-fake.ts`, which should move
|
|
396
|
-
out of `src/`. The six out-of-scope files still ship in the tarball's `src/`, but no export
|
|
397
|
-
reaches them.
|
|
398
|
-
⚠️ The first version of this table said 16 files and left out `launchd/job-form.ts`.
|
|
399
|
-
⚠️ Corrected 2026-09-22: 14 of the 17 break upstream's rule. `launchd/job-form.ts`,
|
|
400
|
-
`proxmox/write-only.ts` and `proxmox/pbs-notification-target-wire.ts` use only synchronous
|
|
401
|
-
`node:crypto` or `Buffer`. Upstream allows those inside `Effect.sync`
|
|
402
|
-
(`AGENTS.md@v2.0.0-beta.79#Workflow`), and 40 upstream `src/` files import `node:crypto`,
|
|
403
|
-
among them `Fly/Secret.ts` and `Railway/Variable.ts`. They were listed under a blanket
|
|
404
|
-
`node:*` ban that upstream does not have.
|
|
405
|
-
|
|
406
|
-
| file | API | exp | portable replacement |
|
|
407
|
-
| ---------------------------------------------------------- | -------------------------------------------- | --- | -------------------------------------------- |
|
|
408
|
-
| `openbao/cloudflare-roles-config.ts` | `Bun.YAML` | yes | parse on the tooling side; pass data in |
|
|
409
|
-
| `openbao/digest.ts` (14 importers) | `Bun.CryptoHasher` | — | `Crypto.digest` + hex; keep `canonical()` ⚠️ |
|
|
410
|
-
| `openbao/cloudflare-parity-snapshot.ts` | `Bun.file`, `Bun.Glob` | — | `FileSystem` + `Path` |
|
|
411
|
-
| `openbao/approle-login-form.ts` | `Bun.file` | — | `FileSystem.readFileString` |
|
|
412
|
-
| `openbao/approle-login-result.ts` | `Bun.inspect` | — | a plain formatter |
|
|
413
|
-
| `openbao/forgejo-bootstrap.ts` | `Bun.spawn` | — | unreferenced; `ChildProcessSpawner` or drop |
|
|
414
|
-
| `talos/credentials.ts` | `Bun.write/file/env/randomUUIDv7`, `node:fs` | — | `FileSystem.makeTempFileScoped` |
|
|
415
|
-
| `talos/kubeconfig.ts` | `Bun.file` | yes | `FileSystem` |
|
|
416
|
-
| `talos/talos-machine-config.ts` | `Bun.file` | yes | `FileSystem` |
|
|
417
|
-
| `talos/values.ts` | `Bun.YAML`, `node:crypto` | — | tooling-side parse; hash may stay |
|
|
418
|
-
| `launchd/local-runner.ts` | `node:child_process`, `node:fs/promises` | yes | H3 above |
|
|
419
|
-
| `launchd/sudo-stage.ts` | `node:fs/promises`, `node:os`, `node:path` | — | `FileSystem`, `Path` |
|
|
420
|
-
| `launchd/job-form.ts` | `node:crypto` (`createHash`) | — | allowed in `Effect.sync` |
|
|
421
|
-
| `linux/ssh-runner.ts` | `node:child_process`, `node:crypto` | yes | H3 above |
|
|
422
|
-
| `caddy/caddy-http-client.ts` | `node:http`, `node:stream` (unix socket) | yes | inherent — an Effect `HttpClient`, not H3 ⚠️ |
|
|
423
|
-
| `proxmox/write-only.ts`, `pbs-notification-target-wire.ts` | `node:crypto`, `node:buffer` | — | allowed in `Effect.sync` ⚠️ |
|
|
424
|
-
| `proxmox/provision-cli-fake.ts` | `Bun.spawn`, `node:fs` | — | test-only; move out of `src/` |
|
|
425
|
-
|
|
426
|
-
The ⚠️ rows:
|
|
427
|
-
|
|
428
|
-
- `openbao/digest.ts`: every `Bao.*` family persists this digest in state, so a swap must
|
|
429
|
-
hash the same bytes (`canonical()`, lowercase hex), or every row plans an update.
|
|
430
|
-
`Crypto.digest` from `effect/Crypto` is in every stack's services and fails with a typed
|
|
431
|
-
error; `alchemy/Util/sha256` wraps WebCrypto in `Effect.promise`.
|
|
432
|
-
- `proxmox/write-only.ts`: a salted scrypt seal with `timingSafeEqual`. No Alchemy helper
|
|
433
|
-
replaces it. A plain `sha256` would make a write-only secret cheap to brute-force from
|
|
434
|
-
state, and it would change the persisted seal format.
|
|
435
|
-
- `caddy/caddy-http-client.ts`: unlike `launchd/local-runner.ts` and `linux/ssh-runner.ts`
|
|
436
|
-
(H3, plain `async` interfaces), this is already the Effect `HttpClient.HttpClient` the
|
|
437
|
-
distilled SDK's own protocol runs on — `node:http` here is the same kind of unavoidable,
|
|
438
|
-
bottom-of-the-stack platform adapter `@effect/platform-node`'s own `NodeHttpClient.ts` is,
|
|
439
|
-
extended with `socketPath` dialing (that module has none) because a unix socket is the
|
|
440
|
-
measured transport a local Caddy needs and neither `FetchHttpClient` (Bun-only `unix`
|
|
441
|
-
option) nor stock `NodeHttpClient` (URL-only) speaks one on both Bun and Node. There is no
|
|
442
|
-
more-portable replacement to point to; the file already conforms to S19 (no async/await,
|
|
443
|
-
no raw `Promise` — `Effect.callback`/`Effect.tryPromise` throughout).
|
|
444
|
-
|
|
445
|
-
Test runners: 51 files import `node:test`/`node:assert` (openbao 42, proxmox 7, forgejo 1,
|
|
446
|
-
talos 1), and 126 import `bun:test`. S43 says `bun:test`.
|
|
167
|
+
[The Bun line inventory](./upstream-conformance-bun.md) preserves the runtime-boundary
|
|
168
|
+
measurements, portable replacements and state-format constraints.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homeflare/alchemy",
|
|
3
|
-
"version": "0.37.
|
|
3
|
+
"version": "0.37.2",
|
|
4
4
|
"description": "Custom Alchemy providers for Cloudflare, Proxmox, OpenBao, Forgejo, Talos, launchd and Caddy, and a verifier that proves adoptions are no-ops.",
|
|
5
5
|
"homepage": "https://github.com/taslabs-net/homeflare-kit/tree/main/packages/alchemy#readme",
|
|
6
6
|
"bugs": "https://github.com/taslabs-net/homeflare-kit/issues",
|
|
@@ -124,8 +124,8 @@
|
|
|
124
124
|
"@distilled.cloud/netbox": "npm:@homeflare/distilled-netbox@0.3.0",
|
|
125
125
|
"@distilled.cloud/opnsense": "npm:@homeflare/distilled-opnsense@0.2.0",
|
|
126
126
|
"@distilled.cloud/paperless-ngx": "npm:@homeflare/distilled-paperless-ngx@0.3.0",
|
|
127
|
-
"@distilled.cloud/proxmox": "npm:@homeflare/distilled-proxmox@0.3.
|
|
128
|
-
"@distilled.cloud/proxmox-backup": "npm:@homeflare/distilled-proxmox-backup@0.3.
|
|
127
|
+
"@distilled.cloud/proxmox": "npm:@homeflare/distilled-proxmox@0.3.1",
|
|
128
|
+
"@distilled.cloud/proxmox-backup": "npm:@homeflare/distilled-proxmox-backup@0.3.1",
|
|
129
129
|
"@distilled.cloud/unifi-network": "npm:@homeflare/distilled-unifi-network@0.2.0"
|
|
130
130
|
},
|
|
131
131
|
"peerDependencies": {
|
|
@@ -4,11 +4,9 @@
|
|
|
4
4
|
* `{node,id}` / `{node,name}`) — the SAME reason `DAEMON_ENDPOINTS` (ceph-daemon-form.ts) is a
|
|
5
5
|
* per-kind table rather than one templated string.
|
|
6
6
|
*
|
|
7
|
-
* ⛔
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* with no `output`-branching, so `readDaemon` below reproduces that exact single-fold shape
|
|
11
|
-
* rather than gaining the newer dual-path pattern along the way.
|
|
7
|
+
* ⛔ ABSENCE IS A SUCCESSFUL INDEX WITH NO MATCHING ROW. The original transport swap
|
|
8
|
+
* preserved `pveOperations.read`'s catch-all fold; the 2026-09-24 follow-up removes it. A failed
|
|
9
|
+
* mon/mgr/mds list proves nothing about whether a daemon exists, so its SDK error propagates.
|
|
12
10
|
*
|
|
13
11
|
* ⚠️ `mon_address` IS THE ONE RENAME THIS FAMILY NEEDS — distilled's generator maps the wire name
|
|
14
12
|
* `mon-address` to `mon_address` on `UpdateNodeCephMonRequest` (`T.Body("mon-address")`,
|
|
@@ -48,11 +46,10 @@ const listRows = (props: CephDaemonProps) => {
|
|
|
48
46
|
}
|
|
49
47
|
};
|
|
50
48
|
|
|
51
|
-
/**
|
|
49
|
+
/** A missing row is absent; a failed index read keeps its typed SDK error. */
|
|
52
50
|
export const readDaemon = (props: CephDaemonProps) =>
|
|
53
51
|
listRows(props).pipe(
|
|
54
52
|
Effect.map((rows) => daemonAttributes(rows as unknown as Record<string, unknown>, props)),
|
|
55
|
-
Effect.orElseSucceed(() => undefined),
|
|
56
53
|
);
|
|
57
54
|
|
|
58
55
|
/** POST — distilled's generator misnames it "update" (checked against `T.Http`); it is a create. */
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
* this credential already has it rather than discovering it the next time something is scoped.
|
|
48
48
|
*
|
|
49
49
|
* ★ MIGRATED OFF `client.ts` (2026-09-24, decision 43, 2c) — ceph-daemon-wire.ts has the rename
|
|
50
|
-
* and why no protocol gap blocks any of the three kinds
|
|
51
|
-
*
|
|
50
|
+
* and why no protocol gap blocks any of the three kinds. The follow-up removes the preserved
|
|
51
|
+
* catch-all read fold: only a successful index without this daemon proves absence.
|
|
52
52
|
*/
|
|
53
53
|
import { Resource } from 'alchemy';
|
|
54
54
|
import { isResolved } from 'alchemy/Diff';
|
|
@@ -74,12 +74,11 @@ export interface CephDaemonProps extends WithTarget {
|
|
|
74
74
|
/**
|
|
75
75
|
* The node the daemon RUNS on. Identity — a daemon cannot be moved, only destroyed and rebuilt.
|
|
76
76
|
* ⚠️ It is also the node whose API is asked, and the read is only as available as that node: a
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* but the message will point at the daemon rather than at the node that would not answer.
|
|
77
|
+
* failed index now propagates its error. Before the typed-read follow-up, a down node,
|
|
78
|
+
* missing Ceph installation or 403 was folded to "absent"; its daemons planned `update`
|
|
79
|
+
* and reconcile POSTed a create for a mon that already existed. PVE refused with "monitor
|
|
80
|
+
* already exists", then read-back failed again, obscuring the original node read error.
|
|
81
|
+
* That incident is why collection failure can never prove a daemon is missing.
|
|
83
82
|
*/
|
|
84
83
|
node: string;
|
|
85
84
|
/**
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* by ceph-fs-write.test.ts and ceph-adopt.test.ts, `destroyFs` alone is unchanged from main, but
|
|
7
|
-
* the two composed together — on a destructive, hard-to-reverse `RemovalPolicy.destroy()` — had
|
|
8
|
-
* no coverage of the seam itself.
|
|
2
|
+
* The CephFS destroy lifecycle through the real SDK protocol and Alchemy engine.
|
|
3
|
+
* The original transport walk left DELETE on the hand client; SDK 0.3.0's query
|
|
4
|
+
* binding now permits the whole sequence to move. These tests keep the destructive
|
|
5
|
+
* read/delete/poll/read-back boundary covered without touching a real filesystem.
|
|
9
6
|
*/
|
|
10
7
|
import { describe, expect, test } from 'bun:test';
|
|
11
8
|
import * as RemovalPolicy from 'alchemy/RemovalPolicy';
|
|
@@ -27,7 +24,7 @@ const declared = () => ProxmoxCephFs(NAME, { name: NAME, node: NODE, target: FAK
|
|
|
27
24
|
describe('destroying a CephFS', () => {
|
|
28
25
|
test(
|
|
29
26
|
'RemovalPolicy.destroy() then undeclaring: readFs (distilled) finds it, destroyFs ' +
|
|
30
|
-
'(
|
|
27
|
+
'(distilled) removes it, and the distilled read-back confirms it is gone',
|
|
31
28
|
async () => {
|
|
32
29
|
let destroyed = false;
|
|
33
30
|
const fake = fakePve((call: PveCall) => {
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/** The real SDK must send destructive flags in the query and fold only its missing-FS tag. */
|
|
2
|
+
import { describe, expect, test } from 'bun:test';
|
|
3
|
+
import * as Effect from 'effect/Effect';
|
|
4
|
+
import * as Layer from 'effect/Layer';
|
|
5
|
+
import * as FetchHttpClient from 'effect/unstable/http/FetchHttpClient';
|
|
6
|
+
import { destroyFs } from './ceph-fs-distilled.ts';
|
|
7
|
+
import { FAKE_TARGET, withoutBao } from './fake-pve.ts';
|
|
8
|
+
|
|
9
|
+
const props = { name: 'oldfs', node: 'node-b', target: FAKE_TARGET };
|
|
10
|
+
const UPID = 'UPID:node-b:fake:destroyfs';
|
|
11
|
+
const fixture = (answer: () => Response) => {
|
|
12
|
+
const calls: { method: string; path: string; body: string }[] = [];
|
|
13
|
+
const stub = async (input: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => {
|
|
14
|
+
const request =
|
|
15
|
+
input instanceof Request ? new Request(input, init) : new Request(String(input), init);
|
|
16
|
+
const url = new URL(request.url);
|
|
17
|
+
if (url.pathname.includes('/creds/')) {
|
|
18
|
+
return Response.json({
|
|
19
|
+
data: { secret: 'fake-secret-not-real', token_id: 'hf-test@pve!fake' },
|
|
20
|
+
lease_duration: 0,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
calls.push({
|
|
24
|
+
method: request.method,
|
|
25
|
+
path: url.pathname + url.search,
|
|
26
|
+
body: await request.text(),
|
|
27
|
+
});
|
|
28
|
+
return request.method === 'DELETE'
|
|
29
|
+
? answer()
|
|
30
|
+
: Response.json({ data: { status: 'stopped', exitstatus: 'OK' } });
|
|
31
|
+
};
|
|
32
|
+
const layer = FetchHttpClient.layer.pipe(
|
|
33
|
+
Layer.provideMerge(
|
|
34
|
+
Layer.succeed(
|
|
35
|
+
FetchHttpClient.Fetch,
|
|
36
|
+
Object.assign(stub, { preconnect: globalThis.fetch.preconnect }) as typeof fetch,
|
|
37
|
+
),
|
|
38
|
+
),
|
|
39
|
+
);
|
|
40
|
+
return { calls, layer };
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
describe('CephFS distilled delete', () => {
|
|
44
|
+
test('destructive flags reach the query, never the DELETE body, and the worker is polled', async () => {
|
|
45
|
+
const fake = fixture(() => Response.json({ data: UPID }));
|
|
46
|
+
await withoutBao(() =>
|
|
47
|
+
Effect.runPromise(
|
|
48
|
+
destroyFs({ ...props, 'remove-pools': true, 'remove-storages': true }).pipe(
|
|
49
|
+
Effect.provide(fake.layer),
|
|
50
|
+
),
|
|
51
|
+
),
|
|
52
|
+
);
|
|
53
|
+
expect(fake.calls[0]).toEqual({
|
|
54
|
+
method: 'DELETE',
|
|
55
|
+
path: '/api2/json/nodes/node-b/ceph/fs/oldfs?remove-pools=1&remove-storages=1',
|
|
56
|
+
body: '',
|
|
57
|
+
});
|
|
58
|
+
expect(fake.calls[1]?.path).toBe(
|
|
59
|
+
`/api2/json/nodes/node-b/tasks/${encodeURIComponent(UPID)}/status`,
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('omitted or false flags preserve the safe defaults', async () => {
|
|
64
|
+
const fake = fixture(() => Response.json({ data: UPID }));
|
|
65
|
+
await withoutBao(() =>
|
|
66
|
+
Effect.runPromise(
|
|
67
|
+
destroyFs({ ...props, 'remove-pools': false }).pipe(Effect.provide(fake.layer)),
|
|
68
|
+
),
|
|
69
|
+
);
|
|
70
|
+
expect(fake.calls[0]).toEqual({
|
|
71
|
+
method: 'DELETE',
|
|
72
|
+
path: '/api2/json/nodes/node-b/ceph/fs/oldfs',
|
|
73
|
+
body: '',
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('a race that already removed the filesystem is idempotent without a task poll', async () => {
|
|
78
|
+
const fake = fixture(() =>
|
|
79
|
+
Response.json({ data: null, message: "no such cephfs 'oldfs'\n" }, { status: 500 }),
|
|
80
|
+
);
|
|
81
|
+
await withoutBao(() => Effect.runPromise(destroyFs(props).pipe(Effect.provide(fake.layer))));
|
|
82
|
+
expect(fake.calls).toHaveLength(1);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
for (const status of [401, 403, 500]) {
|
|
86
|
+
test(`an unrelated HTTP ${String(status)} fails instead of claiming deletion`, async () => {
|
|
87
|
+
const fake = fixture(() =>
|
|
88
|
+
Response.json({ data: null, message: 'refused or unavailable' }, { status }),
|
|
89
|
+
);
|
|
90
|
+
await withoutBao(async () => {
|
|
91
|
+
await expect(
|
|
92
|
+
Effect.runPromise(destroyFs(props).pipe(Effect.provide(fake.layer))),
|
|
93
|
+
).rejects.toBeDefined();
|
|
94
|
+
});
|
|
95
|
+
expect(fake.calls).toHaveLength(1);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
});
|