test-local-deployment.test.ts•19.3 kB
import { testLocalDeployment } from "../../src/tools/test-local-deployment.js";
import * as childProcess from "child_process";
import * as fs from "fs";
// Create simpler mocking approach
describe("testLocalDeployment", () => {
const testRepoPath = process.cwd();
afterEach(() => {
jest.restoreAllMocks();
});
describe("Input validation", () => {
it("should handle invalid SSG parameter", async () => {
await expect(
testLocalDeployment({
repositoryPath: "/test/path",
ssg: "invalid" as any,
}),
).rejects.toThrow();
});
it("should handle missing required parameters", async () => {
await expect(
testLocalDeployment({
ssg: "docusaurus",
} as any),
).rejects.toThrow();
});
it("should handle unsupported SSG gracefully", async () => {
// This should throw a ZodError due to input validation
await expect(
testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "gatsby" as any,
}),
).rejects.toThrow("Invalid enum value");
});
});
describe("Basic functionality", () => {
it("should return proper response structure", async () => {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "hugo",
skipBuild: true,
});
expect(result.content).toBeDefined();
expect(Array.isArray(result.content)).toBe(true);
expect(result.content.length).toBeGreaterThan(0);
expect(() => JSON.parse(result.content[0].text)).not.toThrow();
});
it("should use default port when not specified", async () => {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "hugo",
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.port).toBe(3000);
});
it("should use custom port when specified", async () => {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "hugo",
port: 4000,
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.port).toBe(4000);
});
it("should use custom timeout when specified", async () => {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "hugo",
timeout: 120,
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.buildSuccess).toBeDefined();
});
});
describe("SSG support", () => {
it("should handle all supported SSG types", async () => {
const ssgs = ["jekyll", "hugo", "docusaurus", "mkdocs", "eleventy"];
for (const ssg of ssgs) {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: ssg as any,
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.ssg).toBe(ssg);
expect(parsedResult.buildSuccess).toBeDefined();
}
});
it("should generate test script for all SSG types", async () => {
const ssgs = ["jekyll", "hugo", "docusaurus", "mkdocs", "eleventy"];
for (const ssg of ssgs) {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: ssg as any,
skipBuild: true,
port: 4000,
});
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.testScript).toContain(
`# Local Deployment Test Script for ${ssg}`,
);
expect(parsedResult.testScript).toContain("http://localhost:4000");
}
});
it("should include install commands for Node.js-based SSGs", async () => {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "docusaurus",
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.testScript).toContain("npm install");
});
it("should not include install commands for non-Node.js SSGs", async () => {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "hugo",
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.testScript).not.toContain("npm install");
});
});
describe("Configuration handling", () => {
it("should provide recommendations when configuration is missing", async () => {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "jekyll", // Jekyll config unlikely to exist in this repo
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.recommendations).toEqual(
expect.arrayContaining([
expect.stringContaining("Missing configuration file"),
]),
);
});
it("should provide next steps for missing configuration", async () => {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "jekyll",
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.nextSteps).toEqual(
expect.arrayContaining([expect.stringContaining("generate_config")]),
);
});
});
describe("Error handling", () => {
it("should handle general errors gracefully", async () => {
jest.spyOn(process, "chdir").mockImplementation(() => {
throw new Error("Permission denied");
});
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "hugo",
});
// The tool returns an error response structure instead of throwing
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.success).toBe(false);
expect(parsedResult.error.code).toBe("LOCAL_TEST_FAILED");
expect(parsedResult.error.message).toContain("Permission denied");
});
it("should handle non-existent repository path", async () => {
const result = await testLocalDeployment({
repositoryPath: "/non/existent/path",
ssg: "hugo",
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
// Should still work with skipBuild, but may have warnings
expect(parsedResult).toBeDefined();
expect(result.content).toBeDefined();
});
});
describe("Response structure validation", () => {
it("should include all required response fields", async () => {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "hugo",
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult).toHaveProperty("buildSuccess");
expect(parsedResult).toHaveProperty("ssg");
expect(parsedResult).toHaveProperty("port");
expect(parsedResult).toHaveProperty("testScript");
expect(parsedResult).toHaveProperty("recommendations");
expect(parsedResult).toHaveProperty("nextSteps");
});
it("should include tool recommendations in next steps", async () => {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "hugo",
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
expect(Array.isArray(parsedResult.nextSteps)).toBe(true);
expect(parsedResult.nextSteps.length).toBeGreaterThan(0);
});
it("should validate test script content structure", async () => {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "hugo",
port: 8080,
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
const testScript = parsedResult.testScript;
expect(testScript).toContain("# Local Deployment Test Script for hugo");
expect(testScript).toContain("http://localhost:8080");
expect(testScript).toContain("hugo server");
expect(testScript).toContain("--port 8080");
});
it("should handle different timeout values", async () => {
const timeouts = [30, 60, 120, 300];
for (const timeout of timeouts) {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "hugo",
timeout,
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.buildSuccess).toBeDefined();
// Timeout is not directly returned in response, but test should pass
}
});
it("should provide appropriate recommendations for each SSG type", async () => {
const ssgConfigs = {
jekyll: "_config.yml",
hugo: "config.toml",
docusaurus: "docusaurus.config.js",
mkdocs: "mkdocs.yml",
eleventy: ".eleventy.js",
};
for (const [ssg, configFile] of Object.entries(ssgConfigs)) {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: ssg as any,
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.recommendations).toEqual(
expect.arrayContaining([expect.stringContaining(configFile)]),
);
}
});
it("should include comprehensive next steps", async () => {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "jekyll", // Missing config will trigger recommendations
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
const nextSteps = parsedResult.nextSteps;
expect(Array.isArray(nextSteps)).toBe(true);
expect(nextSteps.length).toBeGreaterThan(0);
// Should include generate_config step for missing config
expect(nextSteps).toEqual(
expect.arrayContaining([expect.stringContaining("generate_config")]),
);
});
it("should handle edge case with empty repository path", async () => {
const result = await testLocalDeployment({
repositoryPath: "",
ssg: "hugo",
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
// Should handle gracefully and provide recommendations
expect(parsedResult).toBeDefined();
expect(result.content).toBeDefined();
});
it("should validate port range handling", async () => {
const ports = [1000, 3000, 8080, 9000, 65535];
for (const port of ports) {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "hugo",
port,
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.port).toBe(port);
expect(parsedResult.testScript).toContain(`http://localhost:${port}`);
}
});
});
describe("Advanced coverage scenarios", () => {
beforeEach(() => {
jest.spyOn(process, "chdir").mockImplementation(() => {});
});
afterEach(() => {
jest.restoreAllMocks();
});
describe("Configuration file scenarios", () => {
it("should detect existing configuration file for hugo", async () => {
// Mock fs.access to succeed for hugo config file
const mockFsAccess = jest
.spyOn(fs.promises, "access")
.mockResolvedValueOnce(undefined);
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "hugo",
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
// Should not recommend missing config since file exists
expect(parsedResult.recommendations).not.toEqual(
expect.arrayContaining([
expect.stringContaining("Missing configuration file"),
]),
);
mockFsAccess.mockRestore();
});
it("should detect existing configuration file for jekyll", async () => {
// Mock fs.access to succeed for jekyll config file
const mockFsAccess = jest
.spyOn(fs.promises, "access")
.mockResolvedValueOnce(undefined);
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "jekyll",
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
// Should not recommend missing config since file exists
expect(parsedResult.recommendations).not.toEqual(
expect.arrayContaining([
expect.stringContaining("Missing configuration file"),
]),
);
mockFsAccess.mockRestore();
});
it("should detect existing configuration file for docusaurus", async () => {
// Mock fs.access to succeed for docusaurus config file
const mockFsAccess = jest
.spyOn(fs.promises, "access")
.mockResolvedValueOnce(undefined);
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "docusaurus",
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
// Should not recommend missing config since file exists
expect(parsedResult.recommendations).not.toEqual(
expect.arrayContaining([
expect.stringContaining("Missing configuration file"),
]),
);
mockFsAccess.mockRestore();
});
it("should detect existing configuration file for mkdocs", async () => {
// Mock fs.access to succeed for mkdocs config file
const mockFsAccess = jest
.spyOn(fs.promises, "access")
.mockResolvedValueOnce(undefined);
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "mkdocs",
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
// Should not recommend missing config since file exists
expect(parsedResult.recommendations).not.toEqual(
expect.arrayContaining([
expect.stringContaining("Missing configuration file"),
]),
);
mockFsAccess.mockRestore();
});
it("should detect existing configuration file for eleventy", async () => {
// Mock fs.access to succeed for eleventy config file
const mockFsAccess = jest
.spyOn(fs.promises, "access")
.mockResolvedValueOnce(undefined);
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "eleventy",
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
// Should not recommend missing config since file exists
expect(parsedResult.recommendations).not.toEqual(
expect.arrayContaining([
expect.stringContaining("Missing configuration file"),
]),
);
mockFsAccess.mockRestore();
});
});
describe("Build scenarios with actual executions", () => {
it("should handle successful build for eleventy without skipBuild", async () => {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "eleventy",
skipBuild: false,
timeout: 10, // Short timeout to avoid long waits
});
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.ssg).toBe("eleventy");
expect(parsedResult.buildSuccess).toBeDefined();
expect(parsedResult.testScript).toContain("npx @11ty/eleventy");
});
it("should handle successful build for mkdocs without skipBuild", async () => {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "mkdocs",
skipBuild: false,
timeout: 10, // Short timeout to avoid long waits
});
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.ssg).toBe("mkdocs");
expect(parsedResult.buildSuccess).toBeDefined();
expect(parsedResult.testScript).toContain("mkdocs build");
});
it("should exercise server start paths with short timeout", async () => {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "hugo",
skipBuild: true,
timeout: 5, // Very short timeout to trigger timeout path
});
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.ssg).toBe("hugo");
expect(parsedResult.serverStarted).toBeDefined();
// localUrl may be undefined if server doesn't start quickly enough
expect(
typeof parsedResult.localUrl === "string" ||
parsedResult.localUrl === undefined,
).toBe(true);
});
it("should test port customization in serve commands", async () => {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "jekyll",
port: 4000,
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.testScript).toContain("--port 4000");
expect(parsedResult.testScript).toContain("http://localhost:4000");
});
it("should test mkdocs serve command with custom port", async () => {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "mkdocs",
port: 8000,
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.testScript).toContain("--dev-addr localhost:8000");
expect(parsedResult.testScript).toContain("http://localhost:8000");
});
it("should test eleventy serve command with custom port", async () => {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "eleventy",
port: 3001,
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.testScript).toContain("--port 3001");
expect(parsedResult.testScript).toContain("http://localhost:3001");
});
it("should provide correct next steps recommendations", async () => {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "docusaurus",
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.nextSteps).toBeDefined();
expect(Array.isArray(parsedResult.nextSteps)).toBe(true);
expect(parsedResult.nextSteps.length).toBeGreaterThan(0);
});
});
});
});