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
55 changes: 55 additions & 0 deletions packages/core/src/extension/marketplace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
loadMarketplaceConfigFromSource,
} from './marketplace.js';
import * as fs from 'node:fs/promises';
import * as http from 'node:http';
import * as https from 'node:https';

// Mock dependencies
Expand All @@ -27,6 +28,10 @@ vi.mock('node:https', () => ({
get: vi.fn(),
}));

vi.mock('node:http', () => ({
get: vi.fn(),
}));

vi.mock('./github.js', () => ({
parseGitHubRepoForReleases: vi.fn((url: string) => {
const match = url.match(/github\.com\/([^/]+)\/([^/]+)/);
Expand All @@ -51,6 +56,17 @@ describe('parseInstallSource', () => {
}
return { on: vi.fn() } as never;
});
vi.mocked(http.get).mockImplementation((_url, _options, callback) => {
const mockRes = {
statusCode: 404,
resume: vi.fn(),
on: vi.fn(),
};
if (typeof callback === 'function') {
callback(mockRes as never);
}
return { on: vi.fn(), setTimeout: vi.fn(), destroy: vi.fn() } as never;
});
});

afterEach(() => {
Expand Down Expand Up @@ -343,6 +359,45 @@ describe('parseInstallSource', () => {
});

describe('loadMarketplaceConfigFromSource', () => {
it('fetches direct HTTP marketplace JSON with the HTTP client', async () => {
vi.mocked(fs.stat).mockRejectedValueOnce(new Error('ENOENT'));
const cfg = {
name: 'http-marketplace',
owner: { name: 'Owner' },
plugins: [{ name: 'p1' }],
};
vi.mocked(http.get).mockImplementation((_url, _options, callback) => {
const mockRes = {
statusCode: 200,
resume: vi.fn(),
on: vi.fn((event, handler) => {
if (event === 'data') {
handler(Buffer.from(JSON.stringify(cfg)));
}
if (event === 'end') {
handler();
}
}),
};
if (typeof callback === 'function') {
callback(mockRes as never);
}
return { on: vi.fn(), setTimeout: vi.fn(), destroy: vi.fn() } as never;
});

const result = await loadMarketplaceConfigFromSource(
'http://example.com/marketplace.json',
);

expect(result).toEqual(cfg);
expect(http.get).toHaveBeenCalledWith(
'http://example.com/marketplace.json',
{ headers: { 'User-Agent': 'qwen-code' } },
expect.any(Function),
);
expect(https.get).not.toHaveBeenCalled();
});

it('resolves a marketplace from a git@ SSH source', async () => {
vi.mocked(fs.stat).mockRejectedValueOnce(new Error('ENOENT'));
const cfg = {
Expand Down
8 changes: 6 additions & 2 deletions packages/core/src/extension/marketplace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { ExtensionInstallMetadata } from '../config/config.js';
import type { ClaudeMarketplaceConfig } from './claude-converter.js';
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as http from 'node:http';
import * as https from 'node:https';
import { stat } from 'node:fs/promises';
import { parseGitHubRepoForReleases } from './github.js';
Expand Down Expand Up @@ -140,6 +141,9 @@ function fetchUrl(
url: string,
headers: Record<string, string>,
): Promise<string | null> {
const protocol = new URL(url).protocol;
const client = protocol === 'http:' ? http : https;

return new Promise((resolve) => {
let settled = false;
const done = (value: string | null) => {
Expand All @@ -155,7 +159,7 @@ function fetchUrl(
req.destroy();
done(null);
}, MARKETPLACE_FETCH_TIMEOUT_MS);
const req = https.get(url, { headers }, (res) => {
const req = client.get(url, { headers }, (res) => {
if (res.statusCode !== 200) {
res.resume(); // drain so the socket can be freed
done(null);
Expand Down Expand Up @@ -254,7 +258,7 @@ async function readLocalMarketplaceConfig(
* - Local directory containing `.claude-plugin/marketplace.json`
* - Local path directly to a `marketplace.json` file
* - `owner/repo`, `https://github.com/owner/repo`, `git@github.com:owner/repo.git`
* - Arbitrary `https://host/.../marketplace.json` returning the JSON document
* - Arbitrary `http(s)://host/.../marketplace.json` returning the JSON document
*
* Returns `null` when no marketplace config can be resolved.
*/
Expand Down
Loading