Skip to content

fix(cli,aspire): generated apps report Healthy only once they can server-render - #963

Merged
rickylabs merged 9 commits into
mainfrom
fix/aspire-app-health-probe
Jul 31, 2026
Merged

fix(cli,aspire): generated apps report Healthy only once they can server-render#963
rickylabs merged 9 commits into
mainfrom
fix/aspire-app-health-probe

Conversation

@rickylabs

@rickylabs rickylabs commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Summary

A generated Fresh app was reported Healthy by Aspire while every request to it returned 500. The
Aspire helper generator registered no health check of any kind for generated resources, and
Aspire treats a resource with no registered health check as ready the moment its process reaches
Running — so "the process started" was the entire health contract. This PR gives app resources
an HTTP health probe against their own server-rendered /health route, and extends
scaffold.runtime so the suite actually asks the running app for a page.

Scope

Root cause

Not a probe that was checking the wrong thing — there was no probe at all.

generateRegisterApps emitted withHttpEndpoint({ port, env: 'PORT' }) and stopped. Aspire's
documented fallback is explicit: "If no health checks are registered for a resource, the AppHost
waits for the resource to be in the Running state."
For an addExecutable resource, Running
means the process was spawned. aspire wait <app> --non-interactive therefore returned healthy for
an app that could not render a single page.

What changed

  • packages/aspireRESOURCE_DEFAULTS.AppHealthCheckPath = '/health' (beside the existing
    HttpEndpointName, which the probe reuses as its endpoint name), and an optional
    AppEntry.HealthCheckPath (string | false).
  • generate-register-apps.ts — emits
    await <app>.withHttpHealthCheck({ path, endpointName: 'http' }) for app entries with a port,
    registered after the endpoint whose base address it resolves against.
  • routes/health.tsx.template — doc comment now states the SSR contract the probe depends on.
    No behaviour change.
  • scaffold.runtime — two new paired gates (see Regression guard).

Why /health and not /

The issue's suggested direction — "an HTTP health probe that renders a minimal SSR route" — is what
this implements. The scaffold already writes apps/<app>/routes/health.tsx, which renders through
definePage() and the app shell and only short-circuits to JSON when the caller sends
Accept: application/json without text/html. Aspire's probe sends no Accept header, so it
takes the SSR branch: a broken render pipeline fails the probe. Probing / instead would
false-negative on any app whose home page is auth-gated or redirects.

One correction to the upstream docs

Aspire's docs show withHttpHealthCheck('/health'). The TypeScript SDK that aspire restore
generates for SDK 13.4.6 — the version the scaffold pins — declares
withHttpHealthCheck(options?: WithHttpHealthCheckOptions) on ExecutableResource. I verified this
by running a real aspire restore against a throwaway apphost and reading the generated
.aspire/modules/aspire.mts. The generator emits the options-object form; the documented positional
form would throw at AppHost start.

Regression guard

The check that was looking past the problem is scaffold.runtime. It started the whole AppHost,
waited on every database, cache, and plugin resource and probed their /health — but never waited
on the generated app and never issued a single HTTP request to any app route
. The only app-shaped
gate, behavior.ui-render, renders AI payload components in-process and never touches the running
server. An app that returned 500 to every request passed the suite cleanly.

Two paired gates close that:

  • runtime.wait.dashboardaspire wait on the app resource, which is only meaningful now that
    the probe exists. 300s timeout, covering Vite cold start and first render.
  • behavior.app-home — fetches the app's home page and requires a 2xx that is actually HTML.
    Asserting the status alone would not do: a 500 error page is text/html too.

Plus unit-level guards that fail before the fix and pass after it:

  • generators-background-app_test.ts — probe emitted, emitted after the endpoint, honours a
    custom HealthCheckPath, omitted for HealthCheckPath: false, and never emitted for
    tauri/desktop/task.
  • route-templates_test.ts — the health route still renders through the page layer, and the JSON
    branch remains the guarded exception rather than the fall-through.
  • suite-registry_test.ts — both new gates are registered in scaffold.runtime, in order.

Definition of Done

  • Root cause identified and fixed at the generator, not worked around at the call site
  • Regression guard extends the check that let the defect through (scaffold.runtime)
  • A test that fails before the fix and passes after it
  • fmt:check, lint, check, test, arch:check all green
  • scaffold.runtime executed end to end (needs a runtime host — not available in this session)
  • Behaviour change and deliberate non-scope stated explicitly

Slices

  • S1 Run artifacts + Design checkpoint — 568ffe11d
  • S2 Probe path contract in @netscript/aspired77932249
  • S3 Generator emits withHttpHealthCheck for app resources — 446a47813
  • S4 Pin SSR as the fall-through of the scaffolded health route — 37c295966
  • S5 scaffold.runtime waits on the app and fetches its home page — 1780ffda7

Validation

Gate Result
deno task fmt:check PASS — 1869 files, 0 findings
deno task lint PASS — 1724 files, 0 occurrences
deno lint packages/cli/{templates,e2e} (cli is excluded from the root lint task) PASS — 95 files
deno task check PASS — 2457 files, 0 occurrences
deno task test PASS — 2230 passed, 0 failed, 12 ignored (3m22s)
deno task arch:check PASS — exit 0, no FAIL= rows; pre-existing WARN/INFO only
run-deno-doc-lint.ts --root packages/aspire PASS — 0 errors, 0 missing JSDoc
deno task e2e:cli run scaffold.runtime NOT RUN — see below

scaffold.runtime was not executed. This environment has no database containers or full
runtime, so the two new E2E gates are proven by registration tests only, not by an actual run. They
need the OpenHands / merge-readiness pass before this merges. Flagging rather than quietly skipping.

Fail-before evidence for the generator guard (generator change stashed):

should register an HTTP health probe for app resources ... FAILED
should probe a custom path when HealthCheckPath is configured ... FAILED
FAILED | 1 passed (35 steps) | 1 failed (2 steps)

What I deliberately did not change

  • The SSR failure itself (fix(sdk,fresh): jsr: specifiers for beta.11 SDK subpaths fail to load under Vite #953). It is the reproduction vehicle for "healthy while 500", not
    this defect. Left open.
  • Services, plugins, and background processors. They have the same missing-probe gap, but
    first-party plugin health paths are heterogeneous — workers/sagas/auth serve /health/live,
    triggers serves /health — so a single defaulted path would be wrong for several of them and
    would newly gate aspire wait on those resources against an unverified path. That needs declared
    per-resource paths, which is its own change. Follow-up issue below.
  • tauri / desktop / task app types. No HTTP page contract; desktop gets no endpoint at
    all.

Behaviour change to be aware of

An existing project whose app has no /health route will now show Unhealthy instead of a false
green, and aspire wait <app> will block on it. That is the intended correction, and
HealthCheckPath: false is the opt-out. The scaffold has always written routes/health.tsx, so
scaffolded projects are unaffected. .helpers/*.mts are regenerated by netscript generate, so
existing projects pick the probe up on their next generate.

Follow-up worth filing

Health probes for generated services, plugins, and background processors, with each resource
declaring its own path rather than inheriting a default. Same root cause, different blast radius.

Harness

  • Run dir: .llm/runs/fix-aspire-app-health-probe--954/
  • Phase: impl — research, plan, and Design checkpoint are committed in S1.
  • Process deviation (recorded in drift.md D-1): this is a single-session run, so PLAN-EVAL and
    IMPL-EVAL were not run as separate evaluator sessions. PR review is the reviewing pass.

Drift / Debt

  • D-1 Single-session run; no separate evaluator sessions.
  • D-2 Aspire docs disagree with the generated SDK on withHttpHealthCheck's signature — options
    object wins. Re-verify if the SDK pin moves.
  • D-3 scaffold.runtime did not exercise the generated app at all; behavior.ui-render does not
    count.
  • D-4 scaffold.runtime not executed in this environment.
  • No new arch-debt entries; none closed.

rickylabs and others added 2 commits July 31, 2026 15:34
…nerated apps

Records the harness run for fix/aspire-app-health-probe: research (including a
verified `aspire restore` against SDK 13.4.6 confirming
`ExecutableResource.withHttpHealthCheck` and its options-object signature), the
plan, and the Design checkpoint with five commit slices.

Refs #954

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Proves an app entry can name the HTTP path Aspire should probe, and that the
scaffold has a single named default for it instead of a literal buried in a
generator template.

`RESOURCE_DEFAULTS.AppHealthCheckPath` sits beside `HttpEndpointName`, which the
probe reuses as its endpoint name. `AppEntry.HealthCheckPath` is optional and
accepts `false`, so an app that serves no health route can opt out rather than
sit permanently Unhealthy.

Refs #954

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
rickylabs and others added 3 commits July 31, 2026 15:38
Proves an Aspire resource for a generated Fresh app is no longer considered
ready the instant its process spawns.

Aspire treats a resource with no registered health check as ready as soon as it
reaches `Running`. The Aspire helper generator registered none, so an app whose
every request failed during SSR still showed green on the dashboard and
satisfied `aspire wait` (#954). `generateRegisterApps` now emits
`withHttpHealthCheck({ path, endpointName })` for `app` entries that expose a
port, immediately after the endpoint whose base address the probe resolves
against.

Only the `app` type gets a probe: `tauri`, `desktop`, and `task` own no HTTP
page contract, and `desktop` is not given an endpoint at all.

The emitted call uses the options-object form. Aspire's published docs show
`withHttpHealthCheck('/health')`, but the TypeScript SDK generated by
`aspire restore` for SDK 13.4.6 declares
`withHttpHealthCheck(options?: WithHttpHealthCheckOptions)` on
`ExecutableResource`; the positional form would throw at AppHost start.

Refs #954

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Proves the route the AppHost probes still renders through the page layer, so the
probe added in the previous slice keeps exercising the SSR pipeline rather than
a JSON short-circuit.

Aspire's probe sends no `Accept` header, so it lands on the server-rendered
branch. Narrowing that branch later — returning JSON for an unspecific Accept,
say — would silently restore "Healthy while every page returns 500" without
touching the generator at all. The route's own doc comment now states the
contract for the next person to edit it.

Refs #954

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Proves the merge-readiness suite would catch a recurrence of #954 rather than
pass straight through it.

The suite started the whole AppHost, waited on every database, cache, and plugin
resource and probed their HTTP health — but never waited on the generated app
and never issued a single request to any app route. `behavior.ui-render`, the
only app-shaped gate, renders AI payload components in-process and never touches
the running server. An app that returned 500 to every request passed the suite.

Two paired gates close that hole: `runtime.wait.dashboard` blocks on the app's
new HTTP health probe (300s, covering Vite's cold start and first render), and
`behavior.app-home` fetches the home page and requires a 2xx that is actually
HTML. Asserting the status alone would not do — a 500 error page is `text/html`
too.

Refs #954

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@rickylabs
rickylabs force-pushed the fix/aspire-app-health-probe branch from 3efbffe to 1780ffd Compare July 31, 2026 13:39
Closes the run's Gate phase: fmt/lint/check/test/arch/doc-lint results with
counts, the fail-before output for the new generator guard, and the
`aspire restore` verification that fixed the emitted SDK call shape.

`scaffold.runtime` is recorded as NOT RUN rather than skipped — this host has no
database containers, so the two new E2E gates are proven by registration tests
only.

Refs #954

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@rickylabs

Copy link
Copy Markdown
Owner Author

[PHASE: IMPL]

Five slices landed; #954 root-caused to a missing Aspire health check, not a mis-aimed one.

What the root cause turned out to be

The Aspire helper generator registered no health check at all for generated resources.
generateRegisterApps emitted withHttpEndpoint({ port, env: 'PORT' }) and stopped. Aspire's
documented fallback then applies: "If no health checks are registered for a resource, the AppHost
waits for the resource to be in the Running state."
For an addExecutable resource that means
"the process was spawned" — so aspire wait <app> returned healthy for an app that could not render
a single page.

Slices

  1. 568ffe11d — run artifacts + Design checkpoint
  2. d77932249RESOURCE_DEFAULTS.AppHealthCheckPath + optional AppEntry.HealthCheckPath
  3. 446a47813 — generator emits withHttpHealthCheck({ path, endpointName }) for app resources
  4. 37c295966 — pin SSR as the fall-through of the scaffolded /health route
  5. 1780ffda7scaffold.runtime waits on the app and fetches its home page
  6. 335570f69 — gate evidence

Two things worth a reviewer's attention

  1. The upstream docs are wrong for this SDK version. Aspire documents
    withHttpHealthCheck('/health'). I ran a real aspire restore against a throwaway apphost pinned
    to SDK 13.4.6 and read the generated .aspire/modules/aspire.mts: ExecutableResource declares
    withHttpHealthCheck(options?: WithHttpHealthCheckOptions) with { path?, statusCode?, endpointName? }. The positional form would have thrown at AppHost start. Recorded as drift D-2.

  2. scaffold.runtime never touched the running app. It started the whole AppHost, waited on
    every database, cache, and plugin resource and probed their /health — but issued zero HTTP
    requests to any app route. behavior.ui-render, the only app-shaped gate, renders AI payload
    components in-process. That is why "Healthy while every request returns 500" passed the
    merge-readiness suite. Recorded as drift D-3.

Gates

Gate Result
deno task fmt:check PASS — 1869 files, 0 findings
deno task lint PASS — 1724 files, 0 occurrences
deno lint packages/cli/{src/kernel/templates,e2e} PASS — 95 files (cli is excluded from the root lint task)
deno task check PASS — 2457 files, 0 occurrences
deno task test PASS — 2230 passed, 0 failed, 12 ignored (3m22s)
deno task arch:check PASS — exit 0, no FAIL= rows
run-deno-doc-lint.ts --root packages/aspire PASS — 0 errors, 0 missing JSDoc
deno task e2e:cli run scaffold.runtime NOT RUN — no database containers on this host

Fail-before evidence for the new generator guard (generator change stashed):

should register an HTTP health probe for app resources ... FAILED
should probe a custom path when HealthCheckPath is configured ... FAILED
FAILED | 1 passed (35 steps) | 1 failed (2 steps)

Next

  • scaffold.runtime must run before merge — the two new E2E gates are proven by registration tests
    only. That is the one unchecked DoD box.
  • Follow-up worth filing: the same probe for generated services, plugins, and background
    processors
    , with declared per-resource paths (workers/sagas/auth serve /health/live, triggers
    serves /health, so a single default would be wrong). Deliberately out of scope here.
  • Process note: single-session run, so no separate PLAN-EVAL / IMPL-EVAL sessions (drift D-1). PR
    review is the reviewing pass.

@rickylabs
rickylabs marked this pull request as ready for review July 31, 2026 13:41
Captures the two reusable findings: Aspire's docs disagree with the generated
13.4.6 TypeScript SDK on withHttpHealthCheck's signature (and how to settle that
in two minutes with `aspire restore`), and scaffold.runtime brought up the whole
AppHost without ever requesting a page from the generated app.

Refs #954

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@augmentcode

augmentcode Bot commented Jul 31, 2026

Copy link
Copy Markdown
🤖 Augment PR Summary

Summary: This PR fixes Aspire “false healthy” reporting for generated Fresh apps by registering an HTTP health probe that exercises the app’s SSR pipeline.

Changes:

  • Adds RESOURCE_DEFAULTS.AppHealthCheckPath = "/health" and an optional AppEntry.HealthCheckPath override (or false to opt out) in @netscript/aspire.
  • Updates the CLI Aspire helper generator to emit withHttpHealthCheck({ path, endpointName: "http" }) for app resources after their HTTP endpoint is registered.
  • Clarifies the scaffolded /health route’s contract: SSR is the fall-through, JSON is only returned for JSON-only Accept requests.
  • Extends the scaffold.runtime E2E suite to (1) aspire wait on the app resource and (2) fetch the app home page and require a 2xx HTML response.
  • Adds/updates focused unit tests covering generator emission, health-route behavior, and suite registration/order.

Technical Notes: The probe targets the app’s own server-rendered /health route so SSR failures surface as Unhealthy instead of “process is Running ⇒ Healthy”.

🤖 Was this summary useful? React with 👍 or 👎

@augmentcode augmentcode Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 2 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

};

/** Home page of the generated app, on `PORT_RANGES.APP.start`. */
const APP_HOME_URL = 'http://127.0.0.1:8000/';

@augmentcode augmentcode Bot Jul 31, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

packages/cli/e2e/src/application/gates/scaffold/runtime-gates.ts:21APP_HOME_URL hardcodes 8000 while the doc comment says it’s on PORT_RANGES.APP.start, so a future default-port change would make this gate probe the wrong URL. If the app port can vary by scaffold options, this would also become a false failure/pass depending on what’s running on 8000.

Severity: low

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

lines.push(``);
lines.push(` // HTTP health probe — a listening socket alone is not "healthy".`);
lines.push(
` await ${id}.withHttpHealthCheck({ path: '${path}', endpointName: '${RESOURCE_DEFAULTS.HttpEndpointName}' });`,

@augmentcode augmentcode Bot Jul 31, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

packages/cli/src/kernel/templates/aspire/helpers/register/generate-register-apps.ts:248HealthCheckPath is interpolated into a single-quoted string literal without escaping, so a configured path containing ' would generate invalid TypeScript in the emitted helper. Since this is user-supplied config, it may be worth ensuring it can’t break codegen output.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

`behavior.app-home` shipped probing a hardcoded `http://127.0.0.1:8000/`
(`PORT_RANGES.APP.start`). The scaffold publishes the app on 8010 —
`PORT_RANGES.APP.start + 10`, offset on purpose so the Aspire proxy does not
collide with Vite's own default of 8000. Nothing has ever listened on 8000, so
all 60 attempts were refused: 60 refusals at 1s apart is the 60182ms CI failure,
and a refused connection reads exactly like an app that cannot render.

The app was fine throughout. `runtime.wait.dashboard` passed in the same run, and
a `curl` at the app's real port returns HTTP 200 `text/html`, 130KB.

The probe now takes a project root and an app name and resolves the URL from that
project's `appsettings.json` — the same file the helper generator reads when it
emits `withHttpEndpoint({ port })`. A literal cannot drift from the artifact it
describes if there is no literal. It moved from an inline `deno eval` string to a
script module so the gate's command factory stays a pure function of the run
context; resolving inside the factory broke the suite-runner test that builds the
real suite against a faked executor.

`SCAFFOLD_APP_PORT` replaces the `PORT_RANGES.APP.start + 10` expression that was
spelled out in three scaffold call sites. Generated output is unchanged.

Refs #954
@rickylabs

Copy link
Copy Markdown
Owner Author

[PHASE: IMPL] [SLICE: S6]

scaffold.runtime ran for the first time and found a defect in this PR's own new gate — not in the app, and not in #953.

What failed and why

behavior.app-home probed a hardcoded http://127.0.0.1:8000/, commented as PORT_RANGES.APP.start. The scaffold publishes the app on PORT_RANGES.APP.start + 108010 — offset on purpose since the TS AppHost landed so the Aspire proxy does not collide with Vite's own default of 8000 (plan-init.ts carries the rationale in a comment). Nothing has ever listened on 8000.

The timing said so before the code did: 60182 ms is exactly 60 iterations of the probe's 1000 ms sleep plus process start, so every fetch failed instantly. That is connection-refused — not a 500, not a slow render. And runtime.wait.dashboard PASSED in 53 s in the same run, which means the health probe added in S3 had already proven the app server-renders /health.

Confirmed against a generated project on a runtime host:

Evidence Result
.helpers/register-apps.mts (generated) await dashboard.withHttpEndpoint({ port: 8010, env: 'PORT' })
appsettings.json (generated) NetScript.Apps.dashboard.Port = 8010
ss -lntp with the AppHost up 127.0.0.1:8010 bound by dcp; nothing on 8000
curl the running app status=200 type=text/html size=130170the home page renders
probe → port 8000 HTTP 0 (): fetch failed after 1m0.483s — CI's 60182 ms, reproduced
probe → the app's real port app home page rendered: HTTP 200 (130162 bytes), 0.4 s

#953 / #957 are not prerequisites for this PR. scaffold.ui-local-source maps every @netscript/* specifier to the workspace member, so the unresolvable JSR subpath in #953 cannot occur in this suite — and generated.deno-check would have failed first if it could. #957's own scaffold-runtime failure is a cancelled runner (The runner has received a shutdown signal) at behavior.workers-health; its previous run passed the same lane. The two PRs are independent and neither blocks the other.

Fix — 2835d7b76

The probe carries no port at all. behavior.app-home now runs probe-app-home.ts <projectRoot> <appName>, which resolves the URL from the project's own appsettings.json — the same file the helper generator reads when it emits withHttpEndpoint({ port }). A literal cannot drift from the artifact it describes if there is no literal.

Moving the probe from an inline deno eval string to a script module (the durable-cli-parity.ts pattern) also keeps the gate's command factory pure. Resolving the port inside the factory broke suite runner skips cleanup phase when cleanup is disabled, which builds the real scaffold.runtime definition against a faked executor — a gate definition that reads the filesystem is not constructible without one.

SCAFFOLD_APP_PORT replaces the PORT_RANGES.APP.start + 10 expression that was written out three times (plan-init.ts, render-ts-apphost.ts, write-app-files.ts). Generated output is unchanged — a fresh netscript init still writes Port: 8010 and a '8010' fallback in main.ts.

Regression guard

The check that let this through is scaffold.runtime itself, which drift.md D-4 recorded as never executed. The guards now fail at unit speed:

  • runtime-gates_test.ts — asserts the app-home command whole, and that no argument contains PORT_RANGES.APP.start. The shipped gate fails this test.
  • generated-app-endpoint_test.ts — the resolver returns a deliberately unusual declared port (9137, proving nothing is hardcoded); returns SCAFFOLD_APP_PORT from an appsettings.json produced by the CLI's real generateAppsettings(), with SCAFFOLD_APP_PORT asserted to differ from PORT_RANGES.APP.start; and names the file and the app when the port is missing, unreadable, or unparseable rather than guessing.

Gates

Gate Result
deno task fmt:check PASS — 1869 files, 0 findings
deno fmt --check --ext ts over the touched packages/cli roots (excluded from the root task) PASS — 94 files
deno task lint PASS — 1724 files, 0 occurrences
deno lint packages/cli/e2e … PASS — 107 files
deno task check PASS — 2460 files, 0 failed batches
deno task test PASS — 2235 passed, 0 failed, 12 ignored (3m35s)
deno task arch:check PASS — exit 0, no FAIL= rows
deno task quality:scan PASS — ok:true, findings [], 7 pre-existing allowances
deno test packages/cli/e2e/tests/ PASS — 53 passed
deno task e2e:cli run scaffold.runtime PARTIAL — environmental, see below

scaffold.runtime reached runtime.wait.dashboard and timed out there after 300 s because two unrelated Aspire AppHosts from other projects on that host already held 127.0.0.1:8010, so this run's app proxy could never bind. Those were not this repository's processes and were left alone. The suite aborts on the first critical failure, so behavior.app-home was not reached in-suite; it was verified directly against the running generated app instead (table above). A clean-host scaffold.runtime on CI is the remaining verdict — which is the whole point of the gate this PR adds.

Drift

  • D-5 — D-4 came true: a gate written without ever executing the suite it extends is not a gate.
  • D-6 — gate command factories must stay pure functions of the run context.
  • D-7 — every scaffolded project defaults to the same app port, so two local projects cannot run at once. Worth its own issue; not filed from this session.

@rickylabs

Copy link
Copy Markdown
Owner Author

[PHASE: IMPL] [SLICE: S6 — confirmed]

Clean-host CI verdict on 2835d7b76 — the outstanding gate from the comment above:

> runtime.wait.dashboard: Wait for dashboard
  PASSED 53239ms
> behavior.app-home: Generated app serves its home page
  PASSED 225ms
Summary: passed=62 failed=0

scaffold-runtime PASS (5m28s); every other check green. drift.md D-4 and D-7 are now closed — this is the first end-to-end execution of scaffold.runtime for this PR, and it is the run that proves both new gates.

225 ms for the home page is the pair working as designed: runtime.wait.dashboard blocks on the health probe until the app can server-render, so by the time behavior.app-home asks for / the app is already warm. The 60-attempt budget is headroom, not the path.

For completeness on the other branch: #957's scaffold-runtime also passes on re-run (job, 4m30s). Both PRs are green independently, which settles it — they were never coupled.

behavior.app-home PASSED 225ms, passed=62 failed=0. Closes drift D-4 (the
suite had never been executed) and D-7 (local port collision). #957 passes
the same lane on re-run with no code change.
@rickylabs rickylabs added this to the 0.0.1-beta.12 milestone Jul 31, 2026
@rickylabs
rickylabs merged commit bd61d7a into main Jul 31, 2026
14 checks passed
@rickylabs
rickylabs deleted the fix/aspire-app-health-probe branch July 31, 2026 15:16
rickylabs added a commit that referenced this pull request Jul 31, 2026
Resolves the six-file port-handling overlap with #963 (app health probe).

The collision was semantic, not textual. #963 introduced SCAFFOLD_APP_PORT as a
*pinned* host port so its app-home probe could reach the app, and resolved that
port by reading `NetScript.Apps.<name>.Port` from appsettings.json. #952 removes
host-port pinning from the pristine scaffold entirely, so that appsettings entry
is now `{"Runtime":"deno","Type":"app"}` — no Port, no HostPort. The old resolver
throws on exactly that input, which would have failed the scaffold-runtime gate
on main the moment this branch merged. Neither PR's checks could see it: #978 ran
green against a main that did not yet contain #963.

Resolution:

- port-ranges.ts — keep both declarations. USER_PORT_RANGE validates an explicitly
  requested port; SCAFFOLD_APP_PORT narrows to what it now actually is, the
  source-literal fallback baked into the app for standalone runs outside the
  AppHost. That is the exact counterpart of how this branch already treats
  PORT_RANGES.SERVICE, and it is no longer a host/proxy port.
- plan-init.ts, render-ts-apphost.ts — take this branch: the scaffold stops
  writing a host port for the app.
- generated-app-endpoint.ts — the probe now resolves both cases. A pinned port
  still comes from appsettings (HostPort, with legacy Port still honoured, so
  existing workspaces resolve identically); an unpinned one is read from the
  running AppHost via `aspire describe --format Json`, mirroring the resolver the
  service-health gate already uses in CI.
- probe-app-home.ts, runtime-gates.ts — the gate hands the probe the AppHost path
  and grants --allow-run=aspire, since the allocated port exists nowhere on disk.

Regression cover: a pristine scaffold resolving to "pins nothing, and that is not
an error" is asserted directly, so the #952 x #963 interaction cannot silently
return.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(aspire): resource reports Healthy while SSR returns 500 — probe checks the port, not the app

1 participant