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
16 changes: 12 additions & 4 deletions packages/loopover-miner/lib/github-token-resolution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ type LoopoverConfigProfile = {
type LoopoverConfig = {
activeProfile?: unknown;
profiles?: Record<string, LoopoverConfigProfile | undefined>;
// #8854: a top-level/global apiUrl, mirroring loopover-mcp's config shape — the fallback the miner's
// hand-copied resolver previously skipped (it read only the per-profile apiUrl).
apiUrl?: unknown;
};

const DEFAULT_API_URL = "https://api.loopover.ai";
Expand Down Expand Up @@ -85,10 +88,15 @@ function loopoverSessionToken(env: NodeJS.ProcessEnv): string | null {

function loopoverApiUrl(env: NodeJS.ProcessEnv): string {
if (env.LOOPOVER_API_URL) return env.LOOPOVER_API_URL.replace(/\/+$/, "");
const profileApiUrl = activeLoopoverProfile(env).apiUrl;
if (typeof profileApiUrl === "string" && profileApiUrl.trim()) {
const normalized = profileApiUrl.replace(/\/+$/, "");
if (!LEGACY_DEFAULT_API_URLS.has(normalized)) return normalized;
// #8854: mirror loopover-mcp's `activeProfile.apiUrl ?? config.apiUrl ?? default` — try the active profile's
// apiUrl first, THEN the top-level/global config.apiUrl, before the hardcoded default. The miner previously
// read only the profile apiUrl, so a config that set apiUrl globally fell straight to the default. Reuses the
// existing activeLoopoverProfile()/loadLoopoverConfig() readers (no new profile-selection branch here).
for (const candidate of [activeLoopoverProfile(env).apiUrl, loadLoopoverConfig(env).apiUrl]) {
if (typeof candidate === "string" && candidate.trim()) {
const normalized = candidate.replace(/\/+$/, "");
if (!LEGACY_DEFAULT_API_URLS.has(normalized)) return normalized;
}
}
return DEFAULT_API_URL;
}
Expand Down
28 changes: 28 additions & 0 deletions test/unit/miner-github-token-resolution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,34 @@ describe("resolveGitHubToken (#6116)", () => {
expect(capturedUrl).toBe("https://api.loopover.ai/v1/auth/github/token");
});

it("falls back to the top-level/global config.apiUrl when the active profile has none (#8854)", async () => {
dir = mkdtempSync(join(tmpdir(), "loopover-miner-github-token-global-apiurl-"));
// apiUrl set globally (not per-profile); trailing slash proves normalization runs on the global branch too.
writeConfig(dir, { apiUrl: "https://global.example/", profiles: { default: { session: { token: "session-token" } } } });
let capturedUrl: string | undefined;
const fetchImpl = async (url: string) => {
capturedUrl = url;
return Response.json({ token: "live-token" });
};
await resolveGitHubToken(configuredEnv(dir), { fetchImpl });
expect(capturedUrl).toBe("https://global.example/v1/auth/github/token");
});

it("prefers the active profile's apiUrl over the global config.apiUrl (#8854)", async () => {
dir = mkdtempSync(join(tmpdir(), "loopover-miner-github-token-profile-over-global-"));
writeConfig(dir, {
apiUrl: "https://global.example",
profiles: { default: { apiUrl: "https://profile.example", session: { token: "session-token" } } },
});
let capturedUrl: string | undefined;
const fetchImpl = async (url: string) => {
capturedUrl = url;
return Response.json({ token: "live-token" });
};
await resolveGitHubToken(configuredEnv(dir), { fetchImpl });
expect(capturedUrl).toBe("https://profile.example/v1/auth/github/token");
});

it("treats a legacy default API URL stored in the profile as absent, falling through to the current default", async () => {
dir = mkdtempSync(join(tmpdir(), "loopover-miner-github-token-legacy-url-"));
writeConfig(dir, { profiles: { default: { apiUrl: "https://gittensory-api.zeronode.workers.dev", session: { token: "session-token" } } } });
Expand Down