🚀 Hello World

This commit is contained in:
Felix Rieseberg 2025-06-26 08:38:59 -07:00
commit 0172c07623
No known key found for this signature in database
8 changed files with 11896 additions and 0 deletions

80
test/cli.test.ts Normal file
View file

@ -0,0 +1,80 @@
import { execSync } from "node:child_process";
import fs from "node:fs";
import { join } from "node:path";
interface ExecSyncError extends Error {
stdout: Buffer;
stderr: Buffer;
status: number | null;
signal: string | null;
}
describe("DXT CLI", () => {
const cliPath = join(__dirname, "../dist/cli/cli.js");
const validManifestPath = join(__dirname, "valid-manifest.json");
const invalidManifestPath = join(__dirname, "invalid-manifest.json");
beforeAll(() => {
// Ensure the CLI is built
execSync("yarn build", { cwd: join(__dirname, "..") });
});
it("should validate a valid manifest", () => {
const result = execSync(`node ${cliPath} validate ${validManifestPath}`, {
encoding: "utf-8",
});
expect(result).toContain("Manifest is valid!");
});
it("should reject an invalid manifest", () => {
expect(() => {
execSync(`node ${cliPath} validate ${invalidManifestPath}`, {
encoding: "utf-8",
});
}).toThrow();
});
it("should show usage when no arguments provided", () => {
try {
execSync(`node ${cliPath}`, { encoding: "utf-8", stdio: "pipe" });
} catch (error) {
// Commander outputs help to stderr when no command is provided
const execError = error as ExecSyncError;
expect(execError.stderr.toString()).toContain(
"Usage: dxt [options] [command]",
);
}
});
it("should handle non-existent files", () => {
try {
execSync(`node ${cliPath} validate /non/existent/file.json`, {
encoding: "utf-8",
stdio: "pipe",
});
fail("Should have thrown an error");
} catch (error) {
const execError = error as ExecSyncError;
expect(execError.stderr.toString()).toContain("ERROR: File not found");
}
});
it("should handle invalid JSON", () => {
const invalidJsonPath = join(__dirname, "invalid-json.json");
fs.writeFileSync(invalidJsonPath, "{ invalid json }");
try {
execSync(`node ${cliPath} validate ${invalidJsonPath}`, {
encoding: "utf-8",
stdio: "pipe",
});
fail("Should have thrown an error");
} catch (error) {
const execError = error as ExecSyncError;
expect(execError.stderr.toString()).toContain("ERROR: Invalid JSON");
}
// Clean up
fs.unlinkSync(invalidJsonPath);
});
});

483
test/config.test.ts Normal file
View file

@ -0,0 +1,483 @@
import {
getMcpConfigForManifest,
hasRequiredConfigMissing,
replaceVariables,
} from "../src/shared/config";
import type { DxtManifest, Logger } from "../src/types";
describe("replaceVariables", () => {
it("should replace variables in strings", () => {
const result = replaceVariables("Hello ${name}!", { name: "World" });
expect(result).toBe("Hello World!");
});
it("should replace multiple variables in strings", () => {
const result = replaceVariables("${greeting} ${name}!", {
greeting: "Hello",
name: "World",
});
expect(result).toBe("Hello World!");
});
it("should warn and not replace arrays in string context", () => {
const consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation();
const result = replaceVariables("Hello ${names}!", {
names: ["Alice", "Bob"],
});
expect(result).toBe("Hello ${names}!");
expect(consoleWarnSpy).toHaveBeenCalled();
consoleWarnSpy.mockRestore();
});
it("should replace variables in objects", () => {
const result = replaceVariables(
{ message: "Hello ${name}!" },
{ name: "World" },
);
expect(result).toEqual({ message: "Hello World!" });
});
it("should handle user config variables in arrays with string expansion", () => {
const result = replaceVariables(["${user_config.path}"], {
"user_config.path": "/some/path",
});
expect(result).toEqual(["/some/path"]);
});
it("should handle user config variables in arrays with array expansion", () => {
const result = replaceVariables(["${user_config.paths}"], {
"user_config.paths": ["/path1", "/path2"],
});
expect(result).toEqual(["/path1", "/path2"]);
});
it("should recursively process arrays", () => {
const result = replaceVariables(["Hello ${name}!", "Goodbye ${name}!"], {
name: "World",
});
expect(result).toEqual(["Hello World!", "Goodbye World!"]);
});
it("should handle mixed arrays with variable expansion", () => {
const result = replaceVariables(["start", "${user_config.items}", "end"], {
"user_config.items": ["item1", "item2"],
});
expect(result).toEqual(["start", "item1", "item2", "end"]);
});
it("should return primitive values unchanged", () => {
expect(replaceVariables(42, {})).toBe(42);
expect(replaceVariables(true, {})).toBe(true);
expect(replaceVariables(null, {})).toBe(null);
});
it("should handle undefined variables by keeping them as-is", () => {
const result = replaceVariables("Hello ${unknown}!", {});
expect(result).toBe("Hello ${unknown}!");
});
});
describe("getMcpConfigForManifest", () => {
const mockLogger: Logger = {
log: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
};
const mockSystemDirs = {
home: "/home/user",
data: "/data",
};
const baseManifest: DxtManifest = {
dxt_version: "1.0.0",
name: "test-extension",
version: "1.0.0",
description: "Test extension",
author: { name: "Test Author" },
server: {
type: "node",
entry_point: "server.js",
mcp_config: {
command: "node",
args: ["server.js", "${__dirname}"],
},
},
compatibility: { claude_desktop: "0.1.0" },
};
beforeEach(() => {
jest.clearAllMocks();
});
it("should return undefined when manifest has no mcp_config", async () => {
const manifest = {
...baseManifest,
server: undefined,
} as unknown as DxtManifest;
const result = await getMcpConfigForManifest({
manifest,
extensionPath: "/ext/path",
systemDirs: mockSystemDirs,
userConfig: {},
pathSeparator: "/",
logger: mockLogger,
});
expect(result).toBeUndefined();
});
it("should return undefined when required config is missing", async () => {
const manifest: DxtManifest = {
...baseManifest,
user_config: {
apiKey: {
type: "string",
title: "API Key",
description: "API Key",
required: true,
},
},
};
const result = await getMcpConfigForManifest({
manifest,
extensionPath: "/ext/path",
systemDirs: mockSystemDirs,
userConfig: {},
pathSeparator: "/",
logger: mockLogger,
});
expect(result).toBeUndefined();
expect(mockLogger.warn).toHaveBeenCalled();
});
it("should replace variables in basic config", async () => {
const result = await getMcpConfigForManifest({
manifest: baseManifest,
extensionPath: "/ext/path",
systemDirs: mockSystemDirs,
userConfig: {},
pathSeparator: "/",
logger: mockLogger,
});
expect(result).toEqual({
command: "node",
args: ["server.js", "/ext/path"],
});
});
it("should apply platform overrides", async () => {
const manifest: DxtManifest = {
...baseManifest,
server: {
type: "node",
entry_point: "server.js",
mcp_config: {
command: "default-command",
args: ["default"],
platform_overrides: {
[process.platform]: {
command: "platform-command",
args: ["platform-specific"],
},
},
},
},
};
const result = await getMcpConfigForManifest({
manifest,
extensionPath: "/ext/path",
systemDirs: mockSystemDirs,
userConfig: {},
pathSeparator: "/",
logger: mockLogger,
});
expect(result?.command).toBe("platform-command");
expect(result?.args).toEqual(["platform-specific"]);
});
it("should handle user config variable replacement with defaults", async () => {
const manifest: DxtManifest = {
...baseManifest,
user_config: {
port: {
type: "number",
title: "Port Number",
description: "Port number",
default: 8080,
},
},
server: {
type: "node",
entry_point: "server.js",
mcp_config: {
command: "node",
args: ["server.js", "--port=${user_config.port}"],
},
},
};
const result = await getMcpConfigForManifest({
manifest,
extensionPath: "/ext/path",
systemDirs: mockSystemDirs,
userConfig: {},
pathSeparator: "/",
logger: mockLogger,
});
expect(result?.args).toEqual(["server.js", "--port=8080"]);
});
it("should handle user config variable replacement with user values", async () => {
const manifest: DxtManifest = {
...baseManifest,
user_config: {
paths: {
type: "string",
title: "File Paths",
description: "File paths",
default: ["/default"],
multiple: true,
},
},
server: {
type: "node",
entry_point: "server.js",
mcp_config: {
command: "node",
args: ["server.js", "${user_config.paths}"],
},
},
};
const result = await getMcpConfigForManifest({
manifest,
extensionPath: "/ext/path",
systemDirs: mockSystemDirs,
userConfig: { paths: ["/user/path1", "/user/path2"] },
pathSeparator: "/",
logger: mockLogger,
});
expect(result?.args).toEqual(["server.js", "/user/path1", "/user/path2"]);
});
it("should convert boolean user config values to strings", async () => {
const manifest: DxtManifest = {
...baseManifest,
user_config: {
verbose: {
type: "boolean",
title: "Verbose Mode",
description: "Verbose output",
default: false,
},
},
server: {
type: "node",
entry_point: "server.js",
mcp_config: {
command: "node",
args: ["server.js", "--verbose=${user_config.verbose}"],
},
},
};
const result = await getMcpConfigForManifest({
manifest,
extensionPath: "/ext/path",
systemDirs: mockSystemDirs,
userConfig: { verbose: true },
pathSeparator: "/",
logger: mockLogger,
});
expect(result?.args).toEqual(["server.js", "--verbose=true"]);
});
});
describe("hasRequiredConfigMissing", () => {
const baseManifest: DxtManifest = {
dxt_version: "1.0.0",
name: "test-extension",
version: "1.0.0",
description: "Test extension",
author: { name: "Test Author" },
server: {
type: "node",
entry_point: "server.js",
mcp_config: {
command: "node",
args: ["server.js"],
},
},
};
it("should return false when manifest has no user_config", () => {
const result = hasRequiredConfigMissing({
manifest: baseManifest,
userConfig: {},
});
expect(result).toBe(false);
});
it("should return false when no config fields are required", () => {
const manifest: DxtManifest = {
...baseManifest,
user_config: {
port: {
type: "number",
title: "Port",
description: "Port number",
required: false,
},
},
};
const result = hasRequiredConfigMissing({
manifest,
userConfig: {},
});
expect(result).toBe(false);
});
it("should return false when required config is provided", () => {
const manifest: DxtManifest = {
...baseManifest,
user_config: {
apiKey: {
type: "string",
title: "API Key",
description: "API Key",
required: true,
},
},
};
const result = hasRequiredConfigMissing({
manifest,
userConfig: { apiKey: "test-key" },
});
expect(result).toBe(false);
});
it("should return true when required config is undefined", () => {
const manifest: DxtManifest = {
...baseManifest,
user_config: {
apiKey: {
type: "string",
title: "API Key",
description: "API Key",
required: true,
},
},
};
const result = hasRequiredConfigMissing({
manifest,
userConfig: {},
});
expect(result).toBe(true);
});
it("should return true when required config is empty string", () => {
const manifest: DxtManifest = {
...baseManifest,
user_config: {
apiKey: {
type: "string",
title: "API Key",
description: "API Key",
required: true,
},
},
};
const result = hasRequiredConfigMissing({
manifest,
userConfig: { apiKey: "" },
});
expect(result).toBe(true);
});
it("should return true when required config is empty array", () => {
const manifest: DxtManifest = {
...baseManifest,
user_config: {
paths: {
type: "string",
title: "Paths",
description: "File paths",
required: true,
multiple: true,
},
},
};
const result = hasRequiredConfigMissing({
manifest,
userConfig: { paths: [] },
});
expect(result).toBe(true);
});
it("should return false when no user config is provided but nothing is required", () => {
const result = hasRequiredConfigMissing({
manifest: baseManifest,
// userConfig not provided
});
expect(result).toBe(false);
});
it("should handle multiple required config fields", () => {
const manifest: DxtManifest = {
...baseManifest,
user_config: {
apiKey: {
type: "string",
title: "API Key",
description: "API Key",
required: true,
},
database: {
type: "string",
title: "Database",
description: "Database connection",
required: true,
},
port: {
type: "number",
title: "Port",
description: "Port number",
required: false,
},
},
};
const resultMissingBoth = hasRequiredConfigMissing({
manifest,
userConfig: {},
});
expect(resultMissingBoth).toBe(true);
const resultMissingOne = hasRequiredConfigMissing({
manifest,
userConfig: { apiKey: "test-key" },
});
expect(resultMissingOne).toBe(true);
const resultHasBoth = hasRequiredConfigMissing({
manifest,
userConfig: { apiKey: "test-key", database: "test-db" },
});
expect(resultHasBoth).toBe(false);
});
});

261
test/dxtignore.test.ts Normal file
View file

@ -0,0 +1,261 @@
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import {
getAllFiles,
readDxtIgnorePatterns,
shouldExclude,
} from "../src/node/files.js";
describe("DxtIgnore functionality", () => {
let tempDir: string;
let dxtIgnorePath: string;
beforeEach(() => {
// Create a temp directory for each test
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "dxt-test-"));
dxtIgnorePath = path.join(tempDir, ".dxtignore");
});
afterEach(() => {
// Clean up temp directory after each test
fs.rmSync(tempDir, { recursive: true, force: true });
});
describe("readDxtIgnorePatterns", () => {
it("should return empty array when .dxtignore doesn't exist", () => {
const patterns = readDxtIgnorePatterns(tempDir);
expect(patterns).toEqual([]);
});
it("should read patterns from .dxtignore file", () => {
const content = `*.log
node_modules/
temp/
.env`;
fs.writeFileSync(dxtIgnorePath, content);
const patterns = readDxtIgnorePatterns(tempDir);
expect(patterns).toEqual(["*.log", "node_modules/", "temp/", ".env"]);
});
it("should ignore empty lines and comments", () => {
const content = `# Comment line
*.log
# Another comment
node_modules/
.env`;
fs.writeFileSync(dxtIgnorePath, content);
const patterns = readDxtIgnorePatterns(tempDir);
expect(patterns).toEqual(["*.log", "node_modules/", ".env"]);
});
it("should trim whitespace from lines", () => {
const content = ` *.log
node_modules/
temp/`;
fs.writeFileSync(dxtIgnorePath, content);
const patterns = readDxtIgnorePatterns(tempDir);
expect(patterns).toEqual(["*.log", "node_modules/", "temp/"]);
});
it("should handle Windows line endings", () => {
const content = `*.log\r\nnode_modules/\r\ntemp/`;
fs.writeFileSync(dxtIgnorePath, content);
const patterns = readDxtIgnorePatterns(tempDir);
expect(patterns).toEqual(["*.log", "node_modules/", "temp/"]);
});
it("should return empty array if file cannot be read", () => {
const content = "*.log";
fs.writeFileSync(dxtIgnorePath, content);
fs.chmodSync(dxtIgnorePath, 0); // Make file unreadable
const patterns = readDxtIgnorePatterns(tempDir);
expect(patterns).toEqual([]);
// Restore permissions for cleanup
fs.chmodSync(dxtIgnorePath, 0o644);
});
});
describe("shouldExclude with additional patterns", () => {
it("should exclude files matching additional patterns", () => {
const additionalPatterns = ["*.test.js", "coverage/"];
expect(shouldExclude("test.test.js", additionalPatterns)).toBe(true);
expect(shouldExclude("coverage/file.txt", additionalPatterns)).toBe(true);
expect(shouldExclude("src/index.js", additionalPatterns)).toBe(false);
});
it("should still exclude default patterns", () => {
const additionalPatterns = ["*.test.js"];
// Test default pattern
expect(shouldExclude(".DS_Store", additionalPatterns)).toBe(true);
// Test additional pattern
expect(shouldExclude("test.test.js", additionalPatterns)).toBe(true);
// Test non-excluded file
expect(shouldExclude("src/index.js", additionalPatterns)).toBe(false);
});
it("should handle glob patterns correctly", () => {
const additionalPatterns = ["*.log", "temp/*"];
expect(shouldExclude("debug.log", additionalPatterns)).toBe(true);
expect(shouldExclude("temp/file.txt", additionalPatterns)).toBe(true);
expect(shouldExclude("src/main.js", additionalPatterns)).toBe(false);
});
it("should handle exact path matches", () => {
const additionalPatterns = ["node_modules", "dist"];
expect(shouldExclude("node_modules", additionalPatterns)).toBe(true);
expect(shouldExclude("dist", additionalPatterns)).toBe(true);
expect(shouldExclude("src/node_modules", additionalPatterns)).toBe(true);
expect(shouldExclude("src", additionalPatterns)).toBe(false);
});
});
describe("getAllFiles with .dxtignore", () => {
let testStructure: string;
beforeEach(() => {
// Create test directory structure
testStructure = path.join(tempDir, "test-extension");
fs.mkdirSync(testStructure);
fs.mkdirSync(path.join(testStructure, "src"));
fs.mkdirSync(path.join(testStructure, "tests"));
fs.mkdirSync(path.join(testStructure, "coverage"));
fs.mkdirSync(path.join(testStructure, "logs"));
// Create test files
fs.writeFileSync(path.join(testStructure, "manifest.json"), "{}");
fs.writeFileSync(
path.join(testStructure, "src/index.js"),
"console.log('hi')",
);
fs.writeFileSync(path.join(testStructure, "tests/test.js"), "test()");
fs.writeFileSync(
path.join(testStructure, "coverage/report.txt"),
"coverage",
);
fs.writeFileSync(path.join(testStructure, "logs/debug.log"), "log");
fs.writeFileSync(path.join(testStructure, "debug.log"), "log");
});
it("should exclude files matching .dxtignore patterns", () => {
// Create .dxtignore
const dxtIgnoreContent = `*.log
tests/
coverage/`;
fs.writeFileSync(
path.join(testStructure, ".dxtignore"),
dxtIgnoreContent,
);
const patterns = readDxtIgnorePatterns(testStructure);
const files = getAllFiles(testStructure, testStructure, {}, patterns);
const fileNames = Object.keys(files);
// Should include
expect(fileNames).toContain("manifest.json");
expect(fileNames).toContain("src/index.js");
// Should exclude
expect(fileNames).not.toContain("debug.log");
expect(fileNames).not.toContain("logs/debug.log");
expect(fileNames).not.toContain("tests/test.js");
expect(fileNames).not.toContain("coverage/report.txt");
expect(fileNames).not.toContain(".dxtignore");
});
it("should exclude both default patterns and .dxtignore patterns", () => {
// Create .dxtignore
const dxtIgnoreContent = `tests/`;
fs.writeFileSync(
path.join(testStructure, ".dxtignore"),
dxtIgnoreContent,
);
// Create a .git file (default exclusion)
fs.writeFileSync(path.join(testStructure, ".gitignore"), "ignore");
const patterns = readDxtIgnorePatterns(testStructure);
const files = getAllFiles(testStructure, testStructure, {}, patterns);
const fileNames = Object.keys(files);
// Should include
expect(fileNames).toContain("manifest.json");
expect(fileNames).toContain("src/index.js");
// Should exclude (from .dxtignore)
expect(fileNames).not.toContain("tests/test.js");
// Should exclude (from default patterns)
expect(fileNames).not.toContain(".gitignore");
});
it("should work without .dxtignore file", () => {
const files = getAllFiles(testStructure, testStructure, {}, []);
const fileNames = Object.keys(files);
// Should include all files except default exclusions
expect(fileNames).toContain("manifest.json");
expect(fileNames).toContain("src/index.js");
expect(fileNames).toContain("tests/test.js");
expect(fileNames).toContain("coverage/report.txt");
// Note: *.log files are excluded by default, so these won't be included
expect(fileNames).not.toContain("logs/debug.log");
expect(fileNames).not.toContain("debug.log");
});
});
describe("Pattern matching edge cases", () => {
it("should handle nested directories", () => {
const additionalPatterns = ["node_modules/"];
expect(
shouldExclude("node_modules/package/index.js", additionalPatterns),
).toBe(true);
expect(
shouldExclude("src/node_modules/package/index.js", additionalPatterns),
).toBe(true);
});
it("should handle paths with directory patterns", () => {
const additionalPatterns = ["tests/"];
expect(shouldExclude("tests/unit/test.js", additionalPatterns)).toBe(
true,
);
expect(
shouldExclude("tests/integration/test.js", additionalPatterns),
).toBe(true);
// Note: "tests" appears anywhere in path due to current implementation
expect(shouldExclude("src/tests/test.js", additionalPatterns)).toBe(true);
// But paths without "tests" should not be excluded
expect(shouldExclude("src/main.js", additionalPatterns)).toBe(false);
});
it("should match patterns (implementation details)", () => {
const additionalPatterns = ["specific-file.LOG"];
// Test that the pattern is matched
expect(shouldExclude("specific-file.LOG", additionalPatterns)).toBe(true);
// Test completely different file name
expect(shouldExclude("totally-different.txt", additionalPatterns)).toBe(
false,
);
});
});
});

363
test/init.test.ts Normal file
View file

@ -0,0 +1,363 @@
import { existsSync, readFileSync } from "fs";
import {
buildManifest,
createMcpConfig,
getDefaultAuthorEmail,
getDefaultAuthorName,
getDefaultAuthorUrl,
getDefaultEntryPoint,
getDefaultRepositoryUrl,
readPackageJson,
} from "../src/cli/init.js";
// Mock the fs module
jest.mock("fs", () => ({
existsSync: jest.fn(),
readFileSync: jest.fn(),
writeFileSync: jest.fn(),
}));
describe("init functions", () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe("readPackageJson", () => {
it("should return empty object when package.json doesn't exist", () => {
(existsSync as jest.Mock).mockReturnValue(false);
const result = readPackageJson("/test/path");
expect(result).toEqual({});
});
it("should parse package.json when it exists", () => {
(existsSync as jest.Mock).mockReturnValue(true);
const mockPackageData = { name: "test-package", version: "1.0.0" };
(readFileSync as jest.Mock).mockReturnValue(
JSON.stringify(mockPackageData),
);
const result = readPackageJson("/test/path");
expect(result).toEqual(mockPackageData);
});
it("should return empty object when package.json is invalid", () => {
(existsSync as jest.Mock).mockReturnValue(true);
(readFileSync as jest.Mock).mockReturnValue("invalid json");
const result = readPackageJson("/test/path");
expect(result).toEqual({});
});
});
describe("getDefaultAuthorName", () => {
it("should return string author when author is string", () => {
const result = getDefaultAuthorName({ author: "John Doe" });
expect(result).toBe("John Doe");
});
it("should return author.name when author is object", () => {
const result = getDefaultAuthorName({ author: { name: "John Doe" } });
expect(result).toBe("John Doe");
});
it("should return empty string when no author", () => {
const result = getDefaultAuthorName({});
expect(result).toBe("");
});
});
describe("getDefaultAuthorEmail", () => {
it("should return author.email when author is object with email", () => {
const result = getDefaultAuthorEmail({
author: { email: "john@example.com" },
});
expect(result).toBe("john@example.com");
});
it("should return empty string when author is string", () => {
const result = getDefaultAuthorEmail({ author: "John Doe" });
expect(result).toBe("");
});
it("should return empty string when no email", () => {
const result = getDefaultAuthorEmail({ author: { name: "John" } });
expect(result).toBe("");
});
});
describe("getDefaultAuthorUrl", () => {
it("should return author.url when author is object with url", () => {
const result = getDefaultAuthorUrl({
author: { url: "https://example.com" },
});
expect(result).toBe("https://example.com");
});
it("should return empty string when author is string", () => {
const result = getDefaultAuthorUrl({ author: "John Doe" });
expect(result).toBe("");
});
it("should return empty string when no url", () => {
const result = getDefaultAuthorUrl({ author: { name: "John" } });
expect(result).toBe("");
});
});
describe("getDefaultRepositoryUrl", () => {
it("should return string repository when repository is string", () => {
const result = getDefaultRepositoryUrl({
repository: "https://github.com/user/repo",
});
expect(result).toBe("https://github.com/user/repo");
});
it("should return repository.url when repository is object", () => {
const result = getDefaultRepositoryUrl({
repository: { type: "git", url: "https://github.com/user/repo" },
});
expect(result).toBe("https://github.com/user/repo");
});
it("should return empty string when no repository", () => {
const result = getDefaultRepositoryUrl({});
expect(result).toBe("");
});
});
describe("createMcpConfig", () => {
it("should create node config", () => {
const result = createMcpConfig("node", "server/index.js");
expect(result).toEqual({
command: "node",
args: ["${__dirname}/server/index.js"],
env: {},
});
});
it("should create python config", () => {
const result = createMcpConfig("python", "server/main.py");
expect(result).toEqual({
command: "python",
args: ["${__dirname}/server/main.py"],
env: {
PYTHONPATH: "server/lib",
},
});
});
it("should create binary config", () => {
const result = createMcpConfig("binary", "server/my-server");
expect(result).toEqual({
command: "${__dirname}/server/my-server",
args: [],
env: {},
});
});
});
describe("getDefaultEntryPoint", () => {
it("should return node default", () => {
expect(getDefaultEntryPoint("node")).toBe("server/index.js");
});
it("should return python default", () => {
expect(getDefaultEntryPoint("python")).toBe("server/main.py");
});
it("should return binary default", () => {
expect(getDefaultEntryPoint("binary")).toBe("server/my-server");
});
});
describe("buildManifest", () => {
it("should build minimal manifest", () => {
const manifest = buildManifest(
{
name: "test-extension",
displayName: "test-extension",
version: "1.0.0",
description: "Test description",
authorName: "John Doe",
},
undefined, // longDescription
{
authorEmail: "",
authorUrl: "",
},
{
homepage: "",
documentation: "",
support: "",
},
{
icon: "",
screenshots: [],
},
{
serverType: "node",
entryPoint: "server/index.js",
mcp_config: {
command: "node",
args: ["${__dirname}/server/index.js"],
env: {},
},
},
[], // tools
false, // toolsGenerated
[], // prompts
false, // promptsGenerated
undefined, // compatibility
{}, // userConfig
{
keywords: "",
license: "",
},
);
expect(manifest).toEqual({
dxt_version: "0.1",
name: "test-extension",
version: "1.0.0",
description: "Test description",
author: {
name: "John Doe",
},
server: {
type: "node",
entry_point: "server/index.js",
mcp_config: {
command: "node",
args: ["${__dirname}/server/index.js"],
env: {},
},
},
});
});
it("should include optional fields when provided", () => {
const manifest = buildManifest(
{
name: "test-extension",
displayName: "Test Extension",
version: "1.0.0",
description: "Test description",
authorName: "John Doe",
},
"This is a detailed long description with more information.", // longDescription
{
authorEmail: "john@example.com",
authorUrl: "https://example.com",
},
{
homepage: "https://homepage.example.com",
documentation: "https://docs.example.com",
support: "https://support.example.com",
},
{
icon: "icon.png",
screenshots: ["screen1.png", "screen2.png"],
},
{
serverType: "python",
entryPoint: "server/main.py",
mcp_config: {
command: "python",
args: ["server/main.py"],
env: { PYTHONPATH: "server/lib" },
},
},
[
{ name: "tool1", description: "Tool 1 description" },
{ name: "tool2" },
], // tools
false, // toolsGenerated
[
{
name: "analyze_code",
description: "Code analysis prompt",
text: "Analyze the following code...",
},
], // prompts
false, // promptsGenerated
{
claude_desktop: ">=0.4.0",
platforms: ["darwin", "linux"],
runtimes: { python: ">=3.8,<4.0" },
}, // compatibility
{
api_key: {
type: "string",
title: "API Key",
description: "Your API key",
required: true,
sensitive: true,
},
}, // userConfig
{
keywords: "test, extension, mcp",
license: "MIT",
repository: { type: "git", url: "https://github.com/user/repo" },
},
);
expect(manifest).toEqual({
dxt_version: "0.1",
name: "test-extension",
display_name: "Test Extension",
version: "1.0.0",
description: "Test description",
long_description:
"This is a detailed long description with more information.",
author: {
name: "John Doe",
email: "john@example.com",
url: "https://example.com",
},
homepage: "https://homepage.example.com",
documentation: "https://docs.example.com",
support: "https://support.example.com",
icon: "icon.png",
screenshots: ["screen1.png", "screen2.png"],
server: {
type: "python",
entry_point: "server/main.py",
mcp_config: {
command: "python",
args: ["server/main.py"],
env: { PYTHONPATH: "server/lib" },
},
},
tools: [
{ name: "tool1", description: "Tool 1 description" },
{ name: "tool2" },
],
prompts: [
{
name: "analyze_code",
description: "Code analysis prompt",
text: "Analyze the following code...",
},
],
compatibility: {
claude_desktop: ">=0.4.0",
platforms: ["darwin", "linux"],
runtimes: { python: ">=3.8,<4.0" },
},
user_config: {
api_key: {
type: "string",
title: "API Key",
description: "Your API key",
required: true,
sensitive: true,
},
},
keywords: ["test", "extension", "mcp"],
license: "MIT",
repository: { type: "git", url: "https://github.com/user/repo" },
});
});
});
});

View file

@ -0,0 +1,13 @@
{
"dxt_version": "1.0",
"name": "test-extension",
"version": "1.0.0",
"description": "A test DXT extension",
"author": {
"email": "not-a-valid-email"
},
"server": {
"type": "invalid-type",
"entry_point": "server/index.js"
}
}

150
test/schemas.test.ts Normal file
View file

@ -0,0 +1,150 @@
import { readFileSync } from "fs";
import { join } from "path";
import { DxtManifestSchema } from "../src/schemas";
describe("DxtManifestSchema", () => {
it("should validate a valid manifest", () => {
const manifestPath = join(__dirname, "valid-manifest.json");
const manifestContent = readFileSync(manifestPath, "utf-8");
const manifestData = JSON.parse(manifestContent);
const result = DxtManifestSchema.safeParse(manifestData);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.name).toBe("test-extension");
expect(result.data.server.type).toBe("node");
}
});
it("should reject an invalid manifest", () => {
const manifestPath = join(__dirname, "invalid-manifest.json");
const manifestContent = readFileSync(manifestPath, "utf-8");
const manifestData = JSON.parse(manifestContent);
const result = DxtManifestSchema.safeParse(manifestData);
expect(result.success).toBe(false);
if (!result.success) {
const errors = result.error.issues.map((issue) => issue.path.join("."));
expect(errors).toContain("author.name");
expect(errors).toContain("author.email");
expect(errors).toContain("server.type");
expect(errors).toContain("server.mcp_config");
}
});
it("should validate manifest with all optional fields", () => {
const fullManifest = {
dxt_version: "1.0",
name: "full-extension",
display_name: "Full Featured Extension",
version: "2.0.0",
description: "An extension with all features",
long_description: "This is a detailed description of the extension",
author: {
name: "Test Author",
email: "test@example.com",
url: "https://example.com",
},
repository: {
type: "git",
url: "https://github.com/example/extension",
},
homepage: "https://example.com/extension",
documentation: "https://docs.example.com",
support: "https://support.example.com",
icon: "icon.png",
screenshots: ["screenshot1.png", "screenshot2.png"],
server: {
type: "python",
entry_point: "main.py",
mcp_config: {
command: "python",
args: ["main.py"],
env: { PYTHONPATH: "." },
},
runtime_requirements: {
python: ">=3.8",
},
},
tools: [
{
name: "my_tool",
description: "A useful tool",
input_schema: { type: "object" },
},
],
keywords: ["test", "example"],
license: "MIT",
compatibility: {
claude_desktop: ">=1.0.0",
platforms: ["darwin", "win32"],
runtimes: {
python: ">=3.8",
node: ">=16.0.0",
},
},
settings: {
configurable: true,
schema: {
type: "object",
properties: {},
},
},
user_config: {
api_key: {
type: "string",
title: "API Key",
description: "Your API key",
required: true,
sensitive: true,
},
max_results: {
type: "number",
title: "Max Results",
description: "Maximum number of results",
default: 10,
min: 1,
max: 100,
},
},
};
const result = DxtManifestSchema.safeParse(fullManifest);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.display_name).toBe("Full Featured Extension");
expect(result.data.tools).toHaveLength(1);
expect(result.data.compatibility?.platforms).toContain("darwin");
expect(result.data.user_config?.api_key.type).toBe("string");
}
});
it("should validate server types correctly", () => {
const serverTypes = ["python", "node", "binary"];
serverTypes.forEach((type) => {
const manifest = {
dxt_version: "1.0",
name: "test",
version: "1.0.0",
description: "Test",
author: { name: "Test" },
server: {
type,
entry_point: "main",
mcp_config: {
command: type === "binary" ? "./main" : type,
args: ["main"],
},
},
};
const result = DxtManifestSchema.safeParse(manifest);
expect(result.success).toBe(true);
});
});
});

467
test/sign.e2e.test.ts Executable file
View file

@ -0,0 +1,467 @@
/**
* End-to-end test for DXT signing and verification
*
* This test:
* 1. Creates a test DXT file
* 2. Generates a self-signed certificate for testing
* 3. Signs the DXT file
* 4. Verifies the signature
* 5. Tests various failure scenarios
*/
import * as fs from "fs";
import forge from "node-forge";
import * as path from "path";
import { signDxtFile, unsignDxtFile, verifyDxtFile } from "../src/node/sign";
// Test directory
const TEST_DIR = path.join(__dirname, "test-output");
// Ensure test directory exists
if (!fs.existsSync(TEST_DIR)) {
fs.mkdirSync(TEST_DIR, { recursive: true });
}
// Test file paths
const TEST_DXT = path.join(TEST_DIR, "test.dxt");
const SELF_SIGNED_CERT = path.join(TEST_DIR, "self-signed.crt");
const SELF_SIGNED_KEY = path.join(TEST_DIR, "self-signed.key");
const CA_CERT = path.join(TEST_DIR, "ca.crt");
const CA_KEY = path.join(TEST_DIR, "ca.key");
const SIGNED_CERT = path.join(TEST_DIR, "signed.crt");
const SIGNED_KEY = path.join(TEST_DIR, "signed.key");
/**
* Generate a self-signed certificate for testing
*/
function generateSelfSignedCert() {
const keys = forge.pki.rsa.generateKeyPair(2048);
const cert = forge.pki.createCertificate();
cert.publicKey = keys.publicKey;
cert.serialNumber = "01";
cert.validity.notBefore = new Date();
cert.validity.notAfter = new Date();
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1);
const attrs = [
{ name: "commonName", value: "Test DXT Publisher" },
{ name: "countryName", value: "US" },
{ name: "organizationName", value: "Test Org" },
];
cert.setSubject(attrs);
cert.setIssuer(attrs);
// Add extensions
cert.setExtensions([
{
name: "basicConstraints",
cA: false,
},
{
name: "keyUsage",
keyCertSign: false,
digitalSignature: true,
nonRepudiation: true,
keyEncipherment: false,
dataEncipherment: false,
},
{
name: "extKeyUsage",
codeSigning: true,
},
]);
// Self-sign
cert.sign(keys.privateKey, forge.md.sha256.create());
// Write files
fs.writeFileSync(SELF_SIGNED_CERT, forge.pki.certificateToPem(cert));
fs.writeFileSync(SELF_SIGNED_KEY, forge.pki.privateKeyToPem(keys.privateKey));
}
/**
* Generate a CA and signed certificate for testing
*/
function generateCASignedCert() {
// Generate CA
const caKeys = forge.pki.rsa.generateKeyPair(2048);
const caCert = forge.pki.createCertificate();
caCert.publicKey = caKeys.publicKey;
caCert.serialNumber = "01";
caCert.validity.notBefore = new Date();
caCert.validity.notAfter = new Date();
caCert.validity.notAfter.setFullYear(
caCert.validity.notBefore.getFullYear() + 10,
);
const caAttrs = [
{ name: "commonName", value: "Test CA" },
{ name: "countryName", value: "US" },
{ name: "organizationName", value: "Test CA Org" },
];
caCert.setSubject(caAttrs);
caCert.setIssuer(caAttrs);
caCert.setExtensions([
{
name: "basicConstraints",
cA: true,
},
{
name: "keyUsage",
keyCertSign: true,
digitalSignature: true,
cRLSign: true,
},
]);
caCert.sign(caKeys.privateKey, forge.md.sha256.create());
// Generate signed certificate
const keys = forge.pki.rsa.generateKeyPair(2048);
const cert = forge.pki.createCertificate();
cert.publicKey = keys.publicKey;
cert.serialNumber = "02";
cert.validity.notBefore = new Date();
cert.validity.notAfter = new Date();
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1);
const attrs = [
{ name: "commonName", value: "Test Publisher (CA Signed)" },
{ name: "countryName", value: "US" },
{ name: "organizationName", value: "Test Publisher Org" },
];
cert.setSubject(attrs);
cert.setIssuer(caCert.subject.attributes);
cert.setExtensions([
{
name: "basicConstraints",
cA: false,
},
{
name: "keyUsage",
digitalSignature: true,
nonRepudiation: true,
},
{
name: "extKeyUsage",
codeSigning: true,
},
]);
// Sign with CA
cert.sign(caKeys.privateKey, forge.md.sha256.create());
// Write files
fs.writeFileSync(CA_CERT, forge.pki.certificateToPem(caCert));
fs.writeFileSync(CA_KEY, forge.pki.privateKeyToPem(caKeys.privateKey));
fs.writeFileSync(SIGNED_CERT, forge.pki.certificateToPem(cert));
fs.writeFileSync(SIGNED_KEY, forge.pki.privateKeyToPem(keys.privateKey));
}
/**
* Create a test DXT file (just a simple ZIP)
*/
function createTestDxt() {
// Create a simple ZIP file content
// This is a minimal valid ZIP with one file
const zipContent = Buffer.from([
0x50,
0x4b,
0x03,
0x04, // Local file header signature
0x14,
0x00, // Version needed
0x00,
0x00, // Flags
0x00,
0x00, // Compression method (stored)
0x00,
0x00, // Last mod time
0x00,
0x00, // Last mod date
0x00,
0x00,
0x00,
0x00, // CRC-32
0x0d,
0x00,
0x00,
0x00, // Compressed size
0x0d,
0x00,
0x00,
0x00, // Uncompressed size
0x0c,
0x00, // Filename length
0x00,
0x00, // Extra field length
// Filename: "manifest.json"
0x6d,
0x61,
0x6e,
0x69,
0x66,
0x65,
0x73,
0x74,
0x2e,
0x6a,
0x73,
0x6f,
0x6e,
// File content: '{"test":true}'
0x7b,
0x22,
0x74,
0x65,
0x73,
0x74,
0x22,
0x3a,
0x74,
0x72,
0x75,
0x65,
0x7d,
// Central directory
0x50,
0x4b,
0x01,
0x02, // Central directory signature
0x14,
0x00, // Version made by
0x14,
0x00, // Version needed
0x00,
0x00, // Flags
0x00,
0x00, // Compression method
0x00,
0x00, // Last mod time
0x00,
0x00, // Last mod date
0x00,
0x00,
0x00,
0x00, // CRC-32
0x0d,
0x00,
0x00,
0x00, // Compressed size
0x0d,
0x00,
0x00,
0x00, // Uncompressed size
0x0c,
0x00, // Filename length
0x00,
0x00, // Extra field length
0x00,
0x00, // File comment length
0x00,
0x00, // Disk number
0x00,
0x00, // Internal attributes
0x00,
0x00,
0x00,
0x00, // External attributes
0x00,
0x00,
0x00,
0x00, // Offset of local header
// Filename: "manifest.json"
0x6d,
0x61,
0x6e,
0x69,
0x66,
0x65,
0x73,
0x74,
0x2e,
0x6a,
0x73,
0x6f,
0x6e,
// End of central directory
0x50,
0x4b,
0x05,
0x06, // End of central directory signature
0x00,
0x00, // Disk number
0x00,
0x00, // Start disk
0x01,
0x00, // Number of central directory records on this disk
0x01,
0x00, // Total number of central directory records
0x3a,
0x00,
0x00,
0x00, // Size of central directory
0x34,
0x00,
0x00,
0x00, // Offset of central directory
0x00,
0x00, // Comment length
]);
fs.writeFileSync(TEST_DXT, zipContent);
}
/**
* Test signing with self-signed certificate
*/
async function testSelfSignedSigning() {
// Create a copy for this test
const testFile = path.join(TEST_DIR, "test-self-signed.dxt");
fs.copyFileSync(TEST_DXT, testFile);
// Sign the file
expect(() =>
signDxtFile(testFile, SELF_SIGNED_CERT, SELF_SIGNED_KEY),
).not.toThrow();
// Verify the signature
const result = await verifyDxtFile(testFile);
// Self-signed certs may not be trusted by OS, so we accept either status
expect(["self-signed", "unsigned"]).toContain(result.status);
// Clean up
fs.unlinkSync(testFile);
}
/**
* Test signing with CA-signed certificate
*/
async function testCASignedSigning() {
// Create a copy for this test
const testFile = path.join(TEST_DIR, "test-ca-signed.dxt");
fs.copyFileSync(TEST_DXT, testFile);
// Sign the file with intermediate
expect(() =>
signDxtFile(testFile, SIGNED_CERT, SIGNED_KEY, [CA_CERT]),
).not.toThrow();
// Verify the signature
const result = await verifyDxtFile(testFile);
// CA-signed status depends on whether test CA is trusted by OS
expect(["signed", "unsigned"]).toContain(result.status);
// Clean up
fs.unlinkSync(testFile);
}
/**
* Test tampering detection
*/
async function testTamperingDetection() {
// Create a copy and sign it
const testFile = path.join(TEST_DIR, "test-tampered.dxt");
fs.copyFileSync(TEST_DXT, testFile);
signDxtFile(testFile, SELF_SIGNED_CERT, SELF_SIGNED_KEY);
// Read the signed file
const signedContent = fs.readFileSync(testFile);
// Tamper with the content (change a byte in the ZIP portion)
const tamperedContent = Buffer.from(signedContent);
tamperedContent[10] = (tamperedContent[10] + 1) % 256;
fs.writeFileSync(testFile, tamperedContent);
// Try to verify - should fail
const result = await verifyDxtFile(testFile);
expect(result.status).toBe("unsigned");
// Clean up
fs.unlinkSync(testFile);
}
/**
* Test unsigned file verification
*/
async function testUnsignedFile() {
const result = await verifyDxtFile(TEST_DXT);
expect(result.status).toBe("unsigned");
}
/**
* Test signature removal
*/
async function testSignatureRemoval() {
// Create a copy and sign it
const testFile = path.join(TEST_DIR, "test-remove-sig.dxt");
fs.copyFileSync(TEST_DXT, testFile);
signDxtFile(testFile, SELF_SIGNED_CERT, SELF_SIGNED_KEY);
// Verify it's signed (or at least has signature data)
await verifyDxtFile(testFile);
// Remove signature
expect(() => unsignDxtFile(testFile)).not.toThrow();
// Verify it's unsigned
const afterResult = await verifyDxtFile(testFile);
expect(afterResult.status).toBe("unsigned");
// Clean up
fs.unlinkSync(testFile);
}
describe("DXT Signing E2E Tests", () => {
beforeAll(() => {
// Ensure test directory exists
if (!fs.existsSync(TEST_DIR)) {
fs.mkdirSync(TEST_DIR, { recursive: true });
}
// Setup
generateSelfSignedCert();
generateCASignedCert();
createTestDxt();
});
afterAll(() => {
// Cleanup
try {
fs.rmSync(TEST_DIR, { recursive: true, force: true });
} catch (e) {
// Ignore cleanup errors
}
});
it("should sign and verify with self-signed certificate", async () => {
await testSelfSignedSigning();
});
it("should sign and verify with CA-signed certificate", async () => {
await testCASignedSigning();
});
it("should detect tampering", async () => {
await testTamperingDetection();
});
it("should identify unsigned files", async () => {
await testUnsignedFile();
});
it("should remove signatures", async () => {
await testSignatureRemoval();
});
});

19
test/valid-manifest.json Normal file
View file

@ -0,0 +1,19 @@
{
"dxt_version": "1.0",
"name": "test-extension",
"display_name": "Test Extension",
"version": "1.0.0",
"description": "A test DXT extension",
"author": {
"name": "Test Author",
"email": "test@example.com"
},
"server": {
"type": "node",
"entry_point": "server/index.js",
"mcp_config": {
"command": "node",
"args": ["${__dirname}/server/index.js"]
}
}
}