Skip to content

fix(cli): percent-encode literal colons in :action-suffixed paths - #47

Merged
robinbraemer merged 4 commits into
mainfrom
fix/colon-suffixed-action-paths
Aug 18, 2026
Merged

fix(cli): percent-encode literal colons in :action-suffixed paths#47
robinbraemer merged 4 commits into
mainfrom
fix/colon-suffixed-action-paths

Conversation

@robinbraemer

@robinbraemer robinbraemer commented Aug 18, 2026

Copy link
Copy Markdown
Member

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 with Missing path parameter: resume before any request was built, even though the underlying endpoint works fine (confirmed against production with curl).

Root cause

openapi/public.json uses a REST-RPC convention for action-style routes, e.g. /clusters/{id}:resume. The generator (@effect/openapi-generator's toHttpApiPath) correctly rewrites the OpenAPI {id} placeholder to Effect's Express-style :id, but does nothing about the literal :resume suffix already in the path, producing /clusters/:id:resume.

Effect's own HttpApiClient.compilePath (node_modules/effect/src/unstable/httpapi/HttpApiClient.ts) matches every :word occurrence in a compiled path as a substitutable parameter — it has no way to distinguish a literal colon from a parameter marker. It matches :id and :resume independently, and since callers never supply a resume value, path compilation throws before any request is built.

Fix

Patched @effect/openapi-generator's toHttpApiPath (patches/@effect%2Fopenapi-generator@4.0.0-beta.106.patch, both dist and src, applied via bun 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 real id is substituted.

This lives in the generator rather than in generated CLI code, per this repo's AGENTS.md rule: "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 emits slash + encodeURIComponent(value), never a literal :), so it cannot reproduce the real request. The percent-encoding approach is the only one that survives compilePath unmangled.

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 :resume suffix and a percent-encoded %3Aresume suffix produce an identical captured route param — Hono decodes %3A before route matching, so the request is unchanged on the wire.

src/generated/openapi-api.gen.ts was regenerated via bun 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 for clusters.resume.

Test-framework migration (bun:test → @effect/vitest)

While reviewing this PR we found every test file (test/*.test.ts, 19 files) imported raw describe/expect/test from bun:test and drove Effect programs with await Effect.runPromise(...), the one place in this repo not on the Effect-first model AGENTS.md requires for src/ and scripts/. 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 or node_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 sibling akua-dev/cnap monorepo (packages/config/vitest/domain.ts).

Installed @effect/vitest@4.0.0-beta.106 (exact peer match for this repo's pinned effect@4.0.0-beta.106) + vitest@^4.1.0. bun run test now runs vitest run; mise.toml's test task called bun test directly (bypassing package.json), so repointed it at bun run test. Files with no Effect execution keep plain describe/expect/test (which @effect/vitest re-exports unchanged from vitest); files that run an Effect use it.effect with Effect.gen/yield* bodies instead of await Effect.runPromise(...).

Runtime fallout: vitest's worker pool runs under real Node, not Bun, even when invoked via bun run vitest (confirmed by probing typeof Bun and process.execPath in both threads and forks pool modes). That broke every test and one production helper (scripts/runtime/release-host-live.ts's runCommand, a -live.ts boundary module where raw Bun usage is AGENTS.md-compliant) that called Bun.spawn/Bun.spawnSync/Bun.file/Bun.sleep directly. Swapped these for node:child_process.spawnSync, node:fs.existsSync, and node: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-duplicate runAkua subprocess helpers (cli.test.ts, strict-effect-control-flow.test.ts) into shared test/run-akua.ts + test/bun-binary.ts, resolving the bun binary via npm_execpath (set by bun run/npm run) instead of process.execPath.

Promise/throw ban scope: this repo already enforces a broader version of CNAP's *.effect.ts fence via two TS-AST invariant tests (test/production-effect-invariants.test.ts, test/strict-effect-control-flow.test.ts) banning raw throw/Promise/async/await across all of src/ and scripts/ — 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.ts test bodies, only requiring the @effect/vitest import; mirrored that here rather than inventing a stricter rule test/ never had. What Promise usage remains in test/ is legitimate process-boundary glue with no Effect replacement: mocking the fetch global's Promise-returning contract, and a Promise-shaped test-double interface (auth-test-layer.ts) matching Node's async I/O contracts.

Test plan

  • Added two end-to-end regression tests (test/generated-command.test.ts) exercising clusters.resume and machines.resume through the real generatedCommandView command-execution path (not just unit-testing path compilation in isolation), asserting the exact compiled request URL against an intercepted fetch.
  • Confirmed RED (Missing path parameter: resume) before the patch, GREEN after.
  • All 19 test files migrated off bun:test; bun run test (vitest run) — 167 pass, same count as bun test before the migration.
  • bun run generate:check
  • bun run build
  • mise run check

Dependency bump: effect 4.0.0-beta.106 → 4.0.0-rc.109

Bumped effect, @effect/openapi-generator, @effect/platform-node, and
@effect/vitest off the beta track onto the release candidate line — all
four publish matching 4.0.0-rc.* versions together (verified via npm
registry metadata: all four shipped 2026-08-14).

4.0.0-rc.110 also exists but is excluded by this machine's global Bun
install 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.109 instead — the
newest 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 their dist/ builds) is byte-identical between the
two 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 index
hashes and diff-context formatting, not in patched code.

bun run generate reproduces src/generated/openapi-api.gen.ts
byte-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.106 references:
skills/effect-v4/SKILL.md's audited-version line and its assertion in
test/effect-v4-skill.test.ts. Left
docs/superpowers/plans/2026-08-12-openapi-effect-executor.md alone as a
historical 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.resume colon-suffix regression tests and both
production-effect-invariants.test.ts / strict-effect-control-flow.test.ts
purity 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:

  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 with a comment.
    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.

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.

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, and release:smoke all succeeded against freshly built
archives. bun run test — 19 files, 169 tests pass (167 baseline + 2 new
regression checks); bun run generate:check; bun run build; mise run check, all green.

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.
@robinbraemer
robinbraemer merged commit 599cbc4 into main Aug 18, 2026
7 checks passed
@robinbraemer
robinbraemer deleted the fix/colon-suffixed-action-paths branch August 18, 2026 17:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Every :action-suffixed OpenAPI path (resume/suspend/cancel/bindFloating/etc.) fails client-side with 'Missing path parameter'

1 participant