mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-11 22:53:00 +00:00
## Summary Fixes sandbox state reporting by separating a requested sandbox plan from an initialized sandbox instance. Runs now project sandbox lifecycle as `planned`, `initializing`, `ready`, or `failed`, and live sandbox operations only proceed once a real instance exists. ## Changes - Introduces `RunSandboxPlan`, `RunSandboxInstance`, and lifecycle-backed `RunSandbox` domain types, with serde validation that prevents `ready` sandboxes without an instance. - Updates store projection behavior so sandbox events transition through planned, initializing, ready, and failed states while preserving requested provider/image/snapshot separately from runtime metadata. - Tightens server sandbox handlers so details/files/services/terminal/VNC helpers require an initialized instance and return a clear 404 when the sandbox was never created. - Updates the OpenAPI contract and regenerated clients so `Run.sandbox` exposes lifecycle state while `SandboxDetails.sandbox` contains only initialized instance metadata. - Updates the web UI to render lifecycle state directly from run summaries, hide the Sandbox tab for pure planned sandboxes, and disable sandbox controls until the instance is ready. - Cleans up duplicated lifecycle display/type logic and duplicate server-side sandbox instance loading found during review. | Lifecycle state | Meaning | Live controls | | --- | --- | --- | | `planned` | Sandbox was requested but no provider instance exists | Hidden/disabled | | `initializing` | Provider setup has started | State view only | | `ready` | Runtime instance exists | Enabled | | `failed` | Provider setup failed with error details | State view only | ## Testing - `cargo check --workspace` - `cargo +nightly-2026-04-14 fmt --check --all` - `git diff --check` - `cd apps/fabro-web && bun run typecheck` - `cd apps/fabro-web && bun test app/routes/run-detail.test.ts app/routes/run-sandbox.test.tsx app/components/run-summary-panel.test.tsx` - `cargo nextest run -p fabro-types --test sandbox_model_serde` - `cargo nextest run -p fabro-store run_created_projects_planned_sandbox_lifecycle sandbox_lifecycle_events_update_projected_sandbox_state run_failed_before_sandbox_events_leaves_sandbox_planned` - `cargo nextest run -p fabro-server planned_sandbox_returns_404_from_details_endpoint planned_sandbox_rejects_live_operations failed_sandbox_rejects_live_operations local_sandbox_returns_provider_neutral_details` - `cargo nextest run -p fabro-api --test run_sandbox_round_trip` - `cargo nextest run -p fabro-api --test sandbox_details_round_trip` --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)
88 lines
2.7 KiB
TypeScript
88 lines
2.7 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
|
|
import {
|
|
buildTerminalWebSocketUrl,
|
|
parseTerminalServerMessage,
|
|
sandboxStatusDetail,
|
|
TERMINAL_DOCK_CLEARANCE_CLASS,
|
|
terminalAccessCommandLabel,
|
|
} from "./terminal-view-helpers";
|
|
|
|
function locationLike(url: string): Location {
|
|
return new URL(url) as unknown as Location;
|
|
}
|
|
|
|
describe("terminal view helpers", () => {
|
|
test("builds ws URLs for local HTTP", () => {
|
|
expect(
|
|
buildTerminalWebSocketUrl(locationLike("http://127.0.0.1:4187/runs/run_1"), "run_1"),
|
|
).toBe("ws://127.0.0.1:4187/api/v1/runs/run_1/terminal");
|
|
});
|
|
|
|
test("builds wss URLs for HTTPS", () => {
|
|
expect(
|
|
buildTerminalWebSocketUrl(locationLike("https://fabro.example/runs/run/1"), "run/1"),
|
|
).toBe("wss://fabro.example/api/v1/runs/run%2F1/terminal");
|
|
});
|
|
|
|
test("parses terminal server control messages", () => {
|
|
expect(parseTerminalServerMessage('{"type":"ready"}')).toEqual({ type: "ready" });
|
|
expect(parseTerminalServerMessage('{"type":"error","message":"no sandbox"}')).toEqual({
|
|
type: "error",
|
|
message: "no sandbox",
|
|
});
|
|
expect(parseTerminalServerMessage('{"type":"unknown"}')).toBeNull();
|
|
expect(parseTerminalServerMessage("{")).toBeNull();
|
|
});
|
|
|
|
test("reserves space above the run steering bar", () => {
|
|
expect(TERMINAL_DOCK_CLEARANCE_CLASS).toContain("--fabro-interview-dock-clearance");
|
|
});
|
|
|
|
test("labels sandbox access commands by provider", () => {
|
|
expect(terminalAccessCommandLabel("daytona")).toBe("SSH");
|
|
expect(terminalAccessCommandLabel("docker")).toBe("Exec");
|
|
expect(terminalAccessCommandLabel("local")).toBeNull();
|
|
expect(terminalAccessCommandLabel(null)).toBeNull();
|
|
});
|
|
|
|
test("uses sandbox id as terminal status detail", () => {
|
|
expect(sandboxStatusDetail({
|
|
provider: "docker",
|
|
image: null,
|
|
snapshot: null,
|
|
runtime: {
|
|
id: "container-abc123",
|
|
working_directory: "/workspace",
|
|
repo_cloned: null,
|
|
clone_origin_url: null,
|
|
clone_branch: null,
|
|
},
|
|
}))
|
|
.toBe("container-abc123");
|
|
expect(sandboxStatusDetail({
|
|
provider: "daytona",
|
|
image: null,
|
|
snapshot: null,
|
|
runtime: {
|
|
id: "sandbox-name",
|
|
working_directory: "/workspace",
|
|
repo_cloned: null,
|
|
clone_origin_url: null,
|
|
clone_branch: null,
|
|
},
|
|
}))
|
|
.toBe("sandbox-name");
|
|
expect(sandboxStatusDetail({
|
|
provider: "docker",
|
|
image: null,
|
|
snapshot: null,
|
|
runtime: null,
|
|
})).toBeNull();
|
|
expect(sandboxStatusDetail({
|
|
kind: "planned",
|
|
plan: { provider: "docker" },
|
|
})).toBeNull();
|
|
expect(sandboxStatusDetail(null)).toBeNull();
|
|
});
|
|
});
|