mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-07 08:27:12 +00:00
Operators choose Docker (default, zero-config) or Daytona (validated via Daytona SDK) during browser install. Selection is captured in settings.toml under [run.sandbox] -- explicitly even for Docker, so the choice is locked in. Daytona keys land in the vault as DAYTONA_API_KEY (Environment secret). Step always runs after object_store and before the LLM step. Server adds POST /install/sandbox/test (validates Daytona key via client.list) and PUT /install/sandbox; both reuse the install-token auth and InstallSecret redaction patterns established by object-store. A resolve_install_sandbox_state helper preserves a saved Daytona key when the operator revisits the step without re-entering it. The in-memory api_key is dropped from PendingInstall after finish, matching the manual_credentials cleanup for S3 access keys. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
140 lines
4.3 KiB
TypeScript
140 lines
4.3 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
|
|
import {
|
|
putInstallObjectStore,
|
|
putInstallSandbox,
|
|
readInstallError,
|
|
testInstallObjectStore,
|
|
testInstallSandbox,
|
|
} from "./install-api";
|
|
|
|
describe("readInstallError", () => {
|
|
test("prefers the structured install error payload", async () => {
|
|
const response = new Response(
|
|
JSON.stringify({
|
|
errors: [{ status: "422", title: "Unprocessable Entity", detail: "invalid token" }],
|
|
}),
|
|
{
|
|
status: 422,
|
|
headers: { "Content-Type": "application/json" },
|
|
},
|
|
);
|
|
|
|
await expect(
|
|
readInstallError(response, "install request failed"),
|
|
).resolves.toBe("invalid token");
|
|
});
|
|
|
|
test("falls back to the provided message when the body is not structured JSON", async () => {
|
|
const response = new Response("boom", {
|
|
status: 500,
|
|
headers: { "Content-Type": "text/plain" },
|
|
});
|
|
|
|
await expect(
|
|
readInstallError(response, "install request failed"),
|
|
).resolves.toBe("install request failed (500)");
|
|
});
|
|
});
|
|
|
|
describe("install object-store requests", () => {
|
|
test("testInstallObjectStore posts the install payload to the validation endpoint", async () => {
|
|
const calls: Array<{ input: RequestInfo | URL; init?: RequestInit }> = [];
|
|
globalThis.fetch = ((input: RequestInfo | URL, init?: RequestInit) => {
|
|
calls.push({ input, init });
|
|
return Promise.resolve(new Response(JSON.stringify({ ok: true }), { status: 200 }));
|
|
}) as typeof fetch;
|
|
|
|
await testInstallObjectStore("test-install-token", {
|
|
provider: "s3",
|
|
bucket: "fabro-data",
|
|
region: "us-east-1",
|
|
credential_mode: "runtime",
|
|
});
|
|
|
|
expect(calls).toHaveLength(1);
|
|
expect(String(calls[0]!.input)).toBe("/install/object-store/test");
|
|
expect(calls[0]!.init?.method).toBe("POST");
|
|
expect(calls[0]!.init?.headers).toEqual({
|
|
Authorization: "Bearer test-install-token",
|
|
"Content-Type": "application/json",
|
|
});
|
|
expect(calls[0]!.init?.body).toBe(
|
|
JSON.stringify({
|
|
provider: "s3",
|
|
bucket: "fabro-data",
|
|
region: "us-east-1",
|
|
credential_mode: "runtime",
|
|
}),
|
|
);
|
|
});
|
|
|
|
test("putInstallObjectStore surfaces structured API errors", async () => {
|
|
globalThis.fetch = (() =>
|
|
Promise.resolve(
|
|
new Response(
|
|
JSON.stringify({
|
|
errors: [
|
|
{
|
|
status: "422",
|
|
title: "Unprocessable Entity",
|
|
detail: "Bucket is required.",
|
|
},
|
|
],
|
|
}),
|
|
{
|
|
status: 422,
|
|
headers: { "Content-Type": "application/json" },
|
|
},
|
|
),
|
|
)) as typeof fetch;
|
|
|
|
await expect(
|
|
putInstallObjectStore("test-install-token", { provider: "s3" }),
|
|
).rejects.toThrow("Bucket is required.");
|
|
});
|
|
});
|
|
|
|
describe("install sandbox requests", () => {
|
|
test("testInstallSandbox posts the install payload to the validation endpoint", async () => {
|
|
const calls: Array<{ input: RequestInfo | URL; init?: RequestInit }> = [];
|
|
globalThis.fetch = ((input: RequestInfo | URL, init?: RequestInit) => {
|
|
calls.push({ input, init });
|
|
return Promise.resolve(new Response(JSON.stringify({ ok: true }), { status: 200 }));
|
|
}) as typeof fetch;
|
|
|
|
await testInstallSandbox("test-install-token", {
|
|
provider: "daytona",
|
|
api_key: "dtn_test",
|
|
});
|
|
|
|
expect(calls).toHaveLength(1);
|
|
expect(String(calls[0]!.input)).toBe("/install/sandbox/test");
|
|
expect(calls[0]!.init?.method).toBe("POST");
|
|
expect(calls[0]!.init?.body).toBe(
|
|
JSON.stringify({ provider: "daytona", api_key: "dtn_test" }),
|
|
);
|
|
});
|
|
|
|
test("putInstallSandbox surfaces structured API errors", async () => {
|
|
globalThis.fetch = (() =>
|
|
Promise.resolve(
|
|
new Response(
|
|
JSON.stringify({
|
|
errors: [
|
|
{
|
|
status: "422",
|
|
title: "Unprocessable Entity",
|
|
detail: "api_key is required for daytona",
|
|
},
|
|
],
|
|
}),
|
|
{ status: 422, headers: { "Content-Type": "application/json" } },
|
|
),
|
|
)) as typeof fetch;
|
|
|
|
await expect(
|
|
putInstallSandbox("test-install-token", { provider: "daytona" }),
|
|
).rejects.toThrow("api_key is required for daytona");
|
|
});
|
|
});
|