Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ By default the CLI and MCP server connect to `https://api.runloop.ai`. To use a
export RUNLOOP_BASE_URL=https://api.runloop.pro
```

The URL must be of the form `https://api.<domain>`. The CLI derives other service hostnames from the domain portion:
The URL must be of the form `https://api.<domain>`, optionally with a `:port` (e.g. `https://api.runloop.pro:8443`). The CLI derives other service hostnames from the domain portion:

| Service | Host |
|----------|------|
Expand Down
11 changes: 3 additions & 8 deletions src/utils/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,10 @@ export function checkBaseDomain(): void {
process.exit(1);
}

if (
parsed.port ||
parsed.pathname.replace(/\/+$/, "") ||
parsed.search ||
parsed.hash
) {
if (parsed.pathname.replace(/\/+$/, "") || parsed.search || parsed.hash) {
console.error(
`Error: RUNLOOP_BASE_URL must not contain port, path, query, or fragment: ${raw}\n` +
`Expected format: https://api.<domain>`,
`Error: RUNLOOP_BASE_URL must not contain path, query, or fragment: ${raw}\n` +
`Expected format: https://api.<domain> (an optional :port is allowed)`,
);
process.exit(1);
}
Expand Down
88 changes: 88 additions & 0 deletions tests/__tests__/utils/baseUrl.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/**
* Tests for RUNLOOP_BASE_URL handling, including optional port overrides.
*/

import {
describe,
it,
expect,
beforeEach,
afterEach,
jest,
} from "@jest/globals";
import {
checkBaseDomain,
baseUrl,
runloopBaseDomain,
_resetBaseDomainCache,
} from "../../../src/utils/config.js";

describe("RUNLOOP_BASE_URL", () => {
const original = process.env.RUNLOOP_BASE_URL;
let exitSpy: jest.SpiedFunction<typeof process.exit>;
let errorSpy: jest.SpiedFunction<typeof console.error>;

beforeEach(() => {
_resetBaseDomainCache();
exitSpy = jest.spyOn(process, "exit").mockImplementation(((
code?: number,
) => {
throw new Error(`process.exit(${code})`);
}) as never);
errorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
});

afterEach(() => {
if (original === undefined) delete process.env.RUNLOOP_BASE_URL;
else process.env.RUNLOOP_BASE_URL = original;
exitSpy.mockRestore();
errorSpy.mockRestore();
_resetBaseDomainCache();
});

describe("checkBaseDomain", () => {
it("accepts a bare api.<domain> URL", () => {
process.env.RUNLOOP_BASE_URL = "https://api.runloop.pro";
expect(() => checkBaseDomain()).not.toThrow();
expect(exitSpy).not.toHaveBeenCalled();
});

it("accepts an api.<domain> URL with a port override", () => {
process.env.RUNLOOP_BASE_URL = "https://api.runloop.pro:8443";
expect(() => checkBaseDomain()).not.toThrow();
expect(exitSpy).not.toHaveBeenCalled();
});

it("rejects a URL with a path", () => {
process.env.RUNLOOP_BASE_URL = "https://api.runloop.pro/v1";
expect(() => checkBaseDomain()).toThrow();
expect(exitSpy).toHaveBeenCalledWith(1);
});

it("rejects a URL with a query string", () => {
process.env.RUNLOOP_BASE_URL = "https://api.runloop.pro?foo=bar";
expect(() => checkBaseDomain()).toThrow();
expect(exitSpy).toHaveBeenCalledWith(1);
});

it("rejects a non-https URL", () => {
process.env.RUNLOOP_BASE_URL = "http://api.runloop.pro:8443";
expect(() => checkBaseDomain()).toThrow();
expect(exitSpy).toHaveBeenCalledWith(1);
});
});

describe("baseUrl", () => {
it("includes the port override in the API base URL", () => {
process.env.RUNLOOP_BASE_URL = "https://api.runloop.pro:8443";
expect(baseUrl()).toBe("https://api.runloop.pro:8443");
});
});

describe("runloopBaseDomain", () => {
it("strips the api. prefix and the port", () => {
process.env.RUNLOOP_BASE_URL = "https://api.runloop.pro:8443";
expect(runloopBaseDomain()).toBe("runloop.pro");
});
});
});
Loading