|
| 1 | +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. |
| 2 | + |
| 3 | +/** |
| 4 | + * [#5122] The `HttpServer` delegating wrapper is RETIRED from `@objectstack/runtime`. |
| 5 | + * |
| 6 | + * ## What was wrong |
| 7 | + * |
| 8 | + * `packages/runtime/src/http-server.ts` declared `class HttpServer implements |
| 9 | + * IHttpServer` and forwarded a constructor-injected server's REQUIRED members |
| 10 | + * (`get`/`post`/`put`/`delete`/`patch`/`use`/`listen`/`close`) — and only |
| 11 | + * those. Every OPTIONAL member of the contract was dropped on the floor: |
| 12 | + * |
| 13 | + * - `getPort?()` — how a harness addresses a `listen(0)` ephemeral port; |
| 14 | + * - `getRawApp?()` — the framework-native escape hatch four consumers |
| 15 | + * feature-detect (cloud-connection ×2, metadata HMR routes, cloud |
| 16 | + * serverless); |
| 17 | + * - `setFallbackHandler?()` — since the #5040 E7 publish flip landed (#5111), |
| 18 | + * the SINGLE seam declarative `apis:` endpoints enter through. |
| 19 | + * |
| 20 | + * `packages/spec/src/contracts/http-server.ts` tells consumers to probe these |
| 21 | + * with `typeof server.X === 'function'`, so wrapping a capable adapter made |
| 22 | + * every probe read **false** and the capability vanish — with the underlying |
| 23 | + * adapter providing it all along. For `setFallbackHandler` that shape is at |
| 24 | + * its worst: a host that wrapped `HonoHttpServer` and registered the wrapper |
| 25 | + * as `http.server` would 404 every endpoint its metadata declared, and the |
| 26 | + * runtime-side warn (#5409, `dispatcher-plugin.fallback-absence-warn.test.ts`) |
| 27 | + * would name the seam's absence without being able to name the wrapper. |
| 28 | + * |
| 29 | + * ## Why retirement rather than conditional forwarding |
| 30 | + * |
| 31 | + * `new HttpServer(` had **zero** occurrences in this repository, examples |
| 32 | + * included; the class was reachable only as a barrel export. Real hosts |
| 33 | + * register an `IHttpServer` adapter INSTANCE as `http.server` and always did. |
| 34 | + * The 2026-08-06 maintainer ruling took the #4939 (`ApiRegistry`) precedent — |
| 35 | + * retiring a part that was never assembled beats repairing it — over growing a |
| 36 | + * forwarding surface nobody composes; ADR-0049's remove side is the process. |
| 37 | + * |
| 38 | + * ## Why these assertions are runtime probes, not type-level ones |
| 39 | + * |
| 40 | + * A removed export cannot be imported by name — that would not compile, so the |
| 41 | + * pin has to interrogate the namespace object instead. And a compile-time pin |
| 42 | + * would be inert here anyway: `packages/runtime/tsconfig.json` excludes |
| 43 | + * `**\/*.test.ts` from `tsc --noEmit`, and vitest never type-checks (#4311), |
| 44 | + * which is the #4642 trap the sibling pin in |
| 45 | + * `packages/spec/src/api/registry-retirement.test.ts` records. Hence runtime |
| 46 | + * probes, each with an anti-vacuity guard so a broken import or a wrong path |
| 47 | + * cannot turn "absent" into a free pass. |
| 48 | + */ |
| 49 | + |
| 50 | +import { existsSync } from 'node:fs'; |
| 51 | +import path from 'node:path'; |
| 52 | +import { fileURLToPath } from 'node:url'; |
| 53 | + |
| 54 | +import { describe, it, expect } from 'vitest'; |
| 55 | + |
| 56 | +// Statically, not `await import()` inside a case: this barrel pulls the whole |
| 57 | +// runtime (sandbox, rest, security, observability) and takes several seconds to |
| 58 | +// evaluate cold — long enough to blow vitest's 5s per-test timeout, which is a |
| 59 | +// flake, not a finding. A namespace object answers `in` and `Object.keys` just |
| 60 | +// as well, and module evaluation happens outside any test's clock. |
| 61 | +import * as runtime from './index.js'; |
| 62 | + |
| 63 | +const SRC_DIR = path.dirname(fileURLToPath(import.meta.url)); |
| 64 | + |
| 65 | +describe('[#5122] `HttpServer` wrapper retired from @objectstack/runtime', () => { |
| 66 | + it('the barrel exports no `HttpServer`', () => { |
| 67 | + // Anti-vacuity FIRST: the namespace we are about to probe must be real and |
| 68 | + // non-trivial, or the `toBe(false)` below passes for the wrong reason. |
| 69 | + const names = Object.keys(runtime); |
| 70 | + expect(names.length, 'the runtime barrel must export a non-trivial surface').toBeGreaterThan(40); |
| 71 | + expect(names).toContain('Runtime'); |
| 72 | + |
| 73 | + expect('HttpServer' in runtime, '@objectstack/runtime must not export HttpServer').toBe(false); |
| 74 | + }); |
| 75 | + |
| 76 | + it('keeps the HTTP exports that were its neighbours — the deletion took nothing with it', () => { |
| 77 | + for (const kept of ['HttpDispatcher', 'DomainHandlerRegistry', 'MiddlewareManager']) { |
| 78 | + expect(kept in runtime, `${kept} must survive the retirement`).toBe(true); |
| 79 | + } |
| 80 | + }); |
| 81 | + |
| 82 | + it('the module file is gone, so it cannot come back as an unexported private wrapper', () => { |
| 83 | + // Anti-vacuity: a wrong base directory would make every `existsSync` false. |
| 84 | + expect( |
| 85 | + existsSync(path.join(SRC_DIR, 'http-dispatcher.ts')), |
| 86 | + 'probe path must point at the runtime source directory', |
| 87 | + ).toBe(true); |
| 88 | + |
| 89 | + expect( |
| 90 | + existsSync(path.join(SRC_DIR, 'http-server.ts')), |
| 91 | + 'packages/runtime/src/http-server.ts is retired (#5122) — a host composes the ' + |
| 92 | + 'framework by registering an IHttpServer ADAPTER INSTANCE as `http.server`, ' + |
| 93 | + 'not by wrapping one in a same-shaped delegator that drops the optional members', |
| 94 | + ).toBe(false); |
| 95 | + }); |
| 96 | +}); |
0 commit comments