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
11 changes: 11 additions & 0 deletions extensions/cli/src/args.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { vi } from "vitest";

// Mock auth functions
vi.mock("./auth/workos.js", () => ({
loadAuthConfig: vi.fn(),
getAccessToken: vi.fn(),
}));

import { getAccessToken, loadAuthConfig } from "./auth/workos.js";
import { processRule as processPromptOrRule } from "./hubLoader.js";
describe("processPromptOrRule (loadRuleFromHub integration)", () => {
// Mock fetch for hub tests
Expand All @@ -16,6 +23,9 @@ describe("processPromptOrRule (loadRuleFromHub integration)", () => {

beforeEach(() => {
mockFetch.mockClear();
// Reset auth mocks to not authenticated state
(loadAuthConfig as any).mockReturnValue(null);
(getAccessToken as any).mockReturnValue(null);
});

describe("loadRuleFromHub", () => {
Expand Down Expand Up @@ -43,6 +53,7 @@ describe("processPromptOrRule (loadRuleFromHub integration)", () => {
"v0/continuedev/sentry-nextjs/latest/download",
"https://api.continue.dev/",
),
{ headers: {} },
);
});

Expand Down
120 changes: 120 additions & 0 deletions extensions/cli/src/hubLoader.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { beforeEach, describe, expect, it, vi } from "vitest";

import { getAccessToken, loadAuthConfig } from "./auth/workos.js";
import * as hubLoader from "./hubLoader.js";

// Mock auth functions
vi.mock("./auth/workos.js", () => ({
loadAuthConfig: vi.fn(),
getAccessToken: vi.fn(),
}));

const {
loadPackageFromHub,
mcpProcessor,
Expand All @@ -28,6 +35,9 @@ const mockedJSZip = vi.fn();
describe("hubLoader", () => {
beforeEach(async () => {
vi.clearAllMocks();
// Reset auth mocks to default state
(loadAuthConfig as any).mockReturnValue(null);
(getAccessToken as any).mockReturnValue(null);
});

describe("loadPackageFromHub", () => {
Expand All @@ -40,6 +50,9 @@ describe("hubLoader", () => {
});

it("should handle HTTP errors", async () => {
(loadAuthConfig as any).mockReturnValue(null);
(getAccessToken as any).mockReturnValue(null);

mockFetch.mockResolvedValueOnce({
ok: false,
status: 404,
Expand All @@ -53,7 +66,108 @@ describe("hubLoader", () => {
);
});

it("should make request without auth headers when not authenticated", async () => {
(loadAuthConfig as any).mockReturnValue(null);
(getAccessToken as any).mockReturnValue(null);

const JSZipModule = await import("jszip");
const JSZip = JSZipModule.default;

if (typeof (JSZip as any).mockImplementation === "function") {
(JSZip as any).mockImplementation(() => ({
loadAsync: vi.fn().mockResolvedValueOnce({
files: {
"README.md": {
dir: false,
async: vi.fn().mockResolvedValue("rule content"),
},
},
}),
}));
} else {
return;
}

mockFetch.mockResolvedValueOnce({
ok: true,
arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(8)),
});

await loadPackageFromHub("owner/rule", ruleProcessor);

expect(mockFetch).toHaveBeenCalledWith(expect.any(URL), { headers: {} });
});

it("should include Authorization header when authenticated", async () => {
const mockAuthConfig = {
accessToken: "test-token-123",
userId: "user123",
};
(loadAuthConfig as any).mockReturnValue(mockAuthConfig);
(getAccessToken as any).mockReturnValue("test-token-123");

const JSZipModule = await import("jszip");
const JSZip = JSZipModule.default;

if (typeof (JSZip as any).mockImplementation === "function") {
(JSZip as any).mockImplementation(() => ({
loadAsync: vi.fn().mockResolvedValueOnce({
files: {
"README.md": {
dir: false,
async: vi.fn().mockResolvedValue("rule content"),
},
},
}),
}));
} else {
return;
}

mockFetch.mockResolvedValueOnce({
ok: true,
arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(8)),
});

await loadPackageFromHub("owner/rule", ruleProcessor);

expect(mockFetch).toHaveBeenCalledWith(expect.any(URL), {
headers: {
Authorization: "Bearer test-token-123",
},
});
});

it("should include Authorization header for MCP requests when authenticated", async () => {
const mockAuthConfig = {
accessToken: "test-token-456",
userId: "user456",
};
(loadAuthConfig as any).mockReturnValue(mockAuthConfig);
(getAccessToken as any).mockReturnValue("test-token-456");

const mcpConfig = {
content: "name: test-mcp\nversion: 1.0.0",
};

mockFetch.mockResolvedValueOnce({
ok: true,
json: vi.fn().mockResolvedValue(mcpConfig),
});

await loadPackageFromHub("owner/mcp", mcpProcessor);

expect(mockFetch).toHaveBeenCalledWith(expect.any(URL), {
headers: {
Authorization: "Bearer test-token-456",
},
});
});

it("should load rule content", async () => {
(loadAuthConfig as any).mockReturnValue(null);
(getAccessToken as any).mockReturnValue(null);

const ruleContent = "# Test Rule\n\nThis is a test rule.";

// Check if JSZip is mocked
Expand Down Expand Up @@ -87,6 +201,9 @@ describe("hubLoader", () => {
});

it("should load MCP configuration", async () => {
(loadAuthConfig as any).mockReturnValue(null);
(getAccessToken as any).mockReturnValue(null);

const mcpConfig = {
content: "name: test-mcp\nversion: 1.0.0",
};
Expand All @@ -105,6 +222,9 @@ describe("hubLoader", () => {
});

it("should handle missing files", async () => {
(loadAuthConfig as any).mockReturnValue(null);
(getAccessToken as any).mockReturnValue(null);

// Check if JSZip is mocked
const JSZipModule = await import("jszip");
const JSZip = JSZipModule.default;
Expand Down
15 changes: 14 additions & 1 deletion extensions/cli/src/hubLoader.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AgentFile, parseAgentFile } from "@continuedev/config-yaml";
import JSZip from "jszip";

import { getAccessToken, loadAuthConfig } from "./auth/workos.js";
import { env } from "./env.js";
import { logger } from "./util/logger.js";

Expand Down Expand Up @@ -111,6 +112,8 @@ export const agentFileProcessor: HubPackageProcessor<AgentFile> = {

/**
* Generic hub package loader
* Automatically includes authentication headers when user is logged in,
* enabling access to private packages.
*/
export async function loadPackageFromHub<T>(
slug: string,
Expand Down Expand Up @@ -142,7 +145,17 @@ export async function loadPackageFromHub<T>(
}

try {
const response = await fetch(downloadUrl);
// Load auth config and get access token for private package access
const authConfig = loadAuthConfig();
const accessToken = getAccessToken(authConfig);

// Prepare headers with optional authorization
const headers: Record<string, string> = {};
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`;
}

const response = await fetch(downloadUrl, { headers });

if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
Expand Down
Loading