import { HttpStatus } from "../../core/errors";import type { LaunchFailure } from "./contracts";/** * The only two renderings a LaunchFailure has. Every HTTP status and every SSE stage is * derived here from the tagged union — never from substring-matching an error message, * which is how the old path turned any message containing "cancelled" into a 400. */const messageOf = (failure: LaunchFailure): string => { switch (failure.kind) { case "unsupported": return `${failure.engine} cannot run here: ${failure.reason}`; case "already-running": return `${failure.name} is already running`; case "no-capacity": return `needs ${failure.need} device(s); only ${failure.free} free`; case "install-failed": return `${failure.engine} install failed: ${failure.detail}`; case "spawn-failed": return failure.detail; case "exited-early": return `process exited (code ${failure.exitCode ?? "?"}${ failure.signal ? `, signal ${failure.signal}` : "" })\n${failure.logTail}`.trim(); case "unhealthy-timeout": return `not healthy after ${Math.round(failure.waitedMs / 1000)}s\n${failure.logTail}`.trim(); case "cancelled": return "launch cancelled"; }};const statusOf = (failure: LaunchFailure): number => { switch (failure.kind) { case "unsupported": return 422; case "already-running": case "no-capacity": return 409; case "cancelled": return 400; case "install-failed": case "spawn-failed": case "exited-early": case "unhealthy-timeout": return 503; }};export const toHttp = (failure: LaunchFailure): HttpStatus => new HttpStatus({ status: statusOf(failure), detail: messageOf(failure) });export interface LaunchEvent { readonly stage: "error" | "cancelled"; readonly message: string;}export const toEvent = (failure: LaunchFailure): LaunchEvent => ({ stage: failure.kind === "cancelled" ? "cancelled" : "error", message: messageOf(failure),});
import { HttpStatus } from "../../core/errors";
import type { LaunchFailure } from "./contracts";
/**
* The only two renderings a LaunchFailure has. Every HTTP status and every SSE stage is
* derived here from the tagged union — never from substring-matching an error message,
* which is how the old path turned any message containing "cancelled" into a 400.
*/
const messageOf = (failure: LaunchFailure): string => {
switch (failure.kind) {
case "unsupported":
return `${failure.engine} cannot run here: ${failure.reason}`;
case "already-running":
return `${failure.name} is already running`;
case "no-capacity":
return `needs ${failure.need} device(s); only ${failure.free} free`;
case "install-failed":
return `${failure.engine} install failed: ${failure.detail}`;
case "spawn-failed":
return failure.detail;
case "exited-early":
return `process exited (code ${failure.exitCode ?? "?"}${
failure.signal ? `, signal ${failure.signal}` : ""
})\n${failure.logTail}`.trim();
case "unhealthy-timeout":
return `not healthy after ${Math.round(failure.waitedMs / 1000)}s\n${failure.logTail}`.trim();
case "cancelled":
return "launch cancelled";
}
};
const statusOf = (failure: LaunchFailure): number => {
switch (failure.kind) {
case "unsupported":
return 422;
case "already-running":
case "no-capacity":
return 409;
case "cancelled":
return 400;
case "install-failed":
case "spawn-failed":
case "exited-early":
case "unhealthy-timeout":
return 503;
}
};
export const toHttp = (failure: LaunchFailure): HttpStatus =>
new HttpStatus({ status: statusOf(failure), detail: messageOf(failure) });
export interface LaunchEvent {
readonly stage: "error" | "cancelled";
readonly message: string;
}
export const toEvent = (failure: LaunchFailure): LaunchEvent => ({
stage: failure.kind === "cancelled" ? "cancelled" : "error",
message: messageOf(failure),
});