2026-06-02 15:36:52 +08:00
|
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
2026-05-29 03:42:53 +08:00
|
|
|
|
2026-07-08 15:46:52 +08:00
|
|
|
import {
|
|
|
|
|
BootstrapAuthRequiredError,
|
|
|
|
|
consumeUrlBootstrapSecret,
|
|
|
|
|
deriveWsUrl,
|
|
|
|
|
fetchBootstrap,
|
|
|
|
|
} from "@/lib/bootstrap";
|
2026-05-29 03:42:53 +08:00
|
|
|
|
|
|
|
|
describe("bootstrap helpers", () => {
|
2026-06-02 15:36:52 +08:00
|
|
|
afterEach(() => {
|
|
|
|
|
vi.useRealTimers();
|
|
|
|
|
vi.unstubAllGlobals();
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-29 03:42:53 +08:00
|
|
|
it("prefers the server-provided websocket URL over the current dev host", () => {
|
|
|
|
|
expect(deriveWsUrl("/", "tok en", "ws://127.0.0.1:8765/")).toBe(
|
|
|
|
|
"ws://127.0.0.1:8765/?token=tok%20en",
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-16 14:16:31 +08:00
|
|
|
it("overrides the server-provided websocket URL when on dev server port 5173", () => {
|
|
|
|
|
vi.stubGlobal("window", {
|
|
|
|
|
location: {
|
|
|
|
|
port: "5173",
|
|
|
|
|
hostname: "192.168.1.100",
|
|
|
|
|
protocol: "http:",
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
expect(deriveWsUrl("/", "tok", "ws://127.0.0.1:8765/")).toBe(
|
|
|
|
|
"ws://192.168.1.100:8765/?token=tok",
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-29 03:42:53 +08:00
|
|
|
it("preserves the host socket bridge URL", () => {
|
|
|
|
|
expect(deriveWsUrl("/", "tok en", "nanobot-host://engine/")).toBe(
|
|
|
|
|
"nanobot-host://engine/?token=tok%20en",
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("falls back to the current window host for legacy bootstrap payloads", () => {
|
|
|
|
|
expect(deriveWsUrl("/", "tok")).toBe(
|
|
|
|
|
"ws://localhost:3000/?token=tok",
|
|
|
|
|
);
|
|
|
|
|
});
|
2026-06-02 15:36:52 +08:00
|
|
|
|
|
|
|
|
it("times out when the bootstrap endpoint never responds", async () => {
|
|
|
|
|
vi.useFakeTimers();
|
|
|
|
|
vi.stubGlobal("fetch", vi.fn(() => new Promise<Response>(() => {})));
|
|
|
|
|
|
|
|
|
|
const pending = expect(fetchBootstrap("", "", 25)).rejects.toThrow(
|
|
|
|
|
"Request timed out after 25ms",
|
|
|
|
|
);
|
|
|
|
|
await vi.advanceTimersByTimeAsync(25);
|
|
|
|
|
|
|
|
|
|
await pending;
|
|
|
|
|
});
|
2026-07-08 14:27:52 +08:00
|
|
|
|
2026-07-08 15:46:52 +08:00
|
|
|
it("treats bootstrap responses without an API token as auth-required", async () => {
|
2026-07-08 14:27:52 +08:00
|
|
|
vi.stubGlobal(
|
|
|
|
|
"fetch",
|
|
|
|
|
vi.fn(async () => ({
|
|
|
|
|
ok: true,
|
|
|
|
|
json: async () => ({ token: "ws-token", ws_path: "/", expires_in: 300 }),
|
|
|
|
|
})),
|
|
|
|
|
);
|
|
|
|
|
|
2026-07-08 15:46:52 +08:00
|
|
|
const promise = fetchBootstrap();
|
|
|
|
|
await expect(promise).rejects.toMatchObject({
|
|
|
|
|
name: "BootstrapAuthRequiredError",
|
|
|
|
|
message: "bootstrap authentication required: missing api_token",
|
|
|
|
|
});
|
|
|
|
|
await expect(promise).rejects.toBeInstanceOf(BootstrapAuthRequiredError);
|
2026-07-08 14:27:52 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("consumes bootstrap secrets from the URL fragment", () => {
|
|
|
|
|
window.history.replaceState(
|
|
|
|
|
null,
|
|
|
|
|
"",
|
|
|
|
|
"/#/settings?bootstrapSecret=s3cret§ion=models",
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(consumeUrlBootstrapSecret()).toBe("s3cret");
|
|
|
|
|
expect(window.location.hash).toBe("#/settings?section=models");
|
|
|
|
|
});
|
2026-05-29 03:42:53 +08:00
|
|
|
});
|