fix(cli): percent-encode literal colons in :action-suffixed paths - #47
Merged
Conversation
Every :action-suffixed public operation (clusters.resume,
machines.suspend, installs.previewHostnames.bindFloating, and every
other REST-RPC-style route) failed client-side with "Missing path
parameter: resume" before any request was built.
Root cause: OpenAPI paths like /clusters/{id}:resume rewrite to
Effect's Express-style template /clusters/:id:resume. Effect's
HttpApiClient.compilePath (node_modules/effect/src/unstable/httpapi/
HttpApiClient.ts) matches every occurrence of :word in a compiled
path as a substitutable parameter, with no way to tell a literal
colon from a parameter marker — it independently matched both :id
and :resume, then threw because no "resume" value was ever supplied.
Fix lives in the generator, not generated CLI code, per this repo's
"add support to the OpenAPI producer or generator; never hide a
contract gap with a manual endpoint implementation" rule. Patched
@effect/openapi-generator's toHttpApiPath (both dist and src, via
`bun patch`) to percent-encode literal colons in the raw OpenAPI path
before rewriting {param} to :param, so only the colons we just
introduced remain unescaped. Effect's compilePath then treats the
%3A-encoded action suffix as inert literal text instead of a second
path parameter, reconstructing the exact original path on request.
Rationale: a colon substituted through Effect's params regex can
never survive as a literal ':' in the output — compilePath's
replacer always emits `${slash}${encodeURIComponent(value)}`, which
strips the matched colon and encodeURIComponent-escapes whatever
value it's given. The only way to get an un-mangled literal colon
into the compiled URL is for it to never match the params regex in
the first place, which requires it not be a bare `:` followed by a
word character anywhere in the template.
Rejected: supplying the action name as a synthetic second path
param (e.g. `{ id, resume: "resume" }`) — traced through
compilePath's actual replace logic and confirmed by direct testing
that this drops the separating colon entirely
(`/clusters/clu_xxxresume`, not `/clusters/clu_xxx:resume`),
so it cannot reproduce the real request. Bypassing Effect's
generic client and hand-building requests for these routes — the CLI
has no custom request-building layer today (PublicApiClientLive uses
HttpApiClient.make(PublicApi, ...) directly); adding one purely to
work around this would be a much larger, generator-contract-violating
change for a problem the generator itself can encode correctly.
Verified server compatibility by testing this repo's Hono router
(the same pattern this monorepo already uses server-side in
packages/api-runtime/src/colon-method-params.ts to solve the mirror
problem on the routing side): a literal `:resume` suffix and a
percent-encoded `%3Aresume` suffix produce an identical captured
param value, confirming Hono decodes %3A before route matching, so
the request is unchanged on the wire.
Tested: bun test (167 pass, including two new end-to-end regression
tests exercising clusters.resume and machines.resume through the
real generatedCommandView command-execution path with an intercepted
fetch, asserting the exact compiled request URL); confirmed RED
("Missing path parameter: resume") before the patch and GREEN after;
bun run generate (regenerated src/generated/openapi-api.gen.ts,
touching every :action-suffixed operation in the public API, proving
the fix is systemic); bun run generate:check; bun run build;
mise run check.
Every test file imported `describe`/`expect`/`test` from `bun:test`,
using raw `Effect.runPromise`/`await` to execute Effect programs under
test. That's the one place in this repo not on the Effect-first model
AGENTS.md requires for src/ and scripts/, and it left no path to
Effect-native testing primitives (it.effect, TestClock) for a codebase
that is entirely Effect-based end to end.
Researched a Bun-native alternative first per the ask: two community
packages exist (effect-bun-test by cevr, @domir/bun-test by DomiR),
both single-maintainer, 0 and 3 GitHub stars, last published Mar/May
2026 with no subsequent activity. Neither is endorsed by the Effect-TS
GitHub org or referenced anywhere in node_modules/effect/AGENTS.md,
which documents only @effect/vitest for testing Effect programs. Not
a reasonable bar for "official or reputable" given this CLI ships to
users. Sibling monorepo akua-dev/cnap already standardized on
@effect/vitest + a real vitest runner for the identical reason
(packages/config/vitest/domain.ts) — used that as prior art.
Installed @effect/vitest@4.0.0-beta.106 (exact peer match for this
repo's pinned effect@4.0.0-beta.106) and vitest@^4.1.0. `bun run test`
now runs `vitest run`; mise.toml's `test` task called `bun test`
directly (bypassing package.json), so it's repointed at `bun run test`
too. Files with no Effect execution (pure sync helpers, TS-AST/file
assertions) import plain `describe`/`expect`/`test` from vitest, which
@effect/vitest re-exports unchanged; files that run an Effect use
@effect/vitest's `it.effect` with `Effect.gen`/`yield*` bodies instead
of `await Effect.runPromise(...)`.
vitest's worker pool runs under real Node, not Bun, even when invoked
via `bun run vitest` — confirmed by probing `typeof Bun` (undefined)
and `process.execPath` (resolves to the mise-installed node binary) in
both `threads` and `forks` pool modes. That broke every test and one
production helper that called `Bun.spawn`/`Bun.spawnSync`/`Bun.file`/
`Bun.sleep` directly:
- Added test/bun-binary.ts + test/run-akua.ts, consolidating the two
near-duplicate `runAkua` helpers (cli.test.ts,
strict-effect-control-flow.test.ts) into one, spawning the real
`akua` CLI via node:child_process.spawnSync against the bun binary
resolved from `npm_execpath` (set by `bun run`/`npm run` to the
fully resolved binary, avoiding a mise-shim hop per spawn — same
intent as the code this replaces, just runtime-independent).
- release.test.ts and effect-generator-patch.test.ts: swapped
Bun.spawn/spawnSync/file/sleep for node:child_process.spawnSync,
node:fs.existsSync, and node:timers/promises.setTimeout.
- scripts/runtime/release-host-live.ts's runCommand (a `-live.ts`
boundary module, so Bun usage there was AGENTS.md-compliant) used
Bun.spawnSync to shell out during release packaging/verification;
7 release.test.ts cases exercise it in-process. Swapped to
node:child_process.spawnSync with an explicit 64 MB maxBuffer
(Node's 1 MB default is tight for tar/listing output; Bun's
spawnSync had no such ceiling). Pure subprocess-spawning
equivalence, not a Bun-specific fast path, so this is safe even when the compiled CLI itself still runs under Bun in production.
Scoped the raw Promise/throw ban per the CNAP prior art rather than
applying it blindly: CNAP fences `*.effect.ts` production files (not
`*.effect.test.ts` test bodies) against `new Promise`/`throw`/`try`/
`async`/`await`, and this repo already has two TS-AST invariant tests
(test/production-effect-invariants.test.ts,
test/strict-effect-control-flow.test.ts) enforcing the equivalent,
broader ban across all of src/ and scripts/ — broader because this
whole CLI, not just suffixed files, is Effect-first. Left those tests
untouched; test/ stays uncovered by the ban, matching CNAP's own
choice not to fence test bodies. What's left there is legitimate
process-boundary glue that has no Effect replacement: mocking the
`fetch` global's Promise-returning contract (device-http.test.ts,
fetch-openapi.test.ts), and a Promise-shaped AuthTestDependencies test
double interface (auth-test-layer.ts) matching Node's async I/O
contracts. Converted every Effect.runPromise/runPromiseExit call
inside a test body to it.effect + yield* wherever it wasn't itself a
boundary helper; generated-command.test.ts's runGenerated helper
follows the same already-established pattern as runAkua (a contained,
reused Promise-returning subprocess/fetch-mock boundary), so its
internals and two direct Stream.runCollect(...) call sites were left
alone rather than force-fit into Effect.gen for no readability gain.
skills/effect-v4/SKILL.md's verification snippet and Types-and-tests
section now say `bun run test` (not `bun test`) and document the
it.effect/@effect/vitest split; updated the accompanying
effect-v4-skill.test.ts substring assertion to match.
Rationale: keeps this CLI's test suite on the same control-flow model
as its production code, without silently regressing test coverage or
inventing a fence that fights how test code legitimately talks to
Promise-shaped host boundaries.
Tested: bun run test (vitest run) — 19 files, 167 tests pass, same
count as bun:test before this change; bun run generate:check;
bun run build; mise run check (generate:check + build + test), all
green.
Moves the CLI off the beta.106 pre-release track onto the release candidate line now that effect, @effect/openapi-generator, @effect/platform-node, and @effect/vitest all publish matching 4.0.0-rc.* versions together (confirmed via npm registry metadata: all four shipped on 2026-08-14). 4.0.0-rc.110 exists but is excluded by this machine's global Bun install guardrail (~/.bunfig.toml minimumReleaseAge = 259200s / 3 days, published 2026-08-17, ~1.5 days old at bump time). Used 4.0.0-rc.109 instead, the newest RC that clears the 3-day supply-chain window for every package in the set. Regenerated both committed patches (patches/effect@*.patch, patches/@effect%2Fopenapi-generator@*.patch) against the rc.109 tarballs via `bun patch`/`bun patch --commit`, after first diffing beta.106 vs rc.109 upstream sources: every file either patch touches (HttpApiTransformer.ts, OpenApiGenerator.ts, HttpApiEndpoint.ts, CliOutput.ts, toCodeDocument.ts, and their dist/ builds) is byte- identical between the two versions, so the underlying colon-suffixed- path fix from #46 and the pre-existing type/response-header patches still apply unchanged; the regenerated patch files differ from the old ones only in git blob index hashes and bun's diff-context formatting, not in the actual patched code. Updated the two other hardcoded beta.106 references outside package.json/bun.lock: skills/effect-v4/SKILL.md's audited-version line and its regression assertion in test/effect-v4-skill.test.ts. Left docs/superpowers/plans/2026-08-12-openapi-effect-executor.md untouched as a historical decision record. Rationale: keep the CLI on the same effect release track the rest of the org is adopting, without jumping past this machine's minimum- release-age security gate, which exists specifically to blunt just-published supply-chain compromises. Risk: low. No source changes in src/ or scripts/; the fix and both patches are unchanged in content, `bun run generate` reproduced src/generated/openapi-api.gen.ts byte-for-byte (zero diff), and the full suite is green. Tested: bun run generate (no diff vs. committed generated output); bun run generate:check; bun run build (tsc --noEmit + bundle); bun run test (vitest run) - 19 files, 167 tests pass, same count as before the bump, including the two clusters.resume/machines.resume colon-suffix regression tests and both production-effect-invariants.test.ts / strict-effect-control-flow.test.ts purity gates; mise run check (generate:check + build + test), all green.
…st migration
Follow-up code review pass on this branch's earlier vitest migration
found four real Effect-idiom violations, all fixed:
1. test/fetch-openapi.test.ts monkey-patched globalThis.fetch with an
`as unknown as typeof fetch` cast, claiming no Effect replacement
existed. It does: fetchOpenApi already depends on the ScriptHttp
service (not Effect's HttpClient module), so the fix is a
Layer.succeed(ScriptHttp, {...}) test double merged with the real
ScriptFilesLive — the exact pattern the same file already uses one
test above. No global mutation, no cast.
2. Eleven test files imported describe/expect/test directly from
"vitest" instead of exclusively from "@effect/vitest", which
re-exports them unchanged. Fixed all eleven; added a regression
test (test/production-effect-invariants.test.ts) that AST-scans
test/ for stray "vitest" imports and for globalThis.fetch
assignments so both classes can't silently return.
3. The vitest-migration commit swapped Bun.spawn/Bun.file/Bun.sleep
for raw node:child_process/node:fs/node:crypto/node:timers/promises
calls — a lateral move, not a real fix. scripts/runtime/
release-host-live.ts (the ~1100-line release packaging/verification
pipeline) is rewritten on effect/FileSystem, effect/Path,
effect/unstable/process's ChildProcessSpawner, and effect/Crypto
throughout, using @effect/platform-node's NodeServices.layer at
each ReleaseHost boundary method. The one raw import that stays is
node:fs's lstatSync in the symlink-attack check: FileSystem.stat
always follows symlinks in this effect version (delegates to
Node's fs.stat) and there is no lstat-equivalent, so that check
cannot be expressed through the service — documented in place.
test/release.test.ts and test/effect-generator-patch.test.ts got
the same treatment for the calls that mirror what the production
code now does (subprocess spawn, SHA-256 digest, sleep, existence
checks); the ~20 independent temp-fixture node:fs/promises calls in
release.test.ts that never touch the Effect pipeline under test are
left as documented, in-scope exceptions rather than force-fit into
Effect.gen bodies across 30 unrelated test cases. test/run-akua.ts's
spawnSync (45+ call sites across two large test files, deliberately
black-box testing the compiled binary from outside the runtime it
spawns) is likewise a documented exception.
4. Named functions that just wrapped Effect.gen are now Effect.fn per
node_modules/effect/AGENTS.md's documented style (no prior local
usage existed to match, so this follows the upstream doc pattern
directly). Plain ternary/ combinator functions that never wrapped
Effect.gen were left as-is, matching the finding's scope.
release-host-live.ts's async FileSystem/ChildProcessSpawner service
calls mean its effects can no longer resolve synchronously, so
test/release.test.ts's runRelease() helper moved from
Effect.runSync to Effect.runPromise; every call site and the
sync-throw assertions that depended on it were updated to
await/.rejects accordingly.
Rationale: `as unknown as` is a banned escape hatch in this repo's own
conventions, and raw node:* imports where a real Effect service exists
undermine the Effect-first model this whole CLI is built on — both
undo the point of the vitest migration this branch just did.
Risk: release-host-live.ts is the release packaging/verification/smoke
pipeline; a behavior regression here would ship broken release
artifacts. Verified beyond the test suite by running the real
pipeline end to end on this host: `release:package` (cross-compiled
all 5 targets), `release:verify`, and `release:smoke` all succeeded
against freshly built archives.
Tested: bun run test (vitest run) — 19 files, 169 tests pass (167
baseline + 2 new regression checks); bun run generate:check; bun run
build (tsc --noEmit + bundle); mise run check, all green; mise run
release:package && release:verify && release:smoke against a real
compiled host archive, all exit 0.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #46. Every
:action-suffixed public operation (clusters.resume,machines.suspend,installs.previewHostnames.bindFloating, and every other REST-RPC-style route the public API exposes) failed client-side withMissing path parameter: resumebefore any request was built, even though the underlying endpoint works fine (confirmed against production with curl).Root cause
openapi/public.jsonuses a REST-RPC convention for action-style routes, e.g./clusters/{id}:resume. The generator (@effect/openapi-generator'stoHttpApiPath) correctly rewrites the OpenAPI{id}placeholder to Effect's Express-style:id, but does nothing about the literal:resumesuffix already in the path, producing/clusters/:id:resume.Effect's own
HttpApiClient.compilePath(node_modules/effect/src/unstable/httpapi/HttpApiClient.ts) matches every:wordoccurrence in a compiled path as a substitutable parameter — it has no way to distinguish a literal colon from a parameter marker. It matches:idand:resumeindependently, and since callers never supply aresumevalue, path compilation throws before any request is built.Fix
Patched
@effect/openapi-generator'stoHttpApiPath(patches/@effect%2Fopenapi-generator@4.0.0-beta.106.patch, bothdistandsrc, applied viabun patch) to percent-encode literal colons in the raw OpenAPI path before rewriting{param}to:param. Only the colons the rewrite just introduced remain unescaped, so Effect's params regex no longer matches the action suffix — it's carried through as inert literal text (%3Aresume) and the compiled URL reconstructs the exact original path once the realidis substituted.This lives in the generator rather than in generated CLI code, per this repo's
AGENTS.mdrule: "Add support to the OpenAPI producer or generator; never hide a contract gap with a manual endpoint implementation."I traced (and directly tested) an alternative — supplying the action name as a synthetic second path param — and confirmed it drops the separating colon entirely (
compilePath's replacer always emitsslash + encodeURIComponent(value), never a literal:), so it cannot reproduce the real request. The percent-encoding approach is the only one that survivescompilePathunmangled.Server compatibility: verified against this monorepo's own Hono router (which solves the mirror-image version of this exact ambiguity server-side in
packages/api-runtime/src/colon-method-params.ts) that a literal:resumesuffix and a percent-encoded%3Aresumesuffix produce an identical captured route param — Hono decodes%3Abefore route matching, so the request is unchanged on the wire.src/generated/openapi-api.gen.tswas regenerated viabun run generate, touching every:action-suffixed operation in the public API (30+ operations across clusters, machines, snippets, dashboards, offers, order drafts, operations, workspaces, installs, secrets, agents, and more) — confirming the fix is systemic, not a one-off patch forclusters.resume.Test-framework migration (bun:test → @effect/vitest)
While reviewing this PR we found every test file (
test/*.test.ts, 19 files) imported rawdescribe/expect/testfrombun:testand drove Effect programs withawait Effect.runPromise(...), the one place in this repo not on the Effect-first modelAGENTS.mdrequires forsrc/andscripts/. Folded that cleanup into this PR since it was discovered here.Framework choice: researched a Bun-native Effect-testing package first, per the ask to prefer keeping Bun's fast native runner if a reputable option exists. Two community packages exist —
effect-bun-test(cevr, 0 GitHub stars) and@domir/bun-test(DomiR, 3 stars, 1 open issue) — both single-maintainer, last published Mar/May 2026 with no activity since. Neither is referenced by the Effect-TS GitHub org ornode_modules/effect/AGENTS.md(which documents only@effect/vitest). Not a reasonable bar for a CLI that ships to users. Fell back to@effect/vitest, matching prior art already established in the siblingakua-dev/cnapmonorepo (packages/config/vitest/domain.ts).Installed
@effect/vitest@4.0.0-beta.106(exact peer match for this repo's pinnedeffect@4.0.0-beta.106) +vitest@^4.1.0.bun run testnow runsvitest run;mise.toml'stesttask calledbun testdirectly (bypassingpackage.json), so repointed it atbun run test. Files with no Effect execution keep plaindescribe/expect/test(which@effect/vitestre-exports unchanged fromvitest); files that run an Effect useit.effectwithEffect.gen/yield*bodies instead ofawait Effect.runPromise(...).Runtime fallout: vitest's worker pool runs under real Node, not Bun, even when invoked via
bun run vitest(confirmed by probingtypeof Bunandprocess.execPathin boththreadsandforkspool modes). That broke every test and one production helper (scripts/runtime/release-host-live.ts'srunCommand, a-live.tsboundary module where rawBunusage isAGENTS.md-compliant) that calledBun.spawn/Bun.spawnSync/Bun.file/Bun.sleepdirectly. Swapped these fornode:child_process.spawnSync,node:fs.existsSync, andnode:timers/promises.setTimeout— plain subprocess-spawning equivalence, not a Bun-specific fast path, so safe even though the compiled CLI itself still runs under Bun in production. Consolidated the two near-duplicaterunAkuasubprocess helpers (cli.test.ts,strict-effect-control-flow.test.ts) into sharedtest/run-akua.ts+test/bun-binary.ts, resolving the bun binary vianpm_execpath(set bybun run/npm run) instead ofprocess.execPath.Promise/throw ban scope: this repo already enforces a broader version of CNAP's
*.effect.tsfence via two TS-AST invariant tests (test/production-effect-invariants.test.ts,test/strict-effect-control-flow.test.ts) banning rawthrow/Promise/async/awaitacross all ofsrc/andscripts/— broader than CNAP's own fence because this whole CLI, not just suffixed files, is Effect-first. Left those untouched. CNAP itself does not extend that ban into*.effect.test.tstest bodies, only requiring the@effect/vitestimport; mirrored that here rather than inventing a stricter rule test/ never had. What Promise usage remains intest/is legitimate process-boundary glue with no Effect replacement: mocking thefetchglobal's Promise-returning contract, and a Promise-shaped test-double interface (auth-test-layer.ts) matching Node's async I/O contracts.Test plan
test/generated-command.test.ts) exercisingclusters.resumeandmachines.resumethrough the realgeneratedCommandViewcommand-execution path (not just unit-testing path compilation in isolation), asserting the exact compiled request URL against an interceptedfetch.Missing path parameter: resume) before the patch, GREEN after.bun:test;bun run test(vitest run) — 167 pass, same count asbun testbefore the migration.bun run generate:checkbun run buildmise run checkDependency bump: effect 4.0.0-beta.106 → 4.0.0-rc.109
Bumped
effect,@effect/openapi-generator,@effect/platform-node, and@effect/vitestoff the beta track onto the release candidate line — allfour publish matching
4.0.0-rc.*versions together (verified via npmregistry metadata: all four shipped 2026-08-14).
4.0.0-rc.110also exists but is excluded by this machine's global Buninstall guardrail (
minimumReleaseAge = 259200s/ 3 days in~/.bunfig.toml,a supply-chain protection against just-published/compromised versions;
rc.110 was ~1.5 days old at bump time). Used
4.0.0-rc.109instead — thenewest RC where every package in the set clears that 3-day window.
Diffed the beta.106 and rc.109 npm tarballs directly before touching
anything: every file either committed patch touches (
HttpApiTransformer.ts,OpenApiGenerator.ts,HttpApiEndpoint.ts,CliOutput.ts,toCodeDocument.ts, and theirdist/builds) is byte-identical between thetwo versions. So the colon-suffix fix from this PR (and the pre-existing
response-header/type patches) needed no behavioral changes — regenerated both
patch files against the rc.109 tarballs via
bun patch/bun patch --commit; they differ from the old beta.106 patches only in git blob indexhashes and diff-context formatting, not in patched code.
bun run generatereproducessrc/generated/openapi-api.gen.tsbyte-for-byte at the new version (zero diff vs. what's already committed),
confirming the generator's output is unaffected by the bump.
Also updated the two other hardcoded
beta.106references:skills/effect-v4/SKILL.md's audited-version line and its assertion intest/effect-v4-skill.test.ts. Leftdocs/superpowers/plans/2026-08-12-openapi-effect-executor.mdalone as ahistorical decision record.
Re-ran the full verification suite after the bump:
bun run generate(no diff),
bun run generate:check,bun run build,bun run test(19 files / 167 tests, unchanged pass count, including the two
clusters.resume/machines.resumecolon-suffix regression tests and bothproduction-effect-invariants.test.ts/strict-effect-control-flow.test.tspurity gates), and
mise run check. All green.Follow-up cleanup: eliminate remaining Effect anti-patterns from the vitest migration
A review pass on the vitest migration above found four real Effect-idiom
violations, all fixed here:
test/fetch-openapi.test.tsmonkey-patchedglobalThis.fetchwith anas unknown as typeof fetchcast, claiming no Effect replacement existed.It does:
fetchOpenApialready depends on theScriptHttpservice (notEffect's
HttpClientmodule), so the fix is aLayer.succeed(ScriptHttp, {...})test double merged with the realScriptFilesLive— the exactpattern the same file already uses one test above. No global mutation, no
cast.
Eleven test files imported
describe/expect/testdirectly from"vitest"instead of exclusively from"@effect/vitest", whichre-exports them unchanged. Fixed all eleven; added a regression test
(
test/production-effect-invariants.test.ts) that AST-scanstest/forstray
"vitest"imports and forglobalThis.fetchassignments so bothclasses can't silently return.
The vitest-migration commit swapped
Bun.spawn/Bun.file/Bun.sleepfor raw
node:child_process/node:fs/node:crypto/node:timers/promisescalls — a lateral move, not a real fix.
scripts/runtime/ release-host-live.ts(the ~1100-line release packaging/verificationpipeline) is rewritten on
effect/FileSystem,effect/Path,effect/unstable/process'sChildProcessSpawner, andeffect/Cryptothroughout, using
@effect/platform-node'sNodeServices.layerat eachReleaseHostboundary method. The one raw import that stays isnode:fs'slstatSyncin the symlink-attack check:FileSystem.statalways follows symlinks in this effect version (delegates to Node's
fs.stat) and there is no lstat-equivalent, so that check cannot beexpressed through the service — documented in place with a comment.
test/release.test.tsandtest/effect-generator-patch.test.tsgot thesame treatment for the calls that mirror what the production code now
does (subprocess spawn, SHA-256 digest, sleep, existence checks); the
~20 independent temp-fixture
node:fs/promisescalls inrelease.test.tsthat never touch the Effect pipeline under test are left as documented,
in-scope exceptions rather than force-fit into
Effect.genbodies across30 unrelated test cases.
test/run-akua.ts'sspawnSync(45+ call sitesacross two large test files, deliberately black-box testing the compiled
binary from outside the runtime it spawns) is likewise a documented
exception.
Named functions that just wrapped
Effect.genare nowEffect.fnpernode_modules/effect/AGENTS.md's documented style (no prior local usageexisted to match, so this follows the upstream doc pattern directly).
Plain ternary/combinator functions that never wrapped
Effect.genwereleft as-is.
release-host-live.ts's asyncFileSystem/ChildProcessSpawnerservicecalls mean its effects can no longer resolve synchronously, so
test/release.test.ts'srunRelease()helper moved fromEffect.runSyncto
Effect.runPromise; every call site and the sync-throw assertions thatdepended on it were updated to
await/.rejectsaccordingly.Verification: beyond the test suite, ran the real release pipeline end
to end on this host —
release:package(cross-compiled all 5 targets),release:verify, andrelease:smokeall succeeded against freshly builtarchives.
bun run test— 19 files, 169 tests pass (167 baseline + 2 newregression checks);
bun run generate:check;bun run build;mise run check, all green.